@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 +21 -0
- package/README.md +53 -0
- package/dist/index.d.mts +553 -0
- package/dist/index.d.ts +553 -0
- package/dist/index.mjs +175 -0
- package/package.json +57 -0
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
|
+
[](./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)
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|