@parity/product-sdk-cloud-storage 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,534 @@
1
+ import { parseCid, CidCodec as CidCodec$1, UnixFsDagBuilder, AsyncBulletinClient } from '@parity/bulletin-sdk';
2
+ export { AsyncBulletinClient, AuthCallBuilder, AuthorizationScope, BulletinError, BulletinPreparer, CID, CallBuilder, ChunkStatus, DEFAULT_CHUNKER_CONFIG, DEFAULT_CLIENT_CONFIG, DEFAULT_STORE_OPTIONS, ErrorCode, MAX_CHUNK_SIZE, MAX_FILE_SIZE, MockBulletinClient, StoreBuilder, TxStatus, WaitFor, calculateCid, cidFromBytes, cidToBytes, convertCid, estimateAuthorization, getContentHash, parseCid, reassembleChunks, resolveClientConfig, validateChunkSize } from '@parity/bulletin-sdk';
3
+ import { getChainAPI, createChainClient } from '@parity/product-sdk-chain-client';
4
+ import { createLogger } from '@parity/product-sdk-logger';
5
+ import { submitAndWatch } from '@parity/product-sdk-tx';
6
+ import { Enum } from 'polkadot-api';
7
+ import { getPreimageManager } from '@parity/product-sdk-host';
8
+ import { CID } from 'multiformats/cid';
9
+ import * as Digest from 'multiformats/hashes/digest';
10
+ import { paseo_bulletin } from '@parity/product-sdk-descriptors/paseo-bulletin';
11
+
12
+ // src/client.ts
13
+ var ProductCloudStorageError = class extends Error {
14
+ constructor(message, options) {
15
+ super(message, options);
16
+ this.name = "ProductCloudStorageError";
17
+ }
18
+ };
19
+ var CloudStorageHostUnavailableError = class extends ProductCloudStorageError {
20
+ constructor(operation) {
21
+ super(
22
+ `Host preimage API unavailable for ${operation}. Ensure you are running inside a host container (Polkadot Browser / Desktop).`
23
+ );
24
+ this.name = "CloudStorageHostUnavailableError";
25
+ }
26
+ };
27
+ var CloudStorageLookupTimeoutError = class extends ProductCloudStorageError {
28
+ /** The CID that was being looked up. */
29
+ cid;
30
+ /** The timeout duration in milliseconds. */
31
+ timeoutMs;
32
+ constructor(cid, timeoutMs) {
33
+ super(`Host preimage lookup timed out after ${timeoutMs}ms for CID: ${cid}`);
34
+ this.name = "CloudStorageLookupTimeoutError";
35
+ this.cid = cid;
36
+ this.timeoutMs = timeoutMs;
37
+ }
38
+ };
39
+ var CloudStorageLookupInterruptedError = class extends ProductCloudStorageError {
40
+ /** The CID that was being looked up. */
41
+ cid;
42
+ constructor(cid) {
43
+ super(`Host preimage lookup was interrupted for CID: ${cid}`);
44
+ this.name = "CloudStorageLookupInterruptedError";
45
+ this.cid = cid;
46
+ }
47
+ };
48
+ var CloudStorageCidError = class extends ProductCloudStorageError {
49
+ /** The invalid CID string, if available. */
50
+ cid;
51
+ constructor(message, cid) {
52
+ super(message);
53
+ this.name = "CloudStorageCidError";
54
+ this.cid = cid;
55
+ }
56
+ };
57
+ var CloudStorageAuthorizationError = class extends ProductCloudStorageError {
58
+ /** The address that was being checked. */
59
+ address;
60
+ constructor(address, cause) {
61
+ super(`Failed to check authorization for ${address}`, { cause });
62
+ this.name = "CloudStorageAuthorizationError";
63
+ this.address = address;
64
+ }
65
+ };
66
+
67
+ // src/authorization.ts
68
+ var log = createLogger("cloudStorage");
69
+ var NOT_AUTHORIZED = Object.freeze({
70
+ authorized: false,
71
+ remainingTransactions: 0,
72
+ remainingBytes: 0n,
73
+ expiration: 0
74
+ });
75
+ async function checkAuthorization(api, address) {
76
+ let auth;
77
+ try {
78
+ auth = await api.query.TransactionStorage.Authorizations.getValue(
79
+ Enum("Account", address)
80
+ );
81
+ } catch (error) {
82
+ log.error("checkAuthorization: query failed", { address, error });
83
+ throw new CloudStorageAuthorizationError(address, error);
84
+ }
85
+ if (!auth) {
86
+ log.debug("checkAuthorization: no authorization found", { address });
87
+ return NOT_AUTHORIZED;
88
+ }
89
+ const status = {
90
+ authorized: true,
91
+ remainingTransactions: auth.extent.transactions_allowance - auth.extent.transactions,
92
+ // `auth` is `any` (TypedApi<any> upstream — see line 53). TS narrows
93
+ // `any - any` to `number`, so cast each u64 operand to bigint to keep
94
+ // the subtraction in bigint space. Runtime values are bigints from PAPI.
95
+ remainingBytes: auth.extent.bytes_allowance - auth.extent.bytes,
96
+ expiration: auth.expiration
97
+ };
98
+ log.debug("checkAuthorization", {
99
+ address,
100
+ remainingTransactions: status.remainingTransactions,
101
+ remainingBytes: status.remainingBytes.toString(),
102
+ expiration: status.expiration
103
+ });
104
+ return status;
105
+ }
106
+ async function authorizeAccount(api, who, transactions, bytes, signer, options = {}) {
107
+ const { viaSudo = false, waitFor, timeoutMs, onStatus } = options;
108
+ const apiTx = api.tx;
109
+ log.info("authorizeAccount: building extrinsic", {
110
+ who,
111
+ transactions,
112
+ bytes: bytes.toString(),
113
+ viaSudo
114
+ });
115
+ if (viaSudo && !apiTx.Sudo?.sudo) {
116
+ throw new ProductCloudStorageError(
117
+ "viaSudo: true requires the Sudo pallet, which is not available on this network. On production networks (Polkadot, Kusama), authorize_account requires governance or a different mechanism."
118
+ );
119
+ }
120
+ const authorizeTx = apiTx.TransactionStorage.authorize_account({
121
+ who,
122
+ transactions,
123
+ bytes
124
+ });
125
+ const txToSubmit = viaSudo ? apiTx.Sudo.sudo({ call: authorizeTx.decodedCall }) : authorizeTx;
126
+ const result = await submitAndWatch(txToSubmit, signer, {
127
+ waitFor,
128
+ timeoutMs,
129
+ onStatus
130
+ });
131
+ log.info("authorizeAccount: included in block", {
132
+ who,
133
+ blockHash: result.block.hash
134
+ });
135
+ return { blockHash: result.block.hash };
136
+ }
137
+ var HashAlgorithm = {
138
+ /** BLAKE2b-256 — chain default. */
139
+ Blake2b256: 45600,
140
+ /** SHA2-256. */
141
+ Sha2_256: 18,
142
+ /** Keccak-256 — Ethereum compatibility. */
143
+ Keccak256: 27
144
+ };
145
+ var CidCodec = {
146
+ /** Raw binary — default for single-chunk data. */
147
+ Raw: 85,
148
+ /** DAG-PB — used for multi-chunk manifests / IPFS UnixFS. */
149
+ DagPb: 112,
150
+ /** DAG-CBOR — alternative DAG encoding. */
151
+ DagCbor: 113
152
+ };
153
+ var SUPPORTED_HASH_CODES = new Set(Object.values(HashAlgorithm));
154
+ var SUPPORTED_CODEC_CODES = new Set(Object.values(CidCodec));
155
+ var EXPECTED_HEX_LENGTH = 66;
156
+ function hashToCid(hexHash, hashCode = HashAlgorithm.Blake2b256, codec = CidCodec.Raw) {
157
+ if (hexHash.length !== EXPECTED_HEX_LENGTH) {
158
+ throw new CloudStorageCidError(
159
+ `Expected a 0x-prefixed 32-byte hex hash (${EXPECTED_HEX_LENGTH} chars), got ${hexHash.length} chars`
160
+ );
161
+ }
162
+ if (!/^0x[0-9a-fA-F]{64}$/.test(hexHash)) {
163
+ throw new CloudStorageCidError(
164
+ `Invalid hash format: expected 0x-prefixed 32-byte hex string, got: ${hexHash}`
165
+ );
166
+ }
167
+ if (!SUPPORTED_HASH_CODES.has(hashCode)) {
168
+ throw new CloudStorageCidError(
169
+ `Unsupported hash algorithm 0x${hashCode.toString(16)}; expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`
170
+ );
171
+ }
172
+ if (!SUPPORTED_CODEC_CODES.has(codec)) {
173
+ throw new CloudStorageCidError(
174
+ `Unsupported CID codec 0x${codec.toString(16)}; expected one of: ${[...SUPPORTED_CODEC_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`
175
+ );
176
+ }
177
+ const digest = hexToBytes(hexHash);
178
+ return CID.createV1(codec, Digest.create(hashCode, digest)).toString();
179
+ }
180
+ function cidToPreimageKey(cid) {
181
+ let parsed;
182
+ try {
183
+ parsed = CID.parse(cid);
184
+ } catch {
185
+ throw new CloudStorageCidError(`Invalid CID: ${cid}`, cid);
186
+ }
187
+ if (parsed.version !== 1) {
188
+ throw new CloudStorageCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);
189
+ }
190
+ if (!SUPPORTED_HASH_CODES.has(parsed.multihash.code)) {
191
+ throw new CloudStorageCidError(
192
+ `Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}; expected one of: ${[...SUPPORTED_HASH_CODES].map((c) => `0x${c.toString(16)}`).join(", ")}`,
193
+ cid
194
+ );
195
+ }
196
+ return `0x${bytesToHex(parsed.multihash.digest)}`;
197
+ }
198
+ function hexToBytes(hex) {
199
+ const out = new Uint8Array(32);
200
+ for (let i = 0; i < 32; i++) {
201
+ out[i] = Number.parseInt(hex.slice(2 + i * 2, 4 + i * 2), 16);
202
+ }
203
+ return out;
204
+ }
205
+ function bytesToHex(bytes) {
206
+ let s = "";
207
+ for (let i = 0; i < bytes.length; i++) {
208
+ s += bytes[i].toString(16).padStart(2, "0");
209
+ }
210
+ return s;
211
+ }
212
+
213
+ // src/resolve-query.ts
214
+ var log2 = createLogger("bulletin");
215
+ var DEFAULT_LOOKUP_TIMEOUT_MS = 3e4;
216
+ async function resolveQueryStrategy() {
217
+ const preimageManager = await getPreimageManager();
218
+ if (preimageManager) {
219
+ log2.info("using host preimage lookup for Cloud Storage queries");
220
+ return {
221
+ kind: "host-lookup",
222
+ lookup: (cid, timeoutMs) => lookupViaHost(preimageManager, cid, timeoutMs)
223
+ };
224
+ }
225
+ throw new CloudStorageHostUnavailableError("query");
226
+ }
227
+ function lookupViaHost(manager, cid, timeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS) {
228
+ const key = cidToPreimageKey(cid);
229
+ return new Promise((resolve, reject) => {
230
+ const cleanup = () => {
231
+ cancelInterrupt();
232
+ sub.unsubscribe();
233
+ };
234
+ const settle = (fn) => {
235
+ if (timer === null) return;
236
+ clearTimeout(timer);
237
+ timer = null;
238
+ cleanup();
239
+ fn();
240
+ };
241
+ let timer = setTimeout(() => {
242
+ settle(() => {
243
+ reject(new CloudStorageLookupTimeoutError(cid, timeoutMs));
244
+ });
245
+ }, timeoutMs);
246
+ const sub = manager.lookup(key, (preimage) => {
247
+ if (preimage !== null) {
248
+ settle(() => resolve(preimage));
249
+ }
250
+ });
251
+ const cancelInterrupt = sub.onInterrupt(() => {
252
+ settle(() => {
253
+ reject(new CloudStorageLookupInterruptedError(cid));
254
+ });
255
+ });
256
+ });
257
+ }
258
+
259
+ // src/query.ts
260
+ var log3 = createLogger("bulletin");
261
+ async function queryBytes(cid, options) {
262
+ const strategy = await resolveQueryStrategy();
263
+ return executeQuery(strategy, cid, options);
264
+ }
265
+ async function queryJson(cid, options) {
266
+ const bytes = await queryBytes(cid, options);
267
+ return JSON.parse(new TextDecoder().decode(bytes));
268
+ }
269
+ async function executeQuery(strategy, cid, options) {
270
+ log3.info("query: host preimage lookup", { cid });
271
+ const bytes = await strategy.lookup(cid, options?.lookupTimeoutMs);
272
+ if (options?.noReassemble) return bytes;
273
+ const parsed = parseCid(cid);
274
+ if (parsed.code !== CidCodec$1.DagPb) return bytes;
275
+ log3.info("query: reassembling DAG-PB manifest", { cid });
276
+ const builder = new UnixFsDagBuilder();
277
+ const { chunkCids } = await builder.parse(bytes);
278
+ const chunks = await Promise.all(
279
+ chunkCids.map((c) => strategy.lookup(c.toString(), options?.lookupTimeoutMs))
280
+ );
281
+ let total = 0;
282
+ for (const chunk of chunks) total += chunk.length;
283
+ const out = new Uint8Array(total);
284
+ let offset = 0;
285
+ for (const chunk of chunks) {
286
+ out.set(chunk, offset);
287
+ offset += chunk.length;
288
+ }
289
+ return out;
290
+ }
291
+ var HASH_CODE_TO_ENUM_TYPE = {
292
+ [HashAlgorithm.Blake2b256]: "Blake2b256",
293
+ [HashAlgorithm.Sha2_256]: "Sha2_256",
294
+ [HashAlgorithm.Keccak256]: "Keccak256"
295
+ };
296
+ async function verifyStored(api, cid, options) {
297
+ const parsed = parseCidForVerify(cid);
298
+ const queryFn = api.query?.TransactionStorage?.Transactions?.getValue;
299
+ if (!queryFn) {
300
+ throw new Error(
301
+ "CloudStorage API does not expose query.TransactionStorage.Transactions \u2014 the typed API may be incomplete or the runtime version doesn't match the descriptor."
302
+ );
303
+ }
304
+ const entries = await queryFn(options.block);
305
+ if (!entries || entries.length === 0) return null;
306
+ if (options.index !== void 0) {
307
+ const entry = entries[options.index];
308
+ if (entry && matchesEntry(entry, parsed)) {
309
+ return {
310
+ block: options.block,
311
+ index: options.index,
312
+ size: entry.size,
313
+ blockChunks: entry.block_chunks
314
+ };
315
+ }
316
+ return null;
317
+ }
318
+ for (let i = 0; i < entries.length; i++) {
319
+ const entry = entries[i];
320
+ if (matchesEntry(entry, parsed)) {
321
+ return {
322
+ block: options.block,
323
+ index: i,
324
+ size: entry.size,
325
+ blockChunks: entry.block_chunks
326
+ };
327
+ }
328
+ }
329
+ return null;
330
+ }
331
+ function parseCidForVerify(cid) {
332
+ let parsed;
333
+ try {
334
+ parsed = CID.parse(cid);
335
+ } catch {
336
+ throw new CloudStorageCidError(`Invalid CID: ${cid}`, cid);
337
+ }
338
+ if (parsed.version !== 1) {
339
+ throw new CloudStorageCidError(`Expected CIDv1, got CIDv${parsed.version}`, cid);
340
+ }
341
+ const hashType = HASH_CODE_TO_ENUM_TYPE[parsed.multihash.code];
342
+ if (!hashType) {
343
+ throw new CloudStorageCidError(
344
+ `Unsupported hash algorithm 0x${parsed.multihash.code.toString(16)}`,
345
+ cid
346
+ );
347
+ }
348
+ return { digest: parsed.multihash.digest, hashType };
349
+ }
350
+ function matchesEntry(entry, target) {
351
+ if (entry.hashing.type !== target.hashType) return false;
352
+ const onChainBytes = entry.content_hash instanceof Uint8Array ? entry.content_hash : entry.content_hash.asBytes();
353
+ if (onChainBytes.length !== target.digest.length) return false;
354
+ for (let i = 0; i < onChainBytes.length; i++) {
355
+ if (onChainBytes[i] !== target.digest[i]) return false;
356
+ }
357
+ return true;
358
+ }
359
+
360
+ // src/client.ts
361
+ var log4 = createLogger("bulletin");
362
+ var CloudStorageClient = class _CloudStorageClient {
363
+ /** Underlying upstream client — exposed for power users. */
364
+ inner;
365
+ /** Typed CloudStorage API. */
366
+ api;
367
+ /** Lazy-resolved host-preimage query strategy, cached for the client lifetime. */
368
+ queryStrategyPromise = null;
369
+ /** Constructed via {@link create} or {@link from}. */
370
+ constructor(inner, api) {
371
+ this.inner = inner;
372
+ this.api = api;
373
+ }
374
+ /** Resolve and cache the host query strategy on first use. */
375
+ resolveQuery() {
376
+ if (!this.queryStrategyPromise) {
377
+ this.queryStrategyPromise = resolveQueryStrategy();
378
+ }
379
+ return this.queryStrategyPromise;
380
+ }
381
+ /**
382
+ * Create a client from an environment shorthand or an explicit network.
383
+ *
384
+ * Environment form uses our `getChainAPI(env)` to resolve the typed API.
385
+ * Explicit form skips the environment lookup and lets you pass any
386
+ * genesis/descriptor combo.
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * // Shorthand
391
+ * const client = await CloudStorageClient.create({ environment: "paseo", signer });
392
+ *
393
+ * // Explicit (custom network)
394
+ * const client = await CloudStorageClient.create({
395
+ * ...CloudStorageNetworks.paseo,
396
+ * signer,
397
+ * config: { defaultChunkSize: 1 << 20 },
398
+ * });
399
+ * ```
400
+ */
401
+ static async create(options) {
402
+ if ("environment" in options) {
403
+ const chain2 = await getChainAPI(options.environment);
404
+ const inner2 = new AsyncBulletinClient(
405
+ chain2.bulletin,
406
+ options.signer,
407
+ chain2.raw.bulletin.submit,
408
+ options.config,
409
+ () => chain2.destroy()
410
+ );
411
+ log4.info("CloudStorageClient created (environment shorthand)", {
412
+ environment: options.environment
413
+ });
414
+ return new _CloudStorageClient(inner2, chain2.bulletin);
415
+ }
416
+ const { genesisHash, descriptor, signer, config } = options;
417
+ if (descriptor.genesis && genesisHash.toLowerCase() !== descriptor.genesis.toLowerCase()) {
418
+ throw new Error(
419
+ `CloudStorageClient.create: genesisHash (${genesisHash}) does not match descriptor.genesis (${descriptor.genesis}). These must refer to the same network \u2014 check that you're pairing the right descriptor with the right genesis hash.`
420
+ );
421
+ }
422
+ const chain = await createChainClient({
423
+ chains: { bulletin: descriptor },
424
+ rpcs: { bulletin: [] }
425
+ });
426
+ const inner = new AsyncBulletinClient(
427
+ chain.bulletin,
428
+ signer,
429
+ chain.raw.bulletin.submit,
430
+ config,
431
+ () => chain.destroy()
432
+ );
433
+ log4.info("CloudStorageClient created (explicit network)");
434
+ return new _CloudStorageClient(inner, chain.bulletin);
435
+ }
436
+ /**
437
+ * Construct from a pre-built `AsyncBulletinClient` and PAPI typed API.
438
+ *
439
+ * Use this when you already own the connection lifecycle (BYOD setups,
440
+ * tests). The caller is responsible for calling `papiClient.destroy()`
441
+ * — this client's {@link destroy} only tears down the upstream's
442
+ * `onDestroy` hook.
443
+ */
444
+ static from(inner, api) {
445
+ return new _CloudStorageClient(inner, api);
446
+ }
447
+ // ─── Upload + authorization (forwarded to upstream) ────────────────
448
+ /** Build a store transaction. See upstream `StoreBuilder` for chained options. */
449
+ store(data) {
450
+ return this.inner.store(data);
451
+ }
452
+ /** Authorize an account to store data on the chain (sudo required on most networks). */
453
+ authorizeAccount(who, transactions, bytes) {
454
+ return this.inner.authorizeAccount(who, transactions, bytes);
455
+ }
456
+ /** Authorize content storage by hash (anyone can store; no fees). */
457
+ authorizePreimage(contentHash, maxSize) {
458
+ return this.inner.authorizePreimage(contentHash, maxSize);
459
+ }
460
+ /** Renew a stored transaction by block + index. */
461
+ renew(block, index) {
462
+ return this.inner.renew(block, index);
463
+ }
464
+ /** Estimate the authorization (transactions + bytes) needed for `dataSize`. */
465
+ estimateAuthorization(dataSize) {
466
+ return this.inner.estimateAuthorization(dataSize);
467
+ }
468
+ // ─── Read side (our own helpers) ───────────────────────────────────
469
+ /**
470
+ * Fetch raw bytes for a CID via the host's preimage lookup.
471
+ *
472
+ * Container-only — outside a Polkadot Browser / Desktop host this
473
+ * throws {@link CloudStorageHostUnavailableError}. The chain stores
474
+ * content metadata (`content_hash`, size, codec) but the bytes
475
+ * themselves are surfaced through the host's preimage subscription.
476
+ *
477
+ * Use {@link verifyStored} if you only need to confirm a CID was
478
+ * recorded on-chain (no byte fetch).
479
+ */
480
+ async fetchBytes(cid, options) {
481
+ const strategy = await this.resolveQuery();
482
+ return executeQuery(strategy, cid, options);
483
+ }
484
+ /** Fetch and parse JSON for a CID. */
485
+ async fetchJson(cid, options) {
486
+ const bytes = await this.fetchBytes(cid, options);
487
+ return JSON.parse(new TextDecoder().decode(bytes));
488
+ }
489
+ /** Pre-flight: check whether `address` can store via Cloud Storage. */
490
+ async checkAuthorization(address) {
491
+ return checkAuthorization(this.api, address);
492
+ }
493
+ /**
494
+ * Verify that a CID was recorded on-chain at the given block.
495
+ *
496
+ * Common pattern: pass `blockNumber` (and optionally `extrinsicIndex`)
497
+ * from a `store(...).send()` receipt to confirm the upload landed.
498
+ * See {@link verifyStored} for details.
499
+ */
500
+ async verifyStored(cid, options) {
501
+ return verifyStored(this.api, cid, options);
502
+ }
503
+ /** Tear down the underlying connection. */
504
+ async destroy() {
505
+ await this.inner.destroy();
506
+ }
507
+ };
508
+ var CloudStorageNetworks = {
509
+ paseo: {
510
+ genesisHash: "0x8cfe6717dc4becfda2e13c488a1e2061ff2dfee96e7d031157f72d36716c0a22",
511
+ descriptor: paseo_bulletin
512
+ }
513
+ };
514
+
515
+ // src/lazy-signer.ts
516
+ function createLazySigner(getSigner, onMissing = "No signer available \u2014 connect a wallet and select an account first.") {
517
+ const resolve = () => {
518
+ const inner = getSigner();
519
+ if (!inner) throw new Error(onMissing);
520
+ return inner;
521
+ };
522
+ const lazy = {
523
+ get publicKey() {
524
+ return resolve().publicKey;
525
+ },
526
+ signTx: async (...args) => resolve().signTx(...args),
527
+ signBytes: async (...args) => resolve().signBytes(...args)
528
+ };
529
+ return lazy;
530
+ }
531
+
532
+ export { CidCodec, CloudStorageAuthorizationError, CloudStorageCidError, CloudStorageClient, CloudStorageHostUnavailableError, CloudStorageLookupInterruptedError, CloudStorageLookupTimeoutError, CloudStorageNetworks, HashAlgorithm, ProductCloudStorageError, authorizeAccount, checkAuthorization, cidToPreimageKey, createLazySigner, executeQuery, hashToCid, queryBytes, queryJson, resolveQueryStrategy, verifyStored };
533
+ //# sourceMappingURL=index.js.map
534
+ //# sourceMappingURL=index.js.map