@book.dev/sdk 3.7.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.
- package/dist/ai.d.ts +12 -2
- package/dist/ai.js.map +1 -1
- package/dist/blockCatalogue.d.ts +560 -0
- package/dist/blockCatalogue.js +336 -0
- package/dist/blockCatalogue.js.map +1 -0
- package/dist/bookFolder.d.ts +9 -1
- package/dist/bookFolder.js +64 -2
- package/dist/bookFolder.js.map +1 -1
- package/dist/client.d.ts +22 -2
- package/dist/client.js +6 -2
- package/dist/client.js.map +1 -1
- package/dist/content.d.ts +40 -1
- package/dist/content.js +29 -7
- package/dist/content.js.map +1 -1
- package/dist/forwarding/forwardingClient.d.ts +68 -2
- package/dist/forwarding/forwardingClient.js +126 -12
- package/dist/forwarding/forwardingClient.js.map +1 -1
- package/dist/forwarding/index.d.ts +2 -1
- package/dist/forwarding/index.js +2 -1
- package/dist/forwarding/index.js.map +1 -1
- package/dist/forwarding/namespacedKeyStore.d.ts +52 -0
- package/dist/forwarding/namespacedKeyStore.js +99 -0
- package/dist/forwarding/namespacedKeyStore.js.map +1 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +29 -0
- package/dist/ledger.js +43 -0
- package/dist/ledger.js.map +1 -1
- package/dist/ledgerExportSection.d.ts +160 -0
- package/dist/ledgerExportSection.js +594 -0
- package/dist/ledgerExportSection.js.map +1 -0
- package/dist/orderKeys.d.ts +36 -0
- package/dist/orderKeys.js +88 -0
- package/dist/orderKeys.js.map +1 -0
- package/dist/plugins.d.ts +76 -4
- package/dist/plugins.js +95 -12
- package/dist/plugins.js.map +1 -1
- package/dist/provenance.d.ts +13 -0
- package/dist/registryClient.d.ts +227 -0
- package/dist/registryClient.js +397 -0
- package/dist/registryClient.js.map +1 -0
- package/dist/routes.d.ts +9 -0
- package/dist/routes.js +9 -0
- package/dist/routes.js.map +1 -1
- package/dist/suggestions.d.ts +12 -2
- package/dist/tableSnapshot.d.ts +233 -0
- package/dist/tableSnapshot.js +680 -0
- package/dist/tableSnapshot.js.map +1 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +7 -2
- package/dist/templates.js.map +1 -1
- 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":"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"}
|
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
|
|
125
|
-
* additional registries are user-added keys on top (Settings →
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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(
|
|
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
|
|
110
|
-
* additional registries are user-added keys on top (Settings →
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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) {
|
package/dist/plugins.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA2BH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"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"}
|
package/dist/provenance.d.ts
CHANGED
|
@@ -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
|
+
}
|