@base44-preview/platform 0.1.0-pr.316.56344d6 → 0.1.0-pr.319.1e807d1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,9 +10,9 @@ shared socket. It is independent of the app SDK (`@base44/sdk`).
10
10
  The workspace must have white-label sockets enabled; this SDK does not open sessions.
11
11
 
12
12
  ```ts
13
- import { Base44PlatformClient } from "@base44/platform";
13
+ import { createBase44PlatformClient } from "@base44/platform";
14
14
 
15
- const client = new Base44PlatformClient({
15
+ const client = createBase44PlatformClient({
16
16
  serverUrl: "https://base44.app",
17
17
  async getSessionToken() {
18
18
  const response = await fetch("/api/builder-socket-session", { method: "POST" });
@@ -34,6 +34,60 @@ subscription.unsubscribe();
34
34
  builder.close();
35
35
  ```
36
36
 
37
+ ## Receiving webhooks (server)
38
+
39
+ `@base44/platform/server` verifies Base44 outbound webhooks and answers the endpoint
40
+ activation challenge. It uses WebCrypto only (Node 20+, Deno, Bun, Workers, edge runtimes) and
41
+ makes no network calls: pass your workspace's public keys (`whpk_…`) explicitly.
42
+
43
+ The simplest route is `createHandler`. It reads the raw body, verifies it, answers the
44
+ activation challenge and picks the status code: 400 for a delivery that fails verification,
45
+ 500 when your keys are misconfigured or a callback throws (Base44 retries), 204 otherwise,
46
+ including for event types you have no handler for.
47
+
48
+ ```ts
49
+ import { createBase44PlatformServer } from "@base44/platform/server";
50
+
51
+ const platform = createBase44PlatformServer({
52
+ serverUrl: "https://base44.app",
53
+ apiKey: process.env.BASE44_API_KEY!,
54
+ webhooks: { publicKeys: process.env.BASE44_WEBHOOK_PUBLIC_KEYS },
55
+ });
56
+
57
+ export const POST = platform.webhooks.createHandler({
58
+ on: {
59
+ "app.deleted.v1": async event => forget(event.data.app_id), // event is narrowed per type
60
+ },
61
+ onEvent: event => log(event.type), // every event, before its handler
62
+ onSuccess: event => {}, // return a Response to replace the 204
63
+ onError: (error, event) => report(error), // return a Response to replace the 400/500
64
+ });
65
+ ```
66
+
67
+ For full control, verify and dispatch yourself:
68
+
69
+ ```ts
70
+ export async function POST(request: Request) {
71
+ const rawBody = await request.text(); // exactly as received
72
+ let event;
73
+ try {
74
+ event = await platform.webhooks.verifyEvent(rawBody, request.headers);
75
+ } catch (error) {
76
+ if (error instanceof WebhookVerificationError) return new Response(null, { status: 400 });
77
+ throw error; // missing/malformed keys: a 500, so Base44 retries
78
+ }
79
+ switch (event.type) {
80
+ case "webhook.test.v1": return Response.json({ challenge: event.data.challenge }); // activates the endpoint
81
+ case "app.deleted.v1": await forget(event.data.app_id); break;
82
+ }
83
+ return new Response(null, { status: 204 });
84
+ }
85
+ ```
86
+
87
+ Delivery is at least once with no ordering guarantee: deduplicate on `event.id`, and answer
88
+ 2xx to event types you do not handle. Event types are generated from the schemas published at
89
+ `https://base44.com/webhooks/schemas`; run `npm run generate:webhooks` to refresh them.
90
+
37
91
  ## Sessions
38
92
 
39
93
  `/api/builder-socket-session` is your backend's route, not an SDK endpoint. It opens a
@@ -55,7 +109,7 @@ older one, which stops with `session_replaced`. Open one session per page.
55
109
 
56
110
  ## Lifecycle
57
111
 
58
- `new Base44PlatformClient({ serverUrl, getSessionToken })` creates a lightweight module
112
+ `createBase44PlatformClient({ serverUrl, getSessionToken })` returns a lightweight module
59
113
  container: no Socket.IO instance, timers, token request or network activity. Its
60
114
  `builder` module follows the server SDK's module-factory pattern.
61
115
 
@@ -1,9 +1,3 @@
1
- import type { PlatformClientOptions } from "./client.types.js";
2
- import type { BuilderModule } from "./modules/builder.types.js";
3
- /** Browser platform client. Construction creates no sockets, timers or network requests. */
4
- export declare class Base44PlatformClient {
5
- /** Lazy builder subscriptions with independent session lifecycles. */
6
- readonly builder: BuilderModule;
7
- /** Configure shared service/auth settings; each module initializes its own resources. */
8
- constructor(options: PlatformClientOptions);
9
- }
1
+ import type { Base44PlatformClient, PlatformClientOptions } from "./client.types.js";
2
+ /** Create a browser platform client. Creates no sockets, timers or network requests; each module initializes its own resources. */
3
+ export declare function createBase44PlatformClient(options: PlatformClientOptions): Base44PlatformClient;
@@ -1,9 +1,8 @@
1
1
  import { normalizeServerUrl } from "../shared/server-url.js";
2
2
  import { createBuilder } from "./modules/builder.js";
3
- /** Browser platform client. Construction creates no sockets, timers or network requests. */
4
- export class Base44PlatformClient {
5
- /** Configure shared service/auth settings; each module initializes its own resources. */
6
- constructor(options) {
7
- this.builder = Object.freeze(createBuilder({ ...options, serverUrl: normalizeServerUrl(options.serverUrl) }));
8
- }
3
+ /** Create a browser platform client. Creates no sockets, timers or network requests; each module initializes its own resources. */
4
+ export function createBase44PlatformClient(options) {
5
+ return Object.freeze({
6
+ builder: Object.freeze(createBuilder({ ...options, serverUrl: normalizeServerUrl(options.serverUrl) })),
7
+ });
9
8
  }
@@ -1,3 +1,4 @@
1
+ import type { BuilderModule } from "./modules/builder.types.js";
1
2
  /** Shared configuration for browser platform modules. Never supply an API key. */
2
3
  export interface PlatformClientOptions {
3
4
  /** Origin of the platform service, e.g. https://base44.app. No path/query/credentials. */
@@ -10,3 +11,8 @@ export interface PlatformClientOptions {
10
11
  */
11
12
  getSessionToken: () => string | Promise<string>;
12
13
  }
14
+ /** Browser platform client, returned by `createBase44PlatformClient`. */
15
+ export interface Base44PlatformClient {
16
+ /** Lazy builder subscriptions with independent session lifecycles. */
17
+ readonly builder: BuilderModule;
18
+ }
@@ -1,7 +1,7 @@
1
1
  /** Browser platform modules, separate from the runtime and server SDKs. */
2
- export { Base44PlatformClient } from "./client.js";
2
+ export { createBase44PlatformClient } from "./client.js";
3
3
  export { PlatformSocketError } from "./errors.js";
4
4
  export type { PlatformSocketErrorCode } from "./errors.types.js";
5
- export type { PlatformClientOptions } from "./client.types.js";
5
+ export type { Base44PlatformClient, PlatformClientOptions } from "./client.types.js";
6
6
  export type { BuilderModule, BuilderInitOptions, BuilderSession, PlatformSubscription, SubscriptionOptions } from "./modules/builder.types.js";
7
7
  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";
@@ -1,3 +1,3 @@
1
1
  /** Browser platform modules, separate from the runtime and server SDKs. */
2
- export { Base44PlatformClient } from "./client.js";
2
+ export { createBase44PlatformClient } from "./client.js";
3
3
  export { PlatformSocketError } from "./errors.js";
@@ -0,0 +1,15 @@
1
+ import type { WebhookConfigurationErrorCode, WebhookVerificationErrorCode } from "./errors.types.js";
2
+ /** A webhook delivery that cannot be trusted. The message never includes the body or signature. */
3
+ export declare class WebhookVerificationError extends Error {
4
+ /** Stable machine-readable reason. */
5
+ readonly code: WebhookVerificationErrorCode;
6
+ /** Create an error carrying only the reason. */
7
+ constructor(code: WebhookVerificationErrorCode);
8
+ }
9
+ /** Webhook public keys are missing or malformed, so no delivery can be verified. */
10
+ export declare class WebhookConfigurationError extends Error {
11
+ /** Stable machine-readable reason. */
12
+ readonly code: WebhookConfigurationErrorCode;
13
+ /** Create an error describing the misconfiguration. */
14
+ constructor(code: WebhookConfigurationErrorCode, detail: string);
15
+ }
@@ -0,0 +1,18 @@
1
+ /** A webhook delivery that cannot be trusted. The message never includes the body or signature. */
2
+ export class WebhookVerificationError extends Error {
3
+ /** Create an error carrying only the reason. */
4
+ constructor(code) {
5
+ super(`Webhook verification failed: ${code}`);
6
+ this.name = "WebhookVerificationError";
7
+ this.code = code;
8
+ }
9
+ }
10
+ /** Webhook public keys are missing or malformed, so no delivery can be verified. */
11
+ export class WebhookConfigurationError extends Error {
12
+ /** Create an error describing the misconfiguration. */
13
+ constructor(code, detail) {
14
+ super(`Webhook configuration: ${detail}`);
15
+ this.name = "WebhookConfigurationError";
16
+ this.code = code;
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ /** Why a delivery was refused. Respond 400: the request is not a genuine, current Base44 event. */
2
+ export type WebhookVerificationErrorCode = "missing_headers" | "timestamp_outside_tolerance" | "no_signature_entries" | "signature_mismatch" | "invalid_payload";
3
+ /** Why verification could not run at all. This is your deployment, not the request: let it surface as a 500 so Base44 retries. */
4
+ export type WebhookConfigurationErrorCode = "missing_public_keys" | "invalid_public_key";
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,6 @@
1
1
  /** Server platform modules, authenticated with a workspace API key. */
2
- export { Base44PlatformServer } from "./server.js";
3
- export type { PlatformServerOptions } from "./server.types.js";
2
+ export { createBase44PlatformServer } from "./server.js";
3
+ export { WebhookConfigurationError, WebhookVerificationError } from "./errors.js";
4
+ export type { WebhookConfigurationErrorCode, WebhookVerificationErrorCode } from "./errors.types.js";
5
+ export type { Base44PlatformServer, PlatformServerOptions } from "./server.types.js";
6
+ export type { AppCreatedV1Data, AppDeletedV1Data, AppPublishedV1Data, AppRestoredV1Data, AppUnpublishedV1Data, CloudEvent, CreateHandlerOptions, DeliveredWebhookEvent, VerifyEventOptions, WebhookEventHandlers, WebhookEvent, WebhookEventMap, WebhookEventType, WebhookHeaders, WebhookPublicKeys, WebhooksModule, WebhooksOptions, WebhookTestV1Data, } from "./modules/webhooks.types.js";
@@ -1,2 +1,3 @@
1
1
  /** Server platform modules, authenticated with a workspace API key. */
2
- export { Base44PlatformServer } from "./server.js";
2
+ export { createBase44PlatformServer } from "./server.js";
3
+ export { WebhookConfigurationError, WebhookVerificationError } from "./errors.js";
@@ -0,0 +1,11 @@
1
+ import type { WebhookHeaders, WebhookPublicKeys } from "./webhooks.types.js";
2
+ /** Split the configured keys into raw 32-byte Ed25519 keys. A truncated paste throws: a wrong key reads as every event being forged. */
3
+ export declare function parsePublicKeys(keys: WebhookPublicKeys | undefined): Uint8Array[];
4
+ export declare function importPublicKey(raw: Uint8Array): Promise<CryptoKey>;
5
+ /** Read one header from a `Headers` or a Node-style object. */
6
+ export declare function readHeader(headers: WebhookHeaders, name: string, separator?: string): string | null;
7
+ /** The `v1a,<base64>` entries of a `webhook-signature` header, decoded. */
8
+ export declare function parseSignatures(header: string): Uint8Array[];
9
+ /** The signed bytes: `<id>.<timestamp>.<raw body>`. */
10
+ export declare function signedPayload(id: string, timestamp: string, body: string | Uint8Array): Uint8Array;
11
+ export declare function anySignatureVerifies(keys: CryptoKey[], signatures: Uint8Array[], payload: Uint8Array): Promise<boolean>;
@@ -0,0 +1,65 @@
1
+ import { WebhookConfigurationError } from "../errors.js";
2
+ const SIGNATURE_SCHEME = "v1a";
3
+ const encoder = new TextEncoder();
4
+ /** Standard base64 to bytes, or `null` when it is not valid base64. */
5
+ function decodeBase64(value) {
6
+ try {
7
+ const binary = atob(value);
8
+ return Uint8Array.from(binary, char => char.charCodeAt(0));
9
+ }
10
+ catch (_a) {
11
+ return null;
12
+ }
13
+ }
14
+ /** Split the configured keys into raw 32-byte Ed25519 keys. A truncated paste throws: a wrong key reads as every event being forged. */
15
+ export function parsePublicKeys(keys) {
16
+ const entries = (Array.isArray(keys) ? keys : (keys !== null && keys !== void 0 ? keys : "").split(/[\s,]+/)).map(key => key.trim()).filter(Boolean);
17
+ if (entries.length === 0)
18
+ throw new WebhookConfigurationError("missing_public_keys", "no public keys were provided");
19
+ return entries.map(entry => {
20
+ const raw = decodeBase64(entry.replace(/^whpk_/, ""));
21
+ if (!raw || raw.length !== 32) {
22
+ throw new WebhookConfigurationError("invalid_public_key", "a public key is not a base64-encoded 32-byte Ed25519 key");
23
+ }
24
+ return raw;
25
+ });
26
+ }
27
+ export function importPublicKey(raw) {
28
+ return crypto.subtle.importKey("raw", raw, "Ed25519", false, ["verify"]);
29
+ }
30
+ /** Read one header from a `Headers` or a Node-style object. */
31
+ export function readHeader(headers, name, separator = ", ") {
32
+ if (typeof headers.get === "function")
33
+ return headers.get(name);
34
+ for (const [key, value] of Object.entries(headers)) {
35
+ if (key.toLowerCase() === name && value !== undefined)
36
+ return Array.isArray(value) ? value.join(separator) : value;
37
+ }
38
+ return null;
39
+ }
40
+ /** The `v1a,<base64>` entries of a `webhook-signature` header, decoded. */
41
+ export function parseSignatures(header) {
42
+ return header
43
+ .split(" ")
44
+ .filter(entry => entry.startsWith(`${SIGNATURE_SCHEME},`))
45
+ .map(entry => decodeBase64(entry.slice(SIGNATURE_SCHEME.length + 1)))
46
+ .filter((signature) => signature !== null);
47
+ }
48
+ /** The signed bytes: `<id>.<timestamp>.<raw body>`. */
49
+ export function signedPayload(id, timestamp, body) {
50
+ const prefix = encoder.encode(`${id}.${timestamp}.`);
51
+ const bytes = typeof body === "string" ? encoder.encode(body) : body;
52
+ const payload = new Uint8Array(prefix.length + bytes.length);
53
+ payload.set(prefix);
54
+ payload.set(bytes, prefix.length);
55
+ return payload;
56
+ }
57
+ export async function anySignatureVerifies(keys, signatures, payload) {
58
+ for (const key of keys) {
59
+ for (const signature of signatures) {
60
+ if (await crypto.subtle.verify("Ed25519", key, signature, payload))
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
@@ -0,0 +1,3 @@
1
+ import type { WebhooksModule, WebhooksOptions } from "./webhooks.types.js";
2
+ /** Webhook receiving utilities. Verification is local: no network calls, no timers. */
3
+ export declare function createWebhooks(options?: WebhooksOptions): WebhooksModule;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * The `data` object of `app.created.v1`. An app was created.
3
+ */
4
+ export interface AppCreatedV1Data {
5
+ /**
6
+ * The app the event is about.
7
+ */
8
+ app_id: string;
9
+ /**
10
+ * The workspace the app belongs to.
11
+ */
12
+ workspace_id: string;
13
+ /**
14
+ * Opaque platform id of the app's current owner.
15
+ */
16
+ owner_id: string;
17
+ /**
18
+ * Your own handle for the owner: the `service_external_id` you gave when you created that service user. Absent when the owner is not a service user.
19
+ */
20
+ owner_service_external_id?: string;
21
+ /**
22
+ * The kind of app. New values arrive as a new event version.
23
+ */
24
+ app_type: "imported_app" | "user_agent" | "user_app" | "user_game";
25
+ /**
26
+ * When the app was created (UTC).
27
+ */
28
+ created_at: string;
29
+ }
30
+ /**
31
+ * The `data` object of `app.published.v1`. An app version was published.
32
+ */
33
+ export interface AppPublishedV1Data {
34
+ /**
35
+ * The app the event is about.
36
+ */
37
+ app_id: string;
38
+ /**
39
+ * The workspace the app belongs to.
40
+ */
41
+ workspace_id: string;
42
+ /**
43
+ * Opaque platform id of the app's current owner.
44
+ */
45
+ owner_id: string;
46
+ /**
47
+ * Your own handle for the owner: the `service_external_id` you gave when you created that service user. Absent when the owner is not a service user.
48
+ */
49
+ owner_service_external_id?: string;
50
+ /**
51
+ * Id of the version now live.
52
+ */
53
+ published_version: string;
54
+ /**
55
+ * When the version went live (UTC).
56
+ */
57
+ published_at: string;
58
+ }
59
+ /**
60
+ * The `data` object of `app.unpublished.v1`. An app was taken offline.
61
+ */
62
+ export interface AppUnpublishedV1Data {
63
+ /**
64
+ * The app the event is about.
65
+ */
66
+ app_id: string;
67
+ /**
68
+ * The workspace the app belongs to.
69
+ */
70
+ workspace_id: string;
71
+ /**
72
+ * Opaque platform id of the app's current owner.
73
+ */
74
+ owner_id: string;
75
+ /**
76
+ * Your own handle for the owner: the `service_external_id` you gave when you created that service user. Absent when the owner is not a service user.
77
+ */
78
+ owner_service_external_id?: string;
79
+ /**
80
+ * When the app was taken offline (UTC).
81
+ */
82
+ unpublished_at: string;
83
+ }
84
+ /**
85
+ * Tombstone identifiers only.
86
+ *
87
+ * Deletion is **not** erasure: the app moves to trash and is self-serve
88
+ * restorable for 30 days, with unbounded restore through support. A receiver
89
+ * that purges on this event will be wrong for every restored app, which is why
90
+ * ``app.restored.v1`` exists and why this is documented as a trash signal.
91
+ */
92
+ export interface AppDeletedV1Data {
93
+ /**
94
+ * The app the event is about.
95
+ */
96
+ app_id: string;
97
+ /**
98
+ * The workspace the app belongs to.
99
+ */
100
+ workspace_id: string;
101
+ /**
102
+ * Opaque platform id of the app's current owner.
103
+ */
104
+ owner_id: string;
105
+ /**
106
+ * Your own handle for the owner: the `service_external_id` you gave when you created that service user. Absent when the owner is not a service user.
107
+ */
108
+ owner_service_external_id?: string;
109
+ /**
110
+ * When the app was moved to trash (UTC). It can still be restored.
111
+ */
112
+ deleted_at: string;
113
+ }
114
+ /**
115
+ * The `data` object of `app.restored.v1`. An app was restored from trash.
116
+ */
117
+ export interface AppRestoredV1Data {
118
+ /**
119
+ * The app the event is about.
120
+ */
121
+ app_id: string;
122
+ /**
123
+ * The workspace the app belongs to.
124
+ */
125
+ workspace_id: string;
126
+ /**
127
+ * Opaque platform id of the app's current owner.
128
+ */
129
+ owner_id: string;
130
+ /**
131
+ * Your own handle for the owner: the `service_external_id` you gave when you created that service user. Absent when the owner is not a service user.
132
+ */
133
+ owner_service_external_id?: string;
134
+ /**
135
+ * When the app was restored (UTC).
136
+ */
137
+ restored_at: string;
138
+ }
139
+ /**
140
+ * Carries no domain data — its only job is to be verifiable and echoed.
141
+ */
142
+ export interface WebhookTestV1Data {
143
+ /**
144
+ * The endpoint being activated.
145
+ */
146
+ endpoint_id: string;
147
+ /**
148
+ * The workspace the endpoint belongs to.
149
+ */
150
+ workspace_id: string;
151
+ /**
152
+ * When the test was sent (UTC).
153
+ */
154
+ sent_at: string;
155
+ /**
156
+ * Return this value in your response body to activate the endpoint.
157
+ */
158
+ challenge: string;
159
+ }
160
+ /** Every event type Base44 sends, mapped to the shape of its `data` object. */
161
+ export interface WebhookEventMap {
162
+ /** An app was created. */
163
+ "app.created.v1": AppCreatedV1Data;
164
+ /** An app version was published. */
165
+ "app.published.v1": AppPublishedV1Data;
166
+ /** An app was taken offline. */
167
+ "app.unpublished.v1": AppUnpublishedV1Data;
168
+ /** Tombstone identifiers only. */
169
+ "app.deleted.v1": AppDeletedV1Data;
170
+ /** An app was restored from trash. */
171
+ "app.restored.v1": AppRestoredV1Data;
172
+ /** Carries no domain data — its only job is to be verifiable and echoed. */
173
+ "webhook.test.v1": WebhookTestV1Data;
174
+ }
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/generate-webhook-types.mjs from the schemas published at
2
+ // https://base44.com/webhooks/schemas. Do not edit by hand; run `npm run generate:webhooks`.
3
+ export {};
@@ -0,0 +1,77 @@
1
+ import { WebhookConfigurationError, WebhookVerificationError } from "../errors.js";
2
+ import { anySignatureVerifies, importPublicKey, parsePublicKeys, parseSignatures, readHeader, signedPayload } from "./webhooks-signature.js";
3
+ const DEFAULT_TOLERANCE_SECONDS = 300;
4
+ /** Webhook receiving utilities. Verification is local: no network calls, no timers. */
5
+ export function createWebhooks(options = {}) {
6
+ // Imported keys are cached by their wire form so a pinned key is imported once.
7
+ const imported = new Map();
8
+ const keysFor = (publicKeys = options.publicKeys) => Promise.all(parsePublicKeys(publicKeys).map(raw => {
9
+ const id = raw.join(",");
10
+ if (!imported.has(id))
11
+ imported.set(id, importPublicKey(raw));
12
+ return imported.get(id);
13
+ }));
14
+ const verifyEvent = async (body, headers, verifyOptions = {}) => {
15
+ var _a;
16
+ // Configuration problems surface first, whatever the request looks like: a missing key is ours to fix.
17
+ const keys = await keysFor(verifyOptions.publicKeys);
18
+ const id = readHeader(headers, "webhook-id");
19
+ const timestamp = readHeader(headers, "webhook-timestamp");
20
+ const signature = readHeader(headers, "webhook-signature", " ");
21
+ if (!id || !timestamp || !signature)
22
+ throw new WebhookVerificationError("missing_headers");
23
+ // Negated so a non-numeric timestamp (NaN) is refused too.
24
+ if (!(Math.abs(Date.now() / 1000 - Number(timestamp)) <= ((_a = verifyOptions.toleranceSeconds) !== null && _a !== void 0 ? _a : DEFAULT_TOLERANCE_SECONDS))) {
25
+ throw new WebhookVerificationError("timestamp_outside_tolerance");
26
+ }
27
+ const signatures = parseSignatures(signature);
28
+ if (signatures.length === 0)
29
+ throw new WebhookVerificationError("no_signature_entries");
30
+ if (!(await anySignatureVerifies(keys, signatures, signedPayload(id, timestamp, body)))) {
31
+ throw new WebhookVerificationError("signature_mismatch");
32
+ }
33
+ // Only now is the body trusted enough to parse.
34
+ let event;
35
+ try {
36
+ event = JSON.parse(typeof body === "string" ? body : new TextDecoder().decode(body));
37
+ }
38
+ catch (_b) {
39
+ throw new WebhookVerificationError("invalid_payload");
40
+ }
41
+ const { type, id: eventId, data } = (event !== null && event !== void 0 ? event : {});
42
+ if (typeof type !== "string" || typeof eventId !== "string" || typeof data !== "object" || data === null) {
43
+ throw new WebhookVerificationError("invalid_payload");
44
+ }
45
+ return event;
46
+ };
47
+ return {
48
+ verifyEvent,
49
+ createHandler({ on = {}, onEvent, onSuccess, onError, ...verifyOptions } = {}) {
50
+ const fail = async (error, status, event) => {
51
+ var _a;
52
+ return (_a = (await (onError === null || onError === void 0 ? void 0 : onError(error, event)))) !== null && _a !== void 0 ? _a : Response.json({ error: error instanceof WebhookVerificationError ? error.code : error instanceof WebhookConfigurationError ? "receiver_misconfigured" : "handler_failed" }, { status });
53
+ };
54
+ return async (request) => {
55
+ var _a, _b;
56
+ let event;
57
+ try {
58
+ event = await verifyEvent(new Uint8Array(await request.arrayBuffer()), request.headers, verifyOptions);
59
+ }
60
+ catch (error) {
61
+ return fail(error, error instanceof WebhookVerificationError ? 400 : 500);
62
+ }
63
+ // Activation probe: echo the challenge, only after the signature verified. Never reaches your callbacks.
64
+ if (event.type === "webhook.test.v1")
65
+ return Response.json({ challenge: event.data.challenge });
66
+ try {
67
+ await (onEvent === null || onEvent === void 0 ? void 0 : onEvent(event));
68
+ await ((_a = on[event.type]) === null || _a === void 0 ? void 0 : _a.call(on, event));
69
+ return (_b = (await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(event)))) !== null && _b !== void 0 ? _b : new Response(null, { status: 204 });
70
+ }
71
+ catch (error) {
72
+ return fail(error, 500, event);
73
+ }
74
+ };
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,102 @@
1
+ import type { WebhookEventMap, WebhookTestV1Data } from "./webhooks.events.generated.js";
2
+ export type * from "./webhooks.events.generated.js";
3
+ /** Name of a webhook event, e.g. `app.deleted.v1`. */
4
+ export type WebhookEventType = keyof WebhookEventMap;
5
+ /** CloudEvents 1.0 envelope every Base44 webhook is delivered in. */
6
+ export interface CloudEvent<TType extends string, TData> {
7
+ /** CloudEvents version. Always `1.0`. */
8
+ specversion: "1.0";
9
+ /** Unique event id (`evt_…`). Identical across redeliveries, so use it to deduplicate. */
10
+ id: string;
11
+ /** The workspace the event happened in, as a URL. */
12
+ source: string;
13
+ /** Event type, including its version. */
14
+ type: TType;
15
+ /** The resource the event is about, e.g. `apps/<app id>`. */
16
+ subject: string;
17
+ /** When the event occurred (RFC 3339, UTC). */
18
+ time: string;
19
+ /** Content type of `data`. Always `application/json`. */
20
+ datacontenttype: string;
21
+ /** URL of the JSON Schema describing `data`. */
22
+ dataschema: string;
23
+ /** W3C trace context, when the event was produced inside a trace. */
24
+ traceparent?: string;
25
+ /** W3C trace state, when present. */
26
+ tracestate?: string;
27
+ /** Kind of actor that caused the event. */
28
+ authtype: string;
29
+ /** Identifier of the actor that caused the event. */
30
+ authid: string;
31
+ /** Event-specific payload. */
32
+ data: TData;
33
+ }
34
+ /**
35
+ * A verified Base44 webhook event. Narrow on `type` to read `data`.
36
+ * Base44 may add event types later, so give a `switch` a default branch that still answers 2xx.
37
+ */
38
+ export type WebhookEvent = {
39
+ [K in WebhookEventType]: CloudEvent<K, WebhookEventMap[K]>;
40
+ }[WebhookEventType];
41
+ /** Request headers in any common shape: a fetch `Headers`, or a Node-style object. Names are case-insensitive. */
42
+ export type WebhookHeaders = Headers | Record<string, string | string[] | undefined>;
43
+ /** One Ed25519 public key (`whpk_…`), or several separated by commas or whitespace. */
44
+ export type WebhookPublicKeys = string | string[];
45
+ /** Webhook settings for `createBase44PlatformServer`. */
46
+ export interface WebhooksOptions {
47
+ /** Public keys of your workspace. Pass more than one while a key rotation is in progress. */
48
+ publicKeys?: WebhookPublicKeys;
49
+ }
50
+ /** Per-call overrides for `verifyEvent`. */
51
+ export interface VerifyEventOptions {
52
+ /** Public keys to verify against, instead of the ones the client was created with. */
53
+ publicKeys?: WebhookPublicKeys;
54
+ /** Maximum age or clock skew of a delivery, in seconds. Defaults to 300. */
55
+ toleranceSeconds?: number;
56
+ }
57
+ /** An event your handler receives: every event except the `webhook.test.v1` probe, which `createHandler` answers itself. */
58
+ export type DeliveredWebhookEvent = Exclude<WebhookEvent, CloudEvent<"webhook.test.v1", WebhookTestV1Data>>;
59
+ /** Per-type handlers for `createHandler`, each receiving the event already narrowed to its type. */
60
+ export type WebhookEventHandlers = {
61
+ [K in DeliveredWebhookEvent["type"]]?: (event: Extract<DeliveredWebhookEvent, {
62
+ type: K;
63
+ }>) => unknown;
64
+ };
65
+ /** Options for `createHandler`. Every callback may be async. */
66
+ export interface CreateHandlerOptions extends VerifyEventOptions {
67
+ /** Handlers keyed by event type. Types without a handler are still answered 2xx, so Base44 does not retry them. */
68
+ on?: WebhookEventHandlers;
69
+ /** Called for every verified event, before its handler in `on`. */
70
+ onEvent?: (event: DeliveredWebhookEvent) => unknown;
71
+ /**
72
+ * Called once `onEvent` and the event's handler finished without throwing. Return a `Response` to replace the default
73
+ * empty 204.
74
+ */
75
+ onSuccess?: (event: DeliveredWebhookEvent) => Response | void | Promise<Response | void>;
76
+ /**
77
+ * Called when a delivery fails: a `WebhookVerificationError` (default response 400), a
78
+ * `WebhookConfigurationError` (500), or whatever a handler threw (500, so Base44 retries). `event`
79
+ * is set when verification passed and a callback failed. Return a `Response` to replace the default.
80
+ */
81
+ onError?: (error: unknown, event?: DeliveredWebhookEvent) => Response | void | Promise<Response | void>;
82
+ }
83
+ /** Receive and verify Base44 outbound webhooks. Makes no network requests. */
84
+ export interface WebhooksModule {
85
+ /**
86
+ * Verify a delivery and return the event.
87
+ *
88
+ * Pass the body exactly as received, before any JSON parsing. Nothing is parsed until the
89
+ * signature checks out. Rejects with `WebhookVerificationError` for a delivery you should
90
+ * refuse (respond 400) and `WebhookConfigurationError` when the public keys are missing
91
+ * or malformed (let it surface as a 500 so Base44 retries). To activate an endpoint, answer
92
+ * the `webhook.test.v1` event with `{ "challenge": event.data.challenge }`.
93
+ */
94
+ verifyEvent(body: string | Uint8Array, headers: WebhookHeaders, options?: VerifyEventOptions): Promise<WebhookEvent>;
95
+ /**
96
+ * Build a fetch-style route handler (`(request: Request) => Promise<Response>`) for Next.js, Hono,
97
+ * Cloudflare Workers, Deno or Bun. It reads the raw body, verifies it, answers the activation
98
+ * challenge, then runs your callbacks. Delivery is at least once, so handlers must tolerate
99
+ * the same event (same `event.id`) arriving twice.
100
+ */
101
+ createHandler(options?: CreateHandlerOptions): (request: Request) => Promise<Response>;
102
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,3 @@
1
- import type { PlatformServerOptions } from "./server.types.js";
2
- /** Server platform client, authenticated with a workspace API key. Construction performs no network requests. */
3
- export declare class Base44PlatformServer {
4
- #private;
5
- /** Configure shared service/auth settings; modules are added on top of this. */
6
- constructor(options: PlatformServerOptions);
7
- }
1
+ import type { Base44PlatformServer, PlatformServerOptions } from "./server.types.js";
2
+ /** Create a server platform client, authenticated with a workspace API key. Performs no network requests. */
3
+ export declare function createBase44PlatformServer(options: PlatformServerOptions): Base44PlatformServer;
@@ -1,21 +1,11 @@
1
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
- if (kind === "m") throw new TypeError("Private method is not writable");
3
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
- };
7
- var _Base44PlatformServer_serverUrl, _Base44PlatformServer_apiKey;
8
1
  import { normalizeServerUrl } from "../shared/server-url.js";
9
- /** Server platform client, authenticated with a workspace API key. Construction performs no network requests. */
10
- export class Base44PlatformServer {
11
- /** Configure shared service/auth settings; modules are added on top of this. */
12
- constructor(options) {
13
- _Base44PlatformServer_serverUrl.set(this, void 0);
14
- _Base44PlatformServer_apiKey.set(this, void 0);
15
- if (!options.apiKey)
16
- throw new TypeError("apiKey is required");
17
- __classPrivateFieldSet(this, _Base44PlatformServer_serverUrl, normalizeServerUrl(options.serverUrl), "f");
18
- __classPrivateFieldSet(this, _Base44PlatformServer_apiKey, options.apiKey, "f");
19
- }
2
+ import { createWebhooks } from "./modules/webhooks.js";
3
+ /** Create a server platform client, authenticated with a workspace API key. Performs no network requests. */
4
+ export function createBase44PlatformServer(options) {
5
+ if (!options.apiKey)
6
+ throw new TypeError("apiKey is required");
7
+ normalizeServerUrl(options.serverUrl);
8
+ return Object.freeze({
9
+ webhooks: Object.freeze(createWebhooks(options.webhooks)),
10
+ });
20
11
  }
21
- _Base44PlatformServer_serverUrl = new WeakMap(), _Base44PlatformServer_apiKey = new WeakMap();
@@ -1,7 +1,15 @@
1
+ import type { WebhooksModule, WebhooksOptions } from "./modules/webhooks.types.js";
1
2
  /** Shared configuration for server platform modules. */
2
3
  export interface PlatformServerOptions {
3
4
  /** Origin of the platform service, e.g. https://base44.app. No path/query/credentials. */
4
5
  serverUrl: string;
5
6
  /** Workspace API key. Keep it on your server; never ship it to a browser. */
6
7
  apiKey: string;
8
+ /** Webhook receiving settings. */
9
+ webhooks?: WebhooksOptions;
10
+ }
11
+ /** Server platform client, returned by `createBase44PlatformServer`. */
12
+ export interface Base44PlatformServer {
13
+ /** Verify Base44 outbound webhooks and answer the activation challenge. */
14
+ readonly webhooks: WebhooksModule;
7
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44-preview/platform",
3
- "version": "0.1.0-pr.316.56344d6",
3
+ "version": "0.1.0-pr.319.1e807d1",
4
4
  "description": "Client and server SDK for building on the Base44 platform",
5
5
  "main": "dist/client/index.js",
6
6
  "types": "dist/client/index.d.ts",
@@ -29,7 +29,8 @@
29
29
  "test:types": "tsc --noEmit -p tsconfig.type-tests.json",
30
30
  "test:unit": "vitest run",
31
31
  "test:package": "npm run build && node --test tests/package.test.mjs",
32
- "prepublishOnly": "npm run build"
32
+ "prepublishOnly": "npm run build",
33
+ "generate:webhooks": "node scripts/generate-webhook-types.mjs"
33
34
  },
34
35
  "dependencies": {
35
36
  "socket.io-client": "^4.8.3"
@@ -56,5 +57,11 @@
56
57
  "dist/server/index.d.ts"
57
58
  ]
58
59
  }
60
+ },
61
+ "devDependencies": {
62
+ "json-schema-to-typescript": "^16.0.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=20"
59
66
  }
60
67
  }