@notionhq/custom-blocks 0.0.64 → 0.0.66

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/HOST.md CHANGED
@@ -49,7 +49,7 @@ Related types: `CustomBlockHostOptions`, `CustomBlockHostInitialState`, `CustomB
49
49
 
50
50
  The bridge is established with a handshake: the sandbox posts a `ready` when `initCustomBlock()` starts, and the host validates it and replies exactly once with `init`. Hosts should log and ignore duplicate `ready` messages instead of re-running the handshake. After that, narrower messages stream live state updates.
51
51
 
52
- The sandbox starts by sending `ready`. It declares the bridge protocol version, includes the custom blocks package semver as `sdkVersion` when available, and tells the host whether manifest discovery produced a usable manifest. Hosts should reject init when `ready` reports a manifest error, echoing the sandbox's error code:
52
+ The sandbox starts by sending `ready`. It declares the bridge protocol version, includes the custom blocks package semver as `sdkVersion` when available, and tells the host whether manifest discovery produced a usable manifest. Hosts should reject init when `ready` reports a manifest error, forwarding the sandbox's full error payload unchanged:
53
53
 
54
54
  ```ts
55
55
  // sandbox → host
@@ -59,7 +59,7 @@ The sandbox starts by sending `ready`. It declares the bridge protocol version,
59
59
  { type: "ready", status: "success", bridgeProtocolVersion, sdkVersion, manifest: null }
60
60
 
61
61
  // sandbox → host, manifest failed to load or validate
62
- { type: "ready", status: "error", bridgeProtocolVersion, sdkVersion, error: { code: "manifest_invalid", message: "..." } }
62
+ { type: "ready", status: "error", bridgeProtocolVersion, sdkVersion, error: { code: "manifest_invalid", message: "...", isRetryable: false } }
63
63
  ```
64
64
 
65
65
  The host replies with exactly one `init`, which has two statuses. On success it carries the full initial state — `theme`, `blockId`, `parent`, `page`, `currentUser` (the `NotionUser` shape from the user result messages), and `dataSources: { bindings }`:
@@ -150,21 +150,21 @@ Messages sent from the sandbox to the host. Parse `window` `message` events with
150
150
 
151
151
  Messages sent from the host to the sandbox. Same `{ wire type, type / schema, behavior }` shape; the type / schema column points to the payload shape.
152
152
 
153
- | Wire type | Type / schema | Behavior |
154
- | ----------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
155
- | `init` | `InitMessage` / `initMessageSchema` | Sent exactly once in response to `ready`; carries the success/error payload described under [Lifecycle](#lifecycle). |
156
- | `themeChanged` | `ThemeChangedMessage` / `themeChangedMessageSchema` | Replaces the current theme. |
157
- | `parentChanged` | `ParentChangedMessage` / `parentChangedMessageSchema` | Replaces the custom block parent without disturbing theme, block ID, page ID, user, or query state. |
158
- | `pageChanged` | `PageChangedMessage` / `pageChangedMessageSchema` | Replaces the nearest page ancestor without disturbing theme, block ID, parent, user, or query state. |
159
- | `currentUserChanged` | `CurrentUserChangedMessage` / `currentUserChangedMessageSchema` | Replaces the current viewer record. Send when any viewer field changes (name, avatar, email). |
160
- | `dataSourcesChanged` | `DataSourcesChangedMessage` / `dataSourcesChangedMessageSchema` | Replaces data-source bindings; the sandbox preserves cached query state for keys that still exist and drops removed keys. |
153
+ | Wire type | Type / schema | Behavior |
154
+ | ----------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
155
+ | `init` | `InitMessage` / `initMessageSchema` | Sent exactly once in response to `ready`; carries the success/error payload described under [Lifecycle](#lifecycle). |
156
+ | `themeChanged` | `ThemeChangedMessage` / `themeChangedMessageSchema` | Replaces the current theme. |
157
+ | `parentChanged` | `ParentChangedMessage` / `parentChangedMessageSchema` | Replaces the custom block parent without disturbing theme, block ID, page ID, user, or query state. |
158
+ | `pageChanged` | `PageChangedMessage` / `pageChangedMessageSchema` | Replaces the nearest page ancestor without disturbing theme, block ID, parent, user, or query state. |
159
+ | `currentUserChanged` | `CurrentUserChangedMessage` / `currentUserChangedMessageSchema` | Replaces the current viewer record. Send when any viewer field changes (name, avatar, email). |
160
+ | `dataSourcesChanged` | `DataSourcesChangedMessage` / `dataSourcesChangedMessageSchema` | Replaces data-source bindings; the sandbox preserves cached query state for keys that still exist and drops removed keys. |
161
161
  | `queryDataSourceResult` | `QueryDataSourceResultMessage` / `queryDataSourceResultMessageSchema` | `requestId` / `snapshotId`-keyed response with `status: "success"`, `items`, and `hasMore`, or `status: "error"` and `error`. |
162
- | `createPageResult` | `CreatePageResultMessage` / `createPageResultMessageSchema` | `requestId`-keyed page response with `status: "success"` or `status: "error"`. |
163
- | `getPageResult` | `GetPageResultMessage` / `getPageResultMessageSchema` | Same success/error shape as `createPageResult`. |
164
- | `updatePageResult` | `UpdatePageResultMessage` / `updatePageResultMessageSchema` | Same success/error shape as `createPageResult`. |
165
- | `getUserResult` | `GetUserResultMessage` / `getUserResultMessageSchema` | `requestId`-keyed user response with `status: "success"` or `status: "error"`. |
166
- | `listUsersResult` | `ListUsersResultMessage` / `listUsersResultMessageSchema` | Same success/error shape as `getUserResult`. |
167
- | `invalidSandboxMessage` | `InvalidSandboxMessage` / `invalidSandboxMessageSchema` | Host-side NACK for a sandbox message it could not parse. |
162
+ | `createPageResult` | `CreatePageResultMessage` / `createPageResultMessageSchema` | `requestId`-keyed page response with `status: "success"` or `status: "error"`. |
163
+ | `getPageResult` | `GetPageResultMessage` / `getPageResultMessageSchema` | Same success/error shape as `createPageResult`. |
164
+ | `updatePageResult` | `UpdatePageResultMessage` / `updatePageResultMessageSchema` | Same success/error shape as `createPageResult`. |
165
+ | `getUserResult` | `GetUserResultMessage` / `getUserResultMessageSchema` | `requestId`-keyed user response with `status: "success"` or `status: "error"`. |
166
+ | `listUsersResult` | `ListUsersResultMessage` / `listUsersResultMessageSchema` | Same success/error shape as `getUserResult`. |
167
+ | `invalidSandboxMessage` | `InvalidSandboxMessage` / `invalidSandboxMessageSchema` | Host-side NACK for a sandbox message it could not parse. |
168
168
 
169
169
  `HostToSandboxMessage` / `hostToSandboxMessageSchema` is the discriminated union over all of the above.
170
170
 
@@ -1,12 +1,7 @@
1
- import * as v from "valibot";
1
+ import type { CustomBlockErrorInfo } from "./errors.js";
2
2
  import { type CustomBlockManifest } from "./manifest.js";
3
- export declare const customBlockReadyErrorCodeSchema: v.StringSchema<undefined>;
4
3
  export type CustomBlockReadyErrorCode = "manifest_unavailable" | "manifest_invalid" | (string & {});
5
- export declare const customBlockReadyErrorSchema: v.ObjectSchema<{
6
- readonly code: v.StringSchema<undefined>;
7
- readonly message: v.StringSchema<undefined>;
8
- }, undefined>;
9
- export type CustomBlockReadyError = v.InferOutput<typeof customBlockReadyErrorSchema>;
4
+ export type CustomBlockReadyError = CustomBlockErrorInfo<CustomBlockReadyErrorCode>;
10
5
  export type ManifestLoadResult = {
11
6
  manifest: CustomBlockManifest | null;
12
7
  error?: undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"loadManifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/loadManifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAC5B,OAAO,EAAE,KAAK,mBAAmB,EAAkB,MAAM,eAAe,CAAA;AAMxE,eAAO,MAAM,+BAA+B,2BAAa,CAAA;AAEzD,MAAM,MAAM,yBAAyB,GAClC,sBAAsB,GACtB,kBAAkB,GAClB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEhB,eAAO,MAAM,2BAA2B;;;aAGtC,CAAA;AAEF,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,WAAW,CAChD,OAAO,2BAA2B,CAClC,CAAA;AAED,MAAM,MAAM,kBAAkB,GAC3B;IACA,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACpC,KAAK,CAAC,EAAE,SAAS,CAAA;CAChB,GACD;IACA,QAAQ,EAAE,IAAI,CAAA;IACd,KAAK,EAAE,qBAAqB,CAAA;CAC3B,CAAA;AAEJ;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAgDhE"}
1
+ {"version":3,"file":"loadManifest.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/loadManifest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EAAE,KAAK,mBAAmB,EAAkB,MAAM,eAAe,CAAA;AAIxE,MAAM,MAAM,yBAAyB,GAClC,sBAAsB,GACtB,kBAAkB,GAClB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEhB,MAAM,MAAM,qBAAqB,GAChC,oBAAoB,CAAC,yBAAyB,CAAC,CAAA;AAEhD,MAAM,MAAM,kBAAkB,GAC3B;IACA,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;IACpC,KAAK,CAAC,EAAE,SAAS,CAAA;CAChB,GACD;IACA,QAAQ,EAAE,IAAI,CAAA;IACd,KAAK,EAAE,qBAAqB,CAAA;CAC3B,CAAA;AAEJ;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAkEhE"}
@@ -1,13 +1,6 @@
1
1
  import * as v from "valibot";
2
2
  import { manifestSchema } from "./manifest.js";
3
3
  const MANIFEST_URL = "custom_blocks.json";
4
- // The schema accepts any string code. The type lists known codes for
5
- // autocomplete, with an open string fallback for newer senders.
6
- export const customBlockReadyErrorCodeSchema = v.string();
7
- export const customBlockReadyErrorSchema = v.object({
8
- code: customBlockReadyErrorCodeSchema,
9
- message: v.string(),
10
- });
11
4
  /**
12
5
  * Attempts to load a `custom_blocks.json` manifest co-located with the bundle.
13
6
  * A missing manifest means the block has no declared data requirements.
@@ -21,7 +14,7 @@ export async function loadManifest() {
21
14
  console.warn(`[custom-blocks-sdk] ${message}`);
22
15
  return {
23
16
  manifest: null,
24
- error: { code: "manifest_unavailable", message },
17
+ error: { code: "manifest_unavailable", message, isRetryable: true },
25
18
  };
26
19
  }
27
20
  let response;
@@ -33,7 +26,10 @@ export async function loadManifest() {
33
26
  console.warn(`[custom-blocks-sdk] ${message}`, error);
34
27
  return {
35
28
  manifest: null,
36
- error: { code: "manifest_unavailable", message },
29
+ // Consider this retryable because it may have just been a network issue. Because `init`
30
+ // errors are considered terminal, the host should "retry" by reloading the iframe and
31
+ // sending another `ready` message.
32
+ error: { code: "manifest_unavailable", message, isRetryable: true },
37
33
  };
38
34
  }
39
35
  if (response.status === 404) {
@@ -46,7 +42,11 @@ export async function loadManifest() {
46
42
  console.warn(`[custom-blocks-sdk] ${message}`);
47
43
  return {
48
44
  manifest: null,
49
- error: { code: "manifest_unavailable", message },
45
+ error: {
46
+ code: "manifest_unavailable",
47
+ message,
48
+ isRetryable: isRetryableManifestHttpStatus(response.status),
49
+ },
50
50
  };
51
51
  }
52
52
  let json;
@@ -56,13 +56,31 @@ export async function loadManifest() {
56
56
  catch (error) {
57
57
  const message = `Manifest at ${MANIFEST_URL} was not valid JSON.`;
58
58
  console.warn(`[custom-blocks-sdk] ${message}`, error);
59
- return { manifest: null, error: { code: "manifest_invalid", message } };
59
+ return {
60
+ manifest: null,
61
+ // Consider invalid manifests as unrecoverable errors.
62
+ error: { code: "manifest_invalid", message, isRetryable: false },
63
+ };
60
64
  }
61
65
  const parsed = v.safeParse(manifestSchema, json);
62
66
  if (!parsed.success) {
63
67
  const message = `Manifest at ${MANIFEST_URL} did not match schema.`;
64
68
  console.warn(`[custom-blocks-sdk] ${message}`, parsed.issues);
65
- return { manifest: null, error: { code: "manifest_invalid", message } };
69
+ return {
70
+ manifest: null,
71
+ // Consider invalid manifests as unrecoverable errors.
72
+ error: { code: "manifest_invalid", message, isRetryable: false },
73
+ };
66
74
  }
67
75
  return { manifest: parsed.output };
68
76
  }
77
+ function isRetryableManifestHttpStatus(status) {
78
+ // Consider these statuses retryable because they may reflect transient failures.
79
+ return (status === 408 || // Request Timeout
80
+ status === 425 || // Too Early
81
+ status === 500 || // Internal Server Error
82
+ status === 502 || // Bad Gateway
83
+ status === 503 || // Service Unavailable
84
+ status === 504 // Gateway Timeout
85
+ );
86
+ }
@@ -39,6 +39,7 @@ export declare const readyMessageSchema: v.UnionSchema<[v.ObjectSchema<{
39
39
  readonly error: v.ObjectSchema<{
40
40
  readonly code: v.StringSchema<undefined>;
41
41
  readonly message: v.StringSchema<undefined>;
42
+ readonly isRetryable: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
42
43
  }, undefined>;
43
44
  }, undefined>], undefined>;
44
45
  export type ReadyMessage = v.InferOutput<typeof readyMessageSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"ready.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/messages/ready.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAmB5B;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;IAG7B;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BASH,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,kBAAkB,CAAC,CAAA"}
1
+ {"version":3,"file":"ready.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/messages/ready.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AA0B5B;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;IAG7B;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BASH,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,kBAAkB,CAAC,CAAA"}
@@ -1,5 +1,4 @@
1
1
  import * as v from "valibot";
2
- import { customBlockReadyErrorSchema } from "../loadManifest.js";
3
2
  import { manifestSchema } from "../manifest.js";
4
3
  const readyMessageCommonEntries = {
5
4
  type: v.literal("ready"),
@@ -15,6 +14,13 @@ const readyMessageCommonEntries = {
15
14
  */
16
15
  sdkVersion: v.string(),
17
16
  };
17
+ // TODO(custom-blocks): Replace with `customBlockErrorInfoSchema` which makes `isRetryable` required
18
+ // once min supported bridge protocol version is bumped to 3.
19
+ const readyErrorSchema = v.object({
20
+ code: v.string(),
21
+ message: v.string(),
22
+ isRetryable: v.optional(v.boolean(), false),
23
+ });
18
24
  /**
19
25
  * First message the sandbox sends after mount, kicking off the bridge handshake. The host replies
20
26
  * with `init`.
@@ -31,7 +37,7 @@ export const readyMessageSchema = v.union([
31
37
  }),
32
38
  v.object({
33
39
  status: v.literal("error"),
34
- error: customBlockReadyErrorSchema,
40
+ error: readyErrorSchema,
35
41
  ...readyMessageCommonEntries,
36
42
  }),
37
43
  ]);
@@ -35,6 +35,7 @@ export declare const sandboxToHostMessageSchema: v.UnionSchema<[v.UnionSchema<[v
35
35
  readonly error: v.ObjectSchema<{
36
36
  readonly code: v.StringSchema<undefined>;
37
37
  readonly message: v.StringSchema<undefined>;
38
+ readonly isRetryable: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
38
39
  }, undefined>;
39
40
  }, undefined>], undefined>, v.ObjectSchema<{
40
41
  readonly type: v.LiteralSchema<"queryDataSource", undefined>;
@@ -1 +1 @@
1
- {"version":3,"file":"sandboxToHost.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/messages/sandboxToHost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAW5B;;;GAGG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAUrC,CAAA;AAEF,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,WAAW,CAC/C,OAAO,0BAA0B,CACjC,CAAA"}
1
+ {"version":3,"file":"sandboxToHost.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/messages/sandboxToHost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAW5B;;;GAGG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAUrC,CAAA;AAEF,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,WAAW,CAC/C,OAAO,0BAA0B,CACjC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"createCustomBlockHost.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/createCustomBlockHost.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAY1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAA;AAkB/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAIzD,OAAO,KAAK,EACX,iCAAiC,EACjC,2BAA2B,EAE3B,mBAAmB,EACnB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACX,uBAAuB,EACvB,2BAA2B,EAC3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,oBAAoB,CAAA;AAE9E,MAAM,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,iBAAiB,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,2BAA2B,CAAA;IACzC,QAAQ,EAAE,uBAAuB,CAAA;IACjC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;IAChD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,2BAA2B,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC1E,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,IAAI,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACtC,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,cAAc,EAAE,CAAC,WAAW,EAAE,iCAAiC,KAAK,IAAI,CAAA;IACxE,cAAc,EAAE,CAAC,WAAW,EAAE,UAAU,KAAK,IAAI,CAAA;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE;QACpB,YAAY,EAAE,kBAAkB,CAAA;QAChC,QAAQ,EAAE,oCAAoC,CAAA;KAC9C,KAAK,IAAI,CAAA;CACV,CAAA;AAKD,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,sBAAsB,GAC7B,qBAAqB,CAuTvB"}
1
+ {"version":3,"file":"createCustomBlockHost.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/createCustomBlockHost.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAY1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAA;AAkB/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAIzD,OAAO,KAAK,EACX,iCAAiC,EACjC,2BAA2B,EAE3B,mBAAmB,EACnB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACX,uBAAuB,EACvB,2BAA2B,EAC3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,oBAAoB,CAAA;AAE9E,MAAM,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,iBAAiB,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,YAAY,EAAE,2BAA2B,CAAA;IACzC,QAAQ,EAAE,uBAAuB,CAAA;IACjC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAA;IAChD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,2BAA2B,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC1E,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,IAAI,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACtC,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,cAAc,EAAE,CAAC,WAAW,EAAE,iCAAiC,KAAK,IAAI,CAAA;IACxE,cAAc,EAAE,CAAC,WAAW,EAAE,UAAU,KAAK,IAAI,CAAA;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE;QACpB,YAAY,EAAE,kBAAkB,CAAA;QAChC,QAAQ,EAAE,oCAAoC,CAAA;KAC9C,KAAK,IAAI,CAAA;CACV,CAAA;AAKD,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,sBAAsB,GAC7B,qBAAqB,CAuUvB"}
@@ -2,7 +2,7 @@ import * as v from "valibot";
2
2
  import { sandboxToHostMessageSchema } from "../bridge/messages/sandboxToHost.js";
3
3
  import { unreachable } from "../utils.js";
4
4
  import { initErrorForFailureReason } from "./lifecycle/initErrors.js";
5
- import { getReadyInitFailure } from "./lifecycle/ready.js";
5
+ import { isValidBridgeProtocolVersion } from "./lifecycle/ready.js";
6
6
  import { getInvalidSandboxMessageResponse } from "./messages/invalidSandboxMessage.js";
7
7
  import { QuerySubscriptions } from "./queries/querySubscriptions.js";
8
8
  const DEFAULT_NO_READY_TIMEOUT_MS = 5000;
@@ -65,26 +65,23 @@ export function createCustomBlockHost(options) {
65
65
  post(message);
66
66
  return true;
67
67
  }
68
- function sendInitError(code, message) {
68
+ function sendInitError(error) {
69
69
  if (initStatus !== "pending") {
70
70
  return;
71
71
  }
72
72
  initStatus = "error";
73
73
  clearNoReadyTimeout();
74
- const error = initErrorForFailureReason(code);
75
74
  post({
76
75
  type: "init",
77
76
  status: "error",
78
- error: {
79
- ...error,
80
- message: message ?? error.message,
81
- },
77
+ error,
82
78
  });
83
79
  }
84
80
  function onIframeLoad() {
85
81
  clearNoReadyTimeout();
86
82
  noReadyTimeoutId = window.setTimeout(() => {
87
- sendInitError("no_ready");
83
+ const initError = initErrorForFailureReason("no_ready");
84
+ sendInitError(initError);
88
85
  }, noReadyTimeoutMs);
89
86
  }
90
87
  async function handleQuery(message) {
@@ -157,12 +154,25 @@ export function createCustomBlockHost(options) {
157
154
  post(result);
158
155
  }
159
156
  function handleReady(message) {
160
- const failure = getReadyInitFailure(message, minBridgeProtocolVersion);
161
- if (failure !== undefined) {
162
- sendInitError(failure.code, failure.message);
157
+ // Verify the bridge protocol version is valid.
158
+ if (!isValidBridgeProtocolVersion(message.bridgeProtocolVersion)) {
159
+ const initError = initErrorForFailureReason("invalid_protocol_version", {
160
+ currentBridgeProtocolVersion: message.bridgeProtocolVersion,
161
+ });
162
+ sendInitError(initError);
163
+ return;
164
+ }
165
+ if (message.bridgeProtocolVersion < minBridgeProtocolVersion) {
166
+ const initError = initErrorForFailureReason("unsupported_protocol_version", {
167
+ currentBridgeProtocolVersion: message.bridgeProtocolVersion,
168
+ minBridgeProtocolVersion,
169
+ });
170
+ sendInitError(initError);
163
171
  return;
164
172
  }
165
- if (message.status !== "success") {
173
+ // If the sandbox reported an error during `ready`, return it as an `init` error.
174
+ if (message.status === "error") {
175
+ sendInitError(message.error);
166
176
  return;
167
177
  }
168
178
  if (sendInit(message)) {
@@ -177,7 +187,8 @@ export function createCustomBlockHost(options) {
177
187
  });
178
188
  const response = getInvalidSandboxMessageResponse(data);
179
189
  if (initStatus === "pending" && response.incomingType === "ready") {
180
- sendInitError("invalid_ready");
190
+ const initError = initErrorForFailureReason("invalid_ready");
191
+ sendInitError(initError);
181
192
  }
182
193
  else if (response.nack !== undefined) {
183
194
  post(response.nack);
@@ -6,5 +6,8 @@ import type { CustomBlockInitErrorCode, CustomBlockInitErrorInfo } from "../../b
6
6
  * `ready` message. `isRetryable` means a fresh iframe / `init` attempt could plausibly succeed
7
7
  * without changing block code or host configuration.
8
8
  */
9
- export declare function initErrorForFailureReason(reason: CustomBlockInitErrorCode): CustomBlockInitErrorInfo;
9
+ export declare function initErrorForFailureReason(reason: CustomBlockInitErrorCode, data?: {
10
+ minBridgeProtocolVersion?: number;
11
+ currentBridgeProtocolVersion?: number;
12
+ }): CustomBlockInitErrorInfo;
10
13
  //# sourceMappingURL=initErrors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"initErrors.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/lifecycle/initErrors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,wBAAwB,EACxB,wBAAwB,EACxB,MAAM,+BAA+B,CAAA;AAKtC;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACxC,MAAM,EAAE,wBAAwB,GAC9B,wBAAwB,CAkH1B"}
1
+ {"version":3,"file":"initErrors.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/lifecycle/initErrors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,wBAAwB,EACxB,wBAAwB,EACxB,MAAM,+BAA+B,CAAA;AAKtC;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACxC,MAAM,EAAE,wBAAwB,EAChC,IAAI,GAAE;IACL,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,4BAA4B,CAAC,EAAE,MAAM,CAAA;CAChC,GACJ,wBAAwB,CA6H1B"}
@@ -6,7 +6,7 @@ const UNKNOWN_ERROR_MESSAGE = "Unexpected error while initializing custom block.
6
6
  * `ready` message. `isRetryable` means a fresh iframe / `init` attempt could plausibly succeed
7
7
  * without changing block code or host configuration.
8
8
  */
9
- export function initErrorForFailureReason(reason) {
9
+ export function initErrorForFailureReason(reason, data = {}) {
10
10
  switch (reason) {
11
11
  case "no_ready":
12
12
  return {
@@ -34,20 +34,31 @@ export function initErrorForFailureReason(reason) {
34
34
  // The sandbox must be updated to a valid manifest.
35
35
  isRetryable: false,
36
36
  };
37
- case "invalid_protocol_version":
37
+ case "invalid_protocol_version": {
38
+ const versionText = data.currentBridgeProtocolVersion
39
+ ? `: ${data.currentBridgeProtocolVersion}`
40
+ : ".";
38
41
  return {
39
42
  code: reason,
40
- message: "Sandbox reported an invalid bridge protocol version.",
43
+ message: `Sandbox reported an invalid bridge protocol version ${versionText}`,
41
44
  // The sandbox must be updated to a supported protocol version.
42
45
  isRetryable: false,
43
46
  };
44
- case "unsupported_protocol_version":
47
+ }
48
+ case "unsupported_protocol_version": {
49
+ const currentVersionText = data.currentBridgeProtocolVersion
50
+ ? `${data.currentBridgeProtocolVersion} `
51
+ : "";
52
+ const minVersionText = data.minBridgeProtocolVersion
53
+ ? ` Minimum supported version is ${data.minBridgeProtocolVersion}.`
54
+ : "";
45
55
  return {
46
56
  code: reason,
47
- message: "Sandbox bridge protocol version is not supported.",
57
+ message: `Sandbox bridge protocol version ${currentVersionText}is not supported.${minVersionText}`,
48
58
  // The sandbox must be updated to a supported protocol version.
49
59
  isRetryable: false,
50
60
  };
61
+ }
51
62
  case "context_unavailable":
52
63
  return {
53
64
  code: reason,
@@ -1,8 +1,2 @@
1
- import type { CustomBlockInitErrorCode } from "../../bridge/messages/init.js";
2
- import type { ReadyMessage } from "../../bridge/messages/ready.js";
3
- export type ReadyInitFailure = {
4
- code: CustomBlockInitErrorCode;
5
- message?: string;
6
- };
7
- export declare function getReadyInitFailure(message: ReadyMessage, minBridgeProtocolVersion: number): ReadyInitFailure | undefined;
1
+ export declare function isValidBridgeProtocolVersion(version: number): boolean;
8
2
  //# sourceMappingURL=ready.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ready.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/lifecycle/ready.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAElE,MAAM,MAAM,gBAAgB,GAAG;IAC9B,IAAI,EAAE,wBAAwB,CAAA;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,YAAY,EACrB,wBAAwB,EAAE,MAAM,GAC9B,gBAAgB,GAAG,SAAS,CAoB9B"}
1
+ {"version":3,"file":"ready.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/lifecycle/ready.ts"],"names":[],"mappings":"AAAA,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE"}
@@ -1,24 +1,3 @@
1
- export function getReadyInitFailure(message, minBridgeProtocolVersion) {
2
- if (!isValidBridgeProtocolVersion(message.bridgeProtocolVersion)) {
3
- return {
4
- code: "invalid_protocol_version",
5
- message: `Sandbox reported invalid bridge protocol version ${message.bridgeProtocolVersion}.`,
6
- };
7
- }
8
- if (message.bridgeProtocolVersion < minBridgeProtocolVersion) {
9
- return {
10
- code: "unsupported_protocol_version",
11
- message: `Sandbox bridge protocol version ${message.bridgeProtocolVersion} is below the host minimum ${minBridgeProtocolVersion}.`,
12
- };
13
- }
14
- if (message.status === "error") {
15
- return {
16
- code: message.error.code,
17
- message: message.error.message,
18
- };
19
- }
20
- return undefined;
21
- }
22
- function isValidBridgeProtocolVersion(version) {
1
+ export function isValidBridgeProtocolVersion(version) {
23
2
  return Number.isFinite(version) && Number.isInteger(version) && version >= 1;
24
3
  }
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  *
5
5
  * WARNING: Generated during SDK publish. Do not edit in the published package.
6
6
  */
7
- export const CUSTOM_BLOCKS_SDK_VERSION = "0.0.64"
7
+ export const CUSTOM_BLOCKS_SDK_VERSION = "0.0.66"
package/docs/manifest.md CHANGED
@@ -18,7 +18,7 @@ A custom block declares its required data sources in `custom_blocks.json` at the
18
18
  }
19
19
  ```
20
20
 
21
- `initCustomBlock()` fetches `custom_blocks.json` and forwards it with `ready`. The `notionCustomBlock()` Vite plugin from `@notionhq/custom-blocks/vite` serves it in dev and emits it into `dist/` on build. If the file is missing, the SDK sends `ready` with `status: "success"` and `manifest: null`, which means the block has no declared data requirements. If the file is unavailable for another reason or invalid, the SDK sends `ready` with `status: "error"` and an `error` payload, and hosts should reject init by echoing the sandbox-reported error code back.
21
+ `initCustomBlock()` fetches the manifest and forwards it with `ready`. The `notionCustomBlock()` Vite plugin from `@notionhq/custom-blocks/vite` serves it in dev and emits it into `dist/` on build. If the file is missing, the SDK sends `ready` with `status: "success"` and `manifest: null`, which means the block has no declared data requirements. If the file is unavailable for another reason or invalid, the SDK sends `ready` with `status: "error"` and an `error` payload. Hosts should reject `init` by forwarding that full error payload unchanged.
22
22
 
23
23
  ## Vite plugin
24
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks",
3
- "version": "0.0.64",
3
+ "version": "0.0.66",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -1,25 +1,16 @@
1
1
  import * as v from "valibot"
2
+ import type { CustomBlockErrorInfo } from "./errors.js"
2
3
  import { type CustomBlockManifest, manifestSchema } from "./manifest.js"
3
4
 
4
5
  const MANIFEST_URL = "custom_blocks.json"
5
6
 
6
- // The schema accepts any string code. The type lists known codes for
7
- // autocomplete, with an open string fallback for newer senders.
8
- export const customBlockReadyErrorCodeSchema = v.string()
9
-
10
7
  export type CustomBlockReadyErrorCode =
11
8
  | "manifest_unavailable"
12
9
  | "manifest_invalid"
13
10
  | (string & {})
14
11
 
15
- export const customBlockReadyErrorSchema = v.object({
16
- code: customBlockReadyErrorCodeSchema,
17
- message: v.string(),
18
- })
19
-
20
- export type CustomBlockReadyError = v.InferOutput<
21
- typeof customBlockReadyErrorSchema
22
- >
12
+ export type CustomBlockReadyError =
13
+ CustomBlockErrorInfo<CustomBlockReadyErrorCode>
23
14
 
24
15
  export type ManifestLoadResult =
25
16
  | {
@@ -44,7 +35,7 @@ export async function loadManifest(): Promise<ManifestLoadResult> {
44
35
  console.warn(`[custom-blocks-sdk] ${message}`)
45
36
  return {
46
37
  manifest: null,
47
- error: { code: "manifest_unavailable", message },
38
+ error: { code: "manifest_unavailable", message, isRetryable: true },
48
39
  }
49
40
  }
50
41
  let response: Response
@@ -55,7 +46,10 @@ export async function loadManifest(): Promise<ManifestLoadResult> {
55
46
  console.warn(`[custom-blocks-sdk] ${message}`, error)
56
47
  return {
57
48
  manifest: null,
58
- error: { code: "manifest_unavailable", message },
49
+ // Consider this retryable because it may have just been a network issue. Because `init`
50
+ // errors are considered terminal, the host should "retry" by reloading the iframe and
51
+ // sending another `ready` message.
52
+ error: { code: "manifest_unavailable", message, isRetryable: true },
59
53
  }
60
54
  }
61
55
  if (response.status === 404) {
@@ -68,22 +62,49 @@ export async function loadManifest(): Promise<ManifestLoadResult> {
68
62
  console.warn(`[custom-blocks-sdk] ${message}`)
69
63
  return {
70
64
  manifest: null,
71
- error: { code: "manifest_unavailable", message },
65
+ error: {
66
+ code: "manifest_unavailable",
67
+ message,
68
+ isRetryable: isRetryableManifestHttpStatus(response.status),
69
+ },
72
70
  }
73
71
  }
72
+
74
73
  let json: unknown
75
74
  try {
76
75
  json = await response.json()
77
76
  } catch (error) {
78
77
  const message = `Manifest at ${MANIFEST_URL} was not valid JSON.`
79
78
  console.warn(`[custom-blocks-sdk] ${message}`, error)
80
- return { manifest: null, error: { code: "manifest_invalid", message } }
79
+ return {
80
+ manifest: null,
81
+ // Consider invalid manifests as unrecoverable errors.
82
+ error: { code: "manifest_invalid", message, isRetryable: false },
83
+ }
81
84
  }
85
+
82
86
  const parsed = v.safeParse(manifestSchema, json)
83
87
  if (!parsed.success) {
84
88
  const message = `Manifest at ${MANIFEST_URL} did not match schema.`
85
89
  console.warn(`[custom-blocks-sdk] ${message}`, parsed.issues)
86
- return { manifest: null, error: { code: "manifest_invalid", message } }
90
+ return {
91
+ manifest: null,
92
+ // Consider invalid manifests as unrecoverable errors.
93
+ error: { code: "manifest_invalid", message, isRetryable: false },
94
+ }
87
95
  }
96
+
88
97
  return { manifest: parsed.output }
89
98
  }
99
+
100
+ function isRetryableManifestHttpStatus(status: number): boolean {
101
+ // Consider these statuses retryable because they may reflect transient failures.
102
+ return (
103
+ status === 408 || // Request Timeout
104
+ status === 425 || // Too Early
105
+ status === 500 || // Internal Server Error
106
+ status === 502 || // Bad Gateway
107
+ status === 503 || // Service Unavailable
108
+ status === 504 // Gateway Timeout
109
+ )
110
+ }
@@ -1,5 +1,4 @@
1
1
  import * as v from "valibot"
2
- import { customBlockReadyErrorSchema } from "../loadManifest.js"
3
2
  import { manifestSchema } from "../manifest.js"
4
3
 
5
4
  const readyMessageCommonEntries = {
@@ -17,6 +16,14 @@ const readyMessageCommonEntries = {
17
16
  sdkVersion: v.string(),
18
17
  }
19
18
 
19
+ // TODO(custom-blocks): Replace with `customBlockErrorInfoSchema` which makes `isRetryable` required
20
+ // once min supported bridge protocol version is bumped to 3.
21
+ const readyErrorSchema = v.object({
22
+ code: v.string(),
23
+ message: v.string(),
24
+ isRetryable: v.optional(v.boolean(), false),
25
+ })
26
+
20
27
  /**
21
28
  * First message the sandbox sends after mount, kicking off the bridge handshake. The host replies
22
29
  * with `init`.
@@ -33,7 +40,7 @@ export const readyMessageSchema = v.union([
33
40
  }),
34
41
  v.object({
35
42
  status: v.literal("error"),
36
- error: customBlockReadyErrorSchema,
43
+ error: readyErrorSchema,
37
44
  ...readyMessageCommonEntries,
38
45
  }),
39
46
  ])
@@ -13,7 +13,7 @@ import type {
13
13
  } from "../bridge/messages/getUser.js"
14
14
  import type { HostToSandboxMessage } from "../bridge/messages/hostToSandbox.js"
15
15
  import type {
16
- CustomBlockInitErrorCode,
16
+ CustomBlockInitErrorInfo,
17
17
  InitMessage,
18
18
  } from "../bridge/messages/init.js"
19
19
  import type {
@@ -35,7 +35,7 @@ import type { NotionTheme } from "../bridge/theme.js"
35
35
  import type { NotionUser } from "../bridge/users/user.js"
36
36
  import { unreachable } from "../utils.js"
37
37
  import { initErrorForFailureReason } from "./lifecycle/initErrors.js"
38
- import { getReadyInitFailure } from "./lifecycle/ready.js"
38
+ import { isValidBridgeProtocolVersion } from "./lifecycle/ready.js"
39
39
  import type {
40
40
  CustomBlockHostDataSourcesPayload,
41
41
  CustomBlockHostInitialState,
@@ -161,27 +161,24 @@ export function createCustomBlockHost(
161
161
  return true
162
162
  }
163
163
 
164
- function sendInitError(code: CustomBlockInitErrorCode, message?: string) {
164
+ function sendInitError(error: CustomBlockInitErrorInfo) {
165
165
  if (initStatus !== "pending") {
166
166
  return
167
167
  }
168
168
  initStatus = "error"
169
169
  clearNoReadyTimeout()
170
- const error = initErrorForFailureReason(code)
171
170
  post({
172
171
  type: "init",
173
172
  status: "error",
174
- error: {
175
- ...error,
176
- message: message ?? error.message,
177
- },
173
+ error,
178
174
  })
179
175
  }
180
176
 
181
177
  function onIframeLoad() {
182
178
  clearNoReadyTimeout()
183
179
  noReadyTimeoutId = window.setTimeout(() => {
184
- sendInitError("no_ready")
180
+ const initError = initErrorForFailureReason("no_ready")
181
+ sendInitError(initError)
185
182
  }, noReadyTimeoutMs)
186
183
  }
187
184
 
@@ -261,14 +258,32 @@ export function createCustomBlockHost(
261
258
  }
262
259
 
263
260
  function handleReady(message: ReadyMessage) {
264
- const failure = getReadyInitFailure(message, minBridgeProtocolVersion)
265
- if (failure !== undefined) {
266
- sendInitError(failure.code, failure.message)
261
+ // Verify the bridge protocol version is valid.
262
+ if (!isValidBridgeProtocolVersion(message.bridgeProtocolVersion)) {
263
+ const initError = initErrorForFailureReason("invalid_protocol_version", {
264
+ currentBridgeProtocolVersion: message.bridgeProtocolVersion,
265
+ })
266
+ sendInitError(initError)
267
267
  return
268
268
  }
269
- if (message.status !== "success") {
269
+ if (message.bridgeProtocolVersion < minBridgeProtocolVersion) {
270
+ const initError = initErrorForFailureReason(
271
+ "unsupported_protocol_version",
272
+ {
273
+ currentBridgeProtocolVersion: message.bridgeProtocolVersion,
274
+ minBridgeProtocolVersion,
275
+ },
276
+ )
277
+ sendInitError(initError)
270
278
  return
271
279
  }
280
+
281
+ // If the sandbox reported an error during `ready`, return it as an `init` error.
282
+ if (message.status === "error") {
283
+ sendInitError(message.error)
284
+ return
285
+ }
286
+
272
287
  if (sendInit(message)) {
273
288
  onReady?.(message)
274
289
  }
@@ -286,7 +301,8 @@ export function createCustomBlockHost(
286
301
  const response = getInvalidSandboxMessageResponse(data)
287
302
 
288
303
  if (initStatus === "pending" && response.incomingType === "ready") {
289
- sendInitError("invalid_ready")
304
+ const initError = initErrorForFailureReason("invalid_ready")
305
+ sendInitError(initError)
290
306
  } else if (response.nack !== undefined) {
291
307
  post(response.nack)
292
308
  }
@@ -15,6 +15,10 @@ const UNKNOWN_ERROR_MESSAGE =
15
15
  */
16
16
  export function initErrorForFailureReason(
17
17
  reason: CustomBlockInitErrorCode,
18
+ data: {
19
+ minBridgeProtocolVersion?: number
20
+ currentBridgeProtocolVersion?: number
21
+ } = {},
18
22
  ): CustomBlockInitErrorInfo {
19
23
  switch (reason) {
20
24
  case "no_ready":
@@ -43,20 +47,31 @@ export function initErrorForFailureReason(
43
47
  // The sandbox must be updated to a valid manifest.
44
48
  isRetryable: false,
45
49
  }
46
- case "invalid_protocol_version":
50
+ case "invalid_protocol_version": {
51
+ const versionText = data.currentBridgeProtocolVersion
52
+ ? `: ${data.currentBridgeProtocolVersion}`
53
+ : "."
47
54
  return {
48
55
  code: reason,
49
- message: "Sandbox reported an invalid bridge protocol version.",
56
+ message: `Sandbox reported an invalid bridge protocol version ${versionText}`,
50
57
  // The sandbox must be updated to a supported protocol version.
51
58
  isRetryable: false,
52
59
  }
53
- case "unsupported_protocol_version":
60
+ }
61
+ case "unsupported_protocol_version": {
62
+ const currentVersionText = data.currentBridgeProtocolVersion
63
+ ? `${data.currentBridgeProtocolVersion} `
64
+ : ""
65
+ const minVersionText = data.minBridgeProtocolVersion
66
+ ? ` Minimum supported version is ${data.minBridgeProtocolVersion}.`
67
+ : ""
54
68
  return {
55
69
  code: reason,
56
- message: "Sandbox bridge protocol version is not supported.",
70
+ message: `Sandbox bridge protocol version ${currentVersionText}is not supported.${minVersionText}`,
57
71
  // The sandbox must be updated to a supported protocol version.
58
72
  isRetryable: false,
59
73
  }
74
+ }
60
75
  case "context_unavailable":
61
76
  return {
62
77
  code: reason,
@@ -1,36 +1,3 @@
1
- import type { CustomBlockInitErrorCode } from "../../bridge/messages/init.js"
2
- import type { ReadyMessage } from "../../bridge/messages/ready.js"
3
-
4
- export type ReadyInitFailure = {
5
- code: CustomBlockInitErrorCode
6
- message?: string
7
- }
8
-
9
- export function getReadyInitFailure(
10
- message: ReadyMessage,
11
- minBridgeProtocolVersion: number,
12
- ): ReadyInitFailure | undefined {
13
- if (!isValidBridgeProtocolVersion(message.bridgeProtocolVersion)) {
14
- return {
15
- code: "invalid_protocol_version",
16
- message: `Sandbox reported invalid bridge protocol version ${message.bridgeProtocolVersion}.`,
17
- }
18
- }
19
- if (message.bridgeProtocolVersion < minBridgeProtocolVersion) {
20
- return {
21
- code: "unsupported_protocol_version",
22
- message: `Sandbox bridge protocol version ${message.bridgeProtocolVersion} is below the host minimum ${minBridgeProtocolVersion}.`,
23
- }
24
- }
25
- if (message.status === "error") {
26
- return {
27
- code: message.error.code,
28
- message: message.error.message,
29
- }
30
- }
31
- return undefined
32
- }
33
-
34
- function isValidBridgeProtocolVersion(version: number): boolean {
1
+ export function isValidBridgeProtocolVersion(version: number): boolean {
35
2
  return Number.isFinite(version) && Number.isInteger(version) && version >= 1
36
3
  }