@book.dev/sdk 3.8.0 → 3.9.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 (53) hide show
  1. package/dist/ai.d.ts +12 -2
  2. package/dist/ai.js.map +1 -1
  3. package/dist/blockCatalogue.d.ts +560 -0
  4. package/dist/blockCatalogue.js +336 -0
  5. package/dist/blockCatalogue.js.map +1 -0
  6. package/dist/bookFolder.d.ts +9 -1
  7. package/dist/bookFolder.js +64 -2
  8. package/dist/bookFolder.js.map +1 -1
  9. package/dist/client.d.ts +22 -2
  10. package/dist/client.js +6 -2
  11. package/dist/client.js.map +1 -1
  12. package/dist/content.d.ts +40 -1
  13. package/dist/content.js +29 -7
  14. package/dist/content.js.map +1 -1
  15. package/dist/forwarding/forwardingClient.d.ts +68 -2
  16. package/dist/forwarding/forwardingClient.js +126 -12
  17. package/dist/forwarding/forwardingClient.js.map +1 -1
  18. package/dist/forwarding/index.d.ts +2 -1
  19. package/dist/forwarding/index.js +2 -1
  20. package/dist/forwarding/index.js.map +1 -1
  21. package/dist/forwarding/namespacedKeyStore.d.ts +52 -0
  22. package/dist/forwarding/namespacedKeyStore.js +99 -0
  23. package/dist/forwarding/namespacedKeyStore.js.map +1 -0
  24. package/dist/index.d.ts +10 -5
  25. package/dist/index.js +10 -5
  26. package/dist/index.js.map +1 -1
  27. package/dist/ledger.d.ts +29 -0
  28. package/dist/ledger.js +43 -0
  29. package/dist/ledger.js.map +1 -1
  30. package/dist/ledgerExportSection.d.ts +160 -0
  31. package/dist/ledgerExportSection.js +594 -0
  32. package/dist/ledgerExportSection.js.map +1 -0
  33. package/dist/orderKeys.d.ts +36 -0
  34. package/dist/orderKeys.js +88 -0
  35. package/dist/orderKeys.js.map +1 -0
  36. package/dist/plugins.d.ts +76 -4
  37. package/dist/plugins.js +95 -12
  38. package/dist/plugins.js.map +1 -1
  39. package/dist/provenance.d.ts +13 -0
  40. package/dist/registryClient.d.ts +227 -0
  41. package/dist/registryClient.js +397 -0
  42. package/dist/registryClient.js.map +1 -0
  43. package/dist/routes.d.ts +9 -0
  44. package/dist/routes.js +9 -0
  45. package/dist/routes.js.map +1 -1
  46. package/dist/suggestions.d.ts +12 -2
  47. package/dist/tableSnapshot.d.ts +233 -0
  48. package/dist/tableSnapshot.js +680 -0
  49. package/dist/tableSnapshot.js.map +1 -0
  50. package/dist/templates.d.ts +8 -0
  51. package/dist/templates.js +7 -2
  52. package/dist/templates.js.map +1 -1
  53. package/package.json +1 -1
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Fractional order keys — pure, dependency-free (TBL-1).
3
+ *
4
+ * A key is a non-empty string over the base-62 alphabet `0-9A-Za-z` whose
5
+ * plain lexicographic order IS its numeric order: read it as a fraction in
6
+ * (0, 1) written in base 62 (`'V'` ≈ 0.5, `'0V'` ≈ 0.008…). The ASCII order
7
+ * of the alphabet matches its digit order, so `a < b` on the raw strings is
8
+ * the whole comparator — no parsing, ever.
9
+ *
10
+ * Invariant: a key never ends with the zero digit `'0'`. That guarantees any
11
+ * two distinct keys (and either open end) always have a midpoint, so a move
12
+ * only ever writes ONE small string — the mover's new key — and never has to
13
+ * renumber neighbours. Keys grow ~1 character per ~5 inserts into the same
14
+ * gap (worst case); callers that see a key longer than
15
+ * {@link ORDER_KEY_REBALANCE_LENGTH} should rewrite the whole axis with
16
+ * {@link keysBetween} (see `model.ts` table ops for the pattern).
17
+ */
18
+ /** Keys longer than this ask the owning axis for a rebalance (rare). */
19
+ export declare const ORDER_KEY_REBALANCE_LENGTH = 48;
20
+ /** True for a well-formed order key (base-62, non-empty, no trailing zero). */
21
+ export declare function isOrderKey(value: unknown): value is string;
22
+ /** Lexicographic comparator (identical to `<` on the raw strings). */
23
+ export declare function compareOrderKeys(a: string, b: string): number;
24
+ /**
25
+ * A key strictly between `a` and `b` (`null` = the open end of the axis).
26
+ * Throws on malformed keys or `a >= b` — callers own their bounds (the table
27
+ * ops fall back to a full-axis rebalance instead of passing bad bounds).
28
+ */
29
+ export declare function keyBetween(a: string | null, b: string | null): string;
30
+ /**
31
+ * `n` fresh keys strictly between `a` and `b`, in ascending order — balanced
32
+ * binary subdivision, so lengths stay ~log₆₂(n). Deterministic: the same
33
+ * bounds and count always produce the same keys (two peers migrating the same
34
+ * legacy table concurrently write identical values and converge trivially).
35
+ */
36
+ export declare function keysBetween(a: string | null, b: string | null, n: number): string[];
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Fractional order keys — pure, dependency-free (TBL-1).
3
+ *
4
+ * A key is a non-empty string over the base-62 alphabet `0-9A-Za-z` whose
5
+ * plain lexicographic order IS its numeric order: read it as a fraction in
6
+ * (0, 1) written in base 62 (`'V'` ≈ 0.5, `'0V'` ≈ 0.008…). The ASCII order
7
+ * of the alphabet matches its digit order, so `a < b` on the raw strings is
8
+ * the whole comparator — no parsing, ever.
9
+ *
10
+ * Invariant: a key never ends with the zero digit `'0'`. That guarantees any
11
+ * two distinct keys (and either open end) always have a midpoint, so a move
12
+ * only ever writes ONE small string — the mover's new key — and never has to
13
+ * renumber neighbours. Keys grow ~1 character per ~5 inserts into the same
14
+ * gap (worst case); callers that see a key longer than
15
+ * {@link ORDER_KEY_REBALANCE_LENGTH} should rewrite the whole axis with
16
+ * {@link keysBetween} (see `model.ts` table ops for the pattern).
17
+ */
18
+ const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
19
+ const ZERO = DIGITS[0];
20
+ const BASE = DIGITS.length;
21
+ /** Keys longer than this ask the owning axis for a rebalance (rare). */
22
+ export const ORDER_KEY_REBALANCE_LENGTH = 48;
23
+ /** True for a well-formed order key (base-62, non-empty, no trailing zero). */
24
+ export function isOrderKey(value) {
25
+ return (typeof value === 'string' &&
26
+ value.length > 0 &&
27
+ !value.endsWith(ZERO) &&
28
+ [...value].every((ch) => DIGITS.includes(ch)));
29
+ }
30
+ /** Lexicographic comparator (identical to `<` on the raw strings). */
31
+ export function compareOrderKeys(a, b) {
32
+ return a < b ? -1 : a > b ? 1 : 0;
33
+ }
34
+ /**
35
+ * The midpoint of two fraction strings, `a < result < b`.
36
+ * `a === ''` means the lower bound 0; `b === null` means the upper bound 1.
37
+ * Neither input may have a trailing zero digit; the result never does.
38
+ */
39
+ function midpoint(a, b) {
40
+ if (b !== null) {
41
+ // Shared prefix passes through untouched; recurse on the differing tail.
42
+ let n = 0;
43
+ while ((a[n] ?? ZERO) === b[n])
44
+ n += 1;
45
+ if (n > 0)
46
+ return b.slice(0, n) + midpoint(a.slice(n), b.slice(n));
47
+ }
48
+ const digitA = a ? DIGITS.indexOf(a[0]) : 0;
49
+ const digitB = b !== null && b.length > 0 ? DIGITS.indexOf(b[0]) : BASE;
50
+ if (digitB - digitA > 1) {
51
+ return DIGITS[Math.round((digitA + digitB) / 2)];
52
+ }
53
+ // Consecutive leading digits: either borrow b's head (when it has a tail to
54
+ // stay below), or extend a's head with a midpoint above a's tail.
55
+ if (b !== null && b.length > 1)
56
+ return b[0];
57
+ return DIGITS[digitA] + midpoint(a.slice(1), null);
58
+ }
59
+ /**
60
+ * A key strictly between `a` and `b` (`null` = the open end of the axis).
61
+ * Throws on malformed keys or `a >= b` — callers own their bounds (the table
62
+ * ops fall back to a full-axis rebalance instead of passing bad bounds).
63
+ */
64
+ export function keyBetween(a, b) {
65
+ if (a !== null && !isOrderKey(a))
66
+ throw new RangeError(`invalid order key: ${JSON.stringify(a)}`);
67
+ if (b !== null && !isOrderKey(b))
68
+ throw new RangeError(`invalid order key: ${JSON.stringify(b)}`);
69
+ if (a !== null && b !== null && a >= b)
70
+ throw new RangeError(`order keys out of order: ${a} >= ${b}`);
71
+ return midpoint(a ?? '', b);
72
+ }
73
+ /**
74
+ * `n` fresh keys strictly between `a` and `b`, in ascending order — balanced
75
+ * binary subdivision, so lengths stay ~log₆₂(n). Deterministic: the same
76
+ * bounds and count always produce the same keys (two peers migrating the same
77
+ * legacy table concurrently write identical values and converge trivially).
78
+ */
79
+ export function keysBetween(a, b, n) {
80
+ if (n <= 0)
81
+ return [];
82
+ const mid = keyBetween(a, b);
83
+ if (n === 1)
84
+ return [mid];
85
+ const left = (n - 1) >> 1;
86
+ return [...keysBetween(a, mid, left), mid, ...keysBetween(mid, b, n - 1 - left)];
87
+ }
88
+ //# sourceMappingURL=orderKeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orderKeys.js","sourceRoot":"","sources":["../src/orderKeys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,GAAG,gEAAgE,CAAC;AAChF,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;AACvB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC;AAE3B,wEAAwE;AACxE,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,CAAC;AAE7C,+EAA+E;AAC/E,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,CAAC,MAAM,GAAG,CAAC;QAChB,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QACrB,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAC9C,CAAC;AACJ,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,gBAAgB,CAAC,CAAS,EAAE,CAAS;IACnD,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,SAAS,QAAQ,CAAC,CAAS,EAAE,CAAgB;IAC3C,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QACf,yEAAyE;QACzE,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACxE,IAAI,MAAM,GAAG,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,4EAA4E;IAC5E,kEAAkE;IAClE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AACrD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,CAAgB,EAAE,CAAgB;IAC3D,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,sBAAsB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAClG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,sBAAsB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAClG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,4BAA4B,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACtG,OAAO,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,CAAgB,EAAE,CAAgB,EAAE,CAAS;IACvE,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAC1B,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;IAC1B,OAAO,CAAC,GAAG,WAAW,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;AACnF,CAAC"}
package/dist/plugins.d.ts CHANGED
@@ -66,6 +66,24 @@ export interface PluginManifest {
66
66
  apiVersion?: number;
67
67
  /** Agent tools this plugin contributes (read server-side from the manifest). */
68
68
  agentTools?: PluginAgentTool[];
69
+ /**
70
+ * The block types this plugin registers at activation (WITHOUT the
71
+ * `<pluginId>/` prefix the host adds). Declared in the manifest so servers —
72
+ * which never run plugin code — can list a plugin's blocks (the agent/MCP
73
+ * `list_block_types` tool) straight from the stored manifest JSON. Optional:
74
+ * a plugin that declares none still works, its block types are simply not
75
+ * enumerable server-side. For bundled first-party plugins the declaration is
76
+ * verified against the source's `blocks.register` calls at bundle time
77
+ * (packages/ui/scripts/bundlePlugins.ts), so it cannot drift.
78
+ */
79
+ blocks?: PluginBlockDeclaration[];
80
+ }
81
+ /** One manifest-declared plugin block (see {@link PluginManifest.blocks}). */
82
+ export interface PluginBlockDeclaration {
83
+ /** The unprefixed block type, e.g. `journal-entry`. */
84
+ type: string;
85
+ /** One-line description surfaced by `list_block_types`. */
86
+ description?: string;
69
87
  }
70
88
  /**
71
89
  * The host-side activation gate for {@link PluginManifest.apiVersion}:
@@ -97,6 +115,21 @@ export interface StoredPlugin extends PluginPackage {
97
115
  /** Set by the client after verification — never trusted from the wire. */
98
116
  verified?: boolean;
99
117
  }
118
+ /**
119
+ * Canonical JSON for signing: objects emit ALL keys, sorted, at EVERY depth;
120
+ * arrays are positional; primitives follow JSON.stringify semantics.
121
+ * `undefined`, functions, and symbols are rejected outright (never silently
122
+ * dropped) — anything the signer skips is something an attacker can vary for
123
+ * free.
124
+ *
125
+ * Deliberately NOT `JSON.stringify(value, sortedKeysArray)`: a replacer ARRAY
126
+ * allowlists those keys at every depth, so nested keys absent from the top
127
+ * level (e.g. `agentTools[0].action`) silently vanish from the signed bytes.
128
+ *
129
+ * Mirrored byte-for-byte by the store's server-side port
130
+ * (open-book-pub `packages/store/lib/canonical.ts`) — change both together.
131
+ */
132
+ export declare function canonicalJson(value: unknown): string;
100
133
  /** SHA-256 of the canonical bytes (hex) — the thing a registry signs. */
101
134
  export declare function canonicalDigest(manifest: PluginManifest, files: Record<string, string>): Promise<string>;
102
135
  /** Generate a registry keypair (raw public / pkcs8 private, base64). */
@@ -109,6 +142,14 @@ export declare function signPlugin(pkg: {
109
142
  manifest: PluginManifest;
110
143
  files: Record<string, string>;
111
144
  }, privateKeyBase64: string, registry: string, publicKeyBase64: string): Promise<PluginSignature>;
145
+ /**
146
+ * Verify an Ed25519 signature (base64, raw 64 bytes) over a UTF-8 message
147
+ * with a base64 RAW 32-byte public key. Returns false — never throws — on
148
+ * malformed inputs. The registry protocol signs the UTF-8 bytes of the
149
+ * lowercase hex canonical digest with exactly this scheme, for the publisher
150
+ * signature, the notary countersignature, and revocation entries alike.
151
+ */
152
+ export declare function verifyEd25519Message(publicKeyBase64: string, message: string, signatureBase64: string): Promise<boolean>;
112
153
  /**
113
154
  * Verify a package against a set of trusted registry keys. Returns the
114
155
  * matching trusted registry name, or null when unsigned, signed by an
@@ -121,10 +162,41 @@ export declare function verifyPlugin(pkg: PluginPackage, trustedKeys: Array<{
121
162
  registry: string;
122
163
  } | null>;
123
164
  /**
124
- * The pinned first-party registry key. Every OpenBook build trusts this;
125
- * additional registries are user-added keys on top (Settings → Extensions).
126
- * NOTE: a placeholder development key until the real registry launches —
127
- * regenerate and replace before publishing plugins for real.
165
+ * The pinned first-party registry keys. Every OpenBook build trusts ALL of
166
+ * these; additional registries are user-added keys on top (Settings →
167
+ * Extensions).
168
+ *
169
+ * THIS LIST IS THE ONE PLACE THE PRODUCTION KEY(S) LIVE. The key ceremony
170
+ * (docs/plugin-signing.md) is: run `node scripts/gen-registry-key.mjs`, commit
171
+ * the printed PUBLIC key here, and store the private half as the
172
+ * `OPENBOOK_REGISTRY_PRIVATE_KEY` GitHub Actions secret — the release build
173
+ * signs the bundled first-party plugins with it (packages/ui/scripts/
174
+ * bundlePlugins.ts).
175
+ *
176
+ * A LIST so rotation can overlap (add-then-remove): append the NEW key as a
177
+ * second entry and ship — clients of that build verify signatures from either
178
+ * key. Once the overlap build is broadly installed, cut signing over to the
179
+ * new key (swap the CI secret), and a later release removes the old entry.
180
+ * The signing key SHOULD be entry [0]; extra entries exist only during
181
+ * rotation windows. Full runbook: docs/plugin-signing.md.
182
+ *
183
+ * NOTE: currently a single PLACEHOLDER key whose private half was generated
184
+ * in memory and destroyed — nothing can ever sign for it, so first-party
185
+ * bundles show Unverified until the owner performs the key ceremony. No
186
+ * entry may EVER match scripts/test-registry-key.json (its private half is
187
+ * public); CI guards enforce that for every entry
188
+ * (.github/workflows/release.yml).
189
+ */
190
+ export declare const OPENBOOK_REGISTRY_KEYS: Array<{
191
+ name: string;
192
+ publicKey: string;
193
+ }>;
194
+ /**
195
+ * The primary pinned first-party key — entry [0] of
196
+ * {@link OPENBOOK_REGISTRY_KEYS}.
197
+ * @deprecated trust decisions must consume the whole list
198
+ * ({@link OPENBOOK_REGISTRY_KEYS}); this alias exists for display and
199
+ * back-compat only.
128
200
  */
129
201
  export declare const OPENBOOK_REGISTRY: {
130
202
  name: string;
package/dist/plugins.js CHANGED
@@ -36,9 +36,43 @@ export function pluginApiVersionError(manifest) {
36
36
  }
37
37
  const te = new TextEncoder();
38
38
  /**
39
- * Deterministic bytes for signing: the manifest (sorted-key JSON), then each
40
- * file in sorted path order, every part length-prefixed so boundaries can't
41
- * be confused (`a + bc` ≠ `ab + c`).
39
+ * Canonical JSON for signing: objects emit ALL keys, sorted, at EVERY depth;
40
+ * arrays are positional; primitives follow JSON.stringify semantics.
41
+ * `undefined`, functions, and symbols are rejected outright (never silently
42
+ * dropped) — anything the signer skips is something an attacker can vary for
43
+ * free.
44
+ *
45
+ * Deliberately NOT `JSON.stringify(value, sortedKeysArray)`: a replacer ARRAY
46
+ * allowlists those keys at every depth, so nested keys absent from the top
47
+ * level (e.g. `agentTools[0].action`) silently vanish from the signed bytes.
48
+ *
49
+ * Mirrored byte-for-byte by the store's server-side port
50
+ * (open-book-pub `packages/store/lib/canonical.ts`) — change both together.
51
+ */
52
+ export function canonicalJson(value) {
53
+ if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
54
+ throw new TypeError(`canonical JSON cannot contain ${typeof value}`);
55
+ }
56
+ if (value === null || typeof value !== 'object')
57
+ return JSON.stringify(value);
58
+ if (Array.isArray(value))
59
+ return `[${value.map((v) => canonicalJson(v)).join(',')}]`;
60
+ const obj = value;
61
+ const members = Object.keys(obj)
62
+ .sort()
63
+ .map((k) => {
64
+ if (obj[k] === undefined || typeof obj[k] === 'function' || typeof obj[k] === 'symbol') {
65
+ throw new TypeError(`canonical JSON cannot contain ${typeof obj[k]} (at key "${k}")`);
66
+ }
67
+ return `${JSON.stringify(k)}:${canonicalJson(obj[k])}`;
68
+ });
69
+ return `{${members.join(',')}}`;
70
+ }
71
+ /**
72
+ * Deterministic bytes for signing: the manifest (canonical JSON — keys sorted
73
+ * recursively, see {@link canonicalJson}), then each file in sorted path
74
+ * order, every part length-prefixed so boundaries can't be confused
75
+ * (`a + bc` ≠ `ab + c`).
42
76
  */
43
77
  function canonicalBytes(manifest, files) {
44
78
  const parts = [];
@@ -48,7 +82,7 @@ function canonicalBytes(manifest, files) {
48
82
  new DataView(len.buffer).setUint32(0, bytes.length);
49
83
  parts.push(len, bytes);
50
84
  };
51
- push(JSON.stringify(manifest, Object.keys(manifest).sort()));
85
+ push(canonicalJson(manifest));
52
86
  for (const path of Object.keys(files).sort()) {
53
87
  push(path);
54
88
  push(files[path]);
@@ -83,6 +117,25 @@ export async function signPlugin(pkg, privateKeyBase64, registry, publicKeyBase6
83
117
  const sig = new Uint8Array(await crypto.subtle.sign('Ed25519', key, te.encode(digest)));
84
118
  return { registry, publicKey: publicKeyBase64, signature: toBase64(sig), algorithm: 'ed25519' };
85
119
  }
120
+ /**
121
+ * Verify an Ed25519 signature (base64, raw 64 bytes) over a UTF-8 message
122
+ * with a base64 RAW 32-byte public key. Returns false — never throws — on
123
+ * malformed inputs. The registry protocol signs the UTF-8 bytes of the
124
+ * lowercase hex canonical digest with exactly this scheme, for the publisher
125
+ * signature, the notary countersignature, and revocation entries alike.
126
+ */
127
+ export async function verifyEd25519Message(publicKeyBase64, message, signatureBase64) {
128
+ try {
129
+ const raw = fromBase64(publicKeyBase64);
130
+ if (raw.length !== 32)
131
+ return false;
132
+ const key = await crypto.subtle.importKey('raw', raw, 'Ed25519', false, ['verify']);
133
+ return await crypto.subtle.verify('Ed25519', key, fromBase64(signatureBase64), te.encode(message));
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ }
86
139
  /**
87
140
  * Verify a package against a set of trusted registry keys. Returns the
88
141
  * matching trusted registry name, or null when unsigned, signed by an
@@ -106,15 +159,45 @@ export async function verifyPlugin(pkg, trustedKeys) {
106
159
  }
107
160
  }
108
161
  /**
109
- * The pinned first-party registry key. Every OpenBook build trusts this;
110
- * additional registries are user-added keys on top (Settings → Extensions).
111
- * NOTE: a placeholder development key until the real registry launches —
112
- * regenerate and replace before publishing plugins for real.
162
+ * The pinned first-party registry keys. Every OpenBook build trusts ALL of
163
+ * these; additional registries are user-added keys on top (Settings →
164
+ * Extensions).
165
+ *
166
+ * THIS LIST IS THE ONE PLACE THE PRODUCTION KEY(S) LIVE. The key ceremony
167
+ * (docs/plugin-signing.md) is: run `node scripts/gen-registry-key.mjs`, commit
168
+ * the printed PUBLIC key here, and store the private half as the
169
+ * `OPENBOOK_REGISTRY_PRIVATE_KEY` GitHub Actions secret — the release build
170
+ * signs the bundled first-party plugins with it (packages/ui/scripts/
171
+ * bundlePlugins.ts).
172
+ *
173
+ * A LIST so rotation can overlap (add-then-remove): append the NEW key as a
174
+ * second entry and ship — clients of that build verify signatures from either
175
+ * key. Once the overlap build is broadly installed, cut signing over to the
176
+ * new key (swap the CI secret), and a later release removes the old entry.
177
+ * The signing key SHOULD be entry [0]; extra entries exist only during
178
+ * rotation windows. Full runbook: docs/plugin-signing.md.
179
+ *
180
+ * NOTE: currently a single PLACEHOLDER key whose private half was generated
181
+ * in memory and destroyed — nothing can ever sign for it, so first-party
182
+ * bundles show Unverified until the owner performs the key ceremony. No
183
+ * entry may EVER match scripts/test-registry-key.json (its private half is
184
+ * public); CI guards enforce that for every entry
185
+ * (.github/workflows/release.yml).
186
+ */
187
+ export const OPENBOOK_REGISTRY_KEYS = [
188
+ {
189
+ name: 'OpenBook Registry',
190
+ publicKey: 'auvZjhjbcZgepWphhILsmuQNl82djsb6dkao+/S+7zU=',
191
+ },
192
+ ];
193
+ /**
194
+ * The primary pinned first-party key — entry [0] of
195
+ * {@link OPENBOOK_REGISTRY_KEYS}.
196
+ * @deprecated trust decisions must consume the whole list
197
+ * ({@link OPENBOOK_REGISTRY_KEYS}); this alias exists for display and
198
+ * back-compat only.
113
199
  */
114
- export const OPENBOOK_REGISTRY = {
115
- name: 'OpenBook Registry',
116
- publicKey: 'nI4eBQzqrIyVPEmJSEzGtqC9B0+kfWTXKyN5t8Yki/E=',
117
- };
200
+ export const OPENBOOK_REGISTRY = OPENBOOK_REGISTRY_KEYS[0];
118
201
  const PLUGIN_ID_RE = /^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/;
119
202
  /** Validate a manifest's shape; returns a human-readable problem or null. */
120
203
  export function validateManifest(m) {
@@ -1 +1 @@
1
- {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"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"}
@@ -173,6 +173,19 @@ export interface InstanceInfo {
173
173
  * identity and falls back to a reachability-only probe. */
174
174
  instanceId?: string | null;
175
175
  ownerSubject: string | null;
176
+ /**
177
+ * Whether this instance has an owner at all — WITHOUT saying who (PUB-1 / Sasha
178
+ * M-1). {@link ownerSubject} cannot answer this: GATE-7 nulls it for an anonymous
179
+ * caller on a claimed instance, so `ownerSubject === null` means either "unclaimed"
180
+ * or "claimed, but you may not know the owner". A client that needs the CLAIM
181
+ * STATE — e.g. to warn that an unclaimed library ignores `defaultVisibility`
182
+ * entirely (authorize rule 0 short-circuits on the guest gate alone) — must read
183
+ * this instead. Deliberately non-sensitive: a boolean "someone owns this" leaks no
184
+ * identity and no recon surface beyond what the guest gate's own behaviour already
185
+ * reveals. Optional: absent on a pre-PUB-1 server / a test fixture — a client then
186
+ * knows nothing and should stay silent rather than guess a claim state.
187
+ */
188
+ claimed?: boolean;
176
189
  trustedIssuers: string[];
177
190
  /** This server's audience identifier, so a client can request an `aud`-scoped
178
191
  * identity token (OB-177). `null` for the single-server (unscoped) model. */
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Client for the OpenBook registry protocol (`openbook-registry/1`) — the
3
+ * read side a store-browsing app needs: the registry identity document, the
4
+ * paginated install index, per-plugin metadata, verified downloads, and the
5
+ * signed revocation feed.
6
+ *
7
+ * Trust model (PROTOCOL.md §6): a registry is identified by its pinned
8
+ * `(baseUrl, keys)`, never by anything a fetch returned. The caller supplies
9
+ * the pin; this client verifies everything it downloads against it —
10
+ * digest recomputed from the bytes, publisher signature against the index's
11
+ * `pinnedKeys`, notary countersignature and revocation entries against the
12
+ * pinned notary key. Any failed step throws: there is no partial trust.
13
+ */
14
+ import { type PluginManifest, type PluginPackage } from './plugins';
15
+ import type { FetchLike } from './client';
16
+ export type { FetchLike };
17
+ export declare function isSemver(v: string): boolean;
18
+ /**
19
+ * Semver precedence compare (spec §11): numeric MAJOR.MINOR.PATCH, then
20
+ * prerelease (absent > present; numeric identifiers numerically, and lower
21
+ * than alphanumeric ones). Build metadata is ignored. Returns <0, 0, >0.
22
+ * Inputs must already satisfy {@link isSemver}. Byte-for-byte port of the
23
+ * store's `compareSemver` (open-book-pub `packages/store/lib/canonical.ts`).
24
+ */
25
+ export declare function compareSemver(a: string, b: string): number;
26
+ /** `GET /api/v1/registry` — the identity document a client pins. */
27
+ export interface RegistryDocument {
28
+ protocol: string;
29
+ name: string;
30
+ baseUrl: string;
31
+ apiVersion: number;
32
+ algorithms: string[];
33
+ notaryPublicKey: string | null;
34
+ registryPublicKey: string | null;
35
+ fingerprints: {
36
+ notary: string | null;
37
+ registry: string | null;
38
+ };
39
+ endpoints: Record<string, string>;
40
+ }
41
+ /** One row of `GET /api/v1/index` — a plugin at its latest approved version. */
42
+ export interface RegistryIndexEntry {
43
+ id: string;
44
+ name: string;
45
+ description: string;
46
+ icon: string | null;
47
+ category: string | null;
48
+ publisher: string | null;
49
+ /** Publisher keys this plugin id is pinned to (TOFU), first-seen first. */
50
+ pinnedKeys?: string[];
51
+ /** @deprecated Use `pinnedKeys`; retained as the pre-rotation fallback. */
52
+ pinnedKey: string;
53
+ latestVersion: string;
54
+ /** Canonical digest of the latest approved version — what both signatures cover. */
55
+ digest: string;
56
+ /** Content address of the publisher's uploaded document (pre-splice). */
57
+ artifactSha256: string;
58
+ }
59
+ export interface RegistryIndexPage {
60
+ plugins: RegistryIndexEntry[];
61
+ limit: number;
62
+ nextCursor: string | null;
63
+ hasMore: boolean;
64
+ }
65
+ /** The notary countersignature spliced into approved downloads (§5.4.1). */
66
+ export interface RegistryNotarization {
67
+ registry: string;
68
+ publicKey: string;
69
+ signature: string;
70
+ algorithm: string;
71
+ timestamp: string;
72
+ }
73
+ export interface RegistryPluginVersion {
74
+ version: string;
75
+ digest: string;
76
+ artifactSha256: string;
77
+ status: string;
78
+ createdAt: string;
79
+ manifest: PluginManifest;
80
+ notarization: RegistryNotarization | null;
81
+ }
82
+ /** `GET /api/v1/plugins/{id}` — a plugin's version history. */
83
+ export interface RegistryPluginMeta {
84
+ id: string;
85
+ pinnedKeys?: string[];
86
+ /** @deprecated Use `pinnedKeys`; retained as the pre-rotation fallback. */
87
+ pinnedKey: string;
88
+ createdAt: string;
89
+ versions: RegistryPluginVersion[];
90
+ }
91
+ /** One signed entry of the revocation feed (§5.5). */
92
+ export interface RegistryRevocation {
93
+ id: string;
94
+ seq: number;
95
+ pluginId: string;
96
+ /** The revoked version, or null = every version, including future ones. */
97
+ version: string | null;
98
+ reason: string;
99
+ signerPublicKey: string | null;
100
+ signature: string | null;
101
+ createdAt: string;
102
+ }
103
+ /** The signed freshness/rollback anchor accompanying a revocation feed. */
104
+ export interface RegistryRevocationHead {
105
+ maxSeq: number;
106
+ generatedAt: string;
107
+ publicKey: string;
108
+ signature: string;
109
+ }
110
+ /** The trust states a verified download can earn (§6.5). */
111
+ export interface RegistryTrust {
112
+ /** Package signature verifies with the pinned `registryPublicKey`. */
113
+ firstParty: boolean;
114
+ /** Notary countersignature verifies with the pinned `notaryPublicKey`. */
115
+ notarised: boolean;
116
+ }
117
+ /** A fully verified download: the package plus what the verification proved. */
118
+ export interface VerifiedDownload {
119
+ pkg: PluginPackage;
120
+ /** The recomputed canonical digest the signatures were checked over. */
121
+ digest: string;
122
+ trust: RegistryTrust;
123
+ notarization: RegistryNotarization | null;
124
+ }
125
+ /** The pin a client persists for a registry: base URL + keys, per §6.2. */
126
+ export interface RegistryPin {
127
+ baseUrl: string;
128
+ name?: string;
129
+ notaryPublicKey?: string | null;
130
+ registryPublicKey?: string | null;
131
+ }
132
+ /** A typed failure from the registry client; `code` is stable for branching. */
133
+ export declare class RegistryError extends Error {
134
+ readonly code: 'unsupported_protocol' | 'insecure_base_url' | 'http_error' | 'revoked' | 'not_found' | 'digest_mismatch' | 'etag_mismatch' | 'signature_invalid' | 'notarization_invalid' | 'revocation_unavailable' | 'bad_response';
135
+ constructor(code: 'unsupported_protocol' | 'insecure_base_url' | 'http_error' | 'revoked' | 'not_found' | 'digest_mismatch' | 'etag_mismatch' | 'signature_invalid' | 'notarization_invalid' | 'revocation_unavailable' | 'bad_response', message: string);
136
+ }
137
+ /** Must exceed the endpoint's 60s max-age + 300s SWR cache floor (§6.6). */
138
+ export declare const REVOCATION_STALENESS_MS: number;
139
+ /** HTTPS only, except an explicit developer-local http://localhost (§3). */
140
+ export declare function registryBaseUrlProblem(baseUrl: string): string | null;
141
+ /**
142
+ * Human-comparable fingerprint of a raw Ed25519 key (§6.2): SHA-256 of the
143
+ * raw 32 bytes, truncated to 128 bits, as eight dash-separated hex quads.
144
+ */
145
+ export declare function registryKeyFingerprint(publicKeyBase64: string): Promise<string>;
146
+ /** Does a revocation entry cover this `(pluginId, version)`? `version: null` covers all. */
147
+ export declare function revocationMatches(entry: Pick<RegistryRevocation, 'pluginId' | 'version'>, pluginId: string, version: string): boolean;
148
+ /**
149
+ * Fetch + validate a registry's identity document WITHOUT a pin — the
150
+ * add-a-registry flow (§6.2): the caller must display the fingerprints for
151
+ * out-of-band confirmation before pinning anything from this response.
152
+ */
153
+ export declare function fetchRegistryDocument(baseUrl: string, fetchImpl?: FetchLike): Promise<RegistryDocument>;
154
+ /**
155
+ * The read client for one pinned registry. All verification happens against
156
+ * the PIN handed to the constructor — never against keys learned from the
157
+ * network afterwards.
158
+ */
159
+ export declare class RegistryClient {
160
+ readonly pin: RegistryPin;
161
+ private readonly fetchImpl;
162
+ private doc;
163
+ constructor(pin: RegistryPin, opts?: {
164
+ fetch?: FetchLike;
165
+ });
166
+ private get base();
167
+ private endpoint;
168
+ private json;
169
+ /**
170
+ * The registry's identity document (fetched once, cached). Checked against
171
+ * the pin: a protocol this client does not speak is refused, and a document
172
+ * whose keys disagree with pinned keys is refused as a possible attack
173
+ * (§6.2 — a pin change is a NEW trust decision, never a silent update).
174
+ */
175
+ document(): Promise<RegistryDocument>;
176
+ /** One page of the install index (§5.2). */
177
+ indexPage(opts?: {
178
+ limit?: number;
179
+ cursor?: string;
180
+ q?: string;
181
+ category?: string;
182
+ }): Promise<RegistryIndexPage>;
183
+ /**
184
+ * Walk the whole (filtered) index, one page at a time — cursors are walked
185
+ * sequentially per §9, capped defensively against a cursor loop.
186
+ */
187
+ indexAll(opts?: {
188
+ q?: string;
189
+ category?: string;
190
+ limit?: number;
191
+ }): Promise<RegistryIndexEntry[]>;
192
+ /** Find one plugin's index row by exact id (via the substring search filter). */
193
+ findIndexEntry(pluginId: string): Promise<RegistryIndexEntry | null>;
194
+ /** A plugin's version history (§5.3). Null when the registry doesn't know the id. */
195
+ pluginMeta(pluginId: string): Promise<RegistryPluginMeta | null>;
196
+ /**
197
+ * Download one version and verify it offline (§6.3). Every step must pass:
198
+ *
199
+ * 1. sha256(served bytes) equals the strong `ETag` (transport integrity).
200
+ * 2. The canonical digest RECOMPUTED from manifest+files equals `expect.digest`
201
+ * (the value the index/metadata promised) and the `X-Canonical-Digest` hint.
202
+ * 3. The publisher signature verifies over that digest with ANY member of
203
+ * `expect.pinnedKeys` — never with the key that travelled inside the package.
204
+ * 4. A present notary countersignature verifies with the PINNED notary key —
205
+ * present-but-invalid is a hard failure, not a downgrade to "unnotarised".
206
+ *
207
+ * Revocations are the caller's step (§6.6): check {@link revocations} before
208
+ * installing what this returns.
209
+ */
210
+ download(pluginId: string, version: string, expect: {
211
+ digest: string;
212
+ pinnedKeys: string[];
213
+ }): Promise<VerifiedDownload>;
214
+ /**
215
+ * The revocation feed (§5.5, §6.6), from `since` (exclusive). Entries are
216
+ * verified against the pinned notary key; on a registry that advertises a
217
+ * notary key, unsigned or badly-signed entries are IGNORED (an injection is
218
+ * not a kill switch). Only a registry with no notary key at all has its
219
+ * unsigned entries honoured — failing open on the kill switch is the
220
+ * dangerous direction.
221
+ */
222
+ revocations(since?: number): Promise<{
223
+ maxSeq: number;
224
+ entries: RegistryRevocation[];
225
+ generatedAt: string | null;
226
+ }>;
227
+ }