diff --git a/.github/workflows/sync-asyncapi.yml b/.github/workflows/sync-asyncapi.yml index 27d45f50..78f2b969 100644 --- a/.github/workflows/sync-asyncapi.yml +++ b/.github/workflows/sync-asyncapi.yml @@ -62,7 +62,7 @@ jobs: - name: Check for changes id: changes run: | - if git diff --quiet -- packages/platform/asyncapi.json packages/platform/src/modules/builder.events.generated.ts; then + if git diff --quiet -- packages/platform/asyncapi.json packages/platform/src/client/modules/builder.events.generated.ts; then echo "changed=false" >> "$GITHUB_OUTPUT" echo "✅ asyncapi.json and the event types are up to date" else @@ -91,7 +91,7 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git checkout -B "$BRANCH" - git add packages/platform/asyncapi.json packages/platform/src/modules/builder.events.generated.ts + git add packages/platform/asyncapi.json packages/platform/src/client/modules/builder.events.generated.ts git commit -m "chore(platform): sync asyncapi.json and the event types from production" pr=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number') diff --git a/packages/platform/README.md b/packages/platform/README.md index e311d789..8f47f96c 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -124,7 +124,7 @@ redaction of generated prose or user content. ## Event types The socket protocol is defined by the service's AsyncAPI document, committed as -`asyncapi.json`. `src/modules/builder.events.generated.ts` is generated from it with +`asyncapi.json`. `src/client/modules/builder.events.generated.ts` is generated from it with `json-schema-to-typescript`: one type per schema, named after the backend model (`Message`, `StatusObject`, `QueueState`, one `*Call` per tool, one `*Approval` per guard), plus `ServerEventMap` and `ClientMessageMap`. Never edit it by hand. diff --git a/packages/platform/package.json b/packages/platform/package.json index 541238eb..175dc185 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -1,17 +1,28 @@ { "name": "@base44/platform", "version": "0.2.0", - "description": "Browser client for building on the Base44 platform", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "description": "Client and server SDK for building on the Base44 platform", + "main": "dist/client/index.js", + "types": "dist/client/index.d.ts", "type": "module", "exports": { ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" + "types": "./dist/client/index.d.ts", + "default": "./dist/client/index.js" + }, + "./server": { + "types": "./dist/server/index.d.ts", + "default": "./dist/server/index.js" }, "./package.json": "./package.json" }, + "typesVersions": { + "*": { + "server": [ + "dist/server/index.d.ts" + ] + } + }, "files": [ "dist" ], diff --git a/packages/platform/scripts/gen-events.mjs b/packages/platform/scripts/gen-events.mjs index 9fc4fa31..c9817795 100644 --- a/packages/platform/scripts/gen-events.mjs +++ b/packages/platform/scripts/gen-events.mjs @@ -1,10 +1,10 @@ #!/usr/bin/env node -// Generates src/modules/builder.events.generated.ts from asyncapi.json. `--check` fails on drift instead. +// Generates src/client/modules/builder.events.generated.ts from asyncapi.json. `--check` fails on drift instead. import { readFile, writeFile } from "node:fs/promises"; import { compile } from "json-schema-to-typescript"; const source = new URL("../asyncapi.json", import.meta.url); -const target = new URL("../src/modules/builder.events.generated.ts", import.meta.url); +const target = new URL("../src/client/modules/builder.events.generated.ts", import.meta.url); const doc = JSON.parse(await readFile(source, "utf8")); const schemas = doc.components.schemas; const refName = (ref) => ref.split("/").pop(); diff --git a/packages/platform/src/client.ts b/packages/platform/src/client/client.ts similarity index 100% rename from packages/platform/src/client.ts rename to packages/platform/src/client/client.ts diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client/client.types.ts similarity index 100% rename from packages/platform/src/client.types.ts rename to packages/platform/src/client/client.types.ts diff --git a/packages/platform/src/errors.ts b/packages/platform/src/client/errors.ts similarity index 100% rename from packages/platform/src/errors.ts rename to packages/platform/src/client/errors.ts diff --git a/packages/platform/src/errors.types.ts b/packages/platform/src/client/errors.types.ts similarity index 100% rename from packages/platform/src/errors.types.ts rename to packages/platform/src/client/errors.types.ts diff --git a/packages/platform/src/index.ts b/packages/platform/src/client/index.ts similarity index 100% rename from packages/platform/src/index.ts rename to packages/platform/src/client/index.ts diff --git a/packages/platform/src/modules/builder-protocol.ts b/packages/platform/src/client/modules/builder-protocol.ts similarity index 100% rename from packages/platform/src/modules/builder-protocol.ts rename to packages/platform/src/client/modules/builder-protocol.ts diff --git a/packages/platform/src/modules/builder-socket.ts b/packages/platform/src/client/modules/builder-socket.ts similarity index 100% rename from packages/platform/src/modules/builder-socket.ts rename to packages/platform/src/client/modules/builder-socket.ts diff --git a/packages/platform/src/modules/builder-subscription.ts b/packages/platform/src/client/modules/builder-subscription.ts similarity index 100% rename from packages/platform/src/modules/builder-subscription.ts rename to packages/platform/src/client/modules/builder-subscription.ts diff --git a/packages/platform/src/modules/builder.events.generated.ts b/packages/platform/src/client/modules/builder.events.generated.ts similarity index 100% rename from packages/platform/src/modules/builder.events.generated.ts rename to packages/platform/src/client/modules/builder.events.generated.ts diff --git a/packages/platform/src/modules/builder.events.types.ts b/packages/platform/src/client/modules/builder.events.types.ts similarity index 100% rename from packages/platform/src/modules/builder.events.types.ts rename to packages/platform/src/client/modules/builder.events.types.ts diff --git a/packages/platform/src/modules/builder.ts b/packages/platform/src/client/modules/builder.ts similarity index 100% rename from packages/platform/src/modules/builder.ts rename to packages/platform/src/client/modules/builder.ts diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/client/modules/builder.types.ts similarity index 100% rename from packages/platform/src/modules/builder.types.ts rename to packages/platform/src/client/modules/builder.types.ts diff --git a/packages/platform/src/server/index.ts b/packages/platform/src/server/index.ts new file mode 100644 index 00000000..c2d81d08 --- /dev/null +++ b/packages/platform/src/server/index.ts @@ -0,0 +1,3 @@ +/** Server platform modules, authenticated with a workspace API key. */ +export { createPlatformServer } from "./server.js"; +export type { PlatformServer, PlatformServerOptions } from "./server.types.js"; diff --git a/packages/platform/src/server/server-url.ts b/packages/platform/src/server/server-url.ts new file mode 100644 index 00000000..99424bba --- /dev/null +++ b/packages/platform/src/server/server-url.ts @@ -0,0 +1,8 @@ +/** Validate a platform origin and return it normalized. Used by the server entrypoint. */ +export function normalizeServerUrl(serverUrl: string): string { + const url = new URL(serverUrl); + if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") { + throw new TypeError("serverUrl must be an HTTP(S) origin without credentials, path, query or fragment"); + } + return url.origin; +} diff --git a/packages/platform/src/server/server.ts b/packages/platform/src/server/server.ts new file mode 100644 index 00000000..559f846e --- /dev/null +++ b/packages/platform/src/server/server.ts @@ -0,0 +1,9 @@ +import { normalizeServerUrl } from "./server-url.js"; +import type { PlatformServer, PlatformServerOptions } from "./server.types.js"; + +/** Create a server platform client, authenticated with a workspace API key. Performs no network requests. */ +export function createPlatformServer(options: PlatformServerOptions): PlatformServer { + if (!options.apiKey) throw new TypeError("apiKey is required"); + normalizeServerUrl(options.serverUrl); + return Object.freeze({}); +} diff --git a/packages/platform/src/server/server.types.ts b/packages/platform/src/server/server.types.ts new file mode 100644 index 00000000..861e7d49 --- /dev/null +++ b/packages/platform/src/server/server.types.ts @@ -0,0 +1,10 @@ +/** Shared configuration for server platform modules. */ +export interface PlatformServerOptions { + /** Origin of the platform service, e.g. https://base44.app. No path/query/credentials. */ + serverUrl: string; + /** Workspace API key. Keep it on your server; never ship it to a browser. */ + apiKey: string; +} + +/** Server platform client, returned by `createPlatformServer`. Modules are added as properties. */ +export interface PlatformServer {} diff --git a/packages/platform/tests/package.test.mjs b/packages/platform/tests/package.test.mjs index 9bfb5ed6..346dabe2 100644 --- a/packages/platform/tests/package.test.mjs +++ b/packages/platform/tests/package.test.mjs @@ -26,7 +26,7 @@ for (const name of Object.keys(manifest.dependencies)) { test("the tarball contains the compiled entry point and no source or test trees", () => { const files = new Set(packed.files.map(file => file.path)); - for (const file of ["dist/index.js", "dist/index.d.ts", "README.md"]) assert.ok(files.has(file), file); + for (const file of ["dist/client/index.js", "dist/client/index.d.ts", "dist/server/index.js", "dist/server/index.d.ts", "README.md"]) assert.ok(files.has(file), file); assert.equal([...files].some(file => /^(src|tests|examples)\//.test(file)), false); }); @@ -41,7 +41,20 @@ test("the client resolves from the installed package without the runtime SDK", ( `]); }); +test("the server entrypoint resolves from the installed package", () => { + run(process.execPath, ["--input-type=module", "--eval", ` + import assert from "node:assert/strict"; + import { createPlatformServer } from "@base44/platform/server"; + assert.ok(Object.isFrozen(createPlatformServer({ serverUrl: "https://example.test", apiKey: "key" }))); + assert.throws(() => createPlatformServer({ serverUrl: "https://example.test/path", apiKey: "key" }), TypeError); + `]); +}); + const consumer = ` +import { createPlatformServer, type PlatformServer } from "@base44/platform/server"; +const server: PlatformServer = createPlatformServer({ serverUrl: "https://example.test", apiKey: "key" }); +void server; + import { createPlatformClient, type PlatformEvent } from "@base44/platform"; const platform = createPlatformClient(); platform.builder.init({ getSession: async () => (await fetch("/session")).json(), onError() {} }).subscribe("a".repeat(24), { diff --git a/packages/platform/tests/unit/client.test.ts b/packages/platform/tests/unit/client.test.ts index 6e936cc4..6d884f6f 100644 --- a/packages/platform/tests/unit/client.test.ts +++ b/packages/platform/tests/unit/client.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { createPlatformClient, PlatformSocketError, type BuilderSession } from "../../src/index.js"; +import { createPlatformClient, PlatformSocketError, type BuilderSession } from "../../src/client/index.js"; const fake = vi.hoisted(() => { const handlers: Record void> = {}; diff --git a/packages/platform/tests/unit/server.test.ts b/packages/platform/tests/unit/server.test.ts new file mode 100644 index 00000000..91c6f19e --- /dev/null +++ b/packages/platform/tests/unit/server.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "vitest"; +import { createPlatformServer } from "../../src/server/index.js"; + +describe("createPlatformServer", () => { + test("creates a frozen client without side effects", () => { + expect(Object.isFrozen(createPlatformServer({ serverUrl: "https://api.example.test", apiKey: "key" }))).toBe(true); + }); + + test("rejects a serverUrl that is not a bare HTTP(S) origin", () => { + for (const serverUrl of ["ftp://example.test", "https://example.test/path", "https://u:p@example.test", "https://example.test?x=1"]) { + expect(() => createPlatformServer({ serverUrl, apiKey: "key" })).toThrow(TypeError); + } + }); + + test("requires an apiKey", () => { + expect(() => createPlatformServer({ serverUrl: "https://example.test", apiKey: "" })).toThrow(TypeError); + }); +}); diff --git a/packages/platform/tests/unit/socket.test.js b/packages/platform/tests/unit/socket.test.js index e98bfe11..b6b5accc 100644 --- a/packages/platform/tests/unit/socket.test.js +++ b/packages/platform/tests/unit/socket.test.js @@ -1,7 +1,7 @@ import { createServer } from "node:http"; import { WebSocketServer } from "ws"; import { expect, test, vi } from "vitest"; -import { createPlatformClient } from "../../src/index.js"; +import { createPlatformClient } from "../../src/client/index.js"; // Minimal Engine.IO/Socket.IO peer exercises the real client without a new server dependency. test("real Socket.IO handshake, join, snapshot, reconnect and session renewal", async () => { diff --git a/packages/platform/tsconfig.type-tests.json b/packages/platform/tsconfig.type-tests.json index c4630f01..163e9b78 100644 --- a/packages/platform/tsconfig.type-tests.json +++ b/packages/platform/tsconfig.type-tests.json @@ -5,7 +5,8 @@ "rootDir": ".", "baseUrl": ".", "paths": { - "@base44/platform": ["src/index.ts"] + "@base44/platform": ["src/client/index.ts"], + "@base44/platform/server": ["src/server/index.ts"] } }, "include": ["src/**/*.ts", "tests/types/**/*.ts", "examples/**/*.ts"] diff --git a/packages/platform/typedoc.json b/packages/platform/typedoc.json index 3e29c0b0..d3206d71 100644 --- a/packages/platform/typedoc.json +++ b/packages/platform/typedoc.json @@ -1,7 +1,8 @@ { "$schema": "https://typedoc.org/schema.json", "entryPoints": [ - "./src/index.ts" + "./src/client/index.ts", + "./src/server/index.ts" ], "tsconfig": "./tsconfig.json", "out": "docs", diff --git a/packages/platform/typedoc.mintlify.json b/packages/platform/typedoc.mintlify.json index f317419a..d295b487 100644 --- a/packages/platform/typedoc.mintlify.json +++ b/packages/platform/typedoc.mintlify.json @@ -1,6 +1,7 @@ { "$schema": "https://typedoc.org/schema.json", "extends": ["./typedoc.json"], + "entryPoints": ["./src/client/index.ts"], "out": "docs/content", "plugin": [ "typedoc-plugin-markdown",