@notionhq/custom-blocks 0.0.58

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 (298) hide show
  1. package/HOST.md +171 -0
  2. package/README.md +57 -0
  3. package/bin/cli/attach.js +180 -0
  4. package/bin/cli/cli.js +248 -0
  5. package/bin/cli/create.js +106 -0
  6. package/bin/cli/datasources.js +255 -0
  7. package/bin/cli/deploy.js +109 -0
  8. package/bin/cli/ids.js +13 -0
  9. package/bin/cli/ntn.js +95 -0
  10. package/bin/cli/pullData.js +76 -0
  11. package/bin/cli/pullManifest.js +158 -0
  12. package/bin/cli/target.js +95 -0
  13. package/bin/src/bridge/dataSources/propertySchema.js +148 -0
  14. package/bin/src/bridge/manifest.js +40 -0
  15. package/dist/autoResize.d.ts +4 -0
  16. package/dist/autoResize.d.ts.map +1 -0
  17. package/dist/autoResize.js +71 -0
  18. package/dist/bridge/SandboxBridge.d.ts +92 -0
  19. package/dist/bridge/SandboxBridge.d.ts.map +1 -0
  20. package/dist/bridge/SandboxBridge.js +718 -0
  21. package/dist/bridge/dataSources/dataSource.d.ts +732 -0
  22. package/dist/bridge/dataSources/dataSource.d.ts.map +1 -0
  23. package/dist/bridge/dataSources/dataSource.js +37 -0
  24. package/dist/bridge/dataSources/dataSourcePage.d.ts +118 -0
  25. package/dist/bridge/dataSources/dataSourcePage.d.ts.map +1 -0
  26. package/dist/bridge/dataSources/dataSourcePage.js +14 -0
  27. package/dist/bridge/dataSources/dataSourceValue.d.ts +67 -0
  28. package/dist/bridge/dataSources/dataSourceValue.d.ts.map +1 -0
  29. package/dist/bridge/dataSources/dataSourceValue.js +14 -0
  30. package/dist/bridge/dataSources/dateValue.d.ts +158 -0
  31. package/dist/bridge/dataSources/dateValue.d.ts.map +1 -0
  32. package/dist/bridge/dataSources/dateValue.js +59 -0
  33. package/dist/bridge/dataSources/errors.d.ts +6 -0
  34. package/dist/bridge/dataSources/errors.d.ts.map +1 -0
  35. package/dist/bridge/dataSources/errors.js +4 -0
  36. package/dist/bridge/dataSources/propertySchema.d.ts +191 -0
  37. package/dist/bridge/dataSources/propertySchema.d.ts.map +1 -0
  38. package/dist/bridge/dataSources/propertySchema.js +148 -0
  39. package/dist/bridge/dataSources/recordPointer.d.ts +10 -0
  40. package/dist/bridge/dataSources/recordPointer.d.ts.map +1 -0
  41. package/dist/bridge/dataSources/recordPointer.js +8 -0
  42. package/dist/bridge/dataSources/resolve.d.ts +22 -0
  43. package/dist/bridge/dataSources/resolve.d.ts.map +1 -0
  44. package/dist/bridge/dataSources/resolve.js +65 -0
  45. package/dist/bridge/dataSources/resolveProperty.d.ts +41 -0
  46. package/dist/bridge/dataSources/resolveProperty.d.ts.map +1 -0
  47. package/dist/bridge/dataSources/resolveProperty.js +73 -0
  48. package/dist/bridge/errors.d.ts +12 -0
  49. package/dist/bridge/errors.d.ts.map +1 -0
  50. package/dist/bridge/errors.js +6 -0
  51. package/dist/bridge/hostState.d.ts +65 -0
  52. package/dist/bridge/hostState.d.ts.map +1 -0
  53. package/dist/bridge/hostState.js +65 -0
  54. package/dist/bridge/ids.d.ts +39 -0
  55. package/dist/bridge/ids.d.ts.map +1 -0
  56. package/dist/bridge/ids.js +5 -0
  57. package/dist/bridge/incomingType.d.ts +10 -0
  58. package/dist/bridge/incomingType.d.ts.map +1 -0
  59. package/dist/bridge/incomingType.js +17 -0
  60. package/dist/bridge/loadManifest.d.ts +25 -0
  61. package/dist/bridge/loadManifest.d.ts.map +1 -0
  62. package/dist/bridge/loadManifest.js +68 -0
  63. package/dist/bridge/manifest.d.ts +66 -0
  64. package/dist/bridge/manifest.d.ts.map +1 -0
  65. package/dist/bridge/manifest.js +40 -0
  66. package/dist/bridge/messages/createPage.d.ts +225 -0
  67. package/dist/bridge/messages/createPage.d.ts.map +1 -0
  68. package/dist/bridge/messages/createPage.js +46 -0
  69. package/dist/bridge/messages/createPageResult.d.ts +211 -0
  70. package/dist/bridge/messages/createPageResult.d.ts.map +1 -0
  71. package/dist/bridge/messages/createPageResult.js +28 -0
  72. package/dist/bridge/messages/currentUserChanged.d.ts +19 -0
  73. package/dist/bridge/messages/currentUserChanged.d.ts.map +1 -0
  74. package/dist/bridge/messages/currentUserChanged.js +9 -0
  75. package/dist/bridge/messages/dataSourcesChanged.d.ts +158 -0
  76. package/dist/bridge/messages/dataSourcesChanged.d.ts.map +1 -0
  77. package/dist/bridge/messages/dataSourcesChanged.js +13 -0
  78. package/dist/bridge/messages/getPage.d.ts +216 -0
  79. package/dist/bridge/messages/getPage.d.ts.map +1 -0
  80. package/dist/bridge/messages/getPage.js +33 -0
  81. package/dist/bridge/messages/getUser.d.ts +45 -0
  82. package/dist/bridge/messages/getUser.d.ts.map +1 -0
  83. package/dist/bridge/messages/getUser.js +32 -0
  84. package/dist/bridge/messages/hostToSandbox.d.ts +1089 -0
  85. package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -0
  86. package/dist/bridge/messages/hostToSandbox.js +32 -0
  87. package/dist/bridge/messages/init.d.ts +209 -0
  88. package/dist/bridge/messages/init.d.ts.map +1 -0
  89. package/dist/bridge/messages/init.js +47 -0
  90. package/dist/bridge/messages/invalidHostMessage.d.ts +15 -0
  91. package/dist/bridge/messages/invalidHostMessage.d.ts.map +1 -0
  92. package/dist/bridge/messages/invalidHostMessage.js +13 -0
  93. package/dist/bridge/messages/invalidSandboxMessage.d.ts +15 -0
  94. package/dist/bridge/messages/invalidSandboxMessage.d.ts.map +1 -0
  95. package/dist/bridge/messages/invalidSandboxMessage.js +13 -0
  96. package/dist/bridge/messages/listUsers.d.ts +53 -0
  97. package/dist/bridge/messages/listUsers.d.ts.map +1 -0
  98. package/dist/bridge/messages/listUsers.js +33 -0
  99. package/dist/bridge/messages/pageChanged.d.ts +12 -0
  100. package/dist/bridge/messages/pageChanged.d.ts.map +1 -0
  101. package/dist/bridge/messages/pageChanged.js +9 -0
  102. package/dist/bridge/messages/parentChanged.d.ts +22 -0
  103. package/dist/bridge/messages/parentChanged.d.ts.map +1 -0
  104. package/dist/bridge/messages/parentChanged.js +9 -0
  105. package/dist/bridge/messages/queryDataSource.d.ts +20 -0
  106. package/dist/bridge/messages/queryDataSource.d.ts.map +1 -0
  107. package/dist/bridge/messages/queryDataSource.js +19 -0
  108. package/dist/bridge/messages/queryDataSourceResult.d.ts +90 -0
  109. package/dist/bridge/messages/queryDataSourceResult.d.ts.map +1 -0
  110. package/dist/bridge/messages/queryDataSourceResult.js +21 -0
  111. package/dist/bridge/messages/ready.d.ts +45 -0
  112. package/dist/bridge/messages/ready.d.ts.map +1 -0
  113. package/dist/bridge/messages/ready.js +37 -0
  114. package/dist/bridge/messages/resize.d.ts +12 -0
  115. package/dist/bridge/messages/resize.d.ts.map +1 -0
  116. package/dist/bridge/messages/resize.js +10 -0
  117. package/dist/bridge/messages/sandboxToHost.d.ts +410 -0
  118. package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -0
  119. package/dist/bridge/messages/sandboxToHost.js +25 -0
  120. package/dist/bridge/messages/themeChanged.d.ts +11 -0
  121. package/dist/bridge/messages/themeChanged.d.ts.map +1 -0
  122. package/dist/bridge/messages/themeChanged.js +10 -0
  123. package/dist/bridge/messages/updatePage.d.ts +171 -0
  124. package/dist/bridge/messages/updatePage.d.ts.map +1 -0
  125. package/dist/bridge/messages/updatePage.js +15 -0
  126. package/dist/bridge/messages/updatePageResult.d.ts +210 -0
  127. package/dist/bridge/messages/updatePageResult.d.ts.map +1 -0
  128. package/dist/bridge/messages/updatePageResult.js +27 -0
  129. package/dist/bridge/pages/page.d.ts +600 -0
  130. package/dist/bridge/pages/page.d.ts.map +1 -0
  131. package/dist/bridge/pages/page.js +217 -0
  132. package/dist/bridge/parent.d.ts +25 -0
  133. package/dist/bridge/parent.d.ts.map +1 -0
  134. package/dist/bridge/parent.js +11 -0
  135. package/dist/bridge/pendingRequests.d.ts +14 -0
  136. package/dist/bridge/pendingRequests.d.ts.map +1 -0
  137. package/dist/bridge/pendingRequests.js +27 -0
  138. package/dist/bridge/sandboxClient.d.ts +63 -0
  139. package/dist/bridge/sandboxClient.d.ts.map +1 -0
  140. package/dist/bridge/sandboxClient.js +92 -0
  141. package/dist/bridge/theme.d.ts +4 -0
  142. package/dist/bridge/theme.d.ts.map +1 -0
  143. package/dist/bridge/theme.js +2 -0
  144. package/dist/bridge/users/user.d.ts +36 -0
  145. package/dist/bridge/users/user.d.ts.map +1 -0
  146. package/dist/bridge/users/user.js +19 -0
  147. package/dist/customBlock.d.ts +36 -0
  148. package/dist/customBlock.d.ts.map +1 -0
  149. package/dist/customBlock.js +65 -0
  150. package/dist/host/createCustomBlockHost.d.ts +34 -0
  151. package/dist/host/createCustomBlockHost.d.ts.map +1 -0
  152. package/dist/host/createCustomBlockHost.js +278 -0
  153. package/dist/host/lifecycle/initErrors.d.ts +10 -0
  154. package/dist/host/lifecycle/initErrors.d.ts.map +1 -0
  155. package/dist/host/lifecycle/initErrors.js +123 -0
  156. package/dist/host/lifecycle/ready.d.ts +8 -0
  157. package/dist/host/lifecycle/ready.d.ts.map +1 -0
  158. package/dist/host/lifecycle/ready.js +24 -0
  159. package/dist/host/lifecycle/types.d.ts +24 -0
  160. package/dist/host/lifecycle/types.d.ts.map +1 -0
  161. package/dist/host/lifecycle/types.js +1 -0
  162. package/dist/host/messages/invalidSandboxMessage.d.ts +7 -0
  163. package/dist/host/messages/invalidSandboxMessage.d.ts.map +1 -0
  164. package/dist/host/messages/invalidSandboxMessage.js +24 -0
  165. package/dist/host/messages/types.d.ts +60 -0
  166. package/dist/host/messages/types.d.ts.map +1 -0
  167. package/dist/host/messages/types.js +1 -0
  168. package/dist/host/queries/querySubscriptions.d.ts +20 -0
  169. package/dist/host/queries/querySubscriptions.d.ts.map +1 -0
  170. package/dist/host/queries/querySubscriptions.js +29 -0
  171. package/dist/host/queries/types.d.ts +9 -0
  172. package/dist/host/queries/types.d.ts.map +1 -0
  173. package/dist/host/queries/types.js +1 -0
  174. package/dist/host.d.ts +73 -0
  175. package/dist/host.d.ts.map +1 -0
  176. package/dist/host.js +42 -0
  177. package/dist/index.d.ts +37 -0
  178. package/dist/index.d.ts.map +1 -0
  179. package/dist/index.js +14 -0
  180. package/dist/init.d.ts +53 -0
  181. package/dist/init.d.ts.map +1 -0
  182. package/dist/init.js +87 -0
  183. package/dist/react/DebugMessageLog.d.ts +2 -0
  184. package/dist/react/DebugMessageLog.d.ts.map +1 -0
  185. package/dist/react/DebugMessageLog.js +22 -0
  186. package/dist/react/NotionCustomBlock.css +110 -0
  187. package/dist/react/NotionCustomBlock.d.ts +50 -0
  188. package/dist/react/NotionCustomBlock.d.ts.map +1 -0
  189. package/dist/react/NotionCustomBlock.js +81 -0
  190. package/dist/react/index.d.ts +14 -0
  191. package/dist/react/index.d.ts.map +1 -0
  192. package/dist/react/index.js +13 -0
  193. package/dist/react/standalonePreview.d.ts +2 -0
  194. package/dist/react/standalonePreview.d.ts.map +1 -0
  195. package/dist/react/standalonePreview.js +26 -0
  196. package/dist/react/useCustomBlockAutoResize.d.ts +30 -0
  197. package/dist/react/useCustomBlockAutoResize.d.ts.map +1 -0
  198. package/dist/react/useCustomBlockAutoResize.js +35 -0
  199. package/dist/react/useCustomBlockInit.d.ts +43 -0
  200. package/dist/react/useCustomBlockInit.d.ts.map +1 -0
  201. package/dist/react/useCustomBlockInit.js +60 -0
  202. package/dist/react/useDataSource.d.ts +13 -0
  203. package/dist/react/useDataSource.d.ts.map +1 -0
  204. package/dist/react/useDataSource.js +37 -0
  205. package/dist/react/useHostState.d.ts +2 -0
  206. package/dist/react/useHostState.d.ts.map +1 -0
  207. package/dist/react/useHostState.js +5 -0
  208. package/dist/react/useRuntimeState.d.ts +66 -0
  209. package/dist/react/useRuntimeState.d.ts.map +1 -0
  210. package/dist/react/useRuntimeState.js +73 -0
  211. package/dist/types.d.ts +154 -0
  212. package/dist/types.d.ts.map +1 -0
  213. package/dist/types.js +1 -0
  214. package/dist/utils.d.ts +9 -0
  215. package/dist/utils.d.ts.map +1 -0
  216. package/dist/utils.js +10 -0
  217. package/dist/version.d.ts +9 -0
  218. package/dist/version.d.ts.map +1 -0
  219. package/dist/version.js +7 -0
  220. package/docs/block-location.md +83 -0
  221. package/docs/data-sources.md +186 -0
  222. package/docs/lifecycle.md +172 -0
  223. package/docs/manifest.md +42 -0
  224. package/docs/pages.md +143 -0
  225. package/docs/users.md +92 -0
  226. package/package.json +69 -0
  227. package/src/autoResize.ts +84 -0
  228. package/src/bridge/SandboxBridge.ts +921 -0
  229. package/src/bridge/dataSources/dataSource.ts +63 -0
  230. package/src/bridge/dataSources/dataSourcePage.ts +71 -0
  231. package/src/bridge/dataSources/dataSourceValue.ts +19 -0
  232. package/src/bridge/dataSources/dateValue.ts +96 -0
  233. package/src/bridge/dataSources/errors.ts +14 -0
  234. package/src/bridge/dataSources/propertySchema.ts +186 -0
  235. package/src/bridge/dataSources/recordPointer.ts +13 -0
  236. package/src/bridge/dataSources/resolve.ts +99 -0
  237. package/src/bridge/dataSources/resolveProperty.ts +130 -0
  238. package/src/bridge/errors.ts +13 -0
  239. package/src/bridge/hostState.ts +154 -0
  240. package/src/bridge/ids.ts +58 -0
  241. package/src/bridge/incomingType.ts +19 -0
  242. package/src/bridge/loadManifest.ts +89 -0
  243. package/src/bridge/manifest.ts +53 -0
  244. package/src/bridge/messages/createPage.ts +67 -0
  245. package/src/bridge/messages/createPageResult.ts +52 -0
  246. package/src/bridge/messages/currentUserChanged.ts +14 -0
  247. package/src/bridge/messages/dataSourcesChanged.ts +18 -0
  248. package/src/bridge/messages/getPage.ts +52 -0
  249. package/src/bridge/messages/getUser.ts +48 -0
  250. package/src/bridge/messages/hostToSandbox.ts +37 -0
  251. package/src/bridge/messages/init.ts +81 -0
  252. package/src/bridge/messages/invalidHostMessage.ts +16 -0
  253. package/src/bridge/messages/invalidSandboxMessage.ts +18 -0
  254. package/src/bridge/messages/listUsers.ts +49 -0
  255. package/src/bridge/messages/pageChanged.ts +12 -0
  256. package/src/bridge/messages/parentChanged.ts +14 -0
  257. package/src/bridge/messages/queryDataSource.ts +24 -0
  258. package/src/bridge/messages/queryDataSourceResult.ts +38 -0
  259. package/src/bridge/messages/ready.ts +41 -0
  260. package/src/bridge/messages/resize.ts +13 -0
  261. package/src/bridge/messages/sandboxToHost.ts +30 -0
  262. package/src/bridge/messages/themeChanged.ts +15 -0
  263. package/src/bridge/messages/updatePage.ts +22 -0
  264. package/src/bridge/messages/updatePageResult.ts +49 -0
  265. package/src/bridge/pages/page.ts +281 -0
  266. package/src/bridge/parent.ts +27 -0
  267. package/src/bridge/pendingRequests.ts +28 -0
  268. package/src/bridge/sandboxClient.ts +137 -0
  269. package/src/bridge/theme.ts +5 -0
  270. package/src/bridge/users/user.ts +31 -0
  271. package/src/css.d.ts +1 -0
  272. package/src/customBlock.ts +102 -0
  273. package/src/host/createCustomBlockHost.ts +392 -0
  274. package/src/host/lifecycle/initErrors.ts +132 -0
  275. package/src/host/lifecycle/ready.ts +36 -0
  276. package/src/host/lifecycle/types.ts +28 -0
  277. package/src/host/messages/invalidSandboxMessage.ts +38 -0
  278. package/src/host/messages/types.ts +67 -0
  279. package/src/host/queries/querySubscriptions.ts +52 -0
  280. package/src/host/queries/types.ts +16 -0
  281. package/src/host.ts +167 -0
  282. package/src/index.ts +115 -0
  283. package/src/init.ts +143 -0
  284. package/src/react/DebugMessageLog.tsx +30 -0
  285. package/src/react/NotionCustomBlock.css +110 -0
  286. package/src/react/NotionCustomBlock.tsx +151 -0
  287. package/src/react/index.ts +28 -0
  288. package/src/react/standalonePreview.ts +28 -0
  289. package/src/react/useCustomBlockAutoResize.ts +46 -0
  290. package/src/react/useCustomBlockInit.ts +91 -0
  291. package/src/react/useDataSource.ts +47 -0
  292. package/src/react/useHostState.ts +9 -0
  293. package/src/react/useRuntimeState.ts +85 -0
  294. package/src/types.ts +175 -0
  295. package/src/utils.ts +13 -0
  296. package/src/version.ts +8 -0
  297. package/vite-plugin/index.d.ts +46 -0
  298. package/vite-plugin/index.js +115 -0
@@ -0,0 +1,718 @@
1
+ import * as v from "valibot";
2
+ import { unreachable } from "../utils.js";
3
+ import { NCBLOCK_SDK_VERSION } from "../version.js";
4
+ import { resolveDataSources } from "./dataSources/resolve.js";
5
+ import { resolvePropertyWriteMapForDataSource } from "./dataSources/resolveProperty.js";
6
+ import { createEmptyDataSourceQueryState, } from "./hostState.js";
7
+ import { readIncomingType } from "./incomingType.js";
8
+ import { hostToSandboxMessageSchema } from "./messages/hostToSandbox.js";
9
+ import { CustomBlockInitError } from "./messages/init.js";
10
+ import { PendingRequests } from "./pendingRequests.js";
11
+ /**
12
+ * Used to ensure that the host and client are using the same version of the bridge protocol. A
13
+ * single host needs to support multiple custom blocks built with different versions of the bridge
14
+ * protocol. Increment this number any time a breaking change is made to the bridge protocol.
15
+ */
16
+ export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 2;
17
+ export class SandboxBridge {
18
+ constructor() {
19
+ this.hostState = {
20
+ status: "uninitialized",
21
+ theme: "light",
22
+ };
23
+ this.listeners = new Set();
24
+ this.messageLog = [];
25
+ this.messageLogListeners = new Set();
26
+ this.nextRequestId = 1;
27
+ this.pendingCreatePage = new PendingRequests("custom-block-create-page");
28
+ this.pendingGetPage = new PendingRequests("custom-block-get-page");
29
+ this.pendingGetUser = new PendingRequests("custom-block-get-user");
30
+ this.pendingListUsers = new PendingRequests("custom-block-list-users");
31
+ this.pendingUpdatePage = new PendingRequests("custom-block-update-page");
32
+ this.hasSentReady = false;
33
+ this.latestDataSourceBindings = {};
34
+ this.initMessage = new Promise((resolve, reject) => {
35
+ this.resolveInit = resolve;
36
+ this.rejectInit = reject;
37
+ });
38
+ this.manifest = null;
39
+ this.notify = () => {
40
+ for (const listener of this.listeners) {
41
+ listener();
42
+ }
43
+ };
44
+ this.handleMessage = (event) => {
45
+ console.debug("[notion-custom-sdk] incoming postMessage", {
46
+ data: event.data,
47
+ fromParent: event.source === window.parent,
48
+ });
49
+ if (event.source !== window.parent) {
50
+ return;
51
+ }
52
+ this.logMessage("received", event.data);
53
+ const parsed = v.safeParse(hostToSandboxMessageSchema, event.data);
54
+ if (!parsed.success) {
55
+ console.warn("[notion-custom-sdk] ignoring malformed host message", parsed.issues);
56
+ const incomingType = readIncomingType(event.data);
57
+ // Loop guard: never NACK a NACK. Excludes both directions —
58
+ // `invalidSandboxMessage` is what the host normally sends, but
59
+ // a buggy host that echoes our own `invalidHostMessage` back
60
+ // would otherwise spin a NACK feedback loop.
61
+ if (incomingType !== "invalidSandboxMessage" &&
62
+ incomingType !== "invalidHostMessage") {
63
+ const nack = {
64
+ type: "invalidHostMessage",
65
+ reason: formatInvalidHostReason(incomingType, parsed.issues),
66
+ };
67
+ this.postToHost(nack);
68
+ }
69
+ return;
70
+ }
71
+ const message = parsed.output;
72
+ // If the host couldn't parse one of our outbound sandbox-to-host messages, it sends back
73
+ // an `invalidSandboxMessage` NACK with a human-readable `reason`. Log it for developer
74
+ // visibility and stop. We don't retry as the sandbox can't recover from a host-side parse
75
+ // failure on its own.
76
+ if (message.type === "invalidSandboxMessage") {
77
+ console.warn("[notion-custom-sdk] host reported invalid sandbox message:", message.reason);
78
+ return;
79
+ }
80
+ // `init` is the only message valid before initialization. Handle it up
81
+ // front so every later case can assume `status === "initialized"`.
82
+ if (message.type === "init") {
83
+ this.applyInit(message);
84
+ return;
85
+ }
86
+ // Alias to keep TS's narrowed `InitializedHostState` across the switch
87
+ // below. Reading `this.hostState` repeatedly would re-widen it.
88
+ const hostState = this.hostState;
89
+ if (hostState.status !== "initialized") {
90
+ console.warn(`[notion-custom-sdk] ignoring ${message.type} before init`);
91
+ return;
92
+ }
93
+ switch (message.type) {
94
+ case "themeChanged": {
95
+ this.hostState = {
96
+ ...hostState,
97
+ theme: message.theme,
98
+ };
99
+ this.notify();
100
+ return;
101
+ }
102
+ case "parentChanged": {
103
+ this.hostState = {
104
+ ...hostState,
105
+ parent: message.parent,
106
+ };
107
+ this.notify();
108
+ return;
109
+ }
110
+ case "pageChanged": {
111
+ this.hostState = {
112
+ ...hostState,
113
+ page: message.page,
114
+ };
115
+ this.notify();
116
+ return;
117
+ }
118
+ case "currentUserChanged": {
119
+ this.hostState = {
120
+ ...hostState,
121
+ currentUser: message.currentUser,
122
+ };
123
+ this.notify();
124
+ return;
125
+ }
126
+ case "dataSourcesChanged": {
127
+ const nextBindings = message.dataSources.bindings;
128
+ const dataSources = reuseDataSourcesForUnchangedBindings({
129
+ previousDataSources: hostState.dataSources,
130
+ previousBindings: this.latestDataSourceBindings,
131
+ nextDataSources: resolveDataSources({
132
+ manifest: this.manifest,
133
+ dataSourceBindings: nextBindings,
134
+ }),
135
+ nextBindings,
136
+ });
137
+ this.latestDataSourceBindings = nextBindings;
138
+ // Drop cached query state for keys that no longer exist in the mapping.
139
+ const nextKeys = new Set(dataSources.map(s => s.key));
140
+ const prunedState = {};
141
+ for (const [key, state] of Object.entries(hostState.dataSourceState)) {
142
+ if (nextKeys.has(key)) {
143
+ prunedState[key] = state;
144
+ }
145
+ }
146
+ this.hostState = {
147
+ ...hostState,
148
+ dataSources,
149
+ dataSourceState: prunedState,
150
+ };
151
+ this.notify();
152
+ return;
153
+ }
154
+ case "createPageResult": {
155
+ const result = message.status === "success"
156
+ ? { status: "success", page: message.page }
157
+ : { status: "error", error: message.error };
158
+ if (!this.pendingCreatePage.resolve(message.requestId, result)) {
159
+ console.warn(`[notion-custom-sdk] createPageResult for unknown requestId ${message.requestId}`);
160
+ }
161
+ return;
162
+ }
163
+ case "getPageResult": {
164
+ const result = message.status === "success"
165
+ ? { status: "success", page: message.page }
166
+ : { status: "error", error: message.error };
167
+ if (!this.pendingGetPage.resolve(message.requestId, result)) {
168
+ console.warn(`[notion-custom-sdk] getPageResult for unknown requestId ${message.requestId}`);
169
+ }
170
+ return;
171
+ }
172
+ case "getUserResult": {
173
+ const result = message.status === "success"
174
+ ? { status: "success", user: message.user }
175
+ : { status: "error", error: message.error };
176
+ if (!this.pendingGetUser.resolve(message.requestId, result)) {
177
+ console.warn(`[notion-custom-sdk] getUserResult for unknown requestId ${message.requestId}`);
178
+ }
179
+ return;
180
+ }
181
+ case "listUsersResult": {
182
+ const result = message.status === "success"
183
+ ? { status: "success", list: message.list }
184
+ : { status: "error", error: message.error };
185
+ if (!this.pendingListUsers.resolve(message.requestId, result)) {
186
+ console.warn(`[notion-custom-sdk] listUsersResult for unknown requestId ${message.requestId}`);
187
+ }
188
+ return;
189
+ }
190
+ case "updatePageResult": {
191
+ const result = message.status === "success"
192
+ ? { status: "success", page: message.page }
193
+ : { status: "error", error: message.error };
194
+ if (!this.pendingUpdatePage.resolve(message.requestId, result)) {
195
+ console.warn(`[notion-custom-sdk] updatePageResult for unknown requestId ${message.requestId}`);
196
+ }
197
+ return;
198
+ }
199
+ case "queryDataSourceResult": {
200
+ const queryEntry = Object.entries(hostState.dataSourceState).find(([, state]) => state.latestSnapshotId === message.snapshotId);
201
+ if (queryEntry === undefined) {
202
+ return;
203
+ }
204
+ const [key, currentState] = queryEntry;
205
+ if (currentState.latestRequestId !== message.requestId) {
206
+ return;
207
+ }
208
+ this.hostState = {
209
+ ...hostState,
210
+ dataSourceState: {
211
+ ...hostState.dataSourceState,
212
+ [key]: {
213
+ items: message.items,
214
+ isLoading: false,
215
+ hasMore: message.hasMore,
216
+ error: message.error,
217
+ // Keep the request ID so later host-pushed refreshes for the
218
+ // same subscription still match.
219
+ latestRequestId: message.requestId,
220
+ latestSnapshotId: message.snapshotId,
221
+ latestLimit: currentState.latestLimit,
222
+ },
223
+ },
224
+ };
225
+ this.notify();
226
+ return;
227
+ }
228
+ default: {
229
+ unreachable(message);
230
+ }
231
+ }
232
+ };
233
+ // `ready` is sent later by `initCustomBlock` (after the manifest fetch
234
+ // resolves). Top-level / no-iframe rejection is handled there too, so
235
+ // the constructor just attaches the listener.
236
+ if (typeof window !== "undefined") {
237
+ window.addEventListener("message", this.handleMessage);
238
+ }
239
+ }
240
+ logMessage(direction, data) {
241
+ if (this.messageLog.length >= SandboxBridge.MAX_LOG_ENTRIES) {
242
+ this.messageLog.shift();
243
+ }
244
+ this.messageLog.push({
245
+ timestamp: new Date().toISOString(),
246
+ direction,
247
+ data,
248
+ });
249
+ for (const listener of this.messageLogListeners) {
250
+ listener();
251
+ }
252
+ }
253
+ getMessageLog() {
254
+ return this.messageLog;
255
+ }
256
+ subscribeToMessageLog(listener) {
257
+ this.messageLogListeners.add(listener);
258
+ return () => this.messageLogListeners.delete(listener);
259
+ }
260
+ awaitInit(signal) {
261
+ if (!signal) {
262
+ return this.initMessage;
263
+ }
264
+ return new Promise((resolve, reject) => {
265
+ if (signal.aborted) {
266
+ reject(signal.reason);
267
+ return;
268
+ }
269
+ const onAbort = () => reject(signal.reason);
270
+ signal.addEventListener("abort", onAbort, { once: true });
271
+ this.initMessage.then(message => {
272
+ signal.removeEventListener("abort", onAbort);
273
+ resolve(message);
274
+ }, err => {
275
+ signal.removeEventListener("abort", onAbort);
276
+ reject(err);
277
+ });
278
+ });
279
+ }
280
+ sendReady(manifestResult) {
281
+ if (typeof window === "undefined") {
282
+ return;
283
+ }
284
+ if (this.hasSentReady) {
285
+ console.warn("[notion-custom-sdk] ignoring duplicate ready message");
286
+ return;
287
+ }
288
+ const { manifest, error } = manifestResult;
289
+ this.hasSentReady = true;
290
+ this.manifest = manifest;
291
+ const readyMessage = error !== undefined
292
+ ? {
293
+ type: "ready",
294
+ status: "error",
295
+ bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
296
+ sdkVersion: NCBLOCK_SDK_VERSION,
297
+ error,
298
+ }
299
+ : {
300
+ type: "ready",
301
+ status: "success",
302
+ bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
303
+ sdkVersion: NCBLOCK_SDK_VERSION,
304
+ manifest,
305
+ };
306
+ this.postToHost(readyMessage);
307
+ }
308
+ postToHost(message) {
309
+ console.debug("[notion-custom-sdk] outbound postMessage", message);
310
+ this.logMessage("sent", message);
311
+ window.parent.postMessage(message, "*");
312
+ }
313
+ subscribe(listener) {
314
+ this.listeners.add(listener);
315
+ return () => this.listeners.delete(listener);
316
+ }
317
+ getHostState() {
318
+ return this.hostState;
319
+ }
320
+ /**
321
+ * The author-declared manifest loaded from `custom_blocks.json` and forwarded
322
+ * to the host in `ready`. `null` when it failed to load/parse and the host
323
+ * should reject init via `ready.status: "error"`. Static for the lifetime
324
+ * of the sandbox.
325
+ */
326
+ getManifest() {
327
+ return this.manifest;
328
+ }
329
+ /**
330
+ * Apply an `init` payload as if it had arrived from the host. Lets callers
331
+ * seed the bridge directly (e.g. the React provider's standalone preview
332
+ * fallback) without going through `postMessage`. The bridge stays unaware
333
+ * of why it's being seeded.
334
+ */
335
+ setMockState(message) {
336
+ this.applyInit(message);
337
+ }
338
+ applyInit(message) {
339
+ if (message.status === "error") {
340
+ // The host couldn't construct block location for this block (most commonly the parent record
341
+ // failed to resolve). Surface the failure through the `awaitInit` promise so callers see
342
+ // it through `useCustomBlockInit().error` instead of timing out. We also log here
343
+ // because not every consumer renders the error UI, and we want the failure visible in
344
+ // the browser console either way. We deliberately don't NACK the host or post anything
345
+ // back over the bridge since the host has already given up.
346
+ console.error(`[notion-custom-sdk] host reported init error (${message.error.code}): ${message.error.message}`);
347
+ if (this.rejectInit) {
348
+ this.rejectInit(new CustomBlockInitError(message.error));
349
+ this.resolveInit = undefined;
350
+ this.rejectInit = undefined;
351
+ }
352
+ return;
353
+ }
354
+ const { blockId, parent, page } = message;
355
+ this.latestDataSourceBindings = message.dataSources.bindings;
356
+ const dataSources = resolveDataSources({
357
+ manifest: this.manifest,
358
+ dataSourceBindings: this.latestDataSourceBindings,
359
+ });
360
+ this.hostState = {
361
+ status: "initialized",
362
+ theme: message.theme,
363
+ blockId,
364
+ parent,
365
+ page,
366
+ currentUser: message.currentUser,
367
+ dataSources,
368
+ dataSourceState: {},
369
+ };
370
+ this.notify();
371
+ // Resolve the awaitInit promise once. Subsequent `init` messages
372
+ // (the host shouldn't send these, but be tolerant) update state but
373
+ // don't re-resolve.
374
+ if (this.resolveInit) {
375
+ this.resolveInit(message);
376
+ this.resolveInit = undefined;
377
+ this.rejectInit = undefined;
378
+ }
379
+ }
380
+ queryDataSource(key, options = {}) {
381
+ if (this.hostState.status !== "initialized") {
382
+ return;
383
+ }
384
+ const dataSource = this.hostState.dataSources.find(entry => entry.key === key);
385
+ const currentState = this.hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState();
386
+ if (dataSource === undefined) {
387
+ this.setDataSourceQueryError(key, currentState, {
388
+ code: "unknown_data_source_key",
389
+ message: `Unknown data source key "${key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
390
+ isRetryable: false,
391
+ });
392
+ return;
393
+ }
394
+ if (dataSource.collectionPointer === undefined) {
395
+ this.setDataSourceQueryError(key, currentState, {
396
+ code: "unmapped_data_source",
397
+ message: `Data source "${key}" has not been mapped to a database yet.`,
398
+ isRetryable: false,
399
+ });
400
+ return;
401
+ }
402
+ const limit = resolveDataSourceQueryLimit(options.limit);
403
+ const snapshotId = makeDataSourceSnapshotId({
404
+ key,
405
+ });
406
+ if (currentState.isLoading &&
407
+ currentState.latestSnapshotId === snapshotId &&
408
+ currentState.latestLimit === limit) {
409
+ return;
410
+ }
411
+ const requestId = `custom-block-query-${this.nextRequestId}`;
412
+ this.nextRequestId += 1;
413
+ this.hostState = {
414
+ ...this.hostState,
415
+ dataSourceState: {
416
+ ...this.hostState.dataSourceState,
417
+ [key]: {
418
+ ...currentState,
419
+ isLoading: true,
420
+ error: undefined,
421
+ latestRequestId: requestId,
422
+ latestSnapshotId: snapshotId,
423
+ latestLimit: limit,
424
+ },
425
+ },
426
+ };
427
+ this.notify();
428
+ const outbound = {
429
+ type: "queryDataSource",
430
+ requestId,
431
+ snapshotId,
432
+ dataSourceId: dataSource.collectionPointer.id,
433
+ limit,
434
+ };
435
+ this.postToHost(outbound);
436
+ }
437
+ setDataSourceQueryError(key, currentState, error) {
438
+ if (this.hostState.status !== "initialized") {
439
+ return;
440
+ }
441
+ this.hostState = {
442
+ ...this.hostState,
443
+ dataSourceState: {
444
+ ...this.hostState.dataSourceState,
445
+ [key]: {
446
+ ...currentState,
447
+ isLoading: false,
448
+ error,
449
+ latestRequestId: undefined,
450
+ latestSnapshotId: undefined,
451
+ latestLimit: undefined,
452
+ },
453
+ },
454
+ };
455
+ this.notify();
456
+ }
457
+ postResize(height) {
458
+ if (typeof window === "undefined") {
459
+ return;
460
+ }
461
+ const safeHeight = Number.isFinite(height) && height >= 0 ? Math.ceil(height) : 0;
462
+ const outbound = {
463
+ type: "resize",
464
+ height: safeHeight,
465
+ };
466
+ this.postToHost(outbound);
467
+ }
468
+ createPage(input) {
469
+ return new Promise(resolve => {
470
+ const resolvedParent = this.resolveCreatePageParent(input.parent);
471
+ if (resolvedParent.status === "error") {
472
+ resolve(resolvedParent);
473
+ return;
474
+ }
475
+ const resolvedProperties = resolvePropertyWriteMapForDataSource({
476
+ dataSource: resolvedParent.dataSource,
477
+ properties: input.properties,
478
+ operationName: "createPage",
479
+ });
480
+ if (resolvedProperties.status === "error") {
481
+ resolve(resolvedProperties);
482
+ return;
483
+ }
484
+ const requestId = this.pendingCreatePage.allocate(resolve);
485
+ const outbound = {
486
+ type: "createPage",
487
+ requestId,
488
+ parent: resolvedParent.parent,
489
+ properties: resolvedProperties.properties,
490
+ };
491
+ if (input.icon !== undefined) {
492
+ outbound.icon = input.icon;
493
+ }
494
+ if (input.cover !== undefined) {
495
+ outbound.cover = input.cover;
496
+ }
497
+ if (input.position !== undefined) {
498
+ outbound.position = input.position;
499
+ }
500
+ this.postToHost(outbound);
501
+ });
502
+ }
503
+ getPage(pageId) {
504
+ return new Promise(resolve => {
505
+ const requestId = this.pendingGetPage.allocate(resolve);
506
+ const outbound = {
507
+ type: "getPage",
508
+ requestId,
509
+ pageId,
510
+ };
511
+ this.postToHost(outbound);
512
+ });
513
+ }
514
+ getUser(userId) {
515
+ return new Promise(resolve => {
516
+ const requestId = this.pendingGetUser.allocate(resolve);
517
+ const outbound = {
518
+ type: "getUser",
519
+ requestId,
520
+ userId,
521
+ };
522
+ this.postToHost(outbound);
523
+ });
524
+ }
525
+ listUsers(input = {}) {
526
+ return new Promise(resolve => {
527
+ const requestId = this.pendingListUsers.allocate(resolve);
528
+ const outbound = {
529
+ type: "listUsers",
530
+ requestId,
531
+ startCursor: input.startCursor,
532
+ pageSize: input.pageSize,
533
+ };
534
+ this.postToHost(outbound);
535
+ });
536
+ }
537
+ updatePage(input) {
538
+ return new Promise(resolve => {
539
+ if ((input.properties === undefined ||
540
+ Object.keys(input.properties).length === 0) &&
541
+ input.icon === undefined &&
542
+ input.cover === undefined &&
543
+ input.archived === undefined) {
544
+ resolve({
545
+ status: "error",
546
+ error: {
547
+ code: "invalid_page_update",
548
+ message: "updatePage requires at least one of: properties, icon, cover, archived.",
549
+ isRetryable: false,
550
+ },
551
+ });
552
+ return;
553
+ }
554
+ const requestId = this.pendingUpdatePage.allocate(resolve);
555
+ const outbound = {
556
+ type: "updatePage",
557
+ requestId,
558
+ pageId: input.pageId,
559
+ };
560
+ if (input.properties !== undefined) {
561
+ outbound.properties = input.properties;
562
+ }
563
+ if (input.icon !== undefined) {
564
+ outbound.icon = input.icon;
565
+ }
566
+ if (input.cover !== undefined) {
567
+ outbound.cover = input.cover;
568
+ }
569
+ if (input.archived !== undefined) {
570
+ outbound.archived = input.archived;
571
+ }
572
+ this.postToHost(outbound);
573
+ });
574
+ }
575
+ /**
576
+ * Updates a page on a known data source, resolving any property keys against the data source's
577
+ * `propertyIdsByKey` before sending the bridge message. Used by the per-row `update` callback
578
+ * returned from {@link getDataSourceQueryView}.
579
+ */
580
+ updateDataSourcePage(args) {
581
+ const { dataSource, pageId, input } = args;
582
+ const resolvedProperties = input.properties === undefined
583
+ ? undefined
584
+ : resolvePropertyWriteMapForDataSource({
585
+ dataSource,
586
+ properties: input.properties,
587
+ operationName: "dataSourcePage.update",
588
+ });
589
+ if (resolvedProperties?.status === "error") {
590
+ return Promise.resolve(resolvedProperties);
591
+ }
592
+ return this.updatePage({
593
+ pageId,
594
+ properties: resolvedProperties?.properties,
595
+ icon: input.icon,
596
+ cover: input.cover,
597
+ archived: input.archived,
598
+ });
599
+ }
600
+ /**
601
+ * Translates the public `CreatePageInput["parent"]` into the bridge-native
602
+ * `CreatePageMessageParent`. The `data_source_key` variant is resolved sandbox-side against
603
+ * the data source mapping the host delivered in `init` / `dataSourcesChanged`.
604
+ */
605
+ resolveCreatePageParent(parent) {
606
+ switch (parent.type) {
607
+ case "page_id":
608
+ return { status: "ok", parent, dataSource: undefined };
609
+ case "data_source_id": {
610
+ const dataSource = this.hostState.status === "initialized"
611
+ ? this.hostState.dataSources.find(entry => entry.collectionPointer?.id === parent.data_source_id)
612
+ : undefined;
613
+ return { status: "ok", parent, dataSource };
614
+ }
615
+ case "data_source_key": {
616
+ if (this.hostState.status !== "initialized") {
617
+ return {
618
+ status: "error",
619
+ error: {
620
+ code: "unknown_data_source_key",
621
+ message: `Cannot resolve data source key "${parent.key}" before the host has initialized the SDK.`,
622
+ isRetryable: false,
623
+ },
624
+ };
625
+ }
626
+ const dataSource = this.hostState.dataSources.find(entry => entry.key === parent.key);
627
+ if (dataSource === undefined) {
628
+ return {
629
+ status: "error",
630
+ error: {
631
+ code: "unknown_data_source_key",
632
+ message: `Unknown data source key "${parent.key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
633
+ isRetryable: false,
634
+ },
635
+ };
636
+ }
637
+ if (dataSource.collectionPointer === undefined) {
638
+ return {
639
+ status: "error",
640
+ error: {
641
+ code: "unmapped_data_source",
642
+ message: `Data source "${parent.key}" has not been mapped to a database yet.`,
643
+ isRetryable: false,
644
+ },
645
+ };
646
+ }
647
+ return {
648
+ status: "ok",
649
+ parent: {
650
+ type: "data_source_id",
651
+ data_source_id: dataSource.collectionPointer.id,
652
+ },
653
+ dataSource,
654
+ };
655
+ }
656
+ default:
657
+ unreachable(parent);
658
+ }
659
+ }
660
+ }
661
+ SandboxBridge.MAX_LOG_ENTRIES = 100;
662
+ /**
663
+ * `postMessage` cloning creates new objects even for bindings that did not change.
664
+ * Preserve their resolved object identity so React effects only run for changed bindings.
665
+ *
666
+ * This comparison runs when the host sends an update instead of on every component render.
667
+ * Bindings are validated JSON-like bridge payloads, so serialization is sufficient here.
668
+ */
669
+ function reuseDataSourcesForUnchangedBindings(args) {
670
+ const previousDataSourcesByKey = new Map(args.previousDataSources.map(dataSource => [dataSource.key, dataSource]));
671
+ return args.nextDataSources.map(dataSource => {
672
+ const previousDataSource = previousDataSourcesByKey.get(dataSource.key);
673
+ if (previousDataSource !== undefined &&
674
+ JSON.stringify(args.previousBindings[dataSource.key]) ===
675
+ JSON.stringify(args.nextBindings[dataSource.key])) {
676
+ return previousDataSource;
677
+ }
678
+ return dataSource;
679
+ });
680
+ }
681
+ // The default number of items to return in a live snapshot response if no limit is provided.
682
+ const DEFAULT_DATA_SOURCE_QUERY_LIMIT = 20;
683
+ // The maximum number of items to return in a single live snapshot response.
684
+ const MAX_DATA_SOURCE_QUERY_LIMIT = 999;
685
+ function resolveDataSourceQueryLimit(limit) {
686
+ if (limit === undefined) {
687
+ return DEFAULT_DATA_SOURCE_QUERY_LIMIT;
688
+ }
689
+ if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1) {
690
+ console.warn(`[notion-custom-sdk] useDataSource limit must be a positive integer; using ${DEFAULT_DATA_SOURCE_QUERY_LIMIT}.`);
691
+ return DEFAULT_DATA_SOURCE_QUERY_LIMIT;
692
+ }
693
+ if (limit > MAX_DATA_SOURCE_QUERY_LIMIT) {
694
+ console.warn(`[notion-custom-sdk] useDataSource limit is capped at ${MAX_DATA_SOURCE_QUERY_LIMIT}.`);
695
+ return MAX_DATA_SOURCE_QUERY_LIMIT;
696
+ }
697
+ return limit;
698
+ }
699
+ function makeDataSourceSnapshotId(args) {
700
+ const { key } = args;
701
+ return `data-source:${encodeURIComponent(key)}`;
702
+ }
703
+ function formatInvalidHostReason(incomingType, issues) {
704
+ const labelled = incomingType
705
+ ? `host message of type "${incomingType}"`
706
+ : "host message";
707
+ const first = issues[0];
708
+ if (!first) {
709
+ return `Could not parse ${labelled}: unknown error`;
710
+ }
711
+ const path = first.path
712
+ ?.map(p => String(p.key ?? ""))
713
+ .filter(Boolean)
714
+ .join(".") ?? "";
715
+ const detail = path ? `${path}: ${first.message}` : first.message;
716
+ const extra = issues.length > 1 ? ` (+${issues.length - 1} more)` : "";
717
+ return `Could not parse ${labelled}: ${detail}${extra}`;
718
+ }