@forestrie/receipt-verify 1.0.0 → 1.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.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Genesis-bound chain binding: which univocity contract and chain a
3
+ * forest's on-chain checkpoints are anchored to, plus the forest's own log
4
+ * id (labels -68011 / -68013 / -68010). Bound once at genesis and public
5
+ * (ADR-0045) — the address and chain id are properties of the FOREST, not
6
+ * of the operator serving it. See {@link decodeChainBindingFromGenesis}.
7
+ * Mirrors mcp-resolve's `ChainBinding` (plan-2609-07 L3) so that consumer
8
+ * can delete its own copy, except `logId` here is the raw 32-byte wire
9
+ * value rather than a formatted UUID string.
10
+ */
11
+ export interface ChainBinding {
12
+ /** `0x` + 40 lowercase hex univocity contract address. */
13
+ univocity: string;
14
+ chainId: number;
15
+ /** Raw 32 bytes of label -68010: 16 zero bytes then the 16-byte forest
16
+ * bootstrap log id. */
17
+ logId: Uint8Array;
18
+ }
19
+ //# sourceMappingURL=chain-binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain-binding.d.ts","sourceRoot":"","sources":["../src/chain-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB;4BACwB;IACxB,KAAK,EAAE,UAAU,CAAC;CACnB"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { ChainBinding } from "./chain-binding.js";
2
+ /**
3
+ * Decode the chain binding — univocity contract address, chain id, and the
4
+ * forest's own log id — out of a forest genesis document CBOR blob (labels
5
+ * -68011 / -68013 / -68010). Mirrors mcp-resolve's `genesis-binding.ts`
6
+ * `decodeChainBindingFromGenesis` (plan-2609-07 L3) so that consumer can
7
+ * delete its own copy, except `logId` here stays the raw 32-byte wire value
8
+ * rather than being reformatted as a UUID string.
9
+ *
10
+ * Throws a plain `Error` for any genesis document that does not decode into
11
+ * a usable chain binding: not CBOR, not a map, wrong schema version, or a
12
+ * label absent or mis-sized/mistyped.
13
+ */
14
+ export declare function decodeChainBindingFromGenesis(genesis: Uint8Array): ChainBinding;
15
+ //# sourceMappingURL=decode-chain-binding-from-genesis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-chain-binding-from-genesis.d.ts","sourceRoot":"","sources":["../src/decode-chain-binding-from-genesis.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAMvD;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,UAAU,GAClB,YAAY,CA4Cd"}
@@ -0,0 +1,55 @@
1
+ import { decodeCborDeterministic } from "@forestrie/encoding";
2
+ import { asGenesisUint8Array, decodeGenesisBodyAsIntKeyMap, } from "./decode-genesis-cbor-map.js";
3
+ import { FOREST_GENESIS_LABEL_CHAIN_ID, FOREST_GENESIS_LABEL_GENESIS_VERSION, FOREST_GENESIS_LABEL_LOG_ID, FOREST_GENESIS_LABEL_UNIVOCITY_ADDR, FOREST_GENESIS_SCHEMA_V2, } from "./forest-genesis-labels.js";
4
+ function bytesToLowerHex(bytes) {
5
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
6
+ }
7
+ /**
8
+ * Decode the chain binding — univocity contract address, chain id, and the
9
+ * forest's own log id — out of a forest genesis document CBOR blob (labels
10
+ * -68011 / -68013 / -68010). Mirrors mcp-resolve's `genesis-binding.ts`
11
+ * `decodeChainBindingFromGenesis` (plan-2609-07 L3) so that consumer can
12
+ * delete its own copy, except `logId` here stays the raw 32-byte wire value
13
+ * rather than being reformatted as a UUID string.
14
+ *
15
+ * Throws a plain `Error` for any genesis document that does not decode into
16
+ * a usable chain binding: not CBOR, not a map, wrong schema version, or a
17
+ * label absent or mis-sized/mistyped.
18
+ */
19
+ export function decodeChainBindingFromGenesis(genesis) {
20
+ let raw;
21
+ try {
22
+ raw = decodeCborDeterministic(genesis);
23
+ }
24
+ catch (err) {
25
+ throw new Error(`genesis CBOR decode failed: ${err instanceof Error ? err.message : String(err)}`);
26
+ }
27
+ const m = decodeGenesisBodyAsIntKeyMap(raw);
28
+ if (!m)
29
+ throw new Error("genesis document must be a CBOR map");
30
+ const versionRaw = m.get(FOREST_GENESIS_LABEL_GENESIS_VERSION);
31
+ if (versionRaw === undefined) {
32
+ throw new Error("genesis version label absent");
33
+ }
34
+ const version = typeof versionRaw === "bigint" ? Number(versionRaw) : versionRaw;
35
+ if (version !== FOREST_GENESIS_SCHEMA_V2) {
36
+ throw new Error(`genesis version ${String(version)} is not ${FOREST_GENESIS_SCHEMA_V2}`);
37
+ }
38
+ const addr = asGenesisUint8Array(m.get(FOREST_GENESIS_LABEL_UNIVOCITY_ADDR));
39
+ if (!addr || addr.length !== 20) {
40
+ throw new Error("univocity address label absent or not 20 bytes");
41
+ }
42
+ const chainIdRaw = m.get(FOREST_GENESIS_LABEL_CHAIN_ID);
43
+ if (typeof chainIdRaw !== "string" || !/^[0-9]+$/.test(chainIdRaw)) {
44
+ throw new Error("chain id label absent or not a decimal string");
45
+ }
46
+ const logId = asGenesisUint8Array(m.get(FOREST_GENESIS_LABEL_LOG_ID));
47
+ if (!logId || logId.length !== 32) {
48
+ throw new Error("log id label absent or not 32 bytes");
49
+ }
50
+ return {
51
+ univocity: `0x${bytesToLowerHex(addr)}`,
52
+ chainId: Number(chainIdRaw),
53
+ logId,
54
+ };
55
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shared decode step for a genesis document's CBOR body: deterministic CBOR
3
+ * decodes an integer-keyed map either as a native `Map` or as a plain
4
+ * object (encoder-dependent), so callers that read genesis labels normalise
5
+ * to a `Map<number, unknown>` once here rather than each re-implementing
6
+ * the same fallback. Used by {@link decodeTrustRootFromGenesis} and
7
+ * {@link decodeChainBindingFromGenesis}.
8
+ */
9
+ export declare function decodeGenesisBodyAsIntKeyMap(raw: unknown): Map<number, unknown> | null;
10
+ /** Narrow a decoded genesis field to `Uint8Array`, or `null` if it is not one. */
11
+ export declare function asGenesisUint8Array(v: unknown): Uint8Array | null;
12
+ //# sourceMappingURL=decode-genesis-cbor-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-genesis-cbor-map.d.ts","sourceRoot":"","sources":["../src/decode-genesis-cbor-map.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,OAAO,GACX,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAW7B;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI,CAEjE"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared decode step for a genesis document's CBOR body: deterministic CBOR
3
+ * decodes an integer-keyed map either as a native `Map` or as a plain
4
+ * object (encoder-dependent), so callers that read genesis labels normalise
5
+ * to a `Map<number, unknown>` once here rather than each re-implementing
6
+ * the same fallback. Used by {@link decodeTrustRootFromGenesis} and
7
+ * {@link decodeChainBindingFromGenesis}.
8
+ */
9
+ export function decodeGenesisBodyAsIntKeyMap(raw) {
10
+ if (raw instanceof Map)
11
+ return raw;
12
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
13
+ const out = new Map();
14
+ for (const [k, v] of Object.entries(raw)) {
15
+ const n = Number(k);
16
+ if (Number.isFinite(n))
17
+ out.set(n, v);
18
+ }
19
+ return out;
20
+ }
21
+ return null;
22
+ }
23
+ /** Narrow a decoded genesis field to `Uint8Array`, or `null` if it is not one. */
24
+ export function asGenesisUint8Array(v) {
25
+ return v instanceof Uint8Array ? v : null;
26
+ }
@@ -1,7 +1,25 @@
1
- import type { RootVerifyKey } from "./root-verify-key.js";
1
+ import { type RootVerifyKey } from "./root-verify-key.js";
2
+ import type { DecodedTrustRoot } from "./decoded-trust-root.js";
2
3
  /**
3
- * Extract receipt verify key from a forest genesis document CBOR blob.
4
- * Offline path: genesis-only trust anchor (ADR-0045).
4
+ * Extract the receipt verify key from a forest genesis document CBOR blob,
5
+ * alongside `bootstrapKeyXy` when the bootstrap key is ES256. Offline path:
6
+ * genesis-only trust anchor (ADR-0045). See {@link DecodedTrustRoot}.
7
+ * `bootstrapKeyXy` is read directly from the genesis-encoded bytes (never
8
+ * exported from the non-extractable `CryptoKey` in `key`), and is
9
+ * `undefined` for a KS256 v2 bootstrap key (an on-chain address — there is
10
+ * no P-256 public key to give up in that case).
11
+ *
12
+ * This is the single decode path: {@link decodeTrustRootFromGenesis} is
13
+ * `(await decodeTrustRootDetailsFromGenesis(genesisCbor)).key`.
14
+ */
15
+ export declare function decodeTrustRootDetailsFromGenesis(genesisCbor: Uint8Array): Promise<DecodedTrustRoot>;
16
+ /**
17
+ * Extract the receipt verify key from a forest genesis document CBOR blob.
18
+ * Offline path: genesis-only trust anchor (ADR-0045). The origin/main
19
+ * signature and behaviour (including a KS256 v2 bootstrap key resolving to
20
+ * the `ParsedKs256RootKey` on-chain address, not a throw): callers pinned
21
+ * to `RootVerifyKey` keep working unchanged. For the raw bootstrap public
22
+ * key bytes as well, use {@link decodeTrustRootDetailsFromGenesis}.
5
23
  */
6
24
  export declare function decodeTrustRootFromGenesis(genesisCbor: Uint8Array): Promise<RootVerifyKey>;
7
25
  //# sourceMappingURL=decode-trust-root-from-genesis.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decode-trust-root-from-genesis.d.ts","sourceRoot":"","sources":["../src/decode-trust-root-from-genesis.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAmB1D;;;GAGG;AACH,wBAAsB,0BAA0B,CAC9C,WAAW,EAAE,UAAU,GACtB,OAAO,CAAC,aAAa,CAAC,CA0CxB"}
1
+ {"version":3,"file":"decode-trust-root-from-genesis.d.ts","sourceRoot":"","sources":["../src/decode-trust-root-from-genesis.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAKhF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;;;;;;GAWG;AACH,wBAAsB,iCAAiC,CACrD,WAAW,EAAE,UAAU,GACtB,OAAO,CAAC,gBAAgB,CAAC,CA+C3B;AAED;;;;;;;GAOG;AACH,wBAAsB,0BAA0B,CAC9C,WAAW,EAAE,UAAU,GACtB,OAAO,CAAC,aAAa,CAAC,CAExB"}
@@ -2,28 +2,21 @@ import { decodeCborDeterministic } from "@forestrie/encoding";
2
2
  import { COSE_ALG_ES256, COSE_CRV_P256, COSE_EC2_CRV, COSE_EC2_X, COSE_EC2_Y, COSE_KEY_ALG, COSE_KEY_KTY, COSE_KTY_EC2, } from "./cose-key.js";
3
3
  import { FOREST_GENESIS_LABEL_BOOTSTRAP_KEY, FOREST_GENESIS_LABEL_GENESIS_ALG, FOREST_GENESIS_LABEL_GENESIS_VERSION, FOREST_GENESIS_SCHEMA_V1, FOREST_GENESIS_SCHEMA_V2, } from "./forest-genesis-labels.js";
4
4
  import { decodeTrustRootCbor } from "./decode-trust-root-cbor.js";
5
- function decodeBodyAsIntKeyMap(raw) {
6
- if (raw instanceof Map)
7
- return raw;
8
- if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
9
- const out = new Map();
10
- for (const [k, v] of Object.entries(raw)) {
11
- const n = Number(k);
12
- if (Number.isFinite(n))
13
- out.set(n, v);
14
- }
15
- return out;
16
- }
17
- return null;
18
- }
19
- function asGenesisUint8Array(v) {
20
- return v instanceof Uint8Array ? v : null;
21
- }
5
+ import { isParsedKs256RootKey } from "./root-verify-key.js";
6
+ import { asGenesisUint8Array, decodeGenesisBodyAsIntKeyMap, } from "./decode-genesis-cbor-map.js";
22
7
  /**
23
- * Extract receipt verify key from a forest genesis document CBOR blob.
24
- * Offline path: genesis-only trust anchor (ADR-0045).
8
+ * Extract the receipt verify key from a forest genesis document CBOR blob,
9
+ * alongside `bootstrapKeyXy` when the bootstrap key is ES256. Offline path:
10
+ * genesis-only trust anchor (ADR-0045). See {@link DecodedTrustRoot}.
11
+ * `bootstrapKeyXy` is read directly from the genesis-encoded bytes (never
12
+ * exported from the non-extractable `CryptoKey` in `key`), and is
13
+ * `undefined` for a KS256 v2 bootstrap key (an on-chain address — there is
14
+ * no P-256 public key to give up in that case).
15
+ *
16
+ * This is the single decode path: {@link decodeTrustRootFromGenesis} is
17
+ * `(await decodeTrustRootDetailsFromGenesis(genesisCbor)).key`.
25
18
  */
26
- export async function decodeTrustRootFromGenesis(genesisCbor) {
19
+ export async function decodeTrustRootDetailsFromGenesis(genesisCbor) {
27
20
  let raw;
28
21
  try {
29
22
  raw = decodeCborDeterministic(genesisCbor);
@@ -31,7 +24,7 @@ export async function decodeTrustRootFromGenesis(genesisCbor) {
31
24
  catch {
32
25
  throw new Error("genesis CBOR decode failed");
33
26
  }
34
- const m = decodeBodyAsIntKeyMap(raw);
27
+ const m = decodeGenesisBodyAsIntKeyMap(raw);
35
28
  if (!m)
36
29
  throw new Error("genesis document must be a CBOR map");
37
30
  const versionRaw = m.get(FOREST_GENESIS_LABEL_GENESIS_VERSION);
@@ -41,7 +34,11 @@ export async function decodeTrustRootFromGenesis(genesisCbor) {
41
34
  if (bootstrapKey === null) {
42
35
  throw new Error("v2 genesis missing bootstrapKey");
43
36
  }
44
- return decodeTrustRootCbor({ alg, key: bootstrapKey });
37
+ const key = await decodeTrustRootCbor({ alg, key: bootstrapKey });
38
+ return {
39
+ key,
40
+ bootstrapKeyXy: isParsedKs256RootKey(key) ? undefined : bootstrapKey,
41
+ };
45
42
  }
46
43
  const kty = m.get(COSE_KEY_KTY);
47
44
  const crv = m.get(COSE_EC2_CRV);
@@ -55,10 +52,22 @@ export async function decodeTrustRootFromGenesis(genesisCbor) {
55
52
  const xy = new Uint8Array(64);
56
53
  xy.set(x, 0);
57
54
  xy.set(y, 32);
58
- return decodeTrustRootCbor({ alg: COSE_ALG_ES256, key: xy });
55
+ const key = await decodeTrustRootCbor({ alg: COSE_ALG_ES256, key: xy });
56
+ return { key, bootstrapKeyXy: xy };
59
57
  }
60
58
  if (versionRaw === FOREST_GENESIS_SCHEMA_V1 || versionRaw === undefined) {
61
59
  throw new Error("unsupported or invalid genesis schema for trust root");
62
60
  }
63
61
  throw new Error("unsupported genesis document");
64
62
  }
63
+ /**
64
+ * Extract the receipt verify key from a forest genesis document CBOR blob.
65
+ * Offline path: genesis-only trust anchor (ADR-0045). The origin/main
66
+ * signature and behaviour (including a KS256 v2 bootstrap key resolving to
67
+ * the `ParsedKs256RootKey` on-chain address, not a throw): callers pinned
68
+ * to `RootVerifyKey` keep working unchanged. For the raw bootstrap public
69
+ * key bytes as well, use {@link decodeTrustRootDetailsFromGenesis}.
70
+ */
71
+ export async function decodeTrustRootFromGenesis(genesisCbor) {
72
+ return (await decodeTrustRootDetailsFromGenesis(genesisCbor)).key;
73
+ }
@@ -0,0 +1,17 @@
1
+ import type { RootVerifyKey } from "./root-verify-key.js";
2
+ /**
3
+ * Result of {@link decodeTrustRootDetailsFromGenesis}: the decoded verify
4
+ * key (an ES256 `CryptoKey`, or a KS256 on-chain address) plus, for the
5
+ * ES256 case, the raw 64-byte x||y P-256 public key coordinates the genesis
6
+ * document carries. `bootstrapKeyXy` is read directly from the
7
+ * genesis-encoded bytes, not exported from `key` (`key` stays
8
+ * non-extractable) — plan-2609-07 L3, for callers that need the
9
+ * serialisable public key material the `CryptoKey` cannot give up.
10
+ * `bootstrapKeyXy` is `undefined` for a KS256 v2 bootstrap key (an on-chain
11
+ * address — there is no P-256 public key to give up).
12
+ */
13
+ export interface DecodedTrustRoot {
14
+ key: RootVerifyKey;
15
+ bootstrapKeyXy?: Uint8Array;
16
+ }
17
+ //# sourceMappingURL=decoded-trust-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decoded-trust-root.d.ts","sourceRoot":"","sources":["../src/decoded-trust-root.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,aAAa,CAAC;IACnB,cAAc,CAAC,EAAE,UAAU,CAAC;CAC7B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,13 @@
1
1
  export declare const FOREST_GENESIS_LABEL_GENESIS_VERSION = -68009;
2
2
  export declare const FOREST_GENESIS_LABEL_GENESIS_ALG = -68014;
3
3
  export declare const FOREST_GENESIS_LABEL_BOOTSTRAP_KEY = -68015;
4
+ /**
5
+ * The forest's own log id, wire-encoded as 32 bytes: 16 zero bytes then the
6
+ * 16-byte forest bootstrap log id (plan-2609-07 L3; mirrors mcp-resolve's
7
+ * `genesis-binding.ts` naming so that consumer can import this instead of
8
+ * defining it locally). See {@link decodeChainBindingFromGenesis}.
9
+ */
10
+ export declare const FOREST_GENESIS_LABEL_LOG_ID = -68010;
4
11
  export declare const FOREST_GENESIS_LABEL_UNIVOCITY_ADDR = -68011;
5
12
  export declare const FOREST_GENESIS_LABEL_CHAIN_ID = -68013;
6
13
  export declare const FOREST_GENESIS_SCHEMA_V2 = 2;
@@ -1 +1 @@
1
- {"version":3,"file":"forest-genesis-labels.d.ts","sourceRoot":"","sources":["../src/forest-genesis-labels.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oCAAoC,SAAS,CAAC;AAC3D,eAAO,MAAM,gCAAgC,SAAS,CAAC;AACvD,eAAO,MAAM,kCAAkC,SAAS,CAAC;AACzD,eAAO,MAAM,mCAAmC,SAAS,CAAC;AAC1D,eAAO,MAAM,6BAA6B,SAAS,CAAC;AACpD,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,eAAO,MAAM,kBAAkB,SAAS,CAAC;AACzC,eAAO,MAAM,yBAAyB,SAAS,CAAC"}
1
+ {"version":3,"file":"forest-genesis-labels.d.ts","sourceRoot":"","sources":["../src/forest-genesis-labels.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oCAAoC,SAAS,CAAC;AAC3D,eAAO,MAAM,gCAAgC,SAAS,CAAC;AACvD,eAAO,MAAM,kCAAkC,SAAS,CAAC;AACzD;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,SAAS,CAAC;AAClD,eAAO,MAAM,mCAAmC,SAAS,CAAC;AAC1D,eAAO,MAAM,6BAA6B,SAAS,CAAC;AACpD,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,eAAO,MAAM,kBAAkB,SAAS,CAAC;AACzC,eAAO,MAAM,yBAAyB,SAAS,CAAC"}
@@ -1,6 +1,13 @@
1
1
  export const FOREST_GENESIS_LABEL_GENESIS_VERSION = -68009;
2
2
  export const FOREST_GENESIS_LABEL_GENESIS_ALG = -68014;
3
3
  export const FOREST_GENESIS_LABEL_BOOTSTRAP_KEY = -68015;
4
+ /**
5
+ * The forest's own log id, wire-encoded as 32 bytes: 16 zero bytes then the
6
+ * 16-byte forest bootstrap log id (plan-2609-07 L3; mirrors mcp-resolve's
7
+ * `genesis-binding.ts` naming so that consumer can import this instead of
8
+ * defining it locally). See {@link decodeChainBindingFromGenesis}.
9
+ */
10
+ export const FOREST_GENESIS_LABEL_LOG_ID = -68010;
4
11
  export const FOREST_GENESIS_LABEL_UNIVOCITY_ADDR = -68011;
5
12
  export const FOREST_GENESIS_LABEL_CHAIN_ID = -68013;
6
13
  export const FOREST_GENESIS_SCHEMA_V2 = 2;
package/dist/index.d.ts CHANGED
@@ -14,7 +14,27 @@ export type { FreshenReceiptInput, FreshenReceiptResult, } from "./freshen-recei
14
14
  * re-exported here to preserve the receipt-verify public surface.
15
15
  */
16
16
  export { peakMMRIndexes } from "@forestrie/merklelog";
17
- export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
17
+ /**
18
+ * `decodeTrustRootFromGenesis` keeps its origin/main signature and
19
+ * behaviour (`Promise<RootVerifyKey>`, including a KS256 v2 bootstrap key
20
+ * resolving without a throw) so pinned consumers (mcp-verify 1.0.0) are
21
+ * unaffected. `decodeTrustRootDetailsFromGenesis` is the additive sibling
22
+ * (plan-2609-07 L3) carrying `bootstrapKeyXy` alongside `key`.
23
+ */
24
+ export { decodeTrustRootDetailsFromGenesis, decodeTrustRootFromGenesis, } from "./decode-trust-root-from-genesis.js";
25
+ export type { DecodedTrustRoot } from "./decoded-trust-root.js";
26
+ /**
27
+ * Genesis document label constants (plan-2609-07 L3): exported from the
28
+ * package root so consumers stop redefining them locally (mcp-resolve's own
29
+ * `genesis-binding.ts` did, before this).
30
+ */
31
+ export { FOREST_GENESIS_LABEL_BOOTSTRAP_KEY, FOREST_GENESIS_LABEL_CHAIN_ID, FOREST_GENESIS_LABEL_GENESIS_ALG, FOREST_GENESIS_LABEL_GENESIS_VERSION, FOREST_GENESIS_LABEL_LOG_ID, FOREST_GENESIS_LABEL_UNIVOCITY_ADDR, } from "./forest-genesis-labels.js";
32
+ /**
33
+ * Genesis-bound chain binding (plan-2609-07 L3): mirrors mcp-resolve's own
34
+ * `decodeChainBindingFromGenesis` so that consumer can delete its copy.
35
+ */
36
+ export { decodeChainBindingFromGenesis } from "./decode-chain-binding-from-genesis.js";
37
+ export type { ChainBinding } from "./chain-binding.js";
18
38
  export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
19
39
  export { verifyReceiptOffline } from "./verify-grant-receipt-offline.js";
20
40
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -7,7 +7,25 @@ export { freshenReceipt } from "./freshen-receipt.js";
7
7
  * re-exported here to preserve the receipt-verify public surface.
8
8
  */
9
9
  export { peakMMRIndexes } from "@forestrie/merklelog";
10
- export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
10
+ /**
11
+ * `decodeTrustRootFromGenesis` keeps its origin/main signature and
12
+ * behaviour (`Promise<RootVerifyKey>`, including a KS256 v2 bootstrap key
13
+ * resolving without a throw) so pinned consumers (mcp-verify 1.0.0) are
14
+ * unaffected. `decodeTrustRootDetailsFromGenesis` is the additive sibling
15
+ * (plan-2609-07 L3) carrying `bootstrapKeyXy` alongside `key`.
16
+ */
17
+ export { decodeTrustRootDetailsFromGenesis, decodeTrustRootFromGenesis, } from "./decode-trust-root-from-genesis.js";
18
+ /**
19
+ * Genesis document label constants (plan-2609-07 L3): exported from the
20
+ * package root so consumers stop redefining them locally (mcp-resolve's own
21
+ * `genesis-binding.ts` did, before this).
22
+ */
23
+ export { FOREST_GENESIS_LABEL_BOOTSTRAP_KEY, FOREST_GENESIS_LABEL_CHAIN_ID, FOREST_GENESIS_LABEL_GENESIS_ALG, FOREST_GENESIS_LABEL_GENESIS_VERSION, FOREST_GENESIS_LABEL_LOG_ID, FOREST_GENESIS_LABEL_UNIVOCITY_ADDR, } from "./forest-genesis-labels.js";
24
+ /**
25
+ * Genesis-bound chain binding (plan-2609-07 L3): mirrors mcp-resolve's own
26
+ * `decodeChainBindingFromGenesis` so that consumer can delete its copy.
27
+ */
28
+ export { decodeChainBindingFromGenesis } from "./decode-chain-binding-from-genesis.js";
11
29
  export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
12
30
  export { verifyReceiptOffline } from "./verify-grant-receipt-offline.js";
13
31
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestrie/receipt-verify",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Offline SCITT grant receipt verification (ADR-0045)",
@@ -28,9 +28,9 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@noble/hashes": "^1.7.1",
31
- "@forestrie/chain-rpc": "0.2.0",
32
- "@forestrie/encoding": "0.7.0",
33
- "@forestrie/merklelog": "0.3.0"
31
+ "@forestrie/chain-rpc": "0.3.0",
32
+ "@forestrie/merklelog": "0.3.0",
33
+ "@forestrie/encoding": "0.7.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "esbuild": "^0.24.0",
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Genesis-bound chain binding: which univocity contract and chain a
3
+ * forest's on-chain checkpoints are anchored to, plus the forest's own log
4
+ * id (labels -68011 / -68013 / -68010). Bound once at genesis and public
5
+ * (ADR-0045) — the address and chain id are properties of the FOREST, not
6
+ * of the operator serving it. See {@link decodeChainBindingFromGenesis}.
7
+ * Mirrors mcp-resolve's `ChainBinding` (plan-2609-07 L3) so that consumer
8
+ * can delete its own copy, except `logId` here is the raw 32-byte wire
9
+ * value rather than a formatted UUID string.
10
+ */
11
+ export interface ChainBinding {
12
+ /** `0x` + 40 lowercase hex univocity contract address. */
13
+ univocity: string;
14
+ chainId: number;
15
+ /** Raw 32 bytes of label -68010: 16 zero bytes then the 16-byte forest
16
+ * bootstrap log id. */
17
+ logId: Uint8Array;
18
+ }
@@ -0,0 +1,77 @@
1
+ import { decodeCborDeterministic } from "@forestrie/encoding";
2
+ import {
3
+ asGenesisUint8Array,
4
+ decodeGenesisBodyAsIntKeyMap,
5
+ } from "./decode-genesis-cbor-map.js";
6
+ import {
7
+ FOREST_GENESIS_LABEL_CHAIN_ID,
8
+ FOREST_GENESIS_LABEL_GENESIS_VERSION,
9
+ FOREST_GENESIS_LABEL_LOG_ID,
10
+ FOREST_GENESIS_LABEL_UNIVOCITY_ADDR,
11
+ FOREST_GENESIS_SCHEMA_V2,
12
+ } from "./forest-genesis-labels.js";
13
+ import type { ChainBinding } from "./chain-binding.js";
14
+
15
+ function bytesToLowerHex(bytes: Uint8Array): string {
16
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
17
+ }
18
+
19
+ /**
20
+ * Decode the chain binding — univocity contract address, chain id, and the
21
+ * forest's own log id — out of a forest genesis document CBOR blob (labels
22
+ * -68011 / -68013 / -68010). Mirrors mcp-resolve's `genesis-binding.ts`
23
+ * `decodeChainBindingFromGenesis` (plan-2609-07 L3) so that consumer can
24
+ * delete its own copy, except `logId` here stays the raw 32-byte wire value
25
+ * rather than being reformatted as a UUID string.
26
+ *
27
+ * Throws a plain `Error` for any genesis document that does not decode into
28
+ * a usable chain binding: not CBOR, not a map, wrong schema version, or a
29
+ * label absent or mis-sized/mistyped.
30
+ */
31
+ export function decodeChainBindingFromGenesis(
32
+ genesis: Uint8Array,
33
+ ): ChainBinding {
34
+ let raw: unknown;
35
+ try {
36
+ raw = decodeCborDeterministic(genesis);
37
+ } catch (err) {
38
+ throw new Error(
39
+ `genesis CBOR decode failed: ${err instanceof Error ? err.message : String(err)}`,
40
+ );
41
+ }
42
+ const m = decodeGenesisBodyAsIntKeyMap(raw);
43
+ if (!m) throw new Error("genesis document must be a CBOR map");
44
+
45
+ const versionRaw = m.get(FOREST_GENESIS_LABEL_GENESIS_VERSION);
46
+ if (versionRaw === undefined) {
47
+ throw new Error("genesis version label absent");
48
+ }
49
+ const version =
50
+ typeof versionRaw === "bigint" ? Number(versionRaw) : versionRaw;
51
+ if (version !== FOREST_GENESIS_SCHEMA_V2) {
52
+ throw new Error(
53
+ `genesis version ${String(version)} is not ${FOREST_GENESIS_SCHEMA_V2}`,
54
+ );
55
+ }
56
+
57
+ const addr = asGenesisUint8Array(m.get(FOREST_GENESIS_LABEL_UNIVOCITY_ADDR));
58
+ if (!addr || addr.length !== 20) {
59
+ throw new Error("univocity address label absent or not 20 bytes");
60
+ }
61
+
62
+ const chainIdRaw = m.get(FOREST_GENESIS_LABEL_CHAIN_ID);
63
+ if (typeof chainIdRaw !== "string" || !/^[0-9]+$/.test(chainIdRaw)) {
64
+ throw new Error("chain id label absent or not a decimal string");
65
+ }
66
+
67
+ const logId = asGenesisUint8Array(m.get(FOREST_GENESIS_LABEL_LOG_ID));
68
+ if (!logId || logId.length !== 32) {
69
+ throw new Error("log id label absent or not 32 bytes");
70
+ }
71
+
72
+ return {
73
+ univocity: `0x${bytesToLowerHex(addr)}`,
74
+ chainId: Number(chainIdRaw),
75
+ logId,
76
+ };
77
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared decode step for a genesis document's CBOR body: deterministic CBOR
3
+ * decodes an integer-keyed map either as a native `Map` or as a plain
4
+ * object (encoder-dependent), so callers that read genesis labels normalise
5
+ * to a `Map<number, unknown>` once here rather than each re-implementing
6
+ * the same fallback. Used by {@link decodeTrustRootFromGenesis} and
7
+ * {@link decodeChainBindingFromGenesis}.
8
+ */
9
+ export function decodeGenesisBodyAsIntKeyMap(
10
+ raw: unknown,
11
+ ): Map<number, unknown> | null {
12
+ if (raw instanceof Map) return raw as Map<number, unknown>;
13
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
14
+ const out = new Map<number, unknown>();
15
+ for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
16
+ const n = Number(k);
17
+ if (Number.isFinite(n)) out.set(n, v);
18
+ }
19
+ return out;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ /** Narrow a decoded genesis field to `Uint8Array`, or `null` if it is not one. */
25
+ export function asGenesisUint8Array(v: unknown): Uint8Array | null {
26
+ return v instanceof Uint8Array ? v : null;
27
+ }
@@ -17,39 +17,35 @@ import {
17
17
  FOREST_GENESIS_SCHEMA_V2,
18
18
  } from "./forest-genesis-labels.js";
19
19
  import { decodeTrustRootCbor } from "./decode-trust-root-cbor.js";
20
- import type { RootVerifyKey } from "./root-verify-key.js";
21
-
22
- function decodeBodyAsIntKeyMap(raw: unknown): Map<number, unknown> | null {
23
- if (raw instanceof Map) return raw as Map<number, unknown>;
24
- if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
25
- const out = new Map<number, unknown>();
26
- for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
27
- const n = Number(k);
28
- if (Number.isFinite(n)) out.set(n, v);
29
- }
30
- return out;
31
- }
32
- return null;
33
- }
34
-
35
- function asGenesisUint8Array(v: unknown): Uint8Array | null {
36
- return v instanceof Uint8Array ? v : null;
37
- }
20
+ import { isParsedKs256RootKey, type RootVerifyKey } from "./root-verify-key.js";
21
+ import {
22
+ asGenesisUint8Array,
23
+ decodeGenesisBodyAsIntKeyMap,
24
+ } from "./decode-genesis-cbor-map.js";
25
+ import type { DecodedTrustRoot } from "./decoded-trust-root.js";
38
26
 
39
27
  /**
40
- * Extract receipt verify key from a forest genesis document CBOR blob.
41
- * Offline path: genesis-only trust anchor (ADR-0045).
28
+ * Extract the receipt verify key from a forest genesis document CBOR blob,
29
+ * alongside `bootstrapKeyXy` when the bootstrap key is ES256. Offline path:
30
+ * genesis-only trust anchor (ADR-0045). See {@link DecodedTrustRoot}.
31
+ * `bootstrapKeyXy` is read directly from the genesis-encoded bytes (never
32
+ * exported from the non-extractable `CryptoKey` in `key`), and is
33
+ * `undefined` for a KS256 v2 bootstrap key (an on-chain address — there is
34
+ * no P-256 public key to give up in that case).
35
+ *
36
+ * This is the single decode path: {@link decodeTrustRootFromGenesis} is
37
+ * `(await decodeTrustRootDetailsFromGenesis(genesisCbor)).key`.
42
38
  */
43
- export async function decodeTrustRootFromGenesis(
39
+ export async function decodeTrustRootDetailsFromGenesis(
44
40
  genesisCbor: Uint8Array,
45
- ): Promise<RootVerifyKey> {
41
+ ): Promise<DecodedTrustRoot> {
46
42
  let raw: unknown;
47
43
  try {
48
44
  raw = decodeCborDeterministic(genesisCbor);
49
45
  } catch {
50
46
  throw new Error("genesis CBOR decode failed");
51
47
  }
52
- const m = decodeBodyAsIntKeyMap(raw);
48
+ const m = decodeGenesisBodyAsIntKeyMap(raw);
53
49
  if (!m) throw new Error("genesis document must be a CBOR map");
54
50
 
55
51
  const versionRaw = m.get(FOREST_GENESIS_LABEL_GENESIS_VERSION);
@@ -61,7 +57,11 @@ export async function decodeTrustRootFromGenesis(
61
57
  if (bootstrapKey === null) {
62
58
  throw new Error("v2 genesis missing bootstrapKey");
63
59
  }
64
- return decodeTrustRootCbor({ alg, key: bootstrapKey });
60
+ const key = await decodeTrustRootCbor({ alg, key: bootstrapKey });
61
+ return {
62
+ key,
63
+ bootstrapKeyXy: isParsedKs256RootKey(key) ? undefined : bootstrapKey,
64
+ };
65
65
  }
66
66
 
67
67
  const kty = m.get(COSE_KEY_KTY);
@@ -76,7 +76,8 @@ export async function decodeTrustRootFromGenesis(
76
76
  const xy = new Uint8Array(64);
77
77
  xy.set(x, 0);
78
78
  xy.set(y, 32);
79
- return decodeTrustRootCbor({ alg: COSE_ALG_ES256, key: xy });
79
+ const key = await decodeTrustRootCbor({ alg: COSE_ALG_ES256, key: xy });
80
+ return { key, bootstrapKeyXy: xy };
80
81
  }
81
82
 
82
83
  if (versionRaw === FOREST_GENESIS_SCHEMA_V1 || versionRaw === undefined) {
@@ -85,3 +86,17 @@ export async function decodeTrustRootFromGenesis(
85
86
 
86
87
  throw new Error("unsupported genesis document");
87
88
  }
89
+
90
+ /**
91
+ * Extract the receipt verify key from a forest genesis document CBOR blob.
92
+ * Offline path: genesis-only trust anchor (ADR-0045). The origin/main
93
+ * signature and behaviour (including a KS256 v2 bootstrap key resolving to
94
+ * the `ParsedKs256RootKey` on-chain address, not a throw): callers pinned
95
+ * to `RootVerifyKey` keep working unchanged. For the raw bootstrap public
96
+ * key bytes as well, use {@link decodeTrustRootDetailsFromGenesis}.
97
+ */
98
+ export async function decodeTrustRootFromGenesis(
99
+ genesisCbor: Uint8Array,
100
+ ): Promise<RootVerifyKey> {
101
+ return (await decodeTrustRootDetailsFromGenesis(genesisCbor)).key;
102
+ }
@@ -0,0 +1,17 @@
1
+ import type { RootVerifyKey } from "./root-verify-key.js";
2
+
3
+ /**
4
+ * Result of {@link decodeTrustRootDetailsFromGenesis}: the decoded verify
5
+ * key (an ES256 `CryptoKey`, or a KS256 on-chain address) plus, for the
6
+ * ES256 case, the raw 64-byte x||y P-256 public key coordinates the genesis
7
+ * document carries. `bootstrapKeyXy` is read directly from the
8
+ * genesis-encoded bytes, not exported from `key` (`key` stays
9
+ * non-extractable) — plan-2609-07 L3, for callers that need the
10
+ * serialisable public key material the `CryptoKey` cannot give up.
11
+ * `bootstrapKeyXy` is `undefined` for a KS256 v2 bootstrap key (an on-chain
12
+ * address — there is no P-256 public key to give up).
13
+ */
14
+ export interface DecodedTrustRoot {
15
+ key: RootVerifyKey;
16
+ bootstrapKeyXy?: Uint8Array;
17
+ }
@@ -1,6 +1,13 @@
1
1
  export const FOREST_GENESIS_LABEL_GENESIS_VERSION = -68009;
2
2
  export const FOREST_GENESIS_LABEL_GENESIS_ALG = -68014;
3
3
  export const FOREST_GENESIS_LABEL_BOOTSTRAP_KEY = -68015;
4
+ /**
5
+ * The forest's own log id, wire-encoded as 32 bytes: 16 zero bytes then the
6
+ * 16-byte forest bootstrap log id (plan-2609-07 L3; mirrors mcp-resolve's
7
+ * `genesis-binding.ts` naming so that consumer can import this instead of
8
+ * defining it locally). See {@link decodeChainBindingFromGenesis}.
9
+ */
10
+ export const FOREST_GENESIS_LABEL_LOG_ID = -68010;
4
11
  export const FOREST_GENESIS_LABEL_UNIVOCITY_ADDR = -68011;
5
12
  export const FOREST_GENESIS_LABEL_CHAIN_ID = -68013;
6
13
  export const FOREST_GENESIS_SCHEMA_V2 = 2;
package/src/index.ts CHANGED
@@ -31,7 +31,37 @@ export type {
31
31
  * re-exported here to preserve the receipt-verify public surface.
32
32
  */
33
33
  export { peakMMRIndexes } from "@forestrie/merklelog";
34
- export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
34
+ /**
35
+ * `decodeTrustRootFromGenesis` keeps its origin/main signature and
36
+ * behaviour (`Promise<RootVerifyKey>`, including a KS256 v2 bootstrap key
37
+ * resolving without a throw) so pinned consumers (mcp-verify 1.0.0) are
38
+ * unaffected. `decodeTrustRootDetailsFromGenesis` is the additive sibling
39
+ * (plan-2609-07 L3) carrying `bootstrapKeyXy` alongside `key`.
40
+ */
41
+ export {
42
+ decodeTrustRootDetailsFromGenesis,
43
+ decodeTrustRootFromGenesis,
44
+ } from "./decode-trust-root-from-genesis.js";
45
+ export type { DecodedTrustRoot } from "./decoded-trust-root.js";
46
+ /**
47
+ * Genesis document label constants (plan-2609-07 L3): exported from the
48
+ * package root so consumers stop redefining them locally (mcp-resolve's own
49
+ * `genesis-binding.ts` did, before this).
50
+ */
51
+ export {
52
+ FOREST_GENESIS_LABEL_BOOTSTRAP_KEY,
53
+ FOREST_GENESIS_LABEL_CHAIN_ID,
54
+ FOREST_GENESIS_LABEL_GENESIS_ALG,
55
+ FOREST_GENESIS_LABEL_GENESIS_VERSION,
56
+ FOREST_GENESIS_LABEL_LOG_ID,
57
+ FOREST_GENESIS_LABEL_UNIVOCITY_ADDR,
58
+ } from "./forest-genesis-labels.js";
59
+ /**
60
+ * Genesis-bound chain binding (plan-2609-07 L3): mirrors mcp-resolve's own
61
+ * `decodeChainBindingFromGenesis` so that consumer can delete its copy.
62
+ */
63
+ export { decodeChainBindingFromGenesis } from "./decode-chain-binding-from-genesis.js";
64
+ export type { ChainBinding } from "./chain-binding.js";
35
65
  export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
36
66
  export { verifyReceiptOffline } from "./verify-grant-receipt-offline.js";
37
67
  /**