@notionhq/custom-blocks-host 0.3.11 → 0.4.1

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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -236
  3. package/THIRD_PARTY_LICENSES.md +9 -0
  4. package/dist/autoBindProperties.d.ts +5 -2
  5. package/dist/autoBindProperties.d.ts.map +1 -1
  6. package/dist/autoBindProperties.js +5 -2
  7. package/dist/browser.js +1 -1
  8. package/dist/compatibility/addMetadataReadSchemasForProtocol3To4.d.ts +1215 -0
  9. package/dist/compatibility/addMetadataReadSchemasForProtocol3To4.d.ts.map +1 -0
  10. package/dist/compatibility/addMetadataReadSchemasForProtocol3To4.js +27 -0
  11. package/dist/compatibility/normalizeHostToSandboxMessage.d.ts +8 -0
  12. package/dist/compatibility/normalizeHostToSandboxMessage.d.ts.map +1 -0
  13. package/dist/compatibility/normalizeHostToSandboxMessage.js +39 -0
  14. package/dist/compatibility/normalizePageResultsForProtocol3To5.d.ts +11 -0
  15. package/dist/compatibility/normalizePageResultsForProtocol3To5.d.ts.map +1 -0
  16. package/dist/compatibility/normalizePageResultsForProtocol3To5.js +18 -0
  17. package/dist/compatibility/normalizePageResultsForProtocol6.d.ts +9 -0
  18. package/dist/compatibility/normalizePageResultsForProtocol6.d.ts.map +1 -0
  19. package/dist/compatibility/normalizePageResultsForProtocol6.js +18 -0
  20. package/dist/compatibility/normalizeSandboxToHostMessage.d.ts +1099 -0
  21. package/dist/compatibility/normalizeSandboxToHostMessage.d.ts.map +1 -0
  22. package/dist/compatibility/normalizeSandboxToHostMessage.js +9 -0
  23. package/dist/compatibility/pageForProtocol3To5.d.ts +159 -0
  24. package/dist/compatibility/pageForProtocol3To5.d.ts.map +1 -0
  25. package/dist/{lifecycle/legacyPage.js → compatibility/pageForProtocol3To5.js} +50 -9
  26. package/dist/compatibility/pageResult.d.ts +11 -0
  27. package/dist/compatibility/pageResult.d.ts.map +1 -0
  28. package/dist/compatibility/pageWithNullTextFallbackProperties.d.ts +9 -0
  29. package/dist/compatibility/pageWithNullTextFallbackProperties.d.ts.map +1 -0
  30. package/dist/compatibility/pageWithNullTextFallbackProperties.js +46 -0
  31. package/dist/createCustomBlockHost.d.ts +18 -19
  32. package/dist/createCustomBlockHost.d.ts.map +1 -1
  33. package/dist/createCustomBlockHost.js +103 -38
  34. package/dist/index.d.ts +2 -19
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -2
  37. package/dist/{localBlockUrl.d.ts → isAllowedLocalPreviewUrl.d.ts} +2 -2
  38. package/dist/isAllowedLocalPreviewUrl.d.ts.map +1 -0
  39. package/dist/{localBlockUrl.js → isAllowedLocalPreviewUrl.js} +3 -7
  40. package/dist/lifecycle/initErrors.d.ts +1 -0
  41. package/dist/lifecycle/initErrors.d.ts.map +1 -1
  42. package/dist/lifecycle/initErrors.js +7 -0
  43. package/dist/lifecycle/initializationController.d.ts +6 -4
  44. package/dist/lifecycle/initializationController.d.ts.map +1 -1
  45. package/dist/lifecycle/initializationController.js +24 -15
  46. package/dist/lifecycle/resolveManifest.d.ts +13 -0
  47. package/dist/lifecycle/resolveManifest.d.ts.map +1 -0
  48. package/dist/lifecycle/resolveManifest.js +21 -0
  49. package/dist/lifecycle/types.d.ts +1637 -17
  50. package/dist/lifecycle/types.d.ts.map +1 -1
  51. package/dist/lifecycle/types.js +18 -0
  52. package/dist/messages/sandboxMessageRouter.d.ts.map +1 -1
  53. package/dist/messages/sandboxMessageRouter.js +2 -2
  54. package/dist/protocol/dataSources/dataSourcePage.d.ts +3 -4
  55. package/dist/protocol/dataSources/dataSourcePage.js +2 -3
  56. package/dist/protocol/index.d.ts +1 -0
  57. package/dist/protocol/index.js +1 -0
  58. package/dist/protocol/isLocalBlockServerUrl.d.ts +2 -0
  59. package/dist/protocol/isLocalBlockServerUrl.js +9 -0
  60. package/dist/protocol/messages/connect.d.ts +2 -2
  61. package/dist/protocol/messages/connect.js +2 -2
  62. package/dist/protocol/messages/createPageResult.d.ts +2 -2
  63. package/dist/protocol/messages/createPageResult.js +3 -3
  64. package/dist/protocol/messages/getPage.d.ts +2 -2
  65. package/dist/protocol/messages/getPage.js +3 -3
  66. package/dist/protocol/messages/hostToSandbox.d.ts +3 -3
  67. package/dist/protocol/messages/hostToSandbox.js +10 -10
  68. package/dist/protocol/messages/init.d.ts +1 -1
  69. package/dist/protocol/messages/queryDataSource.d.ts +24 -0
  70. package/dist/protocol/messages/queryDataSource.js +2 -0
  71. package/dist/protocol/messages/queryDataSourceResult.d.ts +3 -3
  72. package/dist/protocol/messages/queryDataSourceResult.js +6 -3
  73. package/dist/protocol/messages/sandboxToHost.d.ts +8 -0
  74. package/dist/protocol/messages/updatePageResult.d.ts +2 -2
  75. package/dist/protocol/messages/updatePageResult.js +3 -3
  76. package/dist/protocol/pages/page.d.ts +2 -2
  77. package/dist/protocol/pages/page.js +1 -1
  78. package/dist/protocol/protocolVersion.d.ts +2 -2
  79. package/dist/protocol/protocolVersion.js +2 -2
  80. package/dist/protocol.d.ts +9 -0
  81. package/dist/protocol.d.ts.map +1 -0
  82. package/dist/protocol.js +8 -0
  83. package/docs/bridge/changelog.md +67 -0
  84. package/docs/bridge/protocol.md +189 -0
  85. package/docs/bridge/versioning.md +38 -0
  86. package/docs/compatibility.md +68 -0
  87. package/docs/handlers.md +199 -0
  88. package/docs/lifecycle.md +117 -0
  89. package/package.json +10 -2
  90. package/src/autoBindProperties.ts +5 -2
  91. package/src/compatibility/addMetadataReadSchemasForProtocol3To4.ts +36 -0
  92. package/src/compatibility/normalizeHostToSandboxMessage.ts +54 -0
  93. package/src/compatibility/normalizePageResultsForProtocol3To5.ts +28 -0
  94. package/src/compatibility/normalizePageResultsForProtocol6.ts +23 -0
  95. package/src/compatibility/normalizeSandboxToHostMessage.ts +10 -0
  96. package/src/{lifecycle/legacyPage.ts → compatibility/pageForProtocol3To5.ts} +52 -11
  97. package/src/compatibility/pageResult.ts +18 -0
  98. package/src/compatibility/pageWithNullTextFallbackProperties.ts +53 -0
  99. package/src/createCustomBlockHost.ts +152 -62
  100. package/src/index.ts +4 -51
  101. package/src/{localBlockUrl.ts → isAllowedLocalPreviewUrl.ts} +4 -7
  102. package/src/lifecycle/initErrors.ts +9 -0
  103. package/src/lifecycle/initializationController.ts +34 -19
  104. package/src/lifecycle/resolveManifest.ts +31 -0
  105. package/src/lifecycle/types.ts +84 -16
  106. package/src/messages/sandboxMessageRouter.ts +2 -2
  107. package/src/protocol.ts +8 -0
  108. package/dist/lifecycle/legacyPage.d.ts +0 -70
  109. package/dist/lifecycle/legacyPage.d.ts.map +0 -1
  110. package/dist/lifecycle/loadCustomBlockManifest.d.ts +0 -16
  111. package/dist/lifecycle/loadCustomBlockManifest.d.ts.map +0 -1
  112. package/dist/lifecycle/loadCustomBlockManifest.js +0 -65
  113. package/dist/lifecycle/manifestOptions.d.ts +0 -17
  114. package/dist/lifecycle/manifestOptions.d.ts.map +0 -1
  115. package/dist/lifecycle/normalizeHostToSandboxMessage.d.ts +0 -6
  116. package/dist/lifecycle/normalizeHostToSandboxMessage.d.ts.map +0 -1
  117. package/dist/lifecycle/normalizeHostToSandboxMessage.js +0 -14
  118. package/dist/lifecycle/normalizePage.d.ts +0 -7
  119. package/dist/lifecycle/normalizePage.d.ts.map +0 -1
  120. package/dist/lifecycle/normalizePage.js +0 -47
  121. package/dist/localBlockUrl.d.ts.map +0 -1
  122. package/src/lifecycle/loadCustomBlockManifest.ts +0 -89
  123. package/src/lifecycle/manifestOptions.ts +0 -19
  124. package/src/lifecycle/normalizeHostToSandboxMessage.ts +0 -27
  125. package/src/lifecycle/normalizePage.ts +0 -71
  126. /package/dist/{lifecycle/manifestOptions.js → compatibility/pageResult.js} +0 -0
@@ -0,0 +1,117 @@
1
+ # Host initialization and lifecycle
2
+
3
+ The host prepares a block's manifest and context, completes initialization, and manages the
4
+ connection until the iframe is removed. This reference covers state preparation, timeouts,
5
+ callbacks, live state, and cleanup. Start with the [embed example](../README.md#create-a-host).
6
+
7
+ ## Manifest and initial state
8
+
9
+ Pass the full block URL as `sandboxUrl`. The host derives the origin used to authenticate incoming
10
+ messages and target outgoing messages. `CustomBlockManifestSource` selects the manifest:
11
+
12
+ | `manifestSource` | Manifest source |
13
+ | ---------------- | ---------------------------------------------------------------------------------------------------------- |
14
+ | `"host"` | The manifest returned by `getPersistedManifest({ signal })` |
15
+ | `"sandbox"` | The local declaration supplied in `connect.manifest`, subject to the [local URL policy](#local-block-urls) |
16
+
17
+ `getPersistedManifest` is required in both modes and starts when the host is created. It returns
18
+ `CustomBlockHostManifestResult`: either `{ status: "success", manifest }` or
19
+ `{ status: "error", error }`. It must not resolve to `null` or `undefined`. A definition that is
20
+ still loading keeps the promise pending; a failed load returns an error result. The host validates
21
+ the result and normalizes the manifest before calling `prepareInitialState`. Omitted `dataSources`
22
+ and each source's omitted `properties` default to `{}`.
23
+
24
+ The sandbox loads independently and sends `connect`. In sandbox mode, the SDK reads the manifest
25
+ from JSON metadata injected into the local block HTML by dev tooling. The host validates the
26
+ declaration in `connect`. The two sources do not wait for each other; only the selected result
27
+ determines initialization. Sandbox mode has no persisted fallback. The
28
+ [wire reference](bridge/protocol.md#initialization) describes the declaration and its
29
+ validation errors.
30
+
31
+ Once the selected manifest is ready, the host calls `prepareInitialState({ manifest, signal })`
32
+ once. Return `{ status: "success", initialState }` with context and data-source bindings, or
33
+ `{ status: "error", error }`. This callback also must not resolve to `null` or `undefined`. The host
34
+ validates its result. The prepared state omits the manifest; the host adds the selected manifest to
35
+ `init`. Host-mode preparation runs concurrently with connection setup. Sandbox-mode preparation
36
+ starts after `connect` supplies the declaration. The host sends `init` after preparation and a valid
37
+ connection succeed. A preparation failure is retained until the sandbox connects.
38
+
39
+ Errors from the selected manifest source retain their `code`, `message`, and `isRetryable`. A
40
+ missing sandbox declaration produces `manifest_unavailable`. A sandbox `manifest_invalid` error is
41
+ ignored in host mode because that mode uses the persisted manifest; other connect errors remain
42
+ fatal. Unexpected exceptions or invalid results from either callback produce `unknown_error`.
43
+ Diagnostics go to `onLog("warn", payload)`. Stopping aborts preparation and suppresses late results
44
+ without a cancellation warning.
45
+
46
+ ## Timeouts
47
+
48
+ | Option | Waits for | Default | Failure code |
49
+ | ----------------------- | ------------------------------------- | --------- | ----------------------- |
50
+ | `noConnectTimeoutMs` | `connect` after the first iframe load | 5 seconds | `no_connect` |
51
+ | `initialStateTimeoutMs` | State preparation after `connect` | 5 seconds | `initial_state_timeout` |
52
+ | `noInitResultTimeoutMs` | `initResult` after `init` | 5 seconds | `no_init_result` |
53
+
54
+ The SDK waits for `init` without its own timeout. Handshake failures abort preparation and suppress
55
+ late results. Manifest and state preparation providers must respect their `signal`.
56
+
57
+ ## Live state
58
+
59
+ `setTheme()`, `setContrastMode()`, `setParent()`, `setPage()`, `setDataSources()`, and
60
+ `setCurrentUser()` hold updates during initialization and send them after `initResult.success`. For
61
+ data-source query refreshes, see [Handlers and data](handlers.md#refresh-active-queries).
62
+
63
+ ## Callbacks
64
+
65
+ The required `onStatusChange(state)` callback receives the `CustomBlockHostStatus` union:
66
+
67
+ | Status | Behavior | Payload |
68
+ | ---------------------- | -------------------------------------------------------------- | ------------------------------------------------------ |
69
+ | `waitingForConnect` | The host is listening; reported synchronously during creation | None |
70
+ | `preparingInit` | The host accepted a connection and is preparing initialization | `localManifest`, `sdkVersion`, `bridgeProtocolVersion` |
71
+ | `waitingForInitResult` | Successful `init` was sent; acknowledgment is pending | None |
72
+ | `initialized` | The sandbox accepted initialization | `resolvedManifest`, optional `initialHeight` |
73
+ | `failed` | Initialization or runtime failed | `error` |
74
+ | `stopped` | The application explicitly disposed of the host | None |
75
+
76
+ `localManifest` is optional and comes only from a permitted local sandbox. It is available before
77
+ initialization completes, so an adapter can use its requirements to prepare bindings.
78
+ `resolvedManifest` is the selected declaration sent in `init`. Rejected connections can skip
79
+ intermediate statuses. Both `failed` and `stopped` are terminal; stopping a failed host releases
80
+ resources without replacing its failure status.
81
+
82
+ Connection failures contain `{ phase, source, code, message, isRetryable }`. The phase is `connect`,
83
+ `init`, `initResult`, or `runtime`. The source identifies who reported the failure. Failures can
84
+ occur after initialization succeeds.
85
+
86
+ At `initialized`, apply a supplied `initialHeight` in CSS pixels before revealing the iframe. If the
87
+ height is absent, use your application's default size. The `resize` handler applies later height
88
+ changes. Reveal, sizing, and failure UI belong to the embedding application.
89
+
90
+ | Other callback | Behavior |
91
+ | ------------------------------------- | -------------------------------------------------------------------------------------------- |
92
+ | `onQueryUnsubscribed(subscriptionId)` | Runs once when a subscription ends through unsubscribe, replacement, terminal error, or stop |
93
+ | `onLog(direction, payload)` | Receives bridge messages and warnings |
94
+
95
+ ## Iframe lifetime
96
+
97
+ Each host handles one handshake. Create a new host and iframe to retry or replace the block URL. The
98
+ [wire reference](bridge/protocol.md#initialization) defines handshake and
99
+ malformed-message behavior.
100
+
101
+ The first observed iframe `load` is allowed. A second load stops the host and reports retryable
102
+ `iframe_reloaded`. Load-event cleanup does not isolate documents before that event. A replacement
103
+ document at the same origin can receive messages and invoke handlers until the host stops.
104
+
105
+ `stop()` removes listeners, cancels preparation, clears timers, ends subscriptions, and suppresses
106
+ pending responses. Call it when removing the iframe.
107
+
108
+ ## Local block URLs
109
+
110
+ `isAllowedLocalPreviewUrl({ url, hostOrigin })` accepts HTTP or HTTPS URLs on `localhost`,
111
+ `*.localhost`, or `127.0.0.1` without credentials. It rejects the embedding page's origin.
112
+ `hostOrigin` must be nonempty. Pass the embedding page's serialized origin.
113
+
114
+ The protocol helper `isLocalBlockServerUrl(url)` checks the server URL without requiring origin
115
+ isolation. The host adds the embedding-origin restriction. `manifestSource: "sandbox"` enforces this
116
+ policy before the host starts. An invalid sandbox-mode URL throws `CustomBlockInitializationError`
117
+ with code `invalid_local_preview_url`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks-host",
3
- "version": "0.3.11",
3
+ "version": "0.4.1",
4
4
  "description": "Reusable host runtime for Notion custom blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,6 +14,11 @@
14
14
  "import": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts",
16
16
  "default": "./dist/index.js"
17
+ },
18
+ "./protocol": {
19
+ "types": "./dist/protocol.d.ts",
20
+ "import": "./dist/protocol.js",
21
+ "default": "./dist/protocol.js"
17
22
  }
18
23
  },
19
24
  "dependencies": {
@@ -22,6 +27,9 @@
22
27
  "files": [
23
28
  "src",
24
29
  "dist",
25
- "README.md"
30
+ "docs",
31
+ "README.md",
32
+ "LICENSE",
33
+ "THIRD_PARTY_LICENSES.md"
26
34
  ]
27
35
  }
@@ -6,8 +6,8 @@ export type AutoBindableProperty = {
6
6
  }
7
7
 
8
8
  /**
9
- * Finds property IDs for a manifest's properties without changing the
10
- * manifest's semantic keys.
9
+ * Returns a map from manifest property keys to database property IDs.
10
+ * Unmapped keys are omitted.
11
11
  *
12
12
  * For each manifest property:
13
13
  * 1. Consider only schema properties of the same type.
@@ -15,6 +15,9 @@ export type AutoBindableProperty = {
15
15
  * (exact string: no trim, no case folding) or its display name equals
16
16
  * the manifest display name after `trim()` and lowercasing.
17
17
  * 3. Bind when there is exactly one candidate. Otherwise leave it unmapped.
18
+ *
19
+ * An ID match has no priority over a display name match. If they identify two
20
+ * different properties of the same type, the manifest property stays unmapped.
18
21
  */
19
22
  export function getAutoBoundPropertyIdsByKey(args: {
20
23
  manifestProperties: Readonly<Record<string, AutoBindableProperty>>
@@ -0,0 +1,36 @@
1
+ import { NOTION_BUILTIN_PROPERTY_SCHEMAS } from "@notionhq/custom-blocks-protocol/dataSources/propertySchema.js"
2
+ import type { HostToSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
3
+
4
+ /**
5
+ * Protocols 3–4 derive metadata read schemas from host bindings. Restore those
6
+ * schemas only in outgoing copies so host query validation keeps the actual schema.
7
+ *
8
+ * Delete when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION >= 5.
9
+ */
10
+ export function addMetadataReadSchemasForProtocol3To4(
11
+ message: Extract<
12
+ HostToSandboxMessage,
13
+ { type: "init"; status: "success" } | { type: "dataSourcesChanged" }
14
+ >,
15
+ ) {
16
+ return {
17
+ ...message,
18
+ dataSources: {
19
+ bindings: Object.fromEntries(
20
+ Object.entries(message.dataSources.bindings).map(([key, binding]) => [
21
+ key,
22
+ {
23
+ ...binding,
24
+ collectionSchema: {
25
+ ...binding.collectionSchema,
26
+ propertiesById: {
27
+ ...binding.collectionSchema?.propertiesById,
28
+ ...NOTION_BUILTIN_PROPERTY_SCHEMAS,
29
+ },
30
+ },
31
+ },
32
+ ]),
33
+ ),
34
+ },
35
+ }
36
+ }
@@ -0,0 +1,54 @@
1
+ import type {
2
+ HostToSandboxMessage,
3
+ HostToSandboxMessageWithProtocol3To5Compatibility,
4
+ } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
5
+ import {
6
+ TEXT_FALLBACK_PROPERTY_PROTOCOL_VERSION,
7
+ UNIFIED_PAGE_PROTOCOL_VERSION,
8
+ } from "@notionhq/custom-blocks-protocol/protocolVersion.js"
9
+ import { addMetadataReadSchemasForProtocol3To4 } from "./addMetadataReadSchemasForProtocol3To4.js"
10
+ import { normalizePageResultsForProtocol3To5 } from "./normalizePageResultsForProtocol3To5.js"
11
+ import { normalizePageResultsForProtocol6 } from "./normalizePageResultsForProtocol6.js"
12
+
13
+ /**
14
+ * Encodes current host messages for the sandbox's declared protocol. Handlers use
15
+ * current shapes. Each target adapter starts from that shape to avoid data loss.
16
+ * Unchanged messages and error results retain their original payloads.
17
+ */
18
+ export function normalizeHostToSandboxMessage(
19
+ message: HostToSandboxMessage,
20
+ bridgeProtocolVersion: number,
21
+ ): HostToSandboxMessageWithProtocol3To5Compatibility {
22
+ // Accepted compatibility gap (2026-10-31): leave unavailable containing-page
23
+ // context in init/pageChanged unchanged for early protocol 5 SDKs. Protocol 5
24
+ // shipped in SDK 0.2.16 on 2026-09-30. SDK 0.2.17 added unavailable context on
25
+ // 2026-10-02 without a protocol bump. SDK 0.2.16 cannot decode that context.
26
+ // Preserve normal contexts and later protocol 5 support. Affected blocks must
27
+ // upgrade their SDK rather than adding SDK-version-based translation here.
28
+ if (
29
+ (message.type === "init" && message.status === "success") ||
30
+ message.type === "dataSourcesChanged"
31
+ ) {
32
+ return bridgeProtocolVersion < 5
33
+ ? addMetadataReadSchemasForProtocol3To4(message)
34
+ : message
35
+ }
36
+ if (
37
+ message.type !== "queryDataSourceResult" &&
38
+ message.type !== "getPageResult" &&
39
+ message.type !== "createPageResult" &&
40
+ message.type !== "updatePageResult"
41
+ ) {
42
+ return message
43
+ }
44
+ if (message.status === "error") {
45
+ return message
46
+ }
47
+ if (bridgeProtocolVersion < UNIFIED_PAGE_PROTOCOL_VERSION) {
48
+ return normalizePageResultsForProtocol3To5(message)
49
+ }
50
+ if (bridgeProtocolVersion < TEXT_FALLBACK_PROPERTY_PROTOCOL_VERSION) {
51
+ return normalizePageResultsForProtocol6(message)
52
+ }
53
+ return message
54
+ }
@@ -0,0 +1,28 @@
1
+ import type { HostToSandboxMessageWithProtocol3To5Compatibility } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
2
+ import {
3
+ toPageForProtocol3To5,
4
+ toQueryPageForProtocol3To5,
5
+ } from "./pageForProtocol3To5.js"
6
+ import type { SuccessfulPageResult } from "./pageResult.js"
7
+ import { toPageWithNullTextFallbackProperties } from "./pageWithNullTextFallbackProperties.js"
8
+
9
+ /**
10
+ * Encodes current pages for protocols 3–5. Query values are flat and ID-keyed.
11
+ * Page operations omit unsupported property objects and use ID-keyed properties.
12
+ * Convert directly from current pages so query text fallbacks remain available.
13
+ *
14
+ * Delete when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION >= 6.
15
+ */
16
+ export function normalizePageResultsForProtocol3To5(
17
+ message: SuccessfulPageResult,
18
+ ): HostToSandboxMessageWithProtocol3To5Compatibility {
19
+ if (message.type === "queryDataSourceResult") {
20
+ return { ...message, items: message.items.map(toQueryPageForProtocol3To5) }
21
+ }
22
+ return {
23
+ ...message,
24
+ page: toPageForProtocol3To5(
25
+ toPageWithNullTextFallbackProperties(message.page),
26
+ ),
27
+ }
28
+ }
@@ -0,0 +1,23 @@
1
+ import type { HostToSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
2
+ import type { SuccessfulPageResult } from "./pageResult.js"
3
+ import { toPageWithNullTextFallbackProperties } from "./pageWithNullTextFallbackProperties.js"
4
+
5
+ /**
6
+ * Encodes protocol 6 pages, whose receivers require null for text fallback properties.
7
+ *
8
+ * Delete when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION >= 7.
9
+ */
10
+ export function normalizePageResultsForProtocol6(
11
+ message: SuccessfulPageResult,
12
+ ): HostToSandboxMessage {
13
+ if (message.type === "queryDataSourceResult") {
14
+ return {
15
+ ...message,
16
+ items: message.items.map(toPageWithNullTextFallbackProperties),
17
+ }
18
+ }
19
+ return {
20
+ ...message,
21
+ page: toPageWithNullTextFallbackProperties(message.page),
22
+ }
23
+ }
@@ -0,0 +1,10 @@
1
+ import { sandboxToHostMessageSchema } from "@notionhq/custom-blocks-protocol/messages/sandboxToHost.js"
2
+ import * as v from "valibot"
3
+
4
+ /**
5
+ * Validates sandbox wire messages before handler dispatch. Supported requests
6
+ * currently share one schema, so parsing produces the current handler shape.
7
+ */
8
+ export function normalizeSandboxToHostMessage(payload: unknown) {
9
+ return v.safeParse(sandboxToHostMessageSchema, payload)
10
+ }
@@ -1,6 +1,6 @@
1
1
  import type { NotionDataSourceValue } from "@notionhq/custom-blocks-protocol/dataSources/dataSourceValue.js"
2
2
  import type {
3
- LegacyNotionPage,
3
+ NotionPageForProtocol3To5,
4
4
  NotionPagePropertyValue,
5
5
  notionPageDataSchema,
6
6
  } from "@notionhq/custom-blocks-protocol/pages/page.js"
@@ -8,9 +8,17 @@ import type { InferOutput } from "valibot"
8
8
  import { toNotionDateValue } from "../dateValue.js"
9
9
  import { unreachable } from "../utils.js"
10
10
 
11
- export function toLegacyPage(
11
+ /**
12
+ * Converts current page data to the page-operation format used by protocols 3–5.
13
+ * Keys property objects by raw ID and omits null properties. Optional icons,
14
+ * covers, timestamps, and URLs use undefined instead of null in this format.
15
+ * Call after clearing property objects unsupported by these protocols.
16
+ *
17
+ * Delete when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION >= 6.
18
+ */
19
+ export function toPageForProtocol3To5(
12
20
  page: InferOutput<typeof notionPageDataSchema>,
13
- ): LegacyNotionPage {
21
+ ): NotionPageForProtocol3To5 {
14
22
  return {
15
23
  ...page,
16
24
  properties: Object.fromEntries(
@@ -27,23 +35,44 @@ export function toLegacyPage(
27
35
  }
28
36
  }
29
37
 
30
- export function toLegacyQueryPage(
38
+ /**
39
+ * Converts a current page to a protocol 3–5 query row, keeping its ID and archive
40
+ * flags while flattening typed property objects into raw-ID-keyed values.
41
+ * Copies page creation and edit metadata into the reserved property IDs that
42
+ * these query readers expect.
43
+ *
44
+ * Delete when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION >= 6.
45
+ */
46
+ export function toQueryPageForProtocol3To5(
31
47
  page: InferOutput<typeof notionPageDataSchema>,
32
48
  ) {
33
49
  const propertiesById: Record<string, NotionDataSourceValue | undefined> = {}
34
50
  for (const property of Object.values(page.properties)) {
35
51
  if (property) {
36
- propertiesById[property.id] = toLegacyProperty(property)
52
+ propertiesById[property.id] = toPropertyValueForProtocol3To5(property)
37
53
  }
38
54
  }
39
55
  return {
40
56
  id: page.id,
41
57
  is_archived: page.is_archived,
42
58
  in_trash: page.in_trash,
43
- propertiesById,
59
+ // Reserved metadata IDs retain their historical meaning even if an actual
60
+ // property uses the same ID. Binding schemas use the same precedence.
61
+ propertiesById: {
62
+ ...propertiesById,
63
+ created_time: toDateTimeValueForProtocol3To5(page.created_time),
64
+ last_edited_time: toDateTimeValueForProtocol3To5(page.last_edited_time),
65
+ created_by: page.created_by ? [page.created_by] : undefined,
66
+ last_edited_by: page.last_edited_by ? [page.last_edited_by] : undefined,
67
+ },
44
68
  }
45
69
  }
46
70
 
71
+ /**
72
+ * Flattens rich-text items to the string used by protocol 3–5 query values.
73
+ *
74
+ * Delete when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION >= 6.
75
+ */
47
76
  function plainText(items: Record<string, unknown>[]): string {
48
77
  return items
49
78
  .map(item => {
@@ -61,7 +90,13 @@ function plainText(items: Record<string, unknown>[]): string {
61
90
  .join("")
62
91
  }
63
92
 
64
- function timestamp(
93
+ /**
94
+ * Converts a timestamp to the UTC date/time object used by protocol 3–5 queries.
95
+ * Missing or invalid timestamps produce undefined; time is stored to the minute.
96
+ *
97
+ * Delete when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION >= 6.
98
+ */
99
+ function toDateTimeValueForProtocol3To5(
65
100
  value: string | number | null | undefined,
66
101
  ): NotionDataSourceValue | undefined {
67
102
  if (value === undefined || value === null || value === "") {
@@ -79,7 +114,13 @@ function timestamp(
79
114
  }
80
115
  }
81
116
 
82
- function toLegacyProperty(
117
+ /**
118
+ * Converts a typed page property to the scalar, list, record-reference, or
119
+ * date/time value used by protocol 3–5 query results.
120
+ *
121
+ * Delete when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION >= 6.
122
+ */
123
+ function toPropertyValueForProtocol3To5(
83
124
  property: NotionPagePropertyValue,
84
125
  ): NotionDataSourceValue | undefined {
85
126
  switch (property.type) {
@@ -134,11 +175,11 @@ function toLegacyProperty(
134
175
  case "place":
135
176
  return ""
136
177
  case "created_time":
137
- return timestamp(property.created_time)
178
+ return toDateTimeValueForProtocol3To5(property.created_time)
138
179
  case "last_edited_time":
139
- return timestamp(property.last_edited_time)
180
+ return toDateTimeValueForProtocol3To5(property.last_edited_time)
140
181
  case "last_visited_time":
141
- return timestamp(property.last_visited_time)
182
+ return toDateTimeValueForProtocol3To5(property.last_visited_time)
142
183
  case "created_by":
143
184
  return property.created_by ? [property.created_by] : undefined
144
185
  case "last_edited_by":
@@ -0,0 +1,18 @@
1
+ import type { HostToSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
2
+
3
+ /**
4
+ * Page-bearing responses accepted by the target-protocol adapters. The dispatcher
5
+ * passes errors and unrelated messages through unchanged before calling them.
6
+ * Sharing this subset keeps both adapters limited to successful page conversions.
7
+ */
8
+ export type SuccessfulPageResult = Extract<
9
+ HostToSandboxMessage,
10
+ {
11
+ type:
12
+ | "queryDataSourceResult"
13
+ | "getPageResult"
14
+ | "createPageResult"
15
+ | "updatePageResult"
16
+ status: "success"
17
+ }
18
+ >
@@ -0,0 +1,53 @@
1
+ import type { notionPageDataSchema } from "@notionhq/custom-blocks-protocol/pages/page.js"
2
+ import type { InferOutput } from "valibot"
3
+ import { unreachable } from "../utils.js"
4
+
5
+ /**
6
+ * Shared field conversion for receivers that cannot decode text fallback objects.
7
+ *
8
+ * Delete when MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION >= 7.
9
+ */
10
+ export function toPageWithNullTextFallbackProperties(
11
+ page: InferOutput<typeof notionPageDataSchema>,
12
+ ): InferOutput<typeof notionPageDataSchema> {
13
+ return {
14
+ ...page,
15
+ properties: Object.fromEntries(
16
+ Object.entries(page.properties).map(([name, property]) => {
17
+ switch (property?.type) {
18
+ case "unique_id":
19
+ case "button":
20
+ case "formula":
21
+ case "location":
22
+ case "rollup":
23
+ case "verification":
24
+ return [name, null]
25
+ case "title":
26
+ case "rich_text":
27
+ case "number":
28
+ case "url":
29
+ case "email":
30
+ case "phone_number":
31
+ case "checkbox":
32
+ case "select":
33
+ case "status":
34
+ case "multi_select":
35
+ case "date":
36
+ case "people":
37
+ case "relation":
38
+ case "files":
39
+ case "place":
40
+ case "created_time":
41
+ case "last_edited_time":
42
+ case "created_by":
43
+ case "last_edited_by":
44
+ case "last_visited_time":
45
+ case undefined:
46
+ return [name, property]
47
+ default:
48
+ return unreachable(property)
49
+ }
50
+ }),
51
+ ),
52
+ }
53
+ }