@forestrie/forestrie-cli 0.8.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +425 -0
  3. package/TRUST-MODEL.md +8 -0
  4. package/dist/cli.js +33811 -0
  5. package/dist/decode-receipt.d.ts +19 -0
  6. package/dist/decode-receipt.d.ts.map +1 -0
  7. package/dist/decode-receipt.js +19 -0
  8. package/dist/decode-receipt.js.map +1 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +11 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/decode-receipt-cbor.d.ts +26 -0
  14. package/dist/lib/decode-receipt-cbor.d.ts.map +1 -0
  15. package/dist/lib/decode-receipt-cbor.js +182 -0
  16. package/dist/lib/decode-receipt-cbor.js.map +1 -0
  17. package/dist/lib/decode-receipt-decode.d.ts +103 -0
  18. package/dist/lib/decode-receipt-decode.d.ts.map +1 -0
  19. package/dist/lib/decode-receipt-decode.js +237 -0
  20. package/dist/lib/decode-receipt-decode.js.map +1 -0
  21. package/dist/lib/decode-receipt-labels.d.ts +54 -0
  22. package/dist/lib/decode-receipt-labels.d.ts.map +1 -0
  23. package/dist/lib/decode-receipt-labels.js +117 -0
  24. package/dist/lib/decode-receipt-labels.js.map +1 -0
  25. package/dist/lib/decode-receipt-render.d.ts +11 -0
  26. package/dist/lib/decode-receipt-render.d.ts.map +1 -0
  27. package/dist/lib/decode-receipt-render.js +242 -0
  28. package/dist/lib/decode-receipt-render.js.map +1 -0
  29. package/package.json +83 -0
  30. package/src/cli.ts +5 -0
  31. package/src/command.ts +51 -0
  32. package/src/commands/admin-onboard-token.ts +51 -0
  33. package/src/commands/admin.ts +20 -0
  34. package/src/commands/complete-grant.ts +41 -0
  35. package/src/commands/create-consistency-proof.ts +44 -0
  36. package/src/commands/create-log.ts +95 -0
  37. package/src/commands/decode-receipt.ts +20 -0
  38. package/src/commands/delegate.ts +55 -0
  39. package/src/commands/deploy.ts +72 -0
  40. package/src/commands/fetch-accumulator.ts +49 -0
  41. package/src/commands/onboard-genesis.ts +70 -0
  42. package/src/commands/onboard-request.ts +59 -0
  43. package/src/commands/register-grant.ts +74 -0
  44. package/src/commands/register.ts +54 -0
  45. package/src/commands/resolve-receipt.ts +101 -0
  46. package/src/commands/sign-statement.ts +53 -0
  47. package/src/commands/verify-grant.ts +89 -0
  48. package/src/commands/verify.ts +92 -0
  49. package/src/commoncli.ts +34 -0
  50. package/src/decode-receipt.ts +56 -0
  51. package/src/index.ts +10 -0
  52. package/src/lib/bytes.ts +7 -0
  53. package/src/lib/checkpoint-provider.ts +297 -0
  54. package/src/lib/complete-grant-derive.ts +237 -0
  55. package/src/lib/consistency-proof.ts +271 -0
  56. package/src/lib/create-log-build.ts +168 -0
  57. package/src/lib/create-log-prepare.ts +75 -0
  58. package/src/lib/create-receipt-chain-verify.ts +267 -0
  59. package/src/lib/create-receipt-derive.ts +208 -0
  60. package/src/lib/create-receipt-inputs.ts +79 -0
  61. package/src/lib/decode-checkpoint-calldata.ts +188 -0
  62. package/src/lib/decode-receipt-cbor.ts +202 -0
  63. package/src/lib/decode-receipt-decode.ts +333 -0
  64. package/src/lib/decode-receipt-labels.ts +132 -0
  65. package/src/lib/decode-receipt-render.ts +284 -0
  66. package/src/lib/delegate-flow.ts +289 -0
  67. package/src/lib/delegate-voucher.ts +122 -0
  68. package/src/lib/deploy-artifact.ts +79 -0
  69. package/src/lib/deploy-bootstrap.ts +100 -0
  70. package/src/lib/deploy-flow.ts +184 -0
  71. package/src/lib/deploy-genesis-log-id.ts +29 -0
  72. package/src/lib/deploy-rpc.ts +40 -0
  73. package/src/lib/deployment-record.ts +65 -0
  74. package/src/lib/fsio.ts +91 -0
  75. package/src/lib/grant-flags-local.ts +19 -0
  76. package/src/lib/onboard-attestation.ts +120 -0
  77. package/src/lib/onboard-genesis-body.ts +94 -0
  78. package/src/lib/openssl-error-queue.ts +56 -0
  79. package/src/lib/register-flow.ts +292 -0
  80. package/src/lib/register-grant-build.ts +173 -0
  81. package/src/lib/register-grant-complete.ts +47 -0
  82. package/src/lib/register-grant-flow.ts +258 -0
  83. package/src/lib/resolve-receipt-freshen.ts +221 -0
  84. package/src/lib/sign-statement-build.ts +117 -0
  85. package/src/lib/sign-statement-key.ts +145 -0
  86. package/src/lib/verify-anchored.ts +326 -0
  87. package/src/lib/verify-checkpoint-chain.ts +160 -0
  88. package/src/lib/verify-eventscan.ts +214 -0
  89. package/src/lib/verify-inputs.ts +143 -0
  90. package/src/lib/verify-known-accumulator.ts +161 -0
  91. package/src/lib/verify-report.ts +265 -0
  92. package/src/lib/x402-payment.ts +115 -0
  93. package/src/main/admin-onboard-token.ts +216 -0
  94. package/src/main/complete-grant.ts +265 -0
  95. package/src/main/create-consistency-proof.ts +83 -0
  96. package/src/main/create-log.ts +347 -0
  97. package/src/main/create-receipt.ts +763 -0
  98. package/src/main/decode-receipt.ts +72 -0
  99. package/src/main/delegate.ts +141 -0
  100. package/src/main/deploy.ts +273 -0
  101. package/src/main/fetch-accumulator.ts +181 -0
  102. package/src/main/not-implemented.ts +43 -0
  103. package/src/main/onboard-genesis.ts +192 -0
  104. package/src/main/onboard-request.ts +333 -0
  105. package/src/main/register-grant.ts +270 -0
  106. package/src/main/register.ts +243 -0
  107. package/src/main/sign-statement.ts +168 -0
  108. package/src/main/verify.ts +767 -0
  109. package/src/options/admin-onboard-token.ts +48 -0
  110. package/src/options/common.ts +69 -0
  111. package/src/options/complete-grant.ts +40 -0
  112. package/src/options/create-consistency-proof.ts +63 -0
  113. package/src/options/create-log.ts +99 -0
  114. package/src/options/create-receipt.ts +243 -0
  115. package/src/options/decode-receipt.ts +25 -0
  116. package/src/options/delegate.ts +93 -0
  117. package/src/options/deploy.ts +105 -0
  118. package/src/options/fetch-accumulator.ts +62 -0
  119. package/src/options/onboard-genesis.ts +74 -0
  120. package/src/options/onboard-request.ts +57 -0
  121. package/src/options/register-grant.ts +72 -0
  122. package/src/options/register.ts +71 -0
  123. package/src/options/sign-statement.ts +64 -0
  124. package/src/options/verify.ts +250 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Reconstruct a signed checkpoint from a `publishCheckpoint` transaction's
3
+ * CALLDATA (FOR-418 Phase 1, plan-2607-32).
4
+ *
5
+ * The `CheckpointPublished` *event* is a lossy summary — it carries only the
6
+ * accumulator (see verify-eventscan). The transaction *input* to
7
+ * `publishCheckpoint(ConsistencyReceipt, InclusionProof, bytes8, PublishGrant)`
8
+ * carries the whole `ConsistencyReceipt`: the COSE `signature`, the chain of
9
+ * `ConsistencyProof`s (each the draft's `[tree-size-1, tree-size-2, paths,
10
+ * right-peaks]` — the interior climb nodes), and the sealer `DelegationProof`.
11
+ * So the publish transactions ARE the `.sth` chain, recoverable trustlessly
12
+ * from public chain data. This module is the reader; the fold/provider that
13
+ * consumes it lands in later phases.
14
+ *
15
+ * The ABI is written in viem's readable form, mirroring univocity
16
+ * `src/interfaces/types.sol`; its selector is asserted against the
17
+ * foundry-generated `0x87ce4c61` in the tests.
18
+ */
19
+ import { decodeFunctionData, parseAbi } from "viem";
20
+
21
+ /** `publishCheckpoint` ABI — struct shapes mirror univocity types.sol. */
22
+ export const PUBLISH_CHECKPOINT_ABI = parseAbi([
23
+ "struct ConsistencyProof { uint64 treeSize1; uint64 treeSize2; bytes32[][] paths; bytes32[] rightPeaks; }",
24
+ "struct DelegationProof { bytes protectedHeader; bytes delegationKey; uint64 mmrStart; uint64 mmrEnd; bytes signature; }",
25
+ "struct ConsistencyReceipt { bytes protectedHeader; bytes signature; ConsistencyProof[] consistencyProofs; DelegationProof delegationProof; }",
26
+ "struct InclusionProof { uint64 index; bytes32[] path; }",
27
+ "struct PublishGrant { bytes32 logId; uint256 grant; uint256 request; uint64 maxHeight; uint64 minGrowth; bytes32 ownerLogId; bytes grantData; }",
28
+ "function publishCheckpoint(ConsistencyReceipt consistencyParts, InclusionProof grantInclusionProof, bytes8 grantIDTimestampBe, PublishGrant publishGrant)",
29
+ ]);
30
+
31
+ /** One embedded consistency proof — the draft Receipt-of-Consistency shape. */
32
+ export type CalldataConsistencyProof = {
33
+ treeSize1: bigint;
34
+ treeSize2: bigint;
35
+ /** One inclusion path per tree-size-1 peak, proven at tree-size-2. */
36
+ paths: Uint8Array[][];
37
+ /** New peaks completing the tree-size-2 accumulator (draft `right-peaks`). */
38
+ rightPeaks: Uint8Array[];
39
+ };
40
+
41
+ /** Sealer-key authorization carried alongside the checkpoint (ADR-0006). */
42
+ export type CalldataDelegation = {
43
+ protectedHeader: Uint8Array;
44
+ /** Alg-specific key bytes; ES256 = 64-byte x‖y. */
45
+ delegationKey: Uint8Array;
46
+ mmrStart: bigint;
47
+ mmrEnd: bigint;
48
+ signature: Uint8Array;
49
+ };
50
+
51
+ /** The full `ConsistencyReceipt` recovered from `publishCheckpoint` calldata. */
52
+ export type CalldataCheckpoint = {
53
+ protectedHeader: Uint8Array;
54
+ /** COSE signature over the tree-size-2 accumulator (ADR-0046 detached). */
55
+ signature: Uint8Array;
56
+ /** Consistency-proof chain from the on-chain base to the published size. */
57
+ consistencyProofs: CalldataConsistencyProof[];
58
+ delegation: CalldataDelegation;
59
+ };
60
+
61
+ function hexToBytes(hex: string): Uint8Array {
62
+ const h = hex.startsWith("0x") ? hex.slice(2) : hex;
63
+ if (h.length % 2 !== 0) {
64
+ throw new Error(`odd-length hex from calldata decode: ${hex.slice(0, 12)}…`);
65
+ }
66
+ const out = new Uint8Array(h.length / 2);
67
+ for (let i = 0; i < out.length; i++) {
68
+ out[i] = Number.parseInt(h.slice(i * 2, i * 2 + 2), 16);
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * Decode a `publishCheckpoint` transaction input into its `ConsistencyReceipt`.
75
+ * Throws if the calldata is not a `publishCheckpoint` call (wrong selector /
76
+ * malformed — viem's `decodeFunctionData` rejects), if the proof chain is empty
77
+ * (the contract requires ≥1), or if any link does not strictly grow the tree
78
+ * (a hostile RPC returning bogus calldata for a tx hash — same posture as the
79
+ * FOR-414 size guard).
80
+ */
81
+ export function decodePublishCheckpointCalldata(
82
+ inputHex: string,
83
+ ): CalldataCheckpoint {
84
+ const data = (inputHex.startsWith("0x") ? inputHex : `0x${inputHex}`) as `0x${string}`;
85
+ const { args } = decodeFunctionData({ abi: PUBLISH_CHECKPOINT_ABI, data });
86
+ const receipt = args[0] as {
87
+ protectedHeader: string;
88
+ signature: string;
89
+ consistencyProofs: readonly {
90
+ treeSize1: bigint;
91
+ treeSize2: bigint;
92
+ paths: readonly (readonly string[])[];
93
+ rightPeaks: readonly string[];
94
+ }[];
95
+ delegationProof: {
96
+ protectedHeader: string;
97
+ delegationKey: string;
98
+ mmrStart: bigint;
99
+ mmrEnd: bigint;
100
+ signature: string;
101
+ };
102
+ };
103
+
104
+ if (receipt.consistencyProofs.length === 0) {
105
+ throw new Error("publishCheckpoint calldata carries no consistency proofs");
106
+ }
107
+ const consistencyProofs = receipt.consistencyProofs.map((p, i) => {
108
+ if (p.treeSize2 <= p.treeSize1) {
109
+ throw new Error(
110
+ `consistency proof ${i} does not grow the tree: ${p.treeSize1} -> ${p.treeSize2}`,
111
+ );
112
+ }
113
+ return {
114
+ treeSize1: p.treeSize1,
115
+ treeSize2: p.treeSize2,
116
+ paths: p.paths.map((path) => path.map(hexToBytes)),
117
+ rightPeaks: p.rightPeaks.map(hexToBytes),
118
+ };
119
+ });
120
+
121
+ return {
122
+ protectedHeader: hexToBytes(receipt.protectedHeader),
123
+ signature: hexToBytes(receipt.signature),
124
+ consistencyProofs,
125
+ delegation: {
126
+ protectedHeader: hexToBytes(receipt.delegationProof.protectedHeader),
127
+ delegationKey: hexToBytes(receipt.delegationProof.delegationKey),
128
+ mmrStart: receipt.delegationProof.mmrStart,
129
+ mmrEnd: receipt.delegationProof.mmrEnd,
130
+ signature: hexToBytes(receipt.delegationProof.signature),
131
+ },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Fetch a transaction's input (calldata) by hash — the tile-free chain source
137
+ * for {@link decodePublishCheckpointCalldata}. Minimal JSON-RPC, matching the
138
+ * raw-fetch pattern used by the event scan.
139
+ */
140
+ export async function fetchTransactionInput(opts: {
141
+ rpcUrl: string;
142
+ txHash: string;
143
+ fetchImpl?: typeof fetch;
144
+ }): Promise<string> {
145
+ const doFetch = opts.fetchImpl ?? fetch;
146
+ const res = await doFetch(opts.rpcUrl, {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/json" },
149
+ body: JSON.stringify({
150
+ jsonrpc: "2.0",
151
+ id: 1,
152
+ method: "eth_getTransactionByHash",
153
+ params: [opts.txHash],
154
+ }),
155
+ });
156
+ if (!res.ok) {
157
+ throw new Error(`eth_getTransactionByHash: HTTP ${res.status}`);
158
+ }
159
+ const body = (await res.json()) as {
160
+ result?: { input?: string } | null;
161
+ error?: { message?: string };
162
+ };
163
+ if (body.error) {
164
+ throw new Error(`eth_getTransactionByHash: ${body.error.message ?? "rpc error"}`);
165
+ }
166
+ const input = body.result?.input;
167
+ if (typeof input !== "string" || input.length < 10) {
168
+ throw new Error(`transaction ${opts.txHash} has no input calldata`);
169
+ }
170
+ // A real publishCheckpoint calldata is a few KB; cap the accepted size so a
171
+ // hostile RPC cannot return a multi-hundred-MB `input` string (R6). 8M hex
172
+ // chars ≈ 4 MB — generous for any legitimate proof chain.
173
+ if (input.length > 8_000_000) {
174
+ throw new Error(
175
+ `transaction ${opts.txHash} input is implausibly large (${input.length} hex chars)`,
176
+ );
177
+ }
178
+ return input;
179
+ }
180
+
181
+ /** Fetch and decode in one step. */
182
+ export async function fetchPublishCheckpointCalldata(opts: {
183
+ rpcUrl: string;
184
+ txHash: string;
185
+ fetchImpl?: typeof fetch;
186
+ }): Promise<CalldataCheckpoint> {
187
+ return decodePublishCheckpointCalldata(await fetchTransactionInput(opts));
188
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Minimal CBOR reader for `forestrie decode-receipt` display (FOR-346).
3
+ *
4
+ * Only used where the published helpers stop: `parseReceipt`
5
+ * (`@forestrie/receipt-verify`) returns the COSE protected header as an
6
+ * opaque bstr, and this command's whole job is to show what is inside
7
+ * it (alg, kid, vds, CWT claims). Display-only — no re-encoding, no
8
+ * crypto. Definite lengths only: the forestrie encoders (go-cose,
9
+ * cbor-x, `@forestrie/encoding` canonical emit) never produce
10
+ * indefinite-length items.
11
+ */
12
+
13
+ export class CborDecodeError extends Error {}
14
+
15
+ /** A decoded CBOR tag wrapper. */
16
+ export type CborTagged = { tag: number; value: CborValue };
17
+
18
+ export type CborValue =
19
+ | number
20
+ | bigint
21
+ | string
22
+ | boolean
23
+ | null
24
+ | undefined
25
+ | Uint8Array
26
+ | CborValue[]
27
+ | Map<CborValue, CborValue>
28
+ | CborTagged;
29
+
30
+ /** True when `value` is a `CborTagged` wrapper (not a Map/array/bytes). */
31
+ export function isCborTagged(value: unknown): value is CborTagged {
32
+ return (
33
+ typeof value === "object" &&
34
+ value !== null &&
35
+ !(value instanceof Map) &&
36
+ !(value instanceof Uint8Array) &&
37
+ !Array.isArray(value) &&
38
+ typeof (value as CborTagged).tag === "number" &&
39
+ "value" in value
40
+ );
41
+ }
42
+
43
+ type Cursor = { bytes: Uint8Array; view: DataView; offset: number };
44
+
45
+ function need(c: Cursor, n: number): void {
46
+ if (c.offset + n > c.bytes.length) {
47
+ throw new CborDecodeError("unexpected end of CBOR data");
48
+ }
49
+ }
50
+
51
+ function readArgument(c: Cursor, info: number): number | bigint {
52
+ if (info < 24) return info;
53
+ if (info === 24) {
54
+ need(c, 1);
55
+ return c.bytes[c.offset++]!;
56
+ }
57
+ if (info === 25) {
58
+ need(c, 2);
59
+ const v = c.view.getUint16(c.offset);
60
+ c.offset += 2;
61
+ return v;
62
+ }
63
+ if (info === 26) {
64
+ need(c, 4);
65
+ const v = c.view.getUint32(c.offset);
66
+ c.offset += 4;
67
+ return v;
68
+ }
69
+ if (info === 27) {
70
+ need(c, 8);
71
+ const v = c.view.getBigUint64(c.offset);
72
+ c.offset += 8;
73
+ return v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v;
74
+ }
75
+ if (info === 31) {
76
+ throw new CborDecodeError("indefinite-length CBOR items are not supported");
77
+ }
78
+ throw new CborDecodeError(`reserved CBOR additional info ${info}`);
79
+ }
80
+
81
+ function toLength(arg: number | bigint, what: string): number {
82
+ if (typeof arg === "bigint" || !Number.isSafeInteger(arg)) {
83
+ throw new CborDecodeError(`${what} length too large`);
84
+ }
85
+ return arg;
86
+ }
87
+
88
+ function readFloat(c: Cursor, info: number): number {
89
+ if (info === 25) {
90
+ // IEEE 754 half precision (RFC 8949 appendix D).
91
+ need(c, 2);
92
+ const half = c.view.getUint16(c.offset);
93
+ c.offset += 2;
94
+ const sign = half & 0x8000 ? -1 : 1;
95
+ const exp = (half >> 10) & 0x1f;
96
+ const frac = half & 0x03ff;
97
+ if (exp === 0) return sign * frac * 2 ** -24;
98
+ if (exp === 31) return frac ? Number.NaN : sign * Number.POSITIVE_INFINITY;
99
+ return sign * (1024 + frac) * 2 ** (exp - 25);
100
+ }
101
+ if (info === 26) {
102
+ need(c, 4);
103
+ const v = c.view.getFloat32(c.offset);
104
+ c.offset += 4;
105
+ return v;
106
+ }
107
+ need(c, 8);
108
+ const v = c.view.getFloat64(c.offset);
109
+ c.offset += 8;
110
+ return v;
111
+ }
112
+
113
+ function readItem(c: Cursor, depth: number): CborValue {
114
+ if (depth > 64) throw new CborDecodeError("CBOR nesting too deep");
115
+ need(c, 1);
116
+ const initial = c.bytes[c.offset++]!;
117
+ const major = initial >> 5;
118
+ const info = initial & 0x1f;
119
+
120
+ switch (major) {
121
+ case 0:
122
+ return readArgument(c, info);
123
+ case 1: {
124
+ const arg = readArgument(c, info);
125
+ return typeof arg === "bigint" ? -1n - arg : -1 - arg;
126
+ }
127
+ case 2: {
128
+ const len = toLength(readArgument(c, info), "byte string");
129
+ need(c, len);
130
+ const out = c.bytes.slice(c.offset, c.offset + len);
131
+ c.offset += len;
132
+ return out;
133
+ }
134
+ case 3: {
135
+ const len = toLength(readArgument(c, info), "text string");
136
+ need(c, len);
137
+ const out = new TextDecoder("utf-8", { fatal: true }).decode(
138
+ c.bytes.subarray(c.offset, c.offset + len),
139
+ );
140
+ c.offset += len;
141
+ return out;
142
+ }
143
+ case 4: {
144
+ const len = toLength(readArgument(c, info), "array");
145
+ const out: CborValue[] = [];
146
+ for (let i = 0; i < len; i += 1) out.push(readItem(c, depth + 1));
147
+ return out;
148
+ }
149
+ case 5: {
150
+ const len = toLength(readArgument(c, info), "map");
151
+ const out = new Map<CborValue, CborValue>();
152
+ for (let i = 0; i < len; i += 1) {
153
+ const key = readItem(c, depth + 1);
154
+ out.set(key, readItem(c, depth + 1));
155
+ }
156
+ return out;
157
+ }
158
+ case 6: {
159
+ const tag = toLength(readArgument(c, info), "tag");
160
+ return { tag, value: readItem(c, depth + 1) };
161
+ }
162
+ default: {
163
+ // major 7: simple values and floats
164
+ if (info === 20) return false;
165
+ if (info === 21) return true;
166
+ if (info === 22) return null;
167
+ if (info === 23) return undefined;
168
+ if (info === 24) {
169
+ need(c, 1);
170
+ return c.bytes[c.offset++]!;
171
+ }
172
+ if (info >= 25 && info <= 27) return readFloat(c, info);
173
+ throw new CborDecodeError(`unsupported CBOR simple value (info ${info})`);
174
+ }
175
+ }
176
+ }
177
+
178
+ /** Decode a single CBOR item; trailing bytes are an error. */
179
+ export function decodeCborValue(bytes: Uint8Array): CborValue {
180
+ const cursor: Cursor = {
181
+ bytes,
182
+ view: new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength),
183
+ offset: 0,
184
+ };
185
+ const value = readItem(cursor, 0);
186
+ if (cursor.offset !== bytes.length) {
187
+ throw new CborDecodeError(
188
+ `trailing bytes after CBOR item (${bytes.length - cursor.offset} left)`,
189
+ );
190
+ }
191
+ return value;
192
+ }
193
+
194
+ /** Decode CBOR bytes that must contain a map (e.g. a COSE protected header). */
195
+ export function decodeCborMap(bytes: Uint8Array): Map<CborValue, CborValue> {
196
+ if (bytes.length === 0) return new Map();
197
+ const value = decodeCborValue(bytes);
198
+ if (!(value instanceof Map)) {
199
+ throw new CborDecodeError("expected a CBOR map");
200
+ }
201
+ return value;
202
+ }