ncblock 0.0.4 → 0.0.6

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 (102) hide show
  1. package/HOST.md +68 -0
  2. package/dist/bridge/SandboxBridge.d.ts.map +1 -1
  3. package/dist/bridge/context.d.ts.map +1 -1
  4. package/dist/bridge/dataSources/dataSource.d.ts.map +1 -1
  5. package/dist/bridge/dataSources/dataSourcePage.d.ts.map +1 -1
  6. package/dist/bridge/dataSources/dataSourceValue.d.ts.map +1 -1
  7. package/dist/bridge/dataSources/dateValue.d.ts.map +1 -1
  8. package/dist/bridge/dataSources/propertySchema.d.ts.map +1 -1
  9. package/dist/bridge/dataSources/recordPointer.d.ts.map +1 -1
  10. package/dist/bridge/dataSources/resolve.d.ts.map +1 -1
  11. package/dist/bridge/dataSources/resolveProperty.d.ts.map +1 -1
  12. package/dist/bridge/hostState.d.ts.map +1 -1
  13. package/dist/bridge/ids.d.ts.map +1 -1
  14. package/dist/bridge/incomingType.d.ts.map +1 -1
  15. package/dist/bridge/loadManifest.d.ts.map +1 -1
  16. package/dist/bridge/manifest.d.ts.map +1 -1
  17. package/dist/bridge/messages/contextChanged.d.ts.map +1 -1
  18. package/dist/bridge/messages/createPage.d.ts.map +1 -1
  19. package/dist/bridge/messages/createPageResult.d.ts.map +1 -1
  20. package/dist/bridge/messages/dataSourcesChanged.d.ts.map +1 -1
  21. package/dist/bridge/messages/getPage.d.ts.map +1 -1
  22. package/dist/bridge/messages/getUser.d.ts.map +1 -1
  23. package/dist/bridge/messages/hostToSandbox.d.ts.map +1 -1
  24. package/dist/bridge/messages/init.d.ts.map +1 -1
  25. package/dist/bridge/messages/invalidHostMessage.d.ts.map +1 -1
  26. package/dist/bridge/messages/invalidSandboxMessage.d.ts.map +1 -1
  27. package/dist/bridge/messages/listUsers.d.ts.map +1 -1
  28. package/dist/bridge/messages/queryDataSource.d.ts.map +1 -1
  29. package/dist/bridge/messages/queryDataSourceResult.d.ts.map +1 -1
  30. package/dist/bridge/messages/ready.d.ts.map +1 -1
  31. package/dist/bridge/messages/resize.d.ts.map +1 -1
  32. package/dist/bridge/messages/sandboxToHost.d.ts.map +1 -1
  33. package/dist/bridge/messages/themeChanged.d.ts.map +1 -1
  34. package/dist/bridge/messages/updatePage.d.ts.map +1 -1
  35. package/dist/bridge/messages/updatePageResult.d.ts.map +1 -1
  36. package/dist/bridge/pages/page.d.ts.map +1 -1
  37. package/dist/bridge/pendingRequests.d.ts.map +1 -1
  38. package/dist/bridge/sandboxClient.d.ts.map +1 -1
  39. package/dist/bridge/theme.d.ts.map +1 -1
  40. package/dist/bridge/users/user.d.ts.map +1 -1
  41. package/dist/host.d.ts.map +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/init.d.ts.map +1 -1
  44. package/dist/react.d.ts.map +1 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/users.d.ts.map +1 -1
  47. package/dist/utils.d.ts.map +1 -1
  48. package/docs/context.md +45 -0
  49. package/docs/data-sources.md +161 -0
  50. package/docs/lifecycle.md +92 -0
  51. package/docs/manifest.md +42 -0
  52. package/docs/pages.md +143 -0
  53. package/docs/users.md +61 -0
  54. package/package.json +12 -9
  55. package/src/bridge/SandboxBridge.ts +659 -0
  56. package/src/bridge/context.ts +58 -0
  57. package/src/bridge/dataSources/dataSource.ts +63 -0
  58. package/src/bridge/dataSources/dataSourcePage.ts +69 -0
  59. package/src/bridge/dataSources/dataSourceValue.ts +19 -0
  60. package/src/bridge/dataSources/dateValue.ts +96 -0
  61. package/src/bridge/dataSources/propertySchema.ts +186 -0
  62. package/src/bridge/dataSources/recordPointer.ts +13 -0
  63. package/src/bridge/dataSources/resolve.ts +96 -0
  64. package/src/bridge/dataSources/resolveProperty.ts +96 -0
  65. package/src/bridge/hostState.ts +146 -0
  66. package/src/bridge/ids.ts +30 -0
  67. package/src/bridge/incomingType.ts +19 -0
  68. package/src/bridge/loadManifest.ts +54 -0
  69. package/src/bridge/manifest.ts +53 -0
  70. package/src/bridge/messages/contextChanged.ts +15 -0
  71. package/src/bridge/messages/createPage.ts +64 -0
  72. package/src/bridge/messages/createPageResult.ts +25 -0
  73. package/src/bridge/messages/dataSourcesChanged.ts +18 -0
  74. package/src/bridge/messages/getPage.ts +32 -0
  75. package/src/bridge/messages/getUser.ts +32 -0
  76. package/src/bridge/messages/hostToSandbox.ts +33 -0
  77. package/src/bridge/messages/init.ts +20 -0
  78. package/src/bridge/messages/invalidHostMessage.ts +16 -0
  79. package/src/bridge/messages/invalidSandboxMessage.ts +18 -0
  80. package/src/bridge/messages/listUsers.ts +33 -0
  81. package/src/bridge/messages/queryDataSource.ts +16 -0
  82. package/src/bridge/messages/queryDataSourceResult.ts +18 -0
  83. package/src/bridge/messages/ready.ts +25 -0
  84. package/src/bridge/messages/resize.ts +13 -0
  85. package/src/bridge/messages/sandboxToHost.ts +30 -0
  86. package/src/bridge/messages/themeChanged.ts +15 -0
  87. package/src/bridge/messages/updatePage.ts +21 -0
  88. package/src/bridge/messages/updatePageResult.ts +24 -0
  89. package/src/bridge/pages/page.ts +314 -0
  90. package/src/bridge/pendingRequests.ts +28 -0
  91. package/src/bridge/sandboxClient.ts +112 -0
  92. package/src/bridge/theme.ts +5 -0
  93. package/src/bridge/users/user.ts +31 -0
  94. package/src/host.ts +67 -0
  95. package/src/index.ts +86 -0
  96. package/src/init.ts +92 -0
  97. package/src/react.tsx +418 -0
  98. package/src/types.ts +157 -0
  99. package/src/users.ts +26 -0
  100. package/src/utils.ts +13 -0
  101. package/vite-plugin/index.d.ts +46 -0
  102. package/vite-plugin/index.js +115 -0
@@ -0,0 +1,146 @@
1
+ import type { NotionCustomBlockContext } from "./context"
2
+ import type { NotionDataSource } from "./dataSources/dataSource"
3
+ import type {
4
+ NotionDataSourcePage,
5
+ NotionDataSourcePageBridge,
6
+ NotionDataSourcePageUpdateInput,
7
+ NotionDataSourcePageUpdateResult,
8
+ } from "./dataSources/dataSourcePage"
9
+ import type { NotionDataSourceValue } from "./dataSources/dataSourceValue"
10
+ import type { NotionPropertySchema } from "./dataSources/propertySchema"
11
+ import type { NotionPageId } from "./pages/page"
12
+ import type { NotionTheme } from "./theme"
13
+
14
+ export type CustomBlockHostState = UninitializedHostState | InitializedHostState
15
+
16
+ export type UninitializedHostState = {
17
+ status: "uninitialized"
18
+ theme: NotionTheme
19
+ }
20
+
21
+ export type InitializedHostState = {
22
+ status: "initialized"
23
+ theme: NotionTheme
24
+ context: NotionCustomBlockContext
25
+ dataSources: NotionDataSource[]
26
+ dataSourceState: Record<string, DataSourceQueryState>
27
+ }
28
+
29
+ export type DataSourceQueryState = {
30
+ /** Latest pages from the host as parsed from the bridge. `propertiesByKey` is derived lazily. */
31
+ items: NotionDataSourcePageBridge[]
32
+ isLoading: boolean
33
+ hasMore: boolean
34
+ error?: string
35
+ latestRequestId?: string
36
+ }
37
+
38
+ export function createEmptyDataSourceQueryState(): DataSourceQueryState {
39
+ return {
40
+ items: [],
41
+ isLoading: false,
42
+ hasMore: false,
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Resolved page + schema views for a single data source, keyed by raw property
48
+ * ID and by user-defined key. Re-derived from `propertyIdsByKey` on every read so
49
+ * renames don't require a re-query.
50
+ */
51
+ export type DataSourceQueryView = {
52
+ items: NotionDataSourcePage[]
53
+ collectionSchema?: NotionDataSource["collectionSchema"]
54
+ propertySchemasById: { [propertyId: string]: NotionPropertySchema }
55
+ propertySchemasByKey: { [key: string]: NotionPropertySchema | undefined }
56
+ isLoading: boolean
57
+ hasMore: boolean
58
+ error?: string
59
+ }
60
+
61
+ const EMPTY_QUERY_VIEW: DataSourceQueryView = {
62
+ items: [],
63
+ propertySchemasById: {},
64
+ propertySchemasByKey: {},
65
+ isLoading: false,
66
+ hasMore: false,
67
+ }
68
+
69
+ /**
70
+ * Per-row callback that resolves property keys against the data source and forwards the
71
+ * resulting write to the bridge. Injected by callers (typically the SDK singleton in `init.ts`)
72
+ * so this module stays free of bridge-instance dependencies.
73
+ */
74
+ export type UpdateDataSourcePageFn = (args: {
75
+ dataSource: NotionDataSource
76
+ pageId: NotionPageId
77
+ input: NotionDataSourcePageUpdateInput
78
+ }) => Promise<NotionDataSourcePageUpdateResult>
79
+
80
+ export function getDataSourceQueryView(
81
+ hostState: CustomBlockHostState,
82
+ key: string,
83
+ updateDataSourcePage: UpdateDataSourcePageFn,
84
+ ): DataSourceQueryView {
85
+ if (hostState.status !== "initialized") {
86
+ return EMPTY_QUERY_VIEW
87
+ }
88
+
89
+ const dataSource = hostState.dataSources.find(entry => entry.key === key)
90
+ const queryState =
91
+ hostState.dataSourceState[key] ?? createEmptyDataSourceQueryState()
92
+
93
+ if (dataSource === undefined) {
94
+ return {
95
+ items: [],
96
+ collectionSchema: undefined,
97
+ propertySchemasById: {},
98
+ propertySchemasByKey: {},
99
+ isLoading: queryState.isLoading,
100
+ hasMore: queryState.hasMore,
101
+ error: queryState.error,
102
+ }
103
+ }
104
+
105
+ const propertyIdsByKey = dataSource.propertyIdsByKey
106
+ const propertySchemasById = dataSource.propertySchemasById
107
+
108
+ const resolvedItems: NotionDataSourcePage[] = queryState.items.map(entry => {
109
+ const propertiesByKey: {
110
+ [key: string]: NotionDataSourceValue | undefined
111
+ } = {}
112
+ for (const [key, propertyId] of Object.entries(propertyIdsByKey)) {
113
+ propertiesByKey[key] =
114
+ propertyId === undefined ? undefined : entry.propertiesById[propertyId]
115
+ }
116
+ return {
117
+ id: entry.id as NotionPageId,
118
+ propertiesById: entry.propertiesById,
119
+ propertiesByKey,
120
+ update: input =>
121
+ updateDataSourcePage({
122
+ dataSource,
123
+ pageId: entry.id as NotionPageId,
124
+ input,
125
+ }),
126
+ }
127
+ })
128
+
129
+ const propertySchemasByKey: {
130
+ [key: string]: NotionPropertySchema | undefined
131
+ } = {}
132
+ for (const [key, propertyId] of Object.entries(propertyIdsByKey)) {
133
+ propertySchemasByKey[key] =
134
+ propertyId === undefined ? undefined : propertySchemasById[propertyId]
135
+ }
136
+
137
+ return {
138
+ items: resolvedItems,
139
+ collectionSchema: dataSource.collectionSchema,
140
+ propertySchemasById,
141
+ propertySchemasByKey,
142
+ isLoading: queryState.isLoading,
143
+ hasMore: queryState.hasMore,
144
+ error: queryState.error,
145
+ }
146
+ }
@@ -0,0 +1,30 @@
1
+ import * as v from "valibot"
2
+
3
+ declare const notionDataSourceIdBrand: unique symbol
4
+ declare const notionSpaceIdBrand: unique symbol
5
+
6
+ /**
7
+ * Branded Notion data source ID. Host payloads and SDK APIs use plain strings at runtime,
8
+ * but the brand keeps data source IDs from being accidentally mixed with other IDs in TypeScript.
9
+ */
10
+ export type NotionDataSourceId = string & {
11
+ readonly [notionDataSourceIdBrand]: "NotionDataSourceId"
12
+ }
13
+
14
+ export const notionDataSourceIdSchema = v.custom<NotionDataSourceId>(
15
+ value => typeof value === "string",
16
+ "Expected a Notion data source ID",
17
+ )
18
+
19
+ /**
20
+ * Branded Notion workspace/space ID. Host payloads and SDK APIs use plain strings at runtime,
21
+ * but the brand keeps space IDs from being accidentally mixed with other IDs in TypeScript.
22
+ */
23
+ export type NotionSpaceId = string & {
24
+ readonly [notionSpaceIdBrand]: "NotionSpaceId"
25
+ }
26
+
27
+ export const notionSpaceIdSchema = v.custom<NotionSpaceId>(
28
+ value => typeof value === "string",
29
+ "Expected a Notion space ID",
30
+ )
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Reads the `type` field off an inbound bridge message, if any.
3
+ *
4
+ * Used by both sides of the bridge after the canonical message schema rejects a payload. We still
5
+ * want to know what type the sender claimed to be sending so we can (a) avoid NACK loops on
6
+ * `invalidHostMessage` / `invalidSandboxMessage` and (b) include the type in the failure reason
7
+ * for debugging.
8
+ */
9
+ export function readIncomingType(data: unknown): string | undefined {
10
+ if (
11
+ typeof data === "object" &&
12
+ data !== null &&
13
+ "type" in data &&
14
+ typeof data.type === "string"
15
+ ) {
16
+ return data.type
17
+ }
18
+ return undefined
19
+ }
@@ -0,0 +1,54 @@
1
+ import * as v from "valibot"
2
+ import { type CustomBlockManifest, manifestSchema } from "./manifest"
3
+
4
+ const MANIFEST_URL = "custom_blocks.json"
5
+
6
+ /**
7
+ * Attempts to load a `custom_blocks.json` manifest co-located with the bundle. Treats
8
+ * any non-200 response as "no manifest". Warns on every failure path —
9
+ * templates are expected to ship a manifest, so a missing one is worth
10
+ * surfacing.
11
+ */
12
+ export async function loadManifest(): Promise<CustomBlockManifest | null> {
13
+ if (typeof fetch !== "function") {
14
+ console.warn(
15
+ `[notion-custom-sdk] no \`fetch\` available; cannot load ${MANIFEST_URL}`,
16
+ )
17
+ return null
18
+ }
19
+ let response: Response
20
+ try {
21
+ response = await fetch(MANIFEST_URL, { credentials: "omit" })
22
+ } catch (error) {
23
+ console.warn(
24
+ `[notion-custom-sdk] no manifest fetched from ${MANIFEST_URL}`,
25
+ error,
26
+ )
27
+ return null
28
+ }
29
+ if (!response.ok) {
30
+ console.warn(
31
+ `[notion-custom-sdk] no manifest at ${MANIFEST_URL} (status ${response.status})`,
32
+ )
33
+ return null
34
+ }
35
+ let json: unknown
36
+ try {
37
+ json = await response.json()
38
+ } catch (error) {
39
+ console.warn(
40
+ `[notion-custom-sdk] manifest at ${MANIFEST_URL} was not valid JSON`,
41
+ error,
42
+ )
43
+ return null
44
+ }
45
+ const parsed = v.safeParse(manifestSchema, json)
46
+ if (!parsed.success) {
47
+ console.warn(
48
+ `[notion-custom-sdk] manifest at ${MANIFEST_URL} did not match schema`,
49
+ parsed.issues,
50
+ )
51
+ return null
52
+ }
53
+ return parsed.output
54
+ }
@@ -0,0 +1,53 @@
1
+ import * as v from "valibot"
2
+ import { notionPropertyTypeSchema } from "./dataSources/propertySchema"
3
+
4
+ /**
5
+ * User-authored manifest declaring the data sources the custom block expects.
6
+ * Lives at `custom_blocks.json` in the project root and is forwarded to the host with
7
+ * the bridge `ready` message so the host can pre-bind data sources, surface
8
+ * configuration UI, etc. The `notionCustomBlock` Vite plugin from
9
+ * `ncblock/vite` wires the JSON file into the dev server and
10
+ * the build output.
11
+ */
12
+
13
+ /**
14
+ * Decorative icon attached to a manifest data source. Mirrors the
15
+ * `emoji` / `external` icon variants the public Notion API uses, so the host
16
+ * can render a recognizable affordance next to the slot in setup UI.
17
+ */
18
+ export const manifestIconSchema = v.variant("type", [
19
+ v.object({
20
+ type: v.literal("emoji"),
21
+ emoji: v.string(),
22
+ }),
23
+ v.object({
24
+ type: v.literal("external"),
25
+ url: v.string(),
26
+ }),
27
+ ])
28
+
29
+ export type ManifestIcon = v.InferOutput<typeof manifestIconSchema>
30
+
31
+ export const manifestPropertySchema = v.object({
32
+ name: v.string(),
33
+ description: v.optional(v.string()),
34
+ type: notionPropertyTypeSchema,
35
+ })
36
+
37
+ export type ManifestProperty = v.InferOutput<typeof manifestPropertySchema>
38
+
39
+ export const manifestDataSourceSchema = v.object({
40
+ name: v.string(),
41
+ description: v.optional(v.string()),
42
+ icon: v.optional(manifestIconSchema),
43
+ properties: v.optional(v.record(v.string(), manifestPropertySchema), {}),
44
+ })
45
+
46
+ export type ManifestDataSource = v.InferOutput<typeof manifestDataSourceSchema>
47
+
48
+ export const manifestSchema = v.object({
49
+ version: v.literal(1),
50
+ dataSources: v.record(v.string(), manifestDataSourceSchema),
51
+ })
52
+
53
+ export type CustomBlockManifest = v.InferOutput<typeof manifestSchema>
@@ -0,0 +1,15 @@
1
+ import * as v from "valibot"
2
+ import { notionCustomBlockContextSchema } from "../context"
3
+
4
+ /**
5
+ * Message sent by the host whenever the custom block's position in the block tree changes (e.g. it
6
+ * is moved into a different parent block, or its enclosing page changes).
7
+ */
8
+ export const contextChangedMessageSchema = v.object({
9
+ type: v.literal("contextChanged"),
10
+ context: notionCustomBlockContextSchema,
11
+ })
12
+
13
+ export type ContextChangedMessage = v.InferOutput<
14
+ typeof contextChangedMessageSchema
15
+ >
@@ -0,0 +1,64 @@
1
+ import * as v from "valibot"
2
+ import type { NotionDataSourceId } from "../ids"
3
+ import type { NotionPageId } from "../pages/page"
4
+ import {
5
+ notionPageCoverSchema,
6
+ notionPageIconSchema,
7
+ notionPagePropertyWriteMapSchema,
8
+ } from "../pages/page"
9
+
10
+ /**
11
+ * Describes where the newly created page should be inserted.
12
+ * - `start` / `end` (default): prepend / append to the request's `parent`
13
+ * page's children.
14
+ * - `before` / `after`: insert as a sibling of the anchor block identified by
15
+ * `blockId`, using the anchor's own structural parent. The anchor can be
16
+ * nested anywhere below the page (inside a toggle, column, callout, etc.).
17
+ */
18
+ export const notionCreatePagePositionSchema = v.variant("type", [
19
+ v.object({ type: v.literal("start") }),
20
+ v.object({ type: v.literal("end") }),
21
+ v.object({ type: v.literal("before"), blockId: v.string() }),
22
+ v.object({ type: v.literal("after"), blockId: v.string() }),
23
+ ])
24
+
25
+ export type NotionCreatePagePosition = v.InferOutput<
26
+ typeof notionCreatePagePositionSchema
27
+ >
28
+
29
+ /**
30
+ * Parent accepted by the outbound `createPage` bridge message.
31
+ *
32
+ * Mirrors Notion's public API `POST /v1/pages` parent shape: `page_id` for a page-parented child,
33
+ * `data_source_id` for a database row. `data_source_id` is the internal collection ID.
34
+ *
35
+ * The SDK additionally accepts a higher-level `data_source_key` variant that resolves to
36
+ * `data_source_id` locally, using the `dataSources` mapping.
37
+ */
38
+ export const createPageParentSchema = v.variant("type", [
39
+ v.object({ type: v.literal("page_id"), page_id: v.string() }),
40
+ v.object({
41
+ type: v.literal("data_source_id"),
42
+ data_source_id: v.string(),
43
+ }),
44
+ ])
45
+
46
+ export type CreatePageMessageParent =
47
+ | { type: "page_id"; page_id: NotionPageId }
48
+ | { type: "data_source_id"; data_source_id: NotionDataSourceId }
49
+
50
+ /**
51
+ * Message sent by the sandbox to ask the host to create a new page on its behalf.
52
+ * The payload mirrors Notion's public `POST /v1/pages` API.
53
+ */
54
+ export const createPageMessageSchema = v.object({
55
+ type: v.literal("createPage"),
56
+ requestId: v.string(),
57
+ parent: createPageParentSchema,
58
+ properties: notionPagePropertyWriteMapSchema,
59
+ icon: v.optional(notionPageIconSchema),
60
+ cover: v.optional(notionPageCoverSchema),
61
+ position: v.optional(notionCreatePagePositionSchema),
62
+ })
63
+
64
+ export type CreatePageMessage = v.InferOutput<typeof createPageMessageSchema>
@@ -0,0 +1,25 @@
1
+ import * as v from "valibot"
2
+ import { notionPageSchema } from "../pages/page"
3
+
4
+ /**
5
+ * Message sent by the host in response to a sandbox `createPage` request.
6
+ */
7
+ export const createPageResultMessageSchema = v.variant("status", [
8
+ v.object({
9
+ type: v.literal("createPageResult"),
10
+ requestId: v.string(),
11
+ status: v.literal("success"),
12
+ /** The newly created page. */
13
+ page: notionPageSchema,
14
+ }),
15
+ v.object({
16
+ type: v.literal("createPageResult"),
17
+ requestId: v.string(),
18
+ status: v.literal("error"),
19
+ error: v.string(),
20
+ }),
21
+ ])
22
+
23
+ export type CreatePageResultMessage = v.InferOutput<
24
+ typeof createPageResultMessageSchema
25
+ >
@@ -0,0 +1,18 @@
1
+ import * as v from "valibot"
2
+ import { notionDataSourceBindingsSchema } from "../dataSources/dataSource"
3
+
4
+ /**
5
+ * Message sent by the host whenever the custom block's data-source mapping changes (e.g. a key is
6
+ * added, removed, or remapped). The SDK replaces its list of configured data sources but keeps any
7
+ * in-flight `useDataSource` query state for keys that still exist.
8
+ */
9
+ export const dataSourcesChangedMessageSchema = v.object({
10
+ type: v.literal("dataSourcesChanged"),
11
+ dataSources: v.object({
12
+ bindings: notionDataSourceBindingsSchema,
13
+ }),
14
+ })
15
+
16
+ export type DataSourcesChangedMessage = v.InferOutput<
17
+ typeof dataSourcesChangedMessageSchema
18
+ >
@@ -0,0 +1,32 @@
1
+ import * as v from "valibot"
2
+ import { notionPageSchema } from "../pages/page"
3
+
4
+ /**
5
+ * Sandbox -> host: fetch a page by ID.
6
+ */
7
+ export const getPageMessageSchema = v.object({
8
+ type: v.literal("getPage"),
9
+ requestId: v.string(),
10
+ pageId: v.string(),
11
+ })
12
+
13
+ export type GetPageMessage = v.InferOutput<typeof getPageMessageSchema>
14
+
15
+ export const getPageResultMessageSchema = v.variant("status", [
16
+ v.object({
17
+ type: v.literal("getPageResult"),
18
+ requestId: v.string(),
19
+ status: v.literal("success"),
20
+ page: notionPageSchema,
21
+ }),
22
+ v.object({
23
+ type: v.literal("getPageResult"),
24
+ requestId: v.string(),
25
+ status: v.literal("error"),
26
+ error: v.string(),
27
+ }),
28
+ ])
29
+
30
+ export type GetPageResultMessage = v.InferOutput<
31
+ typeof getPageResultMessageSchema
32
+ >
@@ -0,0 +1,32 @@
1
+ import * as v from "valibot"
2
+ import { notionUserSchema } from "../users/user"
3
+
4
+ /**
5
+ * Sandbox -> host: fetch a user by ID.
6
+ */
7
+ export const getUserMessageSchema = v.object({
8
+ type: v.literal("getUser"),
9
+ requestId: v.string(),
10
+ userId: v.string(),
11
+ })
12
+
13
+ export type GetUserMessage = v.InferOutput<typeof getUserMessageSchema>
14
+
15
+ export const getUserResultMessageSchema = v.variant("status", [
16
+ v.object({
17
+ type: v.literal("getUserResult"),
18
+ requestId: v.string(),
19
+ status: v.literal("success"),
20
+ user: notionUserSchema,
21
+ }),
22
+ v.object({
23
+ type: v.literal("getUserResult"),
24
+ requestId: v.string(),
25
+ status: v.literal("error"),
26
+ error: v.string(),
27
+ }),
28
+ ])
29
+
30
+ export type GetUserResultMessage = v.InferOutput<
31
+ typeof getUserResultMessageSchema
32
+ >
@@ -0,0 +1,33 @@
1
+ import * as v from "valibot"
2
+ import { contextChangedMessageSchema } from "./contextChanged"
3
+ import { createPageResultMessageSchema } from "./createPageResult"
4
+ import { dataSourcesChangedMessageSchema } from "./dataSourcesChanged"
5
+ import { getPageResultMessageSchema } from "./getPage"
6
+ import { getUserResultMessageSchema } from "./getUser"
7
+ import { initMessageSchema } from "./init"
8
+ import { invalidSandboxMessageSchema } from "./invalidSandboxMessage"
9
+ import { listUsersResultMessageSchema } from "./listUsers"
10
+ import { queryDataSourceResultMessageSchema } from "./queryDataSourceResult"
11
+ import { themeChangedMessageSchema } from "./themeChanged"
12
+ import { updatePageResultMessageSchema } from "./updatePageResult"
13
+
14
+ /**
15
+ * Discriminated union of every message the host is allowed to send the sandbox.
16
+ */
17
+ export const hostToSandboxMessageSchema = v.variant("type", [
18
+ initMessageSchema,
19
+ themeChangedMessageSchema,
20
+ contextChangedMessageSchema,
21
+ dataSourcesChangedMessageSchema,
22
+ queryDataSourceResultMessageSchema,
23
+ createPageResultMessageSchema,
24
+ getPageResultMessageSchema,
25
+ getUserResultMessageSchema,
26
+ listUsersResultMessageSchema,
27
+ updatePageResultMessageSchema,
28
+ invalidSandboxMessageSchema,
29
+ ])
30
+
31
+ export type HostToSandboxMessage = v.InferOutput<
32
+ typeof hostToSandboxMessageSchema
33
+ >
@@ -0,0 +1,20 @@
1
+ import * as v from "valibot"
2
+ import { notionCustomBlockContextSchema } from "../context"
3
+ import { notionDataSourceBindingsSchema } from "../dataSources/dataSource"
4
+ import { notionThemeSchema } from "../theme"
5
+
6
+ /**
7
+ * Initialization message sent by the host to the sandbox exactly once, in response to the
8
+ * sandbox's `ready` message. After init, live updates flow through narrower messages
9
+ * (`themeChanged`, `contextChanged`, `dataSourcesChanged`).
10
+ */
11
+ export const initMessageSchema = v.object({
12
+ type: v.literal("init"),
13
+ theme: notionThemeSchema,
14
+ context: notionCustomBlockContextSchema,
15
+ dataSources: v.object({
16
+ bindings: notionDataSourceBindingsSchema,
17
+ }),
18
+ })
19
+
20
+ export type InitMessage = v.InferOutput<typeof initMessageSchema>
@@ -0,0 +1,16 @@
1
+ import * as v from "valibot"
2
+
3
+ /**
4
+ * Sent by the sandbox back to the host when an inbound host-to-sandbox message could not be parsed
5
+ * (unknown `type`, schema mismatch, etc.). The host should never reply to an `invalidHostMessage`
6
+ * with another bridge error message.
7
+ */
8
+ export const invalidHostMessageSchema = v.object({
9
+ type: v.literal("invalidHostMessage"),
10
+ /**
11
+ * A human-readable string intended for sandbox-side logging. It has no structured contract.
12
+ */
13
+ reason: v.string(),
14
+ })
15
+
16
+ export type InvalidHostMessage = v.InferOutput<typeof invalidHostMessageSchema>
@@ -0,0 +1,18 @@
1
+ import * as v from "valibot"
2
+
3
+ /**
4
+ * Sent by the host back to the sandbox when an inbound sandbox-to-host message could not be parsed
5
+ * (unknown `type`, schema mismatch, etc.). The sandbox should never reply to an
6
+ * `invalidSandboxMessage` with another bridge error message.
7
+ */
8
+ export const invalidSandboxMessageSchema = v.object({
9
+ type: v.literal("invalidSandboxMessage"),
10
+ /**
11
+ * A human-readable string intended for sandbox-side logging. It has no structured contract.
12
+ */
13
+ reason: v.string(),
14
+ })
15
+
16
+ export type InvalidSandboxMessage = v.InferOutput<
17
+ typeof invalidSandboxMessageSchema
18
+ >
@@ -0,0 +1,33 @@
1
+ import * as v from "valibot"
2
+ import { notionUserListSchema } from "../users/user"
3
+
4
+ /**
5
+ * Sandbox -> host: list users visible in the custom block's workspace.
6
+ */
7
+ export const listUsersMessageSchema = v.object({
8
+ type: v.literal("listUsers"),
9
+ requestId: v.string(),
10
+ startCursor: v.optional(v.string()),
11
+ pageSize: v.optional(v.number()),
12
+ })
13
+
14
+ export type ListUsersMessage = v.InferOutput<typeof listUsersMessageSchema>
15
+
16
+ export const listUsersResultMessageSchema = v.variant("status", [
17
+ v.object({
18
+ type: v.literal("listUsersResult"),
19
+ requestId: v.string(),
20
+ status: v.literal("success"),
21
+ list: notionUserListSchema,
22
+ }),
23
+ v.object({
24
+ type: v.literal("listUsersResult"),
25
+ requestId: v.string(),
26
+ status: v.literal("error"),
27
+ error: v.string(),
28
+ }),
29
+ ])
30
+
31
+ export type ListUsersResultMessage = v.InferOutput<
32
+ typeof listUsersResultMessageSchema
33
+ >
@@ -0,0 +1,16 @@
1
+ import * as v from "valibot"
2
+
3
+ /**
4
+ * Message sent by the sandbox to ask the host to load (or reload) a data
5
+ * source with a given page size.
6
+ */
7
+ export const queryDataSourceMessageSchema = v.object({
8
+ type: v.literal("queryDataSource"),
9
+ requestId: v.string(),
10
+ key: v.string(),
11
+ limit: v.number(),
12
+ })
13
+
14
+ export type QueryDataSourceMessage = v.InferOutput<
15
+ typeof queryDataSourceMessageSchema
16
+ >
@@ -0,0 +1,18 @@
1
+ import * as v from "valibot"
2
+ import { notionDataSourcePageBridgeSchema } from "../dataSources/dataSourcePage"
3
+
4
+ /**
5
+ * Message sent by the host to the sandbox in response to a `queryDataSource` request.
6
+ */
7
+ export const queryDataSourceResultMessageSchema = v.object({
8
+ type: v.literal("queryDataSourceResult"),
9
+ requestId: v.string(),
10
+ key: v.string(),
11
+ items: v.array(notionDataSourcePageBridgeSchema),
12
+ hasMore: v.boolean(),
13
+ error: v.optional(v.string()),
14
+ })
15
+
16
+ export type QueryDataSourceResultMessage = v.InferOutput<
17
+ typeof queryDataSourceResultMessageSchema
18
+ >