From 1962a314deb7b4d4607a78d80f47010d5604257e Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 19:58:01 +0300 Subject: [PATCH 01/32] feat(platform): commit the live updates AsyncAPI document and a sync script The service's AsyncAPI document is the source of truth for the socket protocol. sync-asyncapi.mjs replaces the committed copy from a URL (argument or ASYNCAPI_URL, default production) and refuses a document with no messages, which is what production serves while events are alpha. Co-Authored-By: Claude Opus 5.5 --- packages/platform/asyncapi.json | 5879 +++++++++++++++++++ packages/platform/scripts/sync-asyncapi.mjs | 21 + 2 files changed, 5900 insertions(+) create mode 100644 packages/platform/asyncapi.json create mode 100644 packages/platform/scripts/sync-asyncapi.mjs diff --git a/packages/platform/asyncapi.json b/packages/platform/asyncapi.json new file mode 100644 index 00000000..71721d8b --- /dev/null +++ b/packages/platform/asyncapi.json @@ -0,0 +1,5879 @@ +{ + "asyncapi": "3.0.0", + "info": { + "title": "Base44 Live Builder Updates", + "version": "1.0.0", + "description": "Live updates of the builder chat for the apps a white-label session may watch. The connection is Socket.IO, not a raw WebSocket: connect with `io(socket_url, { path: socket_path, transports: [\"websocket\"], auth: { session_token } })`, using the values your server received from `POST /api/service/socket-sessions`. Then send `join` for each app, and receive its snapshot followed by live events." + }, + "servers": { + "platform": { + "host": "ws.base44.com", + "protocol": "wss", + "pathname": "/ws/socket.io/", + "description": "Send the session token in the Socket.IO CONNECT `auth` as `session_token`. A refused connect is a `connect_error` with `message: \"connection_denied\"` and `data: { retryable }`: retry with backoff when `retryable`, otherwise ask your server for a new session." + } + }, + "channels": { + "platform": { + "title": "Platform socket", + "description": "One connection per session. Every event carries the `room` it belongs to, `/apps/{app_id}`; `room` is null for `session.ended`. Omitted keys in `data` are unchanged; explicit null clears.", + "messages": { + "MessageUpdatedEvent": { + "$ref": "#/components/messages/MessageUpdatedEvent" + }, + "MessageRemovedEvent": { + "$ref": "#/components/messages/MessageRemovedEvent" + }, + "AppStatusChangedEvent": { + "$ref": "#/components/messages/AppStatusChangedEvent" + }, + "PreviewReloadRequestedEvent": { + "$ref": "#/components/messages/PreviewReloadRequestedEvent" + }, + "PreviewNavigationRequestedEvent": { + "$ref": "#/components/messages/PreviewNavigationRequestedEvent" + }, + "QueueUpdatedEvent": { + "$ref": "#/components/messages/QueueUpdatedEvent" + }, + "TaskProgressedEvent": { + "$ref": "#/components/messages/TaskProgressedEvent" + }, + "ImageResolvedEvent": { + "$ref": "#/components/messages/ImageResolvedEvent" + }, + "ConversationChangedEvent": { + "$ref": "#/components/messages/ConversationChangedEvent" + }, + "FilesChangedEvent": { + "$ref": "#/components/messages/FilesChangedEvent" + }, + "BranchDeletedEvent": { + "$ref": "#/components/messages/BranchDeletedEvent" + }, + "RepositoryChangedEvent": { + "$ref": "#/components/messages/RepositoryChangedEvent" + }, + "PullRequestChangedEvent": { + "$ref": "#/components/messages/PullRequestChangedEvent" + }, + "SessionEndedEvent": { + "$ref": "#/components/messages/SessionEndedEvent" + }, + "RoomAccessDeniedEvent": { + "$ref": "#/components/messages/RoomAccessDeniedEvent" + }, + "RoomAccessRevokedEvent": { + "$ref": "#/components/messages/RoomAccessRevokedEvent" + }, + "RoomSnapshotUnavailableEvent": { + "$ref": "#/components/messages/RoomSnapshotUnavailableEvent" + }, + "AppSnapshotEvent": { + "$ref": "#/components/messages/AppSnapshotEvent" + }, + "JoinMessage": { + "$ref": "#/components/messages/JoinMessage" + }, + "LeaveMessage": { + "$ref": "#/components/messages/LeaveMessage" + } + } + } + }, + "operations": { + "sendMessageUpdatedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/MessageUpdatedEvent" + } + ], + "summary": "A chat message was added or replaced. Replace the message with the same `id`." + }, + "sendMessageRemovedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/MessageRemovedEvent" + } + ], + "summary": "A chat message was removed or hidden. An `id` the viewer never saw is a no-op." + }, + "sendAppStatusChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/AppStatusChangedEvent" + } + ], + "summary": "The builder started, finished or failed a turn. Null status clears it." + }, + "sendPreviewReloadRequestedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/PreviewReloadRequestedEvent" + } + ], + "summary": "Reload the app preview." + }, + "sendPreviewNavigationRequestedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/PreviewNavigationRequestedEvent" + } + ], + "summary": "Show this page of the app in the preview." + }, + "sendQueueUpdatedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/QueueUpdatedEvent" + } + ], + "summary": "The whole prompt queue, after any change." + }, + "sendTaskProgressedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/TaskProgressedEvent" + } + ], + "summary": "Progress of a long-running tool, by `tool_call_id`." + }, + "sendImageResolvedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/ImageResolvedEvent" + } + ], + "summary": "A generated image finished or failed. Replace its `placeholder_url` wherever it appears." + }, + "sendConversationChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/ConversationChangedEvent" + } + ], + "summary": "The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot." + }, + "sendFilesChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/FilesChangedEvent" + } + ], + "summary": "The app's files changed outside a chat turn." + }, + "sendBranchDeletedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/BranchDeletedEvent" + } + ], + "summary": "A branch was deleted." + }, + "sendRepositoryChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/RepositoryChangedEvent" + } + ], + "summary": "The imported app's repository changed." + }, + "sendPullRequestChangedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/PullRequestChangedEvent" + } + ], + "summary": "The imported app's pull request changed." + }, + "sendSessionEndedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/SessionEndedEvent" + } + ], + "summary": "The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next." + }, + "sendRoomAccessDeniedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/RoomAccessDeniedEvent" + } + ], + "summary": "A join was refused: the app is not on the session's allowlist, or the socket joins too often." + }, + "sendRoomAccessRevokedEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/RoomAccessRevokedEvent" + } + ], + "summary": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin." + }, + "sendRoomSnapshotUnavailableEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/RoomSnapshotUnavailableEvent" + } + ], + "summary": "The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot." + }, + "sendAppSnapshotEvent": { + "action": "send", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/AppSnapshotEvent" + } + ], + "summary": "The app's current status and its last 50 public messages, after every join. Replace the app's state with it." + }, + "receiveJoinMessage": { + "action": "receive", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/JoinMessage" + } + ], + "summary": "Join an app's room, once per app after every connect. An `app.snapshot` follows, then its live events. Refused with `room.access_denied` when the app is not on the session's allowlist, or past 30 joins a minute." + }, + "receiveLeaveMessage": { + "action": "receive", + "channel": { + "$ref": "#/channels/platform" + }, + "messages": [ + { + "$ref": "#/channels/platform/messages/LeaveMessage" + } + ], + "summary": "Leave an app's room. Its events stop." + } + }, + "components": { + "messages": { + "MessageUpdatedEvent": { + "name": "message.updated", + "title": "message.updated", + "summary": "A chat message was added or replaced. Replace the message with the same `id`.", + "description": "A chat message was added or replaced. Replace the message with the same `id`.", + "payload": { + "$ref": "#/components/schemas/MessageUpdatedEvent" + } + }, + "MessageRemovedEvent": { + "name": "message.removed", + "title": "message.removed", + "summary": "A chat message was removed or hidden. An `id` the viewer never saw is a no-op.", + "description": "A chat message was removed or hidden. An `id` the viewer never saw is a no-op.", + "payload": { + "$ref": "#/components/schemas/MessageRemovedEvent" + } + }, + "AppStatusChangedEvent": { + "name": "app.status_changed", + "title": "app.status_changed", + "summary": "The builder started, finished or failed a turn. Null status clears it.", + "description": "The builder started, finished or failed a turn. Null status clears it.", + "payload": { + "$ref": "#/components/schemas/AppStatusChangedEvent" + } + }, + "PreviewReloadRequestedEvent": { + "name": "preview.reload_requested", + "title": "preview.reload_requested", + "summary": "Reload the app preview.", + "description": "Reload the app preview.", + "payload": { + "$ref": "#/components/schemas/PreviewReloadRequestedEvent" + } + }, + "PreviewNavigationRequestedEvent": { + "name": "preview.navigation_requested", + "title": "preview.navigation_requested", + "summary": "Show this page of the app in the preview.", + "description": "Show this page of the app in the preview.", + "payload": { + "$ref": "#/components/schemas/PreviewNavigationRequestedEvent" + } + }, + "QueueUpdatedEvent": { + "name": "queue.updated", + "title": "queue.updated", + "summary": "The whole prompt queue, after any change.", + "description": "The whole prompt queue, after any change.", + "payload": { + "$ref": "#/components/schemas/QueueUpdatedEvent" + } + }, + "TaskProgressedEvent": { + "name": "task.progressed", + "title": "task.progressed", + "summary": "Progress of a long-running tool, by `tool_call_id`.", + "description": "Progress of a long-running tool, by `tool_call_id`.", + "payload": { + "$ref": "#/components/schemas/TaskProgressedEvent" + } + }, + "ImageResolvedEvent": { + "name": "image.resolved", + "title": "image.resolved", + "summary": "A generated image finished or failed. Replace its `placeholder_url` wherever it appears.", + "description": "A generated image finished or failed. Replace its `placeholder_url` wherever it appears.", + "payload": { + "$ref": "#/components/schemas/ImageResolvedEvent" + } + }, + "ConversationChangedEvent": { + "name": "conversation.changed", + "title": "conversation.changed", + "summary": "The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot.", + "description": "The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot.", + "payload": { + "$ref": "#/components/schemas/ConversationChangedEvent" + } + }, + "FilesChangedEvent": { + "name": "files.changed", + "title": "files.changed", + "summary": "The app's files changed outside a chat turn.", + "description": "The app's files changed outside a chat turn.", + "payload": { + "$ref": "#/components/schemas/FilesChangedEvent" + } + }, + "BranchDeletedEvent": { + "name": "branch.deleted", + "title": "branch.deleted", + "summary": "A branch was deleted.", + "description": "A branch was deleted.", + "payload": { + "$ref": "#/components/schemas/BranchDeletedEvent" + } + }, + "RepositoryChangedEvent": { + "name": "repository.changed", + "title": "repository.changed", + "summary": "The imported app's repository changed.", + "description": "The imported app's repository changed.", + "payload": { + "$ref": "#/components/schemas/RepositoryChangedEvent" + } + }, + "PullRequestChangedEvent": { + "name": "pull_request.changed", + "title": "pull_request.changed", + "summary": "The imported app's pull request changed.", + "description": "The imported app's pull request changed.", + "payload": { + "$ref": "#/components/schemas/PullRequestChangedEvent" + } + }, + "SessionEndedEvent": { + "name": "session.ended", + "title": "session.ended", + "summary": "The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next.", + "description": "The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next.", + "payload": { + "$ref": "#/components/schemas/SessionEndedEvent" + } + }, + "RoomAccessDeniedEvent": { + "name": "room.access_denied", + "title": "room.access_denied", + "summary": "A join was refused: the app is not on the session's allowlist, or the socket joins too often.", + "description": "A join was refused: the app is not on the session's allowlist, or the socket joins too often.", + "payload": { + "$ref": "#/components/schemas/RoomAccessDeniedEvent" + } + }, + "RoomAccessRevokedEvent": { + "name": "room.access_revoked", + "title": "room.access_revoked", + "summary": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin.", + "description": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin.", + "payload": { + "$ref": "#/components/schemas/RoomAccessRevokedEvent" + } + }, + "RoomSnapshotUnavailableEvent": { + "name": "room.snapshot_unavailable", + "title": "room.snapshot_unavailable", + "summary": "The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot.", + "description": "The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot.", + "payload": { + "$ref": "#/components/schemas/RoomSnapshotUnavailableEvent" + } + }, + "AppSnapshotEvent": { + "name": "app.snapshot", + "title": "app.snapshot", + "summary": "The app's current status and its last 50 public messages, after every join. Replace the app's state with it.", + "description": "The app's current status and its last 50 public messages, after every join. Replace the app's state with it.", + "payload": { + "$ref": "#/components/schemas/AppSnapshotEvent" + } + }, + "JoinMessage": { + "name": "join", + "title": "join", + "summary": "Join an app's room, once per app after every connect. An `app.snapshot` follows, then its live events. Refused with `room.access_denied` when the app is not on the session's allowlist, or past 30 joins a minute.", + "description": "Join an app's room, once per app after every connect. An `app.snapshot` follows, then its live events. Refused with `room.access_denied` when the app is not on the session's allowlist, or past 30 joins a minute.", + "payload": { + "$ref": "#/components/schemas/JoinMessage" + } + }, + "LeaveMessage": { + "name": "leave", + "title": "leave", + "summary": "Leave an app's room. Its events stop.", + "description": "Leave an app's room. Its events stop.", + "payload": { + "$ref": "#/components/schemas/LeaveMessage" + } + } + }, + "schemas": { + "AccessRuleChangeDetails": { + "description": "An entity access-rule change waiting for approval.", + "properties": { + "entity_name": { + "description": "Entity whose access rules change.", + "title": "Entity Name", + "type": "string" + }, + "change_type": { + "description": "How the access rules change.", + "title": "Change Type", + "type": "string" + }, + "changed_ops": { + "description": "Access operations whose rules change.", + "items": { + "type": "string" + }, + "title": "Changed Ops", + "type": "array" + } + }, + "title": "AccessRuleChangeDetails", + "type": "object" + }, + "AppSnapshotEvent": { + "description": "The app's current status and its last 50 public messages, after every join. Replace the app's state with it.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/Snapshot" + } + }, + "required": [ + "room", + "data" + ], + "title": "AppSnapshotEvent", + "type": "object" + }, + "AppStatusChanged": { + "properties": { + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/StatusObject" + }, + { + "type": "null" + } + ] + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "required": [ + "status" + ], + "title": "AppStatusChanged", + "type": "object" + }, + "AppStatusChangedEvent": { + "description": "The builder started, finished or failed a turn. Null status clears it.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/AppStatusChanged" + } + }, + "required": [ + "room", + "data" + ], + "title": "AppStatusChangedEvent", + "type": "object" + }, + "AskClarifyingQuestionsCall": { + "description": "A `ask_clarifying_questions` call.", + "properties": { + "name": { + "const": "ask_clarifying_questions", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "user_input": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionInput" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "AskClarifyingQuestionsCall", + "type": "object" + }, + "AskPlanQuestionsCall": { + "description": "A `ask_plan_questions` call.", + "properties": { + "name": { + "const": "ask_plan_questions", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "user_input": { + "anyOf": [ + { + "$ref": "#/components/schemas/QuestionInput" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "AskPlanQuestionsCall", + "type": "object" + }, + "BackendFunctionEntityDeleteApproval": { + "description": "Parked by the `backend_function_entity_delete` guard.", + "properties": { + "guard": { + "const": "backend_function_entity_delete", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/FunctionEntityDeleteDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "BackendFunctionEntityDeleteApproval", + "type": "object" + }, + "BashApproval": { + "description": "Parked by the `bash_approval` guard.", + "properties": { + "guard": { + "const": "bash_approval", + "title": "Guard", + "type": "string" + } + }, + "required": [ + "guard" + ], + "title": "BashApproval", + "type": "object" + }, + "BashCall": { + "description": "A `bash` call.", + "properties": { + "name": { + "const": "bash", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "BashCall", + "type": "object" + }, + "BranchChange": { + "description": "Re-read what changed; the event carries no content.", + "properties": { + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "title": "BranchChange", + "type": "object" + }, + "BranchDeletedEvent": { + "description": "A branch was deleted.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "BranchDeletedEvent", + "type": "object" + }, + "ConversationChangedEvent": { + "description": "The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "ConversationChangedEvent", + "type": "object" + }, + "CreateEntityRecordsCall": { + "description": "A `create_entity_records` call.", + "properties": { + "name": { + "const": "create_entity_records", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityRecordsDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "CreateEntityRecordsCall", + "type": "object" + }, + "DeleteEntitiesCall": { + "description": "A `delete_entities` call.", + "properties": { + "name": { + "const": "delete_entities", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityChangeArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "DeleteEntitiesCall", + "type": "object" + }, + "DeleteFileCall": { + "description": "A `delete_file` call.", + "properties": { + "name": { + "const": "delete_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "DeleteFileCall", + "type": "object" + }, + "EditRepoFileCall": { + "description": "A `edit_repo_file` call.", + "properties": { + "name": { + "const": "edit_repo_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "EditRepoFileCall", + "type": "object" + }, + "EntityChangeArguments": { + "description": "An entity update or delete; the entity is in `display.entity_name`.", + "properties": { + "summary": { + "description": "The agent's summary of the change. Queries and record values are never included.", + "title": "Summary", + "type": "string" + } + }, + "title": "EntityChangeArguments", + "type": "object" + }, + "EntityDisplay": { + "description": "The entity an entity operation touched.", + "properties": { + "entity_name": { + "description": "Entity type. Records and query values are never included.", + "title": "Entity Name", + "type": "string" + } + }, + "title": "EntityDisplay", + "type": "object" + }, + "EntityMutationDetails": { + "description": "What a parked entity mutation would do. Record data is never included.", + "properties": { + "method": { + "description": "Entity method the code calls, such as `delete`.", + "title": "Method", + "type": "string" + }, + "summary": { + "description": "The agent's summary of what the code does.", + "title": "Summary", + "type": "string" + } + }, + "title": "EntityMutationDetails", + "type": "object" + }, + "EntityRecordsDisplay": { + "properties": { + "entity_name": { + "description": "Entity type. Records and query values are never included.", + "title": "Entity Name", + "type": "string" + }, + "record_count": { + "description": "Number of records created.", + "title": "Record Count", + "type": "integer" + } + }, + "title": "EntityRecordsDisplay", + "type": "object" + }, + "EntityRlsGuardApproval": { + "description": "Parked by the `entity_rls_guard` guard.", + "properties": { + "guard": { + "const": "entity_rls_guard", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/AccessRuleChangeDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "EntityRlsGuardApproval", + "type": "object" + }, + "ExecDisplay": { + "description": "What a code execution did.", + "properties": { + "summary": { + "description": "Reviewed activity summary. Code and output are never included.", + "title": "Summary", + "type": "string" + }, + "writes_entities": { + "description": "The code changed entity data: refresh the preview.", + "title": "Writes Entities", + "type": "boolean" + } + }, + "title": "ExecDisplay", + "type": "object" + }, + "ExecToolCall": { + "description": "A `exec_tool` call.", + "properties": { + "name": { + "const": "exec_tool", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExecDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "ExecToolCall", + "type": "object" + }, + "ExecToolEntityMutationApproval": { + "description": "Parked by the `exec_tool_entity_mutation` guard.", + "properties": { + "guard": { + "const": "exec_tool_entity_mutation", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityMutationDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolEntityMutationApproval", + "type": "object" + }, + "ExecToolGithubRepoWriteApproval": { + "description": "Parked by the `exec_tool_github_repo_write` guard.", + "properties": { + "guard": { + "const": "exec_tool_github_repo_write", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolGithubRepoWriteApproval", + "type": "object" + }, + "ExecToolInviteUserApproval": { + "description": "Parked by the `exec_tool_invite_user` guard.", + "properties": { + "guard": { + "const": "exec_tool_invite_user", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolInviteUserApproval", + "type": "object" + }, + "ExecToolSendEmailApproval": { + "description": "Parked by the `exec_tool_send_email` guard.", + "properties": { + "guard": { + "const": "exec_tool_send_email", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolSendEmailApproval", + "type": "object" + }, + "ExecToolSendSmsApproval": { + "description": "Parked by the `exec_tool_send_sms` guard.", + "properties": { + "guard": { + "const": "exec_tool_send_sms", + "title": "Guard", + "type": "string" + }, + "reason": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "Why the guard parked the call.", + "title": "Reason" + }, + "details": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDetails" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "guard" + ], + "title": "ExecToolSendSmsApproval", + "type": "object" + }, + "FileDisplay": { + "description": "The files a file operation touched.", + "properties": { + "file_paths": { + "description": "Changed file paths. Source bodies and diffs are never included.", + "items": { + "type": "string" + }, + "title": "File Paths", + "type": "array" + }, + "content_empty": { + "description": "A write intentionally used empty content.", + "title": "Content Empty", + "type": "boolean" + } + }, + "title": "FileDisplay", + "type": "object" + }, + "FilesChangedEvent": { + "description": "The app's files changed outside a chat turn.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "FilesChangedEvent", + "type": "object" + }, + "FindReplaceCall": { + "description": "A `find_replace` call.", + "properties": { + "name": { + "const": "find_replace", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "FindReplaceCall", + "type": "object" + }, + "FunctionEntityDeleteDetails": { + "description": "A backend function test that would delete entity records.", + "properties": { + "function_name": { + "description": "Backend function under test.", + "title": "Function Name", + "type": "string" + }, + "method": { + "description": "Entity method the function calls.", + "title": "Method", + "type": "string" + }, + "inspection_failed": { + "description": "The guard could not verify the function's source.", + "title": "Inspection Failed", + "type": "boolean" + } + }, + "title": "FunctionEntityDeleteDetails", + "type": "object" + }, + "GameImageArguments": { + "description": "What to generate.", + "properties": { + "aspect_ratio": { + "description": "Requested aspect ratio, such as `1:1`.", + "title": "Aspect Ratio", + "type": "string" + } + }, + "title": "GameImageArguments", + "type": "object" + }, + "GenerateGameBackgroundCall": { + "description": "A `generate_game_background` call.", + "properties": { + "name": { + "const": "generate_game_background", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "results": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Results" + } + }, + "required": [ + "name" + ], + "title": "GenerateGameBackgroundCall", + "type": "object" + }, + "GenerateGameImageCall": { + "description": "A `generate_game_image` call.", + "properties": { + "name": { + "const": "generate_game_image", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/GameImageArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "results": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Results" + } + }, + "required": [ + "name" + ], + "title": "GenerateGameImageCall", + "type": "object" + }, + "GenerateImageCall": { + "description": "A `generate_image` call.", + "properties": { + "name": { + "const": "generate_image", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/MediaArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "results": { + "anyOf": [ + { + "$ref": "#/components/schemas/ImageResult" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "GenerateImageCall", + "type": "object" + }, + "GeneratePrdCall": { + "description": "A `generate_prd` call.", + "properties": { + "name": { + "const": "generate_prd", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/PrdArguments" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "GeneratePrdCall", + "type": "object" + }, + "GenerateVideoCall": { + "description": "A `generate_video` call.", + "properties": { + "name": { + "const": "generate_video", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/MediaArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "results": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Results" + } + }, + "required": [ + "name" + ], + "title": "GenerateVideoCall", + "type": "object" + }, + "ImageResolved": { + "properties": { + "status": { + "anyOf": [ + { + "enum": [ + "pending", + "completed", + "failed" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "placeholder_url": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Placeholder Url" + }, + "image_url": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Image Url" + } + }, + "title": "ImageResolved", + "type": "object" + }, + "ImageResolvedEvent": { + "description": "A generated image finished or failed. Replace its `placeholder_url` wherever it appears.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/ImageResolved" + } + }, + "required": [ + "room", + "data" + ], + "title": "ImageResolvedEvent", + "type": "object" + }, + "ImageResult": { + "description": "The generated image's state.", + "properties": { + "placeholder_url": { + "description": "Placeholder URL, used to associate an `image.resolved` event with this tool.", + "title": "Placeholder Url", + "type": "string" + }, + "status": { + "description": "Current generation status.", + "enum": [ + "pending", + "completed", + "failed" + ], + "title": "Status", + "type": "string" + }, + "image_url": { + "type": [ + "string", + "null" + ], + "description": "Generated image URL, once available.", + "title": "Image Url" + } + }, + "title": "ImageResult", + "type": "object" + }, + "InstallNpmPackageCall": { + "description": "A `install_npm_package` call.", + "properties": { + "name": { + "const": "install_npm_package", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/InstallPackageArguments" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "InstallNpmPackageCall", + "type": "object" + }, + "InstallPackageArguments": { + "description": "The package operations the builder runs.", + "properties": { + "packages": { + "description": "Requested package operations.", + "items": { + "$ref": "#/components/schemas/PackageOperation" + }, + "title": "Packages", + "type": "array" + } + }, + "title": "InstallPackageArguments", + "type": "object" + }, + "JoinMessage": { + "description": "Join an app's room, once per app after every connect. An `app.snapshot` follows, then its live events. Refused with `room.access_denied` when the app is not on the session's allowlist, or past 30 joins a minute.", + "pattern": "^/apps/[0-9a-f]{24}$", + "title": "JoinMessage", + "type": "string" + }, + "LeaveMessage": { + "description": "Leave an app's room. Its events stop.", + "pattern": "^/apps/[0-9a-f]{24}$", + "title": "LeaveMessage", + "type": "string" + }, + "MediaArguments": { + "description": "What to generate.", + "properties": { + "label": { + "description": "Visible media label.", + "title": "Label", + "type": "string" + }, + "aspect_ratio": { + "description": "Requested aspect ratio, such as `16:9`.", + "title": "Aspect Ratio", + "type": "string" + } + }, + "title": "MediaArguments", + "type": "object" + }, + "Message": { + "description": "A public chat message. Replace the message with the same `id` as a whole.", + "properties": { + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "role": { + "anyOf": [ + { + "enum": [ + "user", + "assistant", + "system" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Role" + }, + "content": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Content" + }, + "tool_calls": { + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/AskClarifyingQuestionsCall" + }, + { + "$ref": "#/components/schemas/AskPlanQuestionsCall" + }, + { + "$ref": "#/components/schemas/BashCall" + }, + { + "$ref": "#/components/schemas/CreateEntityRecordsCall" + }, + { + "$ref": "#/components/schemas/DeleteEntitiesCall" + }, + { + "$ref": "#/components/schemas/DeleteFileCall" + }, + { + "$ref": "#/components/schemas/EditRepoFileCall" + }, + { + "$ref": "#/components/schemas/ExecToolCall" + }, + { + "$ref": "#/components/schemas/FindReplaceCall" + }, + { + "$ref": "#/components/schemas/GenerateGameBackgroundCall" + }, + { + "$ref": "#/components/schemas/GenerateGameImageCall" + }, + { + "$ref": "#/components/schemas/GenerateImageCall" + }, + { + "$ref": "#/components/schemas/GeneratePrdCall" + }, + { + "$ref": "#/components/schemas/GenerateVideoCall" + }, + { + "$ref": "#/components/schemas/InstallNpmPackageCall" + }, + { + "$ref": "#/components/schemas/PreviewExecuteCodeCall" + }, + { + "$ref": "#/components/schemas/PreviewScreenshotCall" + }, + { + "$ref": "#/components/schemas/ReadOnlyExecToolCall" + }, + { + "$ref": "#/components/schemas/ReadRepoFileCall" + }, + { + "$ref": "#/components/schemas/RunShellCommandCall" + }, + { + "$ref": "#/components/schemas/SetSecretsCall" + }, + { + "$ref": "#/components/schemas/UpdateEntitiesCall" + }, + { + "$ref": "#/components/schemas/UpdatePlanCall" + }, + { + "$ref": "#/components/schemas/WriteFileCall" + }, + { + "$ref": "#/components/schemas/WriteRepoFileCall" + }, + { + "$ref": "#/components/schemas/OtherToolCall" + } + ] + }, + "type": [ + "array", + "null" + ], + "default": null, + "title": "Tool Calls" + }, + "checkpoint_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Checkpoint Id" + }, + "metadata": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageMetadata" + }, + { + "type": "null" + } + ], + "default": null + }, + "additional_message_params": { + "anyOf": [ + { + "$ref": "#/components/schemas/MessageParams" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Additional Message Params" + } + }, + "title": "Message", + "type": "object" + }, + "MessageMetadata": { + "description": "Message timestamp, without author identity.", + "properties": { + "created_date": { + "format": "date-time", + "type": [ + "string", + "null" + ], + "default": null, + "title": "Created Date" + } + }, + "title": "MessageMetadata", + "type": "object" + }, + "MessageParams": { + "description": "Scalar parameters a chat UI needs while a turn runs.", + "properties": { + "client_creation_id": { + "description": "Identifier the sending client attached, to match its optimistic message.", + "title": "Client Creation Id", + "type": "string" + }, + "plan_mode": { + "description": "The message was sent in plan mode.", + "title": "Plan Mode", + "type": "boolean" + }, + "plan_approved": { + "description": "The message approves a plan.", + "title": "Plan Approved", + "type": "boolean" + }, + "skip_ai_response": { + "description": "The message expects no AI reply.", + "title": "Skip Ai Response", + "type": "boolean" + }, + "system_message_type": { + "description": "System message category.", + "title": "System Message Type", + "type": "string" + } + }, + "title": "MessageParams", + "type": "object" + }, + "MessageRemoved": { + "properties": { + "message_id": { + "title": "Message Id", + "type": "string" + }, + "conversation_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Conversation Id" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "required": [ + "message_id" + ], + "title": "MessageRemoved", + "type": "object" + }, + "MessageRemovedEvent": { + "description": "A chat message was removed or hidden. An `id` the viewer never saw is a no-op.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/MessageRemoved" + } + }, + "required": [ + "room", + "data" + ], + "title": "MessageRemovedEvent", + "type": "object" + }, + "MessageUpdated": { + "properties": { + "message": { + "$ref": "#/components/schemas/Message" + }, + "conversation_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Conversation Id" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "required": [ + "message" + ], + "title": "MessageUpdated", + "type": "object" + }, + "MessageUpdatedEvent": { + "description": "A chat message was added or replaced. Replace the message with the same `id`.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/MessageUpdated" + } + }, + "required": [ + "room", + "data" + ], + "title": "MessageUpdatedEvent", + "type": "object" + }, + "OtherApproval": { + "description": "Parked by a guard that publishes only its name.", + "properties": { + "guard": { + "not": { + "enum": [ + "backend_function_entity_delete", + "bash_approval", + "entity_rls_guard", + "exec_tool_entity_mutation", + "exec_tool_github_repo_write", + "exec_tool_invite_user", + "exec_tool_send_email", + "exec_tool_send_sms" + ] + }, + "title": "Guard", + "type": "string" + } + }, + "required": [ + "guard" + ], + "title": "OtherApproval", + "type": "object" + }, + "OtherToolCall": { + "description": "A call to a tool that publishes only its name and progress.", + "properties": { + "name": { + "not": { + "enum": [ + "ask_clarifying_questions", + "ask_plan_questions", + "bash", + "create_entity_records", + "delete_entities", + "delete_file", + "edit_repo_file", + "exec_tool", + "find_replace", + "generate_game_background", + "generate_game_image", + "generate_image", + "generate_prd", + "generate_video", + "install_npm_package", + "preview_execute_code", + "preview_screenshot", + "read_only_exec_tool", + "read_repo_file", + "run_shell_command", + "set_secrets", + "update_entities", + "update_plan", + "write_file", + "write_repo_file" + ] + }, + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + } + }, + "required": [ + "name" + ], + "title": "OtherToolCall", + "type": "object" + }, + "PackageOperation": { + "description": "A package operation. Versions and package-manager output are never included.", + "properties": { + "name": { + "description": "Package name.", + "title": "Name", + "type": "string" + }, + "action": { + "description": "Requested operation, such as `install`.", + "title": "Action", + "type": "string" + } + }, + "title": "PackageOperation", + "type": "object" + }, + "PlanUpdate": { + "description": "An add-only plan update.", + "properties": { + "action": { + "description": "Update action.", + "title": "Action", + "type": "string" + }, + "section": { + "description": "Plan section key.", + "title": "Section", + "type": "string" + }, + "section_label": { + "description": "User-facing section label.", + "title": "Section Label", + "type": "string" + }, + "text": { + "description": "Plan point.", + "title": "Text", + "type": "string" + } + }, + "title": "PlanUpdate", + "type": "object" + }, + "PrdArguments": { + "description": "The user's own plan. In plan mode the message `content` beside it can be empty.", + "properties": { + "app_name": { + "description": "App or feature name.", + "title": "App Name", + "type": "string" + }, + "intent_and_goal": { + "description": "What is being built and why.", + "title": "Intent And Goal", + "type": "string" + }, + "audience_and_roles": { + "description": "Who uses it and their roles.", + "title": "Audience And Roles", + "type": "string" + }, + "core_flows": { + "description": "End-to-end user flows.", + "items": { + "type": "string" + }, + "title": "Core Flows", + "type": "array" + }, + "technical_requirements": { + "description": "Specific technical requirements.", + "title": "Technical Requirements", + "type": "string" + }, + "design_preferences": { + "description": "Design preferences.", + "title": "Design Preferences", + "type": "string" + }, + "prd_type": { + "description": "Build or plan declaration, when the builder asks for one.", + "title": "Prd Type", + "type": "string" + }, + "store_platform": { + "description": "Store platform settled in planning, for e-commerce plans.", + "title": "Store Platform", + "type": "string" + }, + "game_engine": { + "description": "Engine settled in planning, for game plans.", + "title": "Game Engine", + "type": "string" + } + }, + "title": "PrdArguments", + "type": "object" + }, + "PreviewExecuteCodeCall": { + "description": "A `preview_execute_code` call.", + "properties": { + "name": { + "const": "preview_execute_code", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExecDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "PreviewExecuteCodeCall", + "type": "object" + }, + "PreviewNavigationRequested": { + "properties": { + "path": { + "title": "Path", + "type": "string" + }, + "force": { + "description": "Navigate even when the user has moved away from the page.", + "title": "Force", + "type": "boolean" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "required": [ + "path", + "force" + ], + "title": "PreviewNavigationRequested", + "type": "object" + }, + "PreviewNavigationRequestedEvent": { + "description": "Show this page of the app in the preview.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/PreviewNavigationRequested" + } + }, + "required": [ + "room", + "data" + ], + "title": "PreviewNavigationRequestedEvent", + "type": "object" + }, + "PreviewReloadRequested": { + "properties": { + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "title": "PreviewReloadRequested", + "type": "object" + }, + "PreviewReloadRequestedEvent": { + "description": "Reload the app preview.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/PreviewReloadRequested" + } + }, + "required": [ + "room", + "data" + ], + "title": "PreviewReloadRequestedEvent", + "type": "object" + }, + "PreviewScreenshotCall": { + "description": "A `preview_screenshot` call.", + "properties": { + "name": { + "const": "preview_screenshot", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "PreviewScreenshotCall", + "type": "object" + }, + "PullRequestChangedEvent": { + "description": "The imported app's pull request changed.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "PullRequestChangedEvent", + "type": "object" + }, + "Question": { + "description": "A question for the user. Image and HTML sources and private design context are never included.", + "properties": { + "question": { + "description": "Visible question text.", + "title": "Question", + "type": "string" + }, + "type": { + "description": "Question category, such as `text`.", + "title": "Type", + "type": "string" + }, + "description": { + "description": "Supporting text.", + "title": "Description", + "type": "string" + }, + "multi_select": { + "description": "More than one answer may be selected.", + "title": "Multi Select", + "type": "boolean" + }, + "covers": { + "description": "Plan section the question covers.", + "title": "Covers", + "type": "string" + }, + "options": { + "description": "Selectable options; a bare string is its label.", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/components/schemas/QuestionOption" + } + ] + }, + "title": "Options", + "type": "array" + } + }, + "title": "Question", + "type": "object" + }, + "QuestionAnswer": { + "description": "An answer to a question. Secret-form input is never included.", + "properties": { + "question_index": { + "description": "Zero-based question index.", + "title": "Question Index", + "type": "integer" + }, + "selected_label": { + "description": "Label selected for a single-select question.", + "title": "Selected Label", + "type": "string" + }, + "selected_labels": { + "description": "Labels selected for a multi-select question.", + "items": { + "type": "string" + }, + "title": "Selected Labels", + "type": "array" + }, + "custom_text": { + "description": "Free-text answer. Structural filtering does not redact prose.", + "title": "Custom Text", + "type": "string" + } + }, + "title": "QuestionAnswer", + "type": "object" + }, + "QuestionArguments": { + "description": "The questions the builder asks.", + "properties": { + "questions": { + "description": "Questions presented to the user.", + "items": { + "$ref": "#/components/schemas/Question" + }, + "title": "Questions", + "type": "array" + } + }, + "title": "QuestionArguments", + "type": "object" + }, + "QuestionInput": { + "description": "The user's answers.", + "properties": { + "answers": { + "description": "Answers supplied to the question card.", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" + }, + "title": "Answers", + "type": "array" + } + }, + "title": "QuestionInput", + "type": "object" + }, + "QuestionOption": { + "description": "A selectable answer.", + "properties": { + "label": { + "description": "Visible option label.", + "title": "Label", + "type": "string" + } + }, + "title": "QuestionOption", + "type": "object" + }, + "QueueItem": { + "description": "A prompt waiting in the queue.", + "properties": { + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "content": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Content" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + }, + "created_at": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Created At" + } + }, + "title": "QueueItem", + "type": "object" + }, + "QueueState": { + "description": "The prompt queue: the prompts the builder runs next, in order.", + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/QueueItem" + }, + "type": [ + "array", + "null" + ], + "default": null, + "title": "Items" + }, + "is_paused": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Is Paused" + } + }, + "title": "QueueState", + "type": "object" + }, + "QueueUpdated": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/QueueItem" + }, + "type": [ + "array", + "null" + ], + "default": null, + "title": "Items" + }, + "is_paused": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Is Paused" + }, + "processed_item_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Processed Item Id" + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "title": "QueueUpdated", + "type": "object" + }, + "QueueUpdatedEvent": { + "description": "The whole prompt queue, after any change.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/QueueUpdated" + } + }, + "required": [ + "room", + "data" + ], + "title": "QueueUpdatedEvent", + "type": "object" + }, + "ReadOnlyExecToolCall": { + "description": "A `read_only_exec_tool` call.", + "properties": { + "name": { + "const": "read_only_exec_tool", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExecDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "ReadOnlyExecToolCall", + "type": "object" + }, + "ReadRepoFileCall": { + "description": "A `read_repo_file` call.", + "properties": { + "name": { + "const": "read_repo_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "ReadRepoFileCall", + "type": "object" + }, + "RepositoryChangedEvent": { + "description": "The imported app's repository changed.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/BranchChange" + } + }, + "required": [ + "room", + "data" + ], + "title": "RepositoryChangedEvent", + "type": "object" + }, + "RoomAccessDeniedEvent": { + "description": "A join was refused: the app is not on the session's allowlist, or the socket joins too often.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/RoomNotice" + } + }, + "required": [ + "room", + "data" + ], + "title": "RoomAccessDeniedEvent", + "type": "object" + }, + "RoomAccessRevokedEvent": { + "description": "The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/RoomNotice" + } + }, + "required": [ + "room", + "data" + ], + "title": "RoomAccessRevokedEvent", + "type": "object" + }, + "RoomNotice": { + "description": "The app is named by the event's room.", + "properties": {}, + "title": "RoomNotice", + "type": "object" + }, + "RoomSnapshotUnavailableEvent": { + "description": "The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/RoomNotice" + } + }, + "required": [ + "room", + "data" + ], + "title": "RoomSnapshotUnavailableEvent", + "type": "object" + }, + "RunShellCommandCall": { + "description": "A `run_shell_command` call.", + "properties": { + "name": { + "const": "run_shell_command", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/SummaryDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "RunShellCommandCall", + "type": "object" + }, + "SecretField": { + "description": "A requested secret. Its value is never sent over the socket.", + "properties": { + "secretName": { + "description": "Requested secret name.", + "title": "Secretname", + "type": "string" + }, + "description": { + "description": "Where to obtain it.", + "title": "Description", + "type": "string" + } + }, + "title": "SecretField", + "type": "object" + }, + "SessionEnded": { + "properties": { + "reason": { + "description": "`expired`: ask the partner backend for a new session. `revoked`: the partner or an admin ended it; don't reconnect. `replaced`: another tab took it; stop.", + "enum": [ + "expired", + "revoked", + "replaced" + ], + "title": "Reason", + "type": "string" + } + }, + "required": [ + "reason" + ], + "title": "SessionEnded", + "type": "object" + }, + "SessionEndedEvent": { + "description": "The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/SessionEnded" + } + }, + "required": [ + "room", + "data" + ], + "title": "SessionEndedEvent", + "type": "object" + }, + "SetSecretsArguments": { + "description": "The secrets the builder asks for.", + "properties": { + "secrets_schema": { + "description": "Requested secrets.", + "items": { + "$ref": "#/components/schemas/SecretField" + }, + "title": "Secrets Schema", + "type": "array" + } + }, + "title": "SetSecretsArguments", + "type": "object" + }, + "SetSecretsCall": { + "description": "A `set_secrets` call.", + "properties": { + "name": { + "const": "set_secrets", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/SetSecretsArguments" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "SetSecretsCall", + "type": "object" + }, + "Snapshot": { + "properties": { + "status": { + "anyOf": [ + { + "$ref": "#/components/schemas/StatusObject" + }, + { + "type": "null" + } + ] + }, + "messages": { + "items": { + "$ref": "#/components/schemas/Message" + }, + "title": "Messages", + "type": "array" + }, + "queue": { + "$ref": "#/components/schemas/QueueState", + "description": "The main branch's prompt queue." + } + }, + "required": [ + "status", + "messages", + "queue" + ], + "title": "Snapshot", + "type": "object" + }, + "StatusObject": { + "description": "Public builder state, without error diagnostics or billing context.", + "properties": { + "state": { + "anyOf": [ + { + "enum": [ + "ready", + "processing", + "error" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "State" + }, + "turn_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Turn Id" + }, + "last_updated_date": { + "format": "date-time", + "type": [ + "string", + "null" + ], + "default": null, + "title": "Last Updated Date" + } + }, + "title": "StatusObject", + "type": "object" + }, + "SummaryDetails": { + "description": "What a parked code execution would do.", + "properties": { + "summary": { + "description": "The agent's summary of what the code does.", + "title": "Summary", + "type": "string" + } + }, + "title": "SummaryDetails", + "type": "object" + }, + "SummaryDisplay": { + "description": "What a command or screenshot did.", + "properties": { + "summary": { + "description": "Reviewed activity summary. Commands and output are never included.", + "title": "Summary", + "type": "string" + } + }, + "title": "SummaryDisplay", + "type": "object" + }, + "TaskProgress": { + "properties": { + "current": { + "type": [ + "number", + "null" + ], + "default": null, + "title": "Current" + }, + "total": { + "type": [ + "number", + "null" + ], + "default": null, + "title": "Total" + }, + "percentage": { + "type": [ + "number", + "null" + ], + "default": null, + "title": "Percentage" + } + }, + "title": "TaskProgress", + "type": "object" + }, + "TaskProgressed": { + "properties": { + "tool_call_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Tool Call Id" + }, + "message_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Message Id" + }, + "event_type": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Event Type" + }, + "progress": { + "anyOf": [ + { + "$ref": "#/components/schemas/TaskProgress" + }, + { + "type": "null" + } + ], + "default": null + }, + "branch_id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Branch Id" + } + }, + "title": "TaskProgressed", + "type": "object" + }, + "TaskProgressedEvent": { + "description": "Progress of a long-running tool, by `tool_call_id`.", + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "title": "Room" + }, + "data": { + "$ref": "#/components/schemas/TaskProgressed" + } + }, + "required": [ + "room", + "data" + ], + "title": "TaskProgressedEvent", + "type": "object" + }, + "UpdateEntitiesCall": { + "description": "A `update_entities` call.", + "properties": { + "name": { + "const": "update_entities", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityChangeArguments" + }, + { + "type": "null" + } + ], + "default": null + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/EntityDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "UpdateEntitiesCall", + "type": "object" + }, + "UpdatePlanArguments": { + "description": "Settled plan points.", + "properties": { + "updates": { + "description": "Plan updates in their emitted order.", + "items": { + "$ref": "#/components/schemas/PlanUpdate" + }, + "title": "Updates", + "type": "array" + }, + "sections_with_enough": { + "description": "Base plan sections the builder considers sufficiently specified.", + "items": { + "type": "string" + }, + "title": "Sections With Enough", + "type": "array" + } + }, + "title": "UpdatePlanArguments", + "type": "object" + }, + "UpdatePlanCall": { + "description": "A `update_plan` call.", + "properties": { + "name": { + "const": "update_plan", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "arguments": { + "anyOf": [ + { + "$ref": "#/components/schemas/UpdatePlanArguments" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "UpdatePlanCall", + "type": "object" + }, + "WaitingOn": { + "description": "What a parked call waits for. Render the widget by kind, not by tool name.", + "properties": { + "kind": { + "description": "`choice`: pick among options. `input`: supply values. `approval`: approve or reject.", + "enum": [ + "choice", + "input", + "approval" + ], + "title": "Kind", + "type": "string" + } + }, + "title": "WaitingOn", + "type": "object" + }, + "WriteFileCall": { + "description": "A `write_file` call.", + "properties": { + "name": { + "const": "write_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "WriteFileCall", + "type": "object" + }, + "WriteRepoFileCall": { + "description": "A `write_repo_file` call.", + "properties": { + "name": { + "const": "write_repo_file", + "title": "Name", + "type": "string" + }, + "id": { + "type": [ + "string", + "null" + ], + "default": null, + "title": "Id" + }, + "requires_user_input": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Requires User Input" + }, + "auto_approved": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Auto Approved" + }, + "status": { + "anyOf": [ + { + "enum": [ + "running", + "success", + "error", + "stopped", + "waiting_for_user_input" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Status" + }, + "mutation_applied": { + "type": [ + "boolean", + "null" + ], + "default": null, + "title": "Mutation Applied" + }, + "waiting_on": { + "anyOf": [ + { + "$ref": "#/components/schemas/WaitingOn" + }, + { + "type": "null" + } + ], + "default": null, + "description": "What kind of answer a parked call waits for.", + "title": "Waiting On" + }, + "approval": { + "anyOf": [ + { + "oneOf": [ + { + "$ref": "#/components/schemas/BackendFunctionEntityDeleteApproval" + }, + { + "$ref": "#/components/schemas/BashApproval" + }, + { + "$ref": "#/components/schemas/EntityRlsGuardApproval" + }, + { + "$ref": "#/components/schemas/ExecToolEntityMutationApproval" + }, + { + "$ref": "#/components/schemas/ExecToolGithubRepoWriteApproval" + }, + { + "$ref": "#/components/schemas/ExecToolInviteUserApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendEmailApproval" + }, + { + "$ref": "#/components/schemas/ExecToolSendSmsApproval" + }, + { + "$ref": "#/components/schemas/OtherApproval" + } + ] + }, + { + "type": "null" + } + ], + "default": null, + "description": "Why a safety guard parked the call: the guard, and its declared reason and details.", + "title": "Approval" + }, + "display": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileDisplay" + }, + { + "type": "null" + } + ], + "default": null + } + }, + "required": [ + "name" + ], + "title": "WriteRepoFileCall", + "type": "object" + } + } + } +} diff --git a/packages/platform/scripts/sync-asyncapi.mjs b/packages/platform/scripts/sync-asyncapi.mjs new file mode 100644 index 00000000..a4535abd --- /dev/null +++ b/packages/platform/scripts/sync-asyncapi.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +// Replaces asyncapi.json with the document a backend serves. Production publishes only beta and GA +// messages, so while the events are alpha point this at a dev backend, e.g. a local apper: +// npm run sync:asyncapi -- http://localhost:8000/api/asyncapi.json +// or set ASYNCAPI_URL. Then run `npm run gen:events`. +import { writeFile } from "node:fs/promises"; + +const url = process.argv[2] ?? process.env.ASYNCAPI_URL ?? "https://app.base44.com/api/asyncapi.json"; +const target = new URL("../asyncapi.json", import.meta.url); + +const response = await fetch(url); +if (!response.ok) throw new Error(`GET ${url} returned ${response.status}`); +const document = await response.json(); +const messages = Object.keys(document.components?.messages ?? {}).length; +if (!messages) { + console.error(`${url} has no messages (production omits alpha events). Kept the committed copy.`); + console.error("Pass a dev backend URL instead, e.g. http://localhost:8000/api/asyncapi.json"); + process.exit(1); +} +await writeFile(target, JSON.stringify(document, null, 2) + "\n"); +console.log(`Wrote ${messages} messages from ${url}. Run npm run gen:events next.`); From 682d6a1776838ffd573085b2d63cc4fa3a8634b4 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 19:58:40 +0300 Subject: [PATCH 02/32] feat(platform): generate the event types from the AsyncAPI document gen-events.mjs compiles components.schemas with json-schema-to-typescript into builder.events.generated.ts: one type per schema, plus ServerEventMap and ClientMessageMap from the operations, and the runtime list of event names. PlatformEventMap, PlatformEvent, ToolCall and GuardApproval are derived from them, so a new backend event needs only sync and regen. Types now carry the backend model names (Message, StatusObject, ...); Snapshot becomes PlatformSnapshot. Adds json-schema-to-typescript 16.0.0 as a devDependency. Co-Authored-By: Claude Opus 5.5 --- packages/platform/package.json | 8 +- packages/platform/scripts/gen-events.mjs | 95 + packages/platform/src/index.ts | 9 +- .../platform/src/modules/builder-protocol.ts | 17 +- .../src/modules/builder-subscription.ts | 4 +- .../src/modules/builder.events.generated.ts | 2540 +++++++++++++++++ .../src/modules/builder.events.types.ts | 440 +-- .../platform/src/modules/builder.types.ts | 4 +- packages/platform/tests/types/client.types.ts | 47 +- 9 files changed, 2734 insertions(+), 430 deletions(-) create mode 100644 packages/platform/scripts/gen-events.mjs create mode 100644 packages/platform/src/modules/builder.events.generated.ts diff --git a/packages/platform/package.json b/packages/platform/package.json index 09fd8654..d89422f4 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -20,6 +20,9 @@ }, "scripts": { "build": "tsc", + "gen:events": "node scripts/gen-events.mjs", + "check:events": "node scripts/gen-events.mjs --check", + "sync:asyncapi": "node scripts/sync-asyncapi.mjs", "docs": "typedoc", "test": "npm run test:types && npm run test:unit && npm run docs && npm run test:package", "test:types": "tsc --noEmit -p tsconfig.type-tests.json", @@ -45,5 +48,8 @@ "bugs": { "url": "https://github.com/base44/javascript-sdk/issues" }, - "homepage": "https://github.com/base44/javascript-sdk/tree/main/packages/platform#readme" + "homepage": "https://github.com/base44/javascript-sdk/tree/main/packages/platform#readme", + "devDependencies": { + "json-schema-to-typescript": "16.0.0" + } } diff --git a/packages/platform/scripts/gen-events.mjs b/packages/platform/scripts/gen-events.mjs new file mode 100644 index 00000000..9fc4fa31 --- /dev/null +++ b/packages/platform/scripts/gen-events.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node +// Generates src/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 doc = JSON.parse(await readFile(source, "utf8")); +const schemas = doc.components.schemas; +const refName = (ref) => ref.split("/").pop(); + +// Pydantic titles every property; only component names should become TypeScript names. +function prepare(node, top) { + if (Array.isArray(node)) return node.map((item) => prepare(item, false)); + if (!node || typeof node !== "object") return node; + const out = {}; + for (const [key, value] of Object.entries(node)) { + if (key === "$ref") out.$ref = `#/definitions/${refName(value)}`; + else if (key === "properties") { + out.properties = Object.fromEntries(Object.entries(value).map(([name, property]) => { + const prepared = prepare(property, false); + // TypeDoc requires every public property documented; fall back to the field title. + prepared.description ??= property.title ? `${sentence(property.title)}.` : `The \`${name}\` field.`; + // A description beside a bare $ref would document the referenced type instead of this property. + if (prepared.$ref) return [name, { anyOf: [{ $ref: prepared.$ref }], description: prepared.description }]; + return [name, prepared]; + })); + } else if (key === "title" && !top) continue; + else out[key] = prepare(value, false); + } + return out; +} +const sentence = (title) => title.charAt(0) + title.slice(1).toLowerCase().replace(/\b(id|url)\b/g, (w) => w.toUpperCase()); + +const definitions = Object.fromEntries(Object.entries(schemas).map(([name, schema]) => { + const prepared = prepare(schema, true); + prepared.title = name; + prepared.description ??= `The \`${name}\` schema.`; + return [name, prepared]; +})); + +const body = await compile( + { title: "AsyncApiDocument", type: "object", properties: {}, definitions }, + "AsyncApiDocument", + { additionalProperties: false, unreachableDefinitions: true, bannerComment: "", format: true, strictIndexSignatures: true }, +); +// The root exists only to carry the definitions. +const types = body + .replace("export interface AsyncApiDocument {}\n", "") + .replace(/^ \*\n \* This (?:interface|type) was referenced by `AsyncApiDocument`'s JSON-Schema\n \* via the `definition` "\w+"\.\n/gm, "") + .replace(/^\/\*\*\n \* This (?:interface|type) was referenced by `AsyncApiDocument`'s JSON-Schema\n \* via the `definition` "\w+"\.\n \*\/\n/gm, ""); +if (types.includes("AsyncApiDocument")) throw new Error("Could not strip the root type"); + +const messageEntries = (action) => Object.values(doc.operations) + .filter((operation) => operation.action === action) + .flatMap((operation) => operation.messages.map((ref) => doc.components.messages[refName(ref.$ref)])); +// The service sends `send` operations; every server event is a `{room, data}` frame. +const serverEvents = messageEntries("send").map((message) => { + const frame = schemas[refName(message.payload.$ref)]; + return { name: message.name, summary: message.summary, type: refName(frame.properties.data.$ref) }; +}); +const clientMessages = messageEntries("receive").map((message) => ({ + name: message.name, summary: message.summary, type: refName(message.payload.$ref), +})); +const mapEntries = (entries) => entries.map((entry) => ` /** ${entry.summary} */\n ${JSON.stringify(entry.name)}: ${entry.type};`).join("\n"); + +const output = `// Generated from asyncapi.json by scripts/gen-events.mjs. Do not edit; run \`npm run gen:events\`. + +${types.trim()} + +/** Every event the service sends, by name, mapped to its \`data\` payload. */ +export interface ServerEventMap { +${mapEntries(serverEvents)} +} + +/** Every message a client sends, by name, mapped to its payload. */ +export interface ClientMessageMap { +${mapEntries(clientMessages)} +} + +/** @internal */ +export const serverEventNames = ${JSON.stringify(serverEvents.map((event) => event.name))} as const; +`; + +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."); + process.exit(1); + } + console.log("builder.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.`); +} diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index ce52f374..e9c0cac4 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -4,10 +4,5 @@ export { PlatformSocketError } from "./errors.js"; export type { PlatformSocketErrorCode } from "./errors.types.js"; export type { PlatformClientOptions } from "./client.types.js"; export type { BuilderModule, BuilderInitOptions, BuilderSession, PlatformSubscription, SubscriptionOptions } from "./modules/builder.types.js"; -export type { - AppStatus, AppStatusChanged, BranchScoped, ChatMessage, ImageResolved, MessageParams, MessageRemoved, MessageUpdated, - PlatformEvent, PlatformEventMap, PreviewNavigationRequested, QueueItem, QueueUpdated, Snapshot, SnapshotQueue, TaskProgressed, - ToolArguments, ToolCall, ToolDisplay, ToolEntityMutationArguments, ToolGuardApproval, ToolMediaArguments, ToolMediaResult, ToolPackageArguments, - ToolPackageOperation, ToolPlanArguments, ToolPlanUpdate, ToolPrdArguments, ToolQuestion, ToolQuestionAnswer, - ToolQuestionArguments, ToolQuestionInput, ToolQuestionOption, ToolSecretArguments, ToolSecretField, -} from "./modules/builder.events.types.js"; +export type { GuardApproval, PlatformEvent, PlatformEventMap, PlatformSnapshot, ToolCall } from "./modules/builder.events.types.js"; +export type * from "./modules/builder.events.generated.js"; diff --git a/packages/platform/src/modules/builder-protocol.ts b/packages/platform/src/modules/builder-protocol.ts index bee66141..1f4353b6 100644 --- a/packages/platform/src/modules/builder-protocol.ts +++ b/packages/platform/src/modules/builder-protocol.ts @@ -1,10 +1,11 @@ -import type { PlatformEvent, PlatformEventMap, Snapshot } from "./builder.events.types.js"; +import { serverEventNames } from "./builder.events.generated.js"; +import type { PlatformEvent, PlatformEventMap, PlatformSnapshot } from "./builder.events.types.js"; -export const eventNames = [ - "message.updated", "message.removed", "app.status_changed", "preview.reload_requested", - "preview.navigation_requested", "queue.updated", "task.progressed", "image.resolved", - "conversation.changed", "files.changed", "branch.deleted", "repository.changed", "pull_request.changed", -] as const satisfies readonly (keyof PlatformEventMap)[]; +const sessionEvents: readonly string[] = ["app.snapshot", "session.ended"]; +/** App events delivered to `onEvent`: every server event except the session events the socket handles. */ +export const eventNames = serverEventNames.filter( + (name): name is keyof PlatformEventMap & typeof name => !sessionEvents.includes(name) && !name.startsWith("room."), +); /** Notices that name an app room, by the error code each one reports. */ export const roomNotices = { "room.access_denied": "access_denied", @@ -25,12 +26,12 @@ export function appFromRoom(value: unknown): string | undefined { export function decode(type: keyof PlatformEventMap, appId: string, raw: unknown): PlatformEvent { return { type, appId, data: object(object(raw).data) } as PlatformEvent; } -export function decodeSnapshot(raw: unknown): Snapshot { +export function decodeSnapshot(raw: unknown): PlatformSnapshot { const frame = object(raw); const data = object(frame.data); if (!appFromRoom(frame.room) || !Array.isArray(data.messages) || (data.status !== null && typeof data.status !== "object")) { throw new Error("Invalid snapshot"); } const queue = data.queue !== null && typeof data.queue === "object" ? { queue: data.queue } : {}; - return { room: frame.room, status: data.status, messages: data.messages, ...queue } as Snapshot; + return { room: frame.room, status: data.status, messages: data.messages, ...queue } as PlatformSnapshot; } diff --git a/packages/platform/src/modules/builder-subscription.ts b/packages/platform/src/modules/builder-subscription.ts index b5627320..186ab251 100644 --- a/packages/platform/src/modules/builder-subscription.ts +++ b/packages/platform/src/modules/builder-subscription.ts @@ -1,4 +1,4 @@ -import type { PlatformEvent, Snapshot } from "./builder.events.types.js"; +import type { PlatformEvent, PlatformSnapshot } from "./builder.events.types.js"; import { PlatformSocketError } from "../errors.js"; import type { PlatformSocketErrorCode } from "../errors.types.js"; import type { PlatformSubscription, SubscriptionOptions } from "./builder.types.js"; @@ -29,7 +29,7 @@ export class Subscription implements PlatformSubscription { this.enqueue(() => this.options.onEvent(event)); } - snapshot(snapshot: Snapshot): void { + snapshot(snapshot: PlatformSnapshot): void { this.enqueue(() => this.options.onSnapshot(snapshot)); } diff --git a/packages/platform/src/modules/builder.events.generated.ts b/packages/platform/src/modules/builder.events.generated.ts new file mode 100644 index 00000000..2aba1180 --- /dev/null +++ b/packages/platform/src/modules/builder.events.generated.ts @@ -0,0 +1,2540 @@ +// Generated from asyncapi.json by scripts/gen-events.mjs. Do not edit; run `npm run gen:events`. + +/** + * Join an app's room, once per app after every connect. An `app.snapshot` follows, then its live events. Refused with `room.access_denied` when the app is not on the session's allowlist, or past 30 joins a minute. + */ +export type JoinMessage = string; +/** + * Leave an app's room. Its events stop. + */ +export type LeaveMessage = string; + +/** + * An entity access-rule change waiting for approval. + */ +export interface AccessRuleChangeDetails { + /** + * Entity whose access rules change. + */ + entity_name?: string; + /** + * How the access rules change. + */ + change_type?: string; + /** + * Access operations whose rules change. + */ + changed_ops?: string[]; +} +/** + * The app's current status and its last 50 public messages, after every join. Replace the app's state with it. + */ +export interface AppSnapshotEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: Snapshot; +} +/** + * The `Snapshot` schema. + */ +export interface Snapshot { + /** + * The `status` field. + */ + status: StatusObject | null; + /** + * Messages. + */ + messages: Message[]; + /** + * The main branch's prompt queue. + */ + queue: QueueState; +} +/** + * Public builder state, without error diagnostics or billing context. + */ +export interface StatusObject { + /** + * State. + */ + state?: ("ready" | "processing" | "error") | null; + /** + * Turn ID. + */ + turn_id?: string | null; + /** + * Last updated date. + */ + last_updated_date?: string | null; +} +/** + * A public chat message. Replace the message with the same `id` as a whole. + */ +export interface Message { + /** + * Id. + */ + id?: string | null; + /** + * Role. + */ + role?: ("user" | "assistant" | "system") | null; + /** + * Content. + */ + content?: string | null; + /** + * Tool calls. + */ + tool_calls?: + | ( + | AskClarifyingQuestionsCall + | AskPlanQuestionsCall + | BashCall + | CreateEntityRecordsCall + | DeleteEntitiesCall + | DeleteFileCall + | EditRepoFileCall + | ExecToolCall + | FindReplaceCall + | GenerateGameBackgroundCall + | GenerateGameImageCall + | GenerateImageCall + | GeneratePrdCall + | GenerateVideoCall + | InstallNpmPackageCall + | PreviewExecuteCodeCall + | PreviewScreenshotCall + | ReadOnlyExecToolCall + | ReadRepoFileCall + | RunShellCommandCall + | SetSecretsCall + | UpdateEntitiesCall + | UpdatePlanCall + | WriteFileCall + | WriteRepoFileCall + | OtherToolCall + )[] + | null; + /** + * Checkpoint ID. + */ + checkpoint_id?: string | null; + /** + * The `metadata` field. + */ + metadata?: MessageMetadata | null; + /** + * Additional message params. + */ + additional_message_params?: MessageParams | null; +} +/** + * A `ask_clarifying_questions` call. + */ +export interface AskClarifyingQuestionsCall { + /** + * Name. + */ + name: "ask_clarifying_questions"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: QuestionArguments | null; + /** + * The `user_input` field. + */ + user_input?: QuestionInput | null; +} +/** + * What a parked call waits for. Render the widget by kind, not by tool name. + */ +export interface WaitingOn { + /** + * `choice`: pick among options. `input`: supply values. `approval`: approve or reject. + */ + kind?: "choice" | "input" | "approval"; +} +/** + * Parked by the `backend_function_entity_delete` guard. + */ +export interface BackendFunctionEntityDeleteApproval { + /** + * Guard. + */ + guard: "backend_function_entity_delete"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: FunctionEntityDeleteDetails | null; +} +/** + * A backend function test that would delete entity records. + */ +export interface FunctionEntityDeleteDetails { + /** + * Backend function under test. + */ + function_name?: string; + /** + * Entity method the function calls. + */ + method?: string; + /** + * The guard could not verify the function's source. + */ + inspection_failed?: boolean; +} +/** + * Parked by the `bash_approval` guard. + */ +export interface BashApproval { + /** + * Guard. + */ + guard: "bash_approval"; +} +/** + * Parked by the `entity_rls_guard` guard. + */ +export interface EntityRlsGuardApproval { + /** + * Guard. + */ + guard: "entity_rls_guard"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: AccessRuleChangeDetails | null; +} +/** + * Parked by the `exec_tool_entity_mutation` guard. + */ +export interface ExecToolEntityMutationApproval { + /** + * Guard. + */ + guard: "exec_tool_entity_mutation"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: EntityMutationDetails | null; +} +/** + * What a parked entity mutation would do. Record data is never included. + */ +export interface EntityMutationDetails { + /** + * Entity method the code calls, such as `delete`. + */ + method?: string; + /** + * The agent's summary of what the code does. + */ + summary?: string; +} +/** + * Parked by the `exec_tool_github_repo_write` guard. + */ +export interface ExecToolGithubRepoWriteApproval { + /** + * Guard. + */ + guard: "exec_tool_github_repo_write"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: SummaryDetails | null; +} +/** + * What a parked code execution would do. + */ +export interface SummaryDetails { + /** + * The agent's summary of what the code does. + */ + summary?: string; +} +/** + * Parked by the `exec_tool_invite_user` guard. + */ +export interface ExecToolInviteUserApproval { + /** + * Guard. + */ + guard: "exec_tool_invite_user"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: SummaryDetails | null; +} +/** + * Parked by the `exec_tool_send_email` guard. + */ +export interface ExecToolSendEmailApproval { + /** + * Guard. + */ + guard: "exec_tool_send_email"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: SummaryDetails | null; +} +/** + * Parked by the `exec_tool_send_sms` guard. + */ +export interface ExecToolSendSmsApproval { + /** + * Guard. + */ + guard: "exec_tool_send_sms"; + /** + * Why the guard parked the call. + */ + reason?: string | null; + /** + * The `details` field. + */ + details?: SummaryDetails | null; +} +/** + * Parked by a guard that publishes only its name. + */ +export interface OtherApproval { + /** + * Guard. + */ + guard: string; +} +/** + * The questions the builder asks. + */ +export interface QuestionArguments { + /** + * Questions presented to the user. + */ + questions?: Question[]; +} +/** + * A question for the user. Image and HTML sources and private design context are never included. + */ +export interface Question { + /** + * Visible question text. + */ + question?: string; + /** + * Question category, such as `text`. + */ + type?: string; + /** + * Supporting text. + */ + description?: string; + /** + * More than one answer may be selected. + */ + multi_select?: boolean; + /** + * Plan section the question covers. + */ + covers?: string; + /** + * Selectable options; a bare string is its label. + */ + options?: (string | QuestionOption)[]; +} +/** + * A selectable answer. + */ +export interface QuestionOption { + /** + * Visible option label. + */ + label?: string; +} +/** + * The user's answers. + */ +export interface QuestionInput { + /** + * Answers supplied to the question card. + */ + answers?: QuestionAnswer[]; +} +/** + * An answer to a question. Secret-form input is never included. + */ +export interface QuestionAnswer { + /** + * Zero-based question index. + */ + question_index?: number; + /** + * Label selected for a single-select question. + */ + selected_label?: string; + /** + * Labels selected for a multi-select question. + */ + selected_labels?: string[]; + /** + * Free-text answer. Structural filtering does not redact prose. + */ + custom_text?: string; +} +/** + * A `ask_plan_questions` call. + */ +export interface AskPlanQuestionsCall { + /** + * Name. + */ + name: "ask_plan_questions"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: QuestionArguments | null; + /** + * The `user_input` field. + */ + user_input?: QuestionInput | null; +} +/** + * A `bash` call. + */ +export interface BashCall { + /** + * Name. + */ + name: "bash"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: SummaryDisplay | null; +} +/** + * What a command or screenshot did. + */ +export interface SummaryDisplay { + /** + * Reviewed activity summary. Commands and output are never included. + */ + summary?: string; +} +/** + * A `create_entity_records` call. + */ +export interface CreateEntityRecordsCall { + /** + * Name. + */ + name: "create_entity_records"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: EntityRecordsDisplay | null; +} +/** + * The `EntityRecordsDisplay` schema. + */ +export interface EntityRecordsDisplay { + /** + * Entity type. Records and query values are never included. + */ + entity_name?: string; + /** + * Number of records created. + */ + record_count?: number; +} +/** + * A `delete_entities` call. + */ +export interface DeleteEntitiesCall { + /** + * Name. + */ + name: "delete_entities"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: EntityChangeArguments | null; + /** + * The `display` field. + */ + display?: EntityDisplay | null; +} +/** + * An entity update or delete; the entity is in `display.entity_name`. + */ +export interface EntityChangeArguments { + /** + * The agent's summary of the change. Queries and record values are never included. + */ + summary?: string; +} +/** + * The entity an entity operation touched. + */ +export interface EntityDisplay { + /** + * Entity type. Records and query values are never included. + */ + entity_name?: string; +} +/** + * A `delete_file` call. + */ +export interface DeleteFileCall { + /** + * Name. + */ + name: "delete_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * The files a file operation touched. + */ +export interface FileDisplay { + /** + * Changed file paths. Source bodies and diffs are never included. + */ + file_paths?: string[]; + /** + * A write intentionally used empty content. + */ + content_empty?: boolean; +} +/** + * A `edit_repo_file` call. + */ +export interface EditRepoFileCall { + /** + * Name. + */ + name: "edit_repo_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A `exec_tool` call. + */ +export interface ExecToolCall { + /** + * Name. + */ + name: "exec_tool"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: ExecDisplay | null; +} +/** + * What a code execution did. + */ +export interface ExecDisplay { + /** + * Reviewed activity summary. Code and output are never included. + */ + summary?: string; + /** + * The code changed entity data: refresh the preview. + */ + writes_entities?: boolean; +} +/** + * A `find_replace` call. + */ +export interface FindReplaceCall { + /** + * Name. + */ + name: "find_replace"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A `generate_game_background` call. + */ +export interface GenerateGameBackgroundCall { + /** + * Name. + */ + name: "generate_game_background"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * Results. + */ + results?: string | null; +} +/** + * A `generate_game_image` call. + */ +export interface GenerateGameImageCall { + /** + * Name. + */ + name: "generate_game_image"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: GameImageArguments | null; + /** + * Results. + */ + results?: string | null; +} +/** + * What to generate. + */ +export interface GameImageArguments { + /** + * Requested aspect ratio, such as `1:1`. + */ + aspect_ratio?: string; +} +/** + * A `generate_image` call. + */ +export interface GenerateImageCall { + /** + * Name. + */ + name: "generate_image"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: MediaArguments | null; + /** + * The `results` field. + */ + results?: ImageResult | null; +} +/** + * What to generate. + */ +export interface MediaArguments { + /** + * Visible media label. + */ + label?: string; + /** + * Requested aspect ratio, such as `16:9`. + */ + aspect_ratio?: string; +} +/** + * The generated image's state. + */ +export interface ImageResult { + /** + * Placeholder URL, used to associate an `image.resolved` event with this tool. + */ + placeholder_url?: string; + /** + * Current generation status. + */ + status?: "pending" | "completed" | "failed"; + /** + * Generated image URL, once available. + */ + image_url?: string | null; +} +/** + * A `generate_prd` call. + */ +export interface GeneratePrdCall { + /** + * Name. + */ + name: "generate_prd"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: PrdArguments | null; +} +/** + * The user's own plan. In plan mode the message `content` beside it can be empty. + */ +export interface PrdArguments { + /** + * App or feature name. + */ + app_name?: string; + /** + * What is being built and why. + */ + intent_and_goal?: string; + /** + * Who uses it and their roles. + */ + audience_and_roles?: string; + /** + * End-to-end user flows. + */ + core_flows?: string[]; + /** + * Specific technical requirements. + */ + technical_requirements?: string; + /** + * Design preferences. + */ + design_preferences?: string; + /** + * Build or plan declaration, when the builder asks for one. + */ + prd_type?: string; + /** + * Store platform settled in planning, for e-commerce plans. + */ + store_platform?: string; + /** + * Engine settled in planning, for game plans. + */ + game_engine?: string; +} +/** + * A `generate_video` call. + */ +export interface GenerateVideoCall { + /** + * Name. + */ + name: "generate_video"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: MediaArguments | null; + /** + * Results. + */ + results?: string | null; +} +/** + * A `install_npm_package` call. + */ +export interface InstallNpmPackageCall { + /** + * Name. + */ + name: "install_npm_package"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: InstallPackageArguments | null; +} +/** + * The package operations the builder runs. + */ +export interface InstallPackageArguments { + /** + * Requested package operations. + */ + packages?: PackageOperation[]; +} +/** + * A package operation. Versions and package-manager output are never included. + */ +export interface PackageOperation { + /** + * Package name. + */ + name?: string; + /** + * Requested operation, such as `install`. + */ + action?: string; +} +/** + * A `preview_execute_code` call. + */ +export interface PreviewExecuteCodeCall { + /** + * Name. + */ + name: "preview_execute_code"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: ExecDisplay | null; +} +/** + * A `preview_screenshot` call. + */ +export interface PreviewScreenshotCall { + /** + * Name. + */ + name: "preview_screenshot"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: SummaryDisplay | null; +} +/** + * A `read_only_exec_tool` call. + */ +export interface ReadOnlyExecToolCall { + /** + * Name. + */ + name: "read_only_exec_tool"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: ExecDisplay | null; +} +/** + * A `read_repo_file` call. + */ +export interface ReadRepoFileCall { + /** + * Name. + */ + name: "read_repo_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A `run_shell_command` call. + */ +export interface RunShellCommandCall { + /** + * Name. + */ + name: "run_shell_command"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: SummaryDisplay | null; +} +/** + * A `set_secrets` call. + */ +export interface SetSecretsCall { + /** + * Name. + */ + name: "set_secrets"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: SetSecretsArguments | null; +} +/** + * The secrets the builder asks for. + */ +export interface SetSecretsArguments { + /** + * Requested secrets. + */ + secrets_schema?: SecretField[]; +} +/** + * A requested secret. Its value is never sent over the socket. + */ +export interface SecretField { + /** + * Requested secret name. + */ + secretName?: string; + /** + * Where to obtain it. + */ + description?: string; +} +/** + * A `update_entities` call. + */ +export interface UpdateEntitiesCall { + /** + * Name. + */ + name: "update_entities"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: EntityChangeArguments | null; + /** + * The `display` field. + */ + display?: EntityDisplay | null; +} +/** + * A `update_plan` call. + */ +export interface UpdatePlanCall { + /** + * Name. + */ + name: "update_plan"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `arguments` field. + */ + arguments?: UpdatePlanArguments | null; +} +/** + * Settled plan points. + */ +export interface UpdatePlanArguments { + /** + * Plan updates in their emitted order. + */ + updates?: PlanUpdate[]; + /** + * Base plan sections the builder considers sufficiently specified. + */ + sections_with_enough?: string[]; +} +/** + * An add-only plan update. + */ +export interface PlanUpdate { + /** + * Update action. + */ + action?: string; + /** + * Plan section key. + */ + section?: string; + /** + * User-facing section label. + */ + section_label?: string; + /** + * Plan point. + */ + text?: string; +} +/** + * A `write_file` call. + */ +export interface WriteFileCall { + /** + * Name. + */ + name: "write_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A `write_repo_file` call. + */ +export interface WriteRepoFileCall { + /** + * Name. + */ + name: "write_repo_file"; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; + /** + * The `display` field. + */ + display?: FileDisplay | null; +} +/** + * A call to a tool that publishes only its name and progress. + */ +export interface OtherToolCall { + /** + * Name. + */ + name: string; + /** + * Id. + */ + id?: string | null; + /** + * Requires user input. + */ + requires_user_input?: boolean | null; + /** + * Auto approved. + */ + auto_approved?: boolean | null; + /** + * Status. + */ + status?: ("running" | "success" | "error" | "stopped" | "waiting_for_user_input") | null; + /** + * Mutation applied. + */ + mutation_applied?: boolean | null; + /** + * What kind of answer a parked call waits for. + */ + waiting_on?: WaitingOn | null; + /** + * Why a safety guard parked the call: the guard, and its declared reason and details. + */ + approval?: + | ( + | BackendFunctionEntityDeleteApproval + | BashApproval + | EntityRlsGuardApproval + | ExecToolEntityMutationApproval + | ExecToolGithubRepoWriteApproval + | ExecToolInviteUserApproval + | ExecToolSendEmailApproval + | ExecToolSendSmsApproval + | OtherApproval + ) + | null; +} +/** + * Message timestamp, without author identity. + */ +export interface MessageMetadata { + /** + * Created date. + */ + created_date?: string | null; +} +/** + * Scalar parameters a chat UI needs while a turn runs. + */ +export interface MessageParams { + /** + * Identifier the sending client attached, to match its optimistic message. + */ + client_creation_id?: string; + /** + * The message was sent in plan mode. + */ + plan_mode?: boolean; + /** + * The message approves a plan. + */ + plan_approved?: boolean; + /** + * The message expects no AI reply. + */ + skip_ai_response?: boolean; + /** + * System message category. + */ + system_message_type?: string; +} +/** + * The prompt queue: the prompts the builder runs next, in order. + */ +export interface QueueState { + /** + * Items. + */ + items?: QueueItem[] | null; + /** + * Is paused. + */ + is_paused?: boolean | null; +} +/** + * A prompt waiting in the queue. + */ +export interface QueueItem { + /** + * Id. + */ + id?: string | null; + /** + * Content. + */ + content?: string | null; + /** + * Branch ID. + */ + branch_id?: string | null; + /** + * Created at. + */ + created_at?: string | null; +} +/** + * The `AppStatusChanged` schema. + */ +export interface AppStatusChanged { + /** + * The `status` field. + */ + status: StatusObject | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * The builder started, finished or failed a turn. Null status clears it. + */ +export interface AppStatusChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: AppStatusChanged; +} +/** + * Re-read what changed; the event carries no content. + */ +export interface BranchChange { + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * A branch was deleted. + */ +export interface BranchDeletedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot. + */ +export interface ConversationChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * The app's files changed outside a chat turn. + */ +export interface FilesChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * The `ImageResolved` schema. + */ +export interface ImageResolved { + /** + * Status. + */ + status?: ("pending" | "completed" | "failed") | null; + /** + * Placeholder URL. + */ + placeholder_url?: string | null; + /** + * Image URL. + */ + image_url?: string | null; +} +/** + * A generated image finished or failed. Replace its `placeholder_url` wherever it appears. + */ +export interface ImageResolvedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: ImageResolved; +} +/** + * The `MessageRemoved` schema. + */ +export interface MessageRemoved { + /** + * Message ID. + */ + message_id: string; + /** + * Conversation ID. + */ + conversation_id?: string | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * A chat message was removed or hidden. An `id` the viewer never saw is a no-op. + */ +export interface MessageRemovedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: MessageRemoved; +} +/** + * The `MessageUpdated` schema. + */ +export interface MessageUpdated { + /** + * The `message` field. + */ + message: Message; + /** + * Conversation ID. + */ + conversation_id?: string | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * A chat message was added or replaced. Replace the message with the same `id`. + */ +export interface MessageUpdatedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: MessageUpdated; +} +/** + * The `PreviewNavigationRequested` schema. + */ +export interface PreviewNavigationRequested { + /** + * Path. + */ + path: string; + /** + * Navigate even when the user has moved away from the page. + */ + force: boolean; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * Show this page of the app in the preview. + */ +export interface PreviewNavigationRequestedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: PreviewNavigationRequested; +} +/** + * The `PreviewReloadRequested` schema. + */ +export interface PreviewReloadRequested { + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * Reload the app preview. + */ +export interface PreviewReloadRequestedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: PreviewReloadRequested; +} +/** + * The imported app's pull request changed. + */ +export interface PullRequestChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * The `QueueUpdated` schema. + */ +export interface QueueUpdated { + /** + * Items. + */ + items?: QueueItem[] | null; + /** + * Is paused. + */ + is_paused?: boolean | null; + /** + * Processed item ID. + */ + processed_item_id?: string | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * The whole prompt queue, after any change. + */ +export interface QueueUpdatedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: QueueUpdated; +} +/** + * The imported app's repository changed. + */ +export interface RepositoryChangedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: BranchChange; +} +/** + * A join was refused: the app is not on the session's allowlist, or the socket joins too often. + */ +export interface RoomAccessDeniedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: RoomNotice; +} +/** + * The app is named by the event's room. + */ +export interface RoomNotice {} +/** + * The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin. + */ +export interface RoomAccessRevokedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: RoomNotice; +} +/** + * The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot. + */ +export interface RoomSnapshotUnavailableEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: RoomNotice; +} +/** + * The `SessionEnded` schema. + */ +export interface SessionEnded { + /** + * `expired`: ask the partner backend for a new session. `revoked`: the partner or an admin ended it; don't reconnect. `replaced`: another tab took it; stop. + */ + reason: "expired" | "revoked" | "replaced"; +} +/** + * The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next. + */ +export interface SessionEndedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: SessionEnded; +} +/** + * The `TaskProgress` schema. + */ +export interface TaskProgress { + /** + * Current. + */ + current?: number | null; + /** + * Total. + */ + total?: number | null; + /** + * Percentage. + */ + percentage?: number | null; +} +/** + * The `TaskProgressed` schema. + */ +export interface TaskProgressed { + /** + * Tool call ID. + */ + tool_call_id?: string | null; + /** + * Message ID. + */ + message_id?: string | null; + /** + * Event type. + */ + event_type?: string | null; + /** + * The `progress` field. + */ + progress?: TaskProgress | null; + /** + * Branch ID. + */ + branch_id?: string | null; +} +/** + * Progress of a long-running tool, by `tool_call_id`. + */ +export interface TaskProgressedEvent { + /** + * Room. + */ + room: string | null; + /** + * The `data` field. + */ + data: TaskProgressed; +} + +/** Every event the service sends, by name, mapped to its `data` payload. */ +export interface ServerEventMap { + /** A chat message was added or replaced. Replace the message with the same `id`. */ + "message.updated": MessageUpdated; + /** A chat message was removed or hidden. An `id` the viewer never saw is a no-op. */ + "message.removed": MessageRemoved; + /** The builder started, finished or failed a turn. Null status clears it. */ + "app.status_changed": AppStatusChanged; + /** Reload the app preview. */ + "preview.reload_requested": PreviewReloadRequested; + /** Show this page of the app in the preview. */ + "preview.navigation_requested": PreviewNavigationRequested; + /** The whole prompt queue, after any change. */ + "queue.updated": QueueUpdated; + /** Progress of a long-running tool, by `tool_call_id`. */ + "task.progressed": TaskProgressed; + /** A generated image finished or failed. Replace its `placeholder_url` wherever it appears. */ + "image.resolved": ImageResolved; + /** The conversation was rewritten (undo, restore, sync). Rejoin the room for a fresh snapshot. */ + "conversation.changed": BranchChange; + /** The app's files changed outside a chat turn. */ + "files.changed": BranchChange; + /** A branch was deleted. */ + "branch.deleted": BranchChange; + /** The imported app's repository changed. */ + "repository.changed": BranchChange; + /** The imported app's pull request changed. */ + "pull_request.changed": BranchChange; + /** The session ended (expired, revoked, or taken by a newer socket). The socket disconnects next. */ + "session.ended": SessionEnded; + /** A join was refused: the app is not on the session's allowlist, or the socket joins too often. */ + "room.access_denied": RoomNotice; + /** The app left the session's allowlist, or its workspace. Events for it stop; do not rejoin. */ + "room.access_revoked": RoomNotice; + /** The join held but its snapshot failed. Live events still arrive; rejoin later for a snapshot. */ + "room.snapshot_unavailable": RoomNotice; + /** The app's current status and its last 50 public messages, after every join. Replace the app's state with it. */ + "app.snapshot": Snapshot; +} + +/** Every message a client sends, by name, mapped to its payload. */ +export interface ClientMessageMap { + /** Join an app's room, once per app after every connect. An `app.snapshot` follows, then its live events. Refused with `room.access_denied` when the app is not on the session's allowlist, or past 30 joins a minute. */ + "join": JoinMessage; + /** Leave an app's room. Its events stop. */ + "leave": LeaveMessage; +} + +/** @internal */ +export const serverEventNames = ["message.updated","message.removed","app.status_changed","preview.reload_requested","preview.navigation_requested","queue.updated","task.progressed","image.resolved","conversation.changed","files.changed","branch.deleted","repository.changed","pull_request.changed","session.ended","room.access_denied","room.access_revoked","room.snapshot_unavailable","app.snapshot"] as const; diff --git a/packages/platform/src/modules/builder.events.types.ts b/packages/platform/src/modules/builder.events.types.ts index e395203d..1da396bf 100644 --- a/packages/platform/src/modules/builder.events.types.ts +++ b/packages/platform/src/modules/builder.events.types.ts @@ -1,404 +1,56 @@ -/** Activity a file, execution or entity tool declared public. */ -export interface ToolDisplay { - /** Changed file paths for a file operation. Source bodies and diffs are never included. */ - file_paths?: string[]; - /** Whether a file write intentionally used empty content. */ - content_empty?: boolean; - /** Reviewed execution activity summary. Commands and execution output are never included. */ - summary?: string; - /** Whether a reviewed execution action changed entity data. */ - writes_entities?: boolean; - /** Entity type affected by an entity operation. Records and query values are never included. */ - entity_name?: string; - /** Number of records affected when supplied by the producer. */ - record_count?: number; -} - -/** A selectable answer to a builder question. */ -export interface ToolQuestionOption { - /** Visible option label. */ - label?: string; -} - -/** A builder question. Image/HTML source and private design context are excluded. */ -export interface ToolQuestion { - /** Visible question text. */ - question?: string; - /** Question category. */ - type?: string; - /** Optional visible supporting text. */ - description?: string; - /** Whether more than one answer may be selected. */ - multi_select?: boolean; - /** Plan section the question covers. */ - covers?: string; - /** Selectable options; a bare string option is its label. */ - options?: (ToolQuestionOption | string)[]; -} - -/** `ask_clarifying_questions` and `ask_plan_questions` arguments. */ -export interface ToolQuestionArguments { - /** Questions presented to the user. */ - questions?: ToolQuestion[]; -} - -/** A requested secret field. The secret value is never sent over the socket. */ -export interface ToolSecretField { - /** Requested secret name. */ - secretName?: string; - /** Optional explanation of where to obtain it. */ - description?: string; -} - -/** `set_secrets` arguments. */ -export interface ToolSecretArguments { - /** Requested secret fields. */ - secrets_schema?: ToolSecretField[]; -} - -/** A package operation. Versions and package-manager output are excluded. */ -export interface ToolPackageOperation { - /** Package name. */ - name?: string; - /** Requested package operation. */ - action?: string; -} - -/** `install_npm_package` arguments. */ -export interface ToolPackageArguments { - /** Requested package operations. */ - packages?: ToolPackageOperation[]; -} - -/** An add-only builder plan update. */ -export interface ToolPlanUpdate { - /** Update action. */ - action?: string; - /** Plan section key. */ - section?: string; - /** Optional user-facing section label. */ - section_label?: string; - /** Plan point. */ - text?: string; -} - -/** `update_plan` arguments. */ -export interface ToolPlanArguments { - /** Plan updates in their emitted order. */ - updates?: ToolPlanUpdate[]; - /** Base plan sections the builder considers sufficiently specified. */ - sections_with_enough?: string[]; -} - -/** `generate_prd` arguments: the user's own plan. In plan mode the message `content` beside it can be empty. */ -export interface ToolPrdArguments { - /** App or feature name. */ - app_name?: string; - /** What is being built and why. */ - intent_and_goal?: string; - /** Who uses it and their roles. */ - audience_and_roles?: string; - /** End-to-end user flows. */ - core_flows?: string[]; - /** Specific technical requirements. */ - technical_requirements?: string; - /** Design preferences. */ - design_preferences?: string; - /** Build or plan declaration, when the builder asks for one. */ - prd_type?: string; - /** Store platform settled in planning, for e-commerce plans. */ - store_platform?: string; - /** Engine settled in planning, for game plans. */ - game_engine?: string; -} - -/** Image, game-image and video generation arguments. */ -export interface ToolMediaArguments { - /** Visible media label. */ - label?: string; - /** Requested image or video aspect ratio. */ - aspect_ratio?: string; -} - -/** `update_entities` and `delete_entities` arguments; the entity is in `display.entity_name`. */ -export interface ToolEntityMutationArguments { - /** The agent's summary of the change. Queries and record values are never included. */ - summary?: string; -} - -/** The public arguments of a tool that declares them; narrow by the tool call's `name`. */ -export type ToolArguments = - | ToolQuestionArguments | ToolSecretArguments | ToolPackageArguments - | ToolPlanArguments | ToolPrdArguments | ToolMediaArguments | ToolEntityMutationArguments; - -/** Generated-media state. Raw generation prompts and IDs are never included. */ -export interface ToolMediaResult { - /** Placeholder URL, used to associate an `image.resolved` event with this tool. */ - placeholder_url?: string; - /** Current media-generation status. */ - status?: "pending" | "completed" | "failed"; - /** Generated asset URL, once available. */ - image_url?: string | null; -} - -/** An answer to a clarifying question. Secret-form input is never included. */ -export interface ToolQuestionAnswer { - /** Zero-based question index. */ - question_index?: number; - /** Label selected for a single-select question. */ - selected_label?: string; - /** Labels selected for a multi-select question. */ - selected_labels?: string[]; - /** User-provided free-text answer. Structural filtering does not redact prose. */ - custom_text?: string; -} - -/** Clarifying-question answers. */ -export interface ToolQuestionInput { - /** Answers supplied to the question card. */ - answers?: ToolQuestionAnswer[]; -} - -/** Why a safety guard parked a call for approval. Only present while the call waits. */ -export interface ToolGuardApproval { - /** Guard identifier, e.g. `exec_tool_send_email` or `bash_approval`. */ - guard: string; - /** Human-readable reason, for guards whose reason never quotes input (not shell commands). */ - reason?: string; - /** The details the guard declares public; commands and record data are never included. */ - details?: { - /** Entity method the code calls, e.g. `delete`. */ - method?: string; - /** The agent's summary of what the code does. */ - summary?: string; - /** Entity whose access rules change. */ - entity_name?: string; - /** How the access rules change. */ - change_type?: string; - /** Access operations whose rules change. */ - changed_ops?: string[]; - /** Backend function under test. */ - function_name?: string; - /** The guard could not verify the function's source. */ - inspection_failed?: boolean; - }; -} - -/** Public progress of a builder tool call. Only what the tool and its guard declare public is present. */ -export interface ToolCall { - /** Stable tool call identifier. */ - id?: string; - /** Tool name. */ - name?: string; - /** Current execution state. */ - status?: "running" | "success" | "error" | "stopped" | "waiting_for_user_input"; - /** Whether the tool needs a user response through the partner backend. */ - requires_user_input?: boolean; - /** Whether the builder auto-approved the operation. */ - auto_approved?: boolean; - /** Whether the mutation was applied. */ - mutation_applied?: boolean | null; - /** Kind of response a parked call waits for. */ - waiting_on?: { - /** Kind of response expected. */ - kind?: "approval" | "choice" | "input"; - }; - /** The tool's public arguments; absent for tools that declare none and for partial streaming arguments. */ - arguments?: ToolArguments; - /** What a file, execution or entity tool touched. */ - display?: ToolDisplay; - /** Clarifying-question answers. */ - user_input?: ToolQuestionInput; - /** Generated media, once the call is not waiting on a guard: the image tool's state, or a URL alone - * (a game asset's placeholder, which `image.resolved` replaces, or a finished video). */ - results?: ToolMediaResult | string; - /** A parked call's guard, and its reason and details where the guard declares them. */ - approval?: ToolGuardApproval; -} - -/** Scalar parameters a chat UI needs while a turn runs. */ -export interface MessageParams { - /** Identifier the sending client attached, to match its optimistic message. */ - client_creation_id?: string; - /** The message was sent in plan mode. */ - plan_mode?: boolean; - /** The message approves a plan. */ - plan_approved?: boolean; - /** The message expects no AI reply. */ - skip_ai_response?: boolean; - /** System message category. */ - system_message_type?: string; -} - -/** A public chat message. Replace the message with the same `id` as a whole. */ -export interface ChatMessage { - /** Message identifier. */ - id?: string; - /** Author category. System and hidden messages are never delivered. */ - role?: "user" | "assistant"; - /** Message text; structured content is withheld. Structural filtering is not prose redaction. */ - content?: string | null; - /** Public tool progress. */ - tool_calls?: ToolCall[] | null; - /** Message timestamp, without author identity. */ - metadata?: { - /** ISO timestamp string. */ - created_date?: string; - }; - /** Checkpoint reference; mutations remain on the partner backend. */ - checkpoint_id?: string | null; - /** Scalar message parameters. */ - additional_message_params?: MessageParams; -} - -/** Public builder state, without error diagnostics or billing context. */ -export interface AppStatus { - /** Current builder state. */ - state?: "ready" | "processing" | "error"; - /** State timestamp. */ - last_updated_date?: string | null; - /** The user message that started the turn, when a turn set this state. Quote it to Base44 support. */ - turn_id?: string | null; -} - -/** Branch scope carried by app events. Null or absent is the main branch. */ -export interface BranchScoped { - /** Branch the change belongs to; null is main. */ - branch_id?: string | null; -} - -/** `message.updated`: a chat message was added or replaced. */ -export interface MessageUpdated extends BranchScoped { - /** The whole message; replace the one with the same `id`. */ - message: ChatMessage; - /** Conversation containing the message. */ - conversation_id?: string | null; -} - -/** `message.removed`: a message was removed or hidden. An `id` the viewer never saw is a no-op. */ -export interface MessageRemoved extends BranchScoped { - /** Identifier of the message to remove. */ - message_id: string; - /** Conversation that contained the message. */ - conversation_id?: string | null; -} - -/** `app.status_changed`: a turn started, finished or failed. */ -export interface AppStatusChanged extends BranchScoped { - /** The new state; null clears it. */ - status: AppStatus | null; -} - -/** `preview.navigation_requested`: show this page of the app in the preview. */ -export interface PreviewNavigationRequested extends BranchScoped { - /** App path to show. */ - path: string; - /** Navigate even when the user has moved away from the page. */ - force: boolean; -} - -/** A queued builder request. */ -export interface QueueItem { - /** Queue item identifier. */ - id?: string; - /** User-authored request text. */ - content?: string; - /** Creation timestamp. */ - created_at?: string; - /** Branch the request belongs to. */ - branch_id?: string | null; -} - -/** `queue.updated`: the whole prompt queue, replacing the previous one. */ -export interface QueueUpdated extends BranchScoped { - /** Pending items. */ - items?: QueueItem[]; - /** Whether queue processing is paused. */ - is_paused?: boolean; - /** Identifier of the item just processed, when supplied. */ - processed_item_id?: string | null; -} - -/** `task.progressed`: progress of a long-running tool. */ -export interface TaskProgressed extends BranchScoped { - /** Associated tool call. */ - tool_call_id?: string | null; - /** Associated chat message. */ - message_id?: string | null; - /** Task lifecycle event, e.g. `task_progress`. */ - event_type?: string | null; - /** Numeric progress only; diagnostic text is withheld. */ - progress?: { - /** Completed work units. */ - current?: number | null; - /** Total work units, when known. */ - total?: number | null; - /** Producer-supplied percentage. */ - percentage?: number | null; - } | null; -} - -/** `image.resolved`: a generated image finished or failed. Replace its `placeholder_url` wherever it appears. */ -export type ImageResolved = ToolMediaResult; - -/** Mapping of public event names to their payloads. */ -export interface PlatformEventMap { - /** A chat message was added or replaced. */ - "message.updated": MessageUpdated; - /** A chat message was removed or hidden. */ - "message.removed": MessageRemoved; - /** The builder started, finished or failed a turn. */ - "app.status_changed": AppStatusChanged; - /** Reload the app preview. */ - "preview.reload_requested": BranchScoped; - /** Show a page of the app in the preview. */ - "preview.navigation_requested": PreviewNavigationRequested; - /** The whole prompt queue. */ - "queue.updated": QueueUpdated; - /** Progress of a long-running tool. */ - "task.progressed": TaskProgressed; - /** A generated image finished or failed. */ - "image.resolved": ImageResolved; - /** The conversation was rewritten (undo, restore, sync). On main, the SDK rejoins for a fresh snapshot. */ - "conversation.changed": BranchScoped; - /** The app's files changed outside a chat turn. Re-read them. */ - "files.changed": BranchScoped; - /** A branch was deleted. */ - "branch.deleted": BranchScoped; - /** The imported app's repository changed. */ - "repository.changed": BranchScoped; - /** The imported app's pull request changed. */ - "pull_request.changed": BranchScoped; -} - -/** Ordered delivery of one public event. */ +import type { Message, ServerEventMap, Snapshot } from "./builder.events.generated.js"; + +/** + * Maps each live event name to the type of its `data` payload. + * + * Generated from the service's AsyncAPI document, so it covers every app event the service sends. + * The client handles the session events `app.snapshot`, `session.ended` and `room.*` itself, and + * reports them through `onSnapshot` and `onError` instead. + */ +export type PlatformEventMap = { + [K in keyof ServerEventMap as K extends "app.snapshot" | "session.ended" | `room.${string}` ? never : K]: ServerEventMap[K]; +}; + +/** + * One live event for a subscribed app. + * + * Check `type` to narrow `data` to that event's payload. A key missing from `data` means the value + * is unchanged, and an explicit `null` clears it. + * + * @example + * ```typescript + * function onEvent(event: PlatformEvent) { + * if (event.type === "message.updated") { + * const { message } = event.data; + * console.log(`Message ${message.id} changed`); + * } + * } + * ``` + */ export type PlatformEvent = { [K in keyof PlatformEventMap]: { - /** Public event name; narrows the payload type. */ + /** Event name. Narrows the type of `data`. */ type: K; - /** Authorized app receiving this event. */ + /** ID of the app the event belongs to. */ appId: string; - /** Event payload. Omitted keys mean unchanged; explicit null clears. */ + /** Event payload. */ data: PlatformEventMap[K]; - } + }; }[keyof PlatformEventMap]; -/** An app's current state: sent after every join and rejoin, and after a main-conversation rewrite. */ -export interface Snapshot { - /** Canonical app room. */ +/** + * An app's current state. + * + * Arrives after every join and rejoin, and after a rewrite of the main conversation. Replace + * everything you hold for the app with it. It holds only the last 50 messages. + */ +export interface PlatformSnapshot extends Snapshot { + /** The app's room, `/apps/{app_id}`. */ room: string; - /** Current builder state. */ - status: AppStatus | null; - /** The last 50 public messages, oldest first. */ - messages: ChatMessage[]; - /** The main branch's prompt queue. Replace it, as with `queue.updated`. Absent from older servers. */ - queue?: SnapshotQueue; } -/** The prompt queue in a snapshot. */ -export interface SnapshotQueue { - /** Pending items. */ - items: QueueItem[]; - /** Whether queue processing is paused. */ - is_paused: boolean; -} +/** One tool call in a message's `tool_calls`. Check `name` to narrow it to that tool's call. */ +export type ToolCall = NonNullable[number]; + +/** Why a safety guard parked a tool call for approval. Check `guard` to narrow it. */ +export type GuardApproval = NonNullable; diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index 4d6c9324..b10d221a 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -1,4 +1,4 @@ -import type { PlatformEvent, Snapshot } from "./builder.events.types.js"; +import type { PlatformEvent, PlatformSnapshot } from "./builder.events.types.js"; import type { PlatformSocketError } from "../errors.js"; /** Options for an independent builder socket session. */ @@ -34,7 +34,7 @@ export interface SubscriptionOptions { /** Replace this app's state. Arrives after every join and rejoin, and after a main-conversation * rewrite (undo, restore, sync). Live events follow it; it holds only the last 50 messages. */ - onSnapshot: (snapshot: Snapshot) => void | Promise; + onSnapshot: (snapshot: PlatformSnapshot) => void | Promise; /** Apply each live event. Delivery is serial per app; a rejection ends this subscription. */ onEvent: (event: PlatformEvent) => void | Promise; /** Subscription errors. Only `snapshot_unavailable` leaves the subscription active. */ diff --git a/packages/platform/tests/types/client.types.ts b/packages/platform/tests/types/client.types.ts index 1fd226d2..456c4473 100644 --- a/packages/platform/tests/types/client.types.ts +++ b/packages/platform/tests/types/client.types.ts @@ -1,9 +1,12 @@ -import { Base44PlatformClient, type PlatformEvent, type Snapshot, type ToolCall } from "@base44/platform"; +import { Base44PlatformClient, type PlatformEvent, type PlatformEventMap, type PlatformSnapshot, type ToolCall, type GuardApproval, type Message, type StatusObject } from "@base44/platform"; const client = new Base44PlatformClient({ serverUrl: "https://example.test", getSessionToken: async () => "session-token" }); const builder = client.builder.init({ onError: error => { void error.code; } }); const subscription = builder.subscribe("a".repeat(24), { - onSnapshot(snapshot: Snapshot) { - const state: "ready" | "processing" | "error" | undefined = snapshot.status?.state; + onSnapshot(snapshot: PlatformSnapshot) { + const state: "ready" | "processing" | "error" | null | undefined = snapshot.status?.state; + const room: string = snapshot.room; + void room; + void snapshot.queue.items; void state; void snapshot.messages[0]?.additional_message_params?.plan_mode; }, @@ -19,7 +22,7 @@ const subscription = builder.subscribe("a".repeat(24), { void id; } if (event.type === "image.resolved") { - const status: "pending" | "completed" | "failed" | undefined = event.data.status; + const status: "pending" | "completed" | "failed" | null | undefined = event.data.status; void status; } // @ts-expect-error Payload must be narrowed by event.type. @@ -44,26 +47,38 @@ client.connect(); new Base44PlatformClient({ serverUrl: "https://example.test", refreshToken: async () => "token" }); const tool: ToolCall = { - display: { file_paths: ["src/App.tsx"] }, + name: "ask_clarifying_questions", arguments: { questions: [{ question: "Which layout?", options: [{ label: "Cards" }] }] }, user_input: { answers: [{ question_index: 0, selected_labels: ["Cards"] }] }, }; void tool; +const write: ToolCall = { name: "write_file", display: { file_paths: ["src/App.tsx"] } }; +void write; +function describe(call: ToolCall) { + if (call.name === "generate_image" && "results" in call) void call.results; + // @ts-expect-error Narrowing by name leaves only that tool's fields (and the catch-all call's). + if (call.name === "bash") void call.display; +} +void describe; const guarded: ToolCall = { + name: "update_entities", status: "waiting_for_user_input", waiting_on: { kind: "approval" }, - approval: { guard: "entity_rls_guard", reason: "RLS rules on entity 'Order' will be modified", details: { entity_name: "Order", changed_ops: ["read"] } }, + approval: { guard: "entity_rls_guard", details: { entity_name: "Order", changed_ops: ["read"] } }, }; void guarded; -const generatedMedia: ToolCall = { - results: { - placeholder_url: "/__generating__/hero.png", - status: "completed", - image_url: "https://images.example/hero.png", - }, -}; -void generatedMedia; -// @ts-expect-error Raw command text is not part of the public display. -void tool.display?.command; +const approval: GuardApproval = { guard: "some_new_guard" }; +void approval; +// @ts-expect-error Every tool call names its tool. +const unnamed: ToolCall = { status: "running" }; +void unnamed; // @ts-expect-error Raw arguments are never delivered. void tool.arguments_string; +const message: Message = { id: "m1", role: "assistant", content: "Done", tool_calls: [tool] }; +void message; +const status: StatusObject = { state: "processing", turn_id: "m1" }; +void status; +// @ts-expect-error Session events are handled by the client, not delivered to onEvent. +type NoSnapshot = PlatformEventMap["app.snapshot"]; +// @ts-expect-error Room notices arrive through onError. +type NoNotice = PlatformEventMap["room.access_denied"]; From 4a3c3cb2f4ca4a310f93b0a0d286af2deb63694d Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 19:58:47 +0300 Subject: [PATCH 03/32] ci(platform): fail when the generated event types drift from asyncapi.json Co-Authored-By: Claude Opus 5.5 --- .github/workflows/unit-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 820e887b..f474541c 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -32,5 +32,8 @@ jobs: - name: Run unit tests run: npm run test:unit -w @base44/sdk + - name: Check the Platform SDK event types match asyncapi.json + run: npm run check:events -w @base44/platform + - name: Test the Platform SDK run: npm test -w @base44/platform From bbec25662409e6a2fbb24a9f27c03de77c94c70f Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 20:03:57 +0300 Subject: [PATCH 04/32] docs: run the Mintlify docs pipeline for @base44/platform file-processing.js and copy-to-local-docs.js take --package-dir and read that package's docs folder and config. An optional mintlify-target.json sets the mintlify-docs path and nav dropdown; without it the SDK defaults apply, so @base44/sdk output is byte-identical. The platform package adds its config and create-docs / copy-docs-local scripts, landing the reference in developers/references/platform-sdk/docs. Co-Authored-By: Claude Opus 5.5 --- packages/platform/package.json | 7 +++++- .../appended-articles.json | 8 +++++++ .../category-map.json | 5 ++++ .../mintlify-target.json | 6 +++++ .../types-to-expose.json | 9 ++++++++ packages/platform/typedoc.mintlify.json | 17 ++++++++++++++ .../copy-to-local-docs.js | 20 ++++++++++------ .../file-processing/file-processing.js | 22 +++++++----------- .../mintlify-post-processing/package-paths.js | 23 +++++++++++++++++++ packages/sdk/writing-docs.md | 12 ++++++---- 10 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 packages/platform/scripts/mintlify-post-processing/appended-articles.json create mode 100644 packages/platform/scripts/mintlify-post-processing/category-map.json create mode 100644 packages/platform/scripts/mintlify-post-processing/mintlify-target.json create mode 100644 packages/platform/scripts/mintlify-post-processing/types-to-expose.json create mode 100644 packages/platform/typedoc.mintlify.json create mode 100644 packages/sdk/scripts/mintlify-post-processing/package-paths.js diff --git a/packages/platform/package.json b/packages/platform/package.json index d89422f4..4b42e101 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -28,7 +28,12 @@ "test:types": "tsc --noEmit -p tsconfig.type-tests.json", "test:unit": "vitest run", "test:package": "npm run build && node --test tests/package.test.mjs", - "prepublishOnly": "npm run build" + "prepublishOnly": "npm run build", + "create-docs": "npm run create-docs:generate && npm run create-docs:process", + "create-docs-local": "npm run create-docs && npm run copy-docs-local", + "copy-docs-local": "node ../sdk/scripts/mintlify-post-processing/copy-to-local-docs.js --package-dir .", + "create-docs:generate": "node -e \"fs.rmSync('docs',{recursive:true,force:true})\" && typedoc --options typedoc.mintlify.json", + "create-docs:process": "node ../sdk/scripts/mintlify-post-processing/file-processing/file-processing.js --package-dir ." }, "dependencies": { "socket.io-client": "^4.8.3" diff --git a/packages/platform/scripts/mintlify-post-processing/appended-articles.json b/packages/platform/scripts/mintlify-post-processing/appended-articles.json new file mode 100644 index 00000000..2643df32 --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/appended-articles.json @@ -0,0 +1,8 @@ +{ + "classes/Base44PlatformClient": ["interfaces/PlatformClientOptions"], + "interfaces/builder": [ + "interfaces/BuilderSession", + "interfaces/SubscriptionOptions", + "type-aliases/PlatformEvent" + ] +} diff --git a/packages/platform/scripts/mintlify-post-processing/category-map.json b/packages/platform/scripts/mintlify-post-processing/category-map.json new file mode 100644 index 00000000..dfe7cf10 --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/category-map.json @@ -0,0 +1,5 @@ +{ + "classes": "Client", + "interfaces": "Modules", + "type-aliases": "Modules" +} diff --git a/packages/platform/scripts/mintlify-post-processing/mintlify-target.json b/packages/platform/scripts/mintlify-post-processing/mintlify-target.json new file mode 100644 index 00000000..1ce7a05c --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/mintlify-target.json @@ -0,0 +1,6 @@ +{ + "path": "developers/references/platform-sdk/docs", + "dropdown": "Platform SDK", + "group": "Platform SDK Reference", + "icon": "brackets-curly" +} diff --git a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json new file mode 100644 index 00000000..e31ee38a --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json @@ -0,0 +1,9 @@ +[ + "Base44PlatformClient", + "BuilderModule", + "BuilderSession", + "PlatformClientOptions", + "PlatformEvent", + "PlatformSocketError", + "SubscriptionOptions" +] diff --git a/packages/platform/typedoc.mintlify.json b/packages/platform/typedoc.mintlify.json new file mode 100644 index 00000000..2b27ea02 --- /dev/null +++ b/packages/platform/typedoc.mintlify.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "extends": ["./typedoc.json"], + "out": "docs/content", + "plugin": [ + "typedoc-plugin-markdown", + "../sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js" + ], + "fileExtension": ".mdx", + "gitRevision": "main", + "sort": ["source-order"], + "entryFileName": "README.mdx", + "maxTypeConversionDepth": 2, + "hideBreadcrumbs": true, + "disableSources": true, + "hidePageTitle": true +} diff --git a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js index 4b857875..9837a3b1 100644 --- a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js +++ b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js @@ -19,11 +19,13 @@ import fs from "fs"; import path from "path"; +import { packagePaths } from "./package-paths.js"; console.debug = () => {}; // Disable debug logging. Comment this out to enable debug logging. -const DOCS_SOURCE_PATH = path.join(import.meta.dirname, "../../docs/content"); -const CATEGORY_MAP_PATH = path.join(import.meta.dirname, "./category-map.json"); +const { docsDir, configDir, target: TARGET } = packagePaths(); +const DOCS_SOURCE_PATH = path.join(docsDir, "content"); +const CATEGORY_MAP_PATH = path.join(configDir, "category-map.json"); // Default: assume mintlify-docs is a sibling directory to javascript-sdk const SDK_ROOT = path.join(import.meta.dirname, "../.."); @@ -56,6 +58,7 @@ Usage: Options: --target, -t Path to the mintlify-docs repo. Defaults to ../mintlify-docs (sibling directory) + --package-dir Package to copy the docs of. Defaults to packages/sdk. --help, -h Show this help message Examples: @@ -71,8 +74,8 @@ Examples: return { target }; } -// Target location within mintlify-docs for SDK reference docs -const SDK_DOCS_TARGET_PATH = "developers/references/sdk/docs"; +// Target location within mintlify-docs for the reference docs +const SDK_DOCS_TARGET_PATH = TARGET.path; function scanSdkDocs(sdkDocsDir) { const result = {}; @@ -177,11 +180,14 @@ function updateDocsJson(repoDir, sdkFiles) { for (const tab of tabs) { const sdkAnchor = - tab.dropdowns?.find((d) => d.dropdown === "SDK") ?? - tab.anchors?.find((a) => a.anchor === "SDK"); + tab.dropdowns?.find((d) => d.dropdown === TARGET.dropdown) ?? + tab.anchors?.find((a) => a.anchor === TARGET.dropdown); if (!sdkAnchor?.groups) continue; - const sdkRefIndex = sdkAnchor.groups.findIndex(groupReferencesSdkDocs); + let sdkRefIndex = sdkAnchor.groups.findIndex(groupReferencesSdkDocs); + if (sdkRefIndex === -1 && TARGET.group) { + sdkRefIndex = sdkAnchor.groups.push({ group: TARGET.group, icon: TARGET.icon, pages: [] }) - 1; + } if (sdkRefIndex === -1) continue; const existing = sdkAnchor.groups[sdkRefIndex]; diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 43a7dcb9..0ffbbdaa 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -14,28 +14,22 @@ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; +import { packagePaths } from "../package-paths.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const DOCS_DIR = path.join(__dirname, "..", "..", "..", "docs"); +const { docsDir: DOCS_DIR, configDir: CONFIG_DIR } = packagePaths(); const CONTENT_DIR = path.join(DOCS_DIR, "content"); const LINKED_TYPES_FILE = path.join(CONTENT_DIR, ".linked-types.json"); const TEMPLATE_PATH = path.join(__dirname, "docs-json-template.json"); const STYLING_CSS_PATH = path.join(__dirname, "styling.css"); -const CATEGORY_MAP_PATH = path.join(__dirname, "../category-map.json"); -const TYPES_TO_EXPOSE_PATH = path.join(__dirname, "..", "types-to-expose.json"); -const TYPES_TO_DELETE_PATH = path.join(__dirname, "..", "types-to-delete-after-processing.json"); -const APPENDED_ARTICLES_PATH = path.join( - __dirname, - "../appended-articles.json" -); -const METHOD_ORDER_PATH = path.join(__dirname, "..", "method-order.json"); -const OVERLOAD_PRESENTATION_PATH = path.join( - __dirname, - "..", - "overload-presentation.json" -); +const CATEGORY_MAP_PATH = path.join(CONFIG_DIR, "category-map.json"); +const TYPES_TO_EXPOSE_PATH = path.join(CONFIG_DIR, "types-to-expose.json"); +const TYPES_TO_DELETE_PATH = path.join(CONFIG_DIR, "types-to-delete-after-processing.json"); +const APPENDED_ARTICLES_PATH = path.join(CONFIG_DIR, "appended-articles.json"); +const METHOD_ORDER_PATH = path.join(CONFIG_DIR, "method-order.json"); +const OVERLOAD_PRESENTATION_PATH = path.join(CONFIG_DIR, "overload-presentation.json"); // Controlled via env var so we can re-enable Panel injection when needed. const PANELS_ENABLED = process.env.MINTLIFY_INCLUDE_PANELS === "true"; diff --git a/packages/sdk/scripts/mintlify-post-processing/package-paths.js b/packages/sdk/scripts/mintlify-post-processing/package-paths.js new file mode 100644 index 00000000..af5d7c39 --- /dev/null +++ b/packages/sdk/scripts/mintlify-post-processing/package-paths.js @@ -0,0 +1,23 @@ +import fs from "fs"; +import path from "path"; + +const SDK_DIR = path.join(import.meta.dirname, "..", ".."); + +/** + * Where the pipeline reads and writes for one package. Defaults to @base44/sdk; pass + * `--package-dir ` to document another package, whose config files live under + * `/scripts/mintlify-post-processing/` like this package's. + */ +export function packagePaths(argv = process.argv.slice(2)) { + const index = argv.indexOf("--package-dir"); + const packageDir = index === -1 ? SDK_DIR : path.resolve(argv[index + 1]); + const configDir = path.join(packageDir, "scripts", "mintlify-post-processing"); + const targetFile = path.join(configDir, "mintlify-target.json"); + // Where the reference lands in mintlify-docs. `group` creates the nav group when it is missing. + const target = { + path: "developers/references/sdk/docs", + dropdown: "SDK", + ...(fs.existsSync(targetFile) ? JSON.parse(fs.readFileSync(targetFile, "utf8")) : {}), + }; + return { packageDir, docsDir: path.join(packageDir, "docs"), configDir, target }; +} diff --git a/packages/sdk/writing-docs.md b/packages/sdk/writing-docs.md index ed194756..7115f1eb 100644 --- a/packages/sdk/writing-docs.md +++ b/packages/sdk/writing-docs.md @@ -57,7 +57,11 @@ After generating and reviewing the docs, you can push them to the `base44/mintli 1. Open the [docs repo](https://github.com/base44-dev/mintlify-docs) and created a PR for your branch. 1. Preview your docs using the [Mintlify dashboard](https://dashboard.mintlify.com/base44/base44?section=previews). -The Platform SDK (`packages/platform`) is documented separately. Add field-level -JSDoc to all exports in `packages/platform/src`, update `packages/platform/README.md`, -and run `npm run docs -w @base44/platform`. TypeDoc treats missing public -documentation as an error and emits its reference under `packages/platform/docs`. +## Platform SDK docs + +The same pipeline documents the Platform SDK (`packages/platform`). Pass `--package-dir ` to `file-processing.js` and `copy-to-local-docs.js` to run them for another package. They then read that package's `docs` folder and its config files under `/scripts/mintlify-post-processing/`. A `mintlify-target.json` there sets where the reference lands in mintlify-docs and which nav dropdown it belongs to. Without one, the scripts keep the SDK's defaults. + +From `packages/platform`: +* `npm run docs` validates that every export has JSDoc. Its output in `docs` is not published. +* `npm run create-docs` generates the Mintlify reference into `docs/content`. +* `npm run copy-docs-local -- --target ` copies it into `developers/references/platform-sdk/docs` and updates the **Platform SDK** dropdown in `docs.json`. From a77eb5e19fb04f5a33bb54b86dbbc37a0de13bf0 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 20:03:57 +0300 Subject: [PATCH 05/32] docs(platform): rewrite the JSDoc in the app SDK's voice Adds examples to the client, its options, the builder module and session methods, errors and events, and fixes serverUrl: it is the session's socket_url from POST /api/service/socket-sessions. The README describes the generated event types and the sync and regen workflow. Co-Authored-By: Claude Opus 5.5 --- packages/platform/README.md | 92 +++++------ packages/platform/examples/client.ts | 4 +- packages/platform/src/client.ts | 31 +++- packages/platform/src/client.types.ts | 43 ++++- packages/platform/src/errors.ts | 28 +++- packages/platform/src/errors.types.ts | 18 ++- .../src/modules/builder.events.types.ts | 4 +- .../platform/src/modules/builder.types.ts | 151 +++++++++++++++--- 8 files changed, 284 insertions(+), 87 deletions(-) diff --git a/packages/platform/README.md b/packages/platform/README.md index 4450e697..16b34339 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -13,7 +13,7 @@ The workspace must have white-label sockets enabled; this SDK does not open sess import { Base44PlatformClient } from "@base44/platform"; const client = new Base44PlatformClient({ - serverUrl: "https://base44.app", + serverUrl: socketUrl, // the session's socket_url, from POST /api/service/socket-sessions async getSessionToken() { const response = await fetch("/api/builder-socket-session", { method: "POST" }); if (!response.ok) throw new Error("Session request failed"); @@ -39,7 +39,7 @@ builder.close(); `/api/builder-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 only the -`session_token` to the browser. Never pass the workspace key or any other server +`session_token` and `socket_url` to the browser. Pass `socket_url` as `serverUrl`. Never pass the workspace key or any other server credential to this client. Subscribe only to apps on the session's allowlist; your backend adds or removes apps with `PUT`/`DELETE /api/service/socket-sessions/{session_id}/rooms/{app_id}`. @@ -100,54 +100,46 @@ their exceptions are isolated so they cannot interrupt another app's delivery. - `BuilderSession`: `connect()`, `subscribe(appId, options)`, `close()`. - `SubscriptionOptions`: required `onSnapshot`, `onEvent` and `onError`. - `PlatformSubscription`: read-only `appId`, `active`, and `unsubscribe()`. -- Event/message models live in `modules/builder.events.types.ts`; error codes in `errors.types.ts`. - -Every exported shape and field has JSDoc. Run `npm run docs` for -validated reference pages in `docs`. `PlatformEvent` is a -**discriminated union**: switch on `event.type` to narrow `event.data`. Each delivery -contains `type`, `appId` and `data`. On the wire every event is `{room, data}`; the SDK -routes by `room` and validates the envelope, while payload schemas and private-field -filtering are the server's. Omitted keys mean unchanged and explicit null clears. -Events carrying `branch_id` belong to that branch; null or absent is main. Unknown -event names are not forwarded. - -| Event | Data contract | -| --- | --- | -| `message.updated` | `MessageUpdated`: the whole `message`, optional `conversation_id`. Replace the message with the same `id`. | -| `message.removed` | `MessageRemoved`: `message_id`, optional `conversation_id`. Sent when a message is deleted or hidden; an unknown ID is a no-op. | -| `app.status_changed` | `AppStatusChanged`: `status` (`AppStatus`), null clears it. | -| `preview.reload_requested` | Reload the app preview. | -| `preview.navigation_requested` | `PreviewNavigationRequested`: `path`, and `force` to navigate even when the user moved away. | -| `queue.updated` | `QueueUpdated`: `items`, `is_paused`, optional `processed_item_id`. Replaces the entire queue. | -| `task.progressed` | `TaskProgressed`: optional `tool_call_id`, `message_id`, `event_type`, numeric `progress`. | -| `image.resolved` | `ImageResolved`: `placeholder_url`, `status` (`pending`, `completed`, `failed`), nullable `image_url`. Replace the placeholder wherever it appears. | -| `conversation.changed` | The conversation was rewritten. On main, a fresh snapshot follows. | -| `files.changed`, `branch.deleted`, `repository.changed`, `pull_request.changed` | Re-read signals with no content. | - -`ChatMessage` has optional `id`, `role` (`user`/`assistant`), text/null `content`, -`tool_calls`, timestamp-only `metadata.created_date`, `checkpoint_id`, and scalar -`additional_message_params` (`client_creation_id`, `plan_mode`, `plan_approved`, -`skip_ai_response`, `system_message_type`). `ToolCall` has optional `id`, `name`, -`status`, `requires_user_input`, `auto_approved`, `mutation_applied` and -`waiting_on.kind` (`approval`/`choice`/`input`). Each builder tool and guard declares -which of its parts are public; a tool with no declaration shows only these fields. - -| Field | Public contract | -| --- | --- | -| `arguments` | One of `ToolQuestionArguments`, `ToolSecretArguments`, `ToolPackageArguments`, `ToolPlanArguments`, `ToolPrdArguments` (`generate_prd`, the plan in plan mode) or `ToolMediaArguments`, narrowed by the tool's `name`. Absent for other tools and partial streaming arguments. | -| `display` | File activity has `file_paths` and optional `content_empty`; execution activity has `summary` and `writes_entities`; entity activity has `entity_name` and `record_count`. | -| `user_input` | `ToolQuestionInput` only, for clarifying-question answers (`selected_label` for single-select, `selected_labels` for multi-select). Secret-form values are never exposed. | -| `results` | Once the call is not waiting on a guard: `ToolMediaResult` for image generation, or a URL string for game images and backgrounds (the placeholder `image.resolved` replaces) and videos (the finished video). | -| `approval` | While a call waits on a safety guard: `ToolGuardApproval` with `guard`, and `reason` and `details` where the guard declares them (shell-command approvals carry only `guard`). | - -In plan mode the message `content` beside a `generate_prd` call can be empty; render the -plan from its arguments. Approving a plan, answering a question and approving a -guarded call are HTTP actions of your backend. `Snapshot` contains `room`, `status` -(`AppStatus`: optional `state` of `ready`/`processing`/`error`, nullable -`last_updated_date`, and nullable `turn_id`, the user message that started the turn), -`messages`, and `queue` (`SnapshotQueue`: the main branch's `items` and `is_paused`; -absent from older servers). Dates remain wire strings. Structural filtering -does not promise redaction of generated prose or user content. +- `PlatformEvent`, `PlatformEventMap`, `PlatformSnapshot`, `ToolCall`, `GuardApproval` + (`modules/builder.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 +`event.data`. Each delivery contains `type`, `appId` and `data`. On the wire every +event is `{room, data}`; the SDK routes by `room` and validates the envelope, while +payload schemas and private-field filtering are the server's. Omitted keys mean +unchanged and explicit null clears. Events carrying `branch_id` belong to that branch; +null or absent is main. Unknown event names are not forwarded. `PlatformSnapshot` is +the service's `Snapshot` plus the app's `room`. Structural filtering does not promise +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 +`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. + +To pick up a protocol change: + +```sh +# Production serves only beta and GA events, so point at a dev backend while events are alpha. +npm run sync:asyncapi -w @base44/platform -- http://localhost:8000/api/asyncapi.json +npm run gen:events -w @base44/platform +``` + +`sync:asyncapi` also reads `ASYNCAPI_URL`, defaults to production, and refuses a +document with no messages. CI runs `npm run check:events` and fails when the +generated file differs from what `asyncapi.json` produces. + +## Reference docs + +Every exported shape and field has JSDoc. `npm run docs` validates it into `docs`. +`npm run create-docs` runs the app SDK's TypeDoc and Mintlify pipeline for this package, +and `npm run copy-docs-local -- --target ` copies the result into +`developers/references/platform-sdk/docs`. The pipeline's config for this package lives +in `scripts/mintlify-post-processing/`. `PlatformSocketError` extends `Error` with `code` and optional `appId`. Original exceptions, payloads and credentials are never attached. diff --git a/packages/platform/examples/client.ts b/packages/platform/examples/client.ts index 2ebd9674..e859b57d 100644 --- a/packages/platform/examples/client.ts +++ b/packages/platform/examples/client.ts @@ -1,7 +1,9 @@ import { Base44PlatformClient, type PlatformEvent } from "@base44/platform"; +declare const socketUrl: string; + const client = new Base44PlatformClient({ - serverUrl: "https://base44.app", + serverUrl: socketUrl, // the session's socket_url, from POST /api/service/socket-sessions // Your backend opens the session with its workspace key and returns only the session token. async getSessionToken() { const response = await fetch("/api/builder-socket-session", { method: "POST" }); diff --git a/packages/platform/src/client.ts b/packages/platform/src/client.ts index ee9888d7..61b02c0b 100644 --- a/packages/platform/src/client.ts +++ b/packages/platform/src/client.ts @@ -2,12 +2,37 @@ import type { PlatformClientOptions } from "./client.types.js"; import { createBuilder } from "./modules/builder.js"; import type { BuilderModule } from "./modules/builder.types.js"; -/** Browser platform client. Construction creates no sockets, timers or network requests. */ +/** + * Browser client for the Base44 platform. + * + * Use it in a white-label app editor to follow what the AI chat does in your users' apps. Creating + * the client opens no sockets, starts no timers and sends no requests. Each module sets up its own + * resources when you initialize it. + * + * @example + * ```typescript + * // Create a client + * import { Base44PlatformClient } from '@base44/platform'; + * + * const client = new Base44PlatformClient({ + * serverUrl: socketUrl, + * getSessionToken: () => fetchSessionToken(), + * }); + * const builder = client.builder.init({ onError: (error) => console.error(error.code) }); + * ``` + */ export class Base44PlatformClient { - /** Lazy builder subscriptions with independent session lifecycles. */ + /** + * Live builder updates. Call [`init()`](/developers/references/platform-sdk/docs/interfaces/builder#init) to create a session. + */ readonly builder: BuilderModule; - /** Configure shared service/auth settings; each module initializes its own resources. */ + /** + * Creates a platform client. + * + * @param options - Client options. + * @throws {TypeError} When `serverUrl` isn't an HTTP or HTTPS origin. + */ constructor(options: PlatformClientOptions) { const url = new URL(options.serverUrl); if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== "/") { diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client.types.ts index 09c6b939..0b5883f3 100644 --- a/packages/platform/src/client.types.ts +++ b/packages/platform/src/client.types.ts @@ -1,12 +1,43 @@ -/** Shared configuration for browser platform modules. Never supply an API key. */ +/** + * Options for creating a {@link Base44PlatformClient}. + * + * The client runs in the browser and never sees your workspace API key. Your server opens a socket + * session with the key and hands the browser only the session's token and socket URL. + */ export interface PlatformClientOptions { - /** Origin of the platform service, e.g. https://base44.app. No path/query/credentials. */ + /** + * Origin of the platform socket, such as `https://app.base44.com`. + * + * Use the `socket_url` your server receives from `POST /api/service/socket-sessions`. It must be + * an origin with no path, query, fragment, or credentials. + */ serverUrl: string; /** - * Return a socket-session token from your backend, which opens the session with its - * workspace key (`POST /api/service/socket-sessions`) for the apps this page watches. - * Called when a builder session first connects, and again only after the server - * rejects or expires the current session. Sessions last one hour. + * Returns a socket session token from your server. + * + * Your server opens a session with its workspace API key through + * `POST /api/service/socket-sessions`, for the apps this page shows, and returns the + * `session_token`. The client calls this when a builder session first connects, and again only + * after Base44 rejects or expires the current token. Sessions last one hour. Open a new session + * on every call instead of returning a cached token. + * + * It must settle within 20 seconds and return a non-empty token, or the session reports + * `session_unavailable`. Never return a workspace API key or any other server credential. + * + * @returns The session token, or a promise resolving to it. + * + * @example + * ```typescript + * // Fetch a session token from your server + * const client = new Base44PlatformClient({ + * serverUrl: socketUrl, + * async getSessionToken() { + * const response = await fetch('/api/builder-session', { method: 'POST' }); + * const { session_token } = await response.json(); + * return session_token; + * }, + * }); + * ``` */ getSessionToken: () => string | Promise; } diff --git a/packages/platform/src/errors.ts b/packages/platform/src/errors.ts index 6d589c26..367d6877 100644 --- a/packages/platform/src/errors.ts +++ b/packages/platform/src/errors.ts @@ -1,13 +1,33 @@ import type { PlatformSocketErrorCode } from "./errors.types.js"; -/** Sanitized failure. Original session-token provider, handler and server exceptions are not retained. */ +/** + * An error from a builder session or subscription. + * + * Check `code` to decide what to do. The error never carries the session token, a callback's + * exception, or the server's exception text. + * + * @example + * ```typescript + * // Handle a session error + * const builder = client.builder.init({ + * onError(error) { + * if (error.code === 'session_revoked') showSignedOut(); + * }, + * }); + * ``` + */ export class PlatformSocketError extends Error { - /** Stable machine-readable category. */ + /** Machine-readable error code. */ readonly code: PlatformSocketErrorCode; - /** Associated app, when the server identifies a valid room. */ + /** ID of the app the error is about, when it concerns one subscription. */ readonly appId?: string; - /** Create a sanitized error with no credential-bearing cause or payload. */ + /** + * Creates an error. + * + * @param code - Error code. + * @param appId - ID of the app the error is about. + */ constructor(code: PlatformSocketErrorCode, appId?: string) { super(`Platform socket: ${code}`); this.name = "PlatformSocketError"; diff --git a/packages/platform/src/errors.types.ts b/packages/platform/src/errors.types.ts index 6173bf07..9b6cbd87 100644 --- a/packages/platform/src/errors.types.ts +++ b/packages/platform/src/errors.types.ts @@ -1,4 +1,20 @@ -/** Server refusals and client transport/processing failures. */ +/** + * Codes a {@link PlatformSocketError} reports. + * + * - `connection_denied`: Base44 refused the session even with a fresh token. The workspace, key or session isn't usable. + * - `connection_failed`: The socket couldn't connect or reconnect. + * - `session_unavailable`: `getSessionToken` failed, timed out or returned no token. + * - `session_revoked`: Your server or an admin ended the session. Don't reconnect. + * - `session_replaced`: Another socket took over the session. Stop, or call `connect()` to take it back. + * - `access_denied`: The app isn't on the session's allowlist, or the socket joins too often. + * - `access_revoked`: The app left the session's allowlist or workspace. Don't subscribe again. + * - `snapshot_unavailable`: The subscription is active but its snapshot failed. Events still arrive. + * - `subscription_limit`: The session already has eight subscriptions. + * - `delivery_overflow`: 1,000 deliveries queued for the app. Subscribe again for a fresh snapshot. + * - `protocol_error`: A frame didn't match the protocol. + * - `handler_failed`: Your `onSnapshot` or `onEvent` callback threw or rejected. + * - `client_closed`: The session was closed. + */ export type PlatformSocketErrorCode = | "connection_denied" | "connection_failed" | "session_unavailable" | "session_revoked" | "session_replaced" | "access_denied" | "access_revoked" | "snapshot_unavailable" | "subscription_limit" diff --git a/packages/platform/src/modules/builder.events.types.ts b/packages/platform/src/modules/builder.events.types.ts index 1da396bf..09630209 100644 --- a/packages/platform/src/modules/builder.events.types.ts +++ b/packages/platform/src/modules/builder.events.types.ts @@ -15,10 +15,12 @@ export type PlatformEventMap = { * One live event for a subscribed app. * * Check `type` to narrow `data` to that event's payload. A key missing from `data` means the value - * is unchanged, and an explicit `null` clears it. + * is unchanged, and an explicit `null` clears it. For every event and its payload, see the + * [events reference](/developers/references/platform-sdk/live-updates/events). * * @example * ```typescript + * // Narrow an event by type * function onEvent(event: PlatformEvent) { * if (event.type === "message.updated") { * const { message } = event.data; diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index b10d221a..76effbd0 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -1,52 +1,161 @@ import type { PlatformEvent, PlatformSnapshot } from "./builder.events.types.js"; import type { PlatformSocketError } from "../errors.js"; -/** Options for an independent builder socket session. */ +/** Options for {@linkcode BuilderModule.init | init()}. */ export interface BuilderInitOptions { - /** Connection-level errors, sanitized to exclude tokens and server exception text. */ + /** + * Called on errors that affect the whole session, such as a failed connection or an ended session. + * Errors for one app go to that subscription's `onError` instead. See + * [connection errors](/developers/references/platform-sdk/live-updates/errors#connection-errors). + */ onError: (error: PlatformSocketError) => void; } -/** Lazy builder module; no socket exists until init is called. */ +/** + * Builder module for following the AI chat in your users' apps live. + * + * Each session holds one socket and up to eight app subscriptions. For each app you get a + * snapshot of its chat, then every change as it happens: + * - New and updated chat messages, with the AI's tool calls. + * - The AI's status, from processing to ready. + * - Preview reloads and navigation, the prompt queue, and generated images. + * + * The module does nothing until you call {@linkcode init | init()}. + */ export interface BuilderModule { - /** Create an independent session without connecting. Call connect on the returned session. */ + /** + * Creates a builder session without connecting it. + * + * Call {@linkcode BuilderSession.connect | connect()} on the returned session to open the socket. + * Each session is independent, with its own socket, token and subscriptions. + * + * @param options - Session options. + * @returns The new session. + * + * @example + * ```typescript + * // Create and connect a session + * const builder = client.builder.init({ + * onError(error) { + * console.error('Builder session error', error.code); + * }, + * }); + * await builder.connect(); + * ``` + */ init(options: BuilderInitOptions): BuilderSession; } -/** One builder socket session. Owns its subscriptions and connection lifecycle. */ +/** One builder session. It owns its socket, its subscriptions and its reconnection. */ export interface BuilderSession { - /** Connect with the session token; resolves on CONNECT, before snapshots arrive. - * Transport loss retries five times and rejoins active subscriptions, each with a fresh - * snapshot. An expired or rejected session is replaced once through `getSessionToken`. - * Call again after a failure or `session_replaced`. Concurrent calls share one attempt. + /** + * Connects the session's socket. + * + * Resolves once the socket connects, before any snapshot arrives. If the connection drops, the + * session retries five times and rejoins every active subscription, each with a fresh snapshot. + * An expired or rejected token is replaced once through `getSessionToken`. Concurrent calls share + * one attempt. Call it again after a failure or a `session_replaced` error. + * + * @returns Promise that resolves when the socket connects. + * + * @example + * ```typescript + * // Connect and handle failure + * try { + * await builder.connect(); + * } catch (error) { + * console.error('Could not connect', error.code); + * } + * ``` */ connect(): Promise; - /** Subscribe before or after connecting. One subscription per app, up to eight per session, - * and each app must be on the session's allowlist. Callbacks run serially per app. + /** + * Subscribes to one app's chat. + * + * You can subscribe before or after connecting. Each app can have one subscription per session, + * and a session can have up to eight. The app must be on the session's allowlist. The + * subscription first receives a snapshot of the app, then each live event. + * + * Callbacks for an app run one at a time, in order, and the session waits for a returned promise + * before the next one. A slow app doesn't hold up the others. If more than 1,000 deliveries wait + * for one app, its subscription ends with `delivery_overflow`. + * + * @param appId - ID of the app to follow, 24 lowercase hexadecimal characters. + * @param options - Callbacks for the app's snapshots, events and errors. + * @returns The subscription. + * @throws {TypeError} When `appId` isn't a valid app ID or the app already has a subscription. + * @throws {PlatformSocketError} With `subscription_limit` when the session has eight subscriptions. + * + * @example + * ```typescript + * // Follow an app + * const subscription = builder.subscribe(appId, { + * onSnapshot(snapshot) { + * renderChat(snapshot.messages); + * }, + * onEvent(event) { + * if (event.type === 'message.updated') upsertMessage(event.data.message); + * }, + * onError(error) { + * console.error(`App ${error.appId}`, error.code); + * }, + * }); + * ``` */ subscribe(appId: string, options: SubscriptionOptions): PlatformSubscription; - /** Stop this session, its subscriptions and reconnection. Idempotent and terminal. */ + /** + * Closes the session. + * + * Ends every subscription, stops reconnecting and disconnects the socket. Calling it again does + * nothing. A closed session can't be reused, so call [`init()`](/developers/references/platform-sdk/docs/interfaces/builder#init) + * for a new one. + * + * @example + * ```typescript + * // Close a session + * builder.close(); + * ``` + */ close(): void; } -/** One app subscription; at most eight may be active per builder session. */ +/** Callbacks for one app subscription. */ export interface SubscriptionOptions { - /** Replace this app's state. Arrives after every join and rejoin, and after a main-conversation - * rewrite (undo, restore, sync). Live events follow it; it holds only the last 50 messages. + /** + * Called with the app's current state. Replace everything you hold for the app with it. + * + * Arrives after every join and rejoin, and after the main conversation is rewritten by an undo, + * restore or sync. Live events follow it. It holds only the last 50 messages, so merge by message + * ID to keep older history. */ onSnapshot: (snapshot: PlatformSnapshot) => void | Promise; - /** Apply each live event. Delivery is serial per app; a rejection ends this subscription. */ + /** + * Called with each live event. Events for an app arrive one at a time, in order. If the callback + * throws or rejects, the subscription ends with `handler_failed`. For every event and its payload, + * see the [events reference](/developers/references/platform-sdk/live-updates/events). + */ onEvent: (event: PlatformEvent) => void | Promise; - /** Subscription errors. Only `snapshot_unavailable` leaves the subscription active. */ + /** + * Called on errors for this app. Every error ends the subscription except `snapshot_unavailable`. + * See [app errors](/developers/references/platform-sdk/live-updates/errors#app-errors). + */ onError: (error: PlatformSocketError) => void; } -/** One app subscription's lifetime. */ +/** One app subscription. */ export interface PlatformSubscription { - /** App identifier. */ + /** ID of the app. */ readonly appId: string; - /** True while subscribed; false after an error or explicit unsubscribe. */ + /** Whether the subscription still receives updates. It turns false after an error or {@linkcode unsubscribe | unsubscribe()}. */ readonly active: boolean; - /** Stop this app's delivery and release its subscription slot. Idempotent. */ + /** + * Stops updates for the app and frees its subscription slot. Calling it again does nothing. + * + * @example + * ```typescript + * // Stop following an app + * subscription.unsubscribe(); + * ``` + */ unsubscribe(): void; } From 09ff322d2d4774ca3002f44a652ce4430bc8581c Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 20:05:26 +0300 Subject: [PATCH 06/32] docs: order reference nav groups by the category map Lets the platform reference list its Client group first. The SDK's map already lists its groups in the old fixed order, so its nav is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../mintlify-post-processing/copy-to-local-docs.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js index 9837a3b1..af0523d3 100644 --- a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js +++ b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js @@ -130,14 +130,10 @@ function updateDocsJson(repoDir, sdkFiles) { }; const p = (kind, file) => `${prefix}${basePath}/${kind}/${file}`; - if (sdkFiles.functions?.length > 0 && categoryMap.functions) - addToGroup(categoryMap.functions, sdkFiles.functions.map((f) => p("functions", f))); - if (sdkFiles.interfaces?.length > 0 && categoryMap.interfaces) - addToGroup(categoryMap.interfaces, sdkFiles.interfaces.map((f) => p("interfaces", f))); - if (sdkFiles.classes?.length > 0 && categoryMap.classes) - addToGroup(categoryMap.classes, sdkFiles.classes.map((f) => p("classes", f))); - if (sdkFiles["type-aliases"]?.length > 0 && categoryMap["type-aliases"]) - addToGroup(categoryMap["type-aliases"], sdkFiles["type-aliases"].map((f) => p("type-aliases", f))); + // Groups follow the category map's key order. + for (const [kind, group] of Object.entries(categoryMap)) { + if (sdkFiles[kind]?.length > 0) addToGroup(group, sdkFiles[kind].map((f) => p(kind, f))); + } return Array.from(groupMap.entries()).map(([group, pages]) => ({ group, From 2c7f2923638932b371201db76dbb5a496884fd21 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 21:36:20 +0300 Subject: [PATCH 07/32] docs(platform): shape the reference like the app SDK's - Nav: a target's `groups` moves single pages, so PlatformSocketError sits in Errors instead of Client. - Section names (types with their own methods, Type Definitions groups) move from file-processing.js to each package's page-sections.json. BuilderSession becomes "Session methods" and PlatformEvent a Type Definition, like actors. - Client and subscription options render only as nested fields under Parameters, without their own appended sections. - connect() documents what it rejects with. - Each PlatformSocketErrorCode value has its own comment, rendered as a field on the PlatformSocketError page. The @base44/sdk docs output and nav are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../appended-articles.json | 5 +- .../mintlify-target.json | 5 +- .../page-sections.json | 6 +++ .../types-to-expose.json | 3 +- packages/platform/src/client.ts | 6 ++- packages/platform/src/client.types.ts | 13 ----- packages/platform/src/errors.ts | 2 +- packages/platform/src/errors.types.ts | 47 +++++++++++-------- .../platform/src/modules/builder.types.ts | 8 ++-- .../copy-to-local-docs.js | 11 +++-- .../file-processing/file-processing.js | 47 +++---------------- .../page-sections.json | 33 +++++++++++++ .../typedoc-plugin/typedoc-mintlify-plugin.js | 15 ++++++ 13 files changed, 115 insertions(+), 86 deletions(-) create mode 100644 packages/platform/scripts/mintlify-post-processing/page-sections.json create mode 100644 packages/sdk/scripts/mintlify-post-processing/page-sections.json diff --git a/packages/platform/scripts/mintlify-post-processing/appended-articles.json b/packages/platform/scripts/mintlify-post-processing/appended-articles.json index 2643df32..a8dbda69 100644 --- a/packages/platform/scripts/mintlify-post-processing/appended-articles.json +++ b/packages/platform/scripts/mintlify-post-processing/appended-articles.json @@ -1,8 +1,9 @@ { - "classes/Base44PlatformClient": ["interfaces/PlatformClientOptions"], "interfaces/builder": [ "interfaces/BuilderSession", - "interfaces/SubscriptionOptions", "type-aliases/PlatformEvent" + ], + "classes/PlatformSocketError": [ + "type-aliases/PlatformSocketErrorCode" ] } diff --git a/packages/platform/scripts/mintlify-post-processing/mintlify-target.json b/packages/platform/scripts/mintlify-post-processing/mintlify-target.json index 1ce7a05c..03f5511e 100644 --- a/packages/platform/scripts/mintlify-post-processing/mintlify-target.json +++ b/packages/platform/scripts/mintlify-post-processing/mintlify-target.json @@ -2,5 +2,8 @@ "path": "developers/references/platform-sdk/docs", "dropdown": "Platform SDK", "group": "Platform SDK Reference", - "icon": "brackets-curly" + "icon": "brackets-curly", + "groups": { + "classes/PlatformSocketError": "Errors" + } } diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json new file mode 100644 index 00000000..c04c7eb0 --- /dev/null +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -0,0 +1,6 @@ +{ + "typesWithOwnMethods": { + "BuilderSession": "Session methods" + }, + "typeDefinitionGroups": [["PlatformEvent"], ["PlatformSocketErrorCode"]] +} diff --git a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json index e31ee38a..132e650f 100644 --- a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json +++ b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json @@ -2,8 +2,7 @@ "Base44PlatformClient", "BuilderModule", "BuilderSession", - "PlatformClientOptions", "PlatformEvent", "PlatformSocketError", - "SubscriptionOptions" + "PlatformSocketErrorCode" ] diff --git a/packages/platform/src/client.ts b/packages/platform/src/client.ts index 61b02c0b..ad10d8af 100644 --- a/packages/platform/src/client.ts +++ b/packages/platform/src/client.ts @@ -16,7 +16,11 @@ import type { BuilderModule } from "./modules/builder.types.js"; * * const client = new Base44PlatformClient({ * serverUrl: socketUrl, - * getSessionToken: () => fetchSessionToken(), + * async getSessionToken() { + * const response = await fetch('/api/builder-session', { method: 'POST' }); + * const { session_token } = await response.json(); + * return session_token; + * }, * }); * const builder = client.builder.init({ onError: (error) => console.error(error.code) }); * ``` diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client.types.ts index 0b5883f3..bddf3477 100644 --- a/packages/platform/src/client.types.ts +++ b/packages/platform/src/client.types.ts @@ -25,19 +25,6 @@ export interface PlatformClientOptions { * `session_unavailable`. Never return a workspace API key or any other server credential. * * @returns The session token, or a promise resolving to it. - * - * @example - * ```typescript - * // Fetch a session token from your server - * const client = new Base44PlatformClient({ - * serverUrl: socketUrl, - * async getSessionToken() { - * const response = await fetch('/api/builder-session', { method: 'POST' }); - * const { session_token } = await response.json(); - * return session_token; - * }, - * }); - * ``` */ getSessionToken: () => string | Promise; } diff --git a/packages/platform/src/errors.ts b/packages/platform/src/errors.ts index 367d6877..58e35cd9 100644 --- a/packages/platform/src/errors.ts +++ b/packages/platform/src/errors.ts @@ -3,7 +3,7 @@ import type { PlatformSocketErrorCode } from "./errors.types.js"; /** * An error from a builder session or subscription. * - * Check `code` to decide what to do. The error never carries the session token, a callback's + * Check `code` to decide what to do. Each code is listed under [`PlatformSocketErrorCode`](#platformsocketerrorcode). The error never carries the session token, a callback's * exception, or the server's exception text. * * @example diff --git a/packages/platform/src/errors.types.ts b/packages/platform/src/errors.types.ts index 9b6cbd87..176ca5c8 100644 --- a/packages/platform/src/errors.types.ts +++ b/packages/platform/src/errors.types.ts @@ -1,21 +1,28 @@ -/** - * Codes a {@link PlatformSocketError} reports. - * - * - `connection_denied`: Base44 refused the session even with a fresh token. The workspace, key or session isn't usable. - * - `connection_failed`: The socket couldn't connect or reconnect. - * - `session_unavailable`: `getSessionToken` failed, timed out or returned no token. - * - `session_revoked`: Your server or an admin ended the session. Don't reconnect. - * - `session_replaced`: Another socket took over the session. Stop, or call `connect()` to take it back. - * - `access_denied`: The app isn't on the session's allowlist, or the socket joins too often. - * - `access_revoked`: The app left the session's allowlist or workspace. Don't subscribe again. - * - `snapshot_unavailable`: The subscription is active but its snapshot failed. Events still arrive. - * - `subscription_limit`: The session already has eight subscriptions. - * - `delivery_overflow`: 1,000 deliveries queued for the app. Subscribe again for a fresh snapshot. - * - `protocol_error`: A frame didn't match the protocol. - * - `handler_failed`: Your `onSnapshot` or `onEvent` callback threw or rejected. - * - `client_closed`: The session was closed. - */ +/** Codes a {@link PlatformSocketError} reports. Session codes go to the session's `onError`, app codes to the subscription's. */ export type PlatformSocketErrorCode = - | "connection_denied" | "connection_failed" | "session_unavailable" | "session_revoked" | "session_replaced" - | "access_denied" | "access_revoked" | "snapshot_unavailable" | "subscription_limit" - | "delivery_overflow" | "protocol_error" | "handler_failed" | "client_closed"; + /** Session. `getSessionToken` threw, took longer than 20 seconds or returned no token. Check the endpoint that opens sessions, then call `connect()` again. Retryable. */ + | "session_unavailable" + /** Session. Base44 refused the session even with a fresh token: the token is invalid, or live updates aren't enabled for the workspace. Check that your server opens a new session on every call. Not retryable. */ + | "connection_denied" + /** Session. The socket couldn't connect after five retries, or the server closed it. Call `connect()` again, for example when the tab next gets focus. Retryable. */ + | "connection_failed" + /** Session. Your server ended the session, or its key was disabled, deleted or lost the **Watch app builder updates** permission. Open a new session if the page should still follow apps. Not retryable. */ + | "session_revoked" + /** Session. Another socket connected with the same session token. Use one session per tab, or call `connect()` to take the session back. Not retryable. */ + | "session_replaced" + /** Session or app. A frame didn't match the protocol. Update the SDK, then reconnect or subscribe again. Not retryable. */ + | "protocol_error" + /** Session or app. You called `connect()` or `subscribe()` after `close()`. Create a new session with `init()`. Not retryable. */ + | "client_closed" + /** App. The app isn't on the session's allowlist, the key can't watch it, or the socket joins too often. Add the app to the session from your server first. Not retryable. */ + | "access_denied" + /** App. The app left the session's allowlist or moved to another workspace. Stop showing its live updates. Not retryable. */ + | "access_revoked" + /** App. The subscription is active but its snapshot failed. Events still arrive, and the subscription doesn't end. Load the app's messages over the Apps API instead. */ + | "snapshot_unavailable" + /** App. The session already has eight subscriptions. Unsubscribe from an app, or open another session. Not retryable. */ + | "subscription_limit" + /** App. More than 1,000 deliveries waited for your callbacks. Make them faster, then subscribe again for a fresh snapshot. Retryable. */ + | "delivery_overflow" + /** App. Your `onSnapshot` or `onEvent` callback threw or rejected. Fix the callback, then subscribe again. Not retryable. */ + | "handler_failed"; diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index 76effbd0..50f9cc3f 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -5,8 +5,8 @@ import type { PlatformSocketError } from "../errors.js"; export interface BuilderInitOptions { /** * Called on errors that affect the whole session, such as a failed connection or an ended session. - * Errors for one app go to that subscription's `onError` instead. See - * [connection errors](/developers/references/platform-sdk/live-updates/errors#connection-errors). + * Errors for one app go to that subscription's `onError` instead. See the + * [error codes](/developers/references/platform-sdk/docs/classes/PlatformSocketError#platformsocketerrorcode). */ onError: (error: PlatformSocketError) => void; } @@ -57,6 +57,8 @@ export interface BuilderSession { * one attempt. Call it again after a failure or a `session_replaced` error. * * @returns Promise that resolves when the socket connects. + * @throws {PlatformSocketError} With `connection_denied`, `connection_failed` or `session_unavailable` + * when the session can't connect, or `client_closed` when the session is closed first. * * @example * ```typescript @@ -137,7 +139,7 @@ export interface SubscriptionOptions { onEvent: (event: PlatformEvent) => void | Promise; /** * Called on errors for this app. Every error ends the subscription except `snapshot_unavailable`. - * See [app errors](/developers/references/platform-sdk/live-updates/errors#app-errors). + * See the [error codes](/developers/references/platform-sdk/docs/classes/PlatformSocketError#platformsocketerrorcode). */ onError: (error: PlatformSocketError) => void; } diff --git a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js index af0523d3..cfd3b5b6 100644 --- a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js +++ b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js @@ -130,12 +130,17 @@ function updateDocsJson(repoDir, sdkFiles) { }; const p = (kind, file) => `${prefix}${basePath}/${kind}/${file}`; - // Groups follow the category map's key order. + // Groups follow the category map's key order. The target's `groups` moves single pages, such + // as `"classes/MyError": "Errors"`, into groups placed after the category map's. for (const [kind, group] of Object.entries(categoryMap)) { - if (sdkFiles[kind]?.length > 0) addToGroup(group, sdkFiles[kind].map((f) => p(kind, f))); + for (const file of sdkFiles[kind] ?? []) { + addToGroup(TARGET.groups?.[`${kind}/${file}`] ?? group, [p(kind, file)]); + } } + const order = [...new Set(Object.values(categoryMap))]; + const rank = (group) => (order.includes(group) ? order.indexOf(group) : order.length); - return Array.from(groupMap.entries()).map(([group, pages]) => ({ + return Array.from(groupMap.entries()).sort(([a], [b]) => rank(a) - rank(b)).map(([group, pages]) => ({ group, expanded: true, // Sort by basename, not full path: a plain .sort() groups all diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 0ffbbdaa..9fe94ad0 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -30,6 +30,7 @@ const TYPES_TO_DELETE_PATH = path.join(CONFIG_DIR, "types-to-delete-after-proces const APPENDED_ARTICLES_PATH = path.join(CONFIG_DIR, "appended-articles.json"); const METHOD_ORDER_PATH = path.join(CONFIG_DIR, "method-order.json"); const OVERLOAD_PRESENTATION_PATH = path.join(CONFIG_DIR, "overload-presentation.json"); +const PAGE_SECTIONS_PATH = path.join(CONFIG_DIR, "page-sections.json"); // Controlled via env var so we can re-enable Panel injection when needed. const PANELS_ENABLED = process.env.MINTLIFY_INCLUDE_PANELS === "true"; @@ -1431,12 +1432,10 @@ function applyTypeDeclarationLinking(dir) { * `## Properties` section. The value renames the heading, or is null to keep the * interface name. */ -const TYPES_WITH_OWN_METHODS = { - EntityHandler: "Entity Handler Methods", - ActorRef: "Actor methods", - Connection: "Connection methods", - ActorSubscription: "Subscription methods", -}; +// Appended types whose methods get their own section, by section title, and groups of appended +// types nested under "## Type Definitions" (the first type of a group starts the section). +const { typesWithOwnMethods: TYPES_WITH_OWN_METHODS = {}, typeDefinitionGroups: TYPE_DEFINITION_GROUPS = [] } = + fs.existsSync(PAGE_SECTIONS_PATH) ? JSON.parse(fs.readFileSync(PAGE_SECTIONS_PATH, "utf-8")) : {}; /** * Group intro sections (like "Built-in User Entity", "Generated Types") under an "Overview" heading @@ -1550,7 +1549,7 @@ function applyIntroSectionGrouping(dir) { /** * Group type definition sections under a parent heading. * - * Each group below must list every entry from appended-articles.json for that + * Each group in page-sections.json must list every entry from appended-articles.json for that * module except the one that gets absorbed into the "## ... Methods" section * (typically named `*Module` or `*Handler`, e.g. EntityHandler) — otherwise * the appended type keeps its original `##` heading and renders as a sibling @@ -1559,39 +1558,7 @@ function applyIntroSectionGrouping(dir) { function groupTypeDefinitions(content) { let modified = false; - // Define type definition patterns for different modules - const typeGroups = [ - // Connectors module - { - types: ["ConnectorIntegrationType", "ConnectorIntegrationTypeRegistry"], - indicator: "ConnectorIntegrationType" - }, - // Entities module - { - types: [ - "EntityRecord", - "EntityTypeRegistry", - "SortField", - "EntityFilterQuery" - ], - indicator: "EntityRecord" - }, - // Functions module - { - types: ["FunctionName", "FunctionNameRegistry"], - indicator: "FunctionName" - }, - // Agents module - { - types: ["AgentName", "AgentNameRegistry"], - indicator: "AgentName" - }, - // Actors module - { - types: ["ActorClient", "ActorRegistry", "ActorNameRegistry"], - indicator: "ActorClient" - } - ]; + const typeGroups = TYPE_DEFINITION_GROUPS.map((types) => ({ types, indicator: types[0] })); // Find which type group exists in this file let matchedGroup = null; diff --git a/packages/sdk/scripts/mintlify-post-processing/page-sections.json b/packages/sdk/scripts/mintlify-post-processing/page-sections.json new file mode 100644 index 00000000..72407e5c --- /dev/null +++ b/packages/sdk/scripts/mintlify-post-processing/page-sections.json @@ -0,0 +1,33 @@ +{ + "typesWithOwnMethods": { + "EntityHandler": "Entity Handler Methods", + "ActorRef": "Actor methods", + "Connection": "Connection methods", + "ActorSubscription": "Subscription methods" + }, + "typeDefinitionGroups": [ + [ + "ConnectorIntegrationType", + "ConnectorIntegrationTypeRegistry" + ], + [ + "EntityRecord", + "EntityTypeRegistry", + "SortField", + "EntityFilterQuery" + ], + [ + "FunctionName", + "FunctionNameRegistry" + ], + [ + "AgentName", + "AgentNameRegistry" + ], + [ + "ActorClient", + "ActorRegistry", + "ActorNameRegistry" + ] + ] +} diff --git a/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js b/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js index 966c4a39..54523100 100644 --- a/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js +++ b/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js @@ -92,6 +92,11 @@ export function load(app) { ); } + // 2a. Document each value of a literal union; typedoc-plugin-markdown drops their comments + if (page.model?.kind === ReflectionKind.TypeAlias) { + content = appendUnionValueFields(content, page.model.type); + } + // 3. Convert code examples to CodeGroup content = convertExamplesToCodeGroup(content); @@ -258,3 +263,13 @@ function fixEscapedTypeCharacters(content) { // Simply replace all instances of \| with | in the entire content return content.replace(/\\\|/g, "|"); } + +function appendUnionValueFields(content, type) { + const summaries = type?.type === "union" ? type.elementSummaries : undefined; + if (!summaries || !type.types.every((t) => t.type === "literal")) return content; + const fields = type.types.map((t, i) => { + const text = (summaries[i] ?? []).map((part) => part.text).join("").trim(); + return `\n\n${text}\n\n`; + }); + return `${content.trimEnd()}\n\n${fields.join("\n\n")}\n`; +} From b5a370ead2ebadc0651ec84479e26ca568595ef6 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 21:49:31 +0300 Subject: [PATCH 08/32] chore(platform): lock json-schema-to-typescript from the public registry Every resolved URL and integrity matches registry.npmjs.org, and every version is past the 14-day release cooldown. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 104 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/package-lock.json b/package-lock.json index f4fed864..b10aa8f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,24 @@ "vitest": "^4.1.9" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", + "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -597,6 +615,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", @@ -1026,6 +1051,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", @@ -3676,6 +3708,52 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-typescript": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-16.0.0.tgz", + "integrity": "sha512-Ah6kK4q6SjlMzWBH1eNOQkdRh5Qhr5T/RCFwfjQqWZA7UDW+N47A8wfyoiN0L884s4L4bAEI54IjIuN3/u0B/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.9.3", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.25", + "js-yaml": "^5.2.3", + "lodash": "^4.18.1", + "minimist": "^1.2.8", + "prettier": "^3.9.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/json-schema-to-typescript/node_modules/js-yaml": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", + "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4031,6 +4109,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4552,6 +4637,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", @@ -5881,6 +5982,9 @@ "license": "MIT", "dependencies": { "socket.io-client": "^4.8.3" + }, + "devDependencies": { + "json-schema-to-typescript": "16.0.0" } }, "packages/sdk": { From 5af19ddbdc66ab0f992f4523a578a5d24f188396 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 22:35:35 +0300 Subject: [PATCH 09/32] docs(platform): use one session route in examples and drop the guessed socket host Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/client.ts | 2 +- packages/platform/src/client.types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/platform/src/client.ts b/packages/platform/src/client.ts index ad10d8af..6d421247 100644 --- a/packages/platform/src/client.ts +++ b/packages/platform/src/client.ts @@ -17,7 +17,7 @@ import type { BuilderModule } from "./modules/builder.types.js"; * const client = new Base44PlatformClient({ * serverUrl: socketUrl, * async getSessionToken() { - * const response = await fetch('/api/builder-session', { method: 'POST' }); + * const response = await fetch('/api/builder-socket-session', { method: 'POST' }); * const { session_token } = await response.json(); * return session_token; * }, diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client.types.ts index bddf3477..af6641fe 100644 --- a/packages/platform/src/client.types.ts +++ b/packages/platform/src/client.types.ts @@ -6,7 +6,7 @@ */ export interface PlatformClientOptions { /** - * Origin of the platform socket, such as `https://app.base44.com`. + * Origin of the platform socket. * * Use the `socket_url` your server receives from `POST /api/service/socket-sessions`. It must be * an origin with no path, query, fragment, or credentials. From 5705430bdf4a401e5d4cbcacb430bacedded05b2 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 22:43:01 +0300 Subject: [PATCH 10/32] feat(platform): tie the server's error codes to the generated events roomNotices and the new sessionEndings are typed by ServerEventMap and SessionEnded, so a new room notice or session-end reason fails to compile until the SDK maps it to a code. The five codes the server sends now link to their event for the meaning and say only what to do. The type's summary defines "retryable", and session_replaced no longer reads as both retryable and not. Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/errors.types.ts | 15 +++++++++------ packages/platform/src/modules/builder-protocol.ts | 13 ++++++++++--- packages/platform/src/modules/builder-socket.ts | 9 ++++----- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/platform/src/errors.types.ts b/packages/platform/src/errors.types.ts index 176ca5c8..a3695844 100644 --- a/packages/platform/src/errors.types.ts +++ b/packages/platform/src/errors.types.ts @@ -1,4 +1,7 @@ -/** Codes a {@link PlatformSocketError} reports. Session codes go to the session's `onError`, app codes to the subscription's. */ +/** + * Codes a {@link PlatformSocketError} reports. Session codes go to the session's `onError`, app codes to + * the subscription's. Retryable means the same call can succeed again without changing anything. + */ export type PlatformSocketErrorCode = /** Session. `getSessionToken` threw, took longer than 20 seconds or returned no token. Check the endpoint that opens sessions, then call `connect()` again. Retryable. */ | "session_unavailable" @@ -6,19 +9,19 @@ export type PlatformSocketErrorCode = | "connection_denied" /** Session. The socket couldn't connect after five retries, or the server closed it. Call `connect()` again, for example when the tab next gets focus. Retryable. */ | "connection_failed" - /** Session. Your server ended the session, or its key was disabled, deleted or lost the **Watch app builder updates** permission. Open a new session if the page should still follow apps. Not retryable. */ + /** Session. Reported for [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `revoked`. Open a new session from your server if the page should still follow apps. Not retryable. */ | "session_revoked" - /** Session. Another socket connected with the same session token. Use one session per tab, or call `connect()` to take the session back. Not retryable. */ + /** Session. Reported for [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `replaced`. Use one session per tab. Don't reconnect automatically; call `connect()` only to take the session back on purpose. Not retryable. */ | "session_replaced" /** Session or app. A frame didn't match the protocol. Update the SDK, then reconnect or subscribe again. Not retryable. */ | "protocol_error" /** Session or app. You called `connect()` or `subscribe()` after `close()`. Create a new session with `init()`. Not retryable. */ | "client_closed" - /** App. The app isn't on the session's allowlist, the key can't watch it, or the socket joins too often. Add the app to the session from your server first. Not retryable. */ + /** App. Reported for [`room.access_denied`](/developers/references/platform-sdk/live-updates/events#room-access_denied). Add the app to the session from your server, then subscribe again. Not retryable. */ | "access_denied" - /** App. The app left the session's allowlist or moved to another workspace. Stop showing its live updates. Not retryable. */ + /** App. Reported for [`room.access_revoked`](/developers/references/platform-sdk/live-updates/events#room-access_revoked). Stop showing the app's live updates. Not retryable. */ | "access_revoked" - /** App. The subscription is active but its snapshot failed. Events still arrive, and the subscription doesn't end. Load the app's messages over the Apps API instead. */ + /** App. Reported for [`room.snapshot_unavailable`](/developers/references/platform-sdk/live-updates/events#room-snapshot_unavailable). The subscription stays active and events keep arriving. Load the app's messages over the Apps API instead. */ | "snapshot_unavailable" /** App. The session already has eight subscriptions. Unsubscribe from an app, or open another session. Not retryable. */ | "subscription_limit" diff --git a/packages/platform/src/modules/builder-protocol.ts b/packages/platform/src/modules/builder-protocol.ts index 1f4353b6..b9c9cb19 100644 --- a/packages/platform/src/modules/builder-protocol.ts +++ b/packages/platform/src/modules/builder-protocol.ts @@ -1,4 +1,5 @@ -import { serverEventNames } from "./builder.events.generated.js"; +import { serverEventNames, type ServerEventMap, type SessionEnded } from "./builder.events.generated.js"; +import type { PlatformSocketErrorCode } from "../errors.types.js"; import type { PlatformEvent, PlatformEventMap, PlatformSnapshot } from "./builder.events.types.js"; const sessionEvents: readonly string[] = ["app.snapshot", "session.ended"]; @@ -6,12 +7,18 @@ const sessionEvents: readonly string[] = ["app.snapshot", "session.ended"]; export const eventNames = serverEventNames.filter( (name): name is keyof PlatformEventMap & typeof name => !sessionEvents.includes(name) && !name.startsWith("room."), ); -/** Notices that name an app room, by the error code each one reports. */ +/** Notices that name an app room, by the error code each one reports. A new notice fails to compile until it has one. */ export const roomNotices = { "room.access_denied": "access_denied", "room.access_revoked": "access_revoked", "room.snapshot_unavailable": "snapshot_unavailable", -} as const; +} as const satisfies Record, PlatformSocketErrorCode>; +// `expired` never reaches the partner: the session renews its token. +export const sessionEndings = { + expired: "session_expired", + revoked: "session_revoked", + replaced: "session_replaced", +} as const satisfies Record; export const appPattern = /^[a-f0-9]{24}$/; export const roomFor = (appId: string) => `/apps/${appId}`; diff --git a/packages/platform/src/modules/builder-socket.ts b/packages/platform/src/modules/builder-socket.ts index af128aa3..35a5c2aa 100644 --- a/packages/platform/src/modules/builder-socket.ts +++ b/packages/platform/src/modules/builder-socket.ts @@ -1,6 +1,6 @@ import { io, type Socket } from "socket.io-client"; import { PlatformSocketError } from "../errors.js"; -import { appFromRoom, appPattern, decode, decodeSnapshot, eventNames, object, roomFor, roomNotices } from "./builder-protocol.js"; +import { appFromRoom, appPattern, decode, decodeSnapshot, eventNames, object, roomFor, roomNotices, sessionEndings } from "./builder-protocol.js"; import { notify, Subscription } from "./builder-subscription.js"; import type { PlatformClientOptions } from "../client.types.js"; import type { BuilderInitOptions, BuilderSession, PlatformSubscription, SubscriptionOptions } from "./builder.types.js"; @@ -16,7 +16,7 @@ export class BuilderSocket implements BuilderSession { // The session token outlives reconnects; it is replaced only when the server rejects or expires it. private token?: string; private tokenFresh = false; - private ending?: "session_expired" | "session_revoked" | "session_replaced"; + private ending?: (typeof sessionEndings)[keyof typeof sessionEndings]; private denialRetries = 0; private retryTimer?: ReturnType; private authAttempt = 0; @@ -188,9 +188,8 @@ export class BuilderSocket implements BuilderSession { private sessionEnded(raw: unknown): void { try { const reason = object(object(raw).data).reason; - const ending = reason === "expired" ? "session_expired" - : reason === "revoked" ? "session_revoked" - : reason === "replaced" ? "session_replaced" : undefined; + const ending = typeof reason === "string" && Object.prototype.hasOwnProperty.call(sessionEndings, reason) + ? sessionEndings[reason as keyof typeof sessionEndings] : undefined; if (!ending) throw new Error("Unknown reason"); // The server disconnects next; the disconnect handler renews or stops. this.token = undefined; From 98287c9db1ea36f5b801a63fe04bda385acf55e5 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 22:54:37 +0300 Subject: [PATCH 11/32] docs(platform): tell onEvent handlers to ignore event types they don't handle Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/modules/builder.types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index 50f9cc3f..3973eba4 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -133,7 +133,8 @@ export interface SubscriptionOptions { onSnapshot: (snapshot: PlatformSnapshot) => void | Promise; /** * Called with each live event. Events for an app arrive one at a time, in order. If the callback - * throws or rejects, the subscription ends with `handler_failed`. For every event and its payload, + * throws or rejects, the subscription ends with `handler_failed`. New event types can be added at + * any time, so ignore types you don't handle. For every event and its payload, * see the [events reference](/developers/references/platform-sdk/live-updates/events). */ onEvent: (event: PlatformEvent) => void | Promise; From 823dcc0bcc8a181b63f3a14c565670d05ea3d822 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 23:02:53 +0300 Subject: [PATCH 12/32] docs: describe page-sections.json and nav group overrides in writing-docs.md Co-Authored-By: Claude Opus 5.5 --- packages/sdk/writing-docs.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/sdk/writing-docs.md b/packages/sdk/writing-docs.md index 7115f1eb..18dde0bb 100644 --- a/packages/sdk/writing-docs.md +++ b/packages/sdk/writing-docs.md @@ -61,6 +61,10 @@ After generating and reviewing the docs, you can push them to the `base44/mintli The same pipeline documents the Platform SDK (`packages/platform`). Pass `--package-dir ` to `file-processing.js` and `copy-to-local-docs.js` to run them for another package. They then read that package's `docs` folder and its config files under `/scripts/mintlify-post-processing/`. A `mintlify-target.json` there sets where the reference lands in mintlify-docs and which nav dropdown it belongs to. Without one, the scripts keep the SDK's defaults. +Two config files control page shape and nav, for both packages: +* `page-sections.json`: appended types whose methods get their own section (`typesWithOwnMethods`, by section title), and groups of appended types nested under **Type Definitions** (`typeDefinitionGroups`). +* `mintlify-target.json` `groups`: moves a single page into another nav group, for example `{"classes/PlatformSocketError": "Errors"}`. + From `packages/platform`: * `npm run docs` validates that every export has JSDoc. Its output in `docs` is not published. * `npm run create-docs` generates the Mintlify reference into `docs/content`. From 3f956103c7a5da1bf694614a4c74896664779671 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 23:08:13 +0300 Subject: [PATCH 13/32] docs: stop class pages from listing their properties twice The constructor's Returns lists the instance's properties, like a function's Returns lists its result. Class pages now drop the separate ## Properties section and the class description repeated inside Returns. @base44/sdk has no classes, so its output is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../file-processing/file-processing.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 9fe94ad0..5653f674 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -2525,6 +2525,37 @@ function restructureActorsPage() { fs.writeFileSync(file, tidied, "utf-8"); } +/** + * On class pages, the constructor's Returns shows the instance like a function's Returns shows its + * result. Drop what repeats it: the class description inside Returns, and the ## Properties section. + */ +function cleanupClassPage(content) { + const lines = content.split("\n"); + const returnsIndex = lines.indexOf("#### Returns"); + if (returnsIndex !== -1) { + const typeIndex = lines.findIndex((line, i) => i > returnsIndex && line.trim() !== ""); + const accordionIndex = lines.findIndex((line, i) => i > typeIndex && (line.startsWith(" i > propertiesIndex && line.startsWith("## ")); + lines.splice(propertiesIndex, (next === -1 ? lines.length : next) - propertiesIndex); + } + return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"; +} + +function applyClassPageCleanup(dir) { + const classesDir = path.join(dir, "content", "classes"); + if (!fs.existsSync(classesDir)) return; + for (const file of fs.readdirSync(classesDir).filter((name) => name.endsWith(".mdx"))) { + const filePath = path.join(classesDir, file); + fs.writeFileSync(filePath, cleanupClassPage(fs.readFileSync(filePath, "utf-8")), "utf-8"); + } +} + function main() { console.log("Processing TypeDoc MDX files for Mintlify...\n"); @@ -2574,6 +2605,9 @@ function main() { // Group type definitions under a parent heading applyTypeDefinitionGrouping(DOCS_DIR); + // Class pages: the constructor's Returns already lists the instance's properties + applyClassPageCleanup(DOCS_DIR); + // Reorder methods according to method-order.json applyMethodOrdering(DOCS_DIR); From 85b09eab472e0113750489f14d7083ea50de1ae5 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 23:12:42 +0300 Subject: [PATCH 14/32] docs(platform): link the builder property to its module page, like the app SDK's modules Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/client.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/client.ts b/packages/platform/src/client.ts index 6d421247..aefcc7eb 100644 --- a/packages/platform/src/client.ts +++ b/packages/platform/src/client.ts @@ -27,7 +27,8 @@ import type { BuilderModule } from "./modules/builder.types.js"; */ export class Base44PlatformClient { /** - * Live builder updates. Call [`init()`](/developers/references/platform-sdk/docs/interfaces/builder#init) to create a session. + * [Builder module](/developers/references/platform-sdk/docs/interfaces/builder) for following the AI chat in your users' apps live. + * Call [`init()`](/developers/references/platform-sdk/docs/interfaces/builder#init) to create a session. */ readonly builder: BuilderModule; From bad0a8f8d4a97d9e741fab9657adf4a4b13430e7 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 23:31:40 +0300 Subject: [PATCH 15/32] docs: keep the return type of a method that returns a methods-only interface When the return type has no properties to expand, the Returns section lost its type line. It now falls back to the type TypeDoc linked. This adds `ActorSubscription` to subscribe() on the app SDK's actors page and `BuilderSession` to init() on the Platform SDK's builder page. Co-Authored-By: Claude Opus 5.5 --- .../typedoc-plugin/typedoc-mintlify-returns.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js b/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js index 82d50e3c..4236dff8 100644 --- a/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js +++ b/packages/sdk/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js @@ -579,10 +579,11 @@ function rewriteReturnSections(content, options) { if (fields.length === 0 && !indexSignature) { result.push(...sectionLines); } else { + // The section's own leading type link is the fallback when the signature gave no type. const typeNameForDisplay = enrichTypeNameWithGenerics( extractedTypeName || returnTypeName, returnTypeFromSignature - ); + ) || sectionContent.match(/^\[`([^`]+)`\]\(/)?.[1]; if (typeNameForDisplay) { result.push(""); result.push(`\`${typeNameForDisplay}\``); From a6a7c2b52415c09b2ca452f8b596e6ea7c2d5729 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 23:41:00 +0300 Subject: [PATCH 16/32] docs(platform): group error codes by where they arrive PlatformSocketErrorCode is now SessionErrorCode | AppErrorCode, each value documented with what it means and what to do. The PlatformSocketError page lists them in two sections right after its properties. Pipeline, both generic: a literal union drops its one-line signature and type badge, since every value is listed below it; a class page without a documented constructor keeps its Properties. The constructor is @internal, since only the SDK creates these errors. Co-Authored-By: Claude Opus 5.5 --- .../appended-articles.json | 3 +- .../page-sections.json | 2 +- .../types-to-expose.json | 3 +- packages/platform/src/errors.ts | 7 +- packages/platform/src/errors.types.ts | 103 +++++++++++++++--- packages/platform/src/index.ts | 2 +- .../file-processing/file-processing.js | 12 +- .../typedoc-plugin/typedoc-mintlify-plugin.js | 6 +- 8 files changed, 105 insertions(+), 33 deletions(-) diff --git a/packages/platform/scripts/mintlify-post-processing/appended-articles.json b/packages/platform/scripts/mintlify-post-processing/appended-articles.json index a8dbda69..75b5d9f0 100644 --- a/packages/platform/scripts/mintlify-post-processing/appended-articles.json +++ b/packages/platform/scripts/mintlify-post-processing/appended-articles.json @@ -4,6 +4,7 @@ "type-aliases/PlatformEvent" ], "classes/PlatformSocketError": [ - "type-aliases/PlatformSocketErrorCode" + "type-aliases/SessionErrorCode", + "type-aliases/AppErrorCode" ] } diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json index c04c7eb0..9970f7f1 100644 --- a/packages/platform/scripts/mintlify-post-processing/page-sections.json +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -2,5 +2,5 @@ "typesWithOwnMethods": { "BuilderSession": "Session methods" }, - "typeDefinitionGroups": [["PlatformEvent"], ["PlatformSocketErrorCode"]] + "typeDefinitionGroups": [["PlatformEvent"], ["SessionErrorCode", "AppErrorCode"]] } diff --git a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json index 132e650f..9b1eab82 100644 --- a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json +++ b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json @@ -1,8 +1,9 @@ [ + "AppErrorCode", "Base44PlatformClient", "BuilderModule", "BuilderSession", "PlatformEvent", "PlatformSocketError", - "PlatformSocketErrorCode" + "SessionErrorCode" ] diff --git a/packages/platform/src/errors.ts b/packages/platform/src/errors.ts index 58e35cd9..88f71dfd 100644 --- a/packages/platform/src/errors.ts +++ b/packages/platform/src/errors.ts @@ -3,7 +3,7 @@ import type { PlatformSocketErrorCode } from "./errors.types.js"; /** * An error from a builder session or subscription. * - * Check `code` to decide what to do. Each code is listed under [`PlatformSocketErrorCode`](#platformsocketerrorcode). The error never carries the session token, a callback's + * Check `code` to decide what to do. The error never carries the session token, a callback's * exception, or the server's exception text. * * @example @@ -17,16 +17,17 @@ import type { PlatformSocketErrorCode } from "./errors.types.js"; * ``` */ export class PlatformSocketError extends Error { - /** Machine-readable error code. */ + /** The error code. See [session error codes](#sessionerrorcode) and [app error codes](#apperrorcode). */ readonly code: PlatformSocketErrorCode; /** ID of the app the error is about, when it concerns one subscription. */ readonly appId?: string; /** - * Creates an error. + * Creates an error. The SDK creates these; you don't need to. * * @param code - Error code. * @param appId - ID of the app the error is about. + * @internal */ constructor(code: PlatformSocketErrorCode, appId?: string) { super(`Platform socket: ${code}`); diff --git a/packages/platform/src/errors.types.ts b/packages/platform/src/errors.types.ts index a3695844..04318ba6 100644 --- a/packages/platform/src/errors.types.ts +++ b/packages/platform/src/errors.types.ts @@ -1,31 +1,98 @@ /** - * Codes a {@link PlatformSocketError} reports. Session codes go to the session's `onError`, app codes to - * the subscription's. Retryable means the same call can succeed again without changing anything. + * Codes of errors that affect the whole session. They go to the `onError` you pass to `init()`, and + * `connect()` rejects with them. Retryable means the same call can succeed again without changing anything. */ -export type PlatformSocketErrorCode = - /** Session. `getSessionToken` threw, took longer than 20 seconds or returned no token. Check the endpoint that opens sessions, then call `connect()` again. Retryable. */ +export type SessionErrorCode = + /** + * `getSessionToken` threw, took longer than 20 seconds or returned no token. + * + * **What to do:** check the endpoint that opens sessions, then call `connect()` again. Retryable. + */ | "session_unavailable" - /** Session. Base44 refused the session even with a fresh token: the token is invalid, or live updates aren't enabled for the workspace. Check that your server opens a new session on every call. Not retryable. */ + /** + * Base44 refused the session even with a fresh token: the token is invalid, or live updates aren't enabled for the workspace. + * + * **What to do:** check that your server opens a new session on every call. Not retryable. + */ | "connection_denied" - /** Session. The socket couldn't connect after five retries, or the server closed it. Call `connect()` again, for example when the tab next gets focus. Retryable. */ + /** + * The socket couldn't connect after five retries, or the server closed it. + * + * **What to do:** call `connect()` again, for example when the tab next gets focus. Retryable. + */ | "connection_failed" - /** Session. Reported for [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `revoked`. Open a new session from your server if the page should still follow apps. Not retryable. */ + /** + * Base44 sent [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `revoked`. + * + * **What to do:** open a new session from your server if the page should still follow apps. Not retryable. + */ | "session_revoked" - /** Session. Reported for [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `replaced`. Use one session per tab. Don't reconnect automatically; call `connect()` only to take the session back on purpose. Not retryable. */ + /** + * Base44 sent [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `replaced`: another socket connected with the same token. + * + * **What to do:** use one session per tab. Don't reconnect automatically; call `connect()` only to take the session back on purpose. Not retryable. + */ | "session_replaced" - /** Session or app. A frame didn't match the protocol. Update the SDK, then reconnect or subscribe again. Not retryable. */ + /** + * A session frame didn't match the protocol. + * + * **What to do:** update the SDK, then call `connect()` again. Not retryable. + */ | "protocol_error" - /** Session or app. You called `connect()` or `subscribe()` after `close()`. Create a new session with `init()`. Not retryable. */ - | "client_closed" - /** App. Reported for [`room.access_denied`](/developers/references/platform-sdk/live-updates/events#room-access_denied). Add the app to the session from your server, then subscribe again. Not retryable. */ + /** + * You called `connect()` or `subscribe()` after `close()`. + * + * **What to do:** create a new session with `init()`. Not retryable. + */ + | "client_closed"; + +/** + * Codes of errors for one app. They go to the subscription's `onError`, and `subscribe()` throws + * `subscription_limit`. Every one ends the subscription except `snapshot_unavailable`. + */ +export type AppErrorCode = + /** + * Base44 sent [`room.access_denied`](/developers/references/platform-sdk/live-updates/events#room-access_denied). + * + * **What to do:** add the app to the session from your server, then subscribe again. Not retryable. + */ | "access_denied" - /** App. Reported for [`room.access_revoked`](/developers/references/platform-sdk/live-updates/events#room-access_revoked). Stop showing the app's live updates. Not retryable. */ + /** + * Base44 sent [`room.access_revoked`](/developers/references/platform-sdk/live-updates/events#room-access_revoked). + * + * **What to do:** stop showing the app's live updates. Not retryable. + */ | "access_revoked" - /** App. Reported for [`room.snapshot_unavailable`](/developers/references/platform-sdk/live-updates/events#room-snapshot_unavailable). The subscription stays active and events keep arriving. Load the app's messages over the Apps API instead. */ + /** + * Base44 sent [`room.snapshot_unavailable`](/developers/references/platform-sdk/live-updates/events#room-snapshot_unavailable). The subscription stays active and events keep arriving. + * + * **What to do:** load the app's messages over the Apps API instead. + */ | "snapshot_unavailable" - /** App. The session already has eight subscriptions. Unsubscribe from an app, or open another session. Not retryable. */ + /** + * The session already has eight subscriptions. + * + * **What to do:** unsubscribe from an app, or open another session. Not retryable. + */ | "subscription_limit" - /** App. More than 1,000 deliveries waited for your callbacks. Make them faster, then subscribe again for a fresh snapshot. Retryable. */ + /** + * More than 1,000 deliveries waited for your callbacks. + * + * **What to do:** make the callbacks faster, then subscribe again for a fresh snapshot. Retryable. + */ | "delivery_overflow" - /** App. Your `onSnapshot` or `onEvent` callback threw or rejected. Fix the callback, then subscribe again. Not retryable. */ - | "handler_failed"; + /** + * Your `onSnapshot` or `onEvent` callback threw or rejected. + * + * **What to do:** fix the callback, then subscribe again. Not retryable. + */ + | "handler_failed" + /** + * An event for this app didn't match the protocol. + * + * **What to do:** update the SDK, then subscribe again. Not retryable. + */ + | "protocol_error"; + +/** Every code a {@link PlatformSocketError} reports. */ +export type PlatformSocketErrorCode = SessionErrorCode | AppErrorCode; diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index e9c0cac4..d8447ab5 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -1,7 +1,7 @@ /** Browser platform modules, separate from the runtime and server SDKs. */ export { Base44PlatformClient } from "./client.js"; export { PlatformSocketError } from "./errors.js"; -export type { PlatformSocketErrorCode } from "./errors.types.js"; +export type { AppErrorCode, PlatformSocketErrorCode, SessionErrorCode } from "./errors.types.js"; export type { PlatformClientOptions } from "./client.types.js"; export type { BuilderModule, BuilderInitOptions, BuilderSession, PlatformSubscription, SubscriptionOptions } from "./modules/builder.types.js"; export type { GuardApproval, PlatformEvent, PlatformEventMap, PlatformSnapshot, ToolCall } from "./modules/builder.events.types.js"; diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 5653f674..f9b94f7d 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -2528,16 +2528,16 @@ function restructureActorsPage() { /** * On class pages, the constructor's Returns shows the instance like a function's Returns shows its * result. Drop what repeats it: the class description inside Returns, and the ## Properties section. + * A class without a documented constructor keeps its ## Properties. */ function cleanupClassPage(content) { const lines = content.split("\n"); const returnsIndex = lines.indexOf("#### Returns"); - if (returnsIndex !== -1) { - const typeIndex = lines.findIndex((line, i) => i > returnsIndex && line.trim() !== ""); - const accordionIndex = lines.findIndex((line, i) => i > typeIndex && (line.startsWith(" i > returnsIndex && line.trim() !== ""); + const accordionIndex = lines.findIndex((line, i) => i > typeIndex && (line.startsWith(" t.type === "literal")) return content; const fields = type.types.map((t, i) => { const text = (summaries[i] ?? []).map((part) => part.text).join("").trim(); - return `\n\n${text}\n\n`; + return `\n\n${text}\n\n`; }); - return `${content.trimEnd()}\n\n${fields.join("\n\n")}\n`; + // Each value is listed below with its comment, so the one-line union signature only repeats them. + const withoutSignature = content.replace(/^> \*\*\w+\*\* = .*\n+/m, ""); + return `${withoutSignature.trimEnd()}\n\n${fields.join("\n\n")}\n`; } From d03b25a02556223f77f17c2bb11c115a11b17f85 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 23:50:09 +0300 Subject: [PATCH 17/32] docs(platform): title the error codes section and link session and event types page-sections.json groups can now name their section; the error codes sit under "Error codes" instead of "Type Definitions". init() links its result to the session methods, and onEvent links to PlatformEvent. Co-Authored-By: Claude Opus 5.5 --- .../mintlify-post-processing/page-sections.json | 5 ++++- packages/platform/src/modules/builder.types.ts | 4 ++-- .../file-processing/file-processing.js | 10 +++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json index 9970f7f1..83dd8df3 100644 --- a/packages/platform/scripts/mintlify-post-processing/page-sections.json +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -2,5 +2,8 @@ "typesWithOwnMethods": { "BuilderSession": "Session methods" }, - "typeDefinitionGroups": [["PlatformEvent"], ["SessionErrorCode", "AppErrorCode"]] + "typeDefinitionGroups": [ + ["PlatformEvent"], + { "title": "Error codes", "types": ["SessionErrorCode", "AppErrorCode"] } + ] } diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index 3973eba4..336015a2 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -30,7 +30,7 @@ export interface BuilderModule { * Each session is independent, with its own socket, token and subscriptions. * * @param options - Session options. - * @returns The new session. + * @returns The new session. Use its [session methods](#session-methods) to connect and subscribe. * * @example * ```typescript @@ -132,7 +132,7 @@ export interface SubscriptionOptions { */ onSnapshot: (snapshot: PlatformSnapshot) => void | Promise; /** - * Called with each live event. Events for an app arrive one at a time, in order. If the callback + * Called with each live event, a [`PlatformEvent`](#platformevent). Events for an app arrive one at a time, in order. If the callback * throws or rejects, the subscription ends with `handler_failed`. New event types can be added at * any time, so ignore types you don't handle. For every event and its payload, * see the [events reference](/developers/references/platform-sdk/live-updates/events). diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index f9b94f7d..2f01fdf3 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -1433,7 +1433,8 @@ function applyTypeDeclarationLinking(dir) { * interface name. */ // Appended types whose methods get their own section, by section title, and groups of appended -// types nested under "## Type Definitions" (the first type of a group starts the section). +// types nested under "## Type Definitions" (the first type of a group starts the section). A group +// is a list of type names, or `{ "title": "...", "types": [...] }` for another section heading. const { typesWithOwnMethods: TYPES_WITH_OWN_METHODS = {}, typeDefinitionGroups: TYPE_DEFINITION_GROUPS = [] } = fs.existsSync(PAGE_SECTIONS_PATH) ? JSON.parse(fs.readFileSync(PAGE_SECTIONS_PATH, "utf-8")) : {}; @@ -1558,7 +1559,10 @@ function applyIntroSectionGrouping(dir) { function groupTypeDefinitions(content) { let modified = false; - const typeGroups = TYPE_DEFINITION_GROUPS.map((types) => ({ types, indicator: types[0] })); + const typeGroups = TYPE_DEFINITION_GROUPS.map((group) => { + const { title = "Type Definitions", types } = Array.isArray(group) ? { types: group } : group; + return { types, indicator: types[0], title }; + }); // Find which type group exists in this file let matchedGroup = null; @@ -1599,7 +1603,7 @@ function groupTypeDefinitions(content) { const updatedContent = beforeTypeDefinitions + - "## Type Definitions\n\n" + + `## ${matchedGroup.title}\n\n` + demotedSection; return { content: updatedContent, modified: true }; From f3e1897b2aae7c3a58fa4684ab82675ac5409d09 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Fri, 9 Oct 2026 23:52:18 +0300 Subject: [PATCH 18/32] docs(platform): explain where PlatformSocketError arrives and how code and appId relate Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/errors.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/platform/src/errors.ts b/packages/platform/src/errors.ts index 88f71dfd..3dabd4f4 100644 --- a/packages/platform/src/errors.ts +++ b/packages/platform/src/errors.ts @@ -1,25 +1,39 @@ import type { PlatformSocketErrorCode } from "./errors.types.js"; /** - * An error from a builder session or subscription. + * The error the SDK reports when something goes wrong in a builder session. * - * Check `code` to decide what to do. The error never carries the session token, a callback's - * exception, or the server's exception text. + * 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: + * - [Session error codes](#sessionerrorcode) go to the `onError` you pass to `init()`. The whole + * session is affected, and `appId` isn't set. + * - [App error codes](#apperrorcode) go to the subscription's `onError`. Only that app is affected, + * and `appId` says which one. + * + * The error never carries the session token, a callback's exception, or the server's exception text. * * @example * ```typescript - * // Handle a session error + * // Handle session and app errors * const builder = client.builder.init({ * onError(error) { * if (error.code === 'session_revoked') showSignedOut(); * }, * }); + * + * builder.subscribe(appId, { + * onSnapshot: renderChat, + * onEvent: applyEvent, + * onError(error) { + * if (error.code === 'access_revoked') hideLiveUpdates(error.appId); + * }, + * }); * ``` */ export class PlatformSocketError extends Error { - /** The error code. See [session error codes](#sessionerrorcode) and [app error codes](#apperrorcode). */ + /** What happened. One of the [session error codes](#sessionerrorcode) or [app error codes](#apperrorcode). */ readonly code: PlatformSocketErrorCode; - /** ID of the app the error is about, when it concerns one subscription. */ + /** ID of the app the error is about. Set for app error codes, and not set for session error codes. */ readonly appId?: string; /** From 170b7da92b98af00a3bb025c170086dda66558e1 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 00:04:34 +0300 Subject: [PATCH 19/32] docs: link a Returns type to the section of the same page that documents it init() now links BuilderSession to Session methods. On the app SDK's actors page, Connection and ActorSubscription link to their methods sections. Co-Authored-By: Claude Opus 5.5 --- .../file-processing/file-processing.js | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 2f01fdf3..9e5553bd 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -2560,6 +2560,33 @@ function applyClassPageCleanup(dir) { } } +/** + * A Returns type line such as `BuilderSession` becomes a link when the same page documents that type: + * its methods section (page-sections.json `typesWithOwnMethods`) or its own ### heading. + */ +function linkReturnTypes(content) { + const slug = (heading) => heading.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); + const anchors = new Map(); + for (const [type, title] of Object.entries(TYPES_WITH_OWN_METHODS)) { + if (content.includes(`\n## ${title}\n`)) anchors.set(type, slug(title)); + } + for (const [, type] of content.matchAll(/^### (\w+)$/gm)) anchors.set(type, slug(type)); + return content.replace(/(^#{4,5} Returns\n\n)`(\w+)`$/gm, (match, heading, type) => + anchors.has(type) ? `${heading}[\`${type}\`](#${anchors.get(type)})` : match); +} + +function applyReturnTypeLinks(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) applyReturnTypeLinks(entryPath); + else if (entry.name.endsWith(".mdx")) { + const content = fs.readFileSync(entryPath, "utf-8"); + const linked = linkReturnTypes(content); + if (linked !== content) fs.writeFileSync(entryPath, linked, "utf-8"); + } + } +} + function main() { console.log("Processing TypeDoc MDX files for Mintlify...\n"); @@ -2612,6 +2639,9 @@ function main() { // Class pages: the constructor's Returns already lists the instance's properties applyClassPageCleanup(DOCS_DIR); + // Link a Returns type to where the same page documents it + applyReturnTypeLinks(DOCS_DIR); + // Reorder methods according to method-order.json applyMethodOrdering(DOCS_DIR); From 46bcc75906ae4eda4ecdbc4d89b5d7e67e0929e9 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 00:08:54 +0300 Subject: [PATCH 20/32] docs: render a class page's properties as fields, without separators A class without a documented constructor lists its properties as ResponseFields, like a Returns. @base44/sdk has no classes. Co-Authored-By: Claude Opus 5.5 --- .../file-processing/file-processing.js | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 9e5553bd..ea95a109 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -2532,12 +2532,12 @@ function restructureActorsPage() { /** * On class pages, the constructor's Returns shows the instance like a function's Returns shows its * result. Drop what repeats it: the class description inside Returns, and the ## Properties section. - * A class without a documented constructor keeps its ## Properties. + * A class without a documented constructor keeps its ## Properties, as fields like a Returns. */ function cleanupClassPage(content) { const lines = content.split("\n"); const returnsIndex = lines.indexOf("#### Returns"); - if (returnsIndex === -1) return content; + if (returnsIndex === -1) return propertiesAsFields(content); const typeIndex = lines.findIndex((line, i) => i > returnsIndex && line.trim() !== ""); const accordionIndex = lines.findIndex((line, i) => i > typeIndex && (line.startsWith(" { + const fields = body.split(/^\*{3}$/m).map((block) => { + const signature = block.match(/^> (.*)$/m)?.[1] ?? ""; + const name = block.match(/^#### (.+?)\??$/m)?.[1]?.replace(/\\/g, ""); + if (!name) return ""; + const type = signature.split(/\*\*: /)[1]?.replace(/`/g, "").trim() ?? ""; + const description = block.replace(/^#### .*$/m, "").replace(/^> .*$/m, "").trim(); + const required = signature.includes("`optional`") ? "" : " required"; + return `\n\n${description}\n\n`; + }).filter(Boolean); + return `## Properties\n\n${fields.join("\n\n")}\n\n`; + }); +} + function applyClassPageCleanup(dir) { const classesDir = path.join(dir, "content", "classes"); if (!fs.existsSync(classesDir)) return; From 66ff066d06247950c74446ad3622e6527ade3e0a Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 00:11:20 +0300 Subject: [PATCH 21/32] docs(platform): name BuilderSession in its description Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/modules/builder.types.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index 336015a2..f7a722c2 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -46,7 +46,10 @@ export interface BuilderModule { init(options: BuilderInitOptions): BuilderSession; } -/** One builder session. It owns its socket, its subscriptions and its reconnection. */ +/** + * A `BuilderSession`: one socket with its app subscriptions and its reconnection. Use its methods + * to connect, follow apps and close. + */ export interface BuilderSession { /** * Connects the session's socket. From eb985bbcf45c329e6e08fae98fea41e321cc9016 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 12:19:56 +0300 Subject: [PATCH 22/32] feat(platform)!: replace Base44PlatformClient with createPlatformClient() The client is created like the app SDK's: createPlatformClient(options) returns a PlatformClient with builder. Its reference page is now a function page with the same shape as createClient: signature, description, Parameters, Returns, then the example. Also: the builder property no longer describes init(); options read "Configuration object for the client."; maxTypeConversionDepth 3 for this package, so getSessionToken shows Promise instead of Promise<...>. BREAKING CHANGE: `new Base44PlatformClient(options)` is now `createPlatformClient(options)`. Co-Authored-By: Claude Opus 5.5 --- packages/platform/README.md | 6 +-- packages/platform/examples/client.ts | 4 +- .../category-map.json | 1 + .../types-to-expose.json | 1 - packages/platform/src/client.ts | 47 ++++++++----------- packages/platform/src/client.types.ts | 14 +++++- packages/platform/src/index.ts | 4 +- packages/platform/tests/package.test.mjs | 10 ++-- packages/platform/tests/types/client.types.ts | 8 ++-- packages/platform/tests/unit/client.test.ts | 10 ++-- packages/platform/tests/unit/socket.test.js | 4 +- packages/platform/typedoc.mintlify.json | 2 +- 12 files changed, 58 insertions(+), 53 deletions(-) diff --git a/packages/platform/README.md b/packages/platform/README.md index 16b34339..e48417c2 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -10,9 +10,9 @@ shared socket. It is independent of the app SDK (`@base44/sdk`). The workspace must have white-label sockets enabled; this SDK does not open sessions. ```ts -import { Base44PlatformClient } from "@base44/platform"; +import { createPlatformClient } from "@base44/platform"; -const client = new Base44PlatformClient({ +const client = createPlatformClient({ serverUrl: socketUrl, // the session's socket_url, from POST /api/service/socket-sessions async getSessionToken() { const response = await fetch("/api/builder-socket-session", { method: "POST" }); @@ -55,7 +55,7 @@ older one, which stops with `session_replaced`. Open one session per page. ## Lifecycle -`new Base44PlatformClient({ serverUrl, getSessionToken })` creates a lightweight module +`createPlatformClient({ serverUrl, getSessionToken })` creates a lightweight module container: no Socket.IO instance, timers, token request or network activity. Its `builder` module follows the server SDK's module-factory pattern. diff --git a/packages/platform/examples/client.ts b/packages/platform/examples/client.ts index e859b57d..9218e4c6 100644 --- a/packages/platform/examples/client.ts +++ b/packages/platform/examples/client.ts @@ -1,8 +1,8 @@ -import { Base44PlatformClient, type PlatformEvent } from "@base44/platform"; +import { createPlatformClient, type PlatformEvent } from "@base44/platform"; declare const socketUrl: string; -const client = new Base44PlatformClient({ +const client = createPlatformClient({ serverUrl: socketUrl, // the session's socket_url, from POST /api/service/socket-sessions // Your backend opens the session with its workspace key and returns only the session token. async getSessionToken() { diff --git a/packages/platform/scripts/mintlify-post-processing/category-map.json b/packages/platform/scripts/mintlify-post-processing/category-map.json index dfe7cf10..6e82cc20 100644 --- a/packages/platform/scripts/mintlify-post-processing/category-map.json +++ b/packages/platform/scripts/mintlify-post-processing/category-map.json @@ -1,4 +1,5 @@ { + "functions": "Client", "classes": "Client", "interfaces": "Modules", "type-aliases": "Modules" diff --git a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json index 9b1eab82..aa4380db 100644 --- a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json +++ b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json @@ -1,6 +1,5 @@ [ "AppErrorCode", - "Base44PlatformClient", "BuilderModule", "BuilderSession", "PlatformEvent", diff --git a/packages/platform/src/client.ts b/packages/platform/src/client.ts index aefcc7eb..218a277b 100644 --- a/packages/platform/src/client.ts +++ b/packages/platform/src/client.ts @@ -1,20 +1,24 @@ -import type { PlatformClientOptions } from "./client.types.js"; +import type { PlatformClient, PlatformClientOptions } from "./client.types.js"; import { createBuilder } from "./modules/builder.js"; -import type { BuilderModule } from "./modules/builder.types.js"; /** - * Browser client for the Base44 platform. + * Creates a platform client. * - * Use it in a white-label app editor to follow what the AI chat does in your users' apps. Creating - * the client opens no sockets, starts no timers and sends no requests. Each module sets up its own + * 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). + * Creating it opens no sockets, starts no timers and sends no requests. Each module sets up its own * resources when you initialize it. * + * @param options - Configuration object for the client. + * @returns A configured platform client with access to the platform's modules. + * @throws {TypeError} When `serverUrl` isn't an HTTP or HTTPS origin. + * * @example * ```typescript - * // Create a client - * import { Base44PlatformClient } from '@base44/platform'; + * // Create a client for your app editor + * import { createPlatformClient } from '@base44/platform'; * - * const client = new Base44PlatformClient({ + * const client = createPlatformClient({ * serverUrl: socketUrl, * async getSessionToken() { * const response = await fetch('/api/builder-socket-session', { method: 'POST' }); @@ -22,27 +26,16 @@ import type { BuilderModule } from "./modules/builder.types.js"; * return session_token; * }, * }); + * + * // Use the client to follow the AI chat in an app * const builder = client.builder.init({ onError: (error) => console.error(error.code) }); + * await builder.connect(); * ``` */ -export class Base44PlatformClient { - /** - * [Builder module](/developers/references/platform-sdk/docs/interfaces/builder) for following the AI chat in your users' apps live. - * Call [`init()`](/developers/references/platform-sdk/docs/interfaces/builder#init) to create a session. - */ - readonly builder: BuilderModule; - - /** - * Creates a platform client. - * - * @param options - Client options. - * @throws {TypeError} When `serverUrl` isn't an HTTP or HTTPS origin. - */ - constructor(options: PlatformClientOptions) { - const url = new URL(options.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"); - } - this.builder = Object.freeze(createBuilder({ ...options, serverUrl: url.origin })); +export function createPlatformClient(options: PlatformClientOptions): PlatformClient { + const url = new URL(options.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 Object.freeze({ builder: Object.freeze(createBuilder({ ...options, serverUrl: url.origin })) }); } diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client.types.ts index af6641fe..3b16fcba 100644 --- a/packages/platform/src/client.types.ts +++ b/packages/platform/src/client.types.ts @@ -1,5 +1,7 @@ +import type { BuilderModule } from "./modules/builder.types.js"; + /** - * Options for creating a {@link Base44PlatformClient}. + * Options for {@linkcode createPlatformClient | createPlatformClient()}. * * The client runs in the browser and never sees your workspace API key. Your server opens a socket * session with the key and hands the browser only the session's token and socket URL. @@ -28,3 +30,13 @@ export interface PlatformClientOptions { */ getSessionToken: () => string | Promise; } + +/** + * 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; +} diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index d8447ab5..f5703a91 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -1,8 +1,8 @@ /** Browser platform modules, separate from the runtime and server SDKs. */ -export { Base44PlatformClient } from "./client.js"; +export { createPlatformClient } from "./client.js"; export { PlatformSocketError } from "./errors.js"; export type { AppErrorCode, PlatformSocketErrorCode, SessionErrorCode } from "./errors.types.js"; -export type { PlatformClientOptions } from "./client.types.js"; +export type { PlatformClient, PlatformClientOptions } from "./client.types.js"; export type { BuilderModule, BuilderInitOptions, BuilderSession, PlatformSubscription, SubscriptionOptions } from "./modules/builder.types.js"; export type { GuardApproval, PlatformEvent, PlatformEventMap, PlatformSnapshot, ToolCall } from "./modules/builder.events.types.js"; export type * from "./modules/builder.events.generated.js"; diff --git a/packages/platform/tests/package.test.mjs b/packages/platform/tests/package.test.mjs index f0fdb71a..8f228c1f 100644 --- a/packages/platform/tests/package.test.mjs +++ b/packages/platform/tests/package.test.mjs @@ -34,16 +34,16 @@ test("the client resolves from the installed package without the runtime SDK", ( assert.equal("@base44/sdk" in { ...manifest.dependencies, ...manifest.peerDependencies }, false); run(process.execPath, ["--input-type=module", "--eval", ` import assert from "node:assert/strict"; - import { Base44PlatformClient } from "@base44/platform"; - const client = new Base44PlatformClient({ serverUrl: "https://example.test", getSessionToken: () => "session-token" }); + import { createPlatformClient } from "@base44/platform"; + const client = createPlatformClient({ serverUrl: "https://example.test", getSessionToken: () => "session-token" }); assert.equal(typeof client.builder.init, "function"); client.builder.init({ onError() {} }).close(); `]); }); const consumer = ` -import { Base44PlatformClient, type PlatformEvent } from "@base44/platform"; -const platform = new Base44PlatformClient({ serverUrl: "https://example.test", getSessionToken: async () => "token" }); +import { createPlatformClient, type PlatformEvent } from "@base44/platform"; +const platform = createPlatformClient({ serverUrl: "https://example.test", getSessionToken: async () => "token" }); platform.builder.init({ onError() {} }).subscribe("a".repeat(24), { onSnapshot(snapshot) { void snapshot.messages; }, onEvent(event: PlatformEvent) { @@ -55,7 +55,7 @@ platform.builder.init({ onError() {} }).subscribe("a".repeat(24), { onError() {}, }); // @ts-expect-error No browser API key. -new Base44PlatformClient({ apiKey: "private" }); +createPlatformClient({ apiKey: "private" }); // @ts-expect-error No write channel. platform.send("write_file", {}); `; diff --git a/packages/platform/tests/types/client.types.ts b/packages/platform/tests/types/client.types.ts index 456c4473..e1c6e30b 100644 --- a/packages/platform/tests/types/client.types.ts +++ b/packages/platform/tests/types/client.types.ts @@ -1,5 +1,5 @@ -import { Base44PlatformClient, type PlatformEvent, type PlatformEventMap, type PlatformSnapshot, type ToolCall, type GuardApproval, type Message, type StatusObject } from "@base44/platform"; -const client = new Base44PlatformClient({ serverUrl: "https://example.test", getSessionToken: async () => "session-token" }); +import { createPlatformClient, type PlatformEvent, type PlatformEventMap, type PlatformSnapshot, type ToolCall, type GuardApproval, type Message, type StatusObject } from "@base44/platform"; +const client = createPlatformClient({ serverUrl: "https://example.test", getSessionToken: async () => "session-token" }); const builder = client.builder.init({ onError: error => { void error.code; } }); const subscription = builder.subscribe("a".repeat(24), { onSnapshot(snapshot: PlatformSnapshot) { @@ -40,11 +40,11 @@ builder.subscribe("a".repeat(24), { afterSeq: "saved", onSnapshot() {}, onEvent( // @ts-expect-error No browser mutation channel. client.send("write_file", {}); // @ts-expect-error API keys are not browser credentials. -new Base44PlatformClient({ apiKey: "private" }); +createPlatformClient({ apiKey: "private" }); // @ts-expect-error Socket methods belong to the builder session. client.connect(); // @ts-expect-error The retired token callback is not accepted. -new Base44PlatformClient({ serverUrl: "https://example.test", refreshToken: async () => "token" }); +createPlatformClient({ serverUrl: "https://example.test", refreshToken: async () => "token" }); const tool: ToolCall = { name: "ask_clarifying_questions", diff --git a/packages/platform/tests/unit/client.test.ts b/packages/platform/tests/unit/client.test.ts index 16c11934..10a49753 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 { Base44PlatformClient, type BuilderSession } from "../../src/index.js"; +import { createPlatformClient, type BuilderSession } from "../../src/index.js"; const fake = vi.hoisted(() => { const handlers: Record void> = {}; @@ -26,7 +26,7 @@ const auth = async () => { }; function setup(getSessionToken = vi.fn(async () => "session-token")) { const onError = vi.fn(); - const platform = new Base44PlatformClient({ serverUrl: "https://api.example.test", getSessionToken }); + const platform = createPlatformClient({ serverUrl: "https://api.example.test", getSessionToken }); const client = platform.builder.init({ onError }); clients.push(client); return { client, getSessionToken, onError }; @@ -41,7 +41,7 @@ afterEach(() => { clients.splice(0).forEach(client => client.close()); vi.useRea describe("platform client", () => { test("constructing the root client does not initialize sockets or fetch session tokens", () => { const getSessionToken = vi.fn(async () => "token"); - const platform = new Base44PlatformClient({ serverUrl: "https://api.example.test", getSessionToken }); + const platform = createPlatformClient({ serverUrl: "https://api.example.test", getSessionToken }); expect(fake.io).not.toHaveBeenCalled(); const first = platform.builder.init({ onError: vi.fn() }); const second = platform.builder.init({ onError: vi.fn() }); @@ -53,7 +53,7 @@ describe("platform client", () => { }); test("closing one initialized builder does not close another or the root module", () => { - const platform = new Base44PlatformClient({ serverUrl: "https://api.example.test", getSessionToken: async () => "token" }); + const platform = createPlatformClient({ serverUrl: "https://api.example.test", getSessionToken: async () => "token" }); const firstSocket = { ...fake.socket, disconnect: vi.fn() }; const secondSocket = { ...fake.socket, disconnect: vi.fn() }; fake.io.mockReturnValueOnce(firstSocket).mockReturnValueOnce(secondSocket); @@ -291,7 +291,7 @@ describe("platform client", () => { test("validates origins, app IDs, duplicate subscriptions and limits", () => { for (const serverUrl of ["https://secret@example.test", "https://example.test?token=secret", "https://example.test/path", "ws://example.test"]) { - expect(() => new Base44PlatformClient({ serverUrl, getSessionToken: () => "token" })).toThrow(TypeError); + expect(() => createPlatformClient({ serverUrl, getSessionToken: () => "token" })).toThrow(TypeError); } const { client } = setup(); const options = { onSnapshot: vi.fn(), onEvent: vi.fn(), onError: vi.fn() }; expect(() => client.subscribe("bad", options)).toThrow(TypeError); diff --git a/packages/platform/tests/unit/socket.test.js b/packages/platform/tests/unit/socket.test.js index 76a6220a..2a8d00aa 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 { Base44PlatformClient } from "../../src/index.js"; +import { createPlatformClient } from "../../src/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 () => { @@ -30,7 +30,7 @@ test("real Socket.IO handshake, join, snapshot, reconnect and session renewal", }); }); await new Promise(resolve => http.listen(0, "127.0.0.1", resolve)); - const client = new Base44PlatformClient({ + const client = createPlatformClient({ serverUrl: `http://127.0.0.1:${http.address().port}`, getSessionToken: async () => `session-${++tokenCalls}`, }); diff --git a/packages/platform/typedoc.mintlify.json b/packages/platform/typedoc.mintlify.json index 2b27ea02..f317419a 100644 --- a/packages/platform/typedoc.mintlify.json +++ b/packages/platform/typedoc.mintlify.json @@ -10,7 +10,7 @@ "gitRevision": "main", "sort": ["source-order"], "entryFileName": "README.mdx", - "maxTypeConversionDepth": 2, + "maxTypeConversionDepth": 3, "hideBreadcrumbs": true, "disableSources": true, "hidePageTitle": true From 5a411a00ba6f6e45e04de3e3eb61a61d12c6cb6e Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 12:49:49 +0300 Subject: [PATCH 23/32] feat(platform)!: rename serverUrl to socketUrl The value is the session's socket_url, the socket origin, which in production is a different host from the REST API. In the app SDK, serverUrl is the REST origin, so the shared name read as the same thing. BREAKING CHANGE: pass `socketUrl` instead of `serverUrl` to `createPlatformClient()`. Co-Authored-By: Claude Opus 5.5 --- packages/platform/README.md | 8 ++++---- packages/platform/examples/client.ts | 2 +- packages/platform/src/client.ts | 10 +++++----- packages/platform/src/client.types.ts | 2 +- packages/platform/src/modules/builder-socket.ts | 2 +- packages/platform/tests/package.test.mjs | 4 ++-- packages/platform/tests/types/client.types.ts | 4 ++-- packages/platform/tests/unit/client.test.ts | 10 +++++----- packages/platform/tests/unit/socket.test.js | 2 +- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/platform/README.md b/packages/platform/README.md index e48417c2..81936a50 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -13,7 +13,7 @@ The workspace must have white-label sockets enabled; this SDK does not open sess import { createPlatformClient } from "@base44/platform"; const client = createPlatformClient({ - serverUrl: socketUrl, // the session's socket_url, from POST /api/service/socket-sessions + socketUrl, // the session's socket_url, from POST /api/service/socket-sessions async getSessionToken() { const response = await fetch("/api/builder-socket-session", { method: "POST" }); if (!response.ok) throw new Error("Session request failed"); @@ -39,7 +39,7 @@ builder.close(); `/api/builder-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 only the -`session_token` and `socket_url` to the browser. Pass `socket_url` as `serverUrl`. Never pass the workspace key or any other server +`session_token` and `socket_url` to the browser. Pass `socket_url` as `socketUrl`. Never pass the workspace key or any other server credential to this client. Subscribe only to apps on the session's allowlist; your backend adds or removes apps with `PUT`/`DELETE /api/service/socket-sessions/{session_id}/rooms/{app_id}`. @@ -55,7 +55,7 @@ older one, which stops with `session_replaced`. Open one session per page. ## Lifecycle -`createPlatformClient({ serverUrl, getSessionToken })` creates a lightweight module +`createPlatformClient({ socketUrl, getSessionToken })` creates a lightweight module container: no Socket.IO instance, timers, token request or network activity. Its `builder` module follows the server SDK's module-factory pattern. @@ -94,7 +94,7 @@ their exceptions are isolated so they cannot interrupt another app's delivery. ## Public shapes -- `PlatformClientOptions` (`client.types.ts`): `serverUrl`, `getSessionToken`. +- `PlatformClientOptions` (`client.types.ts`): `socketUrl`, `getSessionToken`. - `BuilderModule` (`modules/builder.types.ts`): lazy `init(options)` factory. - `BuilderInitOptions`: connection-level `onError` observer. - `BuilderSession`: `connect()`, `subscribe(appId, options)`, `close()`. diff --git a/packages/platform/examples/client.ts b/packages/platform/examples/client.ts index 9218e4c6..a3f9d3f9 100644 --- a/packages/platform/examples/client.ts +++ b/packages/platform/examples/client.ts @@ -3,7 +3,7 @@ import { createPlatformClient, type PlatformEvent } from "@base44/platform"; declare const socketUrl: string; const client = createPlatformClient({ - serverUrl: socketUrl, // the session's socket_url, from POST /api/service/socket-sessions + socketUrl, // the session's socket_url, from POST /api/service/socket-sessions // Your backend opens the session with its workspace key and returns only the session token. async getSessionToken() { const response = await fetch("/api/builder-socket-session", { method: "POST" }); diff --git a/packages/platform/src/client.ts b/packages/platform/src/client.ts index 218a277b..7d2f9079 100644 --- a/packages/platform/src/client.ts +++ b/packages/platform/src/client.ts @@ -11,7 +11,7 @@ import { createBuilder } from "./modules/builder.js"; * * @param options - Configuration object for the client. * @returns A configured platform client with access to the platform's modules. - * @throws {TypeError} When `serverUrl` isn't an HTTP or HTTPS origin. + * @throws {TypeError} When `socketUrl` isn't an HTTP or HTTPS origin. * * @example * ```typescript @@ -19,7 +19,7 @@ import { createBuilder } from "./modules/builder.js"; * import { createPlatformClient } from '@base44/platform'; * * const client = createPlatformClient({ - * serverUrl: socketUrl, + * socketUrl, * async getSessionToken() { * const response = await fetch('/api/builder-socket-session', { method: 'POST' }); * const { session_token } = await response.json(); @@ -33,9 +33,9 @@ import { createBuilder } from "./modules/builder.js"; * ``` */ export function createPlatformClient(options: PlatformClientOptions): PlatformClient { - const url = new URL(options.serverUrl); + const url = new URL(options.socketUrl); 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"); + throw new TypeError("socketUrl must be an HTTP(S) origin without credentials, path, query or fragment"); } - return Object.freeze({ builder: Object.freeze(createBuilder({ ...options, serverUrl: url.origin })) }); + return Object.freeze({ builder: Object.freeze(createBuilder({ ...options, socketUrl: url.origin })) }); } diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client.types.ts index 3b16fcba..7394d484 100644 --- a/packages/platform/src/client.types.ts +++ b/packages/platform/src/client.types.ts @@ -13,7 +13,7 @@ export interface PlatformClientOptions { * Use the `socket_url` your server receives from `POST /api/service/socket-sessions`. It must be * an origin with no path, query, fragment, or credentials. */ - serverUrl: string; + socketUrl: string; /** * Returns a socket session token from your server. * diff --git a/packages/platform/src/modules/builder-socket.ts b/packages/platform/src/modules/builder-socket.ts index 35a5c2aa..e8c0755a 100644 --- a/packages/platform/src/modules/builder-socket.ts +++ b/packages/platform/src/modules/builder-socket.ts @@ -27,7 +27,7 @@ export class BuilderSocket implements BuilderSession { constructor(config: PlatformClientOptions, options: BuilderInitOptions) { this.options = { ...config, onError: options.onError }; - this.socket = io(config.serverUrl, { + this.socket = io(config.socketUrl, { path: "/ws/socket.io/", transports: ["websocket"], autoConnect: false, forceNew: true, reconnectionAttempts: 5, reconnectionDelay: 1000, reconnectionDelayMax: 10000, timeout: 20000, diff --git a/packages/platform/tests/package.test.mjs b/packages/platform/tests/package.test.mjs index 8f228c1f..389fd9bf 100644 --- a/packages/platform/tests/package.test.mjs +++ b/packages/platform/tests/package.test.mjs @@ -35,7 +35,7 @@ test("the client resolves from the installed package without the runtime SDK", ( run(process.execPath, ["--input-type=module", "--eval", ` import assert from "node:assert/strict"; import { createPlatformClient } from "@base44/platform"; - const client = createPlatformClient({ serverUrl: "https://example.test", getSessionToken: () => "session-token" }); + const client = createPlatformClient({ socketUrl: "https://example.test", getSessionToken: () => "session-token" }); assert.equal(typeof client.builder.init, "function"); client.builder.init({ onError() {} }).close(); `]); @@ -43,7 +43,7 @@ test("the client resolves from the installed package without the runtime SDK", ( const consumer = ` import { createPlatformClient, type PlatformEvent } from "@base44/platform"; -const platform = createPlatformClient({ serverUrl: "https://example.test", getSessionToken: async () => "token" }); +const platform = createPlatformClient({ socketUrl: "https://example.test", getSessionToken: async () => "token" }); platform.builder.init({ onError() {} }).subscribe("a".repeat(24), { onSnapshot(snapshot) { void snapshot.messages; }, onEvent(event: PlatformEvent) { diff --git a/packages/platform/tests/types/client.types.ts b/packages/platform/tests/types/client.types.ts index e1c6e30b..59631019 100644 --- a/packages/platform/tests/types/client.types.ts +++ b/packages/platform/tests/types/client.types.ts @@ -1,5 +1,5 @@ import { createPlatformClient, type PlatformEvent, type PlatformEventMap, type PlatformSnapshot, type ToolCall, type GuardApproval, type Message, type StatusObject } from "@base44/platform"; -const client = createPlatformClient({ serverUrl: "https://example.test", getSessionToken: async () => "session-token" }); +const client = createPlatformClient({ socketUrl: "https://example.test", getSessionToken: async () => "session-token" }); const builder = client.builder.init({ onError: error => { void error.code; } }); const subscription = builder.subscribe("a".repeat(24), { onSnapshot(snapshot: PlatformSnapshot) { @@ -44,7 +44,7 @@ createPlatformClient({ apiKey: "private" }); // @ts-expect-error Socket methods belong to the builder session. client.connect(); // @ts-expect-error The retired token callback is not accepted. -createPlatformClient({ serverUrl: "https://example.test", refreshToken: async () => "token" }); +createPlatformClient({ socketUrl: "https://example.test", refreshToken: async () => "token" }); const tool: ToolCall = { name: "ask_clarifying_questions", diff --git a/packages/platform/tests/unit/client.test.ts b/packages/platform/tests/unit/client.test.ts index 10a49753..c4e2915c 100644 --- a/packages/platform/tests/unit/client.test.ts +++ b/packages/platform/tests/unit/client.test.ts @@ -26,7 +26,7 @@ const auth = async () => { }; function setup(getSessionToken = vi.fn(async () => "session-token")) { const onError = vi.fn(); - const platform = createPlatformClient({ serverUrl: "https://api.example.test", getSessionToken }); + const platform = createPlatformClient({ socketUrl: "https://api.example.test", getSessionToken }); const client = platform.builder.init({ onError }); clients.push(client); return { client, getSessionToken, onError }; @@ -41,7 +41,7 @@ afterEach(() => { clients.splice(0).forEach(client => client.close()); vi.useRea describe("platform client", () => { test("constructing the root client does not initialize sockets or fetch session tokens", () => { const getSessionToken = vi.fn(async () => "token"); - const platform = createPlatformClient({ serverUrl: "https://api.example.test", getSessionToken }); + const platform = createPlatformClient({ socketUrl: "https://api.example.test", getSessionToken }); expect(fake.io).not.toHaveBeenCalled(); const first = platform.builder.init({ onError: vi.fn() }); const second = platform.builder.init({ onError: vi.fn() }); @@ -53,7 +53,7 @@ describe("platform client", () => { }); test("closing one initialized builder does not close another or the root module", () => { - const platform = createPlatformClient({ serverUrl: "https://api.example.test", getSessionToken: async () => "token" }); + const platform = createPlatformClient({ socketUrl: "https://api.example.test", getSessionToken: async () => "token" }); const firstSocket = { ...fake.socket, disconnect: vi.fn() }; const secondSocket = { ...fake.socket, disconnect: vi.fn() }; fake.io.mockReturnValueOnce(firstSocket).mockReturnValueOnce(secondSocket); @@ -290,8 +290,8 @@ describe("platform client", () => { }); test("validates origins, app IDs, duplicate subscriptions and limits", () => { - for (const serverUrl of ["https://secret@example.test", "https://example.test?token=secret", "https://example.test/path", "ws://example.test"]) { - expect(() => createPlatformClient({ serverUrl, getSessionToken: () => "token" })).toThrow(TypeError); + for (const socketUrl of ["https://secret@example.test", "https://example.test?token=secret", "https://example.test/path", "ws://example.test"]) { + expect(() => createPlatformClient({ socketUrl, getSessionToken: () => "token" })).toThrow(TypeError); } const { client } = setup(); const options = { onSnapshot: vi.fn(), onEvent: vi.fn(), onError: vi.fn() }; expect(() => client.subscribe("bad", options)).toThrow(TypeError); diff --git a/packages/platform/tests/unit/socket.test.js b/packages/platform/tests/unit/socket.test.js index 2a8d00aa..bfc0e19f 100644 --- a/packages/platform/tests/unit/socket.test.js +++ b/packages/platform/tests/unit/socket.test.js @@ -31,7 +31,7 @@ test("real Socket.IO handshake, join, snapshot, reconnect and session renewal", }); await new Promise(resolve => http.listen(0, "127.0.0.1", resolve)); const client = createPlatformClient({ - serverUrl: `http://127.0.0.1:${http.address().port}`, + socketUrl: `http://127.0.0.1:${http.address().port}`, getSessionToken: async () => `session-${++tokenCalls}`, }); const builder = client.builder.init({ onError: error => errors.push(error) }); From e6861e0f059dda09e0641790c49fc9806153d85f Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 12:51:10 +0300 Subject: [PATCH 24/32] docs(platform): shorten the getSessionToken description Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/client.types.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client.types.ts index 7394d484..9d35373f 100644 --- a/packages/platform/src/client.types.ts +++ b/packages/platform/src/client.types.ts @@ -17,14 +17,9 @@ export interface PlatformClientOptions { /** * Returns a socket session token from your server. * - * Your server opens a session with its workspace API key through - * `POST /api/service/socket-sessions`, for the apps this page shows, and returns the - * `session_token`. The client calls this when a builder session first connects, and again only - * after Base44 rejects or expires the current token. Sessions last one hour. Open a new session - * on every call instead of returning a cached token. - * - * It must settle within 20 seconds and return a non-empty token, or the session reports - * `session_unavailable`. Never return a workspace API key or any other server credential. + * Your server opens a session with `POST /api/service/socket-sessions` and returns its + * `session_token`. The client calls this when it connects, and again when the token expires. + * Return a new token on every call. * * @returns The session token, or a promise resolving to it. */ From fffdebcb132e8aac95a512948efb47cd523c0591 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 12:52:34 +0300 Subject: [PATCH 25/32] docs(platform): drop the internals sentence from createPlatformClient Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/client.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/platform/src/client.ts b/packages/platform/src/client.ts index 7d2f9079..d3912c2e 100644 --- a/packages/platform/src/client.ts +++ b/packages/platform/src/client.ts @@ -6,8 +6,6 @@ import { createBuilder } from "./modules/builder.js"; * * 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). - * Creating it opens no sockets, starts no timers and sends no requests. Each module sets up its own - * resources when you initialize it. * * @param options - Configuration object for the client. * @returns A configured platform client with access to the platform's modules. From 446cd2c728c9d8772801721b627acfc570d47b96 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 13:11:09 +0300 Subject: [PATCH 26/32] docs: say a linked Returns type's description once, without separators When a Returns links to a section of the same page, its description is left to that section. The *** separator right under a methods section or a type heading is dropped. On the app SDK this changes the actors page (Connection, ActorSubscription) and the separators under Type Definitions headings on agents, connectors, entities and functions. Co-Authored-By: Claude Opus 5.5 --- .../platform/src/modules/builder.types.ts | 2 +- .../file-processing/file-processing.js | 26 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index f7a722c2..3a2a0366 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -30,7 +30,7 @@ export interface BuilderModule { * Each session is independent, with its own socket, token and subscriptions. * * @param options - Session options. - * @returns The new session. Use its [session methods](#session-methods) to connect and subscribe. + * @returns The new session. * * @example * ```typescript diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index ea95a109..bf280390 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -2587,8 +2587,25 @@ function linkReturnTypes(content) { if (content.includes(`\n## ${title}\n`)) anchors.set(type, slug(title)); } for (const [, type] of content.matchAll(/^### (\w+)$/gm)) anchors.set(type, slug(type)); + // The section a type heads, with no separator under its heading, and its description. + const descriptions = new Map(); + const headings = [...Object.entries(TYPES_WITH_OWN_METHODS).map(([type, title]) => [type, `## ${title}`]), + ...[...anchors.keys()].map((type) => [type, `### ${type}`])]; + for (const [type, heading] of headings) { + const start = content.indexOf(`\n${heading}\n\n***\n\n`); + if (start === -1) continue; + content = content.replace(`\n${heading}\n\n***\n\n`, `\n${heading}\n\n`); + const body = content.slice(start + heading.length + 3); + descriptions.set(type, body.slice(0, body.search(/\n(#|> |<|```)/)).trim()); + } return content.replace(/(^#{4,5} Returns\n\n)`(\w+)`$/gm, (match, heading, type) => - anchors.has(type) ? `${heading}[\`${type}\`](#${anchors.get(type)})` : match); + anchors.has(type) ? `${heading}[\`${type}\`](#${anchors.get(type)})` : match) + // A linked Returns points at the type's own description; don't repeat it there. + .replace(/(^#{4,5} Returns\n\n\[`(\w+)`\]\(#[^)]+\)\n\n)([\s\S]*?)(?=\n#|\n<)/gm, (match, head, type, rest) => { + const description = descriptions.get(type); + const text = rest.replace(/``/g, "`"); + return description && text.includes(description) ? head + text.replace(description, "").trimStart() : match; + }); } function applyReturnTypeLinks(dir) { @@ -2655,9 +2672,6 @@ function main() { // Class pages: the constructor's Returns already lists the instance's properties applyClassPageCleanup(DOCS_DIR); - // Link a Returns type to where the same page documents it - applyReturnTypeLinks(DOCS_DIR); - // Reorder methods according to method-order.json applyMethodOrdering(DOCS_DIR); @@ -2677,6 +2691,10 @@ function main() { // Delete types that should not appear in navigation but were needed for inline rendering deleteTypesAfterProcessing(DOCS_DIR); + // Link a Returns type to where the same page documents it. Last: it drops the *** separators + // that method ordering splits on. + applyReturnTypeLinks(DOCS_DIR); + // Clean up the linked types file try { if (fs.existsSync(LINKED_TYPES_FILE)) { From 577a912c09ab8a34fa7b6acc2e107fd89a3f192f Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 14:13:59 +0300 Subject: [PATCH 27/32] docs(platform): say which onError gets which codes, and drop Extends from class pages Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/errors.ts | 2 ++ .../mintlify-post-processing/file-processing/file-processing.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/platform/src/errors.ts b/packages/platform/src/errors.ts index 3dabd4f4..56d0fdee 100644 --- a/packages/platform/src/errors.ts +++ b/packages/platform/src/errors.ts @@ -16,6 +16,7 @@ import type { PlatformSocketErrorCode } from "./errors.types.js"; * ```typescript * // Handle session and app errors * const builder = client.builder.init({ + * // Session error codes: the whole session is affected * onError(error) { * if (error.code === 'session_revoked') showSignedOut(); * }, @@ -24,6 +25,7 @@ import type { PlatformSocketErrorCode } from "./errors.types.js"; * builder.subscribe(appId, { * onSnapshot: renderChat, * onEvent: applyEvent, + * // App error codes: only this app is affected, and error.appId says which * onError(error) { * if (error.code === 'access_revoked') hideLiveUpdates(error.appId); * }, diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index bf280390..d0a56f46 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -2535,6 +2535,8 @@ function restructureActorsPage() { * A class without a documented constructor keeps its ## Properties, as fields like a Returns. */ function cleanupClassPage(content) { + // "Extends Error" tells a reader nothing the overview doesn't. + content = content.replace(/^### Extends\n\n(- .*\n)+\n?/m, ""); const lines = content.split("\n"); const returnsIndex = lines.indexOf("#### Returns"); if (returnsIndex === -1) return propertiesAsFields(content); From be82e6fe221dac950ce14c783507feed82ea3fb3 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 14:17:16 +0300 Subject: [PATCH 28/32] docs(platform): drop the What to do label from the error codes Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/errors.types.ts | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/platform/src/errors.types.ts b/packages/platform/src/errors.types.ts index 04318ba6..549bf584 100644 --- a/packages/platform/src/errors.types.ts +++ b/packages/platform/src/errors.types.ts @@ -6,43 +6,43 @@ export type SessionErrorCode = /** * `getSessionToken` threw, took longer than 20 seconds or returned no token. * - * **What to do:** check the endpoint that opens sessions, then call `connect()` again. Retryable. + * Check the endpoint that opens sessions, then call `connect()` again. Retryable. */ | "session_unavailable" /** * Base44 refused the session even with a fresh token: the token is invalid, or live updates aren't enabled for the workspace. * - * **What to do:** check that your server opens a new session on every call. Not retryable. + * Check that your server opens a new session on every call. Not retryable. */ | "connection_denied" /** * The socket couldn't connect after five retries, or the server closed it. * - * **What to do:** call `connect()` again, for example when the tab next gets focus. Retryable. + * Call `connect()` again, for example when the tab next gets focus. Retryable. */ | "connection_failed" /** * Base44 sent [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `revoked`. * - * **What to do:** open a new session from your server if the page should still follow apps. Not retryable. + * Open a new session from your server if the page should still follow apps. Not retryable. */ | "session_revoked" /** * Base44 sent [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `replaced`: another socket connected with the same token. * - * **What to do:** use one session per tab. Don't reconnect automatically; call `connect()` only to take the session back on purpose. Not retryable. + * Use one session per tab. Don't reconnect automatically; call `connect()` only to take the session back on purpose. Not retryable. */ | "session_replaced" /** * A session frame didn't match the protocol. * - * **What to do:** update the SDK, then call `connect()` again. Not retryable. + * Update the SDK, then call `connect()` again. Not retryable. */ | "protocol_error" /** * You called `connect()` or `subscribe()` after `close()`. * - * **What to do:** create a new session with `init()`. Not retryable. + * Create a new session with `init()`. Not retryable. */ | "client_closed"; @@ -54,43 +54,43 @@ export type AppErrorCode = /** * Base44 sent [`room.access_denied`](/developers/references/platform-sdk/live-updates/events#room-access_denied). * - * **What to do:** add the app to the session from your server, then subscribe again. Not retryable. + * Add the app to the session from your server, then subscribe again. Not retryable. */ | "access_denied" /** * Base44 sent [`room.access_revoked`](/developers/references/platform-sdk/live-updates/events#room-access_revoked). * - * **What to do:** stop showing the app's live updates. Not retryable. + * Stop showing the app's live updates. Not retryable. */ | "access_revoked" /** * Base44 sent [`room.snapshot_unavailable`](/developers/references/platform-sdk/live-updates/events#room-snapshot_unavailable). The subscription stays active and events keep arriving. * - * **What to do:** load the app's messages over the Apps API instead. + * Load the app's messages over the Apps API instead. */ | "snapshot_unavailable" /** * The session already has eight subscriptions. * - * **What to do:** unsubscribe from an app, or open another session. Not retryable. + * Unsubscribe from an app, or open another session. Not retryable. */ | "subscription_limit" /** * More than 1,000 deliveries waited for your callbacks. * - * **What to do:** make the callbacks faster, then subscribe again for a fresh snapshot. Retryable. + * Make the callbacks faster, then subscribe again for a fresh snapshot. Retryable. */ | "delivery_overflow" /** * Your `onSnapshot` or `onEvent` callback threw or rejected. * - * **What to do:** fix the callback, then subscribe again. Not retryable. + * Fix the callback, then subscribe again. Not retryable. */ | "handler_failed" /** * An event for this app didn't match the protocol. * - * **What to do:** update the SDK, then subscribe again. Not retryable. + * Update the SDK, then subscribe again. Not retryable. */ | "protocol_error"; From fed76b10afc0c264a06873f8d63e6a97dc44b528 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 14:22:22 +0300 Subject: [PATCH 29/32] docs: remove pipeline code the platform docs no longer use No page documents a constructor any more, so the class page rule keeps only "no Extends" and properties as fields. The nav's per-page group override is replaced by mapping classes to "Errors" in the platform's category map, whose key order already places it last. Output is unchanged for both packages. Co-Authored-By: Claude Opus 5.5 --- .../category-map.json | 4 ++-- .../mintlify-target.json | 5 +--- .../copy-to-local-docs.js | 11 +++------ .../file-processing/file-processing.js | 24 ++----------------- packages/sdk/writing-docs.md | 4 +--- 5 files changed, 9 insertions(+), 39 deletions(-) diff --git a/packages/platform/scripts/mintlify-post-processing/category-map.json b/packages/platform/scripts/mintlify-post-processing/category-map.json index 6e82cc20..06493959 100644 --- a/packages/platform/scripts/mintlify-post-processing/category-map.json +++ b/packages/platform/scripts/mintlify-post-processing/category-map.json @@ -1,6 +1,6 @@ { "functions": "Client", - "classes": "Client", "interfaces": "Modules", - "type-aliases": "Modules" + "type-aliases": "Modules", + "classes": "Errors" } diff --git a/packages/platform/scripts/mintlify-post-processing/mintlify-target.json b/packages/platform/scripts/mintlify-post-processing/mintlify-target.json index 03f5511e..1ce7a05c 100644 --- a/packages/platform/scripts/mintlify-post-processing/mintlify-target.json +++ b/packages/platform/scripts/mintlify-post-processing/mintlify-target.json @@ -2,8 +2,5 @@ "path": "developers/references/platform-sdk/docs", "dropdown": "Platform SDK", "group": "Platform SDK Reference", - "icon": "brackets-curly", - "groups": { - "classes/PlatformSocketError": "Errors" - } + "icon": "brackets-curly" } diff --git a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js index cfd3b5b6..af0523d3 100644 --- a/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js +++ b/packages/sdk/scripts/mintlify-post-processing/copy-to-local-docs.js @@ -130,17 +130,12 @@ function updateDocsJson(repoDir, sdkFiles) { }; const p = (kind, file) => `${prefix}${basePath}/${kind}/${file}`; - // Groups follow the category map's key order. The target's `groups` moves single pages, such - // as `"classes/MyError": "Errors"`, into groups placed after the category map's. + // Groups follow the category map's key order. for (const [kind, group] of Object.entries(categoryMap)) { - for (const file of sdkFiles[kind] ?? []) { - addToGroup(TARGET.groups?.[`${kind}/${file}`] ?? group, [p(kind, file)]); - } + if (sdkFiles[kind]?.length > 0) addToGroup(group, sdkFiles[kind].map((f) => p(kind, f))); } - const order = [...new Set(Object.values(categoryMap))]; - const rank = (group) => (order.includes(group) ? order.indexOf(group) : order.length); - return Array.from(groupMap.entries()).sort(([a], [b]) => rank(a) - rank(b)).map(([group, pages]) => ({ + return Array.from(groupMap.entries()).map(([group, pages]) => ({ group, expanded: true, // Sort by basename, not full path: a plain .sort() groups all diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index d0a56f46..3ba8b79f 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -2529,28 +2529,9 @@ function restructureActorsPage() { fs.writeFileSync(file, tidied, "utf-8"); } -/** - * On class pages, the constructor's Returns shows the instance like a function's Returns shows its - * result. Drop what repeats it: the class description inside Returns, and the ## Properties section. - * A class without a documented constructor keeps its ## Properties, as fields like a Returns. - */ +/** Class pages: no "Extends Error", which tells a reader nothing, and properties as fields like a Returns. */ function cleanupClassPage(content) { - // "Extends Error" tells a reader nothing the overview doesn't. - content = content.replace(/^### Extends\n\n(- .*\n)+\n?/m, ""); - const lines = content.split("\n"); - const returnsIndex = lines.indexOf("#### Returns"); - if (returnsIndex === -1) return propertiesAsFields(content); - const typeIndex = lines.findIndex((line, i) => i > returnsIndex && line.trim() !== ""); - const accordionIndex = lines.findIndex((line, i) => i > typeIndex && (line.startsWith(" i > propertiesIndex && line.startsWith("## ")); - lines.splice(propertiesIndex, (next === -1 ? lines.length : next) - propertiesIndex); - } - return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"; + return propertiesAsFields(content.replace(/^### Extends\n\n(- .*\n)+\n?/m, "")); } /** A class page's ## Properties as ResponseFields, the component its Returns and Parameters use. */ @@ -2671,7 +2652,6 @@ function main() { // Group type definitions under a parent heading applyTypeDefinitionGrouping(DOCS_DIR); - // Class pages: the constructor's Returns already lists the instance's properties applyClassPageCleanup(DOCS_DIR); // Reorder methods according to method-order.json diff --git a/packages/sdk/writing-docs.md b/packages/sdk/writing-docs.md index 18dde0bb..60f6a461 100644 --- a/packages/sdk/writing-docs.md +++ b/packages/sdk/writing-docs.md @@ -61,9 +61,7 @@ After generating and reviewing the docs, you can push them to the `base44/mintli The same pipeline documents the Platform SDK (`packages/platform`). Pass `--package-dir ` to `file-processing.js` and `copy-to-local-docs.js` to run them for another package. They then read that package's `docs` folder and its config files under `/scripts/mintlify-post-processing/`. A `mintlify-target.json` there sets where the reference lands in mintlify-docs and which nav dropdown it belongs to. Without one, the scripts keep the SDK's defaults. -Two config files control page shape and nav, for both packages: -* `page-sections.json`: appended types whose methods get their own section (`typesWithOwnMethods`, by section title), and groups of appended types nested under **Type Definitions** (`typeDefinitionGroups`). -* `mintlify-target.json` `groups`: moves a single page into another nav group, for example `{"classes/PlatformSocketError": "Errors"}`. +`page-sections.json` sets the page shape, for both packages: appended types whose methods get their own section (`typesWithOwnMethods`, by section title), and groups of appended types nested under **Type Definitions** or a `title` you give the group (`typeDefinitionGroups`). From `packages/platform`: * `npm run docs` validates that every export has JSDoc. Its output in `docs` is not published. From 30904478341b869ea38db59d5f6ad1f22b6ab18f Mon Sep 17 00:00:00 2001 From: amitbardos Date: Sat, 10 Oct 2026 15:20:45 +0300 Subject: [PATCH 30/32] docs(platform): render the builder events page in the SDK's docs pipeline create-docs now renders docs/content/events/builder-events.mdx from the committed asyncapi.json (scripts/events-to-mdx.mjs, the renderer moved from mintlify-docs), for exactly the events eventNames forwards to onEvent, so the page and the types come from the same copy and release. It opens with how an event reaches onEvent, with no AsyncAPI connection prose. The nav gets an Events group from the category map. Also: PlatformSnapshot is documented on the builder page, linked from onSnapshot; properties under Type Definitions render as fields; error codes for server notices describe the notice instead of linking to events the page no longer lists; socketUrl's JSDoc says it is fixed per environment. Co-Authored-By: Claude Opus 5.5 --- packages/platform/README.md | 3 +- packages/platform/package.json | 5 +- packages/platform/scripts/events-to-mdx.mjs | 225 ++++++++++++++++++ .../appended-articles.json | 3 +- .../category-map.json | 3 +- .../page-sections.json | 2 +- .../types-to-expose.json | 1 + packages/platform/src/client.types.ts | 5 +- packages/platform/src/errors.types.ts | 10 +- .../src/modules/builder.events.types.ts | 2 +- .../platform/src/modules/builder.types.ts | 4 +- .../file-processing/file-processing.js | 50 ++-- packages/sdk/writing-docs.md | 2 +- 13 files changed, 282 insertions(+), 33 deletions(-) create mode 100644 packages/platform/scripts/events-to-mdx.mjs diff --git a/packages/platform/README.md b/packages/platform/README.md index 81936a50..9acdae05 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -136,7 +136,8 @@ generated file differs from what `asyncapi.json` produces. ## Reference docs Every exported shape and field has JSDoc. `npm run docs` validates it into `docs`. -`npm run create-docs` runs the app SDK's TypeDoc and Mintlify pipeline for this package, +`npm run create-docs` runs the app SDK's TypeDoc and Mintlify pipeline for this package, then renders +the builder events page from `asyncapi.json` with `scripts/events-to-mdx.mjs`, and `npm run copy-docs-local -- --target ` copies the result into `developers/references/platform-sdk/docs`. The pipeline's config for this package lives in `scripts/mintlify-post-processing/`. diff --git a/packages/platform/package.json b/packages/platform/package.json index 4b42e101..b9efb036 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -29,11 +29,12 @@ "test:unit": "vitest run", "test:package": "npm run build && node --test tests/package.test.mjs", "prepublishOnly": "npm run build", - "create-docs": "npm run create-docs:generate && npm run create-docs:process", + "create-docs": "npm run create-docs:generate && npm run create-docs:process && npm run create-docs:events", "create-docs-local": "npm run create-docs && npm run copy-docs-local", "copy-docs-local": "node ../sdk/scripts/mintlify-post-processing/copy-to-local-docs.js --package-dir .", "create-docs:generate": "node -e \"fs.rmSync('docs',{recursive:true,force:true})\" && typedoc --options typedoc.mintlify.json", - "create-docs:process": "node ../sdk/scripts/mintlify-post-processing/file-processing/file-processing.js --package-dir ." + "create-docs:process": "node ../sdk/scripts/mintlify-post-processing/file-processing/file-processing.js --package-dir .", + "create-docs:events": "tsc && node scripts/events-to-mdx.mjs" }, "dependencies": { "socket.io-client": "^4.8.3" diff --git a/packages/platform/scripts/events-to-mdx.mjs b/packages/platform/scripts/events-to-mdx.mjs new file mode 100644 index 00000000..0e03bf50 --- /dev/null +++ b/packages/platform/scripts/events-to-mdx.mjs @@ -0,0 +1,225 @@ +#!/usr/bin/env node +// Renders the events that reach onEvent as an MDX page beside the TypeDoc pages, from the committed +// asyncapi.json, with the ResponseField / Expandable components the rest of the reference uses. +// The event list is the SDK's own (eventNames), so the page and the types come from one copy. +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { eventNames } from "../dist/modules/builder-protocol.js"; + +const MAX_DEPTH = 8; +const source = new URL("../asyncapi.json", import.meta.url); +const target = new URL("../docs/content/events/builder-events.mdx", import.meta.url); +const doc = JSON.parse(readFileSync(source, "utf8")); +const PLATFORM_EVENT = "/developers/references/platform-sdk/docs/interfaces/builder#platformevent"; + +function resolvePointer(ref) { + if (!ref.startsWith("#/")) throw new Error(`Only local $refs are supported: ${ref}`); + return ref + .slice(2) + .split("/") + .map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~")) + .reduce((node, key) => node?.[key], doc); +} + +/** Follow $refs; sibling keys on the referring node win. Returns [schema, refNames]. */ +function deref(schema) { + const names = []; + let s = schema ?? {}; + while (s && typeof s === "object" && s.$ref) { + names.push(s.$ref); + const { $ref, ...rest } = s; + s = { ...resolvePointer($ref), ...rest }; + } + return [s, names]; +} + +const isNull = (s) => deref(s)[0].type === "null"; + +/** Union members, without null, plus whether null was one of them. */ +function unionOf(s) { + const members = s.anyOf ?? s.oneOf; + if (!Array.isArray(members)) return null; + const flat = members.flatMap((m) => { + const inner = unionOf(deref(m)[0]); + return inner ? [...inner.members, ...(inner.nullable ? [{ type: "null" }] : [])] : [m]; + }); + return { members: flat.filter((m) => !isNull(m)), nullable: flat.some(isNull) }; +} + +function typesOf(s) { + const t = s.type; + const list = Array.isArray(t) ? t : t ? [t] : []; + return { list: list.filter((x) => x !== "null"), nullable: list.includes("null") }; +} + +const isObject = (s) => { + const [d] = deref(s); + return d.type === "object" || (Array.isArray(d.type) && d.type.includes("object")) || !!d.properties; +}; + +const quote = (v) => (typeof v === "string" ? `'${v}'` : JSON.stringify(v)); + +/** The type label, e.g. `string | null`, `'a' | 'b'`, `object[]`. */ +function typeLabel(schema) { + const [s] = deref(schema); + const union = unionOf(s); + if (union) { + const parts = [...new Set(union.members.map(typeLabel))]; + return [...parts, ...(union.nullable ? ["null"] : [])].join(" | ") || "null"; + } + const { list, nullable } = typesOf(s); + let label; + if ("const" in s) label = quote(s.const); + else if (Array.isArray(s.enum)) label = s.enum.map(quote).join(" | "); + else if (list.includes("array")) { + const inner = typeLabel(s.items ?? {}); + label = `${inner.includes(" | ") ? `(${inner})` : inner}[]`; + } else if (list.length) label = list.map((t) => (t === "string" && s.format ? `string<${s.format}>` : t)).join(" | "); + else label = s.properties ? "object" : "any"; + return nullable && !label.endsWith("| null") ? `${label} | null` : label; +} + +/** Escape MDX-significant characters outside inline code spans. */ +function prose(text) { + return String(text ?? "") + .split(/(`[^`]*`)/) + .map((part, i) => (i % 2 ? part : part.replace(/[{}]/g, (c) => `\\${c}`).replace(/ String(v).replace(/&/g, "&").replace(/"/g, """); + +/** Notes beyond the description: allowed values and constraints. */ +function constraints(s) { + const notes = []; + if (s.not && Array.isArray(s.not.enum)) notes.push("Any value not matched by the other shapes."); + if (s.pattern) notes.push(`Pattern: \`${s.pattern}\``); + for (const [k, word] of [["minimum", "Minimum"], ["maximum", "Maximum"], ["minLength", "Minimum length"], ["maxLength", "Maximum length"], ["maxItems", "Maximum items"]]) { + if (k in s) notes.push(`${word}: \`${s[k]}\``); + } + return notes; +} + +/** The object schema whose fields a value shows: itself, or an array's item. */ +function containerOf(schema) { + const [s] = deref(schema); + if (typesOf(s).list.includes("array")) return containerOf(s.items ?? {}); + const union = unionOf(s); + if (union) { + const objects = union.members.filter(isObject); + if (objects.length === 1) return containerOf(objects[0]); + if (objects.length > 1) return { variants: objects }; + return null; + } + return isObject(s) ? { object: schema } : null; +} + +/** The property every variant pins with a distinct `const`, if any. */ +function discriminator(variants) { + const resolved = variants.map((v) => deref(v)[0]); + const keys = Object.keys(resolved[0].properties ?? {}); + return keys.find((k) => { + const consts = resolved.map((v) => deref(v.properties?.[k])[0]).filter((p) => p && "const" in p).map((p) => p.const); + return consts.length >= resolved.length - 1 && new Set(consts).size === consts.length && consts.length > 1; + }); +} + +const humanize = (title) => title.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase(); + +function variantTitle(variant, key) { + const [v, refs] = deref(variant); + const pinned = key && deref(v.properties?.[key])[0]; + if (pinned && "const" in pinned) return String(pinned.const); + const title = v.title ?? refs.at(-1)?.split("/").pop() ?? "variant"; + const words = humanize(title); + return words.startsWith("other ") ? `any ${words}` : words; +} + +function fieldsOf(objectSchema, depth, path) { + const [s] = deref(objectSchema); + const required = new Set(s.required ?? []); + return Object.entries(s.properties ?? {}).map(([name, p]) => field(name, p, required.has(name), depth, path)); +} + +function children(schema, depth, path) { + const [, refs] = deref(schema); + if (depth >= MAX_DEPTH || refs.some((r) => path.includes(r))) return ""; + const next = [...path, ...refs]; + const container = containerOf(schema); + if (!container) return ""; + if (container.object) { + const innerRefs = deref(container.object)[1].filter((r) => !refs.includes(r)); + if (innerRefs.some((r) => next.includes(r))) return ""; + const inner = fieldsOf(container.object, depth + 1, [...next, ...innerRefs]); + return inner.length ? `\n${inner.join("\n\n")}\n` : ""; + } + const key = discriminator(container.variants); + const shared = sharedFields(container.variants, key); + const out = []; + if (shared.length) { + const [first] = deref(container.variants[0]); + const required = new Set(first.required ?? []); + const inner = shared.map((name) => field(name, first.properties[name], required.has(name), depth + 1, next)); + out.push("Every shape has these fields:", `\n${inner.join("\n\n")}\n`); + } + const which = key ? `, told apart by \`${key}\`` : ""; + out.push(shared.length ? `Each shape adds its own fields${which}:` : `One of these shapes${which}:`); + for (const variant of container.variants) { + const [v, innerRefs] = deref(variant); + const body = [prose(v.description)]; + if (!innerRefs.some((r) => next.includes(r))) { + const required = new Set(v.required ?? []); + const own = Object.entries(v.properties ?? {}).filter(([name]) => !shared.includes(name)); + body.push(...own.map(([name, p]) => field(name, p, required.has(name), depth + 1, [...next, ...innerRefs]))); + } + out.push(`\n${body.filter(Boolean).join("\n\n")}\n`); + } + return out.join("\n\n"); +} + +/** Fields every variant declares identically (same schema, same required-ness), so they're listed once. */ +function sharedFields(variants, key) { + const resolved = variants.map((v) => deref(v)[0]); + const sig = (v, name) => JSON.stringify([v.properties?.[name], (v.required ?? []).includes(name)]); + return Object.keys(resolved[0].properties ?? {}).filter( + (name) => name !== key && resolved.every((v) => v.properties && name in v.properties && sig(v, name) === sig(resolved[0], name)), + ); +} + +function field(name, schema, required, depth, path) { + const [s] = deref(schema); + const attrs = [`name="${attr(name)}"`, `type="${attr(typeLabel(schema))}"`]; + if (s.default !== undefined && s.default !== null) attrs.push(`default="${attr(JSON.stringify(s.default))}"`); + if (required) attrs.push("required"); + if (s.deprecated) attrs.push("deprecated"); + const body = [prose(s.description), ...constraints(s).map(prose), children(schema, depth, path)].filter(Boolean); + if (!body.length) return ``; + return `\n\n${body.join("\n\n")}\n\n`; +} + +const messages = Object.fromEntries(Object.values(doc.components.messages).map((m) => [m.name, m])); +const lines = [ + "---", + 'title: "Builder events"', + 'description: "Every live event a builder subscription delivers to onEvent, and its payload."', + 'sidebarTitle: "Builder events"', + "---", + "", + "{/* Generated from packages/platform/asyncapi.json by scripts/events-to-mdx.mjs in base44/javascript-sdk. */}", + "", + `Each event reaches your \`onEvent\` callback as a [\`PlatformEvent\`](${PLATFORM_EVENT}): \`{ type, appId, data }\`. ` + + "Check `type` to narrow `data` to that event's payload, listed below. A key missing from `data` means " + + "the value is unchanged, and an explicit `null` clears it.", + "", +]; +for (const name of eventNames) { + const message = messages[name]; + const [frame] = deref(message.payload); + const data = frame.properties.data; + lines.push(`## \`${name}\``, "", prose(message.description ?? message.summary), ""); + const fields = deref(data)[0].properties ? fieldsOf(data, 0, deref(data)[1]) : []; + lines.push(fields.length ? fields.join("\n\n") : "`data` is empty.", ""); +} +mkdirSync(new URL(".", target), { recursive: true }); +writeFileSync(target, lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"); +console.log(`Wrote ${eventNames.length} events to docs/content/events/builder-events.mdx`); diff --git a/packages/platform/scripts/mintlify-post-processing/appended-articles.json b/packages/platform/scripts/mintlify-post-processing/appended-articles.json index 75b5d9f0..dcce4030 100644 --- a/packages/platform/scripts/mintlify-post-processing/appended-articles.json +++ b/packages/platform/scripts/mintlify-post-processing/appended-articles.json @@ -1,7 +1,8 @@ { "interfaces/builder": [ "interfaces/BuilderSession", - "type-aliases/PlatformEvent" + "type-aliases/PlatformEvent", + "interfaces/PlatformSnapshot" ], "classes/PlatformSocketError": [ "type-aliases/SessionErrorCode", diff --git a/packages/platform/scripts/mintlify-post-processing/category-map.json b/packages/platform/scripts/mintlify-post-processing/category-map.json index 06493959..107f3f6c 100644 --- a/packages/platform/scripts/mintlify-post-processing/category-map.json +++ b/packages/platform/scripts/mintlify-post-processing/category-map.json @@ -2,5 +2,6 @@ "functions": "Client", "interfaces": "Modules", "type-aliases": "Modules", - "classes": "Errors" + "classes": "Errors", + "events": "Events" } diff --git a/packages/platform/scripts/mintlify-post-processing/page-sections.json b/packages/platform/scripts/mintlify-post-processing/page-sections.json index 83dd8df3..eb835b5a 100644 --- a/packages/platform/scripts/mintlify-post-processing/page-sections.json +++ b/packages/platform/scripts/mintlify-post-processing/page-sections.json @@ -3,7 +3,7 @@ "BuilderSession": "Session methods" }, "typeDefinitionGroups": [ - ["PlatformEvent"], + ["PlatformEvent", "PlatformSnapshot"], { "title": "Error codes", "types": ["SessionErrorCode", "AppErrorCode"] } ] } diff --git a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json index aa4380db..0c677790 100644 --- a/packages/platform/scripts/mintlify-post-processing/types-to-expose.json +++ b/packages/platform/scripts/mintlify-post-processing/types-to-expose.json @@ -3,6 +3,7 @@ "BuilderModule", "BuilderSession", "PlatformEvent", + "PlatformSnapshot", "PlatformSocketError", "SessionErrorCode" ] diff --git a/packages/platform/src/client.types.ts b/packages/platform/src/client.types.ts index 9d35373f..c9dc5ff3 100644 --- a/packages/platform/src/client.types.ts +++ b/packages/platform/src/client.types.ts @@ -10,8 +10,9 @@ export interface PlatformClientOptions { /** * Origin of the platform socket. * - * Use the `socket_url` your server receives from `POST /api/service/socket-sessions`. It must be - * an origin with no path, query, fragment, or credentials. + * Use the `socket_url` your server receives from `POST /api/service/socket-sessions`. It's the same + * for every session in an environment, so your server can pass it to the page once. It must be an + * origin with no path, query, fragment, or credentials. */ socketUrl: string; /** diff --git a/packages/platform/src/errors.types.ts b/packages/platform/src/errors.types.ts index 549bf584..2ac3fa15 100644 --- a/packages/platform/src/errors.types.ts +++ b/packages/platform/src/errors.types.ts @@ -22,13 +22,13 @@ export type SessionErrorCode = */ | "connection_failed" /** - * Base44 sent [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `revoked`. + * Your server or an admin ended the session, or its key lost access. * * Open a new session from your server if the page should still follow apps. Not retryable. */ | "session_revoked" /** - * Base44 sent [`session.ended`](/developers/references/platform-sdk/live-updates/events#session-ended) with `replaced`: another socket connected with the same token. + * Another socket connected with the same session token. * * Use one session per tab. Don't reconnect automatically; call `connect()` only to take the session back on purpose. Not retryable. */ @@ -52,19 +52,19 @@ export type SessionErrorCode = */ export type AppErrorCode = /** - * Base44 sent [`room.access_denied`](/developers/references/platform-sdk/live-updates/events#room-access_denied). + * The app isn't on the session's allowlist, or the socket joins too often. * * Add the app to the session from your server, then subscribe again. Not retryable. */ | "access_denied" /** - * Base44 sent [`room.access_revoked`](/developers/references/platform-sdk/live-updates/events#room-access_revoked). + * The app left the session's allowlist, or its workspace. * * Stop showing the app's live updates. Not retryable. */ | "access_revoked" /** - * Base44 sent [`room.snapshot_unavailable`](/developers/references/platform-sdk/live-updates/events#room-snapshot_unavailable). The subscription stays active and events keep arriving. + * The subscription is active but its snapshot failed. Events keep arriving. * * Load the app's messages over the Apps API instead. */ diff --git a/packages/platform/src/modules/builder.events.types.ts b/packages/platform/src/modules/builder.events.types.ts index 09630209..b6f34dad 100644 --- a/packages/platform/src/modules/builder.events.types.ts +++ b/packages/platform/src/modules/builder.events.types.ts @@ -16,7 +16,7 @@ export type PlatformEventMap = { * * Check `type` to narrow `data` to that event's payload. A key missing from `data` means the value * is unchanged, and an explicit `null` clears it. For every event and its payload, see the - * [events reference](/developers/references/platform-sdk/live-updates/events). + * [events reference](/developers/references/platform-sdk/docs/events/builder-events). * * @example * ```typescript diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index 3a2a0366..3f5eebf8 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -127,7 +127,7 @@ export interface BuilderSession { /** Callbacks for one app subscription. */ export interface SubscriptionOptions { /** - * Called with the app's current state. Replace everything you hold for the app with it. + * Called with the app's current state, a [`PlatformSnapshot`](#platformsnapshot). Replace everything you hold for the app with it. * * Arrives after every join and rejoin, and after the main conversation is rewritten by an undo, * restore or sync. Live events follow it. It holds only the last 50 messages, so merge by message @@ -138,7 +138,7 @@ export interface SubscriptionOptions { * Called with each live event, a [`PlatformEvent`](#platformevent). Events for an app arrive one at a time, in order. If the callback * throws or rejects, the subscription ends with `handler_failed`. New event types can be added at * any time, so ignore types you don't handle. For every event and its payload, - * see the [events reference](/developers/references/platform-sdk/live-updates/events). + * see the [events reference](/developers/references/platform-sdk/docs/events/builder-events). */ onEvent: (event: PlatformEvent) => void | Promise; /** diff --git a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js index 3ba8b79f..6cea33b3 100755 --- a/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js +++ b/packages/sdk/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -2529,25 +2529,42 @@ function restructureActorsPage() { fs.writeFileSync(file, tidied, "utf-8"); } -/** Class pages: no "Extends Error", which tells a reader nothing, and properties as fields like a Returns. */ +/** Class pages: no "Extends Error", which tells a reader nothing. */ function cleanupClassPage(content) { - return propertiesAsFields(content.replace(/^### Extends\n\n(- .*\n)+\n?/m, "")); + return content.replace(/^### Extends\n\n(- .*\n)+\n?/m, ""); } -/** A class page's ## Properties as ResponseFields, the component its Returns and Parameters use. */ -function propertiesAsFields(content) { - return content.replace(/^## Properties\n([\s\S]*?)(?=^## |(?![\s\S]))/m, (section, body) => { - const fields = body.split(/^\*{3}$/m).map((block) => { - const signature = block.match(/^> (.*)$/m)?.[1] ?? ""; - const name = block.match(/^#### (.+?)\??$/m)?.[1]?.replace(/\\/g, ""); - if (!name) return ""; - const type = signature.split(/\*\*: /)[1]?.replace(/`/g, "").trim() ?? ""; - const description = block.replace(/^#### .*$/m, "").replace(/^> .*$/m, "").trim(); - const required = signature.includes("`optional`") ? "" : " required"; - return `\n\n${description}\n\n`; - }).filter(Boolean); - return `## Properties\n\n${fields.join("\n\n")}\n\n`; - }); +/** + * A class page's ## Properties, and any #### Properties (a type under Type Definitions), as + * ResponseFields, the component Returns and Parameters use, without TypeDoc's inheritance notes. + */ +function propertiesAsFields(content, classPage) { + const fieldsOf = (body) => body.split(/^\*{3}$/m).map((block) => { + const signature = block.match(/^> (.*)$/m)?.[1] ?? ""; + const name = block.match(/^#### (.+?)\??$/m)?.[1]?.replace(/\\/g, ""); + if (!name) return ""; + const type = signature.split(/\*\*: /)[1]?.replace(/`/g, "").trim() ?? ""; + const description = block.replace(/^#### .*$/m, "").replace(/^> .*$/m, "").trim(); + const required = signature.includes("`optional`") ? "" : " required"; + return `\n\n${description}\n\n`; + }).filter(Boolean).join("\n\n"); + return content + .replace(/^#### Inherited from\n\n.*\n\n?/gm, "") + .replace(/^#### Extends\n\n(- .*\n)+\n?/gm, "") + .replace(classPage ? /^## Properties\n([\s\S]*?)(?=^## |(?![\s\S]))/m : /(?!)/, (section, body) => `## Properties\n\n${fieldsOf(body)}\n\n`) + .replace(/^#### Properties\n([\s\S]*?)(?=^#{1,3} |(?![\s\S]))/gm, (section, body) => `${fieldsOf(body)}\n\n`); +} + +function applyPropertiesAsFields(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) applyPropertiesAsFields(entryPath); + else if (entry.name.endsWith(".mdx")) { + const content = fs.readFileSync(entryPath, "utf-8"); + const converted = propertiesAsFields(content, path.basename(dir) === "classes"); + if (converted !== content) fs.writeFileSync(entryPath, converted, "utf-8"); + } + } } function applyClassPageCleanup(dir) { @@ -2653,6 +2670,7 @@ function main() { applyTypeDefinitionGrouping(DOCS_DIR); applyClassPageCleanup(DOCS_DIR); + applyPropertiesAsFields(DOCS_DIR); // Reorder methods according to method-order.json applyMethodOrdering(DOCS_DIR); diff --git a/packages/sdk/writing-docs.md b/packages/sdk/writing-docs.md index 60f6a461..877c0442 100644 --- a/packages/sdk/writing-docs.md +++ b/packages/sdk/writing-docs.md @@ -65,5 +65,5 @@ The same pipeline documents the Platform SDK (`packages/platform`). Pass `--pack From `packages/platform`: * `npm run docs` validates that every export has JSDoc. Its output in `docs` is not published. -* `npm run create-docs` generates the Mintlify reference into `docs/content`. +* `npm run create-docs` generates the Mintlify reference into `docs/content`, plus the builder events page (`docs/content/events`), rendered from `asyncapi.json` for the events that reach `onEvent`. * `npm run copy-docs-local -- --target ` copies it into `developers/references/platform-sdk/docs` and updates the **Platform SDK** dropdown in `docs.json`. From dd8b394596ffc65ad839f5c679dae277ba7beef0 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Mon, 12 Oct 2026 16:54:34 +0300 Subject: [PATCH 31/32] fix(platform): PlatformSnapshot's queue is optional, as decodeSnapshot delivers it decodeSnapshot leaves queue out when the server sends none or null, but PlatformSnapshot inherited Snapshot's required queue, so snapshot.queue.items compiled and threw. The ToolCall JSDoc says that narrowing by name keeps OtherToolCall in the union. Co-Authored-By: Claude Opus 5.5 --- packages/platform/src/modules/builder.events.types.ts | 11 ++++++++--- packages/platform/tests/types/client.types.ts | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/platform/src/modules/builder.events.types.ts b/packages/platform/src/modules/builder.events.types.ts index b6f34dad..373ea2ac 100644 --- a/packages/platform/src/modules/builder.events.types.ts +++ b/packages/platform/src/modules/builder.events.types.ts @@ -1,4 +1,4 @@ -import type { Message, ServerEventMap, Snapshot } from "./builder.events.generated.js"; +import type { Message, QueueState, ServerEventMap, Snapshot } from "./builder.events.generated.js"; /** * Maps each live event name to the type of its `data` payload. @@ -46,12 +46,17 @@ export type PlatformEvent = { * Arrives after every join and rejoin, and after a rewrite of the main conversation. Replace * everything you hold for the app with it. It holds only the last 50 messages. */ -export interface PlatformSnapshot extends Snapshot { +export interface PlatformSnapshot extends Omit { /** The app's room, `/apps/{app_id}`. */ room: string; + /** The main branch's prompt queue. Missing when the server sends none. */ + queue?: QueueState; } -/** One tool call in a message's `tool_calls`. Check `name` to narrow it to that tool's call. */ +/** + * One tool call in a message's `tool_calls`. Checking `name` narrows it to that tool's call and + * `OtherToolCall`, which covers tools the SDK doesn't know, so check a field with `in` before using it. + */ export type ToolCall = NonNullable[number]; /** Why a safety guard parked a tool call for approval. Check `guard` to narrow it. */ diff --git a/packages/platform/tests/types/client.types.ts b/packages/platform/tests/types/client.types.ts index 59631019..695fcfab 100644 --- a/packages/platform/tests/types/client.types.ts +++ b/packages/platform/tests/types/client.types.ts @@ -6,6 +6,8 @@ const subscription = builder.subscribe("a".repeat(24), { const state: "ready" | "processing" | "error" | null | undefined = snapshot.status?.state; const room: string = snapshot.room; void room; + void snapshot.queue?.items; + // @ts-expect-error A snapshot may arrive without a queue. void snapshot.queue.items; void state; void snapshot.messages[0]?.additional_message_params?.plan_mode; From 419e113a8d06cf195665410435c8679ec35c81a7 Mon Sep 17 00:00:00 2001 From: amitbardos Date: Mon, 12 Oct 2026 17:14:58 +0300 Subject: [PATCH 32/32] docs(platform): leave the Socket messages page to mintlify-docs The events page documents the service's protocol, so mintlify-docs builds it from the AsyncAPI document production serves, as it does the REST references. events-to-mdx.mjs, create-docs:events and the Events nav category are removed, and the JSDoc links point to /developers/references/platform-sdk/socket-messages. Co-Authored-By: Claude Opus 5.5 --- packages/platform/README.md | 6 +- packages/platform/package.json | 5 +- packages/platform/scripts/events-to-mdx.mjs | 225 ------------------ .../category-map.json | 3 +- .../src/modules/builder.events.types.ts | 4 +- .../platform/src/modules/builder.types.ts | 2 +- packages/sdk/writing-docs.md | 2 +- 7 files changed, 10 insertions(+), 237 deletions(-) delete mode 100644 packages/platform/scripts/events-to-mdx.mjs diff --git a/packages/platform/README.md b/packages/platform/README.md index 9acdae05..451ac8a5 100644 --- a/packages/platform/README.md +++ b/packages/platform/README.md @@ -136,11 +136,11 @@ generated file differs from what `asyncapi.json` produces. ## Reference docs Every exported shape and field has JSDoc. `npm run docs` validates it into `docs`. -`npm run create-docs` runs the app SDK's TypeDoc and Mintlify pipeline for this package, then renders -the builder events page from `asyncapi.json` with `scripts/events-to-mdx.mjs`, +`npm run create-docs` runs the app SDK's TypeDoc and Mintlify pipeline for this package, and `npm run copy-docs-local -- --target ` copies the result into `developers/references/platform-sdk/docs`. The pipeline's config for this package lives -in `scripts/mintlify-post-processing/`. +in `scripts/mintlify-post-processing/`. The Socket messages page is built in mintlify-docs from +the service's AsyncAPI document, not here. `PlatformSocketError` extends `Error` with `code` and optional `appId`. Original exceptions, payloads and credentials are never attached. diff --git a/packages/platform/package.json b/packages/platform/package.json index b9efb036..4b42e101 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -29,12 +29,11 @@ "test:unit": "vitest run", "test:package": "npm run build && node --test tests/package.test.mjs", "prepublishOnly": "npm run build", - "create-docs": "npm run create-docs:generate && npm run create-docs:process && npm run create-docs:events", + "create-docs": "npm run create-docs:generate && npm run create-docs:process", "create-docs-local": "npm run create-docs && npm run copy-docs-local", "copy-docs-local": "node ../sdk/scripts/mintlify-post-processing/copy-to-local-docs.js --package-dir .", "create-docs:generate": "node -e \"fs.rmSync('docs',{recursive:true,force:true})\" && typedoc --options typedoc.mintlify.json", - "create-docs:process": "node ../sdk/scripts/mintlify-post-processing/file-processing/file-processing.js --package-dir .", - "create-docs:events": "tsc && node scripts/events-to-mdx.mjs" + "create-docs:process": "node ../sdk/scripts/mintlify-post-processing/file-processing/file-processing.js --package-dir ." }, "dependencies": { "socket.io-client": "^4.8.3" diff --git a/packages/platform/scripts/events-to-mdx.mjs b/packages/platform/scripts/events-to-mdx.mjs deleted file mode 100644 index 0e03bf50..00000000 --- a/packages/platform/scripts/events-to-mdx.mjs +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env node -// Renders the events that reach onEvent as an MDX page beside the TypeDoc pages, from the committed -// asyncapi.json, with the ResponseField / Expandable components the rest of the reference uses. -// The event list is the SDK's own (eventNames), so the page and the types come from one copy. -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { eventNames } from "../dist/modules/builder-protocol.js"; - -const MAX_DEPTH = 8; -const source = new URL("../asyncapi.json", import.meta.url); -const target = new URL("../docs/content/events/builder-events.mdx", import.meta.url); -const doc = JSON.parse(readFileSync(source, "utf8")); -const PLATFORM_EVENT = "/developers/references/platform-sdk/docs/interfaces/builder#platformevent"; - -function resolvePointer(ref) { - if (!ref.startsWith("#/")) throw new Error(`Only local $refs are supported: ${ref}`); - return ref - .slice(2) - .split("/") - .map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~")) - .reduce((node, key) => node?.[key], doc); -} - -/** Follow $refs; sibling keys on the referring node win. Returns [schema, refNames]. */ -function deref(schema) { - const names = []; - let s = schema ?? {}; - while (s && typeof s === "object" && s.$ref) { - names.push(s.$ref); - const { $ref, ...rest } = s; - s = { ...resolvePointer($ref), ...rest }; - } - return [s, names]; -} - -const isNull = (s) => deref(s)[0].type === "null"; - -/** Union members, without null, plus whether null was one of them. */ -function unionOf(s) { - const members = s.anyOf ?? s.oneOf; - if (!Array.isArray(members)) return null; - const flat = members.flatMap((m) => { - const inner = unionOf(deref(m)[0]); - return inner ? [...inner.members, ...(inner.nullable ? [{ type: "null" }] : [])] : [m]; - }); - return { members: flat.filter((m) => !isNull(m)), nullable: flat.some(isNull) }; -} - -function typesOf(s) { - const t = s.type; - const list = Array.isArray(t) ? t : t ? [t] : []; - return { list: list.filter((x) => x !== "null"), nullable: list.includes("null") }; -} - -const isObject = (s) => { - const [d] = deref(s); - return d.type === "object" || (Array.isArray(d.type) && d.type.includes("object")) || !!d.properties; -}; - -const quote = (v) => (typeof v === "string" ? `'${v}'` : JSON.stringify(v)); - -/** The type label, e.g. `string | null`, `'a' | 'b'`, `object[]`. */ -function typeLabel(schema) { - const [s] = deref(schema); - const union = unionOf(s); - if (union) { - const parts = [...new Set(union.members.map(typeLabel))]; - return [...parts, ...(union.nullable ? ["null"] : [])].join(" | ") || "null"; - } - const { list, nullable } = typesOf(s); - let label; - if ("const" in s) label = quote(s.const); - else if (Array.isArray(s.enum)) label = s.enum.map(quote).join(" | "); - else if (list.includes("array")) { - const inner = typeLabel(s.items ?? {}); - label = `${inner.includes(" | ") ? `(${inner})` : inner}[]`; - } else if (list.length) label = list.map((t) => (t === "string" && s.format ? `string<${s.format}>` : t)).join(" | "); - else label = s.properties ? "object" : "any"; - return nullable && !label.endsWith("| null") ? `${label} | null` : label; -} - -/** Escape MDX-significant characters outside inline code spans. */ -function prose(text) { - return String(text ?? "") - .split(/(`[^`]*`)/) - .map((part, i) => (i % 2 ? part : part.replace(/[{}]/g, (c) => `\\${c}`).replace(/ String(v).replace(/&/g, "&").replace(/"/g, """); - -/** Notes beyond the description: allowed values and constraints. */ -function constraints(s) { - const notes = []; - if (s.not && Array.isArray(s.not.enum)) notes.push("Any value not matched by the other shapes."); - if (s.pattern) notes.push(`Pattern: \`${s.pattern}\``); - for (const [k, word] of [["minimum", "Minimum"], ["maximum", "Maximum"], ["minLength", "Minimum length"], ["maxLength", "Maximum length"], ["maxItems", "Maximum items"]]) { - if (k in s) notes.push(`${word}: \`${s[k]}\``); - } - return notes; -} - -/** The object schema whose fields a value shows: itself, or an array's item. */ -function containerOf(schema) { - const [s] = deref(schema); - if (typesOf(s).list.includes("array")) return containerOf(s.items ?? {}); - const union = unionOf(s); - if (union) { - const objects = union.members.filter(isObject); - if (objects.length === 1) return containerOf(objects[0]); - if (objects.length > 1) return { variants: objects }; - return null; - } - return isObject(s) ? { object: schema } : null; -} - -/** The property every variant pins with a distinct `const`, if any. */ -function discriminator(variants) { - const resolved = variants.map((v) => deref(v)[0]); - const keys = Object.keys(resolved[0].properties ?? {}); - return keys.find((k) => { - const consts = resolved.map((v) => deref(v.properties?.[k])[0]).filter((p) => p && "const" in p).map((p) => p.const); - return consts.length >= resolved.length - 1 && new Set(consts).size === consts.length && consts.length > 1; - }); -} - -const humanize = (title) => title.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase(); - -function variantTitle(variant, key) { - const [v, refs] = deref(variant); - const pinned = key && deref(v.properties?.[key])[0]; - if (pinned && "const" in pinned) return String(pinned.const); - const title = v.title ?? refs.at(-1)?.split("/").pop() ?? "variant"; - const words = humanize(title); - return words.startsWith("other ") ? `any ${words}` : words; -} - -function fieldsOf(objectSchema, depth, path) { - const [s] = deref(objectSchema); - const required = new Set(s.required ?? []); - return Object.entries(s.properties ?? {}).map(([name, p]) => field(name, p, required.has(name), depth, path)); -} - -function children(schema, depth, path) { - const [, refs] = deref(schema); - if (depth >= MAX_DEPTH || refs.some((r) => path.includes(r))) return ""; - const next = [...path, ...refs]; - const container = containerOf(schema); - if (!container) return ""; - if (container.object) { - const innerRefs = deref(container.object)[1].filter((r) => !refs.includes(r)); - if (innerRefs.some((r) => next.includes(r))) return ""; - const inner = fieldsOf(container.object, depth + 1, [...next, ...innerRefs]); - return inner.length ? `\n${inner.join("\n\n")}\n` : ""; - } - const key = discriminator(container.variants); - const shared = sharedFields(container.variants, key); - const out = []; - if (shared.length) { - const [first] = deref(container.variants[0]); - const required = new Set(first.required ?? []); - const inner = shared.map((name) => field(name, first.properties[name], required.has(name), depth + 1, next)); - out.push("Every shape has these fields:", `\n${inner.join("\n\n")}\n`); - } - const which = key ? `, told apart by \`${key}\`` : ""; - out.push(shared.length ? `Each shape adds its own fields${which}:` : `One of these shapes${which}:`); - for (const variant of container.variants) { - const [v, innerRefs] = deref(variant); - const body = [prose(v.description)]; - if (!innerRefs.some((r) => next.includes(r))) { - const required = new Set(v.required ?? []); - const own = Object.entries(v.properties ?? {}).filter(([name]) => !shared.includes(name)); - body.push(...own.map(([name, p]) => field(name, p, required.has(name), depth + 1, [...next, ...innerRefs]))); - } - out.push(`\n${body.filter(Boolean).join("\n\n")}\n`); - } - return out.join("\n\n"); -} - -/** Fields every variant declares identically (same schema, same required-ness), so they're listed once. */ -function sharedFields(variants, key) { - const resolved = variants.map((v) => deref(v)[0]); - const sig = (v, name) => JSON.stringify([v.properties?.[name], (v.required ?? []).includes(name)]); - return Object.keys(resolved[0].properties ?? {}).filter( - (name) => name !== key && resolved.every((v) => v.properties && name in v.properties && sig(v, name) === sig(resolved[0], name)), - ); -} - -function field(name, schema, required, depth, path) { - const [s] = deref(schema); - const attrs = [`name="${attr(name)}"`, `type="${attr(typeLabel(schema))}"`]; - if (s.default !== undefined && s.default !== null) attrs.push(`default="${attr(JSON.stringify(s.default))}"`); - if (required) attrs.push("required"); - if (s.deprecated) attrs.push("deprecated"); - const body = [prose(s.description), ...constraints(s).map(prose), children(schema, depth, path)].filter(Boolean); - if (!body.length) return ``; - return `\n\n${body.join("\n\n")}\n\n`; -} - -const messages = Object.fromEntries(Object.values(doc.components.messages).map((m) => [m.name, m])); -const lines = [ - "---", - 'title: "Builder events"', - 'description: "Every live event a builder subscription delivers to onEvent, and its payload."', - 'sidebarTitle: "Builder events"', - "---", - "", - "{/* Generated from packages/platform/asyncapi.json by scripts/events-to-mdx.mjs in base44/javascript-sdk. */}", - "", - `Each event reaches your \`onEvent\` callback as a [\`PlatformEvent\`](${PLATFORM_EVENT}): \`{ type, appId, data }\`. ` + - "Check `type` to narrow `data` to that event's payload, listed below. A key missing from `data` means " + - "the value is unchanged, and an explicit `null` clears it.", - "", -]; -for (const name of eventNames) { - const message = messages[name]; - const [frame] = deref(message.payload); - const data = frame.properties.data; - lines.push(`## \`${name}\``, "", prose(message.description ?? message.summary), ""); - const fields = deref(data)[0].properties ? fieldsOf(data, 0, deref(data)[1]) : []; - lines.push(fields.length ? fields.join("\n\n") : "`data` is empty.", ""); -} -mkdirSync(new URL(".", target), { recursive: true }); -writeFileSync(target, lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n"); -console.log(`Wrote ${eventNames.length} events to docs/content/events/builder-events.mdx`); diff --git a/packages/platform/scripts/mintlify-post-processing/category-map.json b/packages/platform/scripts/mintlify-post-processing/category-map.json index 107f3f6c..06493959 100644 --- a/packages/platform/scripts/mintlify-post-processing/category-map.json +++ b/packages/platform/scripts/mintlify-post-processing/category-map.json @@ -2,6 +2,5 @@ "functions": "Client", "interfaces": "Modules", "type-aliases": "Modules", - "classes": "Errors", - "events": "Events" + "classes": "Errors" } diff --git a/packages/platform/src/modules/builder.events.types.ts b/packages/platform/src/modules/builder.events.types.ts index 373ea2ac..b3d46e7c 100644 --- a/packages/platform/src/modules/builder.events.types.ts +++ b/packages/platform/src/modules/builder.events.types.ts @@ -15,8 +15,8 @@ export type PlatformEventMap = { * One live event for a subscribed app. * * Check `type` to narrow `data` to that event's payload. A key missing from `data` means the value - * is unchanged, and an explicit `null` clears it. For every event and its payload, see the - * [events reference](/developers/references/platform-sdk/docs/events/builder-events). + * is unchanged, and an explicit `null` clears it. For every event and its payload, see + * [Socket messages](/developers/references/apps-api/sections/socket-messages). * * @example * ```typescript diff --git a/packages/platform/src/modules/builder.types.ts b/packages/platform/src/modules/builder.types.ts index 3f5eebf8..81128234 100644 --- a/packages/platform/src/modules/builder.types.ts +++ b/packages/platform/src/modules/builder.types.ts @@ -138,7 +138,7 @@ export interface SubscriptionOptions { * Called with each live event, a [`PlatformEvent`](#platformevent). Events for an app arrive one at a time, in order. If the callback * throws or rejects, the subscription ends with `handler_failed`. New event types can be added at * any time, so ignore types you don't handle. For every event and its payload, - * see the [events reference](/developers/references/platform-sdk/docs/events/builder-events). + * see [Socket messages](/developers/references/apps-api/sections/socket-messages). */ onEvent: (event: PlatformEvent) => void | Promise; /** diff --git a/packages/sdk/writing-docs.md b/packages/sdk/writing-docs.md index 877c0442..60f6a461 100644 --- a/packages/sdk/writing-docs.md +++ b/packages/sdk/writing-docs.md @@ -65,5 +65,5 @@ The same pipeline documents the Platform SDK (`packages/platform`). Pass `--pack From `packages/platform`: * `npm run docs` validates that every export has JSDoc. Its output in `docs` is not published. -* `npm run create-docs` generates the Mintlify reference into `docs/content`, plus the builder events page (`docs/content/events`), rendered from `asyncapi.json` for the events that reach `onEvent`. +* `npm run create-docs` generates the Mintlify reference into `docs/content`. * `npm run copy-docs-local -- --target ` copies it into `developers/references/platform-sdk/docs` and updates the **Platform SDK** dropdown in `docs.json`.