@notionhq/custom-blocks-host 0.3.8 → 0.4.0

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -225
  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 +1091 -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/queryDataSourceResult.d.ts +3 -3
  70. package/dist/protocol/messages/queryDataSourceResult.js +6 -3
  71. package/dist/protocol/messages/updatePageResult.d.ts +2 -2
  72. package/dist/protocol/messages/updatePageResult.js +3 -3
  73. package/dist/protocol/pages/page.d.ts +2 -2
  74. package/dist/protocol/pages/page.js +1 -1
  75. package/dist/protocol/protocolVersion.d.ts +3 -2
  76. package/dist/protocol/protocolVersion.js +4 -2
  77. package/dist/protocol.d.ts +9 -0
  78. package/dist/protocol.d.ts.map +1 -0
  79. package/dist/protocol.js +8 -0
  80. package/docs/bridge/changelog.md +67 -0
  81. package/docs/bridge/protocol.md +182 -0
  82. package/docs/bridge/versioning.md +38 -0
  83. package/docs/compatibility.md +68 -0
  84. package/docs/handlers.md +199 -0
  85. package/docs/lifecycle.md +117 -0
  86. package/package.json +11 -3
  87. package/src/autoBindProperties.ts +5 -2
  88. package/src/compatibility/addMetadataReadSchemasForProtocol3To4.ts +36 -0
  89. package/src/compatibility/normalizeHostToSandboxMessage.ts +54 -0
  90. package/src/compatibility/normalizePageResultsForProtocol3To5.ts +28 -0
  91. package/src/compatibility/normalizePageResultsForProtocol6.ts +23 -0
  92. package/src/compatibility/normalizeSandboxToHostMessage.ts +10 -0
  93. package/src/{lifecycle/legacyPage.ts → compatibility/pageForProtocol3To5.ts} +52 -11
  94. package/src/compatibility/pageResult.ts +18 -0
  95. package/src/compatibility/pageWithNullTextFallbackProperties.ts +53 -0
  96. package/src/createCustomBlockHost.ts +152 -62
  97. package/src/index.ts +4 -51
  98. package/src/{localBlockUrl.ts → isAllowedLocalPreviewUrl.ts} +4 -7
  99. package/src/lifecycle/initErrors.ts +9 -0
  100. package/src/lifecycle/initializationController.ts +34 -19
  101. package/src/lifecycle/resolveManifest.ts +31 -0
  102. package/src/lifecycle/types.ts +84 -16
  103. package/src/messages/sandboxMessageRouter.ts +2 -2
  104. package/src/protocol.ts +8 -0
  105. package/dist/lifecycle/legacyPage.d.ts +0 -70
  106. package/dist/lifecycle/legacyPage.d.ts.map +0 -1
  107. package/dist/lifecycle/loadCustomBlockManifest.d.ts +0 -16
  108. package/dist/lifecycle/loadCustomBlockManifest.d.ts.map +0 -1
  109. package/dist/lifecycle/loadCustomBlockManifest.js +0 -65
  110. package/dist/lifecycle/manifestOptions.d.ts +0 -17
  111. package/dist/lifecycle/manifestOptions.d.ts.map +0 -1
  112. package/dist/lifecycle/normalizeHostToSandboxMessage.d.ts +0 -6
  113. package/dist/lifecycle/normalizeHostToSandboxMessage.d.ts.map +0 -1
  114. package/dist/lifecycle/normalizeHostToSandboxMessage.js +0 -16
  115. package/dist/lifecycle/normalizePage.d.ts +0 -7
  116. package/dist/lifecycle/normalizePage.d.ts.map +0 -1
  117. package/dist/lifecycle/normalizePage.js +0 -22
  118. package/dist/localBlockUrl.d.ts.map +0 -1
  119. package/src/lifecycle/loadCustomBlockManifest.ts +0 -89
  120. package/src/lifecycle/manifestOptions.ts +0 -19
  121. package/src/lifecycle/normalizeHostToSandboxMessage.ts +0 -29
  122. package/src/lifecycle/normalizePage.ts +0 -35
  123. /package/dist/{lifecycle/manifestOptions.js → compatibility/pageResult.js} +0 -0
@@ -0,0 +1,182 @@
1
+ # Bridge wire protocol
2
+
3
+ The bridge carries initialization, data requests, results, and live updates between a custom block's
4
+ SDK and its host. This reference defines the handshake, message identity, validation, and payload
5
+ semantics for host integrators and bridge maintainers. Block authors use the SDK's public APIs.
6
+
7
+ Messages are `postMessage` payloads defined by shared Valibot schemas, exposed through
8
+ `@notionhq/custom-blocks-host/protocol`. The embedding application enforces permissions and record
9
+ access. Version-specific formats and conversions are described in
10
+ [Versioning and compatibility](versioning.md).
11
+
12
+ ## Initialization
13
+
14
+ The handshake is:
15
+
16
+ ```text
17
+ sandbox connect → host init → sandbox initResult
18
+ ```
19
+
20
+ All three messages carry the sandbox-generated `initializationId`. A sandbox document sends
21
+ `connect` once. The host sends one `init` for the accepted connection and ignores subsequent
22
+ `connect` messages, including malformed ones. Live updates wait for `initResult.success`.
23
+ `init.error` and `initResult.error` are terminal.
24
+
25
+ The sandbox waits for `init` without a timeout. If host state preparation times out, the host sends
26
+ `init.error` with `initial_state_timeout` to end that wait.
27
+
28
+ `connect` carries connection identity, version metadata, and an optional local manifest declaration.
29
+ The SDK reads that declaration from the `application/json` script with ID
30
+ `notion-custom-block-manifest`, injected into the block HTML by dev tooling. It reads metadata only
31
+ from supported local block server URLs. Missing metadata omits `connect.manifest`; invalid metadata
32
+ sends `connect.error` with `manifest_invalid`.
33
+
34
+ The embedding application selects the manifest source. In host mode, the host uses its persisted
35
+ manifest and ignores the sandbox declaration. In sandbox mode, the host uses `connect.manifest` from
36
+ a permitted local sandbox; a missing declaration produces `manifest_unavailable`. The selected
37
+ manifest is authoritative and is returned in `init` with context and data-source bindings. Errors
38
+ from the selected manifest source pass through unchanged. Host mode can initialize after a sandbox
39
+ `manifest_invalid` error using its own manifest; other connect errors remain fatal. The connect
40
+ parser discards unknown properties.
41
+
42
+ `initResult.success` may include `initialHeight`, the height of the sandbox's rendered content in
43
+ CSS pixels. The host's [lifecycle guide](../lifecycle.md) describes iframe sizing,
44
+ reveal, and retry behavior.
45
+
46
+ `connect.bridgeProtocolVersion` declares the sandbox's wire contract. `sdkVersion` is analytics
47
+ metadata. See [versioning and compatibility](versioning.md) for supported formats and
48
+ [Validation and errors](#validation-and-errors) for malformed handshake behavior.
49
+
50
+ ## Manifests
51
+
52
+ A manifest declares a block's data sources and display metadata. `manifestSchema` defines its fields
53
+ and defaults omitted `dataSources` and each data source's omitted `properties` to `{}`.
54
+
55
+ `isLocalBlockServerUrl(url)` accepts HTTP(S) URLs on `localhost`, `*.localhost`, or `127.0.0.1`
56
+ without credentials. It checks the address only. The host also requires a sandbox-mode block to use
57
+ a different origin from its embedding page.
58
+
59
+ ## Message identity
60
+
61
+ - One-shot request/result pairs use `requestId`.
62
+ - `queryDataSource` is a long-lived subscription; requests and every update use its stable
63
+ `subscriptionId`.
64
+ - The initialization lifecycle uses `initializationId`, not `requestId` or `subscriptionId`.
65
+ - One-way messages, such as `resize` and bridge NACKs, have no correlation ID.
66
+
67
+ Hosts key query state by `subscriptionId`, replace state when a request reuses one, and suppress
68
+ stale asynchronous results.
69
+
70
+ The sandbox can send `unsubscribeDataSourceQuery` with the same `subscriptionId` when it no longer
71
+ needs the subscription. An unknown or repeated unsubscribe is a no-op. Hosts stop future refreshes
72
+ and suppress results from work already in progress.
73
+
74
+ ## Validation and errors
75
+
76
+ Each direction has a validation schema:
77
+
78
+ - Sandbox to host: `sandboxToHostMessageSchema`
79
+ - Host to sandbox: `hostToSandboxMessageSchema`
80
+
81
+ On parse failure, the host replies with `invalidSandboxMessage` and the sandbox replies with
82
+ `invalidHostMessage`, except for the handshake cases below. These replies are negative
83
+ acknowledgments (NACKs). Receivers do not NACK another NACK.
84
+
85
+ Malformed sandbox messages do not change the host's initialization state or timeout, except as
86
+ follows:
87
+
88
+ - The host expects `connect` and receives a malformed `connect` with a readable `initializationId`.
89
+ The host sends `init.error` instead of a NACK, then fails initialization. An invalid or
90
+ unsupported numeric protocol version takes precedence over `invalid_connect_payload`.
91
+ - The host expects `initResult` and receives a malformed `initResult` with the active
92
+ `initializationId`. The host sends a NACK and fails initialization with
93
+ `invalid_init_result_payload`. This error code describes a local host failure. The host does not
94
+ send it in `initResult.error`.
95
+
96
+ A malformed `initResult` with a missing or stale ID does not end initialization or reset its
97
+ timeout. Malformed messages do not restart initialization after it succeeds or fails.
98
+
99
+ The sandbox handles a malformed `init` differently if it can verify the `initializationId`. In that
100
+ case, it sends `initResult.error` with `invalid_init_payload` instead of a NACK. Otherwise, it sends
101
+ `invalidHostMessage`.
102
+
103
+ `CustomBlockErrorInfo` carries `code`, `message`, and `isRetryable`. Error schemas accept any string
104
+ for `code`, including unknown codes. TypeScript aliases list known codes for autocomplete.
105
+
106
+ ## Data sources
107
+
108
+ ### Bindings and IDs
109
+
110
+ Hosts send `NotionDataSourceBindings` as `dataSources: { bindings }`, keyed by the semantic data
111
+ source key declared in the manifest. `collectionSchema` contains only actual database properties.
112
+
113
+ Sandbox-to-host object references are raw IDs. The SDK resolves semantic data-source and property
114
+ keys before sending query or page-operation requests. Get and update requests can carry an optional
115
+ resolved `dataSourceId`.
116
+
117
+ ### Query requests
118
+
119
+ `queryDataSource` may include a resolved `filter` and an ordered `sorts` list. Each sort contains a
120
+ raw `propertyId` and an `ascending` or `descending` direction. Hosts apply sorts in array order. The
121
+ SDK and host validate that the list contains at most ten unique properties. The host rejects
122
+ property types without a supported sort value.
123
+
124
+ Every filter and sort property ID must exist in `collectionSchema.propertiesById`. Queries with
125
+ filters or sorts return `data_source_schema_unavailable` when the schema is unavailable. Filter
126
+ branches, operators, person references, and sort objects reject extra fields.
127
+
128
+ #### Creator and status filters
129
+
130
+ Creator filters require `propertyId` to identify an actual database property of type `created_by`.
131
+ The `created_by` filter branch selects the property type, while `propertyId` addresses the column.
132
+
133
+ The `created_by` filter supports human user targets with `contains` and `does_not_contain`, plus
134
+ `is_empty` and `is_not_empty`. Hosts use row creator metadata, including non-human and missing
135
+ creators in exclusion results.
136
+
137
+ Status filters support option operators and stored group names. Group operators accept one name or
138
+ an array of 1–5 names. Hosts must validate each name against the bound Status property.
139
+
140
+ #### Date filters
141
+
142
+ Date comparisons accept ISO values and relative values such as `today`. The query schema defines the
143
+ accepted values and range operators. Range operators require an empty object.
144
+
145
+ Date comparisons accept `target` inside `date`, beside the operator. The target is `start_date` by
146
+ default. For ranges, `end_date` compares the range end. For individual dates, it compares the date.
147
+ Empty checks accept either target, but they test the whole property.
148
+
149
+ ```ts
150
+ filter: {
151
+ propertyId: "schedule-id",
152
+ date: { target: "end_date", on_or_after: "2026-10-08" },
153
+ }
154
+ ```
155
+
156
+ ## Page context
157
+
158
+ Containing-page context uses `CustomBlockPage`. The `init.page` and `pageChanged.page` fields
159
+ contain `{ type: "page", id, parent }` for an accessible page, or `{ type: "unavailable" }` when the
160
+ containing page itself is inaccessible. The unavailable shape never carries a page ID. This is
161
+ distinct from an accessible page with an inaccessible parent
162
+ (`page.parent: { type: "unavailable" }`).
163
+
164
+ ## Page writes
165
+
166
+ The `createPage` and `updatePage` messages use raw property IDs from Notion as map keys. Each value
167
+ must carry the same raw property ID in its `id` field.
168
+
169
+ ## Page responses
170
+
171
+ Host handlers return unified `NotionPageData` for queries and page operations. Properties use
172
+ display names as keys and typed values that include their raw property IDs. Only actual schema
173
+ properties are returned. Query results contain `items` and `hasMore`. `notionPageDataSchema` defines
174
+ the page fields, including archive state and creation, edit, and visit metadata.
175
+
176
+ Page reads return string values for `unique_id`, `formula`, `rollup`, `button`, `verification`, and
177
+ `location`, preserving each property's type. These properties are read-only. Values use stored text,
178
+ or `""` when absent. Hosts do not evaluate formulas or rollups.
179
+
180
+ The host converts responses to the sandbox's declared protocol. See
181
+ [versioning and compatibility](versioning.md) for protocol version rules. The host's
182
+ [compatibility reference](../compatibility.md) describes older response formats.
@@ -0,0 +1,38 @@
1
+ # Bridge protocol versioning
2
+
3
+ The SDK inside a block and the host in its embedding application release independently. A bridge
4
+ protocol version identifies the wire contract they use, independently of their package versions.
5
+ This reference defines the version identifiers, protocol format names, and rules for contract
6
+ changes. Release boundaries and exceptions are recorded in the [changelog](changelog.md). The host's
7
+ [compatibility reference](../compatibility.md) describes runtime support and response
8
+ conversions.
9
+
10
+ ## Version identifiers
11
+
12
+ - **Bridge protocol:** `connect.bridgeProtocolVersion` declares the sandbox's wire contract.
13
+ - **SDK package:** identifies the SDK's public API and implementation. It can change without a wire
14
+ change.
15
+ - **Host package:** identifies the runtime implementation. Publishing it does not deploy it to
16
+ Notion.
17
+
18
+ `connect.bridgeProtocolVersion` selects the wire contract. `sdkVersion` is analytics metadata.
19
+ `LATEST_BRIDGE_PROTOCOL_VERSION` identifies the latest contract defined by the package.
20
+
21
+ ## Protocol export names
22
+
23
+ The `@notionhq/custom-blocks-host/protocol` entrypoint exports wire definitions. Page formats used
24
+ by protocols 3–5 are named `NotionPageForProtocol3To5` and `NotionDataSourcePageForProtocol3To5`,
25
+ with matching `notionPageForProtocol3To5Schema` and `notionDataSourcePageForProtocol3To5Schema`
26
+ validators. Message types and schemas containing `WithProtocol3To5Compatibility` accept both current
27
+ page formats and protocol 3–5 page formats. `UNIFIED_PAGE_PROTOCOL_VERSION` is 6, the minimum
28
+ protocol version that uses unified pages.
29
+
30
+ ## Version changes
31
+
32
+ A breaking wire change increases the bridge protocol version. This includes changed meanings,
33
+ required fields, and new response variants an older receiver rejects. Optional fields and new
34
+ messages need no bump only when older receivers can safely ignore or NACK them. Parsing success
35
+ alone does not prove compatible SDK behavior.
36
+
37
+ An SDK that requires a newer protocol needs a host that supports that contract. Updating the SDK
38
+ package does not update the host in the embedding application.
@@ -0,0 +1,68 @@
1
+ # Host compatibility
2
+
3
+ A host can update while deployed blocks keep older SDKs. The runtime accepts supported protocol
4
+ versions and converts current handler results to each block's declared contract. This reference
5
+ covers version admission, normalization, and the formats sent to older blocks. Protocol version
6
+ rules and release history are in the [versioning guide](bridge/versioning.md) and
7
+ [changelog](bridge/changelog.md).
8
+
9
+ ## Supported versions
10
+
11
+ The protocol subpath exports `LATEST_BRIDGE_PROTOCOL_VERSION` and
12
+ `MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION`. These constants define the package's latest known
13
+ protocol and runtime minimum. Embedders can raise the minimum but cannot enable retired versions.
14
+ The host admits future positive integer versions and validates them against known schemas. Admission
15
+ does not guarantee that future features work.
16
+
17
+ `sdkVersion` is analytics metadata. Compatibility behavior depends on the protocol version.
18
+
19
+ The current SDK declares protocol 8. The shared runtime minimum is 3. The dev shell uses the Worker
20
+ manifest in host mode and accepts the shared runtime's supported protocol versions. Protocols 7 and
21
+ 8 use the same page and query response formats. Protocol 8's local manifest declaration is selected
22
+ only in sandbox mode; host mode uses the persisted manifest. See the
23
+ [protocol 8 release entry](bridge/changelog.md#protocol-8-local-manifest-declarations).
24
+
25
+ The `minBridgeProtocolVersion` option defaults to the runtime minimum. Raising the option rejects
26
+ older SDKs for this embedding application. Lowering it cannot restore retired versions. Raising
27
+ `MIN_HOST_SUPPORTED_BRIDGE_PROTOCOL_VERSION` retires those versions across the runtime.
28
+
29
+ ## Host normalization
30
+
31
+ The host validates sandbox requests before dispatching them to handlers. Handlers use current data
32
+ shapes, and the host converts responses to the sandbox's declared protocol. Setters, query
33
+ refreshes, and handler responses use the same conversion. Unchanged messages and errors pass
34
+ through.
35
+
36
+ ## Current compatibility behavior
37
+
38
+ | Target | Query results | Page operation results | Binding schemas |
39
+ | ------ | --------------------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------ |
40
+ | 3–4 | Flat ID-keyed values, including synthetic metadata entries | ID-keyed properties with unsupported property objects omitted | Synthetic metadata read schemas restored in outgoing copies |
41
+ | 5 | Same query values as protocols 3–4 | Same page format as protocols 3–4 | Actual database schemas. The SDK supplies synthetic read schemas locally |
42
+ | 6 | Unified name-keyed properties. Text fallback property objects become `null` | Same unified page format | Actual database schemas |
43
+ | 7+ | Unified pages with typed text fallback properties | Same unified page format | Actual database schemas |
44
+
45
+ The synthetic metadata IDs are `created_time`, `last_edited_time`, `created_by`, and
46
+ `last_edited_by`. They retain metadata meaning if an actual property uses the same raw ID. This
47
+ matches the protocol 3–4 binding schema precedence. Host validation always uses the unmodified
48
+ actual database schema.
49
+
50
+ Release-specific compatibility limits are documented in the changelog entries for
51
+ [protocol 5 page context](bridge/changelog.md#protocol-5-unavailable-containing-page-context)
52
+ and
53
+ [protocol 6 text fallback properties](bridge/changelog.md#protocol-6-richer-fallback-properties).
54
+
55
+ ### Unavailable containing-page context
56
+
57
+ Protocols 3–4 receive `context_unavailable` during initialization or stop when containing-page
58
+ context becomes unavailable. This concerns `init.page` and `pageChanged.page`, not a page
59
+ operation's `page_unavailable` error.
60
+
61
+ ### Protocol 3–5 metadata query semantics
62
+
63
+ Page creation and edit metadata is readable without corresponding database columns. Current page
64
+ objects carry these fields at the top level. Protocol 3–5 query adapters copy them into
65
+ `propertiesById` for older SDKs.
66
+
67
+ Filters and sorts require actual database properties in the bound schema. The host does not support
68
+ queries against synthetic metadata IDs without corresponding database properties.
@@ -0,0 +1,199 @@
1
+ # Host handlers and data
2
+
3
+ Handlers connect block requests to your application's data access, persistence, and navigation. The
4
+ runtime validates bridge messages and manages subscriptions. Your application enforces viewer
5
+ permissions and implements the requested operations. This guide covers handler contracts, a
6
+ data-source example, refreshes, and external navigation.
7
+
8
+ The [dev shell](https://www.npmjs.com/package/@notionhq/custom-blocks-dev-shell) is a working host
9
+ implementation with local sample data. Use it as a reference for a larger integration. Production
10
+ hosts must provide their own permissions, data access, and persistence.
11
+
12
+ ## Handler contracts
13
+
14
+ `queryDataSource` is required. Other handlers are optional. Omitting a one-shot request handler
15
+ sends no response to that request. A handler returns a result synchronously or as a promise. Errors
16
+ carry `{ code, message, isRetryable }`, with open string codes.
17
+
18
+ Handler requests are validated messages with branded IDs. Initial state, setters, handler results,
19
+ and `*MessageInput` types accept plain string IDs. The runtime validates these inputs and applies
20
+ schema defaults. ID brands do not check record existence or access.
21
+
22
+ Query filters and sorts are validated against the actual bound database schema before
23
+ `queryDataSource` runs. See the [wire reference](bridge/protocol.md#data-sources) for
24
+ query semantics.
25
+
26
+ ## Connect a data source
27
+
28
+ Connect an embedded block to an in-memory Tasks data source with one numeric Estimate property. This
29
+ walkthrough extends the [README's embed example](../README.md#create-a-host): declare the data
30
+ source, bind its keys to IDs, return a query result, and refresh that result after a value changes.
31
+
32
+ Keep the iframe, context, and lifecycle callbacks from the embed example. The block must declare the
33
+ same manifest and query `tasks` through the SDK's public APIs.
34
+
35
+ ### Declare and bind the data source
36
+
37
+ The manifest uses block-defined keys. Bindings connect those keys to actual data-source and property
38
+ IDs. Add these definitions before creating the host:
39
+
40
+ ```ts
41
+ import type {
42
+ CustomBlockHostDataSourcesPayload,
43
+ CustomBlockHostHandlers,
44
+ } from "@notionhq/custom-blocks-host"
45
+ import type { CustomBlockManifest, NotionPageData } from "@notionhq/custom-blocks-host/protocol"
46
+
47
+ const tasksId = "44444444-4444-4444-8444-444444444444"
48
+
49
+ const manifest: CustomBlockManifest = {
50
+ version: 1,
51
+ dataSources: {
52
+ tasks: {
53
+ name: "Tasks",
54
+ properties: {
55
+ estimate: { name: "Estimate", type: "number" },
56
+ },
57
+ },
58
+ },
59
+ }
60
+
61
+ const taskBindings: CustomBlockHostDataSourcesPayload = {
62
+ bindings: {
63
+ tasks: {
64
+ collectionPointer: { table: "collection", id: tasksId },
65
+ collectionSchema: {
66
+ propertiesById: {
67
+ "estimate-id": { name: "Estimate", type: "number" },
68
+ },
69
+ },
70
+ propertyIdsByKey: { estimate: "estimate-id" },
71
+ },
72
+ },
73
+ }
74
+ ```
75
+
76
+ In the embed example, return `{ status: "success", manifest }` from `getPersistedManifest` and
77
+ replace `initialState.dataSources` with `taskBindings`. Here, `tasks` and `estimate` are semantic
78
+ keys; `tasksId` and `estimate-id` identify the bound data source and property.
79
+
80
+ ### Return query results
81
+
82
+ Add this handler and use it as the embed example's `handlers.queryDataSource`. Keep its existing
83
+ `resize` handler.
84
+
85
+ ```ts
86
+ let estimate = 3
87
+
88
+ const handlers: CustomBlockHostHandlers = {
89
+ queryDataSource: message => {
90
+ if (message.dataSourceId !== tasksId) {
91
+ return {
92
+ status: "error",
93
+ error: {
94
+ code: "unmapped_data_source",
95
+ message: "This data source is not available.",
96
+ isRetryable: false,
97
+ },
98
+ }
99
+ }
100
+
101
+ if (message.filter || message.sorts?.length) {
102
+ return {
103
+ status: "error",
104
+ error: {
105
+ code: "invalid_data_source_query",
106
+ message: "This demo supports only unfiltered, unsorted queries.",
107
+ isRetryable: false,
108
+ },
109
+ }
110
+ }
111
+
112
+ const items: NotionPageData[] = [
113
+ {
114
+ object: "page",
115
+ id: "55555555-5555-4555-8555-555555555555",
116
+ parent: { type: "data_source_id", data_source_id: tasksId },
117
+ is_archived: false,
118
+ in_trash: false,
119
+ properties: {
120
+ Estimate: { id: "estimate-id", type: "number", number: estimate },
121
+ },
122
+ },
123
+ ]
124
+
125
+ return {
126
+ status: "success",
127
+ items: items.slice(0, message.limit),
128
+ hasMore: items.length > message.limit,
129
+ }
130
+ },
131
+ }
132
+ ```
133
+
134
+ The SDK resolves `tasks` to `tasksId` before the host receives the request. Result properties use
135
+ display names as keys (`Estimate`) and include their raw property IDs (`estimate-id`). The runtime
136
+ handles subscription IDs, validates results, and converts them for supported older SDKs.
137
+
138
+ This demo rejects filters and sorts rather than silently ignoring them. A production adapter must
139
+ enforce record access, execute the requested filter and sorts, and honor the result limit. See
140
+ [query semantics](bridge/protocol.md#data-sources).
141
+
142
+ ## Refresh active queries
143
+
144
+ After creating the host, update the in-memory value and notify active subscriptions:
145
+
146
+ ```ts
147
+ estimate = 5
148
+ host.refreshQuery({ dataSourceId: tasksId })
149
+ ```
150
+
151
+ If the block has an active query for Tasks, its next result contains an estimate of `5`. A query
152
+ started later also reads the updated value. The application triggers refreshes when its underlying
153
+ data changes; the runtime re-runs matching subscriptions. Call `host.stop()` when removing the
154
+ embed.
155
+
156
+ `refreshQuery()` re-runs active subscriptions for the supplied data-source ID with their existing
157
+ filter, sorts, and limit. Overlapping refreshes queue one follow-up query. The running query
158
+ delivers its result first. Unsubscribe, replacement, and stop discard queued work and suppress late
159
+ results. Non-retryable query errors end subscriptions. Retryable errors retain them.
160
+
161
+ ## Page and user operations
162
+
163
+ `createPage`, `getPage`, and `updatePage` connect page requests to your application's data access
164
+ and persistence. `getUser` and `listUsers` supply user data. Each handler returns a success payload
165
+ or an error. The runtime adds the request's correlation ID and sends the response. The application
166
+ enforces record access before reading or writing data.
167
+
168
+ `openPage` handles navigation to a Notion page. `resize` receives the block's requested height in
169
+ CSS pixels. The application owns navigation and iframe sizing.
170
+
171
+ ## External navigation
172
+
173
+ `openExternalUrl` receives an `AbortSignal` and an absolute HTTP or HTTPS URL without credentials.
174
+ The URL is limited to 2,048 JavaScript code units before and after normalization. The adapter
175
+ displays the exact URL, obtains approval, and navigates only after approval. It may apply further
176
+ restrictions. Dismiss pending confirmation UI when the signal aborts.
177
+
178
+ Each iframe permits one pending navigation. A different request receives `pending_navigation`. A
179
+ duplicate of the active request is ignored. Completed requests are forgotten, so later requests
180
+ require fresh approval. The runtime redacts the URL from `onLog` payloads and suppresses results
181
+ after stop.
182
+
183
+ ## Property binding
184
+
185
+ `getAutoBoundPropertyIdsByKey({ manifestProperties, propertySchemasById })` requires matching
186
+ property types and either an exact raw ID match or a display-name match after trimming and
187
+ lowercasing. IDs are not trimmed or case-folded. Exactly one candidate creates a binding. Zero or
188
+ multiple candidates leave the property unmapped. The result maps manifest property keys to database
189
+ property IDs. An ID match has no priority over a name match: if they identify different compatible
190
+ properties, the manifest property stays unmapped.
191
+
192
+ ## Date conversion and types
193
+
194
+ `toNotionDateValue()` converts `NotionDateInput` into the four `CustomBlockDateValue` variants,
195
+ including reminders. Invalid input throws `InvalidNotionDateError`.
196
+
197
+ The root exports host configuration, handlers, results, and `*MessageInput` types. Wire schemas,
198
+ message and page types, and constants such as `LATEST_BRIDGE_PROTOCOL_VERSION` use the
199
+ [protocol subpath](../README.md#protocol-schemas-and-types).
@@ -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.8",
3
+ "version": "0.4.0",
4
4
  "description": "Reusable host runtime for Notion custom blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,14 +14,22 @@
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": {
20
- "valibot": "^1.4.2"
25
+ "valibot": "^1.5.0"
21
26
  },
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>>