@notionhq/custom-blocks 0.0.77 → 0.0.79

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 (100) hide show
  1. package/HOST.md +68 -34
  2. package/README.md +1 -1
  3. package/bin/src/bridge/manifest.js +3 -3
  4. package/dist/bridge/SandboxBridge.d.ts +8 -13
  5. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  6. package/dist/bridge/SandboxBridge.js +110 -39
  7. package/dist/bridge/dataSources/resolve.d.ts +3 -4
  8. package/dist/bridge/dataSources/resolve.d.ts.map +1 -1
  9. package/dist/bridge/dataSources/resolve.js +2 -12
  10. package/dist/bridge/hostState.d.ts +2 -0
  11. package/dist/bridge/hostState.d.ts.map +1 -1
  12. package/dist/bridge/loadManifest.d.ts +4 -4
  13. package/dist/bridge/loadManifest.d.ts.map +1 -1
  14. package/dist/bridge/loadManifest.js +2 -2
  15. package/dist/bridge/manifest.d.ts +3 -3
  16. package/dist/bridge/manifest.js +3 -3
  17. package/dist/bridge/messages/{ready.d.ts → connect.d.ts} +14 -12
  18. package/dist/bridge/messages/connect.d.ts.map +1 -0
  19. package/dist/bridge/messages/connect.js +38 -0
  20. package/dist/bridge/messages/hostToSandbox.d.ts +21 -0
  21. package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -1
  22. package/dist/bridge/messages/init.d.ts +35 -6
  23. package/dist/bridge/messages/init.d.ts.map +1 -1
  24. package/dist/bridge/messages/init.js +8 -4
  25. package/dist/bridge/messages/initResult.d.ts +29 -0
  26. package/dist/bridge/messages/initResult.d.ts.map +1 -0
  27. package/dist/bridge/messages/initResult.js +25 -0
  28. package/dist/bridge/messages/sandboxToHost.d.ts +21 -6
  29. package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -1
  30. package/dist/bridge/messages/sandboxToHost.js +4 -2
  31. package/dist/bridge/sandboxClient.d.ts +2 -4
  32. package/dist/bridge/sandboxClient.d.ts.map +1 -1
  33. package/dist/bridge/sandboxClient.js +2 -5
  34. package/dist/customBlock.d.ts +1 -1
  35. package/dist/customBlock.d.ts.map +1 -1
  36. package/dist/customBlock.js +1 -1
  37. package/dist/host/createCustomBlockHost.d.ts +7 -4
  38. package/dist/host/createCustomBlockHost.d.ts.map +1 -1
  39. package/dist/host/createCustomBlockHost.js +215 -38
  40. package/dist/host/lifecycle/initErrors.d.ts +1 -1
  41. package/dist/host/lifecycle/initErrors.d.ts.map +1 -1
  42. package/dist/host/lifecycle/initErrors.js +25 -7
  43. package/dist/host/lifecycle/{ready.d.ts → protocolVersion.d.ts} +1 -1
  44. package/dist/host/lifecycle/protocolVersion.d.ts.map +1 -0
  45. package/dist/host/lifecycle/types.d.ts +6 -4
  46. package/dist/host/lifecycle/types.d.ts.map +1 -1
  47. package/dist/host/messages/invalidSandboxMessage.js +0 -3
  48. package/dist/host.d.ts +5 -3
  49. package/dist/host.d.ts.map +1 -1
  50. package/dist/host.js +2 -1
  51. package/dist/index.d.ts +2 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/init.d.ts +7 -7
  55. package/dist/init.d.ts.map +1 -1
  56. package/dist/init.js +70 -50
  57. package/dist/react/index.d.ts +1 -1
  58. package/dist/react/index.d.ts.map +1 -1
  59. package/dist/react/standalonePreview.d.ts.map +1 -1
  60. package/dist/react/standalonePreview.js +2 -0
  61. package/dist/react/useCustomBlockInit.d.ts +5 -6
  62. package/dist/react/useCustomBlockInit.d.ts.map +1 -1
  63. package/dist/react/useCustomBlockInit.js +3 -19
  64. package/dist/react/useRuntimeState.d.ts +4 -3
  65. package/dist/react/useRuntimeState.d.ts.map +1 -1
  66. package/dist/react/useRuntimeState.js +3 -2
  67. package/dist/version.js +1 -1
  68. package/docs/data-sources.md +10 -8
  69. package/docs/errors.md +5 -1
  70. package/docs/lifecycle.md +19 -31
  71. package/docs/manifest.md +1 -1
  72. package/package.json +1 -1
  73. package/src/bridge/SandboxBridge.ts +138 -44
  74. package/src/bridge/dataSources/resolve.ts +3 -14
  75. package/src/bridge/hostState.ts +2 -0
  76. package/src/bridge/loadManifest.ts +6 -6
  77. package/src/bridge/manifest.ts +3 -3
  78. package/src/bridge/messages/connect.ts +42 -0
  79. package/src/bridge/messages/init.ts +24 -10
  80. package/src/bridge/messages/initResult.ts +37 -0
  81. package/src/bridge/messages/sandboxToHost.ts +4 -2
  82. package/src/bridge/sandboxClient.ts +3 -8
  83. package/src/customBlock.ts +2 -2
  84. package/src/host/createCustomBlockHost.ts +265 -45
  85. package/src/host/lifecycle/initErrors.ts +25 -7
  86. package/src/host/lifecycle/types.ts +13 -4
  87. package/src/host/messages/invalidSandboxMessage.ts +0 -3
  88. package/src/host.ts +13 -2
  89. package/src/index.ts +3 -1
  90. package/src/init.ts +73 -60
  91. package/src/react/index.ts +0 -1
  92. package/src/react/standalonePreview.ts +2 -0
  93. package/src/react/useCustomBlockInit.ts +6 -27
  94. package/src/react/useRuntimeState.ts +4 -3
  95. package/dist/bridge/messages/ready.d.ts.map +0 -1
  96. package/dist/bridge/messages/ready.js +0 -43
  97. package/dist/host/lifecycle/ready.d.ts.map +0 -1
  98. package/src/bridge/messages/ready.ts +0 -48
  99. /package/dist/host/lifecycle/{ready.js → protocolVersion.js} +0 -0
  100. /package/src/host/lifecycle/{ready.ts → protocolVersion.ts} +0 -0
@@ -32,6 +32,7 @@ import {
32
32
  import { readIncomingType } from "./incomingType.js"
33
33
  import type { ManifestLoadResult } from "./loadManifest.js"
34
34
  import type { CustomBlockManifest } from "./manifest.js"
35
+ import type { ConnectMessage } from "./messages/connect.js"
35
36
  import type {
36
37
  CreatePageMessage,
37
38
  CreatePageMessageParent,
@@ -39,12 +40,19 @@ import type {
39
40
  import type { CustomBlockCreatePageErrorInfo } from "./messages/createPageResult.js"
40
41
  import type { GetUserMessage } from "./messages/getUser.js"
41
42
  import { hostToSandboxMessageSchema } from "./messages/hostToSandbox.js"
42
- import { CustomBlockInitError, type InitMessage } from "./messages/init.js"
43
+ import {
44
+ CustomBlockInitializationError,
45
+ type InitMessage,
46
+ initMessageSchema,
47
+ } from "./messages/init.js"
48
+ import type {
49
+ CustomBlockInitResultErrorInfo,
50
+ InitResultMessage,
51
+ } from "./messages/initResult.js"
43
52
  import type { InvalidHostMessage } from "./messages/invalidHostMessage.js"
44
53
  import type { ListUsersMessage } from "./messages/listUsers.js"
45
54
  import type { QueryDataSourceMessage } from "./messages/queryDataSource.js"
46
55
  import type { CustomBlockQueryDataSourceErrorInfo } from "./messages/queryDataSourceResult.js"
47
- import type { ReadyMessage } from "./messages/ready.js"
48
56
  import type { ResizeMessage } from "./messages/resize.js"
49
57
  import type { UpdatePageMessage } from "./messages/updatePage.js"
50
58
  import { PendingRequests } from "./pendingRequests.js"
@@ -55,7 +63,7 @@ import type { NotionUser } from "./users/user.js"
55
63
  * single host needs to support multiple custom blocks built with different versions of the bridge
56
64
  * protocol. Increment this number any time a breaking change is made to the bridge protocol.
57
65
  */
58
- export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 2
66
+ export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3
59
67
 
60
68
  /**
61
69
  * A single entry in the bridge message log. Kept intentionally plain so the log
@@ -75,6 +83,7 @@ export class SandboxBridge {
75
83
  private listeners = new Set<() => void>()
76
84
  private messageLog: MessageLogEntry[] = []
77
85
  private messageLogListeners = new Set<() => void>()
86
+ private nextRequestId = 1
78
87
  private readonly pendingCreatePage = new PendingRequests<CreatePageResult>(
79
88
  "custom-block-create-page",
80
89
  )
@@ -90,20 +99,22 @@ export class SandboxBridge {
90
99
  private readonly pendingUpdatePage = new PendingRequests<UpdatePageResult>(
91
100
  "custom-block-update-page",
92
101
  )
93
- private hasSentReady = false
102
+ private hasSentConnect = false
103
+ private hasReceivedInit = false
104
+ private initializationId: string | undefined
94
105
  private latestDataSourceBindings: NotionDataSourceBindings = {}
95
- private resolveInit: ((message: InitMessage) => void) | undefined
106
+ private isMockState = false
107
+ private resolveInit: (() => void) | undefined
96
108
  private rejectInit: ((reason: Error) => void) | undefined
97
- private readonly initMessage: Promise<InitMessage> = new Promise(
109
+ private readonly initMessage: Promise<void> = new Promise(
98
110
  (resolve, reject) => {
99
111
  this.resolveInit = resolve
100
112
  this.rejectInit = reject
101
113
  },
102
114
  )
103
- private manifest: CustomBlockManifest | null = null
104
115
 
105
116
  constructor() {
106
- // `ready` is sent later by `initCustomBlock` (after the manifest fetch
117
+ // `connect` is sent later by `initCustomBlock` (after the manifest fetch
107
118
  // resolves). Top-level / no-iframe rejection is handled there too, so
108
119
  // the constructor just attaches the listener.
109
120
  if (typeof window !== "undefined") {
@@ -136,7 +147,7 @@ export class SandboxBridge {
136
147
  return () => this.messageLogListeners.delete(listener)
137
148
  }
138
149
 
139
- awaitInit(signal?: AbortSignal): Promise<InitMessage> {
150
+ awaitInit(signal?: AbortSignal): Promise<void> {
140
151
  if (!signal) {
141
152
  return this.initMessage
142
153
  }
@@ -148,9 +159,9 @@ export class SandboxBridge {
148
159
  const onAbort = () => reject(signal.reason)
149
160
  signal.addEventListener("abort", onAbort, { once: true })
150
161
  this.initMessage.then(
151
- message => {
162
+ () => {
152
163
  signal.removeEventListener("abort", onAbort)
153
- resolve(message)
164
+ resolve()
154
165
  },
155
166
  err => {
156
167
  signal.removeEventListener("abort", onAbort)
@@ -160,34 +171,38 @@ export class SandboxBridge {
160
171
  })
161
172
  }
162
173
 
163
- sendReady(manifestResult: ManifestLoadResult) {
174
+ sendConnect(manifestResult: ManifestLoadResult) {
164
175
  if (typeof window === "undefined") {
165
176
  return
166
177
  }
167
- if (this.hasSentReady) {
168
- console.warn("[custom-blocks-sdk] ignoring duplicate ready message")
178
+ if (this.hasSentConnect) {
179
+ console.warn("[custom-blocks-sdk] ignoring duplicate connect message")
169
180
  return
170
181
  }
171
182
  const { manifest, error } = manifestResult
172
- this.hasSentReady = true
173
- this.manifest = manifest
174
- const readyMessage: ReadyMessage =
183
+ this.hasSentConnect = true
184
+ const initializationId = `custom-block-initialization-${this.nextRequestId}`
185
+ this.nextRequestId += 1
186
+ this.initializationId = initializationId
187
+ const connectMessage: ConnectMessage =
175
188
  error !== undefined
176
189
  ? {
177
- type: "ready",
190
+ type: "connect",
191
+ initializationId,
178
192
  status: "error",
179
193
  bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
180
194
  sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
181
195
  error,
182
196
  }
183
197
  : {
184
- type: "ready",
198
+ type: "connect",
199
+ initializationId,
185
200
  status: "success",
186
201
  bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
187
202
  sdkVersion: CUSTOM_BLOCKS_SDK_VERSION,
188
- manifest,
203
+ ...(manifest !== null ? { manifest } : {}),
189
204
  }
190
- this.postToHost(readyMessage)
205
+ this.postToHost(connectMessage)
191
206
  }
192
207
 
193
208
  private postToHost(message: unknown) {
@@ -252,7 +267,18 @@ export class SandboxBridge {
252
267
  // `init` is the only message valid before initialization. Handle it up
253
268
  // front so every later case can assume `status === "initialized"`.
254
269
  if (message.type === "init") {
255
- this.applyInit(message)
270
+ if (message.initializationId !== this.initializationId) {
271
+ console.warn(
272
+ `[custom-blocks-sdk] ignoring init for unknown initializationId ${message.initializationId}`,
273
+ )
274
+ return
275
+ }
276
+ if (this.hasReceivedInit) {
277
+ console.warn("[custom-blocks-sdk] ignoring duplicate init message")
278
+ return
279
+ }
280
+ this.hasReceivedInit = true
281
+ this.applyInit(message, true)
256
282
  return
257
283
  }
258
284
 
@@ -307,10 +333,12 @@ export class SandboxBridge {
307
333
  const dataSources = reuseDataSourcesForUnchangedBindings({
308
334
  previousDataSources: hostState.dataSources,
309
335
  previousBindings: this.latestDataSourceBindings,
310
- nextDataSources: resolveDataSources({
311
- manifest: this.manifest,
312
- dataSourceBindings: nextBindings,
313
- }),
336
+ nextDataSources: this.isMockState
337
+ ? resolveMockDataSources(nextBindings)
338
+ : resolveDataSources({
339
+ manifest: hostState.manifest,
340
+ dataSourceBindings: nextBindings,
341
+ }),
314
342
  nextBindings,
315
343
  })
316
344
  this.latestDataSourceBindings = nextBindings
@@ -449,16 +477,6 @@ export class SandboxBridge {
449
477
  return this.hostState
450
478
  }
451
479
 
452
- /**
453
- * The author-declared manifest loaded from `custom_blocks.json` and forwarded
454
- * to the host in `ready`. `null` when it failed to load/parse and the host
455
- * should reject init via `ready.status: "error"`. Static for the lifetime
456
- * of the sandbox.
457
- */
458
- getManifest(): CustomBlockManifest | null {
459
- return this.manifest
460
- }
461
-
462
480
  /**
463
481
  * Apply an `init` payload as if it had arrived from the host. Lets callers
464
482
  * seed the bridge directly (e.g. the React provider's standalone preview
@@ -466,10 +484,14 @@ export class SandboxBridge {
466
484
  * of why it's being seeded.
467
485
  */
468
486
  setMockState(message: InitMessage) {
469
- this.applyInit(message)
487
+ this.isMockState = true
488
+ this.applyInit(v.parse(initMessageSchema, message), false)
470
489
  }
471
490
 
472
- private applyInit(message: InitMessage) {
491
+ private applyInit(message: InitMessage, postResult: boolean) {
492
+ if (postResult) {
493
+ this.isMockState = false
494
+ }
473
495
  if (message.status === "error") {
474
496
  // The host couldn't construct block location for this block (most commonly the parent record
475
497
  // failed to resolve). Surface the failure through the `awaitInit` promise so callers see
@@ -481,7 +503,7 @@ export class SandboxBridge {
481
503
  `[custom-blocks-sdk] host reported init error (${message.error.code}): ${message.error.message}`,
482
504
  )
483
505
  if (this.rejectInit) {
484
- this.rejectInit(new CustomBlockInitError(message.error))
506
+ this.rejectInit(new CustomBlockInitializationError(message.error))
485
507
  this.resolveInit = undefined
486
508
  this.rejectInit = undefined
487
509
  }
@@ -491,10 +513,28 @@ export class SandboxBridge {
491
513
  const { blockId, parent, page } = message
492
514
 
493
515
  this.latestDataSourceBindings = message.dataSources.bindings
494
- const dataSources = resolveDataSources({
495
- manifest: this.manifest,
496
- dataSourceBindings: this.latestDataSourceBindings,
497
- })
516
+ const dataSources = postResult
517
+ ? resolveDataSources({
518
+ manifest: message.manifest,
519
+ dataSourceBindings: this.latestDataSourceBindings,
520
+ })
521
+ : resolveMockDataSources(this.latestDataSourceBindings)
522
+ const bindingError = getInitBindingError(message.manifest, dataSources)
523
+ if (postResult && bindingError !== undefined) {
524
+ const result: InitResultMessage = {
525
+ type: "initResult",
526
+ initializationId: message.initializationId,
527
+ status: "error",
528
+ error: bindingError,
529
+ }
530
+ this.postToHost(result)
531
+ if (this.rejectInit) {
532
+ this.rejectInit(new CustomBlockInitializationError(bindingError))
533
+ this.resolveInit = undefined
534
+ this.rejectInit = undefined
535
+ }
536
+ return
537
+ }
498
538
  this.hostState = {
499
539
  status: "initialized",
500
540
  theme: message.theme,
@@ -502,15 +542,24 @@ export class SandboxBridge {
502
542
  parent,
503
543
  page,
504
544
  currentUser: message.currentUser,
545
+ manifest: message.manifest,
505
546
  dataSources,
506
547
  dataSourceState: {},
507
548
  }
508
549
  this.notify()
550
+ if (postResult) {
551
+ const result: InitResultMessage = {
552
+ type: "initResult",
553
+ initializationId: message.initializationId,
554
+ status: "success",
555
+ }
556
+ this.postToHost(result)
557
+ }
509
558
  // Resolve the awaitInit promise once. Subsequent `init` messages
510
559
  // (the host shouldn't send these, but be tolerant) update state but
511
560
  // don't re-resolve.
512
561
  if (this.resolveInit) {
513
- this.resolveInit(message)
562
+ this.resolveInit()
514
563
  this.resolveInit = undefined
515
564
  this.rejectInit = undefined
516
565
  }
@@ -892,6 +941,51 @@ function makeDataSourceSubscriptionId(args: { key: string }): string {
892
941
  return `data-source:${encodeURIComponent(key)}`
893
942
  }
894
943
 
944
+ function getInitBindingError(
945
+ manifest: CustomBlockManifest,
946
+ dataSources: NotionDataSource[],
947
+ ): CustomBlockInitResultErrorInfo | undefined {
948
+ for (const [dataSourceKey, manifestDataSource] of Object.entries(
949
+ manifest.dataSources,
950
+ )) {
951
+ const dataSource = dataSources.find(entry => entry.key === dataSourceKey)
952
+ if (
953
+ dataSource?.collectionPointer === undefined ||
954
+ dataSource.collectionSchema === undefined
955
+ ) {
956
+ return {
957
+ code: "invalid_init_bindings",
958
+ message: `Host did not provide a complete binding for data source "${dataSourceKey}".`,
959
+ isRetryable: false,
960
+ }
961
+ }
962
+ for (const propertyKey of Object.keys(
963
+ manifestDataSource.properties ?? {},
964
+ )) {
965
+ if (dataSource.propertyIdsByKey[propertyKey] === undefined) {
966
+ return {
967
+ code: "invalid_init_bindings",
968
+ message: `Host did not provide a valid binding for property "${dataSourceKey}.${propertyKey}".`,
969
+ isRetryable: false,
970
+ }
971
+ }
972
+ }
973
+ }
974
+ return undefined
975
+ }
976
+
977
+ function resolveMockDataSources(
978
+ bindings: NotionDataSourceBindings,
979
+ ): NotionDataSource[] {
980
+ return Object.entries(bindings).map(([key, binding]) => ({
981
+ key,
982
+ collectionPointer: binding.collectionPointer,
983
+ collectionSchema: binding.collectionSchema,
984
+ propertyIdsByKey: { ...(binding.propertyIdsByKey ?? {}) },
985
+ propertySchemasById: binding.collectionSchema?.propertiesById ?? {},
986
+ }))
987
+ }
988
+
895
989
  function formatInvalidHostReason(
896
990
  incomingType: string | undefined,
897
991
  issues: readonly v.BaseIssue<unknown>[],
@@ -5,7 +5,7 @@ import type {
5
5
  } from "./dataSource.js"
6
6
 
7
7
  type ResolveDataSourcesArgs = {
8
- manifest: CustomBlockManifest | null
8
+ manifest: CustomBlockManifest
9
9
  dataSourceBindings: NotionDataSourceBindings
10
10
  }
11
11
 
@@ -13,23 +13,12 @@ type ResolveDataSourcesArgs = {
13
13
  * Builds the public {@link NotionDataSource} list the SDK exposes to consumers.
14
14
  *
15
15
  * Combines the host-supplied bindings (collection pointers + schemas) with the
16
- * manifest's declared data-source keys. Property mappings are host-owned: the
17
- * SDK validates the supplied property id against the manifest and live schema,
18
- * but does not infer a missing mapping.
16
+ * manifest's declared data-source keys. The host owns every mapping; the SDK
17
+ * validates supplied IDs but never infers omitted bindings.
19
18
  */
20
19
  export function resolveDataSources(
21
20
  args: ResolveDataSourcesArgs,
22
21
  ): NotionDataSource[] {
23
- if (args.manifest === null) {
24
- return Object.entries(args.dataSourceBindings).map(([key, binding]) => ({
25
- key,
26
- collectionPointer: binding.collectionPointer,
27
- collectionSchema: binding.collectionSchema,
28
- propertyIdsByKey: { ...(binding.propertyIdsByKey ?? {}) },
29
- propertySchemasById: binding.collectionSchema?.propertiesById ?? {},
30
- }))
31
- }
32
-
33
22
  return Object.entries(args.manifest.dataSources).map(
34
23
  ([key, manifestDataSource]) => {
35
24
  const binding = args.dataSourceBindings[key]
@@ -8,6 +8,7 @@ import type {
8
8
  import type { NotionDataSourceValue } from "./dataSources/dataSourceValue.js"
9
9
  import type { NotionPropertySchema } from "./dataSources/propertySchema.js"
10
10
  import type { NotionBlockId } from "./ids.js"
11
+ import type { CustomBlockManifest } from "./manifest.js"
11
12
  import type { CustomBlockQueryDataSourceErrorInfo } from "./messages/queryDataSourceResult.js"
12
13
  import type { CustomBlockPage, NotionPageId } from "./pages/page.js"
13
14
  import type { NotionParent } from "./parent.js"
@@ -28,6 +29,7 @@ export type InitializedHostState = {
28
29
  parent: NotionParent
29
30
  page: CustomBlockPage
30
31
  currentUser: NotionUser
32
+ manifest: CustomBlockManifest
31
33
  dataSources: NotionDataSource[]
32
34
  dataSourceState: Record<string, DataSourceQueryState>
33
35
  }
@@ -4,13 +4,13 @@ import { type CustomBlockManifest, manifestSchema } from "./manifest.js"
4
4
 
5
5
  const MANIFEST_URL = "custom_blocks.json"
6
6
 
7
- export type CustomBlockReadyErrorCode =
7
+ export type CustomBlockConnectErrorCode =
8
8
  | "manifest_unavailable"
9
9
  | "manifest_invalid"
10
10
  | (string & {})
11
11
 
12
- export type CustomBlockReadyError =
13
- CustomBlockErrorInfo<CustomBlockReadyErrorCode>
12
+ export type CustomBlockConnectError =
13
+ CustomBlockErrorInfo<CustomBlockConnectErrorCode>
14
14
 
15
15
  export type ManifestLoadResult =
16
16
  | {
@@ -19,7 +19,7 @@ export type ManifestLoadResult =
19
19
  }
20
20
  | {
21
21
  manifest: null
22
- error: CustomBlockReadyError
22
+ error: CustomBlockConnectError
23
23
  }
24
24
 
25
25
  /**
@@ -27,7 +27,7 @@ export type ManifestLoadResult =
27
27
  * A missing manifest means the block has no declared data requirements.
28
28
  * Other failures return a structured error so the host can reject the manifest.
29
29
  * The SDK validates the manifest locally for author feedback; the host still
30
- * validates the ready message as the iframe trust boundary.
30
+ * validates the connect message as the iframe trust boundary.
31
31
  */
32
32
  export async function loadManifest(): Promise<ManifestLoadResult> {
33
33
  if (typeof fetch !== "function") {
@@ -48,7 +48,7 @@ export async function loadManifest(): Promise<ManifestLoadResult> {
48
48
  manifest: null,
49
49
  // Consider this retryable because it may have just been a network issue. Because `init`
50
50
  // errors are considered terminal, the host should "retry" by reloading the iframe and
51
- // sending another `ready` message.
51
+ // sending another `connect` message.
52
52
  error: { code: "manifest_unavailable", message, isRetryable: true },
53
53
  }
54
54
  }
@@ -3,9 +3,9 @@ import { notionPropertyTypeSchema } from "./dataSources/propertySchema.js"
3
3
 
4
4
  /**
5
5
  * User-authored manifest declaring the data sources the custom block expects.
6
- * Lives at `custom_blocks.json` in the project root and is forwarded to the host with
7
- * the bridge `ready` message so the host can pre-bind data sources, surface
8
- * configuration UI, etc. The `notionCustomBlock` Vite plugin from
6
+ * Lives at `custom_blocks.json` in the project root. The sandbox may send it in
7
+ * `connect`, and the host returns the authoritative manifest in `init`. The
8
+ * `notionCustomBlock` Vite plugin from
9
9
  * `@notionhq/custom-blocks/vite` wires the JSON file into the dev server and
10
10
  * the build output.
11
11
  */
@@ -0,0 +1,42 @@
1
+ import * as v from "valibot"
2
+ import { customBlockErrorInfoSchema } from "../errors.js"
3
+ import { manifestSchema } from "../manifest.js"
4
+
5
+ const connectMessageCommonEntries = {
6
+ type: v.literal("connect"),
7
+ initializationId: v.string(),
8
+ /**
9
+ * Used to ensure that the host and client are using the same version of the bridge protocol. A
10
+ * single host needs to support multiple custom blocks built with different versions of the bridge
11
+ * protocol. Increment this number any time a breaking change is made to the bridge protocol.
12
+ */
13
+ bridgeProtocolVersion: v.number(),
14
+ /**
15
+ * Semver version of the SDK package that sent this connect message. Should only be used for
16
+ * analytics purposes. Business logic should compare against `bridgeProtocolVersion` instead.
17
+ */
18
+ sdkVersion: v.string(),
19
+ }
20
+
21
+ /**
22
+ * First message the sandbox sends after mount. It identifies the sandbox and starts the bridge
23
+ * initialization exchange. The host echoes `initializationId` in its `init` response.
24
+ */
25
+ export const connectMessageSchema = v.variant("status", [
26
+ v.object({
27
+ status: v.literal("success"),
28
+ /**
29
+ * The data sources and settings the custom block expects. Hosts may omit
30
+ * this when they already have an authoritative manifest.
31
+ */
32
+ manifest: v.optional(manifestSchema),
33
+ ...connectMessageCommonEntries,
34
+ }),
35
+ v.object({
36
+ status: v.literal("error"),
37
+ error: customBlockErrorInfoSchema,
38
+ ...connectMessageCommonEntries,
39
+ }),
40
+ ])
41
+
42
+ export type ConnectMessage = v.InferOutput<typeof connectMessageSchema>
@@ -2,18 +2,18 @@ import * as v from "valibot"
2
2
  import { notionDataSourceBindingsSchema } from "../dataSources/dataSource.js"
3
3
  import type { CustomBlockErrorCode, CustomBlockErrorInfo } from "../errors.js"
4
4
  import { notionBlockIdSchema } from "../ids.js"
5
+ import { manifestSchema } from "../manifest.js"
5
6
  import { customBlockPageSchema } from "../pages/page.js"
6
7
  import { notionParentSchema } from "../parent.js"
7
8
  import { notionThemeSchema } from "../theme.js"
8
9
  import { notionUserSchema } from "../users/user.js"
10
+ import type { CustomBlockInitResultErrorCode } from "./initResult.js"
9
11
 
10
12
  // The schema accepts any string code. The type lists known codes for
11
13
  // autocomplete, with an open string fallback for newer senders.
12
14
  export const customBlockInitErrorCodeSchema = v.string()
13
15
 
14
16
  export type CustomBlockInitErrorCode = CustomBlockErrorCode<
15
- | "no_ready"
16
- | "invalid_ready"
17
17
  | "manifest_unavailable"
18
18
  | "manifest_invalid"
19
19
  | "invalid_protocol_version"
@@ -24,47 +24,60 @@ export type CustomBlockInitErrorCode = CustomBlockErrorCode<
24
24
  | "data_source_unavailable"
25
25
  | "missing_property_binding"
26
26
  | "invalid_property_binding"
27
- | "not_in_iframe"
28
- | "init_timeout"
29
27
  >
30
28
 
31
29
  export type CustomBlockInitErrorInfo =
32
30
  CustomBlockErrorInfo<CustomBlockInitErrorCode>
33
31
 
32
+ /**
33
+ * Every initialization failure visible to block code. This combines errors
34
+ * received in `init`, errors the sandbox reports in `initResult`, and failures
35
+ * detected locally while running `initCustomBlock()`.
36
+ */
37
+ export type CustomBlockInitializationErrorCode =
38
+ | CustomBlockInitErrorCode
39
+ | CustomBlockInitResultErrorCode
40
+ | CustomBlockErrorCode<"not_in_iframe" | "init_timeout">
41
+
42
+ export type CustomBlockInitializationErrorInfo =
43
+ CustomBlockErrorInfo<CustomBlockInitializationErrorCode>
44
+
34
45
  export const customBlockInitErrorInfoSchema = v.object({
35
46
  code: customBlockInitErrorCodeSchema,
36
47
  message: v.string(),
37
48
  isRetryable: v.boolean(),
38
49
  })
39
50
 
40
- export class CustomBlockInitError
51
+ export class CustomBlockInitializationError
41
52
  extends Error
42
53
  implements CustomBlockErrorInfo
43
54
  {
44
- constructor(error: CustomBlockInitErrorInfo) {
55
+ constructor(error: CustomBlockInitializationErrorInfo) {
45
56
  super(error.message)
46
- this.name = "CustomBlockInitError"
57
+ this.name = "CustomBlockInitializationError"
47
58
  this.code = error.code
48
59
  this.isRetryable = error.isRetryable
49
60
  }
50
61
 
51
- code: CustomBlockInitErrorCode
62
+ code: CustomBlockInitializationErrorCode
52
63
  isRetryable: boolean
53
64
  }
54
65
 
55
66
  /**
56
67
  * Initialization message sent by the host to the sandbox exactly once, in response to the
57
- * sandbox's `ready` message. After init, live updates flow through narrower messages
58
- * (`themeChanged`, `parentChanged`, `pageChanged`, `dataSourcesChanged`).
68
+ * sandbox's `connect` message. The sandbox echoes `initializationId` in `initResult`. After
69
+ * successful initialization, live updates flow through narrower messages.
59
70
  */
60
71
  export const initMessageSchema = v.variant("status", [
61
72
  v.object({
62
73
  type: v.literal("init"),
74
+ initializationId: v.string(),
63
75
  status: v.literal("success"),
64
76
  theme: notionThemeSchema,
65
77
  blockId: notionBlockIdSchema,
66
78
  parent: notionParentSchema,
67
79
  page: customBlockPageSchema,
80
+ manifest: manifestSchema,
68
81
  dataSources: v.object({
69
82
  bindings: notionDataSourceBindingsSchema,
70
83
  }),
@@ -72,6 +85,7 @@ export const initMessageSchema = v.variant("status", [
72
85
  }),
73
86
  v.object({
74
87
  type: v.literal("init"),
88
+ initializationId: v.string(),
75
89
  status: v.literal("error"),
76
90
  error: customBlockInitErrorInfoSchema,
77
91
  }),
@@ -0,0 +1,37 @@
1
+ import * as v from "valibot"
2
+ import type { CustomBlockErrorCode, CustomBlockErrorInfo } from "../errors.js"
3
+
4
+ // The schema accepts any string code. The type lists known codes for
5
+ // autocomplete, with an open string fallback for newer senders.
6
+ export const customBlockInitResultErrorCodeSchema = v.string()
7
+
8
+ export type CustomBlockInitResultErrorCode =
9
+ CustomBlockErrorCode<"invalid_init_bindings">
10
+
11
+ export type CustomBlockInitResultErrorInfo =
12
+ CustomBlockErrorInfo<CustomBlockInitResultErrorCode>
13
+
14
+ export const customBlockInitResultErrorInfoSchema = v.object({
15
+ code: customBlockInitResultErrorCodeSchema,
16
+ message: v.string(),
17
+ isRetryable: v.boolean(),
18
+ })
19
+
20
+ /**
21
+ * Acknowledges whether the sandbox successfully applied the host's `init` response.
22
+ */
23
+ export const initResultMessageSchema = v.variant("status", [
24
+ v.object({
25
+ type: v.literal("initResult"),
26
+ initializationId: v.string(),
27
+ status: v.literal("success"),
28
+ }),
29
+ v.object({
30
+ type: v.literal("initResult"),
31
+ initializationId: v.string(),
32
+ status: v.literal("error"),
33
+ error: customBlockInitResultErrorInfoSchema,
34
+ }),
35
+ ])
36
+
37
+ export type InitResultMessage = v.InferOutput<typeof initResultMessageSchema>
@@ -1,11 +1,12 @@
1
1
  import * as v from "valibot"
2
+ import { connectMessageSchema } from "./connect.js"
2
3
  import { createPageMessageSchema } from "./createPage.js"
3
4
  import { getPageMessageSchema } from "./getPage.js"
4
5
  import { getUserMessageSchema } from "./getUser.js"
6
+ import { initResultMessageSchema } from "./initResult.js"
5
7
  import { invalidHostMessageSchema } from "./invalidHostMessage.js"
6
8
  import { listUsersMessageSchema } from "./listUsers.js"
7
9
  import { queryDataSourceMessageSchema } from "./queryDataSource.js"
8
- import { readyMessageSchema } from "./ready.js"
9
10
  import { resizeMessageSchema } from "./resize.js"
10
11
  import { updatePageMessageSchema } from "./updatePage.js"
11
12
 
@@ -14,7 +15,8 @@ import { updatePageMessageSchema } from "./updatePage.js"
14
15
  * inbound traffic from sandboxes without re-implementing validation logic.
15
16
  */
16
17
  export const sandboxToHostMessageSchema = v.union([
17
- readyMessageSchema,
18
+ connectMessageSchema,
19
+ initResultMessageSchema,
18
20
  queryDataSourceMessageSchema,
19
21
  createPageMessageSchema,
20
22
  getPageMessageSchema,
@@ -17,7 +17,6 @@ import {
17
17
  getDataSourceQueryView as getDataSourceQueryViewWithBridge,
18
18
  } from "./hostState.js"
19
19
  import type { ManifestLoadResult } from "./loadManifest.js"
20
- import type { CustomBlockManifest } from "./manifest.js"
21
20
  import type { InitMessage } from "./messages/init.js"
22
21
  import { type MessageLogEntry, SandboxBridge } from "./SandboxBridge.js"
23
22
 
@@ -26,11 +25,11 @@ export type { MessageLogEntry }
26
25
  const bridge = new SandboxBridge()
27
26
 
28
27
  export const customBlockHost = {
29
- sendReady: (manifestResult: ManifestLoadResult) => {
30
- bridge.sendReady(manifestResult)
28
+ sendConnect: (manifestResult: ManifestLoadResult) => {
29
+ bridge.sendConnect(manifestResult)
31
30
  },
32
31
 
33
- awaitInit: (signal?: AbortSignal): Promise<InitMessage> => {
32
+ awaitInit: (signal?: AbortSignal): Promise<void> => {
34
33
  return bridge.awaitInit(signal)
35
34
  },
36
35
 
@@ -42,10 +41,6 @@ export const customBlockHost = {
42
41
  return bridge.getHostState()
43
42
  },
44
43
 
45
- getManifest: (): CustomBlockManifest | null => {
46
- return bridge.getManifest()
47
- },
48
-
49
44
  /**
50
45
  * Apply an `init` payload directly, bypassing the postMessage handshake for
51
46
  * standalone preview state.