@notionhq/custom-blocks-host 0.1.8 → 0.1.10

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 (33) hide show
  1. package/README.md +43 -24
  2. package/dist/createCustomBlockHost.d.ts +13 -11
  3. package/dist/createCustomBlockHost.d.ts.map +1 -1
  4. package/dist/createCustomBlockHost.js +35 -106
  5. package/dist/lifecycle/initializationController.d.ts +9 -4
  6. package/dist/lifecycle/initializationController.d.ts.map +1 -1
  7. package/dist/lifecycle/initializationController.js +34 -26
  8. package/dist/lifecycle/types.d.ts +5 -1
  9. package/dist/lifecycle/types.d.ts.map +1 -1
  10. package/dist/messages/invalidSandboxMessage.js +1 -1
  11. package/dist/messages/sandboxMessageRouter.d.ts +35 -0
  12. package/dist/messages/sandboxMessageRouter.d.ts.map +1 -0
  13. package/dist/messages/sandboxMessageRouter.js +107 -0
  14. package/dist/messages/types.d.ts +1 -1
  15. package/dist/messages/types.d.ts.map +1 -1
  16. package/dist/protocol/index.d.ts +3 -2
  17. package/dist/protocol/index.js +3 -2
  18. package/dist/protocol/messages/isInitHandshakeMessage.d.ts +4 -0
  19. package/dist/protocol/messages/isInitHandshakeMessage.js +11 -0
  20. package/dist/protocol/{messagePayload.d.ts → messages/messagePayload.d.ts} +1 -1
  21. package/dist/queries/types.d.ts +1 -1
  22. package/dist/queries/types.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/createCustomBlockHost.ts +49 -134
  25. package/src/lifecycle/initializationController.ts +54 -33
  26. package/src/lifecycle/types.ts +5 -1
  27. package/src/messages/invalidSandboxMessage.ts +1 -1
  28. package/src/messages/sandboxMessageRouter.ts +176 -0
  29. package/src/messages/types.ts +1 -1
  30. package/src/queries/types.ts +1 -1
  31. /package/dist/protocol/{incomingType.d.ts → messages/incomingType.d.ts} +0 -0
  32. /package/dist/protocol/{incomingType.js → messages/incomingType.js} +0 -0
  33. /package/dist/protocol/{messagePayload.js → messages/messagePayload.js} +0 -0
@@ -0,0 +1,107 @@
1
+ import { isInitHandshakeMessage } from "../protocol/messages/isInitHandshakeMessage.js";
2
+ import { sandboxToHostMessageSchema } from "../protocol/messages/sandboxToHost.js";
3
+ import * as v from "valibot";
4
+ import { initErrorForFailureReason } from "../lifecycle/initErrors.js";
5
+ import { unreachable } from "../utils.js";
6
+ import { getInvalidSandboxMessageResponse } from "./invalidSandboxMessage.js";
7
+ const initializationIdentitySchema = v.object({
8
+ initializationId: v.string(),
9
+ });
10
+ export function createSandboxMessageRouter(options) {
11
+ const { iframe, sandboxOrigin, getInitStatus, emit, post, onInvalidConnect, onInvalidInitResult, onConnect, onInitResult, onQueryDataSource, onUnsubscribeDataSourceQuery, onCreatePage, onGetPage, onGetUser, onListUsers, onUpdatePage, onResize, } = options;
12
+ function onInvalidSandboxMessage(data, issues) {
13
+ emit("warn", {
14
+ message: "Ignored unsupported sandbox message",
15
+ payload: data,
16
+ issues,
17
+ });
18
+ const response = getInvalidSandboxMessageResponse(data);
19
+ if (response.incomingType === "connect") {
20
+ const parsedIdentity = v.safeParse(initializationIdentitySchema, data);
21
+ if (parsedIdentity.success) {
22
+ const responseType = onInvalidConnect({
23
+ initializationId: parsedIdentity.output.initializationId,
24
+ error: initErrorForFailureReason("invalid_connect_payload"),
25
+ });
26
+ if (responseType === "initErrorSent") {
27
+ // The controller already sent `init.error`, so no need to also send a NACK.
28
+ return;
29
+ }
30
+ }
31
+ }
32
+ if (response.incomingType === "initResult") {
33
+ const parsedIdentity = v.safeParse(initializationIdentitySchema, data);
34
+ onInvalidInitResult({
35
+ initializationId: parsedIdentity.success
36
+ ? parsedIdentity.output.initializationId
37
+ : undefined,
38
+ error: initErrorForFailureReason("invalid_init_result_payload"),
39
+ });
40
+ }
41
+ // Other messages get NACKed indicating they were invalid and skipped
42
+ if (response.nack !== undefined) {
43
+ post(response.nack);
44
+ }
45
+ }
46
+ return function onMessage(event) {
47
+ // Verify the sender is this host's iframe, not another window with the same origin.
48
+ const isExpectedSource = event.source === iframe.contentWindow;
49
+ // Verify the sender's origin, since the iframe can navigate to another origin.
50
+ const isExpectedOrigin = event.origin === sandboxOrigin;
51
+ if (!isExpectedSource || !isExpectedOrigin) {
52
+ return;
53
+ }
54
+ // Parse and validate the message from the sandbox.
55
+ const parsed = v.safeParse(sandboxToHostMessageSchema, event.data);
56
+ if (!parsed.success) {
57
+ onInvalidSandboxMessage(event.data, parsed.issues);
58
+ return;
59
+ }
60
+ const message = parsed.output;
61
+ if (!isInitHandshakeMessage(message) && getInitStatus() !== "success") {
62
+ emit("warn", {
63
+ message: `Ignored ${message.type} before initialization completed`,
64
+ payload: message,
65
+ });
66
+ return;
67
+ }
68
+ emit("in", message);
69
+ switch (message.type) {
70
+ case "connect":
71
+ onConnect(message);
72
+ return;
73
+ case "initResult":
74
+ onInitResult(message);
75
+ return;
76
+ case "queryDataSource":
77
+ void onQueryDataSource(message);
78
+ return;
79
+ case "unsubscribeDataSourceQuery":
80
+ onUnsubscribeDataSourceQuery(message);
81
+ return;
82
+ case "createPage":
83
+ void onCreatePage(message);
84
+ return;
85
+ case "getPage":
86
+ void onGetPage(message);
87
+ return;
88
+ case "getUser":
89
+ void onGetUser(message);
90
+ return;
91
+ case "listUsers":
92
+ void onListUsers(message);
93
+ return;
94
+ case "updatePage":
95
+ void onUpdatePage(message);
96
+ return;
97
+ case "resize":
98
+ onResize(message);
99
+ return;
100
+ case "invalidHostMessage":
101
+ // The inbound log records this rejection. Do not reply, to avoid an error-response loop.
102
+ return;
103
+ default:
104
+ unreachable(message);
105
+ }
106
+ };
107
+ }
@@ -1,4 +1,3 @@
1
- import type { BridgeMessagePayload } from "../protocol/messagePayload.js";
2
1
  import type { CreatePageMessage } from "../protocol/messages/createPage.js";
3
2
  import type { CreatePageResultMessage, CustomBlockCreatePageErrorInfo } from "../protocol/messages/createPageResult.js";
4
3
  import type { CustomBlockGetPageErrorInfo, GetPageMessage, GetPageResultMessage } from "../protocol/messages/getPage.js";
@@ -6,6 +5,7 @@ import type { CustomBlockGetUserErrorInfo, GetUserMessage, GetUserResultMessage
6
5
  import type { CustomBlockInitErrorInfo } from "../protocol/messages/init.js";
7
6
  import type { CustomBlockInitResultErrorInfo } from "../protocol/messages/initResult.js";
8
7
  import type { CustomBlockListUsersErrorInfo, ListUsersMessage, ListUsersResultMessage } from "../protocol/messages/listUsers.js";
8
+ import type { BridgeMessagePayload } from "../protocol/messages/messagePayload.js";
9
9
  import type { QueryDataSourceMessage } from "../protocol/messages/queryDataSource.js";
10
10
  import type { ResizeMessage } from "../protocol/messages/resize.js";
11
11
  import type { UpdatePageMessage } from "../protocol/messages/updatePage.js";
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/messages/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oDAAoD,CAAA;AAC9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAA;AAChG,OAAO,KAAK,EACX,uBAAuB,EACvB,8BAA8B,EAC9B,MAAM,+DAA+D,CAAA;AACtE,OAAO,KAAK,EACX,2BAA2B,EAC3B,cAAc,EACd,oBAAoB,EACpB,MAAM,sDAAsD,CAAA;AAC7D,OAAO,KAAK,EACX,2BAA2B,EAC3B,cAAc,EACd,oBAAoB,EACpB,MAAM,sDAAsD,CAAA;AAC7D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mDAAmD,CAAA;AACjG,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,yDAAyD,CAAA;AAC7G,OAAO,KAAK,EACX,6BAA6B,EAC7B,gBAAgB,EAChB,sBAAsB,EACtB,MAAM,wDAAwD,CAAA;AAC/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,8DAA8D,CAAA;AAC1G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qDAAqD,CAAA;AACxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAA;AAChG,OAAO,KAAK,EACX,8BAA8B,EAC9B,uBAAuB,EACvB,MAAM,+DAA+D,CAAA;AACtE,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,qBAAqB,CAAA;AAE/E,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;AAErC,MAAM,MAAM,+BAA+B,GAAG,oBAAoB,CACjE,uBAAuB,EACvB,8BAA8B,CAC9B,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,CAC9D,oBAAoB,EACpB,2BAA2B,CAC3B,CAAA;AAED,MAAM,MAAM,+BAA+B,GAAG,oBAAoB,CACjE,uBAAuB,EACvB,8BAA8B,CAC9B,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,CAC9D,oBAAoB,EACpB,2BAA2B,CAC3B,CAAA;AAED,MAAM,MAAM,8BAA8B,GAAG,oBAAoB,CAChE,sBAAsB,EACtB,6BAA6B,CAC7B,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACrC,eAAe,EAAE,CAChB,OAAO,EAAE,sBAAsB,KAC3B,YAAY,CAAC,oCAAoC,CAAC,CAAA;IACvD,UAAU,CAAC,EAAE,CACZ,OAAO,EAAE,iBAAiB,KACtB,YAAY,CAAC,+BAA+B,CAAC,CAAA;IAClD,OAAO,CAAC,EAAE,CACT,OAAO,EAAE,cAAc,KACnB,YAAY,CAAC,4BAA4B,CAAC,CAAA;IAC/C,OAAO,CAAC,EAAE,CACT,OAAO,EAAE,cAAc,KACnB,YAAY,CAAC,4BAA4B,CAAC,CAAA;IAC/C,SAAS,CAAC,EAAE,CACX,OAAO,EAAE,gBAAgB,KACrB,YAAY,CAAC,8BAA8B,CAAC,CAAA;IACjD,UAAU,CAAC,EAAE,CACZ,OAAO,EAAE,iBAAiB,KACtB,YAAY,CAAC,+BAA+B,CAAC,CAAA;IAClD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CACzC,CAAA;AAED,MAAM,MAAM,oCAAoC,GAC7C,wBAAwB,GACxB,8BAA8B,CAAA;AAEjC,MAAM,MAAM,2BAA2B,GAAG,IAAI,GAAG,KAAK,GAAG,MAAM,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/messages/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAA;AAChG,OAAO,KAAK,EACX,uBAAuB,EACvB,8BAA8B,EAC9B,MAAM,+DAA+D,CAAA;AACtE,OAAO,KAAK,EACX,2BAA2B,EAC3B,cAAc,EACd,oBAAoB,EACpB,MAAM,sDAAsD,CAAA;AAC7D,OAAO,KAAK,EACX,2BAA2B,EAC3B,cAAc,EACd,oBAAoB,EACpB,MAAM,sDAAsD,CAAA;AAC7D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mDAAmD,CAAA;AACjG,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,yDAAyD,CAAA;AAC7G,OAAO,KAAK,EACX,6BAA6B,EAC7B,gBAAgB,EAChB,sBAAsB,EACtB,MAAM,wDAAwD,CAAA;AAC/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6DAA6D,CAAA;AACvG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,8DAA8D,CAAA;AAC1G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qDAAqD,CAAA;AACxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yDAAyD,CAAA;AAChG,OAAO,KAAK,EACX,8BAA8B,EAC9B,uBAAuB,EACvB,MAAM,+DAA+D,CAAA;AACtE,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,qBAAqB,CAAA;AAE/E,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;AAErC,MAAM,MAAM,+BAA+B,GAAG,oBAAoB,CACjE,uBAAuB,EACvB,8BAA8B,CAC9B,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,CAC9D,oBAAoB,EACpB,2BAA2B,CAC3B,CAAA;AAED,MAAM,MAAM,+BAA+B,GAAG,oBAAoB,CACjE,uBAAuB,EACvB,8BAA8B,CAC9B,CAAA;AAED,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,CAC9D,oBAAoB,EACpB,2BAA2B,CAC3B,CAAA;AAED,MAAM,MAAM,8BAA8B,GAAG,oBAAoB,CAChE,sBAAsB,EACtB,6BAA6B,CAC7B,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACrC,eAAe,EAAE,CAChB,OAAO,EAAE,sBAAsB,KAC3B,YAAY,CAAC,oCAAoC,CAAC,CAAA;IACvD,UAAU,CAAC,EAAE,CACZ,OAAO,EAAE,iBAAiB,KACtB,YAAY,CAAC,+BAA+B,CAAC,CAAA;IAClD,OAAO,CAAC,EAAE,CACT,OAAO,EAAE,cAAc,KACnB,YAAY,CAAC,4BAA4B,CAAC,CAAA;IAC/C,OAAO,CAAC,EAAE,CACT,OAAO,EAAE,cAAc,KACnB,YAAY,CAAC,4BAA4B,CAAC,CAAA;IAC/C,SAAS,CAAC,EAAE,CACX,OAAO,EAAE,gBAAgB,KACrB,YAAY,CAAC,8BAA8B,CAAC,CAAA;IACjD,UAAU,CAAC,EAAE,CACZ,OAAO,EAAE,iBAAiB,KACtB,YAAY,CAAC,+BAA+B,CAAC,CAAA;IAClD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAA;CACzC,CAAA;AAED,MAAM,MAAM,oCAAoC,GAC7C,wBAAwB,GACxB,8BAA8B,CAAA;AAEjC,MAAM,MAAM,2BAA2B,GAAG,IAAI,GAAG,KAAK,GAAG,MAAM,CAAA"}
@@ -15,9 +15,7 @@ export * from "./dataSources/propertySchema.js";
15
15
  export * from "./dataSources/recordPointer.js";
16
16
  export * from "./errors.js";
17
17
  export * from "./ids.js";
18
- export * from "./incomingType.js";
19
18
  export * from "./manifest.js";
20
- export * from "./messagePayload.js";
21
19
  export * from "./messages/connect.js";
22
20
  export * from "./messages/contrastModeChanged.js";
23
21
  export * from "./messages/createPage.js";
@@ -27,11 +25,14 @@ export * from "./messages/dataSourcesChanged.js";
27
25
  export * from "./messages/getPage.js";
28
26
  export * from "./messages/getUser.js";
29
27
  export * from "./messages/hostToSandbox.js";
28
+ export * from "./messages/incomingType.js";
30
29
  export * from "./messages/init.js";
31
30
  export * from "./messages/initResult.js";
32
31
  export * from "./messages/invalidHostMessage.js";
33
32
  export * from "./messages/invalidSandboxMessage.js";
33
+ export * from "./messages/isInitHandshakeMessage.js";
34
34
  export * from "./messages/listUsers.js";
35
+ export * from "./messages/messagePayload.js";
35
36
  export * from "./messages/pageChanged.js";
36
37
  export * from "./messages/parentChanged.js";
37
38
  export * from "./messages/queryDataSource.js";
@@ -15,9 +15,7 @@ export * from "./dataSources/propertySchema.js";
15
15
  export * from "./dataSources/recordPointer.js";
16
16
  export * from "./errors.js";
17
17
  export * from "./ids.js";
18
- export * from "./incomingType.js";
19
18
  export * from "./manifest.js";
20
- export * from "./messagePayload.js";
21
19
  export * from "./messages/connect.js";
22
20
  export * from "./messages/contrastModeChanged.js";
23
21
  export * from "./messages/createPage.js";
@@ -27,11 +25,14 @@ export * from "./messages/dataSourcesChanged.js";
27
25
  export * from "./messages/getPage.js";
28
26
  export * from "./messages/getUser.js";
29
27
  export * from "./messages/hostToSandbox.js";
28
+ export * from "./messages/incomingType.js";
30
29
  export * from "./messages/init.js";
31
30
  export * from "./messages/initResult.js";
32
31
  export * from "./messages/invalidHostMessage.js";
33
32
  export * from "./messages/invalidSandboxMessage.js";
33
+ export * from "./messages/isInitHandshakeMessage.js";
34
34
  export * from "./messages/listUsers.js";
35
+ export * from "./messages/messagePayload.js";
35
36
  export * from "./messages/pageChanged.js";
36
37
  export * from "./messages/parentChanged.js";
37
38
  export * from "./messages/queryDataSource.js";
@@ -0,0 +1,4 @@
1
+ import type { HostToSandboxMessage } from "./hostToSandbox.js";
2
+ import type { SandboxToHostMessage } from "./sandboxToHost.js";
3
+ /** Returns whether a message belongs to the initialization handshake. */
4
+ export declare function isInitHandshakeMessage(message: SandboxToHostMessage | HostToSandboxMessage): boolean;
@@ -0,0 +1,11 @@
1
+ /** Returns whether a message belongs to the initialization handshake. */
2
+ export function isInitHandshakeMessage(message) {
3
+ switch (message.type) {
4
+ case "connect":
5
+ case "init":
6
+ case "initResult":
7
+ return true;
8
+ default:
9
+ return false;
10
+ }
11
+ }
@@ -1,4 +1,4 @@
1
- import type { CustomBlockErrorInfo } from "./errors.js";
1
+ import type { CustomBlockErrorInfo } from "../errors.js";
2
2
  type WithTypedErrorInfo<T, TErrorInfo extends CustomBlockErrorInfo | undefined> = TErrorInfo extends CustomBlockErrorInfo ? T extends {
3
3
  error: CustomBlockErrorInfo;
4
4
  } ? Omit<T, "error"> & {
@@ -1,4 +1,4 @@
1
- import type { BridgeMessagePayload } from "../protocol/messagePayload.js";
1
+ import type { BridgeMessagePayload } from "../protocol/messages/messagePayload.js";
2
2
  import type { CustomBlockQueryDataSourceErrorInfo, QueryDataSourceResultMessage } from "../protocol/messages/queryDataSourceResult.js";
3
3
  export type CustomBlockHostQueryDataSourceResult = BridgeMessagePayload<QueryDataSourceResultMessage, CustomBlockQueryDataSourceErrorInfo>;
4
4
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/queries/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oDAAoD,CAAA;AAC9F,OAAO,KAAK,EACX,mCAAmC,EACnC,4BAA4B,EAC5B,MAAM,oEAAoE,CAAA;AAE3E,MAAM,MAAM,oCAAoC,GAAG,oBAAoB,CACtE,4BAA4B,EAC5B,mCAAmC,CACnC,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/queries/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6DAA6D,CAAA;AACvG,OAAO,KAAK,EACX,mCAAmC,EACnC,4BAA4B,EAC5B,MAAM,oEAAoE,CAAA;AAE3E,MAAM,MAAM,oCAAoC,GAAG,oBAAoB,CACtE,4BAA4B,EAC5B,mCAAmC,CACnC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks-host",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Reusable host runtime for Notion custom blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,7 +4,6 @@ import {
4
4
  } from "@notionhq/custom-blocks-protocol/contrast.js"
5
5
  import type { NotionDataSourceId } from "@notionhq/custom-blocks-protocol/ids.js"
6
6
  import type { CustomBlockManifest } from "@notionhq/custom-blocks-protocol/manifest.js"
7
- import type { ConnectMessage } from "@notionhq/custom-blocks-protocol/messages/connect.js"
8
7
  import type { ContrastModeChangedMessage } from "@notionhq/custom-blocks-protocol/messages/contrastModeChanged.js"
9
8
  import type { CreatePageMessage } from "@notionhq/custom-blocks-protocol/messages/createPage.js"
10
9
  import type { CreatePageResultMessage } from "@notionhq/custom-blocks-protocol/messages/createPageResult.js"
@@ -19,7 +18,6 @@ import type {
19
18
  } from "@notionhq/custom-blocks-protocol/messages/getUser.js"
20
19
  import type { HostToSandboxMessage } from "@notionhq/custom-blocks-protocol/messages/hostToSandbox.js"
21
20
  import type { CustomBlockInitErrorCode } from "@notionhq/custom-blocks-protocol/messages/init.js"
22
- import type { InitResultMessage } from "@notionhq/custom-blocks-protocol/messages/initResult.js"
23
21
  import type {
24
22
  ListUsersMessage,
25
23
  ListUsersResultMessage,
@@ -28,7 +26,6 @@ import type { PageChangedMessage } from "@notionhq/custom-blocks-protocol/messag
28
26
  import type { ParentChangedMessage } from "@notionhq/custom-blocks-protocol/messages/parentChanged.js"
29
27
  import type { QueryDataSourceMessage } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
30
28
  import type { QueryDataSourceResultMessage } from "@notionhq/custom-blocks-protocol/messages/queryDataSourceResult.js"
31
- import { sandboxToHostMessageSchema } from "@notionhq/custom-blocks-protocol/messages/sandboxToHost.js"
32
29
  import type { ThemeChangedMessage } from "@notionhq/custom-blocks-protocol/messages/themeChanged.js"
33
30
  import type { UpdatePageMessage } from "@notionhq/custom-blocks-protocol/messages/updatePage.js"
34
31
  import type { UpdatePageResultMessage } from "@notionhq/custom-blocks-protocol/messages/updatePageResult.js"
@@ -37,15 +34,13 @@ import type { NotionParent } from "@notionhq/custom-blocks-protocol/parent.js"
37
34
  import { CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION } from "@notionhq/custom-blocks-protocol/protocolVersion.js"
38
35
  import type { NotionTheme } from "@notionhq/custom-blocks-protocol/theme.js"
39
36
  import type { NotionUser } from "@notionhq/custom-blocks-protocol/users/user.js"
40
- import * as v from "valibot"
41
37
  import { initErrorForFailureReason } from "./lifecycle/initErrors.js"
42
38
  import { createInitializationController } from "./lifecycle/initializationController.js"
43
39
  import type {
44
- ConnectSuccessMessage,
45
40
  CustomBlockHostDataSourcesPayload,
46
41
  CustomBlockHostInitialState,
47
42
  } from "./lifecycle/types.js"
48
- import { getInvalidSandboxMessageResponse } from "./messages/invalidSandboxMessage.js"
43
+ import { createSandboxMessageRouter } from "./messages/sandboxMessageRouter.js"
49
44
  import type {
50
45
  CustomBlockHostHandlers,
51
46
  CustomBlockHostInitializationFailure,
@@ -58,7 +53,6 @@ import {
58
53
  import type { CustomBlockHostQueryDataSourceResult } from "./queries/types.js"
59
54
  import { validateQueryDataSourceFilter } from "./queries/validateQueryDataSourceFilter.js"
60
55
  import { validateQueryDataSourceSorts } from "./queries/validateQueryDataSourceSorts.js"
61
- import { unreachable } from "./utils.js"
62
56
 
63
57
  export type CustomBlockHostOptions = {
64
58
  iframe: HTMLIFrameElement
@@ -68,19 +62,22 @@ export type CustomBlockHostOptions = {
68
62
  noInitResultTimeoutMs?: number
69
63
  initialState: CustomBlockHostInitialState
70
64
  handlers: CustomBlockHostHandlers
71
- onConnect?: (message: ConnectSuccessMessage) => void
72
- onInitResult?: (message: InitResultMessage) => void
65
+ /** Called as soon as the `manifest` is available after `connect`, before `onInitialized` and binding validation. */
66
+ onManifest?: (manifest: CustomBlockManifest) => void
67
+ /** Callback fired when the initialization handshake with the sandbox completed successfully. */
68
+ onInitialized?: (state: {
69
+ manifest: CustomBlockManifest
70
+ initialHeight?: number
71
+ }) => void
72
+ /** Callback fired when the initialization handshake with the sandbox failed or timed out. */
73
73
  onInitializationFailure?: (
74
74
  error: CustomBlockHostInitializationFailure,
75
75
  ) => void
76
76
  /**
77
- * A `connect` arrived after this host's initialization settled the
78
- * document was replaced, or its first `connect` came after the host
79
- * stopped waiting. Either way the host cannot serve it: the embedder
80
- * should recreate host and iframe together so the document can run a
81
- * fresh handshake.
77
+ * Callback fired when the current host and iframe must restart.
78
+ * Recreate both to continue.
82
79
  */
83
- onIgnoredConnect?: (message: ConnectMessage) => void
80
+ onRestartRequired?: () => void
84
81
  onLog?: (direction: CustomBlockHostLogDirection, payload: unknown) => void
85
82
  }
86
83
 
@@ -96,10 +93,6 @@ export type CustomBlockHostHandle = {
96
93
  refreshQuery: (args: { dataSourceId: NotionDataSourceId }) => void
97
94
  }
98
95
 
99
- const initializationIdentitySchema = v.object({
100
- initializationId: v.string(),
101
- })
102
-
103
96
  export function createCustomBlockHost(
104
97
  options: CustomBlockHostOptions,
105
98
  ): CustomBlockHostHandle {
@@ -107,10 +100,10 @@ export function createCustomBlockHost(
107
100
  iframe,
108
101
  initialState,
109
102
  handlers,
110
- onConnect,
111
- onInitResult,
103
+ onManifest,
104
+ onInitialized,
112
105
  onInitializationFailure,
113
- onIgnoredConnect,
106
+ onRestartRequired,
114
107
  onLog,
115
108
  sandboxOrigin,
116
109
  minBridgeProtocolVersion: configuredMinBridgeProtocolVersion,
@@ -322,6 +315,13 @@ export function createCustomBlockHost(
322
315
  }),
323
316
  prepareInit(connect) {
324
317
  manifest = connect.manifest ?? initialState.manifest
318
+ if (manifest === undefined) {
319
+ return {
320
+ status: "error",
321
+ error: initErrorForFailureReason("manifest_unavailable"),
322
+ }
323
+ }
324
+ onManifest?.(manifest)
325
325
  const resolvedDataSources =
326
326
  typeof dataSources === "function" ? dataSources(connect) : dataSources
327
327
  const bindingErrorCode = getBindingErrorCode(
@@ -347,121 +347,35 @@ export function createCustomBlockHost(
347
347
  minBridgeProtocolVersion,
348
348
  noConnectTimeoutMs,
349
349
  noInitResultTimeoutMs,
350
- onConnect,
351
- onInitResult,
352
- onIgnoredConnect,
350
+ onInitResult(message) {
351
+ if (message.status === "success" && manifest !== undefined) {
352
+ onInitialized?.({ manifest, initialHeight: message.initialHeight })
353
+ }
354
+ },
355
+ onRestartRequired,
353
356
  onInitializationFailure,
354
357
  })
355
358
 
356
- function onInvalidSandboxMessage(
357
- data: unknown,
358
- issues: readonly v.BaseIssue<unknown>[],
359
- ) {
360
- emit("warn", {
361
- message: "Ignored unsupported sandbox message",
362
- payload: data,
363
- issues,
364
- })
365
- const response = getInvalidSandboxMessageResponse(data)
366
-
367
- const parsedIdentity = v.safeParse(initializationIdentitySchema, data)
368
- const initializationId = parsedIdentity.success
369
- ? parsedIdentity.output.initializationId
370
- : undefined
371
- if (response.incomingType === "connect") {
372
- if (initializationController.handleInvalidConnect(initializationId)) {
373
- if (initializationId === undefined && response.nack !== undefined) {
374
- post(response.nack)
375
- }
376
- return
377
- }
378
- }
379
- if (
380
- response.incomingType === "initResult" &&
381
- initializationController.handleInvalidInitResult()
382
- ) {
383
- if (response.nack !== undefined) {
384
- post(response.nack)
385
- }
386
- return
387
- }
388
- if (response.nack !== undefined) {
389
- post(response.nack)
390
- }
391
- }
392
-
393
- function onMessage(event: MessageEvent) {
394
- // Ignore messages from windows other than this host's iframe.
395
- if (event.source !== iframe.contentWindow) {
396
- return
397
- }
398
-
399
- // Ignore messages that do not come from the configured sandbox origin.
400
- if (event.origin !== sandboxOrigin) {
401
- return
402
- }
403
-
404
- const parsed = v.safeParse(sandboxToHostMessageSchema, event.data)
405
- if (!parsed.success) {
406
- onInvalidSandboxMessage(event.data, parsed.issues)
407
- return
408
- }
409
-
410
- const message = parsed.output
411
- emit("in", message)
412
-
413
- if (
414
- message.type !== "connect" &&
415
- message.type !== "initResult" &&
416
- initializationController.status !== "success"
417
- ) {
418
- // Ignore all non-handshake messages until initialization succeeds.
419
- if (message.type !== "resize") {
420
- emit("warn", {
421
- message: `Ignored ${message.type} before initialization completed`,
422
- payload: message,
423
- })
424
- }
425
- return
426
- }
427
-
428
- switch (message.type) {
429
- case "connect":
430
- initializationController.handleConnect(message)
431
- return
432
- case "initResult":
433
- initializationController.handleInitResult(message)
434
- return
435
- case "queryDataSource":
436
- void handleQuery(message)
437
- return
438
- case "unsubscribeDataSourceQuery":
439
- querySubscriptions.unsubscribe(message.subscriptionId)
440
- return
441
- case "createPage":
442
- void handleCreatePage(message)
443
- return
444
- case "getPage":
445
- void handleGetPage(message)
446
- return
447
- case "getUser":
448
- void handleGetUser(message)
449
- return
450
- case "listUsers":
451
- void handleListUsers(message)
452
- return
453
- case "updatePage":
454
- void handleUpdatePage(message)
455
- return
456
- case "resize":
457
- handlers.resize?.(message)
458
- return
459
- case "invalidHostMessage":
460
- return
461
- default:
462
- unreachable(message)
463
- }
464
- }
359
+ const onMessage = createSandboxMessageRouter({
360
+ iframe,
361
+ sandboxOrigin,
362
+ getInitStatus: () => initializationController.status,
363
+ emit,
364
+ post,
365
+ onInvalidConnect: initializationController.handleInvalidConnect,
366
+ onInvalidInitResult: initializationController.handleInvalidInitResult,
367
+ onConnect: initializationController.handleConnect,
368
+ onInitResult: initializationController.handleInitResult,
369
+ onQueryDataSource: handleQuery,
370
+ onUnsubscribeDataSourceQuery: message =>
371
+ querySubscriptions.unsubscribe(message.subscriptionId),
372
+ onCreatePage: handleCreatePage,
373
+ onGetPage: handleGetPage,
374
+ onGetUser: handleGetUser,
375
+ onListUsers: handleListUsers,
376
+ onUpdatePage: handleUpdatePage,
377
+ onResize: message => handlers.resize?.(message),
378
+ })
465
379
 
466
380
  window.addEventListener("message", onMessage)
467
381
  iframe.addEventListener("load", initializationController.onIframeLoad)
@@ -524,6 +438,7 @@ export function createCustomBlockHost(
524
438
  setDataSources(nextDataSources) {
525
439
  if (
526
440
  initializationController.hasConnectedSandbox &&
441
+ manifest !== undefined &&
527
442
  getBindingErrorCode(manifest, nextDataSources) !== undefined
528
443
  ) {
529
444
  emit("warn", {
@@ -53,9 +53,8 @@ export type InitializationControllerOptions = {
53
53
  minBridgeProtocolVersion: number
54
54
  noConnectTimeoutMs?: number
55
55
  noInitResultTimeoutMs?: number
56
- onConnect?: (message: ConnectSuccessMessage) => void
57
56
  onInitResult?: (message: InitResultMessage) => void
58
- onIgnoredConnect?: (message: ConnectMessage) => void
57
+ onRestartRequired?: () => void
59
58
  onInitializationFailure?: (
60
59
  error: CustomBlockHostInitializationFailure,
61
60
  ) => void
@@ -66,8 +65,14 @@ export type InitializationController = {
66
65
  readonly hasConnectedSandbox: boolean
67
66
  handleConnect: (message: ConnectMessage) => void
68
67
  handleInitResult: (message: InitResultMessage) => void
69
- handleInvalidConnect: (initializationId?: string) => boolean
70
- handleInvalidInitResult: () => boolean
68
+ handleInvalidConnect: (args: {
69
+ initializationId: string
70
+ error: CustomBlockInitErrorInfo
71
+ }) => "initErrorSent" | "nack"
72
+ handleInvalidInitResult: (args: {
73
+ initializationId: string | undefined
74
+ error: CustomBlockInitErrorInfo
75
+ }) => void
71
76
  onIframeLoad: () => void
72
77
  stop: () => void
73
78
  }
@@ -85,9 +90,8 @@ export function createInitializationController(
85
90
  minBridgeProtocolVersion,
86
91
  noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS,
87
92
  noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS,
88
- onConnect,
89
93
  onInitResult,
90
- onIgnoredConnect,
94
+ onRestartRequired,
91
95
  onInitializationFailure,
92
96
  } = options
93
97
 
@@ -111,10 +115,11 @@ export function createInitializationController(
111
115
  }
112
116
  }
113
117
 
114
- function sendInitError(
115
- initializationId: string,
116
- error: CustomBlockInitErrorInfo,
117
- ) {
118
+ function sendInitError(args: {
119
+ initializationId: string
120
+ error: CustomBlockInitErrorInfo
121
+ }) {
122
+ const { initializationId, error } = args
118
123
  if (initStatus !== "waitingForConnect") {
119
124
  return
120
125
  }
@@ -134,10 +139,12 @@ export function createInitializationController(
134
139
  if (initStatus !== "waitingForConnect") {
135
140
  return false
136
141
  }
137
- onConnect?.(connect)
138
142
  const preparation = prepareInit(connect)
139
143
  if (preparation.status === "error") {
140
- sendInitError(connect.initializationId, preparation.error)
144
+ sendInitError({
145
+ initializationId: connect.initializationId,
146
+ error: preparation.error,
147
+ })
141
148
  return false
142
149
  }
143
150
  initStatus = "waitingForInitResult"
@@ -185,7 +192,7 @@ export function createInitializationController(
185
192
  "Ignored connect after initialization settled; recreate the host and iframe to re-initialize",
186
193
  initializationId: message.initializationId,
187
194
  })
188
- onIgnoredConnect?.(message)
195
+ onRestartRequired?.()
189
196
  return
190
197
  }
191
198
 
@@ -194,7 +201,10 @@ export function createInitializationController(
194
201
  const initError = initErrorForFailureReason("invalid_protocol_version", {
195
202
  currentBridgeProtocolVersion: message.bridgeProtocolVersion,
196
203
  })
197
- sendInitError(message.initializationId, initError)
204
+ sendInitError({
205
+ initializationId: message.initializationId,
206
+ error: initError,
207
+ })
198
208
  return
199
209
  }
200
210
  if (message.bridgeProtocolVersion < minBridgeProtocolVersion) {
@@ -205,13 +215,19 @@ export function createInitializationController(
205
215
  minBridgeProtocolVersion,
206
216
  },
207
217
  )
208
- sendInitError(message.initializationId, initError)
218
+ sendInitError({
219
+ initializationId: message.initializationId,
220
+ error: initError,
221
+ })
209
222
  return
210
223
  }
211
224
 
212
225
  // If the sandbox reported an error during `connect`, return it as an `init` error.
213
226
  if (message.status === "error") {
214
- sendInitError(message.initializationId, message.error)
227
+ sendInitError({
228
+ initializationId: message.initializationId,
229
+ error: message.error,
230
+ })
215
231
  return
216
232
  }
217
233
 
@@ -220,32 +236,37 @@ export function createInitializationController(
220
236
  }
221
237
  }
222
238
 
223
- function handleInvalidConnect(initializationId?: string): boolean {
239
+ function handleInvalidConnect(args: {
240
+ initializationId: string
241
+ error: CustomBlockInitErrorInfo
242
+ }): "initErrorSent" | "nack" {
243
+ const { initializationId, error } = args
224
244
  if (initStatus !== "waitingForConnect") {
225
- return false
226
- }
227
- const error = initErrorForFailureReason("invalid_connect_payload")
228
- if (initializationId === undefined) {
229
- initStatus = "error"
230
- clearNoConnectTimeout()
231
- warn(error)
232
- onInitializationFailure?.(error)
233
- } else {
234
- sendInitError(initializationId, error)
245
+ // `connect` messages received when not waiting for one are NACKed like generic invalid messages.
246
+ return "nack"
235
247
  }
236
- return true
248
+ // The sandbox awaits `init`. Reply with `init.error` using its initialization ID.
249
+ sendInitError({ initializationId, error })
250
+ return "initErrorSent"
237
251
  }
238
252
 
239
- function handleInvalidInitResult(): boolean {
240
- if (initStatus !== "waitingForInitResult") {
241
- return false
253
+ function handleInvalidInitResult(args: {
254
+ initializationId: string | undefined
255
+ error: CustomBlockInitErrorInfo
256
+ }): void {
257
+ const { initializationId, error } = args
258
+ if (
259
+ initStatus !== "waitingForInitResult" ||
260
+ initializationId === undefined ||
261
+ initializationId !== activeInitializationId
262
+ ) {
263
+ return
242
264
  }
265
+ clearNoConnectTimeout()
243
266
  clearNoInitResultTimeout()
244
267
  initStatus = "error"
245
- const error = initErrorForFailureReason("invalid_init_result_payload")
246
268
  warn(error)
247
269
  onInitializationFailure?.(error)
248
- return true
249
270
  }
250
271
 
251
272
  function onIframeLoad() {