@chillwhales/lsp31 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chillwhales contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @chillwhales/lsp31
2
+
3
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
4
+
5
+ LSP31 Multi-Storage URI — encode, decode, and resolve multi-backend content references for redundant storage on LUKSO Universal Profiles.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @chillwhales/lsp31
11
+ ```
12
+
13
+ > **Peer dependency:** This package requires [`viem`](https://viem.sh) ^2.0.0
14
+ >
15
+ > ```bash
16
+ > pnpm add viem
17
+ > ```
18
+
19
+ ## Usage
20
+
21
+ ```typescript
22
+ import {
23
+ encodeLsp31Uri,
24
+ parseLsp31Uri,
25
+ computeContentHash,
26
+ } from "@chillwhales/lsp31";
27
+
28
+ // Define storage entries (minimum 2 backends for redundancy)
29
+ const entries = [
30
+ { backend: "ipfs" as const, cid: "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" },
31
+ { backend: "arweave" as const, id: "abcdef1234567890abcdef1234567890abcdef123456" },
32
+ ];
33
+
34
+ // Encode with a verification hash of the content bytes
35
+ const contentBytes = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
36
+ const hash = computeContentHash(contentBytes);
37
+ const encoded = encodeLsp31Uri(entries, hash);
38
+ // encoded is a hex string ready for setData()
39
+
40
+ // Later, parse the on-chain value back into structured entries
41
+ const { verificationData, entries: decoded } = parseLsp31Uri(encoded);
42
+ console.log(decoded[0].backend); // "ipfs"
43
+ ```
44
+
45
+ > **Spec:** See `LSP-31-MultiStorageURI.md` in the repository for the full specification.
46
+
47
+ ## API
48
+
49
+ Types are exported and available in your editor via TypeScript IntelliSense.
50
+
51
+ ## License
52
+
53
+ [MIT](./LICENSE)
@@ -0,0 +1,553 @@
1
+ import { Hex } from 'viem';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * LSP31 Multi-Storage URI Constants
6
+ *
7
+ * These constants mirror LSP2 verification constants locally to avoid
8
+ * external dependencies (this package is standalone).
9
+ *
10
+ * @see LSP-31-MultiStorageURI.md for full specification
11
+ */
12
+ /**
13
+ * Reserved prefix for LSP31 Multi-Storage URI (2 bytes)
14
+ * Distinguishes LSP31 from LSP2 VerifiableURI (0x0000)
15
+ */
16
+ declare const LSP31_RESERVED_PREFIX: "0x0031";
17
+ /**
18
+ * Verification method ID for keccak256(bytes)
19
+ * Computed as: bytes4(keccak256('keccak256(bytes)')) = 0x8019f9b1
20
+ * Same as LSP2 — verification model is identical
21
+ */
22
+ declare const KECCAK256_BYTES_METHOD_ID: "0x8019f9b1";
23
+ /**
24
+ * Length of a keccak256 hash in bytes (32 bytes = 0x0020)
25
+ */
26
+ declare const HASH_LENGTH_PREFIX: "0x0020";
27
+ /**
28
+ * Minimum length of a valid LSP31 URI value in hex characters
29
+ * 2 (prefix) + 4 (method ID) + 2 (hash length) + 32 (hash) = 40 bytes = 80 hex chars + '0x' prefix
30
+ * Note: A valid LSP31 URI will always be longer (entries JSON adds more), but 82 is the structural minimum
31
+ */
32
+ declare const MIN_LSP31_URI_LENGTH = 82;
33
+ /**
34
+ * Closed set of supported storage backends
35
+ * Adding a backend requires a schema update — this is intentional for validation safety
36
+ */
37
+ declare const LSP31_BACKENDS: readonly ["ipfs", "s3", "lumera", "arweave"];
38
+ /**
39
+ * Minimum number of entries required in an LSP31 multi-storage URI
40
+ * Single-backend content should use LSP2 VerifiableURI instead
41
+ */
42
+ declare const LSP31_MIN_ENTRIES = 2;
43
+
44
+ /**
45
+ * LSP31 Multi-Storage URI Zod Schemas
46
+ *
47
+ * Validates multi-storage entries with a discriminated union on the `backend` field.
48
+ * Four backend types: ipfs, s3, lumera, arweave — each with backend-specific identifier fields.
49
+ *
50
+ * @see LSP-31-MultiStorageURI.md for full specification
51
+ */
52
+
53
+ /**
54
+ * IPFS storage entry
55
+ * Identified by a Content Identifier (CID)
56
+ */
57
+ declare const lsp31IpfsEntrySchema: z.ZodObject<{
58
+ backend: z.ZodLiteral<"ipfs">;
59
+ /** IPFS content identifier (CIDv0 or CIDv1) */
60
+ cid: z.ZodString;
61
+ }, "strip", z.ZodTypeAny, {
62
+ backend: "ipfs";
63
+ cid: string;
64
+ }, {
65
+ backend: "ipfs";
66
+ cid: string;
67
+ }>;
68
+ /**
69
+ * S3 storage entry
70
+ * Identified by bucket, key, and region
71
+ */
72
+ declare const lsp31S3EntrySchema: z.ZodObject<{
73
+ backend: z.ZodLiteral<"s3">;
74
+ /** S3 bucket name */
75
+ bucket: z.ZodString;
76
+ /** S3 object key */
77
+ key: z.ZodString;
78
+ /** AWS region (e.g., "us-east-1") */
79
+ region: z.ZodString;
80
+ }, "strip", z.ZodTypeAny, {
81
+ backend: "s3";
82
+ bucket: string;
83
+ key: string;
84
+ region: string;
85
+ }, {
86
+ backend: "s3";
87
+ bucket: string;
88
+ key: string;
89
+ region: string;
90
+ }>;
91
+ /**
92
+ * Lumera/Pastel Cascade storage entry
93
+ * Identified by a Cascade action ID
94
+ */
95
+ declare const lsp31LumeraEntrySchema: z.ZodObject<{
96
+ backend: z.ZodLiteral<"lumera">;
97
+ /** Lumera/Pastel Cascade action ID */
98
+ actionId: z.ZodString;
99
+ }, "strip", z.ZodTypeAny, {
100
+ backend: "lumera";
101
+ actionId: string;
102
+ }, {
103
+ backend: "lumera";
104
+ actionId: string;
105
+ }>;
106
+ /**
107
+ * Arweave storage entry
108
+ * Identified by a transaction ID
109
+ */
110
+ declare const lsp31ArweaveEntrySchema: z.ZodObject<{
111
+ backend: z.ZodLiteral<"arweave">;
112
+ /** Arweave transaction ID */
113
+ transactionId: z.ZodString;
114
+ }, "strip", z.ZodTypeAny, {
115
+ backend: "arweave";
116
+ transactionId: string;
117
+ }, {
118
+ backend: "arweave";
119
+ transactionId: string;
120
+ }>;
121
+ /**
122
+ * LSP31 entry schema — discriminated union on `backend` field
123
+ *
124
+ * Each entry represents a single storage backend with its backend-specific identifiers.
125
+ * The `backend` field determines which additional fields are required.
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * const ipfsEntry = { backend: 'ipfs', cid: 'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG' };
130
+ * const s3Entry = { backend: 's3', bucket: 'my-bucket', key: 'content/file.bin', region: 'us-east-1' };
131
+ * ```
132
+ */
133
+ declare const lsp31EntrySchema: z.ZodDiscriminatedUnion<"backend", [z.ZodObject<{
134
+ backend: z.ZodLiteral<"ipfs">;
135
+ /** IPFS content identifier (CIDv0 or CIDv1) */
136
+ cid: z.ZodString;
137
+ }, "strip", z.ZodTypeAny, {
138
+ backend: "ipfs";
139
+ cid: string;
140
+ }, {
141
+ backend: "ipfs";
142
+ cid: string;
143
+ }>, z.ZodObject<{
144
+ backend: z.ZodLiteral<"s3">;
145
+ /** S3 bucket name */
146
+ bucket: z.ZodString;
147
+ /** S3 object key */
148
+ key: z.ZodString;
149
+ /** AWS region (e.g., "us-east-1") */
150
+ region: z.ZodString;
151
+ }, "strip", z.ZodTypeAny, {
152
+ backend: "s3";
153
+ bucket: string;
154
+ key: string;
155
+ region: string;
156
+ }, {
157
+ backend: "s3";
158
+ bucket: string;
159
+ key: string;
160
+ region: string;
161
+ }>, z.ZodObject<{
162
+ backend: z.ZodLiteral<"lumera">;
163
+ /** Lumera/Pastel Cascade action ID */
164
+ actionId: z.ZodString;
165
+ }, "strip", z.ZodTypeAny, {
166
+ backend: "lumera";
167
+ actionId: string;
168
+ }, {
169
+ backend: "lumera";
170
+ actionId: string;
171
+ }>, z.ZodObject<{
172
+ backend: z.ZodLiteral<"arweave">;
173
+ /** Arweave transaction ID */
174
+ transactionId: z.ZodString;
175
+ }, "strip", z.ZodTypeAny, {
176
+ backend: "arweave";
177
+ transactionId: string;
178
+ }, {
179
+ backend: "arweave";
180
+ transactionId: string;
181
+ }>]>;
182
+ /**
183
+ * LSP31 entries array — minimum 2 entries required
184
+ *
185
+ * Single-backend content should use LSP2 VerifiableURI instead.
186
+ * The entries array is unordered — resolvers select based on viewer preference.
187
+ */
188
+ declare const lsp31EntriesSchema: z.ZodArray<z.ZodDiscriminatedUnion<"backend", [z.ZodObject<{
189
+ backend: z.ZodLiteral<"ipfs">;
190
+ /** IPFS content identifier (CIDv0 or CIDv1) */
191
+ cid: z.ZodString;
192
+ }, "strip", z.ZodTypeAny, {
193
+ backend: "ipfs";
194
+ cid: string;
195
+ }, {
196
+ backend: "ipfs";
197
+ cid: string;
198
+ }>, z.ZodObject<{
199
+ backend: z.ZodLiteral<"s3">;
200
+ /** S3 bucket name */
201
+ bucket: z.ZodString;
202
+ /** S3 object key */
203
+ key: z.ZodString;
204
+ /** AWS region (e.g., "us-east-1") */
205
+ region: z.ZodString;
206
+ }, "strip", z.ZodTypeAny, {
207
+ backend: "s3";
208
+ bucket: string;
209
+ key: string;
210
+ region: string;
211
+ }, {
212
+ backend: "s3";
213
+ bucket: string;
214
+ key: string;
215
+ region: string;
216
+ }>, z.ZodObject<{
217
+ backend: z.ZodLiteral<"lumera">;
218
+ /** Lumera/Pastel Cascade action ID */
219
+ actionId: z.ZodString;
220
+ }, "strip", z.ZodTypeAny, {
221
+ backend: "lumera";
222
+ actionId: string;
223
+ }, {
224
+ backend: "lumera";
225
+ actionId: string;
226
+ }>, z.ZodObject<{
227
+ backend: z.ZodLiteral<"arweave">;
228
+ /** Arweave transaction ID */
229
+ transactionId: z.ZodString;
230
+ }, "strip", z.ZodTypeAny, {
231
+ backend: "arweave";
232
+ transactionId: string;
233
+ }, {
234
+ backend: "arweave";
235
+ transactionId: string;
236
+ }>]>, "many">;
237
+ /**
238
+ * LSP31 URI data schema — input for encoding an LSP31 multi-storage URI
239
+ *
240
+ * Contains the pre-computed content verification hash and the entries array.
241
+ * The hash covers the content bytes (identical across all backends), not the entries JSON.
242
+ */
243
+ declare const lsp31UriDataSchema: z.ZodObject<{
244
+ /** keccak256 hash of content bytes, 0x-prefixed 64-char hex string */
245
+ verificationHash: z.ZodString;
246
+ /** Storage backend entries (minimum 2) */
247
+ entries: z.ZodArray<z.ZodDiscriminatedUnion<"backend", [z.ZodObject<{
248
+ backend: z.ZodLiteral<"ipfs">;
249
+ /** IPFS content identifier (CIDv0 or CIDv1) */
250
+ cid: z.ZodString;
251
+ }, "strip", z.ZodTypeAny, {
252
+ backend: "ipfs";
253
+ cid: string;
254
+ }, {
255
+ backend: "ipfs";
256
+ cid: string;
257
+ }>, z.ZodObject<{
258
+ backend: z.ZodLiteral<"s3">;
259
+ /** S3 bucket name */
260
+ bucket: z.ZodString;
261
+ /** S3 object key */
262
+ key: z.ZodString;
263
+ /** AWS region (e.g., "us-east-1") */
264
+ region: z.ZodString;
265
+ }, "strip", z.ZodTypeAny, {
266
+ backend: "s3";
267
+ bucket: string;
268
+ key: string;
269
+ region: string;
270
+ }, {
271
+ backend: "s3";
272
+ bucket: string;
273
+ key: string;
274
+ region: string;
275
+ }>, z.ZodObject<{
276
+ backend: z.ZodLiteral<"lumera">;
277
+ /** Lumera/Pastel Cascade action ID */
278
+ actionId: z.ZodString;
279
+ }, "strip", z.ZodTypeAny, {
280
+ backend: "lumera";
281
+ actionId: string;
282
+ }, {
283
+ backend: "lumera";
284
+ actionId: string;
285
+ }>, z.ZodObject<{
286
+ backend: z.ZodLiteral<"arweave">;
287
+ /** Arweave transaction ID */
288
+ transactionId: z.ZodString;
289
+ }, "strip", z.ZodTypeAny, {
290
+ backend: "arweave";
291
+ transactionId: string;
292
+ }, {
293
+ backend: "arweave";
294
+ transactionId: string;
295
+ }>]>, "many">;
296
+ }, "strip", z.ZodTypeAny, {
297
+ entries: ({
298
+ backend: "ipfs";
299
+ cid: string;
300
+ } | {
301
+ backend: "s3";
302
+ bucket: string;
303
+ key: string;
304
+ region: string;
305
+ } | {
306
+ backend: "lumera";
307
+ actionId: string;
308
+ } | {
309
+ backend: "arweave";
310
+ transactionId: string;
311
+ })[];
312
+ verificationHash: string;
313
+ }, {
314
+ entries: ({
315
+ backend: "ipfs";
316
+ cid: string;
317
+ } | {
318
+ backend: "s3";
319
+ bucket: string;
320
+ key: string;
321
+ region: string;
322
+ } | {
323
+ backend: "lumera";
324
+ actionId: string;
325
+ } | {
326
+ backend: "arweave";
327
+ transactionId: string;
328
+ })[];
329
+ verificationHash: string;
330
+ }>;
331
+
332
+ /**
333
+ * LSP31 Multi-Storage URI TypeScript Types
334
+ *
335
+ * Entry types are inferred from Zod schemas via z.infer.
336
+ * ParsedLsp31Uri is a manual interface representing the parsed on-chain byte shape
337
+ * (not user input, so no Zod schema needed).
338
+ *
339
+ * @see LSP-31-MultiStorageURI.md for full specification
340
+ */
341
+
342
+ /** Union of supported storage backend identifiers */
343
+ type Lsp31Backend = (typeof LSP31_BACKENDS)[number];
344
+ /** IPFS storage entry */
345
+ type Lsp31IpfsEntry = z.infer<typeof lsp31IpfsEntrySchema>;
346
+ /** S3 storage entry */
347
+ type Lsp31S3Entry = z.infer<typeof lsp31S3EntrySchema>;
348
+ /** Lumera/Pastel Cascade storage entry */
349
+ type Lsp31LumeraEntry = z.infer<typeof lsp31LumeraEntrySchema>;
350
+ /** Arweave storage entry */
351
+ type Lsp31ArweaveEntry = z.infer<typeof lsp31ArweaveEntrySchema>;
352
+ /** Discriminated union of all storage entry types */
353
+ type Lsp31Entry = z.infer<typeof lsp31EntrySchema>;
354
+ /** Array of storage entries (minimum 2) */
355
+ type Lsp31Entries = z.infer<typeof lsp31EntriesSchema>;
356
+ /** Input data for encoding an LSP31 multi-storage URI */
357
+ type Lsp31UriData = z.infer<typeof lsp31UriDataSchema>;
358
+ /**
359
+ * Output of parsing an LSP31 multi-storage URI from on-chain bytes
360
+ * Used by parseLsp31Uri (implemented in Plan 02)
361
+ */
362
+ interface ParsedLsp31Uri {
363
+ /** Verification method (4 bytes, e.g., 0x8019f9b1 for keccak256(bytes)) */
364
+ verificationMethod: Hex;
365
+ /** Verification data (keccak256 hash of content bytes) */
366
+ verificationData: Hex;
367
+ /** Decoded and validated storage entries */
368
+ entries: Lsp31Entry[];
369
+ }
370
+
371
+ /**
372
+ * LSP31 Multi-Storage URI Decoding
373
+ *
374
+ * Parses and verifies LSP31 on-chain hex values back into structured data.
375
+ * Mirrors the LSP2 parseVerifiableUri/decodeVerifiableUri pattern.
376
+ *
377
+ * @see LSP-31-MultiStorageURI.md for full specification
378
+ */
379
+
380
+ /**
381
+ * Parses an LSP31 Multi-Storage URI hex value into its components.
382
+ *
383
+ * Format: 0x + reserved (2 bytes) + method (4 bytes) + length (2 bytes) + hash (N bytes) + entries JSON
384
+ *
385
+ * @param value - The raw hex value from ERC725Y getData
386
+ * @returns Parsed components (verificationMethod, verificationData, entries)
387
+ * @throws Error if the value is malformed, wrong prefix, or invalid JSON
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * const { verificationMethod, verificationData, entries } = parseLsp31Uri(rawValue);
392
+ * entries.forEach(entry => console.log(entry.backend));
393
+ * ```
394
+ */
395
+ declare function parseLsp31Uri(value: Hex): ParsedLsp31Uri;
396
+ /**
397
+ * Decodes and verifies an LSP31 Multi-Storage URI against its content bytes.
398
+ *
399
+ * 1. Parses the URI structure
400
+ * 2. Verifies the verification method is keccak256(bytes)
401
+ * 3. Computes keccak256 of the provided content and checks against the embedded hash
402
+ * 4. Validates entries through Zod schema
403
+ *
404
+ * @param value - The raw hex value from ERC725Y getData
405
+ * @param content - The actual content bytes (used to verify the hash)
406
+ * @returns Validated entries and verification data
407
+ * @throws Error if hash doesn't match, method is unsupported, or entries are invalid
408
+ *
409
+ * @example
410
+ * ```typescript
411
+ * const contentBytes = await fetchFromBackend(entry);
412
+ * const { entries, verificationData } = decodeLsp31Uri(rawValue, contentBytes);
413
+ * ```
414
+ */
415
+ declare function decodeLsp31Uri(value: Hex, content: Uint8Array): {
416
+ entries: Lsp31Entry[];
417
+ verificationData: Hex;
418
+ };
419
+
420
+ /**
421
+ * LSP31 Multi-Storage URI Encoding
422
+ *
423
+ * Encodes multi-storage entries into the LSP31 on-chain format:
424
+ * 0x0031 + method (4) + hashLength (2) + hash (32) + toUtf8Hex(JSON.stringify(entries))
425
+ *
426
+ * @see LSP-31-MultiStorageURI.md for full specification
427
+ */
428
+
429
+ /**
430
+ * Computes the keccak256 content hash from raw bytes.
431
+ *
432
+ * @param content - Raw content bytes (identical across all storage backends)
433
+ * @returns keccak256 hash as a 0x-prefixed hex string
434
+ *
435
+ * @example
436
+ * ```typescript
437
+ * const contentBytes = new Uint8Array([...]);
438
+ * const hash = computeContentHash(contentBytes);
439
+ * const encoded = encodeLsp31Uri(entries, hash);
440
+ * ```
441
+ */
442
+ declare function computeContentHash(content: Uint8Array): Hex;
443
+ /**
444
+ * Encodes storage entries and a pre-computed verification hash into the LSP31 on-chain format.
445
+ *
446
+ * The output is a hex string with the layout:
447
+ * - 2 bytes: reserved prefix (0x0031)
448
+ * - 4 bytes: verification method ID (0x8019f9b1, keccak256(bytes))
449
+ * - 2 bytes: hash length (0x0020 = 32 bytes)
450
+ * - 32 bytes: content verification hash
451
+ * - variable: UTF-8 encoded JSON of entries array
452
+ *
453
+ * @param entries - Array of storage backend entries (minimum 2)
454
+ * @param verificationHash - Pre-computed keccak256 hash of the content bytes (0x + 64 hex chars)
455
+ * @returns Hex-encoded LSP31 URI value
456
+ * @throws Error if entries fail validation or hash format is invalid
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * const entries = [
461
+ * { backend: 'ipfs', cid: 'QmTest...' },
462
+ * { backend: 's3', bucket: 'my-bucket', key: 'file.bin', region: 'us-east-1' },
463
+ * ];
464
+ * const hash = computeContentHash(contentBytes);
465
+ * const encoded = encodeLsp31Uri(entries, hash);
466
+ * ```
467
+ */
468
+ declare function encodeLsp31Uri(entries: Lsp31Entry[], verificationHash: Hex): Hex;
469
+
470
+ /**
471
+ * LSP31 Multi-Storage URI Type Guards
472
+ *
473
+ * Quick structural checks for identifying LSP31 encoded values.
474
+ *
475
+ * @see LSP-31-MultiStorageURI.md for full specification
476
+ */
477
+
478
+ /**
479
+ * Checks if a hex value appears to be a valid LSP31 Multi-Storage URI format.
480
+ *
481
+ * This is a quick structural check based on length and prefix, not full content validation.
482
+ * Distinguishes LSP31 (0x0031 prefix) from LSP2 VerifiableURI (0x0000 prefix).
483
+ *
484
+ * @param value - Hex value to check
485
+ * @returns true if the value has valid LSP31 structure
486
+ *
487
+ * @example
488
+ * ```typescript
489
+ * if (isLsp31Uri(rawValue)) {
490
+ * const { entries } = parseLsp31Uri(rawValue);
491
+ * } else if (isVerifiableUri(rawValue)) {
492
+ * const { url } = parseVerifiableUri(rawValue);
493
+ * }
494
+ * ```
495
+ */
496
+ declare function isLsp31Uri(value: Hex): boolean;
497
+
498
+ /**
499
+ * LSP31 Multi-Storage URI Resolver
500
+ *
501
+ * Pure functions for selecting preferred storage backends and deriving access URLs.
502
+ * No actual fetching — that's the app layer's responsibility.
503
+ *
504
+ * @see LSP-31-MultiStorageURI.md for full specification
505
+ */
506
+
507
+ /**
508
+ * Selects and orders storage entries based on preference.
509
+ *
510
+ * Uses exhaustive fallback: preferred entries come first, then remaining entries
511
+ * in their original order. Output always has the same length as input — no entries
512
+ * are ever dropped.
513
+ *
514
+ * @param entries - Array of storage entries to order
515
+ * @param preference - Optional preferred backend(s). Can be:
516
+ * - undefined: returns entries in original order
517
+ * - string: single preferred backend
518
+ * - array: ordered list of preferred backends
519
+ * - empty array: treated as undefined (no preference)
520
+ * @returns New array with all entries, preferred ones first
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * const ordered = selectBackend(entries, 'ipfs');
525
+ * // IPFS entry first, then remaining in original order
526
+ *
527
+ * const ordered2 = selectBackend(entries, ['lumera', 'ipfs']);
528
+ * // Lumera first, IPFS second, then remaining
529
+ * ```
530
+ */
531
+ declare function selectBackend(entries: Lsp31Entry[], preference?: Lsp31Backend | Lsp31Backend[]): Lsp31Entry[];
532
+ /**
533
+ * Derives an access URL from a storage backend entry.
534
+ *
535
+ * Returns protocol URLs for IPFS and Lumera (actual gateway resolution is app-layer),
536
+ * and direct HTTPS URLs for S3 and Arweave.
537
+ *
538
+ * @param entry - A single storage entry
539
+ * @returns The derived access URL
540
+ *
541
+ * @example
542
+ * ```typescript
543
+ * const url = resolveUrl({ backend: 'ipfs', cid: 'QmTest...' });
544
+ * // 'ipfs://QmTest...'
545
+ *
546
+ * const url2 = resolveUrl({ backend: 's3', bucket: 'b', key: 'k', region: 'r' });
547
+ * // 'https://b.s3.r.amazonaws.com/k'
548
+ * ```
549
+ */
550
+ declare function resolveUrl(entry: Lsp31Entry): string;
551
+
552
+ export { HASH_LENGTH_PREFIX, KECCAK256_BYTES_METHOD_ID, LSP31_BACKENDS, LSP31_MIN_ENTRIES, LSP31_RESERVED_PREFIX, MIN_LSP31_URI_LENGTH, computeContentHash, decodeLsp31Uri, encodeLsp31Uri, isLsp31Uri, lsp31ArweaveEntrySchema, lsp31EntriesSchema, lsp31EntrySchema, lsp31IpfsEntrySchema, lsp31LumeraEntrySchema, lsp31S3EntrySchema, lsp31UriDataSchema, parseLsp31Uri, resolveUrl, selectBackend };
553
+ export type { Lsp31ArweaveEntry, Lsp31Backend, Lsp31Entries, Lsp31Entry, Lsp31IpfsEntry, Lsp31LumeraEntry, Lsp31S3Entry, Lsp31UriData, ParsedLsp31Uri };
@@ -0,0 +1,553 @@
1
+ import { Hex } from 'viem';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * LSP31 Multi-Storage URI Constants
6
+ *
7
+ * These constants mirror LSP2 verification constants locally to avoid
8
+ * external dependencies (this package is standalone).
9
+ *
10
+ * @see LSP-31-MultiStorageURI.md for full specification
11
+ */
12
+ /**
13
+ * Reserved prefix for LSP31 Multi-Storage URI (2 bytes)
14
+ * Distinguishes LSP31 from LSP2 VerifiableURI (0x0000)
15
+ */
16
+ declare const LSP31_RESERVED_PREFIX: "0x0031";
17
+ /**
18
+ * Verification method ID for keccak256(bytes)
19
+ * Computed as: bytes4(keccak256('keccak256(bytes)')) = 0x8019f9b1
20
+ * Same as LSP2 — verification model is identical
21
+ */
22
+ declare const KECCAK256_BYTES_METHOD_ID: "0x8019f9b1";
23
+ /**
24
+ * Length of a keccak256 hash in bytes (32 bytes = 0x0020)
25
+ */
26
+ declare const HASH_LENGTH_PREFIX: "0x0020";
27
+ /**
28
+ * Minimum length of a valid LSP31 URI value in hex characters
29
+ * 2 (prefix) + 4 (method ID) + 2 (hash length) + 32 (hash) = 40 bytes = 80 hex chars + '0x' prefix
30
+ * Note: A valid LSP31 URI will always be longer (entries JSON adds more), but 82 is the structural minimum
31
+ */
32
+ declare const MIN_LSP31_URI_LENGTH = 82;
33
+ /**
34
+ * Closed set of supported storage backends
35
+ * Adding a backend requires a schema update — this is intentional for validation safety
36
+ */
37
+ declare const LSP31_BACKENDS: readonly ["ipfs", "s3", "lumera", "arweave"];
38
+ /**
39
+ * Minimum number of entries required in an LSP31 multi-storage URI
40
+ * Single-backend content should use LSP2 VerifiableURI instead
41
+ */
42
+ declare const LSP31_MIN_ENTRIES = 2;
43
+
44
+ /**
45
+ * LSP31 Multi-Storage URI Zod Schemas
46
+ *
47
+ * Validates multi-storage entries with a discriminated union on the `backend` field.
48
+ * Four backend types: ipfs, s3, lumera, arweave — each with backend-specific identifier fields.
49
+ *
50
+ * @see LSP-31-MultiStorageURI.md for full specification
51
+ */
52
+
53
+ /**
54
+ * IPFS storage entry
55
+ * Identified by a Content Identifier (CID)
56
+ */
57
+ declare const lsp31IpfsEntrySchema: z.ZodObject<{
58
+ backend: z.ZodLiteral<"ipfs">;
59
+ /** IPFS content identifier (CIDv0 or CIDv1) */
60
+ cid: z.ZodString;
61
+ }, "strip", z.ZodTypeAny, {
62
+ backend: "ipfs";
63
+ cid: string;
64
+ }, {
65
+ backend: "ipfs";
66
+ cid: string;
67
+ }>;
68
+ /**
69
+ * S3 storage entry
70
+ * Identified by bucket, key, and region
71
+ */
72
+ declare const lsp31S3EntrySchema: z.ZodObject<{
73
+ backend: z.ZodLiteral<"s3">;
74
+ /** S3 bucket name */
75
+ bucket: z.ZodString;
76
+ /** S3 object key */
77
+ key: z.ZodString;
78
+ /** AWS region (e.g., "us-east-1") */
79
+ region: z.ZodString;
80
+ }, "strip", z.ZodTypeAny, {
81
+ backend: "s3";
82
+ bucket: string;
83
+ key: string;
84
+ region: string;
85
+ }, {
86
+ backend: "s3";
87
+ bucket: string;
88
+ key: string;
89
+ region: string;
90
+ }>;
91
+ /**
92
+ * Lumera/Pastel Cascade storage entry
93
+ * Identified by a Cascade action ID
94
+ */
95
+ declare const lsp31LumeraEntrySchema: z.ZodObject<{
96
+ backend: z.ZodLiteral<"lumera">;
97
+ /** Lumera/Pastel Cascade action ID */
98
+ actionId: z.ZodString;
99
+ }, "strip", z.ZodTypeAny, {
100
+ backend: "lumera";
101
+ actionId: string;
102
+ }, {
103
+ backend: "lumera";
104
+ actionId: string;
105
+ }>;
106
+ /**
107
+ * Arweave storage entry
108
+ * Identified by a transaction ID
109
+ */
110
+ declare const lsp31ArweaveEntrySchema: z.ZodObject<{
111
+ backend: z.ZodLiteral<"arweave">;
112
+ /** Arweave transaction ID */
113
+ transactionId: z.ZodString;
114
+ }, "strip", z.ZodTypeAny, {
115
+ backend: "arweave";
116
+ transactionId: string;
117
+ }, {
118
+ backend: "arweave";
119
+ transactionId: string;
120
+ }>;
121
+ /**
122
+ * LSP31 entry schema — discriminated union on `backend` field
123
+ *
124
+ * Each entry represents a single storage backend with its backend-specific identifiers.
125
+ * The `backend` field determines which additional fields are required.
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * const ipfsEntry = { backend: 'ipfs', cid: 'QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG' };
130
+ * const s3Entry = { backend: 's3', bucket: 'my-bucket', key: 'content/file.bin', region: 'us-east-1' };
131
+ * ```
132
+ */
133
+ declare const lsp31EntrySchema: z.ZodDiscriminatedUnion<"backend", [z.ZodObject<{
134
+ backend: z.ZodLiteral<"ipfs">;
135
+ /** IPFS content identifier (CIDv0 or CIDv1) */
136
+ cid: z.ZodString;
137
+ }, "strip", z.ZodTypeAny, {
138
+ backend: "ipfs";
139
+ cid: string;
140
+ }, {
141
+ backend: "ipfs";
142
+ cid: string;
143
+ }>, z.ZodObject<{
144
+ backend: z.ZodLiteral<"s3">;
145
+ /** S3 bucket name */
146
+ bucket: z.ZodString;
147
+ /** S3 object key */
148
+ key: z.ZodString;
149
+ /** AWS region (e.g., "us-east-1") */
150
+ region: z.ZodString;
151
+ }, "strip", z.ZodTypeAny, {
152
+ backend: "s3";
153
+ bucket: string;
154
+ key: string;
155
+ region: string;
156
+ }, {
157
+ backend: "s3";
158
+ bucket: string;
159
+ key: string;
160
+ region: string;
161
+ }>, z.ZodObject<{
162
+ backend: z.ZodLiteral<"lumera">;
163
+ /** Lumera/Pastel Cascade action ID */
164
+ actionId: z.ZodString;
165
+ }, "strip", z.ZodTypeAny, {
166
+ backend: "lumera";
167
+ actionId: string;
168
+ }, {
169
+ backend: "lumera";
170
+ actionId: string;
171
+ }>, z.ZodObject<{
172
+ backend: z.ZodLiteral<"arweave">;
173
+ /** Arweave transaction ID */
174
+ transactionId: z.ZodString;
175
+ }, "strip", z.ZodTypeAny, {
176
+ backend: "arweave";
177
+ transactionId: string;
178
+ }, {
179
+ backend: "arweave";
180
+ transactionId: string;
181
+ }>]>;
182
+ /**
183
+ * LSP31 entries array — minimum 2 entries required
184
+ *
185
+ * Single-backend content should use LSP2 VerifiableURI instead.
186
+ * The entries array is unordered — resolvers select based on viewer preference.
187
+ */
188
+ declare const lsp31EntriesSchema: z.ZodArray<z.ZodDiscriminatedUnion<"backend", [z.ZodObject<{
189
+ backend: z.ZodLiteral<"ipfs">;
190
+ /** IPFS content identifier (CIDv0 or CIDv1) */
191
+ cid: z.ZodString;
192
+ }, "strip", z.ZodTypeAny, {
193
+ backend: "ipfs";
194
+ cid: string;
195
+ }, {
196
+ backend: "ipfs";
197
+ cid: string;
198
+ }>, z.ZodObject<{
199
+ backend: z.ZodLiteral<"s3">;
200
+ /** S3 bucket name */
201
+ bucket: z.ZodString;
202
+ /** S3 object key */
203
+ key: z.ZodString;
204
+ /** AWS region (e.g., "us-east-1") */
205
+ region: z.ZodString;
206
+ }, "strip", z.ZodTypeAny, {
207
+ backend: "s3";
208
+ bucket: string;
209
+ key: string;
210
+ region: string;
211
+ }, {
212
+ backend: "s3";
213
+ bucket: string;
214
+ key: string;
215
+ region: string;
216
+ }>, z.ZodObject<{
217
+ backend: z.ZodLiteral<"lumera">;
218
+ /** Lumera/Pastel Cascade action ID */
219
+ actionId: z.ZodString;
220
+ }, "strip", z.ZodTypeAny, {
221
+ backend: "lumera";
222
+ actionId: string;
223
+ }, {
224
+ backend: "lumera";
225
+ actionId: string;
226
+ }>, z.ZodObject<{
227
+ backend: z.ZodLiteral<"arweave">;
228
+ /** Arweave transaction ID */
229
+ transactionId: z.ZodString;
230
+ }, "strip", z.ZodTypeAny, {
231
+ backend: "arweave";
232
+ transactionId: string;
233
+ }, {
234
+ backend: "arweave";
235
+ transactionId: string;
236
+ }>]>, "many">;
237
+ /**
238
+ * LSP31 URI data schema — input for encoding an LSP31 multi-storage URI
239
+ *
240
+ * Contains the pre-computed content verification hash and the entries array.
241
+ * The hash covers the content bytes (identical across all backends), not the entries JSON.
242
+ */
243
+ declare const lsp31UriDataSchema: z.ZodObject<{
244
+ /** keccak256 hash of content bytes, 0x-prefixed 64-char hex string */
245
+ verificationHash: z.ZodString;
246
+ /** Storage backend entries (minimum 2) */
247
+ entries: z.ZodArray<z.ZodDiscriminatedUnion<"backend", [z.ZodObject<{
248
+ backend: z.ZodLiteral<"ipfs">;
249
+ /** IPFS content identifier (CIDv0 or CIDv1) */
250
+ cid: z.ZodString;
251
+ }, "strip", z.ZodTypeAny, {
252
+ backend: "ipfs";
253
+ cid: string;
254
+ }, {
255
+ backend: "ipfs";
256
+ cid: string;
257
+ }>, z.ZodObject<{
258
+ backend: z.ZodLiteral<"s3">;
259
+ /** S3 bucket name */
260
+ bucket: z.ZodString;
261
+ /** S3 object key */
262
+ key: z.ZodString;
263
+ /** AWS region (e.g., "us-east-1") */
264
+ region: z.ZodString;
265
+ }, "strip", z.ZodTypeAny, {
266
+ backend: "s3";
267
+ bucket: string;
268
+ key: string;
269
+ region: string;
270
+ }, {
271
+ backend: "s3";
272
+ bucket: string;
273
+ key: string;
274
+ region: string;
275
+ }>, z.ZodObject<{
276
+ backend: z.ZodLiteral<"lumera">;
277
+ /** Lumera/Pastel Cascade action ID */
278
+ actionId: z.ZodString;
279
+ }, "strip", z.ZodTypeAny, {
280
+ backend: "lumera";
281
+ actionId: string;
282
+ }, {
283
+ backend: "lumera";
284
+ actionId: string;
285
+ }>, z.ZodObject<{
286
+ backend: z.ZodLiteral<"arweave">;
287
+ /** Arweave transaction ID */
288
+ transactionId: z.ZodString;
289
+ }, "strip", z.ZodTypeAny, {
290
+ backend: "arweave";
291
+ transactionId: string;
292
+ }, {
293
+ backend: "arweave";
294
+ transactionId: string;
295
+ }>]>, "many">;
296
+ }, "strip", z.ZodTypeAny, {
297
+ entries: ({
298
+ backend: "ipfs";
299
+ cid: string;
300
+ } | {
301
+ backend: "s3";
302
+ bucket: string;
303
+ key: string;
304
+ region: string;
305
+ } | {
306
+ backend: "lumera";
307
+ actionId: string;
308
+ } | {
309
+ backend: "arweave";
310
+ transactionId: string;
311
+ })[];
312
+ verificationHash: string;
313
+ }, {
314
+ entries: ({
315
+ backend: "ipfs";
316
+ cid: string;
317
+ } | {
318
+ backend: "s3";
319
+ bucket: string;
320
+ key: string;
321
+ region: string;
322
+ } | {
323
+ backend: "lumera";
324
+ actionId: string;
325
+ } | {
326
+ backend: "arweave";
327
+ transactionId: string;
328
+ })[];
329
+ verificationHash: string;
330
+ }>;
331
+
332
+ /**
333
+ * LSP31 Multi-Storage URI TypeScript Types
334
+ *
335
+ * Entry types are inferred from Zod schemas via z.infer.
336
+ * ParsedLsp31Uri is a manual interface representing the parsed on-chain byte shape
337
+ * (not user input, so no Zod schema needed).
338
+ *
339
+ * @see LSP-31-MultiStorageURI.md for full specification
340
+ */
341
+
342
+ /** Union of supported storage backend identifiers */
343
+ type Lsp31Backend = (typeof LSP31_BACKENDS)[number];
344
+ /** IPFS storage entry */
345
+ type Lsp31IpfsEntry = z.infer<typeof lsp31IpfsEntrySchema>;
346
+ /** S3 storage entry */
347
+ type Lsp31S3Entry = z.infer<typeof lsp31S3EntrySchema>;
348
+ /** Lumera/Pastel Cascade storage entry */
349
+ type Lsp31LumeraEntry = z.infer<typeof lsp31LumeraEntrySchema>;
350
+ /** Arweave storage entry */
351
+ type Lsp31ArweaveEntry = z.infer<typeof lsp31ArweaveEntrySchema>;
352
+ /** Discriminated union of all storage entry types */
353
+ type Lsp31Entry = z.infer<typeof lsp31EntrySchema>;
354
+ /** Array of storage entries (minimum 2) */
355
+ type Lsp31Entries = z.infer<typeof lsp31EntriesSchema>;
356
+ /** Input data for encoding an LSP31 multi-storage URI */
357
+ type Lsp31UriData = z.infer<typeof lsp31UriDataSchema>;
358
+ /**
359
+ * Output of parsing an LSP31 multi-storage URI from on-chain bytes
360
+ * Used by parseLsp31Uri (implemented in Plan 02)
361
+ */
362
+ interface ParsedLsp31Uri {
363
+ /** Verification method (4 bytes, e.g., 0x8019f9b1 for keccak256(bytes)) */
364
+ verificationMethod: Hex;
365
+ /** Verification data (keccak256 hash of content bytes) */
366
+ verificationData: Hex;
367
+ /** Decoded and validated storage entries */
368
+ entries: Lsp31Entry[];
369
+ }
370
+
371
+ /**
372
+ * LSP31 Multi-Storage URI Decoding
373
+ *
374
+ * Parses and verifies LSP31 on-chain hex values back into structured data.
375
+ * Mirrors the LSP2 parseVerifiableUri/decodeVerifiableUri pattern.
376
+ *
377
+ * @see LSP-31-MultiStorageURI.md for full specification
378
+ */
379
+
380
+ /**
381
+ * Parses an LSP31 Multi-Storage URI hex value into its components.
382
+ *
383
+ * Format: 0x + reserved (2 bytes) + method (4 bytes) + length (2 bytes) + hash (N bytes) + entries JSON
384
+ *
385
+ * @param value - The raw hex value from ERC725Y getData
386
+ * @returns Parsed components (verificationMethod, verificationData, entries)
387
+ * @throws Error if the value is malformed, wrong prefix, or invalid JSON
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * const { verificationMethod, verificationData, entries } = parseLsp31Uri(rawValue);
392
+ * entries.forEach(entry => console.log(entry.backend));
393
+ * ```
394
+ */
395
+ declare function parseLsp31Uri(value: Hex): ParsedLsp31Uri;
396
+ /**
397
+ * Decodes and verifies an LSP31 Multi-Storage URI against its content bytes.
398
+ *
399
+ * 1. Parses the URI structure
400
+ * 2. Verifies the verification method is keccak256(bytes)
401
+ * 3. Computes keccak256 of the provided content and checks against the embedded hash
402
+ * 4. Validates entries through Zod schema
403
+ *
404
+ * @param value - The raw hex value from ERC725Y getData
405
+ * @param content - The actual content bytes (used to verify the hash)
406
+ * @returns Validated entries and verification data
407
+ * @throws Error if hash doesn't match, method is unsupported, or entries are invalid
408
+ *
409
+ * @example
410
+ * ```typescript
411
+ * const contentBytes = await fetchFromBackend(entry);
412
+ * const { entries, verificationData } = decodeLsp31Uri(rawValue, contentBytes);
413
+ * ```
414
+ */
415
+ declare function decodeLsp31Uri(value: Hex, content: Uint8Array): {
416
+ entries: Lsp31Entry[];
417
+ verificationData: Hex;
418
+ };
419
+
420
+ /**
421
+ * LSP31 Multi-Storage URI Encoding
422
+ *
423
+ * Encodes multi-storage entries into the LSP31 on-chain format:
424
+ * 0x0031 + method (4) + hashLength (2) + hash (32) + toUtf8Hex(JSON.stringify(entries))
425
+ *
426
+ * @see LSP-31-MultiStorageURI.md for full specification
427
+ */
428
+
429
+ /**
430
+ * Computes the keccak256 content hash from raw bytes.
431
+ *
432
+ * @param content - Raw content bytes (identical across all storage backends)
433
+ * @returns keccak256 hash as a 0x-prefixed hex string
434
+ *
435
+ * @example
436
+ * ```typescript
437
+ * const contentBytes = new Uint8Array([...]);
438
+ * const hash = computeContentHash(contentBytes);
439
+ * const encoded = encodeLsp31Uri(entries, hash);
440
+ * ```
441
+ */
442
+ declare function computeContentHash(content: Uint8Array): Hex;
443
+ /**
444
+ * Encodes storage entries and a pre-computed verification hash into the LSP31 on-chain format.
445
+ *
446
+ * The output is a hex string with the layout:
447
+ * - 2 bytes: reserved prefix (0x0031)
448
+ * - 4 bytes: verification method ID (0x8019f9b1, keccak256(bytes))
449
+ * - 2 bytes: hash length (0x0020 = 32 bytes)
450
+ * - 32 bytes: content verification hash
451
+ * - variable: UTF-8 encoded JSON of entries array
452
+ *
453
+ * @param entries - Array of storage backend entries (minimum 2)
454
+ * @param verificationHash - Pre-computed keccak256 hash of the content bytes (0x + 64 hex chars)
455
+ * @returns Hex-encoded LSP31 URI value
456
+ * @throws Error if entries fail validation or hash format is invalid
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * const entries = [
461
+ * { backend: 'ipfs', cid: 'QmTest...' },
462
+ * { backend: 's3', bucket: 'my-bucket', key: 'file.bin', region: 'us-east-1' },
463
+ * ];
464
+ * const hash = computeContentHash(contentBytes);
465
+ * const encoded = encodeLsp31Uri(entries, hash);
466
+ * ```
467
+ */
468
+ declare function encodeLsp31Uri(entries: Lsp31Entry[], verificationHash: Hex): Hex;
469
+
470
+ /**
471
+ * LSP31 Multi-Storage URI Type Guards
472
+ *
473
+ * Quick structural checks for identifying LSP31 encoded values.
474
+ *
475
+ * @see LSP-31-MultiStorageURI.md for full specification
476
+ */
477
+
478
+ /**
479
+ * Checks if a hex value appears to be a valid LSP31 Multi-Storage URI format.
480
+ *
481
+ * This is a quick structural check based on length and prefix, not full content validation.
482
+ * Distinguishes LSP31 (0x0031 prefix) from LSP2 VerifiableURI (0x0000 prefix).
483
+ *
484
+ * @param value - Hex value to check
485
+ * @returns true if the value has valid LSP31 structure
486
+ *
487
+ * @example
488
+ * ```typescript
489
+ * if (isLsp31Uri(rawValue)) {
490
+ * const { entries } = parseLsp31Uri(rawValue);
491
+ * } else if (isVerifiableUri(rawValue)) {
492
+ * const { url } = parseVerifiableUri(rawValue);
493
+ * }
494
+ * ```
495
+ */
496
+ declare function isLsp31Uri(value: Hex): boolean;
497
+
498
+ /**
499
+ * LSP31 Multi-Storage URI Resolver
500
+ *
501
+ * Pure functions for selecting preferred storage backends and deriving access URLs.
502
+ * No actual fetching — that's the app layer's responsibility.
503
+ *
504
+ * @see LSP-31-MultiStorageURI.md for full specification
505
+ */
506
+
507
+ /**
508
+ * Selects and orders storage entries based on preference.
509
+ *
510
+ * Uses exhaustive fallback: preferred entries come first, then remaining entries
511
+ * in their original order. Output always has the same length as input — no entries
512
+ * are ever dropped.
513
+ *
514
+ * @param entries - Array of storage entries to order
515
+ * @param preference - Optional preferred backend(s). Can be:
516
+ * - undefined: returns entries in original order
517
+ * - string: single preferred backend
518
+ * - array: ordered list of preferred backends
519
+ * - empty array: treated as undefined (no preference)
520
+ * @returns New array with all entries, preferred ones first
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * const ordered = selectBackend(entries, 'ipfs');
525
+ * // IPFS entry first, then remaining in original order
526
+ *
527
+ * const ordered2 = selectBackend(entries, ['lumera', 'ipfs']);
528
+ * // Lumera first, IPFS second, then remaining
529
+ * ```
530
+ */
531
+ declare function selectBackend(entries: Lsp31Entry[], preference?: Lsp31Backend | Lsp31Backend[]): Lsp31Entry[];
532
+ /**
533
+ * Derives an access URL from a storage backend entry.
534
+ *
535
+ * Returns protocol URLs for IPFS and Lumera (actual gateway resolution is app-layer),
536
+ * and direct HTTPS URLs for S3 and Arweave.
537
+ *
538
+ * @param entry - A single storage entry
539
+ * @returns The derived access URL
540
+ *
541
+ * @example
542
+ * ```typescript
543
+ * const url = resolveUrl({ backend: 'ipfs', cid: 'QmTest...' });
544
+ * // 'ipfs://QmTest...'
545
+ *
546
+ * const url2 = resolveUrl({ backend: 's3', bucket: 'b', key: 'k', region: 'r' });
547
+ * // 'https://b.s3.r.amazonaws.com/k'
548
+ * ```
549
+ */
550
+ declare function resolveUrl(entry: Lsp31Entry): string;
551
+
552
+ export { HASH_LENGTH_PREFIX, KECCAK256_BYTES_METHOD_ID, LSP31_BACKENDS, LSP31_MIN_ENTRIES, LSP31_RESERVED_PREFIX, MIN_LSP31_URI_LENGTH, computeContentHash, decodeLsp31Uri, encodeLsp31Uri, isLsp31Uri, lsp31ArweaveEntrySchema, lsp31EntriesSchema, lsp31EntrySchema, lsp31IpfsEntrySchema, lsp31LumeraEntrySchema, lsp31S3EntrySchema, lsp31UriDataSchema, parseLsp31Uri, resolveUrl, selectBackend };
553
+ export type { Lsp31ArweaveEntry, Lsp31Backend, Lsp31Entries, Lsp31Entry, Lsp31IpfsEntry, Lsp31LumeraEntry, Lsp31S3Entry, Lsp31UriData, ParsedLsp31Uri };
package/dist/index.mjs ADDED
@@ -0,0 +1,175 @@
1
+ import { keccak256, slice, hexToString, stringToHex, concat } from 'viem';
2
+ import { z } from 'zod';
3
+
4
+ const LSP31_RESERVED_PREFIX = "0x0031";
5
+ const KECCAK256_BYTES_METHOD_ID = "0x8019f9b1";
6
+ const HASH_LENGTH_PREFIX = "0x0020";
7
+ const MIN_LSP31_URI_LENGTH = 82;
8
+ const LSP31_BACKENDS = ["ipfs", "s3", "lumera", "arweave"];
9
+ const LSP31_MIN_ENTRIES = 2;
10
+
11
+ const lsp31IpfsEntrySchema = z.object({
12
+ backend: z.literal("ipfs"),
13
+ /** IPFS content identifier (CIDv0 or CIDv1) */
14
+ cid: z.string().min(1, "CID is required")
15
+ });
16
+ const lsp31S3EntrySchema = z.object({
17
+ backend: z.literal("s3"),
18
+ /** S3 bucket name */
19
+ bucket: z.string().min(1, "Bucket is required"),
20
+ /** S3 object key */
21
+ key: z.string().min(1, "Key is required"),
22
+ /** AWS region (e.g., "us-east-1") */
23
+ region: z.string().min(1, "Region is required")
24
+ });
25
+ const lsp31LumeraEntrySchema = z.object({
26
+ backend: z.literal("lumera"),
27
+ /** Lumera/Pastel Cascade action ID */
28
+ actionId: z.string().min(1, "Action ID is required")
29
+ });
30
+ const lsp31ArweaveEntrySchema = z.object({
31
+ backend: z.literal("arweave"),
32
+ /** Arweave transaction ID */
33
+ transactionId: z.string().min(1, "Transaction ID is required")
34
+ });
35
+ const lsp31EntrySchema = z.discriminatedUnion("backend", [
36
+ lsp31IpfsEntrySchema,
37
+ lsp31S3EntrySchema,
38
+ lsp31LumeraEntrySchema,
39
+ lsp31ArweaveEntrySchema
40
+ ]);
41
+ const lsp31EntriesSchema = z.array(lsp31EntrySchema).min(
42
+ LSP31_MIN_ENTRIES,
43
+ "LSP31 requires at least 2 storage entries \u2014 use LSP2 VerifiableURI for single-backend content"
44
+ );
45
+ const lsp31UriDataSchema = z.object({
46
+ /** keccak256 hash of content bytes, 0x-prefixed 64-char hex string */
47
+ verificationHash: z.string().regex(/^0x[0-9a-fA-F]{64}$/, "Must be a 0x-prefixed 64-char hex hash"),
48
+ /** Storage backend entries (minimum 2) */
49
+ entries: lsp31EntriesSchema
50
+ });
51
+
52
+ function parseLsp31Uri(value) {
53
+ if (value.length < MIN_LSP31_URI_LENGTH) {
54
+ throw new Error(
55
+ `Invalid LSP31 URI: value too short (${value.length} chars, minimum ${MIN_LSP31_URI_LENGTH})`
56
+ );
57
+ }
58
+ const reservedPrefix = slice(value, 0, 2);
59
+ if (reservedPrefix !== LSP31_RESERVED_PREFIX) {
60
+ throw new Error(
61
+ `Invalid LSP31 URI: expected prefix ${LSP31_RESERVED_PREFIX}, got ${reservedPrefix}`
62
+ );
63
+ }
64
+ const verificationMethod = slice(value, 2, 6);
65
+ const hashLengthHex = slice(value, 6, 8);
66
+ const hashLength = parseInt(hashLengthHex.slice(2), 16);
67
+ const verificationData = slice(value, 8, 8 + hashLength);
68
+ const entriesHex = slice(value, 8 + hashLength);
69
+ let entries;
70
+ try {
71
+ const entriesJson = hexToString(entriesHex);
72
+ entries = JSON.parse(entriesJson);
73
+ } catch {
74
+ throw new Error("Invalid LSP31 URI: entries portion contains invalid JSON");
75
+ }
76
+ return {
77
+ verificationMethod,
78
+ verificationData,
79
+ entries
80
+ };
81
+ }
82
+ function decodeLsp31Uri(value, content) {
83
+ const parsed = parseLsp31Uri(value);
84
+ if (parsed.verificationMethod !== KECCAK256_BYTES_METHOD_ID) {
85
+ throw new Error(
86
+ `Unsupported verification method: ${parsed.verificationMethod}. Expected ${KECCAK256_BYTES_METHOD_ID} (keccak256(bytes))`
87
+ );
88
+ }
89
+ const computedHash = keccak256(content);
90
+ if (computedHash.toLowerCase() !== parsed.verificationData.toLowerCase()) {
91
+ throw new Error(
92
+ `LSP31 hash mismatch: content hash ${computedHash} does not match verification data ${parsed.verificationData}`
93
+ );
94
+ }
95
+ const validatedEntries = lsp31EntriesSchema.parse(parsed.entries);
96
+ return {
97
+ entries: validatedEntries,
98
+ verificationData: parsed.verificationData
99
+ };
100
+ }
101
+
102
+ function computeContentHash(content) {
103
+ return keccak256(content);
104
+ }
105
+ function encodeLsp31Uri(entries, verificationHash) {
106
+ lsp31EntriesSchema.parse(entries);
107
+ if (typeof verificationHash !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(verificationHash)) {
108
+ throw new Error(
109
+ `Invalid verification hash: expected 0x-prefixed 64-char hex string, got ${String(verificationHash).slice(0, 20)}...`
110
+ );
111
+ }
112
+ const entriesHex = stringToHex(JSON.stringify(entries));
113
+ return concat([
114
+ LSP31_RESERVED_PREFIX,
115
+ KECCAK256_BYTES_METHOD_ID,
116
+ HASH_LENGTH_PREFIX,
117
+ verificationHash,
118
+ entriesHex
119
+ ]);
120
+ }
121
+
122
+ function isLsp31Uri(value) {
123
+ if (value.length < MIN_LSP31_URI_LENGTH) {
124
+ return false;
125
+ }
126
+ try {
127
+ const reservedPrefix = slice(value, 0, 2);
128
+ return reservedPrefix === LSP31_RESERVED_PREFIX;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ function selectBackend(entries, preference) {
135
+ if (!preference || Array.isArray(preference) && preference.length === 0) {
136
+ return [...entries];
137
+ }
138
+ const prefs = typeof preference === "string" ? [preference] : preference;
139
+ const placed = /* @__PURE__ */ new Set();
140
+ const result = [];
141
+ for (const pref of prefs) {
142
+ for (let i = 0; i < entries.length; i++) {
143
+ if (!placed.has(i) && entries[i].backend === pref) {
144
+ result.push(entries[i]);
145
+ placed.add(i);
146
+ }
147
+ }
148
+ }
149
+ for (let i = 0; i < entries.length; i++) {
150
+ if (!placed.has(i)) {
151
+ result.push(entries[i]);
152
+ }
153
+ }
154
+ return result;
155
+ }
156
+ function resolveUrl(entry) {
157
+ switch (entry.backend) {
158
+ case "ipfs":
159
+ return `ipfs://${entry.cid}`;
160
+ case "s3":
161
+ return `https://${entry.bucket}.s3.${entry.region}.amazonaws.com/${encodeURIComponent(entry.key)}`;
162
+ case "lumera":
163
+ return `lumera://${entry.actionId}`;
164
+ case "arweave":
165
+ return `https://arweave.net/${entry.transactionId}`;
166
+ default: {
167
+ const _exhaustive = entry;
168
+ throw new Error(
169
+ `Unknown backend: ${_exhaustive.backend}`
170
+ );
171
+ }
172
+ }
173
+ }
174
+
175
+ export { HASH_LENGTH_PREFIX, KECCAK256_BYTES_METHOD_ID, LSP31_BACKENDS, LSP31_MIN_ENTRIES, LSP31_RESERVED_PREFIX, MIN_LSP31_URI_LENGTH, computeContentHash, decodeLsp31Uri, encodeLsp31Uri, isLsp31Uri, lsp31ArweaveEntrySchema, lsp31EntriesSchema, lsp31EntrySchema, lsp31IpfsEntrySchema, lsp31LumeraEntrySchema, lsp31S3EntrySchema, lsp31UriDataSchema, parseLsp31Uri, resolveUrl, selectBackend };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@chillwhales/lsp31",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "LSP31 Multi-Storage URI standard — encode, decode, and resolve multi-backend content references",
6
+ "author": "b00ste",
7
+ "license": "MIT",
8
+ "types": "./dist/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=22"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/chillwhales/LSPs.git",
26
+ "directory": "packages/lsp31"
27
+ },
28
+ "keywords": [
29
+ "chillwhales",
30
+ "lukso",
31
+ "lsp",
32
+ "lsp31",
33
+ "multi-storage",
34
+ "storage-uri"
35
+ ],
36
+ "sideEffects": false,
37
+ "dependencies": {
38
+ "zod": "^3.24.1"
39
+ },
40
+ "peerDependencies": {
41
+ "viem": "^2.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "typescript": "^5.9.3",
45
+ "unbuild": "^3.6.1",
46
+ "viem": "^2.0.0",
47
+ "vitest": "^4.0.17",
48
+ "@chillwhales/config": "0.0.0"
49
+ },
50
+ "scripts": {
51
+ "build": "unbuild",
52
+ "build:watch": "unbuild --watch",
53
+ "clean": "rm -rf dist",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest"
56
+ }
57
+ }