@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
@@ -10,6 +10,7 @@ import {
10
10
  } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
11
11
  import {
12
12
  type CustomBlockInitErrorInfo,
13
+ CustomBlockInitializationError,
13
14
  initMessageSchema,
14
15
  } from "@notionhq/custom-blocks-protocol/messages/init.js"
15
16
  import type { InvalidSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/invalidSandboxMessage.js"
@@ -30,25 +31,34 @@ import type { notionThemeSchema } from "@notionhq/custom-blocks-protocol/theme.j
30
31
  import type { notionUserSchema } from "@notionhq/custom-blocks-protocol/users/user.js"
31
32
  import type { InferInput } from "valibot"
32
33
  import * as v from "valibot"
34
+ import { normalizeHostToSandboxMessage } from "./compatibility/normalizeHostToSandboxMessage.js"
35
+ import { isAllowedLocalPreviewUrl } from "./isAllowedLocalPreviewUrl.js"
33
36
  import {
34
37
  contextUnavailableError,
38
+ invalidLocalPreviewUrlError,
35
39
  invalidPropertyBindingError,
36
40
  missingDataSourceBindingError,
37
41
  missingPropertyBindingError,
38
42
  unknownInitializationError,
39
43
  } from "./lifecycle/initErrors.js"
40
44
  import { createInitializationController } from "./lifecycle/initializationController.js"
41
- import { loadCustomBlockManifest } from "./lifecycle/loadCustomBlockManifest.js"
42
- import type { CustomBlockHostManifestOptions } from "./lifecycle/manifestOptions.js"
43
- import { normalizeHostToSandboxMessage } from "./lifecycle/normalizeHostToSandboxMessage.js"
45
+ import { resolveManifest } from "./lifecycle/resolveManifest.js"
44
46
  import type {
45
47
  CustomBlockHostDataSourcesPayload,
46
48
  CustomBlockHostInitialState,
47
49
  CustomBlockHostInitialStateResult,
50
+ CustomBlockHostManifestResult,
48
51
  CustomBlockHostNormalizedDataSourcesPayload,
52
+ CustomBlockHostNormalizedManifestResult,
49
53
  CustomBlockHostPrepareInitialState,
54
+ CustomBlockHostStatus,
55
+ CustomBlockManifestSource,
56
+ } from "./lifecycle/types.js"
57
+ import {
58
+ customBlockHostInitialStateSchema,
59
+ customBlockHostManifestResultSchema,
60
+ customBlockHostPreparedStateResultSchema,
50
61
  } from "./lifecycle/types.js"
51
- import { customBlockHostInitialStateSchema } from "./lifecycle/types.js"
52
62
  import { parseExternalUrl } from "./messages/parseExternalUrl.js"
53
63
  import { createSandboxMessageRouter } from "./messages/sandboxMessageRouter.js"
54
64
  import type {
@@ -70,9 +80,20 @@ import { unreachable } from "./utils.js"
70
80
  // Earlier SDKs cannot represent this context, so the host reports context_unavailable.
71
81
  const BRIDGE_PROTOCOL_VERSION_INTRODUCING_UNAVAILABLE_PAGE = 5
72
82
 
73
- export type CustomBlockHostOptions = CustomBlockHostManifestOptions & {
83
+ export type CustomBlockHostOptions = {
84
+ manifestSource: CustomBlockManifestSource
85
+ /**
86
+ * Returns success with a persisted manifest or an explicit loading error, never null or
87
+ * undefined. The host validates the result and applies manifest defaults. It calls this
88
+ * when created, regardless of manifestSource. A returned error prevents initialization
89
+ * only when manifestSource is "host".
90
+ */
91
+ getPersistedManifest: (args: {
92
+ signal: AbortSignal
93
+ }) => Promise<CustomBlockHostManifestResult>
74
94
  iframe: HTMLIFrameElement
75
- sandboxOrigin: string
95
+ /** Full block URL. Its origin is used to authenticate bridge messages. */
96
+ sandboxUrl: string
76
97
  /**
77
98
  * Minimum sandbox protocol version to accept. Defaults to the host runtime
78
99
  * minimum. Higher values retire older SDKs; lower values cannot enable
@@ -82,22 +103,12 @@ export type CustomBlockHostOptions = CustomBlockHostManifestOptions & {
82
103
  noConnectTimeoutMs?: number
83
104
  noInitResultTimeoutMs?: number
84
105
  handlers: CustomBlockHostHandlers
85
- /** Callback fired when the initialization handshake with the sandbox completed successfully. */
86
- onInitialized?: (state: {
87
- manifest: CustomBlockManifest
88
- initialHeight?: number
89
- }) => void
90
- /**
91
- * Reports a handshake failure, timeout, unavailable page context, or iframe reload.
92
- * Iframe reloads and unavailable page context for older SDKs stop the host
93
- * before this callback runs.
94
- * This callback can run after initialization succeeds.
95
- */
96
- onConnectionFailure: ((error: CustomBlockHostFailure) => void) | undefined
106
+ /** Reports accepted lifecycle transitions, starting synchronously with waitingForConnect. */
107
+ onStatusChange: (state: CustomBlockHostStatus) => void
97
108
  /** Releases adapter resources when a query ends, including replacement and terminal errors. */
98
109
  onQueryUnsubscribed: ((subscriptionId: string) => void) | undefined
99
110
  onLog?: (direction: CustomBlockHostLogDirection, payload: unknown) => void
100
- /** Runs once, as soon as the manifest is validated, independently of connect. */
111
+ /** Runs once, after the selected manifest is validated. */
101
112
  prepareInitialState: CustomBlockHostPrepareInitialState
102
113
  initialStateTimeoutMs?: number
103
114
  }
@@ -130,16 +141,16 @@ export function createCustomBlockHost(
130
141
  iframe,
131
142
  prepareInitialState: prepareAdapterState,
132
143
  handlers,
133
- onInitialized,
134
- onConnectionFailure,
144
+ onStatusChange,
135
145
  onLog,
136
146
  onQueryUnsubscribed,
137
- sandboxOrigin,
138
147
  minBridgeProtocolVersion: configuredMinBridgeProtocolVersion,
139
148
  noConnectTimeoutMs,
140
149
  initialStateTimeoutMs,
141
150
  noInitResultTimeoutMs,
142
151
  } = options
152
+ const sandboxUrl = parseSandboxUrl(options.sandboxUrl, options.manifestSource)
153
+ const sandboxOrigin = sandboxUrl.origin
143
154
  // Callers can retire older SDKs, but cannot restore support for protocols
144
155
  // the runtime no longer implements. Omission uses the runtime minimum.
145
156
  const minBridgeProtocolVersion = Math.max(
@@ -175,7 +186,7 @@ export function createCustomBlockHost(
175
186
  case "preparingInit":
176
187
  case "waitingForInitResult":
177
188
  case "initialized":
178
- case "error":
189
+ case "failed":
179
190
  break
180
191
  // A stopped host must not send messages, including pending responses.
181
192
  // Return from the function to skip delivery below.
@@ -210,7 +221,7 @@ export function createCustomBlockHost(
210
221
  case "waitingForConnect":
211
222
  case "preparingInit":
212
223
  case "waitingForInitResult":
213
- case "error":
224
+ case "failed":
214
225
  case "stopped":
215
226
  return
216
227
  default:
@@ -304,7 +315,7 @@ export function createCustomBlockHost(
304
315
  function updateHostState(update: HostStateUpdates) {
305
316
  if (
306
317
  initializationController.status === "stopped" ||
307
- initializationController.status === "error"
318
+ initializationController.status === "failed"
308
319
  ) {
309
320
  return
310
321
  }
@@ -419,15 +430,30 @@ export function createCustomBlockHost(
419
430
  }
420
431
 
421
432
  const initializationController = createInitializationController({
433
+ manifestSource: options.manifestSource,
422
434
  async prepareInit(connect) {
435
+ const connectManifest =
436
+ connect.status === "success" ? connect.manifest : undefined
423
437
  sandboxBridgeProtocolVersion = connect.bridgeProtocolVersion
424
- const result = await preparation
438
+ onStatusChange({
439
+ status: "preparingInit",
440
+ localManifest: isAllowedLocalPreviewUrl({
441
+ url: sandboxUrl,
442
+ hostOrigin: window.location.origin,
443
+ })
444
+ ? connectManifest
445
+ : undefined,
446
+ bridgeProtocolVersion: connect.bridgeProtocolVersion,
447
+ sdkVersion: connect.sdkVersion,
448
+ })
449
+ const result = await (preparation ?? prepare(connectManifest))
425
450
  if (initializationController.status !== "preparingInit") {
426
451
  return result
427
452
  }
428
453
  return finalizeInitialState(result)
429
454
  },
430
455
  sendInitMessage,
456
+ onInitSent: () => onStatusChange({ status: "waitingForInitResult" }),
431
457
  warn: payload => emit("warn", payload),
432
458
  flushPendingHostStateChanges,
433
459
  minBridgeProtocolVersion,
@@ -437,12 +463,16 @@ export function createCustomBlockHost(
437
463
  stopHostWithFailure: stopWithFailure,
438
464
  onInitResult(message) {
439
465
  if (message.status === "success" && manifest !== undefined) {
440
- onInitialized?.({ manifest, initialHeight: message.initialHeight })
466
+ onStatusChange({
467
+ status: "initialized",
468
+ resolvedManifest: manifest,
469
+ initialHeight: message.initialHeight,
470
+ })
441
471
  }
442
472
  },
443
473
  onConnectionFailure: failure => {
444
474
  preparationController.abort()
445
- onConnectionFailure?.(failure)
475
+ onStatusChange({ status: "failed", error: failure })
446
476
  },
447
477
  })
448
478
 
@@ -484,46 +514,64 @@ export function createCustomBlockHost(
484
514
  return { status: "success", initialState }
485
515
  }
486
516
 
487
- // Attach rejection handling immediately: preparation may finish before connect.
488
- const preparation = prepare().catch(
489
- (error: unknown): CustomBlockHostInitialStateResult => {
490
- if (!preparationController.signal.aborted) {
491
- emit("warn", {
492
- message: "Failed to prepare custom block initialization",
493
- error,
494
- })
495
- }
496
- return { status: "error", error: unknownInitializationError() }
497
- },
498
- )
517
+ // Neither manifest source waits for the other. Observe rejections immediately,
518
+ // including failures from a host manifest that is not selected.
519
+ const hostManifest = readHostManifest().catch(handlePreparationError)
520
+ const preparation =
521
+ options.manifestSource === "host"
522
+ ? prepare(undefined).catch(handlePreparationError)
523
+ : undefined
499
524
 
500
- async function prepare(): Promise<CustomBlockHostInitialStateResult> {
501
- const loaded = await loadCustomBlockManifest({
502
- ...options,
503
- sandboxOrigin,
525
+ async function readHostManifest(): Promise<CustomBlockHostNormalizedManifestResult> {
526
+ preparationController.signal.throwIfAborted()
527
+ const result = await options.getPersistedManifest({
504
528
  signal: preparationController.signal,
505
- onError: error => {
506
- emit("warn", {
507
- message: "Failed to load custom block manifest",
508
- error,
509
- })
510
- },
511
529
  })
512
- if (loaded.status === "error") {
513
- return loaded
514
- }
515
- if (preparationController.signal.aborted) {
516
- return { status: "error", error: unknownInitializationError() }
530
+ preparationController.signal.throwIfAborted()
531
+ return v.parse(customBlockHostManifestResultSchema, result)
532
+ }
533
+
534
+ function handlePreparationError(error: unknown): {
535
+ status: "error"
536
+ error: CustomBlockInitErrorInfo
537
+ } {
538
+ if (!preparationController.signal.aborted) {
539
+ emit("warn", {
540
+ message: "Failed to prepare custom block initialization",
541
+ error,
542
+ })
517
543
  }
518
- const result = await prepareAdapterState({
519
- manifest: loaded.manifest,
520
- signal: preparationController.signal,
544
+ return { status: "error", error: unknownInitializationError() }
545
+ }
546
+
547
+ async function prepare(
548
+ connectManifest: CustomBlockManifest | undefined,
549
+ ): Promise<CustomBlockHostInitialStateResult> {
550
+ preparationController.signal.throwIfAborted()
551
+ const manifestResult = await resolveManifest({
552
+ manifestSource: options.manifestSource,
553
+ hostManifest,
554
+ connectManifest,
521
555
  })
556
+ preparationController.signal.throwIfAborted()
557
+ if (manifestResult.status === "error") {
558
+ return manifestResult
559
+ }
560
+ const result = v.parse(
561
+ customBlockHostPreparedStateResultSchema,
562
+ await prepareAdapterState({
563
+ manifest: manifestResult.manifest,
564
+ signal: preparationController.signal,
565
+ }),
566
+ )
522
567
  return result.status === "error"
523
568
  ? result
524
569
  : {
525
570
  status: "success",
526
- initialState: { ...result.initialState, manifest: loaded.manifest },
571
+ initialState: {
572
+ ...result.initialState,
573
+ manifest: manifestResult.manifest,
574
+ },
527
575
  }
528
576
  }
529
577
 
@@ -552,7 +600,7 @@ export function createCustomBlockHost(
552
600
 
553
601
  window.addEventListener("message", onMessage)
554
602
  iframe.addEventListener("load", initializationController.onIframeLoad)
555
- function stop() {
603
+ function releaseResources() {
556
604
  navigationAbortController.abort()
557
605
  preparationController.abort()
558
606
  window.removeEventListener("message", onMessage)
@@ -561,12 +609,29 @@ export function createCustomBlockHost(
561
609
  querySubscriptions.unsubscribeAll()
562
610
  }
563
611
 
612
+ function stop() {
613
+ const previousStatus = initializationController.status
614
+ releaseResources()
615
+ if (previousStatus !== "stopped" && previousStatus !== "failed") {
616
+ onStatusChange({ status: "stopped" })
617
+ }
618
+ }
619
+
564
620
  function stopWithFailure(error: CustomBlockHostFailure) {
565
- stop()
621
+ if (initializationController.status === "stopped") {
622
+ return
623
+ }
624
+ if (initializationController.status === "failed") {
625
+ releaseResources()
626
+ return
627
+ }
628
+ releaseResources()
566
629
  emit("warn", error)
567
- onConnectionFailure?.(error)
630
+ onStatusChange({ status: "failed", error })
568
631
  }
569
632
 
633
+ onStatusChange({ status: "waitingForConnect" })
634
+
570
635
  return {
571
636
  stop,
572
637
  setTheme(theme) {
@@ -642,6 +707,31 @@ export function createCustomBlockHost(
642
707
  }
643
708
  }
644
709
 
710
+ function parseSandboxUrl(
711
+ url: string,
712
+ manifestSource: CustomBlockManifestSource,
713
+ ): URL {
714
+ let parsed: URL
715
+ try {
716
+ parsed = new URL(url)
717
+ } catch (error) {
718
+ if (manifestSource === "host") {
719
+ throw error
720
+ }
721
+ throw new CustomBlockInitializationError(invalidLocalPreviewUrlError())
722
+ }
723
+ if (
724
+ manifestSource === "sandbox" &&
725
+ !isAllowedLocalPreviewUrl({
726
+ url: parsed,
727
+ hostOrigin: window.location.origin,
728
+ })
729
+ ) {
730
+ throw new CustomBlockInitializationError(invalidLocalPreviewUrlError())
731
+ }
732
+ return parsed
733
+ }
734
+
645
735
  function getCollectionSchema(
646
736
  dataSources: CustomBlockHostNormalizedDataSourcesPayload | undefined,
647
737
  dataSourceId: NotionDataSourceId,
package/src/index.ts CHANGED
@@ -1,51 +1,3 @@
1
- export type { LegacyNotionDataSourcePageBridge } from "@notionhq/custom-blocks-protocol/dataSources/dataSourcePage.js"
2
- export type { NotionPropertySchemasById } from "@notionhq/custom-blocks-protocol/dataSources/propertySchema.js"
3
- export type { CustomBlockErrorInfo } from "@notionhq/custom-blocks-protocol/errors.js"
4
- export type { NotionPageId } from "@notionhq/custom-blocks-protocol/ids.js"
5
- export type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
6
- export type { CreatePageMessage } from "@notionhq/custom-blocks-protocol/messages/createPage.js"
7
- export type { GetPageMessage } from "@notionhq/custom-blocks-protocol/messages/getPage.js"
8
- export type { GetUserMessage } from "@notionhq/custom-blocks-protocol/messages/getUser.js"
9
- export type { ListUsersMessage } from "@notionhq/custom-blocks-protocol/messages/listUsers.js"
10
- export type { OpenExternalUrlMessage } from "@notionhq/custom-blocks-protocol/messages/openExternalUrl.js"
11
- export type { OpenPageMessage } from "@notionhq/custom-blocks-protocol/messages/openPage.js"
12
- export type {
13
- CustomBlockCheckboxFilterOperator,
14
- CustomBlockContainsFilterOperator,
15
- CustomBlockCreatedByFilterOperator,
16
- CustomBlockDateFilterOperator,
17
- CustomBlockNumberFilterOperator,
18
- CustomBlockOptionFilterOperator,
19
- CustomBlockPeopleFilterOperator,
20
- CustomBlockPersonFilterValue,
21
- CustomBlockRelationFilterOperator,
22
- CustomBlockStatusFilterOperator,
23
- CustomBlockStatusGroupFilterOperator,
24
- CustomBlockStatusGroupName,
25
- CustomBlockTextFilterOperator,
26
- QueryDataSourceMessage,
27
- ResolvedCustomBlockDataSourceFilter,
28
- ResolvedCustomBlockDataSourcePropertyFilter,
29
- ResolvedCustomBlockDataSourceSort,
30
- } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
31
- export type { ResizeMessage } from "@notionhq/custom-blocks-protocol/messages/resize.js"
32
- export type { UpdatePageMessage } from "@notionhq/custom-blocks-protocol/messages/updatePage.js"
33
- export type {
34
- NotionDateInput,
35
- NotionFileInput,
36
- NotionPageCover,
37
- NotionPageData,
38
- NotionPageIcon,
39
- NotionPagePropertyReadMap,
40
- NotionPagePropertyValue,
41
- NotionPagePropertyWriteMap,
42
- NotionRichTextItem,
43
- NotionSelectOptionInput,
44
- } from "@notionhq/custom-blocks-protocol/pages/page.js"
45
- export {
46
- LATEST_BRIDGE_PROTOCOL_VERSION,
47
- LEGACY_PROPERTY_SHAPE_PROTOCOL_VERSION,
48
- } from "@notionhq/custom-blocks-protocol/protocolVersion.js"
49
1
  export {
50
2
  type AutoBindableProperty,
51
3
  getAutoBoundPropertyIdsByKey,
@@ -61,16 +13,17 @@ export {
61
13
  toNotionDateValue,
62
14
  } from "./dateValue.js"
63
15
  export { toNotionPageId } from "./ids.js"
64
- export type { CustomBlockHostManifestOptions } from "./lifecycle/manifestOptions.js"
16
+ export { isAllowedLocalPreviewUrl } from "./isAllowedLocalPreviewUrl.js"
65
17
  export type {
66
- ConnectSuccessMessage,
67
18
  CustomBlockHostDataSourcesPayload,
68
19
  CustomBlockHostInitialState,
69
20
  CustomBlockHostInitialStateResult,
21
+ CustomBlockHostManifestResult,
70
22
  CustomBlockHostPreparedStateResult,
71
23
  CustomBlockHostPrepareInitialState,
24
+ CustomBlockHostStatus,
25
+ CustomBlockManifestSource,
72
26
  } from "./lifecycle/types.js"
73
- export { isLocalCustomBlockUrl } from "./localBlockUrl.js"
74
27
  export type {
75
28
  CreatePageMessageInput,
76
29
  CustomBlockHostCreatePageResult,
@@ -1,10 +1,12 @@
1
+ import { isLocalBlockServerUrl } from "@notionhq/custom-blocks-protocol/isLocalBlockServerUrl.js"
2
+
1
3
  /**
2
4
  * Checks whether a URL can serve a local custom block over HTTP or HTTPS.
3
5
  * Accepts localhost, any .localhost subdomain, and 127.0.0.1 without credentials.
4
6
  * Local refers to the block server, not the environment where the host runs.
5
7
  * Requires the embedding page's origin and rejects URLs that share that origin.
6
8
  */
7
- export function isLocalCustomBlockUrl({
9
+ export function isAllowedLocalPreviewUrl({
8
10
  url,
9
11
  hostOrigin,
10
12
  }: {
@@ -15,11 +17,6 @@ export function isLocalCustomBlockUrl({
15
17
  typeof hostOrigin === "string" &&
16
18
  hostOrigin !== "" &&
17
19
  url.origin !== hostOrigin &&
18
- (url.protocol === "http:" || url.protocol === "https:") &&
19
- (url.hostname === "localhost" ||
20
- url.hostname === "127.0.0.1" ||
21
- url.hostname.endsWith(".localhost")) &&
22
- url.username === "" &&
23
- url.password === ""
20
+ isLocalBlockServerUrl(url)
24
21
  )
25
22
  }
@@ -120,3 +120,12 @@ export function unknownInitializationError(): CustomBlockErrorInfo<"unknown_erro
120
120
  isRetryable: false,
121
121
  }
122
122
  }
123
+
124
+ export function invalidLocalPreviewUrlError(): CustomBlockErrorInfo<"invalid_local_preview_url"> {
125
+ return {
126
+ code: "invalid_local_preview_url",
127
+ message:
128
+ "Local custom blocks require an HTTP(S) URL on localhost, a .localhost subdomain, or 127.0.0.1, without credentials and on a different origin from the host.",
129
+ isRetryable: false,
130
+ }
131
+ }
@@ -18,8 +18,8 @@ import {
18
18
  } from "./initErrors.js"
19
19
  import { isValidBridgeProtocolVersion } from "./protocolVersion.js"
20
20
  import type {
21
- ConnectSuccessMessage,
22
21
  CustomBlockHostInitialStateResult,
22
+ CustomBlockManifestSource,
23
23
  InitStatus,
24
24
  } from "./types.js"
25
25
 
@@ -28,8 +28,9 @@ const DEFAULT_INITIAL_STATE_TIMEOUT_MS = 5_000
28
28
  const DEFAULT_NO_INIT_RESULT_TIMEOUT_MS = 5_000
29
29
 
30
30
  export type InitializationControllerOptions = {
31
+ manifestSource: CustomBlockManifestSource
31
32
  prepareInit: (
32
- connect: ConnectSuccessMessage,
33
+ connect: ConnectMessage,
33
34
  ) => Promise<CustomBlockHostInitialStateResult>
34
35
  sendInitMessage: (message: InferInput<typeof initMessageSchema>) => void
35
36
  warn: (payload: unknown) => void
@@ -39,8 +40,9 @@ export type InitializationControllerOptions = {
39
40
  initialStateTimeoutMs?: number
40
41
  noInitResultTimeoutMs?: number
41
42
  stopHostWithFailure: (error: CustomBlockHostFailure) => void
42
- onInitResult?: (message: InitResultMessage) => void
43
- onConnectionFailure: ((error: CustomBlockHostFailure) => void) | undefined
43
+ onInitResult: (message: InitResultMessage) => void
44
+ onInitSent: () => void
45
+ onConnectionFailure: (error: CustomBlockHostFailure) => void
44
46
  }
45
47
 
46
48
  export type InitializationController = {
@@ -65,6 +67,7 @@ export function createInitializationController(
65
67
  options: InitializationControllerOptions,
66
68
  ): InitializationController {
67
69
  const {
70
+ manifestSource,
68
71
  prepareInit,
69
72
  sendInitMessage,
70
73
  warn,
@@ -75,6 +78,7 @@ export function createInitializationController(
75
78
  noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS,
76
79
  stopHostWithFailure,
77
80
  onInitResult,
81
+ onInitSent,
78
82
  onConnectionFailure,
79
83
  } = options
80
84
 
@@ -120,13 +124,13 @@ export function createInitializationController(
120
124
  break
121
125
  case "waitingForInitResult":
122
126
  case "initialized":
123
- case "error":
127
+ case "failed":
124
128
  case "stopped":
125
129
  return
126
130
  default:
127
131
  return unreachable(initStatus)
128
132
  }
129
- initStatus = "error"
133
+ initStatus = "failed"
130
134
  activeInitializationId = initializationId
131
135
  clearNoConnectTimeout()
132
136
  clearInitialStateTimeout()
@@ -136,10 +140,10 @@ export function createInitializationController(
136
140
  status: "error",
137
141
  error,
138
142
  })
139
- onConnectionFailure?.({ ...error, phase, source })
143
+ onConnectionFailure({ ...error, phase, source })
140
144
  }
141
145
 
142
- async function prepareAndSendInit(connect: ConnectSuccessMessage) {
146
+ async function prepareAndSendInit(connect: ConnectMessage) {
143
147
  if (initStatus !== "waitingForConnect") {
144
148
  return
145
149
  }
@@ -194,13 +198,20 @@ export function createInitializationController(
194
198
  dataSources: preparation.initialState.dataSources,
195
199
  }
196
200
  sendInitMessage(message)
201
+ if (initStatus !== "waitingForInitResult") {
202
+ return
203
+ }
204
+ onInitSent()
205
+ if (initStatus !== "waitingForInitResult") {
206
+ return
207
+ }
197
208
  noInitResultTimeoutId = window.setTimeout(() => {
198
209
  if (initStatus !== "waitingForInitResult") {
199
210
  return
200
211
  }
201
- initStatus = "error"
212
+ initStatus = "failed"
202
213
  const error = noInitResultError()
203
- onConnectionFailure?.({ ...error, phase: "initResult", source: "host" })
214
+ onConnectionFailure({ ...error, phase: "initResult", source: "host" })
204
215
  warn({
205
216
  ...error,
206
217
  initializationId: connect.initializationId,
@@ -258,8 +269,12 @@ export function createInitializationController(
258
269
  return
259
270
  }
260
271
 
261
- // If the sandbox reported an error during `connect`, return it as an `init` error.
262
- if (message.status === "error") {
272
+ // Invalid sandbox metadata does not prevent initialization from a host manifest.
273
+ if (
274
+ message.status === "error" &&
275
+ (manifestSource === "sandbox" ||
276
+ message.error.code !== "manifest_invalid")
277
+ ) {
263
278
  sendInitError({
264
279
  initializationId: message.initializationId,
265
280
  error: message.error,
@@ -322,9 +337,9 @@ export function createInitializationController(
322
337
  }
323
338
  clearNoConnectTimeout()
324
339
  clearNoInitResultTimeout()
325
- initStatus = "error"
340
+ initStatus = "failed"
326
341
  warn(error)
327
- onConnectionFailure?.({ ...error, phase: "initResult", source: "host" })
342
+ onConnectionFailure({ ...error, phase: "initResult", source: "host" })
328
343
  }
329
344
 
330
345
  function onIframeLoad() {
@@ -353,10 +368,10 @@ export function createInitializationController(
353
368
  // Only do anything if the iframe loaded before the `connect` message was sent.
354
369
  return
355
370
  }
356
- initStatus = "error"
371
+ initStatus = "failed"
357
372
  const error = noConnectError()
358
373
  warn(error)
359
- onConnectionFailure?.({ ...error, phase: "connect", source: "host" })
374
+ onConnectionFailure({ ...error, phase: "connect", source: "host" })
360
375
  }, noConnectTimeoutMs)
361
376
  }
362
377
 
@@ -372,17 +387,17 @@ export function createInitializationController(
372
387
  return
373
388
  }
374
389
  clearNoInitResultTimeout()
375
- initStatus = message.status === "success" ? "initialized" : "error"
390
+ initStatus = message.status === "success" ? "initialized" : "failed"
376
391
  if (message.status === "success") {
377
392
  flushPendingHostStateChanges()
378
393
  } else {
379
- onConnectionFailure?.({
394
+ onConnectionFailure({
380
395
  ...message.error,
381
396
  phase: "initResult",
382
397
  source: "sandbox",
383
398
  })
384
399
  }
385
- onInitResult?.(message)
400
+ onInitResult(message)
386
401
  }
387
402
 
388
403
  function stop() {
@@ -0,0 +1,31 @@
1
+ import type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
2
+ import type {
3
+ CustomBlockHostNormalizedManifestResult,
4
+ CustomBlockManifestSource,
5
+ } from "./types.js"
6
+
7
+ /**
8
+ * Select the host's manifest result or the sandbox's connect declaration. Both
9
+ * sources are already validated. Forward host errors unchanged, and report
10
+ * manifest_unavailable if the sandbox omits its declaration.
11
+ */
12
+ export async function resolveManifest(args: {
13
+ manifestSource: CustomBlockManifestSource
14
+ hostManifest: Promise<CustomBlockHostNormalizedManifestResult>
15
+ connectManifest: CustomBlockManifest | undefined
16
+ }): Promise<CustomBlockHostNormalizedManifestResult> {
17
+ if (args.manifestSource === "host") {
18
+ return args.hostManifest
19
+ }
20
+ if (args.connectManifest !== undefined) {
21
+ return { status: "success", manifest: args.connectManifest }
22
+ }
23
+ return {
24
+ status: "error",
25
+ error: {
26
+ code: "manifest_unavailable",
27
+ message: "The sandbox did not provide a manifest in connect.",
28
+ isRetryable: false,
29
+ },
30
+ }
31
+ }