Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/sync-asyncapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion packages/platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 16 additions & 5 deletions packages/platform/package.json
Original file line number Diff line number Diff line change
@@ -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"
],
Expand Down
4 changes: 2 additions & 2 deletions packages/platform/scripts/gen-events.mjs
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions packages/platform/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -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";
8 changes: 8 additions & 0 deletions packages/platform/src/server/server-url.ts
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 9 additions & 0 deletions packages/platform/src/server/server.ts
Original file line number Diff line number Diff line change
@@ -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({});
}
10 changes: 10 additions & 0 deletions packages/platform/src/server/server.types.ts
Original file line number Diff line number Diff line change
@@ -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 {}
15 changes: 14 additions & 1 deletion packages/platform/tests/package.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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), {
Expand Down
2 changes: 1 addition & 1 deletion packages/platform/tests/unit/client.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, (...args: any[]) => void> = {};
Expand Down
18 changes: 18 additions & 0 deletions packages/platform/tests/unit/server.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
2 changes: 1 addition & 1 deletion packages/platform/tests/unit/socket.test.js
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/platform/tsconfig.type-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
3 changes: 2 additions & 1 deletion packages/platform/typedoc.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/platform/typedoc.mintlify.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading