ncblock 0.0.1

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 (113) hide show
  1. package/README.md +286 -0
  2. package/dist/bridge/context.d.ts +44 -0
  3. package/dist/bridge/context.d.ts.map +1 -0
  4. package/dist/bridge/context.js +42 -0
  5. package/dist/bridge/dataSources/dataSource.d.ts +732 -0
  6. package/dist/bridge/dataSources/dataSource.d.ts.map +1 -0
  7. package/dist/bridge/dataSources/dataSource.js +37 -0
  8. package/dist/bridge/dataSources/dataSourcePage.d.ts +117 -0
  9. package/dist/bridge/dataSources/dataSourcePage.d.ts.map +1 -0
  10. package/dist/bridge/dataSources/dataSourcePage.js +13 -0
  11. package/dist/bridge/dataSources/dataSourceValue.d.ts +67 -0
  12. package/dist/bridge/dataSources/dataSourceValue.d.ts.map +1 -0
  13. package/dist/bridge/dataSources/dataSourceValue.js +14 -0
  14. package/dist/bridge/dataSources/dateValue.d.ts +158 -0
  15. package/dist/bridge/dataSources/dateValue.d.ts.map +1 -0
  16. package/dist/bridge/dataSources/dateValue.js +59 -0
  17. package/dist/bridge/dataSources/propertySchema.d.ts +191 -0
  18. package/dist/bridge/dataSources/propertySchema.d.ts.map +1 -0
  19. package/dist/bridge/dataSources/propertySchema.js +148 -0
  20. package/dist/bridge/dataSources/recordPointer.d.ts +10 -0
  21. package/dist/bridge/dataSources/recordPointer.d.ts.map +1 -0
  22. package/dist/bridge/dataSources/recordPointer.js +8 -0
  23. package/dist/bridge/ids.d.ts +21 -0
  24. package/dist/bridge/ids.d.ts.map +1 -0
  25. package/dist/bridge/ids.js +3 -0
  26. package/dist/bridge/incomingType.d.ts +10 -0
  27. package/dist/bridge/incomingType.d.ts.map +1 -0
  28. package/dist/bridge/incomingType.js +17 -0
  29. package/dist/bridge/manifest.d.ts +66 -0
  30. package/dist/bridge/manifest.d.ts.map +1 -0
  31. package/dist/bridge/manifest.js +40 -0
  32. package/dist/bridge/messages/contextChanged.d.ts +20 -0
  33. package/dist/bridge/messages/contextChanged.d.ts.map +1 -0
  34. package/dist/bridge/messages/contextChanged.js +10 -0
  35. package/dist/bridge/messages/createPage.d.ts +233 -0
  36. package/dist/bridge/messages/createPage.d.ts.map +1 -0
  37. package/dist/bridge/messages/createPage.js +45 -0
  38. package/dist/bridge/messages/createPageResult.d.ts +198 -0
  39. package/dist/bridge/messages/createPageResult.d.ts.map +1 -0
  40. package/dist/bridge/messages/createPageResult.js +20 -0
  41. package/dist/bridge/messages/dataSourcesChanged.d.ts +158 -0
  42. package/dist/bridge/messages/dataSourcesChanged.d.ts.map +1 -0
  43. package/dist/bridge/messages/dataSourcesChanged.js +13 -0
  44. package/dist/bridge/messages/getPage.d.ts +203 -0
  45. package/dist/bridge/messages/getPage.d.ts.map +1 -0
  46. package/dist/bridge/messages/getPage.js +24 -0
  47. package/dist/bridge/messages/hostToSandbox.d.ts +974 -0
  48. package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -0
  49. package/dist/bridge/messages/hostToSandbox.js +24 -0
  50. package/dist/bridge/messages/init.d.ts +169 -0
  51. package/dist/bridge/messages/init.d.ts.map +1 -0
  52. package/dist/bridge/messages/init.js +17 -0
  53. package/dist/bridge/messages/invalidHostMessage.d.ts +15 -0
  54. package/dist/bridge/messages/invalidHostMessage.d.ts.map +1 -0
  55. package/dist/bridge/messages/invalidHostMessage.js +13 -0
  56. package/dist/bridge/messages/invalidSandboxMessage.d.ts +15 -0
  57. package/dist/bridge/messages/invalidSandboxMessage.d.ts.map +1 -0
  58. package/dist/bridge/messages/invalidSandboxMessage.js +13 -0
  59. package/dist/bridge/messages/queryDataSource.d.ts +13 -0
  60. package/dist/bridge/messages/queryDataSource.d.ts.map +1 -0
  61. package/dist/bridge/messages/queryDataSource.js +11 -0
  62. package/dist/bridge/messages/queryDataSourceResult.d.ts +77 -0
  63. package/dist/bridge/messages/queryDataSourceResult.d.ts.map +1 -0
  64. package/dist/bridge/messages/queryDataSourceResult.js +13 -0
  65. package/dist/bridge/messages/ready.d.ts +41 -0
  66. package/dist/bridge/messages/ready.d.ts.map +1 -0
  67. package/dist/bridge/messages/ready.js +22 -0
  68. package/dist/bridge/messages/resize.d.ts +12 -0
  69. package/dist/bridge/messages/resize.d.ts.map +1 -0
  70. package/dist/bridge/messages/resize.js +10 -0
  71. package/dist/bridge/messages/sandboxToHost.d.ts +389 -0
  72. package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -0
  73. package/dist/bridge/messages/sandboxToHost.js +21 -0
  74. package/dist/bridge/messages/themeChanged.d.ts +11 -0
  75. package/dist/bridge/messages/themeChanged.d.ts.map +1 -0
  76. package/dist/bridge/messages/themeChanged.js +10 -0
  77. package/dist/bridge/messages/updatePage.d.ts +171 -0
  78. package/dist/bridge/messages/updatePage.d.ts.map +1 -0
  79. package/dist/bridge/messages/updatePage.js +14 -0
  80. package/dist/bridge/messages/updatePageResult.d.ts +197 -0
  81. package/dist/bridge/messages/updatePageResult.d.ts.map +1 -0
  82. package/dist/bridge/messages/updatePageResult.js +19 -0
  83. package/dist/bridge/pages/page.d.ts +651 -0
  84. package/dist/bridge/pages/page.d.ts.map +1 -0
  85. package/dist/bridge/pages/page.js +229 -0
  86. package/dist/bridge/pendingRequests.d.ts +14 -0
  87. package/dist/bridge/pendingRequests.d.ts.map +1 -0
  88. package/dist/bridge/pendingRequests.js +27 -0
  89. package/dist/bridge/theme.d.ts +4 -0
  90. package/dist/bridge/theme.d.ts.map +1 -0
  91. package/dist/bridge/theme.js +2 -0
  92. package/dist/host.d.ts +52 -0
  93. package/dist/host.d.ts.map +1 -0
  94. package/dist/host.js +32 -0
  95. package/dist/index.d.ts +27 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +14 -0
  98. package/dist/notion.d.ts +113 -0
  99. package/dist/notion.d.ts.map +1 -0
  100. package/dist/notion.js +773 -0
  101. package/dist/pages.d.ts +23 -0
  102. package/dist/pages.d.ts.map +1 -0
  103. package/dist/pages.js +30 -0
  104. package/dist/react.d.ts +171 -0
  105. package/dist/react.d.ts.map +1 -0
  106. package/dist/react.js +284 -0
  107. package/dist/types.d.ts +124 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/dist/types.js +1 -0
  110. package/dist/utils.d.ts +9 -0
  111. package/dist/utils.d.ts.map +1 -0
  112. package/dist/utils.js +10 -0
  113. package/package.json +48 -0
package/dist/notion.js ADDED
@@ -0,0 +1,773 @@
1
+ import * as v from "valibot";
2
+ import { readIncomingType } from "./bridge/incomingType";
3
+ import { manifestSchema } from "./bridge/manifest";
4
+ import { hostToSandboxMessageSchema } from "./bridge/messages/hostToSandbox";
5
+ import { notionPagePropertyValueSchema } from "./bridge/pages/page";
6
+ import { PendingRequests } from "./bridge/pendingRequests";
7
+ import { unreachable } from "./utils";
8
+ /**
9
+ * Used to ensure that the host and client are using the same version of the bridge protocol. A
10
+ * single host needs to support multiple custom blocks built with different versions of the bridge
11
+ * protocol. Increment this number any time a breaking change is made to the bridge protocol.
12
+ */
13
+ const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 1;
14
+ const MANIFEST_URL = "custom_blocks.json";
15
+ /**
16
+ * Resolves a public SDK property write map into the ID-keyed bridge shape.
17
+ */
18
+ function resolvePropertyWriteMapForDataSource(args) {
19
+ const { dataSource, properties, operationName } = args;
20
+ const resolvedProperties = {};
21
+ for (const [identifier, value] of Object.entries(properties)) {
22
+ const propertyIdResult = resolvePropertyIdentifierForDataSource({
23
+ dataSource,
24
+ identifier,
25
+ operationName,
26
+ });
27
+ if (propertyIdResult.status === "error") {
28
+ return propertyIdResult;
29
+ }
30
+ const propertyId = propertyIdResult.propertyId;
31
+ if (value.id !== undefined &&
32
+ value.id !== identifier &&
33
+ value.id !== propertyId) {
34
+ return {
35
+ status: "error",
36
+ error: `Property ${identifier} resolved to ${propertyId} but value id was ${value.id}.`,
37
+ };
38
+ }
39
+ if (resolvedProperties[propertyId] !== undefined) {
40
+ return {
41
+ status: "error",
42
+ error: `Cannot set property ${propertyId} more than once.`,
43
+ };
44
+ }
45
+ const parsedValue = v.safeParse(notionPagePropertyValueSchema, {
46
+ ...value,
47
+ id: propertyId,
48
+ });
49
+ if (!parsedValue.success) {
50
+ return {
51
+ status: "error",
52
+ error: `Invalid value for property ${identifier}.`,
53
+ };
54
+ }
55
+ resolvedProperties[propertyId] = parsedValue.output;
56
+ }
57
+ return { status: "success", properties: resolvedProperties };
58
+ }
59
+ /**
60
+ * Treats matching data-source keys as aliases for property IDs; all other identifiers are raw IDs.
61
+ */
62
+ function resolvePropertyIdentifierForDataSource(args) {
63
+ const { dataSource, identifier, operationName } = args;
64
+ if (dataSource !== undefined && identifier in dataSource.propertyIdsByKey) {
65
+ const propertyId = dataSource.propertyIdsByKey[identifier];
66
+ if (propertyId === undefined) {
67
+ return {
68
+ status: "error",
69
+ error: `${operationName} cannot resolve property key "${identifier}" because it is not bound to a Notion property.`,
70
+ };
71
+ }
72
+ return { status: "success", propertyId };
73
+ }
74
+ return { status: "success", propertyId: identifier };
75
+ }
76
+ function createEmptyDataSourceQueryState() {
77
+ return {
78
+ items: [],
79
+ isLoading: false,
80
+ hasMore: false,
81
+ };
82
+ }
83
+ class NotionCustomBlockBridge {
84
+ constructor() {
85
+ this.hostState = {
86
+ status: "uninitialized",
87
+ theme: "light",
88
+ };
89
+ this.listeners = new Set();
90
+ this.nextRequestId = 1;
91
+ this.pendingCreatePage = new PendingRequests("custom-block-create-page");
92
+ this.pendingGetPage = new PendingRequests("custom-block-get-page");
93
+ this.pendingUpdatePage = new PendingRequests("custom-block-update-page");
94
+ this.initMessage = new Promise(resolve => {
95
+ this.resolveInit = resolve;
96
+ });
97
+ this.manifest = null;
98
+ this.notify = () => {
99
+ for (const listener of this.listeners) {
100
+ listener();
101
+ }
102
+ };
103
+ this.handleMessage = (event) => {
104
+ console.debug("[notion-custom-sdk] incoming postMessage", {
105
+ data: event.data,
106
+ fromParent: event.source === window.parent,
107
+ });
108
+ if (event.source !== window.parent) {
109
+ return;
110
+ }
111
+ const parsed = v.safeParse(hostToSandboxMessageSchema, event.data);
112
+ if (!parsed.success) {
113
+ console.warn("[notion-custom-sdk] ignoring malformed host message", parsed.issues);
114
+ const incomingType = readIncomingType(event.data);
115
+ // Loop guard: never NACK a NACK. Excludes both directions —
116
+ // `invalidSandboxMessage` is what the host normally sends, but
117
+ // a buggy host that echoes our own `invalidHostMessage` back
118
+ // would otherwise spin a NACK feedback loop.
119
+ if (incomingType !== "invalidSandboxMessage" &&
120
+ incomingType !== "invalidHostMessage") {
121
+ const nack = {
122
+ type: "invalidHostMessage",
123
+ reason: formatInvalidHostReason(incomingType, parsed.issues),
124
+ };
125
+ console.debug("[notion-custom-sdk] outbound postMessage", nack);
126
+ window.parent.postMessage(nack, "*");
127
+ }
128
+ return;
129
+ }
130
+ const message = parsed.output;
131
+ // If the host couldn't parse one of our outbound sandbox-to-host messages, it sends back
132
+ // an `invalidSandboxMessage` NACK with a human-readable `reason`. Log it for developer
133
+ // visibility and stop. We don't retry as the sandbox can't recover from a host-side parse
134
+ // failure on its own.
135
+ if (message.type === "invalidSandboxMessage") {
136
+ console.warn("[notion-custom-sdk] host reported invalid sandbox message:", message.reason);
137
+ return;
138
+ }
139
+ // `init` is the only message valid before initialization. Handle it up
140
+ // front so every later case can assume `status === "initialized"`.
141
+ if (message.type === "init") {
142
+ this.applyInit(message);
143
+ return;
144
+ }
145
+ // Alias to keep TS's narrowed `InitializedHostState` across the switch
146
+ // below. Reading `this.hostState` repeatedly would re-widen it.
147
+ const hostState = this.hostState;
148
+ if (hostState.status !== "initialized") {
149
+ console.warn(`[notion-custom-sdk] ignoring ${message.type} before init`);
150
+ return;
151
+ }
152
+ switch (message.type) {
153
+ case "themeChanged": {
154
+ this.hostState = {
155
+ ...hostState,
156
+ theme: message.theme,
157
+ };
158
+ this.notify();
159
+ return;
160
+ }
161
+ case "contextChanged": {
162
+ this.hostState = {
163
+ ...hostState,
164
+ context: message.context,
165
+ };
166
+ this.notify();
167
+ return;
168
+ }
169
+ case "dataSourcesChanged": {
170
+ const dataSources = resolveDataSources({
171
+ manifest: this.manifest,
172
+ dataSourceBindings: message.dataSources.bindings,
173
+ });
174
+ // Drop cached query state for keys that no longer exist in the mapping.
175
+ const nextKeys = new Set(dataSources.map(s => s.key));
176
+ const prunedState = {};
177
+ for (const [key, state] of Object.entries(hostState.dataSourceState)) {
178
+ if (nextKeys.has(key)) {
179
+ prunedState[key] = state;
180
+ }
181
+ }
182
+ this.hostState = {
183
+ ...hostState,
184
+ dataSources,
185
+ dataSourceState: prunedState,
186
+ };
187
+ this.notify();
188
+ return;
189
+ }
190
+ case "createPageResult": {
191
+ const result = message.status === "success"
192
+ ? { status: "success", page: message.page }
193
+ : { status: "error", error: message.error };
194
+ if (!this.pendingCreatePage.resolve(message.requestId, result)) {
195
+ console.warn(`[notion-custom-sdk] createPageResult for unknown requestId ${message.requestId}`);
196
+ }
197
+ return;
198
+ }
199
+ case "getPageResult": {
200
+ const result = message.status === "success"
201
+ ? { status: "success", page: message.page }
202
+ : { status: "error", error: message.error };
203
+ if (!this.pendingGetPage.resolve(message.requestId, result)) {
204
+ console.warn(`[notion-custom-sdk] getPageResult for unknown requestId ${message.requestId}`);
205
+ }
206
+ return;
207
+ }
208
+ case "updatePageResult": {
209
+ const result = message.status === "success"
210
+ ? { status: "success", page: message.page }
211
+ : { status: "error", error: message.error };
212
+ if (!this.pendingUpdatePage.resolve(message.requestId, result)) {
213
+ console.warn(`[notion-custom-sdk] updatePageResult for unknown requestId ${message.requestId}`);
214
+ }
215
+ return;
216
+ }
217
+ case "queryDataSourceResult": {
218
+ const currentState = hostState.dataSourceState[message.key] ??
219
+ createEmptyDataSourceQueryState();
220
+ if (currentState.latestRequestId !== message.requestId) {
221
+ return;
222
+ }
223
+ this.hostState = {
224
+ ...hostState,
225
+ dataSourceState: {
226
+ ...hostState.dataSourceState,
227
+ [message.key]: {
228
+ items: message.items,
229
+ isLoading: false,
230
+ hasMore: message.hasMore,
231
+ error: message.error,
232
+ // Keep the request ID so later host-pushed refreshes for the
233
+ // same subscription still match.
234
+ latestRequestId: message.requestId,
235
+ },
236
+ },
237
+ };
238
+ this.notify();
239
+ return;
240
+ }
241
+ default: {
242
+ unreachable(message);
243
+ }
244
+ }
245
+ };
246
+ // `ready` is sent later by `initCustomBlock` (after the manifest fetch
247
+ // resolves). Top-level / no-iframe rejection is handled there too, so
248
+ // the constructor just attaches the listener.
249
+ if (typeof window !== "undefined") {
250
+ window.addEventListener("message", this.handleMessage);
251
+ }
252
+ }
253
+ awaitInit(signal) {
254
+ if (!signal) {
255
+ return this.initMessage;
256
+ }
257
+ return new Promise((resolve, reject) => {
258
+ if (signal.aborted) {
259
+ reject(signal.reason);
260
+ return;
261
+ }
262
+ const onAbort = () => reject(signal.reason);
263
+ signal.addEventListener("abort", onAbort, { once: true });
264
+ this.initMessage.then(message => {
265
+ signal.removeEventListener("abort", onAbort);
266
+ resolve(message);
267
+ }, err => {
268
+ signal.removeEventListener("abort", onAbort);
269
+ reject(err);
270
+ });
271
+ });
272
+ }
273
+ async sendReady(manifest) {
274
+ if (typeof window === "undefined") {
275
+ return;
276
+ }
277
+ this.manifest = manifest;
278
+ const readyMessage = {
279
+ type: "ready",
280
+ bridgeProtocolVersion: CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
281
+ manifest,
282
+ };
283
+ console.debug("[notion-custom-sdk] outbound postMessage", readyMessage);
284
+ window.parent.postMessage(readyMessage, "*");
285
+ }
286
+ subscribe(listener) {
287
+ this.listeners.add(listener);
288
+ return () => this.listeners.delete(listener);
289
+ }
290
+ getHostState() {
291
+ return this.hostState;
292
+ }
293
+ /**
294
+ * Apply an `init` payload as if it had arrived from the host. Lets callers
295
+ * seed the bridge directly (e.g. the React provider's standalone preview
296
+ * fallback) without going through `postMessage`. The bridge stays unaware
297
+ * of why it's being seeded.
298
+ */
299
+ setMockState(message) {
300
+ this.applyInit(message);
301
+ }
302
+ applyInit(message) {
303
+ const dataSources = resolveDataSources({
304
+ manifest: this.manifest,
305
+ dataSourceBindings: message.dataSources.bindings,
306
+ });
307
+ this.hostState = {
308
+ status: "initialized",
309
+ theme: message.theme,
310
+ context: message.context,
311
+ dataSources,
312
+ dataSourceState: {},
313
+ };
314
+ this.notify();
315
+ // Resolve the awaitInit promise once. Subsequent `init` messages
316
+ // (the host shouldn't send these, but be tolerant) update state but
317
+ // don't re-resolve.
318
+ if (this.resolveInit) {
319
+ this.resolveInit(message);
320
+ this.resolveInit = undefined;
321
+ }
322
+ }
323
+ queryDataSource(key, limit) {
324
+ if (this.hostState.status !== "initialized") {
325
+ return;
326
+ }
327
+ const currentState = this.hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState();
328
+ if (currentState.isLoading) {
329
+ return;
330
+ }
331
+ const requestId = `custom-block-query-${this.nextRequestId}`;
332
+ this.nextRequestId += 1;
333
+ this.hostState = {
334
+ ...this.hostState,
335
+ dataSourceState: {
336
+ ...this.hostState.dataSourceState,
337
+ [key]: {
338
+ ...currentState,
339
+ isLoading: true,
340
+ error: undefined,
341
+ latestRequestId: requestId,
342
+ },
343
+ },
344
+ };
345
+ this.notify();
346
+ const outbound = {
347
+ type: "queryDataSource",
348
+ requestId,
349
+ key,
350
+ limit,
351
+ };
352
+ console.debug("[notion-custom-sdk] outbound postMessage", outbound);
353
+ window.parent.postMessage(outbound, "*");
354
+ }
355
+ postResize(height) {
356
+ if (typeof window === "undefined") {
357
+ return;
358
+ }
359
+ const safeHeight = Number.isFinite(height) && height >= 0 ? Math.ceil(height) : 0;
360
+ const outbound = {
361
+ type: "resize",
362
+ height: safeHeight,
363
+ };
364
+ console.debug("[notion-custom-sdk] outbound postMessage", outbound);
365
+ window.parent.postMessage(outbound, "*");
366
+ }
367
+ createPage(input) {
368
+ return new Promise(resolve => {
369
+ const resolvedParent = this.resolveCreatePageParent(input.parent);
370
+ if (resolvedParent.status === "error") {
371
+ resolve(resolvedParent);
372
+ return;
373
+ }
374
+ const resolvedProperties = resolvePropertyWriteMapForDataSource({
375
+ dataSource: resolvedParent.dataSource,
376
+ properties: input.properties,
377
+ operationName: "createPage",
378
+ });
379
+ if (resolvedProperties.status === "error") {
380
+ resolve(resolvedProperties);
381
+ return;
382
+ }
383
+ const requestId = this.pendingCreatePage.allocate(resolve);
384
+ const outbound = {
385
+ type: "createPage",
386
+ requestId,
387
+ parent: resolvedParent.parent,
388
+ properties: resolvedProperties.properties,
389
+ };
390
+ if (input.icon !== undefined) {
391
+ outbound.icon = input.icon;
392
+ }
393
+ if (input.cover !== undefined) {
394
+ outbound.cover = input.cover;
395
+ }
396
+ if (input.position !== undefined) {
397
+ outbound.position = input.position;
398
+ }
399
+ console.debug("[notion-custom-sdk] outbound postMessage", outbound);
400
+ window.parent.postMessage(outbound, "*");
401
+ });
402
+ }
403
+ getPage(pageId) {
404
+ return new Promise(resolve => {
405
+ const requestId = this.pendingGetPage.allocate(resolve);
406
+ const outbound = {
407
+ type: "getPage",
408
+ requestId,
409
+ pageId,
410
+ };
411
+ console.debug("[notion-custom-sdk] outbound postMessage", outbound);
412
+ window.parent.postMessage(outbound, "*");
413
+ });
414
+ }
415
+ updatePage(input) {
416
+ return new Promise(resolve => {
417
+ if ((input.properties === undefined ||
418
+ Object.keys(input.properties).length === 0) &&
419
+ input.icon === undefined &&
420
+ input.cover === undefined &&
421
+ input.archived === undefined) {
422
+ resolve({
423
+ status: "error",
424
+ error: "updatePage requires at least one of: properties, icon, cover, archived.",
425
+ });
426
+ return;
427
+ }
428
+ const requestId = this.pendingUpdatePage.allocate(resolve);
429
+ const outbound = {
430
+ type: "updatePage",
431
+ requestId,
432
+ pageId: input.pageId,
433
+ };
434
+ if (input.properties !== undefined) {
435
+ outbound.properties = input.properties;
436
+ }
437
+ if (input.icon !== undefined) {
438
+ outbound.icon = input.icon;
439
+ }
440
+ if (input.cover !== undefined) {
441
+ outbound.cover = input.cover;
442
+ }
443
+ if (input.archived !== undefined) {
444
+ outbound.archived = input.archived;
445
+ }
446
+ console.debug("[notion-custom-sdk] outbound postMessage", outbound);
447
+ window.parent.postMessage(outbound, "*");
448
+ });
449
+ }
450
+ /**
451
+ * Translates the public `CreatePageInput["parent"]` into the bridge-native
452
+ * `CreatePageMessageParent`. The `data_source_key` variant is resolved sandbox-side against
453
+ * the data source mapping the host delivered in `init` / `dataSourcesChanged`.
454
+ */
455
+ resolveCreatePageParent(parent) {
456
+ switch (parent.type) {
457
+ case "page_id":
458
+ return { status: "ok", parent, dataSource: undefined };
459
+ case "data_source_id": {
460
+ const dataSource = this.hostState.status === "initialized"
461
+ ? this.hostState.dataSources.find(entry => entry.collectionPointer?.id === parent.data_source_id)
462
+ : undefined;
463
+ return { status: "ok", parent, dataSource };
464
+ }
465
+ case "data_source_key": {
466
+ if (this.hostState.status !== "initialized") {
467
+ return {
468
+ status: "error",
469
+ error: `Cannot resolve data source key "${parent.key}" before the host has initialized the SDK.`,
470
+ };
471
+ }
472
+ const dataSource = this.hostState.dataSources.find(entry => entry.key === parent.key);
473
+ if (dataSource === undefined) {
474
+ return {
475
+ status: "error",
476
+ error: `Unknown data source key "${parent.key}". Known keys: [${this.hostState.dataSources.map(entry => entry.key).join(", ")}].`,
477
+ };
478
+ }
479
+ if (dataSource.collectionPointer === undefined) {
480
+ return {
481
+ status: "error",
482
+ error: `Data source "${parent.key}" has not been mapped to a database yet.`,
483
+ };
484
+ }
485
+ return {
486
+ status: "ok",
487
+ parent: {
488
+ type: "data_source_id",
489
+ data_source_id: dataSource.collectionPointer.id,
490
+ },
491
+ dataSource,
492
+ };
493
+ }
494
+ default:
495
+ unreachable(parent);
496
+ }
497
+ }
498
+ }
499
+ function resolveDataSources(args) {
500
+ if (args.manifest === null) {
501
+ return Object.entries(args.dataSourceBindings).map(([key, binding]) => ({
502
+ key,
503
+ collectionPointer: binding.collectionPointer,
504
+ collectionSchema: binding.collectionSchema,
505
+ propertyIdsByKey: { ...(binding.propertyIdsByKey ?? {}) },
506
+ propertySchemasById: binding.collectionSchema?.propertiesById ?? {},
507
+ }));
508
+ }
509
+ return Object.entries(args.manifest.dataSources).flatMap(([key, manifestDataSource]) => {
510
+ const binding = args.dataSourceBindings[key];
511
+ const propertySchemasById = binding?.collectionSchema?.propertiesById ?? {};
512
+ const bindingPropertyIdsByKey = binding?.propertyIdsByKey ?? {};
513
+ const propertyIdsByKey = {};
514
+ const manifestProperties = Object.entries(manifestDataSource.properties ?? {});
515
+ for (const [propertyKey, manifestProperty] of manifestProperties) {
516
+ const propertyId = propertyKey in bindingPropertyIdsByKey
517
+ ? bindingPropertyIdsByKey[propertyKey]
518
+ : findPropertyIdByManifestProperty(propertySchemasById, {
519
+ key: propertyKey,
520
+ type: manifestProperty.type,
521
+ });
522
+ const propertySchema = propertyId === undefined ? undefined : propertySchemasById[propertyId];
523
+ propertyIdsByKey[propertyKey] =
524
+ propertySchema?.type === manifestProperty.type
525
+ ? propertyId
526
+ : undefined;
527
+ }
528
+ return [
529
+ {
530
+ key,
531
+ collectionPointer: binding?.collectionPointer,
532
+ collectionSchema: binding?.collectionSchema,
533
+ propertyIdsByKey,
534
+ propertySchemasById,
535
+ },
536
+ ];
537
+ });
538
+ }
539
+ function findPropertyIdByManifestProperty(propertySchemasById, manifestProperty) {
540
+ // `ManifestProperty.name` is display copy for setup UI. Binding fallback is based
541
+ // on the stable semantic key so copy changes don't affect property resolution.
542
+ const propertySchemaForKey = propertySchemasById[manifestProperty.key];
543
+ if (propertySchemaForKey?.type === manifestProperty.type) {
544
+ return manifestProperty.key;
545
+ }
546
+ const normalizedKey = normalizePropertyName(manifestProperty.key);
547
+ for (const [propertyId, propertySchema] of Object.entries(propertySchemasById)) {
548
+ if (propertySchema.type === manifestProperty.type &&
549
+ normalizePropertyName(propertySchema.name) === normalizedKey) {
550
+ return propertyId;
551
+ }
552
+ }
553
+ return undefined;
554
+ }
555
+ function normalizePropertyName(value) {
556
+ return value.trim().toLowerCase();
557
+ }
558
+ function formatInvalidHostReason(incomingType, issues) {
559
+ const labelled = incomingType
560
+ ? `host message of type "${incomingType}"`
561
+ : "host message";
562
+ const first = issues[0];
563
+ if (!first) {
564
+ return `Could not parse ${labelled}: unknown error`;
565
+ }
566
+ const path = first.path
567
+ ?.map(p => String(p.key ?? ""))
568
+ .filter(Boolean)
569
+ .join(".") ?? "";
570
+ const detail = path ? `${path}: ${first.message}` : first.message;
571
+ const extra = issues.length > 1 ? ` (+${issues.length - 1} more)` : "";
572
+ return `Could not parse ${labelled}: ${detail}${extra}`;
573
+ }
574
+ /**
575
+ * Attempts to load a `custom_blocks.json` manifest co-located with the bundle. Treats
576
+ * any non-200 response as "no manifest".
577
+ */
578
+ async function tryFetchManifest() {
579
+ if (typeof fetch !== "function") {
580
+ console.warn(`[notion-custom-sdk] no \`fetch\` available; cannot load ${MANIFEST_URL}`);
581
+ return null;
582
+ }
583
+ let response;
584
+ try {
585
+ response = await fetch(MANIFEST_URL, { credentials: "omit" });
586
+ }
587
+ catch (error) {
588
+ console.warn(`[notion-custom-sdk] no manifest fetched from ${MANIFEST_URL}`, error);
589
+ return null;
590
+ }
591
+ if (!response.ok) {
592
+ console.warn(`[notion-custom-sdk] no manifest at ${MANIFEST_URL} (status ${response.status})`);
593
+ return null;
594
+ }
595
+ let json;
596
+ try {
597
+ json = await response.json();
598
+ }
599
+ catch (error) {
600
+ console.warn(`[notion-custom-sdk] manifest at ${MANIFEST_URL} was not valid JSON`, error);
601
+ return null;
602
+ }
603
+ const parsed = v.safeParse(manifestSchema, json);
604
+ if (!parsed.success) {
605
+ console.warn(`[notion-custom-sdk] manifest at ${MANIFEST_URL} did not match schema`, parsed.issues);
606
+ return null;
607
+ }
608
+ return parsed.output;
609
+ }
610
+ const bridge = new NotionCustomBlockBridge();
611
+ export function subscribeToCustomBlockHost(listener) {
612
+ return bridge.subscribe(listener);
613
+ }
614
+ export function getCustomBlockHostState() {
615
+ return bridge.getHostState();
616
+ }
617
+ export function queryCustomBlockDataSource(key, limit) {
618
+ bridge.queryDataSource(key, limit);
619
+ }
620
+ /**
621
+ * Apply an `init` payload to the bridge directly, bypassing the postMessage
622
+ * handshake. Used by the React provider when it needs to seed placeholder
623
+ * state (e.g. the standalone preview fallback when not embedded in Notion).
624
+ * The bridge applies the payload through the same code path as a real host.
625
+ */
626
+ export function setMockCustomBlockState(message) {
627
+ bridge.setMockState(message);
628
+ }
629
+ /**
630
+ * Thrown by {@link initCustomBlock} when the SDK is loaded in a top-level
631
+ * window (no parent frame) — `postMessage` would just hit the same window and
632
+ * the handshake can never complete. `<NotionCustomBlock>` catches this
633
+ * specifically and falls back to a standalone preview with a warning banner;
634
+ * direct callers can `instanceof` it to apply their own policy.
635
+ */
636
+ export class NotInIframeError extends Error {
637
+ constructor(message = NOT_IN_IFRAME_MESSAGE) {
638
+ super(message);
639
+ this.name = "NotInIframeError";
640
+ }
641
+ }
642
+ const DEFAULT_INIT_TIMEOUT_MS = 2000;
643
+ const NOT_IN_IFRAME_MESSAGE = "<NotionCustomBlock> only works inside an iframe — use the dev shell or deploy to Notion.";
644
+ let initPromise;
645
+ /**
646
+ * Performs the SDK ↔ host handshake: loads `custom_blocks.json`, posts
647
+ * `ready`, then awaits the host's first `init` message. Resolves with that
648
+ * payload; rejects with a
649
+ * `TimeoutError` if the host doesn't respond inside `timeoutMs`.
650
+ *
651
+ * Idempotent: subsequent calls return the same promise as the first and
652
+ * ignore any new options. Mount your React tree (or call any SDK hook /
653
+ * `subscribeToCustomBlockHost`) only after the returned promise resolves.
654
+ *
655
+ * @example
656
+ * // src/index.tsx
657
+ * const initial = await initCustomBlock()
658
+ * console.log(initial.theme, initial.context.customBlockId)
659
+ * ReactDOM.createRoot(root).render(<App />)
660
+ */
661
+ export function initCustomBlock(opts = {}) {
662
+ initPromise ?? (initPromise = (async () => {
663
+ // Standalone tab (no parent frame) — fail fast with a typed error
664
+ // rather than letting `awaitInit` time out, since `postMessage` to
665
+ // `window.parent` would just hit the same window and never arrive.
666
+ // `<NotionCustomBlock>` catches this specifically to render a warning
667
+ // banner + children for dev-time previews.
668
+ if (typeof window !== "undefined" && window.parent === window) {
669
+ throw new NotInIframeError();
670
+ }
671
+ const manifest = await tryFetchManifest();
672
+ await bridge.sendReady(manifest);
673
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS;
674
+ const message = await bridge.awaitInit(AbortSignal.timeout(timeoutMs));
675
+ const hostState = bridge.getHostState();
676
+ return {
677
+ theme: message.theme,
678
+ context: message.context,
679
+ dataSources: hostState.status === "initialized" ? hostState.dataSources : [],
680
+ };
681
+ })());
682
+ return initPromise;
683
+ }
684
+ const EMPTY_QUERY_VIEW = {
685
+ items: [],
686
+ propertySchemasById: {},
687
+ propertySchemasByKey: {},
688
+ isLoading: false,
689
+ hasMore: false,
690
+ };
691
+ export function getDataSourceQueryView(hostState, key) {
692
+ if (hostState.status !== "initialized") {
693
+ return EMPTY_QUERY_VIEW;
694
+ }
695
+ const dataSource = hostState.dataSources.find(entry => entry.key === key);
696
+ const queryState = hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState();
697
+ if (dataSource === undefined) {
698
+ return {
699
+ items: [],
700
+ collectionSchema: undefined,
701
+ propertySchemasById: {},
702
+ propertySchemasByKey: {},
703
+ isLoading: queryState.isLoading,
704
+ hasMore: queryState.hasMore,
705
+ error: queryState.error,
706
+ };
707
+ }
708
+ const propertyIdsByKey = dataSource.propertyIdsByKey;
709
+ const propertySchemasById = dataSource.propertySchemasById;
710
+ const resolvedItems = queryState.items.map(entry => {
711
+ const propertiesByKey = {};
712
+ for (const [key, propertyId] of Object.entries(propertyIdsByKey)) {
713
+ propertiesByKey[key] =
714
+ propertyId === undefined ? undefined : entry.propertiesById[propertyId];
715
+ }
716
+ return {
717
+ id: entry.id,
718
+ propertiesById: entry.propertiesById,
719
+ propertiesByKey,
720
+ update: input => updateDataSourcePage({
721
+ dataSource,
722
+ pageId: entry.id,
723
+ input,
724
+ }),
725
+ };
726
+ });
727
+ const propertySchemasByKey = {};
728
+ for (const [key, propertyId] of Object.entries(propertyIdsByKey)) {
729
+ propertySchemasByKey[key] =
730
+ propertyId === undefined ? undefined : propertySchemasById[propertyId];
731
+ }
732
+ return {
733
+ items: resolvedItems,
734
+ collectionSchema: dataSource.collectionSchema,
735
+ propertySchemasById,
736
+ propertySchemasByKey,
737
+ isLoading: queryState.isLoading,
738
+ hasMore: queryState.hasMore,
739
+ error: queryState.error,
740
+ };
741
+ }
742
+ function updateDataSourcePage(args) {
743
+ const { dataSource, pageId, input } = args;
744
+ const resolvedProperties = input.properties === undefined
745
+ ? undefined
746
+ : resolvePropertyWriteMapForDataSource({
747
+ dataSource,
748
+ properties: input.properties,
749
+ operationName: "dataSourcePage.update",
750
+ });
751
+ if (resolvedProperties?.status === "error") {
752
+ return Promise.resolve(resolvedProperties);
753
+ }
754
+ return bridge.updatePage({
755
+ pageId,
756
+ properties: resolvedProperties?.properties,
757
+ icon: input.icon,
758
+ cover: input.cover,
759
+ archived: input.archived,
760
+ });
761
+ }
762
+ export function createPage(input) {
763
+ return bridge.createPage(input);
764
+ }
765
+ export function getPage(pageId) {
766
+ return bridge.getPage(pageId);
767
+ }
768
+ export function updatePage(input) {
769
+ return bridge.updatePage(input);
770
+ }
771
+ export function postCustomBlockResize(height) {
772
+ bridge.postResize(height);
773
+ }