@notionhq/custom-blocks-host 0.2.20 → 0.3.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 (35) hide show
  1. package/README.md +170 -239
  2. package/dist/browser.js +1 -1
  3. package/dist/createCustomBlockHost.d.ts +5 -5
  4. package/dist/createCustomBlockHost.d.ts.map +1 -1
  5. package/dist/createCustomBlockHost.js +54 -15
  6. package/dist/ids.d.ts +3 -0
  7. package/dist/ids.d.ts.map +1 -0
  8. package/dist/ids.js +5 -0
  9. package/dist/index.d.ts +8 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -0
  12. package/dist/lifecycle/loadCustomBlockManifest.d.ts +16 -0
  13. package/dist/lifecycle/loadCustomBlockManifest.d.ts.map +1 -0
  14. package/dist/lifecycle/loadCustomBlockManifest.js +65 -0
  15. package/dist/lifecycle/manifestOptions.d.ts +17 -0
  16. package/dist/lifecycle/manifestOptions.d.ts.map +1 -0
  17. package/dist/lifecycle/manifestOptions.js +1 -0
  18. package/dist/lifecycle/types.d.ts +14 -2
  19. package/dist/lifecycle/types.d.ts.map +1 -1
  20. package/dist/lifecycle/types.js +1 -1
  21. package/dist/protocol/dataSources/dataSourcePage.d.ts +160 -1
  22. package/dist/protocol/dataSources/dataSourcePage.js +8 -1
  23. package/dist/protocol/dataSources/propertySchema.d.ts +1 -0
  24. package/dist/protocol/messages/hostToSandbox.d.ts +159 -2
  25. package/dist/protocol/messages/queryDataSourceResult.d.ts +159 -2
  26. package/dist/protocol/messages/queryDataSourceResult.js +5 -2
  27. package/dist/protocol/protocolVersion.d.ts +2 -1
  28. package/dist/protocol/protocolVersion.js +4 -1
  29. package/package.json +1 -1
  30. package/src/createCustomBlockHost.ts +64 -23
  31. package/src/ids.ts +9 -0
  32. package/src/index.ts +12 -1
  33. package/src/lifecycle/loadCustomBlockManifest.ts +89 -0
  34. package/src/lifecycle/manifestOptions.ts +19 -0
  35. package/src/lifecycle/types.ts +17 -5
package/README.md CHANGED
@@ -1,51 +1,17 @@
1
1
  # @notionhq/custom-blocks-host
2
2
 
3
- Host runtime for the custom block bridge. This package owns the generic `postMessage` protocol loop
4
- used by hosts such as the local dev shell. It does not own iframe policy, permissions, persistence,
5
- analytics, or host UI state. Host implementations such as the dev shell and `notion-next` depend on
6
- this package.
3
+ The shared host connects an embedding application to a custom block sandbox. It loads the manifest,
4
+ prepares initialization, validates messages, routes requests, manages query subscriptions, and
5
+ cleans up the connection.
7
6
 
8
- ## Standalone browser module
9
-
10
- The published package includes `dist/browser.js`, a single ES module containing the host runtime
11
- and all dependencies. Load this file directly when a browser
12
- must not fetch additional modules, such as under an exact-URL Content Security Policy.
13
-
14
- ## `getAutoBoundPropertyIdsByKey()`
15
-
16
- `getAutoBoundPropertyIdsByKey()` matches manifest properties to schema properties for initial
17
- property binding. For each manifest property:
18
-
19
- 1. Consider only schema properties of the same type.
20
- 2. A schema property is a candidate if its ID equals the manifest key (exact string: no trim, no
21
- case folding) or its display name equals the manifest display name after `trim()` and
22
- lowercasing.
23
- 3. Bind when there is exactly one candidate. Otherwise leave it unmapped.
24
-
25
- Wrong-type properties never enter the candidate set.
26
-
27
- ```ts
28
- import { getAutoBoundPropertyIdsByKey } from "@notionhq/custom-blocks-host"
29
-
30
- const propertyIdsByKey = getAutoBoundPropertyIdsByKey({
31
- manifestProperties: {
32
- title: { name: "Task", type: "title" },
33
- estimate: { name: "Estimate", type: "number" },
34
- },
35
- propertySchemasById: {
36
- title: { name: "Different label", type: "title" },
37
- "property-1": { name: " estimate ", type: "number" },
38
- },
39
- })
40
-
41
- // { title: "title", estimate: "property-1" }
42
- ```
7
+ The embedding application creates the iframe and supplies permissions, record access, bindings,
8
+ persistence, analytics, and UI. Both Notion and the dev shell use this package.
43
9
 
44
10
  ## `createCustomBlockHost()`
45
11
 
46
- `createCustomBlockHost()` attaches a message listener, performs the `connect` / `init` /
47
- `initResult` handshake, validates sandbox messages, routes requests to handlers, manages query
48
- subscriptions, and emits live state updates.
12
+ Create a host before navigating its iframe to the block URL. Set `sandboxOrigin` to that URL's exact
13
+ origin. The host accepts messages only from that iframe and origin, and targets outbound messages to
14
+ the same origin.
49
15
 
50
16
  ```ts
51
17
  import { createCustomBlockHost } from "@notionhq/custom-blocks-host"
@@ -53,8 +19,9 @@ import { createCustomBlockHost } from "@notionhq/custom-blocks-host"
53
19
  const host = createCustomBlockHost({
54
20
  iframe,
55
21
  sandboxOrigin: "https://example.com",
56
- minBridgeProtocolVersion: 3,
57
- getInitialState: async () => ({
22
+ manifestSource: "persisted",
23
+ getPersistedManifest: async ({ signal }) => loadSavedManifest(signal),
24
+ prepareInitialState: async ({ manifest, signal }) => ({
58
25
  status: "success",
59
26
  initialState: {
60
27
  theme,
@@ -63,8 +30,9 @@ const host = createCustomBlockHost({
63
30
  parent,
64
31
  page,
65
32
  currentUser,
66
- manifest,
67
- dataSources: { bindings },
33
+ dataSources: {
34
+ bindings: await loadBindings(manifest, signal),
35
+ },
68
36
  },
69
37
  }),
70
38
  handlers: {
@@ -73,232 +41,195 @@ const host = createCustomBlockHost({
73
41
  items: await queryRows(message),
74
42
  hasMore: false,
75
43
  }),
76
- createPage: async message => createPage(message),
77
- getPage: async message => getPage(message),
78
- openPage: async message => openPage(message),
79
- updatePage: async message => updatePage(message),
80
- getUser: async message => getUser(message),
81
- listUsers: async message => listUsers(message),
82
44
  resize: message => resizeIframe(message.height),
83
45
  },
84
- onManifest: manifest => {
85
- console.log("Selected manifest", manifest)
86
- },
87
- onInitialized: state => {
88
- console.log("Initialized manifest", state.manifest)
89
- },
90
- onConnectionFailure: error => {
91
- showHostError(error)
92
- },
46
+ onInitialized: state => showBlock(state),
47
+ onConnectionFailure: error => showHostError(error),
93
48
  onQueryUnsubscribed: undefined,
94
- onLog: (direction, payload) => {
95
- console.log(direction, payload)
96
- },
49
+ onLog: (direction, payload) => console.log(direction, payload),
97
50
  })
98
51
  ```
99
52
 
100
- `minBridgeProtocolVersion` is optional and defaults to the oldest protocol version the host runtime
101
- supports. Set a higher value to retire older SDKs. Lower values cannot enable versions the runtime
102
- no longer supports. The host does not enforce a maximum protocol version.
103
-
104
- Each host handles one initialization handshake with one sandbox iframe:
105
-
106
- - Create the host before navigating the iframe to the sandbox URL.
107
- - The host accepts the first `connect` attempt and ignores subsequent `connect` messages, including
108
- malformed ones. A connection after an initialization timeout does not restart it.
109
- - The first observed iframe `load` is allowed, including when initialization completes before it.
110
- - A second observed `load` stops the host and reports retryable `iframe_reloaded` through
111
- `onConnectionFailure`. A reload or navigation can replace the sandbox document, so the host ends
112
- the connection instead of continuing to serve the replacement.
113
- - Stopping removes message and load listeners, clears initialization timers, ends query
114
- subscriptions, and prevents further outbound messages, including pending responses.
115
- - To retry or apply a deployment or configuration change, the embedder creates a new host and
116
- iframe. The host does not restart automatically.
117
-
118
- The stop takes effect at the second observed load. It does not prevent navigation or isolate the
119
- original document before that event: a replacement document at the same origin can receive messages
120
- and invoke handlers until the host stops.
121
-
122
- SDKs using protocol 5 or later support unavailable page context. For older SDKs, an unavailable
123
- initial page fails initialization with `context_unavailable`. If page context becomes unavailable
124
- after initialization, the host stops and reports the same error instead of sending an unsupported
125
- `pageChanged` message.
126
-
127
- `getInitialState` runs after the sandbox sends `connect`. It returns a promise for the initial state
128
- result. The host listens immediately and sends `init` only after `getInitialState` resolves. Await
129
- record and binding loads inside `getInitialState`.
130
-
131
- `getInitialState` selects the manifest that the host uses. The host uses this manifest even when
132
- `connect` supplies another. Production hosts can select persisted definitions. Preview hosts can
133
- select sandbox manifests. A successful result must include a manifest. If no manifest is available,
134
- return `manifest_unavailable`.
135
-
136
- `getInitialState` can return `{ status: "error", error }` to stop initialization. The host converts
137
- a thrown error or rejected promise to `unknown_error`. If preparation exceeds
138
- `initialStateTimeoutMs`, the host reports `initial_state_timeout` and stops the handshake.
139
-
140
- `no_connect` means the host did not receive `connect`. `no_init_result` means the sandbox did not
141
- acknowledge `init`. The SDK waits for the host without its own timeout.
142
-
143
- The host discards `getInitialState` results after a timeout or stop, including a stop caused by an
144
- iframe reload.
145
-
146
- - `onManifest` provides the selected manifest when `getInitialState` resolves successfully, before
147
- binding validation and `onInitialized`. Use it to configure bindings even when initialization
148
- cannot yet succeed.
149
-
150
- - `onInitialized` reports success after the sandbox accepts initialization. It provides the selected
151
- `manifest` and the sandbox's optional `initialHeight`.
152
- - `onConnectionFailure` reports a handshake failure, timeout, unavailable page context, or iframe
153
- reload. It receives `{ phase, source, code, message, isRetryable }`. The phase identifies
154
- `connect`, `init`, `initResult`, or `runtime`; the source identifies whether the host detected the
155
- failure or the sandbox reported it. It can run after initialization succeeds.
156
-
157
- - `onQueryUnsubscribed` reports when a query subscription ends. Use it to release per-query
158
- resources, such as listeners or timers. The host removes the subscription itself. A query failure
159
- can end one subscription while the connection remains active.
160
-
161
- `queryDataSource` is required. Other request handlers are optional. Every request handler may return
162
- its result synchronously or as a promise. Results use a `status` discriminator: successful results
163
- carry their response payload, while errors carry `{ code, message, isRetryable }`.
53
+ ### Manifest and initial state
164
54
 
165
- ### Request types
55
+ The host starts loading the manifest when it is created. `manifestSource` determines where the
56
+ manifest comes from:
166
57
 
167
- TypeScript infers request types for handlers passed to `createCustomBlockHost()`. For separately
168
- defined handlers and helpers, import the corresponding request and page-property types directly from
169
- `@notionhq/custom-blocks-host`.
58
+ - `"persisted"` requires `getPersistedManifest({ signal })`. The callback supplies a saved block
59
+ definition or a dev-shell worker registry manifest.
60
+ - `"local"` fetches `/manifest` from `sandboxOrigin`. The URL must pass
61
+ [`isLocalCustomBlockUrl()`](#local-block-urls) and differ from the embedding page's origin.
62
+ Requests omit credentials, reject redirects, bypass caches, and time out after five seconds. This
63
+ source has no callback or persisted fallback. Local refers to the block server, not the host's
64
+ environment.
170
65
 
171
- Adapters can pass plain string IDs in initial state, live-state setters, and handler results such as
172
- `CustomBlockHostGetPageResult`. The host validates these values before using or sending them.
173
- Parsing assigns protocol ID brands for internal type checking and applies schema defaults, such as
174
- an empty manifest data source `properties` map. Brands distinguish ID kinds in TypeScript. They do
175
- not check record existence or access.
66
+ Hosts and SDKs use one [manifest schema](../protocol/README.md#manifests), including optional block
67
+ metadata. The host validates and normalizes the manifest, then calls
68
+ `prepareInitialState({ manifest, signal })` once. The embedding application uses that manifest to
69
+ prepare context and bindings, and returns them in `initialState`. The host includes the manifest
70
+ with that state in `init`.
176
71
 
177
- Request types such as `GetPageMessage` describe validated messages that handlers receive. The
178
- corresponding `*MessageInput` types accept plain string IDs for adapters and test fixtures that
179
- construct requests. These types do not validate values at runtime. The host runtime validates
180
- incoming messages before it calls handlers. Your implementation remains responsible for permissions
181
- and record access.
72
+ Manifest loading and state preparation do not wait for `connect`. The host listens concurrently and
73
+ sends `init` after both preparation and connection succeed. If preparation fails before `connect`,
74
+ the host retains the failure and sends `init.error` when the sandbox connects.
182
75
 
183
- ### Query requests
76
+ Manifest failures produce `manifest_unavailable`. Exceptions during state preparation produce
77
+ `unknown_error`. `onLog("warn", payload)` receives diagnostic details. User-facing manifest errors
78
+ remain generic. Stopping the host cancels preparation without logging a cancellation warning.
184
79
 
185
- The `queryDataSource` handler receives raw data source and property IDs. The SDK resolves semantic
186
- keys before it sends a request. The runtime requires every filter and sort property ID to exist in
187
- `collectionSchema.propertiesById`. This map must contain only actual database properties. The SDK
188
- adds synthetic metadata to its separate read map, `propertySchemasById`.
80
+ The embedding application can load saved definitions separately during local preview, for example to
81
+ restore saved bindings after preview ends. Local initialization does not wait for those reads.
189
82
 
190
- For SDKs using protocol 3 or 4, the runtime adds synthetic metadata schemas to outgoing
191
- `collectionSchema` payloads during initialization and binding updates. Query validation still uses
192
- the actual database schema supplied by the host. Protocol 5 SDKs receive that schema directly and
193
- add the metadata read schemas themselves.
83
+ ### Connection lifecycle
194
84
 
195
- The runtime validates option names, Status group names, supported sort types, and ten unique sort
196
- properties before it invokes the handler. It checks group names against the current bound schema.
197
- Sorts retain their array order. Extra fields and mixed filter types fail protocol validation.
85
+ Each host handles one handshake with one iframe:
198
86
 
199
- Derive bindings from the successful `connect` message inside `getInitialState` and return them in
200
- `initialState.dataSources`.
87
+ ```text
88
+ sandbox connect → host init → sandbox initResult
89
+ ```
201
90
 
202
- Set `sandboxOrigin` to the iframe's exact origin. The runtime targets outbound messages to that
203
- origin and accepts inbound messages only from that iframe and origin. The runtime accepts
204
- `noConnectTimeoutMs`, `initialStateTimeoutMs`, and `noInitResultTimeoutMs`. All default to five
205
- seconds.
91
+ The host accepts the first `connect` attempt. Later attempts do not restart initialization. Live
92
+ updates wait for a matching `initResult.success`.
206
93
 
207
- ### Initialization and malformed messages
94
+ The first observed iframe `load` is allowed. A second load stops the host and reports retryable
95
+ `iframe_reloaded`. Create a new host and iframe to retry initialization or replace the block URL.
208
96
 
209
- `createCustomBlockHost()` performs one handshake for each host and iframe pair:
97
+ Stopping removes listeners, cancels preparation, clears timers, ends query subscriptions, and
98
+ prevents further outbound messages, including pending responses. Call `stop()` when the iframe is
99
+ removed.
210
100
 
211
- ```text
212
- sandbox connect → host init → sandbox initResult
213
- ```
101
+ Load-event cleanup does not isolate documents before that event. A replacement document at the same
102
+ origin can receive messages and invoke handlers until the host stops.
214
103
 
215
- The sandbox creates the `initializationId`. The host copies that ID into `init`. The sandbox copies
216
- it into `initResult`. The host sends live state changes only after it receives a matching
217
- `initResult.success`.
104
+ All handshake timeouts default to five seconds:
218
105
 
219
- The host must check that the `initializationId` in `initResult` exactly matches the ID in the `init`
220
- message. The host ignores a result with a different ID. If no result arrives, or only a stale valid
221
- result arrives, before the timeout, initialization ends with `no_init_result`.
106
+ | Option | Waits for | Failure code |
107
+ | ----------------------- | --------------------------------------- | ----------------------- |
108
+ | `noConnectTimeoutMs` | `connect` after the iframe's first load | `no_connect` |
109
+ | `initialStateTimeoutMs` | State preparation after `connect` | `initial_state_timeout` |
110
+ | `noInitResultTimeoutMs` | `initResult` after `init` | `no_init_result` |
222
111
 
223
- The host validates each inbound sandbox message with `sandboxToHostMessageSchema` before it handles
224
- the message. If parsing fails, the host uses `readIncomingType()` to identify the attempted message.
225
- It replies with `invalidSandboxMessage`, a negative acknowledgment (NACK). The host never sends a
226
- NACK in response to a NACK.
112
+ The local manifest fetch has its own five-second timeout, starting when the host is created. The SDK
113
+ waits for `init` without its own timeout. Handshake failures abort preparation and suppress late
114
+ results. Asynchronous callbacks must respect their `signal`.
227
115
 
228
- Malformed sandbox messages do not change the host's initialization state or timeout, except in these
229
- two cases:
116
+ `minBridgeProtocolVersion` defaults to the runtime's minimum supported version. A higher value
117
+ rejects SDKs below that minimum. A lower value cannot enable unsupported versions. The host does not
118
+ enforce a maximum protocol version.
230
119
 
231
- - The host expects `connect` and receives a malformed `connect` with a readable `initializationId`.
232
- The host sends `init.error` instead of a NACK, then fails initialization. An invalid or
233
- unsupported numeric protocol version takes precedence over `invalid_connect_payload`.
234
- - The host expects `initResult` and receives a malformed `initResult` with the active
235
- `initializationId`. The host sends a NACK and fails initialization with
236
- `invalid_init_result_payload`. This error code describes a local host failure. The host does not
237
- send it in `initResult.error`.
120
+ Unavailable page context requires protocol 5. With protocols 3 and 4, an unavailable page produces
121
+ `context_unavailable` during initialization or ends an active connection.
238
122
 
239
- A malformed `initResult` with a missing or stale ID does not end initialization or reset its
240
- timeout. Malformed messages do not restart initialization after it succeeds or fails.
123
+ ### Callbacks
241
124
 
242
- The host accepts a valid `initResult.error` as a terminal initialization failure. It logs and
243
- ignores a valid `initResult` with a stale `initializationId` or an unexpected state.
125
+ - `onInitialized({ manifest, initialHeight })` runs after the sandbox accepts initialization. The
126
+ height is optional.
127
+ - `onConnectionFailure(error)` reports a handshake failure, timeout, unavailable page context, or
128
+ iframe reload. The error contains `{ phase, source, code, message, isRetryable }`. `phase` is
129
+ `connect`, `init`, `initResult`, or `runtime`. `source` identifies the host or sandbox that
130
+ reported the failure. This callback can run after initialization succeeds.
131
+ - `onQueryUnsubscribed(subscriptionId)` releases application resources when a subscription ends. The
132
+ host removes the subscription itself. The callback runs for unsubscribe, replacement, terminal
133
+ error, or host stop, once per ended subscription.
134
+ - `onLog(direction, payload)` receives bridge messages and warnings. The embedding application
135
+ chooses where to record them.
136
+
137
+ ### Request types
244
138
 
245
- The host does not retry malformed messages.
139
+ `queryDataSource` is required. Other request handlers are optional. Handlers return a result
140
+ synchronously or as a promise. Successful results carry their response payload. Errors carry
141
+ `{ code, message, isRetryable }`; error codes are open strings.
142
+
143
+ TypeScript infers handler request types. Separately defined handlers can import request and
144
+ page-property types from `@notionhq/custom-blocks-host`.
145
+
146
+ Request types such as `GetPageMessage` describe validated messages. Corresponding `*MessageInput`
147
+ types accept plain string IDs for callers that construct requests. Initial state, live setters, and
148
+ handler results also accept plain string IDs. The runtime validates these values and applies schema
149
+ defaults. ID brands distinguish ID kinds in TypeScript; they do not check record existence or
150
+ access. The embedding application enforces permissions and record access.
151
+
152
+ ### Query requests
153
+
154
+ The SDK resolves semantic data-source and property keys to raw IDs before sending requests. The host
155
+ validates filters and sorts against actual database properties in `collectionSchema.propertiesById`
156
+ before invoking `queryDataSource`. Status group names and option names must match the bound schema.
157
+ Sorts retain their order and contain at most ten unique properties.
158
+
159
+ The SDK exposes synthetic metadata through its separate `propertySchemasById` read map. For
160
+ protocols 3 and 4, the host adds synthetic metadata to outgoing `collectionSchema` payloads.
161
+ Protocol 5 SDKs add those read schemas themselves. Host query validation always uses the actual
162
+ database schema.
163
+
164
+ ### Initialization and malformed messages
165
+
166
+ The host validates inbound messages, correlates handshake IDs, and rejects malformed requests.
167
+ Malformed messages do not restart initialization. The host logs and ignores stale valid `initResult`
168
+ messages.
169
+
170
+ See the protocol's [initialization contract](../protocol/README.md#initialization) and
171
+ [validation and error rules](../protocol/README.md#validation-and-errors) for message shapes and
172
+ failure behavior.
246
173
 
247
174
  ## Updating live state
248
175
 
249
- The returned handle exposes:
250
-
251
- - `setTheme()`
252
- - `setContrastMode()`
253
- - `setParent()`
254
- - `setPage()`
255
- - `setDataSources()`
256
- - `setCurrentUser()`
257
- - `refreshQuery()`
258
- - `stop()`
259
-
260
- State updates are sent after `initResult.success`. Updates made earlier are held until
261
- initialization completes. `refreshQuery()` re-runs `queryDataSource` for every active subscription
262
- with the supplied data source ID, preserving each subscription's filter, sorts, and limit. If that
263
- query is already running, refresh requests coalesce into one follow-up query. The running query
264
- delivers its snapshot before the follow-up starts, so continuous changes do not starve updates.
265
- Unsubscribing, replacing the query, or stopping the host discards its queued refresh.
266
-
267
- After `stop()`, the host drops outbound messages, including pending request responses. The runtime
268
- removes a subscription when it receives `unsubscribeDataSourceQuery`. Repeated unsubscribe messages
269
- are safe. The runtime also ignores results from a query that was unsubscribed while its handler was
270
- running.
271
-
272
- A non-retryable query error ends the subscription and discards any queued refresh. This includes
273
- validation errors before the query handler runs. Retryable errors retain the subscription.
274
-
275
- Use `onQueryUnsubscribed(subscriptionId)` to release adapter resources such as collection listeners.
276
- The callback runs when a subscription ends through an explicit unsubscribe, replacement, terminal
277
- error, or host stop. Repeated removal of the same subscription does not repeat the callback. A late
278
- result from a replaced query cannot remove its replacement. The sandbox still receives the terminal
279
- error result.
280
-
281
- Call `stop()` when the iframe is removed. It detaches listeners, clears timeouts, and discards
282
- active query subscriptions.
176
+ The returned handle exposes `setTheme()`, `setContrastMode()`, `setParent()`, `setPage()`,
177
+ `setDataSources()`, `setCurrentUser()`, `refreshQuery()`, and `stop()`.
283
178
 
284
- ## Wire protocol
179
+ State updates made during initialization are held until `initResult.success`. Updates after that
180
+ point are sent to the sandbox.
181
+
182
+ `refreshQuery()` re-runs `queryDataSource` for each active subscription with the supplied data
183
+ source ID, preserving its filter, sorts, and limit. Overlapping refreshes queue one follow-up query.
184
+ The running query delivers its result before the follow-up starts.
185
+
186
+ Unsubscribe, replacement, and host stop discard queued refreshes and suppress late results.
187
+ Non-retryable query errors also end the subscription. Retryable errors retain it. A failed query can
188
+ end its subscription while the connection stays active.
189
+
190
+ ## Local block URLs
191
+
192
+ `isLocalCustomBlockUrl(url: URL)` accepts HTTP(S) URLs on `localhost`, any `.localhost` subdomain,
193
+ or `127.0.0.1`, without a username or password. Both the shared host and the dev shell use this
194
+ policy. The helper does not enforce origin isolation. Local manifest fetching separately rejects the
195
+ embedding page's own origin.
196
+
197
+ ## `getAutoBoundPropertyIdsByKey()`
198
+
199
+ `getAutoBoundPropertyIdsByKey()` matches manifest properties to database properties for initial
200
+ bindings. A candidate must have the same property type and either:
201
+
202
+ - An ID that exactly equals the manifest key, without trimming or case folding.
203
+ - A display name that equals the manifest display name after trimming and lowercasing.
285
204
 
286
- `createCustomBlockHost()` implements the protocol: it validates inbound messages, runs the
287
- initialization handshake, correlates requests, manages query subscriptions, and emits bridge NACKs.
288
- Use its state-update methods and handlers instead of posting bridge messages directly.
205
+ Exactly one candidate produces a binding. Zero or multiple candidates leave the property unmapped.
289
206
 
290
- The bridge protocol is maintained in this repository's `protocol/` package. The published host
291
- package includes a compiled copy of that protocol and does not expose it as a public subpath.
292
- Consumers should use the host APIs above instead of importing protocol implementation files.
207
+ ```ts
208
+ import { getAutoBoundPropertyIdsByKey } from "@notionhq/custom-blocks-host"
293
209
 
294
- Host source in this repository imports protocol schemas and types from the workspace package. The
295
- publish build rewrites those imports to the bundled copy.
210
+ const propertyIdsByKey = getAutoBoundPropertyIdsByKey({
211
+ manifestProperties: {
212
+ title: { name: "Task", type: "title" },
213
+ estimate: { name: "Estimate", type: "number" },
214
+ },
215
+ propertySchemasById: {
216
+ title: { name: "Different label", type: "title" },
217
+ "property-1": { name: " estimate ", type: "number" },
218
+ },
219
+ })
220
+
221
+ // { title: "title", estimate: "property-1" }
222
+ ```
296
223
 
297
- Handler implementations must preserve message identity: one-shot operations return one result with
298
- the request's `requestId`, while `queryDataSource` is a long-lived subscription keyed by
299
- `subscriptionId`. The runtime handles the `connect` → `init` → `initResult` lifecycle, including
300
- waiting for `initResult.success` before live updates.
224
+ ## Standalone browser module
225
+
226
+ The published package includes `dist/browser.js`, a single ES module with all dependencies. Load
227
+ this file directly when the browser must not fetch additional modules, such as under an exact-URL
228
+ Content Security Policy.
229
+
230
+ ## Wire protocol
301
231
 
302
- Error payloads use `{ code, message, isRetryable }`. The runtime accepts any string for `code`,
303
- including codes that this package does not know yet. Host code should preserve unknown codes and use
304
- `isRetryable` to decide whether a retry is appropriate.
232
+ The host package bundles the shared [protocol](../protocol/README.md) and exposes its public types.
233
+ Use the host APIs instead of posting bridge messages directly or importing protocol implementation
234
+ files. Runtime source imports schemas from the workspace protocol package; the publish build bundles
235
+ that dependency into the host package.