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
6 changes: 3 additions & 3 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/client/modules/builder.events.generated.ts; then
if git diff --quiet -- packages/platform/asyncapi.json packages/platform/src/client/modules/chat.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/client/modules/builder.events.generated.ts
git add packages/platform/asyncapi.json packages/platform/src/client/modules/chat.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 All @@ -103,7 +103,7 @@ jobs:
# Rebuilt from main every run, so the branch never drifts or conflicts.
git push --force origin "$BRANCH"

body="Refreshes \`packages/platform/asyncapi.json\` from $ASYNCAPI_URL and regenerates \`builder.events.generated.ts\`. The docs build and the Platform SDK tests passed on this change. Checked against production every hour."
body="Refreshes \`packages/platform/asyncapi.json\` from $ASYNCAPI_URL and regenerates \`chat.events.generated.ts\`. The docs build and the Platform SDK tests passed on this change. Checked against production every hour."
if [ -z "$pr" ]; then
gh pr create --head "$BRANCH" --base main --title "Sync Platform SDK events from production" --body "$body"
fi
25 changes: 25 additions & 0 deletions packages/platform/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## Unreleased

**Breaking.** Base44 calls it the AI chat, so the client and the webhook events say `chat`, not
`builder`.

### Client

| Before | After |
| --- | --- |
| `client.builder` | `client.chat` |
| `BuilderModule` | `ChatModule` |
| `BuilderSession` | `ChatSession` |
| `BuilderInitOptions` | `ChatInitOptions` |

```ts
const chat = client.chat.init({ getSession, onError });
```

### Webhooks

`chat.turn_started.v1` and `chat.turn_ended.v1` replace `builder.turn_started.v1` and
`builder.turn_ended.v1`, with the same payloads. Their types are `ChatTurnStartedV1Data` and
`ChatTurnEndedV1Data`. Base44 no longer sends the old names, so an endpoint that selected them
must select `chat.turn_*` instead.

## 0.2.0

**Breaking.** The client, its options and the event type names change, and events the SDK doesn't
Expand Down
56 changes: 28 additions & 28 deletions packages/platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ npm install @base44/platform
The Platform SDK for white-label platforms built on Base44. It is independent of the app SDK
(`@base44/sdk`) and has two entrypoints:

- `@base44/platform`, for the browser: implements the read-only white-label builder socket
contract. A partner's browser watches the builder chat of up to eight apps over the platform's
- `@base44/platform`, for the browser: implements the read-only white-label live AI chat
contract. A partner's browser watches the AI chat of up to eight apps over the platform's
shared socket. The workspace must have white-label sockets enabled; this SDK does not open sessions.
- `@base44/platform/server`, for your backend: verify the webhooks Base44 sends when apps are
created, published, unpublished, trashed or restored. See
Expand All @@ -18,24 +18,24 @@ The Platform SDK for white-label platforms built on Base44. It is independent of
import { createPlatformClient } from "@base44/platform";

const client = createPlatformClient();
const builder = client.builder.init({
const chat = client.chat.init({
// Returns the body of POST /api/service/socket-sessions, unchanged.
async getSession() {
const response = await fetch("/api/builder-socket-session", { method: "POST" });
const response = await fetch("/api/chat-socket-session", { method: "POST" });
if (!response.ok) throw new Error("Session request failed");
return response.json();
},
onError(error) { showConnectionError(error.code); },
});
const subscription = builder.subscribe(appId, {
const subscription = chat.subscribe(appId, {
onSnapshot(snapshot) { replaceAppState(snapshot); },
async onEvent(event) { await applyEvent(event); }, // awaited before the next event
onError(error) { showSubscriptionError(error.code); },
});
await builder.connect();
await chat.connect();
// On view teardown:
subscription.unsubscribe();
builder.close();
chat.close();
```

## Receiving webhooks (server)
Expand Down Expand Up @@ -97,7 +97,7 @@ Delivery is at least once with no ordering guarantee: deduplicate on `event.id`,

## Sessions

`/api/builder-socket-session` is your backend's route, not an SDK endpoint. It opens a
`/api/chat-socket-session` is your backend's route, not an SDK endpoint. It opens a
socket session with its workspace key (`apps:watch` scope) through
`POST /api/service/socket-sessions` with `{ app_ids }`, and returns the response body
to the browser. `getSession` returns that body unchanged: the SDK reads `session_token`,
Expand All @@ -107,7 +107,7 @@ backend adds or removes apps with `PUT`/`DELETE /api/service/socket-sessions/{se

A session lasts one hour and is never extended. The token goes exclusively in
CONNECT `auth.session_token`, never a URL, and the SDK keeps it in memory only. It
is reused across automatic reconnects. `getSession` is called when a builder
is reused across automatic reconnects. `getSession` is called when a chat
session first connects, and again only when the server rejects the cached token or
expires the session; the SDK then reconnects once with the new session. If the new
session has a different `socket_url` or `socket_path`, the SDK closes the old socket,
Expand All @@ -123,26 +123,26 @@ older one, which stops with `session_replaced`. Open one session per page.

`createPlatformClient()` creates a lightweight module container: no Socket.IO
instance, timers, session request or network activity. It takes no options. Its
`builder` module follows the server SDK's module-factory pattern.
`chat` module follows the server SDK's module-factory pattern.

`client.builder.init({ getSession, onError })` synchronously creates an independent
`BuilderSession` without connecting or creating a socket. Repeated calls create separate sessions, each owning its own
`client.chat.init({ getSession, onError })` synchronously creates an independent
`ChatSession` without connecting or creating a socket. Repeated calls create separate sessions, each owning its own
socket, listeners, subscriptions and cleanup. Close the returned session when its
view is disposed; the root client and other sessions remain usable.

`builder.connect()` fetches the first session, creates the socket at its address and
`chat.connect()` fetches the first session, creates the socket at its address and
resolves on CONNECT, before any snapshot. The socket uses the default namespace on the
session's `socket_path`, WebSocket only, with a dedicated manager.
Transport reconnection uses five attempts, starting at one second and capped at ten
seconds with jitter; each attempt has a twenty-second timeout. A temporary server
refusal is retried the same way. After exhaustion, a refusal or `session_replaced`,
fix the cause and call `builder.connect()`.
fix the cause and call `chat.connect()`.

`builder.subscribe(appId, options)` returns a handle with `appId`, `active` and
`chat.subscribe(appId, options)` returns a handle with `appId`, `active` and
idempotent `unsubscribe()`. App IDs are 24 lowercase hexadecimal characters. One
subscription per app and at most eight subscriptions are allowed per builder session.
Subscriptions can be created before connecting. `builder.close()` is terminal for that
session; call `client.builder.init(...)` again to start another.
subscription per app and at most eight subscriptions are allowed per chat session.
Subscriptions can be created before connecting. `chat.close()` is terminal for that
session; call `client.chat.init(...)` again to start another.

## Snapshots and events

Expand All @@ -161,15 +161,15 @@ their exceptions are isolated so they cannot interrupt another app's delivery.

## Public shapes

- `PlatformClient` (`client.types.ts`): the `builder` module.
- `BuilderModule` (`modules/builder.types.ts`): lazy `init(options)` factory.
- `BuilderInitOptions`: `getSession` and the connection-level `onError` observer.
- `PlatformClient` (`client.types.ts`): the `chat` module.
- `ChatModule` (`modules/chat.types.ts`): lazy `init(options)` factory.
- `ChatInitOptions`: `getSession` and the connection-level `onError` observer.
- `SocketSession`: the `session_token`, `socket_url` and `socket_path` that `getSession` returns.
- `BuilderSession`: `connect()`, `subscribe(appId, options)`, `close()`.
- `ChatSession`: `connect()`, `subscribe(appId, options)`, `close()`.
- `SubscriptionOptions`: required `onSnapshot`, `onEvent` and `onError`.
- `PlatformSubscription`: read-only `appId`, `active`, and `unsubscribe()`.
- `PlatformEvent`, `PlatformEventMap`, `PlatformSnapshot`, `ToolCall`, `GuardApproval`
(`modules/builder.events.types.ts`), derived from the generated event types.
(`modules/chat.events.types.ts`), derived from the generated event types.
- Error codes in `errors.types.ts`.

`PlatformEvent` is a **discriminated union**: switch on `event.type` to narrow
Expand All @@ -186,7 +186,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/client/modules/builder.events.generated.ts` is generated from it with
`asyncapi.json`. `src/client/modules/chat.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 Expand Up @@ -229,16 +229,16 @@ exceptions, payloads and credentials are never attached.
| `delivery_overflow` | 1,000 deliveries queued for the app; subscribe again for a fresh snapshot. |
| `protocol_error` | Invalid routing or envelope. Stop and investigate. |
| `handler_failed` | An application callback failed; fix it and subscribe again. |
| `client_closed` | The builder session was explicitly closed; initialize another session. |
| `client_closed` | The chat session was explicitly closed; initialize another session. |

No SDK dependency, package version or lockfile changes are required. Socket.IO
remains the existing SDK dependency. Chat commands and HTTP APIs are outside this entry point.

## React

`@base44/platform/react` adds one hook, `useBase44Chat`, that runs a builder chat for one
`@base44/platform/react` adds one hook, `useBase44Chat`, that runs the AI chat for one
app on top of this client: it opens the live session and renews it, turns messages into items
made for rendering, binds every question the builder asks to the step it belongs to. It renders nothing. React is an optional peer dependency, loaded only by this
made for rendering, binds every question the AI asks to the step it belongs to. It renders nothing. React is an optional peer dependency, loaded only by this
entry; the root entry stays framework-free.

```tsx
Expand All @@ -258,5 +258,5 @@ call with your credentials, so the browser never holds a key. The hook returns `
the first, and the chat watches it while `appId` is `null`. A sent prompt shows at once as a
`pending` item until Base44's own message arrives; a prompt sent mid-build waits in Base44's
queue meanwhile. A question is a union on `kind` (`choice`, `secrets`, `approval`) and carries only the
actions its kind allows, and declining is always one of them. As in Base44's builder, only the
actions its kind allows, and declining is always one of them. As in Base44's app editor, only the
last assistant message makes the chat wait; a kind this release doesn't know is declined for you. Every shape has JSDoc in `src/react/chat.types.ts`.
10 changes: 5 additions & 5 deletions packages/platform/examples/client.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { createPlatformClient, type PlatformEvent } from "@base44/platform";

const client = createPlatformClient();
const builder = client.builder.init({
const chat = client.chat.init({
// Your backend opens the session with its workspace key and returns the Create socket session body.
async getSession() {
const response = await fetch("/api/builder-socket-session", { method: "POST" });
const response = await fetch("/api/chat-socket-session", { method: "POST" });
if (!response.ok) throw new Error("Unable to open a socket session");
return response.json();
},
onError(error) { console.error(error.code); },
});

const subscription = builder.subscribe("0123456789abcdef01234567", {
const subscription = chat.subscribe("0123456789abcdef01234567", {
onSnapshot(snapshot) {
// Replace this app's state; merge messages by id to keep history older than the last 50.
console.log(snapshot.status?.state, snapshot.messages.length);
Expand All @@ -27,7 +27,7 @@ const subscription = builder.subscribe("0123456789abcdef01234567", {
},
});

await builder.connect();
await chat.connect();
// Later, during teardown:
subscription.unsubscribe();
builder.close();
chat.close();
2 changes: 1 addition & 1 deletion packages/platform/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@base44/platform",
"version": "0.3.0",
"description": "Base44 Platform SDK for white-label platforms: a browser client for watching the builder and a server entrypoint for webhooks",
"description": "Base44 Platform SDK for white-label platforms: a browser client for following the AI chat and a server entrypoint for webhooks",
"main": "dist/client/index.js",
"types": "dist/client/index.d.ts",
"type": "module",
Expand Down
8 changes: 4 additions & 4 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/client/modules/builder.events.generated.ts from asyncapi.json. `--check` fails on drift instead.
// Generates src/client/modules/chat.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/client/modules/builder.events.generated.ts", import.meta.url);
const target = new URL("../src/client/modules/chat.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 Expand Up @@ -85,10 +85,10 @@ export const serverEventNames = ${JSON.stringify(serverEvents.map((event) => eve
if (process.argv.includes("--check")) {
const committed = await readFile(target, "utf8").catch(() => "");
if (committed !== output) {
console.error("builder.events.generated.ts is out of date with asyncapi.json. Run `npm run gen:events -w @base44/platform` and commit the result.");
console.error("chat.events.generated.ts is out of date with asyncapi.json. Run `npm run gen:events -w @base44/platform` and commit the result.");
process.exit(1);
}
console.log("builder.events.generated.ts is up to date.");
console.log("chat.events.generated.ts is up to date.");
} else {
await writeFile(target, output);
console.log(`Wrote ${Object.keys(schemas).length} schemas and ${serverEvents.length} server events.`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"interfaces/builder": [
"interfaces/BuilderSession",
"interfaces/chat": [
"interfaces/ChatSession",
"interfaces/SocketSession",
"type-aliases/PlatformEvent",
"interfaces/PlatformSnapshot"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"typesWithOwnMethods": {
"BuilderSession": "Session methods"
"ChatSession": "Session methods"
},
"typeDefinitionGroups": [
["SocketSession", "PlatformEvent", "PlatformSnapshot"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
"AppErrorCode",
"BuilderModule",
"BuilderSession",
"ChatModule",
"ChatSession",
"PlatformEvent",
"PlatformSnapshot",
"PlatformSocketError",
Expand Down
4 changes: 2 additions & 2 deletions packages/platform/scripts/webhook-event-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"app.deleted.v1",
"app.restored.v1",
"webhook.test.v1",
"builder.turn_started.v1",
"builder.turn_ended.v1"
"chat.turn_started.v1",
"chat.turn_ended.v1"
]
14 changes: 7 additions & 7 deletions packages/platform/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { PlatformClient } from "./client.types.js";
import { createBuilder } from "./modules/builder.js";
import { createChat } from "./modules/chat.js";

/**
* Creates a platform client.
*
* This is the entry point of the Platform SDK. The client gives your white-label app editor
* access to the platform's modules, such as [`builder`](/developers/references/platform-sdk/docs/interfaces/builder).
* It takes no options and opens nothing: each builder session gets its socket session from the
* access to the platform's modules, such as [`chat`](/developers/references/platform-sdk/docs/interfaces/chat).
* It takes no options and opens nothing: each chat session gets its socket session from the
* `getSession` you pass to `init()`.
*
* @returns A platform client with access to the platform's modules.
Expand All @@ -19,16 +19,16 @@ import { createBuilder } from "./modules/builder.js";
* const client = createPlatformClient();
*
* // Use the client to follow the AI chat in an app
* const builder = client.builder.init({
* const chat = client.chat.init({
* async getSession() {
* const response = await fetch('/api/builder-socket-session', { method: 'POST' });
* const response = await fetch('/api/chat-socket-session', { method: 'POST' });
* return response.json();
* },
* onError: (error) => console.error(error.code),
* });
* await builder.connect();
* await chat.connect();
* ```
*/
export function createPlatformClient(): PlatformClient {
return Object.freeze({ builder: Object.freeze(createBuilder()) });
return Object.freeze({ chat: Object.freeze(createChat()) });
}
6 changes: 3 additions & 3 deletions packages/platform/src/client/client.types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { BuilderModule } from "./modules/builder.types.js";
import type { ChatModule } from "./modules/chat.types.js";

/**
* The platform client.
*
* Provides access to the platform's modules.
*/
export interface PlatformClient {
/** [Builder module](/developers/references/platform-sdk/docs/interfaces/builder) for following the AI chat in your users' apps live. */
readonly builder: BuilderModule;
/** [Chat module](/developers/references/platform-sdk/docs/interfaces/chat) for following the AI chat in your users' apps live. */
readonly chat: ChatModule;
}
6 changes: 3 additions & 3 deletions packages/platform/src/client/errors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PlatformSocketErrorCode } from "./errors.types.js";

/**
* The error the SDK reports when something goes wrong in a builder session.
* The error the SDK reports when something goes wrong in a chat session.
*
* You don't create it. The SDK passes it to your `onError` callbacks, `connect()` rejects with it,
* and `subscribe()` throws it. Check `code` to see what happened, then look the code up below:
Expand All @@ -15,14 +15,14 @@ import type { PlatformSocketErrorCode } from "./errors.types.js";
* @example
* ```typescript
* // Handle session and app errors
* const builder = client.builder.init({
* const chat = client.chat.init({
* // Session error codes: the whole session is affected
* onError(error) {
* if (error.code === 'session_revoked') showSignedOut();
* },
* });
*
* builder.subscribe(appId, {
* chat.subscribe(appId, {
* onSnapshot: renderChat,
* onEvent: applyEvent,
* // App error codes: only this app is affected, and error.appId says which
Expand Down
Loading
Loading