@optimystic/quereus-plugin-crypto 0.21.0 → 0.24.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/src/cid.ts CHANGED
@@ -1,200 +1,200 @@
1
- /**
2
- * Self-describing content identifiers (CIDv1) for Quereus.
3
- *
4
- * Where {@link ./crypto.ts | digest} emits a *bare* hash (raw digest bytes in
5
- * some text encoding), this module emits an interoperable, self-describing
6
- * CIDv1:
7
- *
8
- * ```
9
- * CIDv1 = multibase( version ‖ multicodec(content-type) ‖ multihash )
10
- * multihash = hashFnCode ‖ digestLength ‖ digestBytes
11
- * ```
12
- *
13
- * The value carries its own multibase, multicodec (content type), and multihash
14
- * (hash algorithm + length), so a consumer can decode it without out-of-band
15
- * knowledge, and an algorithm migration (e.g. sha2-256 → another hash) is
16
- * unambiguous because the hash code is recorded *in the value*.
17
- *
18
- * All framing/parsing comes from the audited `multiformats` library — there is
19
- * no bespoke byte-pushing here. The actual hashing reuses the same synchronous,
20
- * cross-platform `@noble/hashes` functions the rest of the plugin uses (via
21
- * {@link resolveHasher}), so the output is byte-identical to the CID an external
22
- * content-addressed store (IPFS/IPLD) computes for the same bytes:
23
- * `cid(utf8('hello world'))` === `bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e`.
24
- */
25
-
26
- import { CID } from 'multiformats/cid';
27
- import * as Digest from 'multiformats/hashes/digest';
28
- import { base32 } from 'multiformats/bases/base32';
29
- import { base58btc } from 'multiformats/bases/base58';
30
- import { base64url } from 'multiformats/bases/base64';
31
- import { base16 } from 'multiformats/bases/base16';
32
- import type { MultibaseEncoder, MultibaseDecoder } from 'multiformats/bases/interface';
33
- import { resolveHasher, type HashAlgorithm } from './crypto.js';
34
-
35
- /** Content-type multicodec selectable for the CID. Extensible. */
36
- export type Multicodec = 'raw' | 'dag-cbor';
37
- /** Hash-algorithm multihash code selectable for the CID. */
38
- export type MultihashCode = 'sha2-256' | 'sha2-512' | 'blake3';
39
- /** Multibase the CID string is rendered in. */
40
- export type Multibase = 'base32' | 'base58btc' | 'base64url' | 'base16';
41
-
42
- /** Parsed parts of a CIDv1 (or CIDv0), as returned by {@link cidDecode}. */
43
- export interface CidParts {
44
- /** CID version (1 for the values this module produces; 0 for legacy CIDv0). */
45
- readonly version: number;
46
- /** Content-type codec name when recognized, else the raw multicodec number. */
47
- readonly codec: Multicodec | number;
48
- /** Hash-algorithm code name when recognized, else the raw multihash number. */
49
- readonly hashCode: MultihashCode | number;
50
- /** Raw digest bytes (without the multihash code/length prefix). */
51
- readonly digest: Uint8Array;
52
- }
53
-
54
- // --- Multiformats code tables (see multiformats/multicodec table.csv) --- //
55
-
56
- /** Content-type name → multicodec code. */
57
- const MULTICODEC_CODES: Record<Multicodec, number> = {
58
- 'raw': 0x55,
59
- 'dag-cbor': 0x71,
60
- };
61
-
62
- /** Hash name → multihash code. */
63
- const MULTIHASH_CODES: Record<MultihashCode, number> = {
64
- 'sha2-256': 0x12,
65
- 'sha2-512': 0x13,
66
- 'blake3': 0x1e,
67
- };
68
-
69
- /** Multihash code → the synchronous `@noble/hashes` algorithm that produces it. */
70
- const MULTIHASH_TO_ALGORITHM: Record<MultihashCode, HashAlgorithm> = {
71
- 'sha2-256': 'sha256',
72
- 'sha2-512': 'sha512',
73
- 'blake3': 'blake3',
74
- };
75
-
76
- /**
77
- * Multihash code → exact digest length in bytes. A CID is replicable only if its
78
- * digest length is fixed, so blake3 (which is variable-length in general) is
79
- * pinned to 32 bytes here, matching the plugin's blake3 output and sha2-256.
80
- */
81
- const MULTIHASH_DIGEST_LENGTHS: Record<MultihashCode, number> = {
82
- 'sha2-256': 32,
83
- 'sha2-512': 64,
84
- 'blake3': 32,
85
- };
86
-
87
- /** Reverse lookups for {@link cidDecode}: code number → friendly name. */
88
- const MULTICODEC_NAMES: ReadonlyMap<number, Multicodec> = new Map(
89
- (Object.entries(MULTICODEC_CODES) as [Multicodec, number][]).map(([name, code]) => [code, name])
90
- );
91
- const MULTIHASH_NAMES: ReadonlyMap<number, MultihashCode> = new Map(
92
- (Object.entries(MULTIHASH_CODES) as [MultihashCode, number][]).map(([name, code]) => [code, name])
93
- );
94
-
95
- /** Multibase name → its multiformats encoder. */
96
- const MULTIBASE_ENCODERS: Record<Multibase, MultibaseEncoder<string>> = {
97
- 'base32': base32,
98
- 'base58btc': base58btc,
99
- 'base64url': base64url,
100
- 'base16': base16,
101
- };
102
-
103
- /**
104
- * Combined decoder that dispatches on the multibase prefix character, so
105
- * {@link cidDecode} accepts a CID in any of the supported bases without the
106
- * caller having to declare which.
107
- */
108
- const MULTIBASE_DECODER: MultibaseDecoder<string> = base32.decoder
109
- .or(base58btc.decoder)
110
- .or(base64url.decoder)
111
- .or(base16.decoder);
112
-
113
- function resolveCodecCode(codec: Multicodec): number {
114
- const code = MULTICODEC_CODES[codec];
115
- if (code === undefined) {
116
- throw new Error(`cid: unsupported multicodec '${codec}' (expected one of ${Object.keys(MULTICODEC_CODES).join(', ')})`);
117
- }
118
- return code;
119
- }
120
-
121
- function resolveBaseEncoder(base: Multibase): MultibaseEncoder<string> {
122
- const encoder = MULTIBASE_ENCODERS[base];
123
- if (!encoder) {
124
- throw new Error(`cid: unsupported multibase '${base}' (expected one of ${Object.keys(MULTIBASE_ENCODERS).join(', ')})`);
125
- }
126
- return encoder;
127
- }
128
-
129
- /**
130
- * Frame an **already-computed** digest as a CIDv1 string. The caller asserts
131
- * which `hash` produced the digest; the digest length is validated against that
132
- * hash so a mismatched assertion is rejected rather than silently mis-framed.
133
- *
134
- * Use this to turn an existing field-tuple digest into a CID without re-hashing,
135
- * e.g. `cidV1(digest(fields, 'sha256', 'bytes'), 'sha2-256')`.
136
- *
137
- * @param digest - Raw digest bytes (no multihash prefix).
138
- * @param hash - The multihash code asserting which algorithm produced `digest`.
139
- * @param codec - Content-type multicodec (default `'raw'`).
140
- * @param base - Multibase to render in (default `'base32'`, the IPFS canonical).
141
- */
142
- export function cidV1(
143
- digest: Uint8Array,
144
- hash: MultihashCode,
145
- codec: Multicodec = 'raw',
146
- base: Multibase = 'base32'
147
- ): string {
148
- const hashCode = MULTIHASH_CODES[hash];
149
- if (hashCode === undefined) {
150
- throw new Error(`cid: unsupported multihash code '${hash}' (expected one of ${Object.keys(MULTIHASH_CODES).join(', ')})`);
151
- }
152
- const expectedLength = MULTIHASH_DIGEST_LENGTHS[hash];
153
- if (digest.length !== expectedLength) {
154
- throw new Error(`cid: digest length ${digest.length} does not match asserted hash '${hash}' (expected ${expectedLength} bytes)`);
155
- }
156
- const codecCode = resolveCodecCode(codec);
157
- const encoder = resolveBaseEncoder(base);
158
- const multihash = Digest.create(hashCode, digest);
159
- return CID.createV1(codecCode, multihash).toString(encoder);
160
- }
161
-
162
- /**
163
- * Hash `data`, wrap the digest as a multihash, frame it as a CIDv1, and encode
164
- * in `base`. The result is the same interoperable address an IPFS/IPLD store
165
- * computes for the same bytes (for the matching codec/hash).
166
- *
167
- * @param data - The content bytes to address.
168
- * @param codec - Content-type multicodec (default `'raw'`).
169
- * @param hash - Hash algorithm (default `'sha2-256'`).
170
- * @param base - Multibase to render in (default `'base32'`, the IPFS canonical).
171
- */
172
- export function cid(
173
- data: Uint8Array,
174
- codec: Multicodec = 'raw',
175
- hash: MultihashCode = 'sha2-256',
176
- base: Multibase = 'base32'
177
- ): string {
178
- const algorithm = MULTIHASH_TO_ALGORITHM[hash];
179
- if (!algorithm) {
180
- throw new Error(`cid: unsupported multihash code '${hash}' (expected one of ${Object.keys(MULTIHASH_CODES).join(', ')})`);
181
- }
182
- const digest = resolveHasher(algorithm)(data);
183
- return cidV1(digest, hash, codec, base);
184
- }
185
-
186
- /**
187
- * Parse a CID string back into its parts, for schema validation and migration.
188
- * Recognized codec/hash codes are returned as friendly names; unrecognized ones
189
- * as their raw numbers. Throws cleanly on malformed input (delegated to
190
- * `multiformats`), never silently mis-framing.
191
- */
192
- export function cidDecode(value: string): CidParts {
193
- const parsed = CID.parse(value, MULTIBASE_DECODER);
194
- return {
195
- version: parsed.version,
196
- codec: MULTICODEC_NAMES.get(parsed.code) ?? parsed.code,
197
- hashCode: MULTIHASH_NAMES.get(parsed.multihash.code) ?? parsed.multihash.code,
198
- digest: parsed.multihash.digest,
199
- };
200
- }
1
+ /**
2
+ * Self-describing content identifiers (CIDv1) for Quereus.
3
+ *
4
+ * Where {@link ./crypto.ts | digest} emits a *bare* hash (raw digest bytes in
5
+ * some text encoding), this module emits an interoperable, self-describing
6
+ * CIDv1:
7
+ *
8
+ * ```
9
+ * CIDv1 = multibase( version ‖ multicodec(content-type) ‖ multihash )
10
+ * multihash = hashFnCode ‖ digestLength ‖ digestBytes
11
+ * ```
12
+ *
13
+ * The value carries its own multibase, multicodec (content type), and multihash
14
+ * (hash algorithm + length), so a consumer can decode it without out-of-band
15
+ * knowledge, and an algorithm migration (e.g. sha2-256 → another hash) is
16
+ * unambiguous because the hash code is recorded *in the value*.
17
+ *
18
+ * All framing/parsing comes from the audited `multiformats` library — there is
19
+ * no bespoke byte-pushing here. The actual hashing reuses the same synchronous,
20
+ * cross-platform `@noble/hashes` functions the rest of the plugin uses (via
21
+ * {@link resolveHasher}), so the output is byte-identical to the CID an external
22
+ * content-addressed store (IPFS/IPLD) computes for the same bytes:
23
+ * `cid(utf8('hello world'))` === `bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e`.
24
+ */
25
+
26
+ import { CID } from 'multiformats/cid';
27
+ import * as Digest from 'multiformats/hashes/digest';
28
+ import { base32 } from 'multiformats/bases/base32';
29
+ import { base58btc } from 'multiformats/bases/base58';
30
+ import { base64url } from 'multiformats/bases/base64';
31
+ import { base16 } from 'multiformats/bases/base16';
32
+ import type { MultibaseEncoder, MultibaseDecoder } from 'multiformats/bases/interface';
33
+ import { resolveHasher, type HashAlgorithm } from './crypto.js';
34
+
35
+ /** Content-type multicodec selectable for the CID. Extensible. */
36
+ export type Multicodec = 'raw' | 'dag-cbor';
37
+ /** Hash-algorithm multihash code selectable for the CID. */
38
+ export type MultihashCode = 'sha2-256' | 'sha2-512' | 'blake3';
39
+ /** Multibase the CID string is rendered in. */
40
+ export type Multibase = 'base32' | 'base58btc' | 'base64url' | 'base16';
41
+
42
+ /** Parsed parts of a CIDv1 (or CIDv0), as returned by {@link cidDecode}. */
43
+ export interface CidParts {
44
+ /** CID version (1 for the values this module produces; 0 for legacy CIDv0). */
45
+ readonly version: number;
46
+ /** Content-type codec name when recognized, else the raw multicodec number. */
47
+ readonly codec: Multicodec | number;
48
+ /** Hash-algorithm code name when recognized, else the raw multihash number. */
49
+ readonly hashCode: MultihashCode | number;
50
+ /** Raw digest bytes (without the multihash code/length prefix). */
51
+ readonly digest: Uint8Array;
52
+ }
53
+
54
+ // --- Multiformats code tables (see multiformats/multicodec table.csv) --- //
55
+
56
+ /** Content-type name → multicodec code. */
57
+ const MULTICODEC_CODES: Record<Multicodec, number> = {
58
+ 'raw': 0x55,
59
+ 'dag-cbor': 0x71,
60
+ };
61
+
62
+ /** Hash name → multihash code. */
63
+ const MULTIHASH_CODES: Record<MultihashCode, number> = {
64
+ 'sha2-256': 0x12,
65
+ 'sha2-512': 0x13,
66
+ 'blake3': 0x1e,
67
+ };
68
+
69
+ /** Multihash code → the synchronous `@noble/hashes` algorithm that produces it. */
70
+ const MULTIHASH_TO_ALGORITHM: Record<MultihashCode, HashAlgorithm> = {
71
+ 'sha2-256': 'sha256',
72
+ 'sha2-512': 'sha512',
73
+ 'blake3': 'blake3',
74
+ };
75
+
76
+ /**
77
+ * Multihash code → exact digest length in bytes. A CID is replicable only if its
78
+ * digest length is fixed, so blake3 (which is variable-length in general) is
79
+ * pinned to 32 bytes here, matching the plugin's blake3 output and sha2-256.
80
+ */
81
+ const MULTIHASH_DIGEST_LENGTHS: Record<MultihashCode, number> = {
82
+ 'sha2-256': 32,
83
+ 'sha2-512': 64,
84
+ 'blake3': 32,
85
+ };
86
+
87
+ /** Reverse lookups for {@link cidDecode}: code number → friendly name. */
88
+ const MULTICODEC_NAMES: ReadonlyMap<number, Multicodec> = new Map(
89
+ (Object.entries(MULTICODEC_CODES) as [Multicodec, number][]).map(([name, code]) => [code, name])
90
+ );
91
+ const MULTIHASH_NAMES: ReadonlyMap<number, MultihashCode> = new Map(
92
+ (Object.entries(MULTIHASH_CODES) as [MultihashCode, number][]).map(([name, code]) => [code, name])
93
+ );
94
+
95
+ /** Multibase name → its multiformats encoder. */
96
+ const MULTIBASE_ENCODERS: Record<Multibase, MultibaseEncoder<string>> = {
97
+ 'base32': base32,
98
+ 'base58btc': base58btc,
99
+ 'base64url': base64url,
100
+ 'base16': base16,
101
+ };
102
+
103
+ /**
104
+ * Combined decoder that dispatches on the multibase prefix character, so
105
+ * {@link cidDecode} accepts a CID in any of the supported bases without the
106
+ * caller having to declare which.
107
+ */
108
+ const MULTIBASE_DECODER: MultibaseDecoder<string> = base32.decoder
109
+ .or(base58btc.decoder)
110
+ .or(base64url.decoder)
111
+ .or(base16.decoder);
112
+
113
+ function resolveCodecCode(codec: Multicodec): number {
114
+ const code = MULTICODEC_CODES[codec];
115
+ if (code === undefined) {
116
+ throw new Error(`cid: unsupported multicodec '${codec}' (expected one of ${Object.keys(MULTICODEC_CODES).join(', ')})`);
117
+ }
118
+ return code;
119
+ }
120
+
121
+ function resolveBaseEncoder(base: Multibase): MultibaseEncoder<string> {
122
+ const encoder = MULTIBASE_ENCODERS[base];
123
+ if (!encoder) {
124
+ throw new Error(`cid: unsupported multibase '${base}' (expected one of ${Object.keys(MULTIBASE_ENCODERS).join(', ')})`);
125
+ }
126
+ return encoder;
127
+ }
128
+
129
+ /**
130
+ * Frame an **already-computed** digest as a CIDv1 string. The caller asserts
131
+ * which `hash` produced the digest; the digest length is validated against that
132
+ * hash so a mismatched assertion is rejected rather than silently mis-framed.
133
+ *
134
+ * Use this to turn an existing field-tuple digest into a CID without re-hashing,
135
+ * e.g. `cidV1(digest(fields, 'sha256', 'bytes'), 'sha2-256')`.
136
+ *
137
+ * @param digest - Raw digest bytes (no multihash prefix).
138
+ * @param hash - The multihash code asserting which algorithm produced `digest`.
139
+ * @param codec - Content-type multicodec (default `'raw'`).
140
+ * @param base - Multibase to render in (default `'base32'`, the IPFS canonical).
141
+ */
142
+ export function cidV1(
143
+ digest: Uint8Array,
144
+ hash: MultihashCode,
145
+ codec: Multicodec = 'raw',
146
+ base: Multibase = 'base32'
147
+ ): string {
148
+ const hashCode = MULTIHASH_CODES[hash];
149
+ if (hashCode === undefined) {
150
+ throw new Error(`cid: unsupported multihash code '${hash}' (expected one of ${Object.keys(MULTIHASH_CODES).join(', ')})`);
151
+ }
152
+ const expectedLength = MULTIHASH_DIGEST_LENGTHS[hash];
153
+ if (digest.length !== expectedLength) {
154
+ throw new Error(`cid: digest length ${digest.length} does not match asserted hash '${hash}' (expected ${expectedLength} bytes)`);
155
+ }
156
+ const codecCode = resolveCodecCode(codec);
157
+ const encoder = resolveBaseEncoder(base);
158
+ const multihash = Digest.create(hashCode, digest);
159
+ return CID.createV1(codecCode, multihash).toString(encoder);
160
+ }
161
+
162
+ /**
163
+ * Hash `data`, wrap the digest as a multihash, frame it as a CIDv1, and encode
164
+ * in `base`. The result is the same interoperable address an IPFS/IPLD store
165
+ * computes for the same bytes (for the matching codec/hash).
166
+ *
167
+ * @param data - The content bytes to address.
168
+ * @param codec - Content-type multicodec (default `'raw'`).
169
+ * @param hash - Hash algorithm (default `'sha2-256'`).
170
+ * @param base - Multibase to render in (default `'base32'`, the IPFS canonical).
171
+ */
172
+ export function cid(
173
+ data: Uint8Array,
174
+ codec: Multicodec = 'raw',
175
+ hash: MultihashCode = 'sha2-256',
176
+ base: Multibase = 'base32'
177
+ ): string {
178
+ const algorithm = MULTIHASH_TO_ALGORITHM[hash];
179
+ if (!algorithm) {
180
+ throw new Error(`cid: unsupported multihash code '${hash}' (expected one of ${Object.keys(MULTIHASH_CODES).join(', ')})`);
181
+ }
182
+ const digest = resolveHasher(algorithm)(data);
183
+ return cidV1(digest, hash, codec, base);
184
+ }
185
+
186
+ /**
187
+ * Parse a CID string back into its parts, for schema validation and migration.
188
+ * Recognized codec/hash codes are returned as friendly names; unrecognized ones
189
+ * as their raw numbers. Throws cleanly on malformed input (delegated to
190
+ * `multiformats`), never silently mis-framing.
191
+ */
192
+ export function cidDecode(value: string): CidParts {
193
+ const parsed = CID.parse(value, MULTIBASE_DECODER);
194
+ return {
195
+ version: parsed.version,
196
+ codec: MULTICODEC_NAMES.get(parsed.code) ?? parsed.code,
197
+ hashCode: MULTIHASH_NAMES.get(parsed.multihash.code) ?? parsed.multihash.code,
198
+ digest: parsed.multihash.digest,
199
+ };
200
+ }