@notionhq/custom-blocks 0.0.78 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/HOST.md +9 -2
  2. package/README.md +4 -2
  3. package/dist/bridge/SandboxBridge.d.ts +1 -1
  4. package/dist/bridge/SandboxBridge.js +1 -1
  5. package/dist/bridge/messages/connect.d.ts +1 -1
  6. package/dist/bridge/messages/connect.d.ts.map +1 -1
  7. package/dist/bridge/messages/connect.js +2 -8
  8. package/dist/bridge/messages/sandboxToHost.d.ts +1 -1
  9. package/dist/host/createCustomBlockHost.d.ts +1 -1
  10. package/dist/host/createCustomBlockHost.d.ts.map +1 -1
  11. package/dist/host/createCustomBlockHost.js +17 -4
  12. package/dist/host/lifecycle/{ready.d.ts → protocolVersion.d.ts} +1 -1
  13. package/dist/host/lifecycle/protocolVersion.d.ts.map +1 -0
  14. package/dist/version.js +1 -1
  15. package/docs/data-sources.md +7 -5
  16. package/docs/vite-plugin.md +46 -0
  17. package/package.json +1 -2
  18. package/src/bridge/SandboxBridge.ts +1 -1
  19. package/src/bridge/messages/connect.ts +2 -9
  20. package/src/host/createCustomBlockHost.ts +25 -5
  21. package/bin/cli/attach.js +0 -180
  22. package/bin/cli/cli.js +0 -248
  23. package/bin/cli/create.js +0 -106
  24. package/bin/cli/datasources.js +0 -255
  25. package/bin/cli/deploy.js +0 -109
  26. package/bin/cli/ids.js +0 -13
  27. package/bin/cli/ntn.js +0 -95
  28. package/bin/cli/pullData.js +0 -76
  29. package/bin/cli/pullManifest.js +0 -158
  30. package/bin/cli/target.js +0 -95
  31. package/bin/src/bridge/dataSources/propertySchema.js +0 -148
  32. package/bin/src/bridge/manifest.js +0 -40
  33. package/dist/host/lifecycle/ready.d.ts.map +0 -1
  34. package/docs/manifest.md +0 -42
  35. /package/dist/host/lifecycle/{ready.js → protocolVersion.js} +0 -0
  36. /package/src/host/lifecycle/{ready.ts → protocolVersion.ts} +0 -0
package/HOST.md CHANGED
@@ -25,6 +25,7 @@ import { createCustomBlockHost } from "@notionhq/custom-blocks/host";
25
25
 
26
26
  const host = createCustomBlockHost({
27
27
  iframe,
28
+ minBridgeProtocolVersion: 3,
28
29
  initialState: {
29
30
  theme,
30
31
  blockId,
@@ -35,14 +36,20 @@ const host = createCustomBlockHost({
35
36
  dataSources: { bindings },
36
37
  },
37
38
  handlers: {
38
- queryDataSource: async (message) => ({ status: "success", items: [], hasMore: false }),
39
+ queryDataSource: async (message) => ({
40
+ status: "success",
41
+ items: [],
42
+ hasMore: false,
43
+ }),
39
44
  },
40
45
  });
41
46
  ```
42
47
 
43
48
  Use it when the generic protocol loop is helpful. Hosts with more specialized runtime needs can keep using the schemas and message types directly.
44
49
 
45
- Related types: `CustomBlockHostOptions`, `CustomBlockHostInitialState`, `CustomBlockHostInitialDataSources`, `CustomBlockHostDataSourcesPayload`, `ConnectSuccessMessage`, `CustomBlockHostHandlers`, `CustomBlockHostHandle`, `CustomBlockHostLogDirection`, `CustomBlockHostQueryDataSourceResult`, `CustomBlockHostCreatePageResult`, `CustomBlockHostGetPageResult`, `CustomBlockHostUpdatePageResult`, `CustomBlockHostGetUserResult`, and `CustomBlockHostListUsersResult`.
50
+ Responses return a `status`-discriminated result: `"success"` carries `items` and required `hasMore`; `"error"` carries the structured `error`. Set `hasMore` to `false` when no additional results are available.
51
+
52
+ Related exports: `CustomBlockHostOptions`, `CustomBlockHostInitialState`, `CustomBlockHostInitialDataSources`, `CustomBlockHostDataSourcesPayload`, `ConnectSuccessMessage`, `CustomBlockHostHandlers`, `CustomBlockHostHandle`, `CustomBlockHostLogDirection`, `CustomBlockHostQueryDataSourceResult`, `CustomBlockHostCreatePageResult`, `CustomBlockHostGetPageResult`, `CustomBlockHostUpdatePageResult`, `CustomBlockHostGetUserResult`, and `CustomBlockHostListUsersResult`.
46
53
 
47
54
  ## Bridge protocol
48
55
 
package/README.md CHANGED
@@ -35,7 +35,7 @@ export function App() {
35
35
  }
36
36
  ```
37
37
 
38
- `<NotionCustomBlock>` runs the SDK ↔ host handshake (sends `ready` with the manifest, awaits `init`) and only mounts `children` once it resolves. Inside the wrapper, every hook returns non-nullable values — there's no separate gating component to write. It also runs `useCustomBlockAutoResize` for you by default; pass `autoResize={false}` to opt out.
38
+ `<NotionCustomBlock>` runs the SDK ↔ host handshake (`connect` `init` `initResult`) and only mounts `children` once it resolves. Inside the wrapper, every hook returns non-nullable values — there's no separate gating component to write. It also runs `useCustomBlockAutoResize` for you by default; pass `autoResize={false}` to opt out.
39
39
 
40
40
  ## Reference
41
41
 
@@ -47,7 +47,9 @@ API surface, one page per category. Import framework-neutral APIs from `@notionh
47
47
  - [`docs/pages.md`](./docs/pages.md) — `pages.create / get / update / delete`, parent variants (including the recommended `data_source_key`), property input shapes.
48
48
  - [`docs/users.md`](./docs/users.md) — `users.list / get`, the `NotionUser` shape, paging.
49
49
  - [`docs/errors.md`](./docs/errors.md) — request results, error format, error codes, retries, and initialization failures.
50
- - [`docs/manifest.md`](./docs/manifest.md) — `custom_blocks.json`, the Vite plugin, manifest types.
50
+ - [`docs/vite-plugin.md`](./docs/vite-plugin.md) — the `notionCustomBlock()` Vite plugin and `custom_blocks.json`, the block's local-preview manifest.
51
+
52
+ Declare the block source and data-source schema in the worker file with `worker.customBlock(...)`, then build and deploy it with `ntn workers deploy`. The worker definition is the source of truth; the iframe frontend uses semantic keys from that definition with APIs such as `useDataSource`. For local previews, the Vite plugin serves the block's `custom_blocks.json` — see [`docs/vite-plugin.md`](./docs/vite-plugin.md).
51
53
 
52
54
  ## Forbidden APIs
53
55
 
@@ -9,7 +9,7 @@ import { type InitMessage } from "./messages/init.js";
9
9
  * single host needs to support multiple custom blocks built with different versions of the bridge
10
10
  * protocol. Increment this number any time a breaking change is made to the bridge protocol.
11
11
  */
12
- export declare const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 2;
12
+ export declare const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3;
13
13
  /**
14
14
  * A single entry in the bridge message log. Kept intentionally plain so the log
15
15
  * is copy-pasteable to a local coding agent without needing extra context.
@@ -13,7 +13,7 @@ import { PendingRequests } from "./pendingRequests.js";
13
13
  * single host needs to support multiple custom blocks built with different versions of the bridge
14
14
  * protocol. Increment this number any time a breaking change is made to the bridge protocol.
15
15
  */
16
- export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 2;
16
+ export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3;
17
17
  export class SandboxBridge {
18
18
  constructor() {
19
19
  this.hostState = {
@@ -41,7 +41,7 @@ export declare const connectMessageSchema: v.VariantSchema<"status", [v.ObjectSc
41
41
  readonly error: v.ObjectSchema<{
42
42
  readonly code: v.StringSchema<undefined>;
43
43
  readonly message: v.StringSchema<undefined>;
44
- readonly isRetryable: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
44
+ readonly isRetryable: v.BooleanSchema<undefined>;
45
45
  }, undefined>;
46
46
  }, undefined>], undefined>;
47
47
  export type ConnectMessage = v.InferOutput<typeof connectMessageSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/messages/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AA2B5B;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;IAG/B;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BASH,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,oBAAoB,CAAC,CAAA"}
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/bridge/messages/connect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAA;AAoB5B;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;IAG/B;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BASH,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,oBAAoB,CAAC,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import * as v from "valibot";
2
+ import { customBlockErrorInfoSchema } from "../errors.js";
2
3
  import { manifestSchema } from "../manifest.js";
3
4
  const connectMessageCommonEntries = {
4
5
  type: v.literal("connect"),
@@ -15,13 +16,6 @@ const connectMessageCommonEntries = {
15
16
  */
16
17
  sdkVersion: v.string(),
17
18
  };
18
- // TODO(custom-blocks): Replace with `customBlockErrorInfoSchema` which makes `isRetryable` required
19
- // once min supported bridge protocol version is bumped to 3.
20
- const connectErrorSchema = v.object({
21
- code: v.string(),
22
- message: v.string(),
23
- isRetryable: v.optional(v.boolean(), false),
24
- });
25
19
  /**
26
20
  * First message the sandbox sends after mount. It identifies the sandbox and starts the bridge
27
21
  * initialization exchange. The host echoes `initializationId` in its `init` response.
@@ -38,7 +32,7 @@ export const connectMessageSchema = v.variant("status", [
38
32
  }),
39
33
  v.object({
40
34
  status: v.literal("error"),
41
- error: connectErrorSchema,
35
+ error: customBlockErrorInfoSchema,
42
36
  ...connectMessageCommonEntries,
43
37
  }),
44
38
  ]);
@@ -37,7 +37,7 @@ export declare const sandboxToHostMessageSchema: v.UnionSchema<[v.VariantSchema<
37
37
  readonly error: v.ObjectSchema<{
38
38
  readonly code: v.StringSchema<undefined>;
39
39
  readonly message: v.StringSchema<undefined>;
40
- readonly isRetryable: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
40
+ readonly isRetryable: v.BooleanSchema<undefined>;
41
41
  }, undefined>;
42
42
  }, undefined>], undefined>, v.VariantSchema<"status", [v.ObjectSchema<{
43
43
  readonly type: v.LiteralSchema<"initResult", undefined>;
@@ -11,7 +11,7 @@ import type { CustomBlockHostQueryDataSourceResult } from "./queries/types.js";
11
11
  export type CustomBlockHostOptions = {
12
12
  iframe: HTMLIFrameElement;
13
13
  targetOrigin?: string;
14
- minBridgeProtocolVersion?: number;
14
+ minBridgeProtocolVersion: number;
15
15
  noConnectTimeoutMs?: number;
16
16
  noInitResultTimeoutMs?: number;
17
17
  initialState: CustomBlockHostInitialState;
@@ -1 +1 @@
1
- {"version":3,"file":"createCustomBlockHost.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/createCustomBlockHost.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAc1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAA;AAM/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA;AAazE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAIzD,OAAO,KAAK,EACX,qBAAqB,EACrB,iCAAiC,EACjC,2BAA2B,EAE3B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACX,uBAAuB,EACvB,2BAA2B,EAC3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,oBAAoB,CAAA;AAE9E,MAAM,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,iBAAiB,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,YAAY,EAAE,2BAA2B,CAAA;IACzC,QAAQ,EAAE,uBAAuB,CAAA;IACjC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAA;IACpD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACnD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,2BAA2B,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC1E,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,IAAI,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACtC,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,cAAc,EAAE,CAAC,WAAW,EAAE,iCAAiC,KAAK,IAAI,CAAA;IACxE,cAAc,EAAE,CAAC,WAAW,EAAE,UAAU,KAAK,IAAI,CAAA;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE;QACpB,YAAY,EAAE,kBAAkB,CAAA;QAChC,QAAQ,EAAE,oCAAoC,CAAA;KAC9C,KAAK,IAAI,CAAA;CACV,CAAA;AAMD,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,sBAAsB,GAC7B,qBAAqB,CA8evB"}
1
+ {"version":3,"file":"createCustomBlockHost.d.ts","sourceRoot":"","sources":["../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/createCustomBlockHost.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAc1D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAA;AAM/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAA;AAazE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAIzD,OAAO,KAAK,EACX,qBAAqB,EACrB,iCAAiC,EACjC,2BAA2B,EAE3B,MAAM,sBAAsB,CAAA;AAE7B,OAAO,KAAK,EACX,uBAAuB,EACvB,2BAA2B,EAC3B,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EAAE,oCAAoC,EAAE,MAAM,oBAAoB,CAAA;AAE9E,MAAM,MAAM,sBAAsB,GAAG;IACpC,MAAM,EAAE,iBAAiB,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wBAAwB,EAAE,MAAM,CAAA;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,YAAY,EAAE,2BAA2B,CAAA;IACzC,QAAQ,EAAE,uBAAuB,CAAA;IACjC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAA;IACpD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAA;IACnD,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,2BAA2B,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;CAC1E,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG;IACnC,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,IAAI,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAA;IAC7C,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;IACtC,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;IACzC,OAAO,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC,cAAc,EAAE,CAAC,WAAW,EAAE,iCAAiC,KAAK,IAAI,CAAA;IACxE,cAAc,EAAE,CAAC,WAAW,EAAE,UAAU,KAAK,IAAI,CAAA;IACjD,YAAY,EAAE,CAAC,IAAI,EAAE;QACpB,YAAY,EAAE,kBAAkB,CAAA;QAChC,QAAQ,EAAE,oCAAoC,CAAA;KAC9C,KAAK,IAAI,CAAA;CACV,CAAA;AAQD,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,sBAAsB,GAC7B,qBAAqB,CA+fvB"}
@@ -1,15 +1,19 @@
1
1
  import * as v from "valibot";
2
2
  import { sandboxToHostMessageSchema } from "../bridge/messages/sandboxToHost.js";
3
+ import { CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION } from "../bridge/SandboxBridge.js";
3
4
  import { unreachable } from "../utils.js";
4
5
  import { initErrorForFailureReason } from "./lifecycle/initErrors.js";
5
- import { isValidBridgeProtocolVersion } from "./lifecycle/ready.js";
6
+ import { isValidBridgeProtocolVersion } from "./lifecycle/protocolVersion.js";
6
7
  import { getInvalidSandboxMessageResponse } from "./messages/invalidSandboxMessage.js";
7
8
  import { QuerySubscriptions } from "./queries/querySubscriptions.js";
8
9
  const DEFAULT_NO_CONNECT_TIMEOUT_MS = 5000;
9
10
  const DEFAULT_NO_INIT_RESULT_TIMEOUT_MS = 5000;
10
- const DEFAULT_MIN_BRIDGE_PROTOCOL_VERSION = 2;
11
+ const initializationIdentitySchema = v.object({
12
+ initializationId: v.string(),
13
+ });
11
14
  export function createCustomBlockHost(options) {
12
- const { iframe, initialState, handlers, onConnect, onInitResult, onLog, targetOrigin = "*", minBridgeProtocolVersion = DEFAULT_MIN_BRIDGE_PROTOCOL_VERSION, noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS, noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS, } = options;
15
+ const { iframe, initialState, handlers, onConnect, onInitResult, onLog, targetOrigin = "*", minBridgeProtocolVersion: configuredMinBridgeProtocolVersion, noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS, noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS, } = options;
16
+ const minBridgeProtocolVersion = Math.max(configuredMinBridgeProtocolVersion ?? CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION, CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION);
13
17
  let theme = initialState.theme;
14
18
  let parent = initialState.parent;
15
19
  let page = initialState.page;
@@ -286,7 +290,16 @@ export function createCustomBlockHost(options) {
286
290
  issues,
287
291
  });
288
292
  const response = getInvalidSandboxMessageResponse(data);
289
- if (response.nack !== undefined) {
293
+ const parsedIdentity = v.safeParse(initializationIdentitySchema, data);
294
+ const initializationId = parsedIdentity.success
295
+ ? parsedIdentity.output.initializationId
296
+ : undefined;
297
+ if (initStatus === "waitingForConnect" &&
298
+ response.incomingType === "connect" &&
299
+ initializationId !== undefined) {
300
+ sendInitError(initializationId, initErrorForFailureReason("invalid_connect"));
301
+ }
302
+ else if (response.nack !== undefined) {
290
303
  post(response.nack);
291
304
  }
292
305
  }
@@ -1,2 +1,2 @@
1
1
  export declare function isValidBridgeProtocolVersion(version: number): boolean;
2
- //# sourceMappingURL=ready.d.ts.map
2
+ //# sourceMappingURL=protocolVersion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocolVersion.d.ts","sourceRoot":"","sources":["../../../../../home/runner/work/custom-blocks/custom-blocks/sdk/src/host/lifecycle/protocolVersion.ts"],"names":[],"mappings":"AAAA,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAErE"}
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  *
5
5
  * WARNING: Generated during SDK publish. Do not edit in the published package.
6
6
  */
7
- export const CUSTOM_BLOCKS_SDK_VERSION = "0.0.78"
7
+ export const CUSTOM_BLOCKS_SDK_VERSION = "0.1.0"
@@ -1,8 +1,10 @@
1
1
  # Data sources
2
2
 
3
- A custom block declares its **data sources** — semantic keys like `people` or `tasks` — in `custom_blocks.json`.
3
+ A custom block declares its **data sources** — semantic keys like `people` or `tasks` — in the worker's `worker.customBlock(...)` configuration.
4
4
 
5
- Data source mapping - resolving keys to ids - is stored on the block itself. This can be read/written programmatically using the `ncblock` CLI (try `npx ncblock --help`) or in the Notion UI.
5
+ The mapping from those semantic keys to concrete Notion data sources is stored on each block instance and configured in Notion.
6
+
7
+ Use `ManifestDataSource`, `ManifestProperty`, and `ManifestIcon` when code needs the corresponding definition metadata types.
6
8
 
7
9
  At runtime, your code references the semantic key and the SDK handles the lookup for you. Use `useDataSource(key)` for the rows themselves (it also exposes the resolved schema), and `useManifest()` when you need the declared data-source keys and their declarations (e.g. picking a default key, or rendering a key switcher). For non-React renderers, `customBlock.getManifest()` exposes the same manifest, but row querying is currently only exposed through the React `useDataSource` hook.
8
10
 
@@ -115,8 +117,8 @@ export function ScoreList() {
115
117
  if (error) return <div role="alert">Couldn't load: {error.message}</div>;
116
118
  if (isLoading && items.length === 0) return <div>Loading…</div>;
117
119
 
118
- const ready = items.filter(isComplete);
119
- if (ready.length === 0) {
120
+ const displayItems = items.filter(isComplete);
121
+ if (displayItems.length === 0) {
120
122
  return (
121
123
  <div>
122
124
  Map a data source with key <code>{KEY}</code> exposing <code>name</code>{" "}
@@ -128,7 +130,7 @@ export function ScoreList() {
128
130
  return (
129
131
  <div>
130
132
  <ul>
131
- {ready.map((item) => (
133
+ {displayItems.map((item) => (
132
134
  <li key={item.id}>
133
135
  {String(item.propertiesByKey.name)} —{" "}
134
136
  {Number(item.propertiesByKey.score)}
@@ -0,0 +1,46 @@
1
+ # Vite plugin
2
+
3
+ The `notionCustomBlock()` plugin from `@notionhq/custom-blocks/vite` wires a block's local dev server and build output to the SDK handshake.
4
+
5
+ ```ts
6
+ import { defineConfig } from "vite";
7
+ import react from "@vitejs/plugin-react";
8
+ import { notionCustomBlock } from "@notionhq/custom-blocks/vite";
9
+
10
+ export default defineConfig({
11
+ plugins: [react(), notionCustomBlock()],
12
+ });
13
+ ```
14
+
15
+ In dev, the plugin serves `custom_blocks.json` from the project root so HMR and the SDK handshake see the same file. On `vite build`, it emits `custom_blocks.json` into `dist/` as a separate asset alongside the bundled HTML and JS. It also forces Vite's `base` to `"./"` (and rejects any other explicit value) — custom blocks are served from a content-addressed path, so asset URLs in the built bundle must be relative.
16
+
17
+ ## `custom_blocks.json` and local previews
18
+
19
+ `custom_blocks.json` is the block's local manifest — the data-source keys and property shapes the block expects:
20
+
21
+ ```json
22
+ {
23
+ "version": 1,
24
+ "dataSources": {
25
+ "tasks": {
26
+ "name": "Tasks",
27
+ "description": "The collection of tasks to render",
28
+ "properties": {
29
+ "title": { "name": "Title", "type": "title" },
30
+ "dueDate": { "name": "Due date", "type": "date" }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ For a worker-backed block, the `worker.customBlock(...)` declaration in the worker's `src/index.ts` is the source of truth at deploy time. But local previews (pasting the Vite dev URL into a live Notion block, or the dev shell) don't read the worker declaration — the SDK fetches `custom_blocks.json` from the plugin and forwards it on `connect`, and the host binds data sources against those keys. Keep the file mirroring the worker's `dataSources` schema so previews see the same slots the deployed block will; leave it as `{ "version": 1, "dataSources": {} }` if the block reads no host data.
38
+
39
+ If the file is missing, the SDK omits `manifest` from `connect`. If it is unreadable or invalid, the SDK sends `connect` with `status: "error"` and an `error` payload. The host returns its authoritative manifest in `init`, and the SDK uses that one even when it differs from what was sent in `connect`.
40
+
41
+ ## Types
42
+
43
+ - `CustomBlockManifest` — the parsed shape of `custom_blocks.json`.
44
+ - `ManifestDataSource` — a single entry in `dataSources` (name, description, properties).
45
+ - `ManifestProperty` — a single property declaration inside a `ManifestDataSource`.
46
+ - `ManifestIcon` — the icon variant accepted on a `ManifestDataSource`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/custom-blocks",
3
- "version": "0.0.78",
3
+ "version": "0.1.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,7 +32,6 @@
32
32
  }
33
33
  },
34
34
  "bin": {
35
- "ncblock": "./bin/cli/cli.js",
36
35
  "notion-custom-blocks": "./bin/notion-custom-blocks/cli.js"
37
36
  },
38
37
  "scripts": {
@@ -63,7 +63,7 @@ import type { NotionUser } from "./users/user.js"
63
63
  * single host needs to support multiple custom blocks built with different versions of the bridge
64
64
  * protocol. Increment this number any time a breaking change is made to the bridge protocol.
65
65
  */
66
- export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 2
66
+ export const CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION = 3
67
67
 
68
68
  /**
69
69
  * A single entry in the bridge message log. Kept intentionally plain so the log
@@ -1,4 +1,5 @@
1
1
  import * as v from "valibot"
2
+ import { customBlockErrorInfoSchema } from "../errors.js"
2
3
  import { manifestSchema } from "../manifest.js"
3
4
 
4
5
  const connectMessageCommonEntries = {
@@ -17,14 +18,6 @@ const connectMessageCommonEntries = {
17
18
  sdkVersion: v.string(),
18
19
  }
19
20
 
20
- // TODO(custom-blocks): Replace with `customBlockErrorInfoSchema` which makes `isRetryable` required
21
- // once min supported bridge protocol version is bumped to 3.
22
- const connectErrorSchema = v.object({
23
- code: v.string(),
24
- message: v.string(),
25
- isRetryable: v.optional(v.boolean(), false),
26
- })
27
-
28
21
  /**
29
22
  * First message the sandbox sends after mount. It identifies the sandbox and starts the bridge
30
23
  * initialization exchange. The host echoes `initializationId` in its `init` response.
@@ -41,7 +34,7 @@ export const connectMessageSchema = v.variant("status", [
41
34
  }),
42
35
  v.object({
43
36
  status: v.literal("error"),
44
- error: connectErrorSchema,
37
+ error: customBlockErrorInfoSchema,
45
38
  ...connectMessageCommonEntries,
46
39
  }),
47
40
  ])
@@ -34,11 +34,12 @@ import type { UpdatePageMessage } from "../bridge/messages/updatePage.js"
34
34
  import type { UpdatePageResultMessage } from "../bridge/messages/updatePageResult.js"
35
35
  import type { CustomBlockPage } from "../bridge/pages/page.js"
36
36
  import type { NotionParent } from "../bridge/parent.js"
37
+ import { CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION } from "../bridge/SandboxBridge.js"
37
38
  import type { NotionTheme } from "../bridge/theme.js"
38
39
  import type { NotionUser } from "../bridge/users/user.js"
39
40
  import { unreachable } from "../utils.js"
40
41
  import { initErrorForFailureReason } from "./lifecycle/initErrors.js"
41
- import { isValidBridgeProtocolVersion } from "./lifecycle/ready.js"
42
+ import { isValidBridgeProtocolVersion } from "./lifecycle/protocolVersion.js"
42
43
  import type {
43
44
  ConnectSuccessMessage,
44
45
  CustomBlockHostDataSourcesPayload,
@@ -56,7 +57,7 @@ import type { CustomBlockHostQueryDataSourceResult } from "./queries/types.js"
56
57
  export type CustomBlockHostOptions = {
57
58
  iframe: HTMLIFrameElement
58
59
  targetOrigin?: string
59
- minBridgeProtocolVersion?: number
60
+ minBridgeProtocolVersion: number
60
61
  noConnectTimeoutMs?: number
61
62
  noInitResultTimeoutMs?: number
62
63
  initialState: CustomBlockHostInitialState
@@ -82,7 +83,9 @@ export type CustomBlockHostHandle = {
82
83
 
83
84
  const DEFAULT_NO_CONNECT_TIMEOUT_MS = 5_000
84
85
  const DEFAULT_NO_INIT_RESULT_TIMEOUT_MS = 5_000
85
- const DEFAULT_MIN_BRIDGE_PROTOCOL_VERSION = 2
86
+ const initializationIdentitySchema = v.object({
87
+ initializationId: v.string(),
88
+ })
86
89
 
87
90
  export function createCustomBlockHost(
88
91
  options: CustomBlockHostOptions,
@@ -95,10 +98,14 @@ export function createCustomBlockHost(
95
98
  onInitResult,
96
99
  onLog,
97
100
  targetOrigin = "*",
98
- minBridgeProtocolVersion = DEFAULT_MIN_BRIDGE_PROTOCOL_VERSION,
101
+ minBridgeProtocolVersion: configuredMinBridgeProtocolVersion,
99
102
  noConnectTimeoutMs = DEFAULT_NO_CONNECT_TIMEOUT_MS,
100
103
  noInitResultTimeoutMs = DEFAULT_NO_INIT_RESULT_TIMEOUT_MS,
101
104
  } = options
105
+ const minBridgeProtocolVersion = Math.max(
106
+ configuredMinBridgeProtocolVersion ?? CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
107
+ CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION,
108
+ )
102
109
 
103
110
  let theme = initialState.theme
104
111
  let parent = initialState.parent
@@ -418,7 +425,20 @@ export function createCustomBlockHost(
418
425
  })
419
426
  const response = getInvalidSandboxMessageResponse(data)
420
427
 
421
- if (response.nack !== undefined) {
428
+ const parsedIdentity = v.safeParse(initializationIdentitySchema, data)
429
+ const initializationId = parsedIdentity.success
430
+ ? parsedIdentity.output.initializationId
431
+ : undefined
432
+ if (
433
+ initStatus === "waitingForConnect" &&
434
+ response.incomingType === "connect" &&
435
+ initializationId !== undefined
436
+ ) {
437
+ sendInitError(
438
+ initializationId,
439
+ initErrorForFailureReason("invalid_connect"),
440
+ )
441
+ } else if (response.nack !== undefined) {
422
442
  post(response.nack)
423
443
  }
424
444
  }
package/bin/cli/attach.js DELETED
@@ -1,180 +0,0 @@
1
- import { extractId, formatUuid } from "./ids.js";
2
- import { ntnApi, ntnApiPatch, ntnWorkersCapabilities } from "./ntn.js";
3
- // ── Capability resolution ───────────────────────────────────────
4
- function normalizeCapabilities(raw) {
5
- if (Array.isArray(raw)) {
6
- return raw;
7
- }
8
- if (raw && typeof raw === "object") {
9
- const obj = raw;
10
- for (const field of ["capabilities", "results", "data"]) {
11
- if (Array.isArray(obj[field])) {
12
- return obj[field];
13
- }
14
- }
15
- }
16
- return [];
17
- }
18
- function capabilityKey(cap) {
19
- for (const field of ["key", "name", "id"]) {
20
- const value = cap[field];
21
- if (typeof value === "string" && value.length > 0) {
22
- return value;
23
- }
24
- }
25
- return undefined;
26
- }
27
- function capabilityType(cap) {
28
- // `ntn` reports the capability kind under `_tag`; accept `type` too in case
29
- // the wire shape shifts.
30
- for (const field of ["_tag", "type"]) {
31
- const value = cap[field];
32
- if (typeof value === "string") {
33
- return value;
34
- }
35
- }
36
- return undefined;
37
- }
38
- function isCustomBlockCapability(cap) {
39
- const type = capabilityType(cap);
40
- // When no type field is present we can't filter — keep it and let the
41
- // count-based selection below decide.
42
- if (type === undefined) {
43
- return true;
44
- }
45
- return type === "customBlock" || type === "custom_block";
46
- }
47
- function firstString(obj, fields) {
48
- for (const field of fields) {
49
- const value = obj[field];
50
- if (typeof value === "string" && value.length > 0) {
51
- return value;
52
- }
53
- }
54
- return undefined;
55
- }
56
- function capabilityDefinitionId(cap) {
57
- const top = firstString(cap, ["definition_id", "definitionId"]);
58
- if (top) {
59
- return top;
60
- }
61
- // The real `ntn` shape nests it under `state.definitionId`.
62
- const state = cap.state;
63
- if (state && typeof state === "object") {
64
- const nested = firstString(state, [
65
- "definitionId",
66
- "definition_id",
67
- ]);
68
- if (nested) {
69
- return nested;
70
- }
71
- }
72
- // Some shapes nest the definition under a `definition` object.
73
- const definition = cap.definition;
74
- if (definition && typeof definition === "object") {
75
- const id = definition.id;
76
- if (typeof id === "string" && id.length > 0) {
77
- return id;
78
- }
79
- }
80
- return undefined;
81
- }
82
- /**
83
- * Resolve a worker's custom-block capability to a `definition_id`.
84
- *
85
- * - Filters `ntn workers capabilities list` down to customBlock capabilities.
86
- * - With `capability` set, picks the entry with that key.
87
- * - With exactly one candidate, uses it.
88
- * - With more than one and no `capability`, errors with the available keys.
89
- */
90
- export function resolveDefinitionId(capability) {
91
- const raw = ntnWorkersCapabilities();
92
- const all = normalizeCapabilities(raw);
93
- if (all.length === 0) {
94
- console.error("Error: no worker capabilities found. Deploy the worker first with `ntn workers deploy`.\n");
95
- console.error(" Raw `ntn workers capabilities list --json` output:");
96
- console.error(JSON.stringify(raw, null, "\t"));
97
- process.exit(1);
98
- }
99
- const customBlocks = all.filter(isCustomBlockCapability);
100
- if (customBlocks.length === 0) {
101
- console.error("Error: this worker exposes no customBlock capability to attach.\n");
102
- process.exit(1);
103
- }
104
- let selected;
105
- if (capability) {
106
- const match = customBlocks.find(c => capabilityKey(c) === capability);
107
- if (!match) {
108
- const keys = customBlocks.map(c => capabilityKey(c) ?? "(unnamed)");
109
- console.error(`Error: no customBlock capability named '${capability}'.\n`);
110
- console.error(` Available: ${keys.join(", ")}`);
111
- process.exit(1);
112
- }
113
- selected = match;
114
- }
115
- else if (customBlocks.length > 1) {
116
- const keys = customBlocks.map(c => capabilityKey(c) ?? "(unnamed)");
117
- console.error(`Error: this worker exposes ${customBlocks.length} customBlock capabilities. Pick one with --capability <key>.\n`);
118
- console.error(` Available: ${keys.join(", ")}`);
119
- process.exit(1);
120
- }
121
- else {
122
- selected = customBlocks[0];
123
- }
124
- const definitionId = capabilityDefinitionId(selected);
125
- if (!definitionId) {
126
- console.error("Error: could not read a definition_id from the selected capability.\n");
127
- console.error(" Raw capability entry:");
128
- console.error(JSON.stringify(selected, null, "\t"));
129
- process.exit(1);
130
- }
131
- return { definitionId, capabilityKey: capabilityKey(selected) };
132
- }
133
- /**
134
- * Body for pointing a custom block at a worker's definition. The update-block
135
- * endpoint requires the payload be keyed by block type — a top-level
136
- * `definition_id` is rejected (`should be not present`). So it nests under
137
- * `custom_block`, matching how `patchBlock` (datasources.ts) sets data_sources.
138
- */
139
- function definitionPatchBody(definitionId) {
140
- return { custom_block: { definition_id: definitionId } };
141
- }
142
- /**
143
- * Point an existing custom block at a worker's customBlock definition.
144
- *
145
- * Resolves the block, verifies it's a `custom_block`, resolves the worker's
146
- * definition_id (erroring on ambiguity), then PATCHes the block. Returns the
147
- * dashed block UUID so callers (e.g. `create`) can chain.
148
- */
149
- export function attach(options) {
150
- const uuid = formatUuid(extractId(options.idOrUrl));
151
- if (!options.jsonOutput) {
152
- console.log(`\nFetching block ${uuid}…`);
153
- }
154
- const block = ntnApi(`/v1/blocks/${uuid}`);
155
- if (block.type !== "custom_block") {
156
- console.error(`Error: block ${uuid} is type "${block.type}", not "custom_block".\n`);
157
- console.error(" Pass a custom block's ID/URL, or create one with `ncblock create`.");
158
- process.exit(1);
159
- }
160
- const { definitionId, capabilityKey: key } = resolveDefinitionId(options.capability);
161
- if (!options.jsonOutput) {
162
- console.log(`Attaching definition ${definitionId}${key ? ` (capability "${key}")` : ""}…`);
163
- }
164
- ntnApiPatch(`/v1/blocks/${uuid}`, definitionPatchBody(definitionId));
165
- const emitResult = options.emitResult ?? true;
166
- if (options.jsonOutput) {
167
- if (emitResult) {
168
- console.log(JSON.stringify({
169
- action: "attach",
170
- block_id: uuid,
171
- definition_id: definitionId,
172
- capability: key,
173
- }, null, "\t"));
174
- }
175
- }
176
- else {
177
- console.log(`\n✓ Attached block ${uuid} to definition ${definitionId}\n`);
178
- }
179
- return uuid;
180
- }