@dstorage-tech/dstorage-sdk 0.0.5 → 0.0.6

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.mjs CHANGED
@@ -100,9 +100,6 @@ var CoreErrorCode = {
100
100
  // removeReference — 10070-10079
101
101
  REMOVE_REF_REQUIRES_CHAIN: 10070,
102
102
  REMOVE_REF_NOT_SUPPORTED: 10071,
103
- // executeMetaTransaction / prepareUploadCrypto — 10080-10089
104
- META_TX_REQUIRES_ENCRYPTION: 10080,
105
- PREPARE_UPLOAD_NO_PROVIDERS: 10081,
106
103
  // manifest / chunking — 10090-10099
107
104
  MANIFEST_INVALID_STRUCTURE: 10090,
108
105
  MANIFEST_CHUNK_COUNT_MISMATCH: 10091,
@@ -112,8 +109,6 @@ var CoreErrorCode = {
112
109
  REASSEMBLY_SIZE_MISMATCH: 10095,
113
110
  // init state — 10100-10109
114
111
  NOT_INITIALIZED: 10100,
115
- // MetaTx — 10200-10209
116
- META_TX_UNKNOWN_STEP: 10200,
117
112
  // generic catch-all — 10999
118
113
  UNKNOWN_ERROR: 10999
119
114
  };
@@ -134,7 +129,7 @@ function isStorePartialError(err) {
134
129
  // package.json
135
130
  var package_default = {
136
131
  name: "@dstorage-tech/dstorage-sdk",
137
- version: "0.0.5",
132
+ version: "0.0.6",
138
133
  description: "Privacy-first decentralized storage SDK with pluggable adapters",
139
134
  license: "MIT",
140
135
  homepage: "https://dstorage.pro",
@@ -221,2382 +216,2032 @@ var package_default = {
221
216
  }
222
217
  };
223
218
 
224
- // src/metaTx/utils.ts
225
- function defaultChainToken(chainProvider) {
226
- switch (chainProvider) {
227
- case "midnight":
228
- return "DUST";
229
- default:
230
- return "MOCK";
231
- }
232
- }
233
- function buildInitialSteps() {
234
- return [
235
- {
236
- id: "estimate",
237
- label: "Estimate Costs",
238
- description: "Calculate storage fee + chain transaction fee",
239
- status: "pending"
240
- },
241
- {
242
- id: "encrypt",
243
- label: "Encrypt File",
244
- description: "Client side encryption using your wallet key",
245
- status: "pending"
246
- },
247
- {
248
- id: "payment_storage",
249
- label: "Storage Payment",
250
- description: "Pay storage network fee",
251
- status: "pending"
252
- },
253
- {
254
- id: "upload",
255
- label: "Upload to Storage",
256
- description: "Send encrypted file to decentralised network",
257
- status: "pending"
258
- },
259
- {
260
- id: "chain_reference",
261
- label: "Write On-Chain Reference",
262
- description: "Store file pointer + metadata on blockchain",
263
- status: "pending"
264
- },
265
- {
266
- id: "complete",
267
- label: "Complete",
268
- description: "Meta-transaction finalised",
269
- status: "pending"
270
- }
271
- ];
272
- }
273
- function truncateId(id) {
274
- return id.length > 20 ? `${id.slice(0, 10)}\u2026${id.slice(-6)}` : id;
275
- }
276
- async function readFileAsBytes(file) {
277
- return new Promise((resolve, reject) => {
278
- const reader = new FileReader();
279
- reader.onload = () => resolve(new Uint8Array(reader.result));
280
- reader.onerror = () => reject(new Error("Failed to read file."));
281
- reader.readAsArrayBuffer(file);
282
- });
219
+ // src/chunked.ts
220
+ var CHUNK_SIZE = 10 * 1024 * 1024;
221
+ var MAX_CHUNK_COUNT = 1e4;
222
+ var MAX_REASSEMBLY_BYTES = MAX_CHUNK_COUNT * CHUNK_SIZE;
223
+ function isManifest(payload) {
224
+ if (typeof payload !== "object" || payload === null) return false;
225
+ const m = payload;
226
+ if (m["type"] !== "dstorage-chunked-manifest") return false;
227
+ return Number.isInteger(m["totalSize"]) && m["totalSize"] >= 0 && m["totalSize"] <= MAX_REASSEMBLY_BYTES && Number.isInteger(m["chunkSize"]) && m["chunkSize"] > 0 && Number.isInteger(m["chunkCount"]) && m["chunkCount"] >= 0 && m["chunkCount"] <= MAX_CHUNK_COUNT;
283
228
  }
284
229
 
285
- // src/metaTx/MetaTx.ts
286
- var MetaTx = class {
287
- constructor(sdk) {
288
- this.progressCallback = null;
289
- this.steps = buildInitialSteps();
290
- this.startedAt = 0;
291
- this.sdk = sdk;
292
- }
293
- /** Register a progress listener. Called after every step state change. */
294
- onProgress(cb) {
295
- this.progressCallback = cb;
296
- return this;
297
- }
298
- // ── Public: execute the full meta-transaction ─────────────────────────────
299
- async execute(file, config = {}) {
300
- this.startedAt = Date.now();
301
- this.steps = buildInitialSteps();
302
- this.emit({ overallStatus: "running" });
230
+ // src/dStorage.ts
231
+ var TAG_SDK_VERSION = "dStorage-Version";
232
+ var TAG_CONTENT_TYPE = "Content-Type";
233
+ var TAG_CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
234
+ var TAG_CONTENT_TYPE_JSON = "application/json";
235
+ var TAG_CHUNK_INDEX = "dStorage-Chunk-Index";
236
+ var TAG_CHUNK_TOTAL = "dStorage-Chunk-Total";
237
+ var TAG_MANIFEST_UPLOAD = "dStorage-Manifest-Upload";
238
+ function wrapError(err) {
239
+ if (err instanceof DStorageError || isDStorageError(err)) throw err;
240
+ throw new DStorageError(
241
+ CoreErrorCode.UNKNOWN_ERROR,
242
+ `[dStorage] Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
243
+ { cause: err }
244
+ );
245
+ }
246
+ var DStorage = class {
247
+ constructor(config) {
248
+ this.wallet = null;
249
+ /** Encryption adapters — each wraps/unwraps per-upload DEKs using its own algorithm. */
250
+ this._encryptionProviders = [];
251
+ this._initialized = false;
252
+ this.config = config;
253
+ this.logger = config.logger;
254
+ }
255
+ // ── Public accessors ─────────────────────────────────────────────────────
256
+ get storageAdapter() {
257
+ return this.config.storageAdapter;
258
+ }
259
+ get chainAdapter() {
260
+ return this.config.chainAdapter;
261
+ }
262
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
263
+ /**
264
+ * Initialize the SDK:
265
+ * 1. Initialize the chain adapter wallet (if supported), and
266
+ * deploy a new DataRegistry contract, or connect to an existing one.
267
+ * 2. Derive an encryption key from the chain adapter's credentials (if supported).
268
+ *
269
+ * Returns the address of the DataRegistry contract in use.
270
+ * Call this instead of connect() when using MidnightChainAdapter directly.
271
+ */
272
+ async init() {
303
273
  try {
304
- this.startStep("estimate", "Analysing file and fetching price feeds\u2026");
305
- const estimate = await this.sdk.estimateCost(file.size);
306
- this.completeStep(
307
- "estimate",
308
- estimate.chainCost ? `${estimate.storageCost.amount} ${estimate.storageCost.token} + ${estimate.chainCost.amount} ${estimate.chainCost.token}` : `${estimate.storageCost.amount} ${estimate.storageCost.token} (storage only)`
309
- );
310
- this.emit({ overallStatus: "running", estimate });
311
- this.startStep("encrypt", "Reading file bytes\u2026");
312
- const fileBytes = await readFileAsBytes(file);
313
- this.updateStepDetail("encrypt", "Encrypting with XChaCha20-Poly1305\u2026");
314
- const uploadCrypto = await this.sdk.prepareUploadCrypto();
315
- const uploadData = uploadCrypto.uploadScheme ? await uploadCrypto.uploadScheme.encryptPayload(fileBytes, PAYLOAD_AAD) : fileBytes;
316
- const fileSizeMB = file.size / (1024 * 1024);
317
- const fileSizeDisplay = fileSizeMB < 1.1 ? `${(file.size / 1024).toFixed(2)} KB` : `${fileSizeMB.toFixed(2)} MB`;
318
- this.completeStep(
319
- "encrypt",
320
- `${fileSizeDisplay} encrypted (${(uploadData.byteLength / 1024).toFixed(2)} KB ciphertext)`
321
- );
322
- const uploadMetadata = {
323
- "dstorage-version": "0.1.0",
324
- "dstorage-filename": file.name,
325
- "dstorage-filetype": file.type,
326
- "dstorage-filesize": String(file.size),
327
- ...config.metadata ?? {}
328
- };
329
- if (this.sdk.storageAdapter.paymentAdapter.provideUploadData) {
330
- this.sdk.storageAdapter.paymentAdapter.provideUploadData(
331
- uploadData,
332
- uploadMetadata
274
+ if (this.config.chainAdapter?.init) {
275
+ await this.config.chainAdapter.init();
276
+ }
277
+ this._encryptionProviders = this.config.encryptionAdapters ?? [];
278
+ this._initialized = true;
279
+ return this.config.chainAdapter?.getContractAddress();
280
+ } catch (err) {
281
+ wrapError(err);
282
+ }
283
+ }
284
+ /** Tear down active state and clear all in-memory key material. */
285
+ destroy() {
286
+ this.wallet = null;
287
+ this._encryptionProviders = [];
288
+ this._initialized = false;
289
+ this.logger?.log(
290
+ "dStorage",
291
+ "Destroyed \u2014 encryption key cleared from memory."
292
+ );
293
+ }
294
+ get isConnected() {
295
+ return this._initialized || this.wallet !== null;
296
+ }
297
+ get connectedAddress() {
298
+ return this.wallet?.address ?? null;
299
+ }
300
+ // ── Core operations ───────────────────────────────────────────────────────
301
+ /**
302
+ * Encrypt content client-side, store to decentralised storage,
303
+ * and write a cryptographic reference on-chain.
304
+ *
305
+ * Files larger than CHUNK_SIZE (10 MB) are automatically split into chunks,
306
+ * each encrypted and stored independently. A manifest file ties them together.
307
+ * Only the manifest's storageId is written on-chain.
308
+ *
309
+ * @param bytes Uint8Array to store
310
+ * @param options Optional StoreOptions (metadata, onProgress, isPublic, refId)
311
+ * @returns StoreResult containing the chainRefId/storageId needed to retrieve later
312
+ */
313
+ async store(bytes, options) {
314
+ try {
315
+ this.assertConnected();
316
+ const {
317
+ tags = {},
318
+ metadata: rawMeta,
319
+ onProgress,
320
+ isPublic = false,
321
+ refId
322
+ } = options ?? {};
323
+ if (rawMeta !== void 0 && isPublic) {
324
+ throw new DStorageError(
325
+ CoreErrorCode.METADATA_WITH_PUBLIC_UPLOAD,
326
+ "[dStorage] metadata cannot be used with isPublic uploads \u2014 there is no DEK to encrypt it with."
333
327
  );
334
328
  }
335
- this.startStep(
336
- "payment_storage",
337
- `Preparing ${estimate.storageCost.amount} ${estimate.storageCost.token} payment\u2026`
338
- );
339
- const storageReceipt = await this.sdk.storageAdapter.paymentAdapter.pay(
340
- {
341
- network: this.sdk.storageAdapter.name,
342
- dataSizeBytes: uploadData.byteLength,
343
- expiresAt: Date.now() + 6e5
344
- },
345
- (msg) => this.updateStepDetail("payment_storage", msg)
346
- );
347
- this.completeStep(
348
- "payment_storage",
349
- `${storageReceipt.amount} ${storageReceipt.token}`
350
- );
351
- this.startStep("upload", `Uploading to ${this.sdk.storageAdapter.name}\u2026`);
352
- const uploadResult = await this.sdk.storageAdapter.store(
353
- uploadData,
354
- uploadMetadata
355
- );
356
- this.completeStep(
357
- "upload",
358
- `Stored on ${uploadResult.provider} \u2192 ${truncateId(uploadResult.id)}`
359
- );
360
- let chainRefId;
361
- let chainPayment;
362
- let chainToken;
363
- if (this.sdk.chainAdapter) {
364
- this.startStep(
365
- "chain_reference",
366
- `Fee payment and writing reference to ${this.sdk.chainAdapter.name}\u2026`
329
+ if (bytes.length > CHUNK_SIZE) {
330
+ return this._uploadChunked(
331
+ bytes,
332
+ tags,
333
+ onProgress,
334
+ isPublic,
335
+ rawMeta,
336
+ refId
337
+ );
338
+ }
339
+ let uploadData;
340
+ let onChainStorageId;
341
+ const encryptionScheme = isPublic ? "" : "xchacha20poly1305-v1";
342
+ let uploadScheme = null;
343
+ let keyEnvelope = "";
344
+ let dek = null;
345
+ let ownerSecret = new Uint8Array(0);
346
+ if (isPublic) {
347
+ uploadData = bytes;
348
+ } else {
349
+ if (this._encryptionProviders.length === 0) {
350
+ throw new DStorageError(
351
+ CoreErrorCode.UPLOAD_NO_PROVIDERS,
352
+ "[dStorage] No encryption providers available. Call init() before uploading."
353
+ );
354
+ }
355
+ dek = generateDek();
356
+ uploadScheme = XChaChaScheme.fromKey(dek);
357
+ this.logger?.log(
358
+ "dStorage",
359
+ `Encrypting content using scheme: ${encryptionScheme}`
360
+ );
361
+ uploadData = await uploadScheme.encryptPayload(bytes, PAYLOAD_AAD);
362
+ const wrapperEntries = await Promise.all(
363
+ this._encryptionProviders.map(async (provider) => ({
364
+ wrapKeyName: provider.name,
365
+ ...await provider.wrapDek(dek)
366
+ }))
367
367
  );
368
- const onChainStorageId = uploadCrypto.uploadScheme ? await uploadCrypto.uploadScheme.encryptStorageId(uploadResult.id) : uploadResult.id;
369
- const { deriveOwnerSecret: deriveOwnerSecret2 } = await import("./dist-INTAFXQ7.mjs");
370
- let ownerSecret;
371
- if (uploadCrypto.dek) {
372
- ownerSecret = await deriveOwnerSecret2(
373
- uploadCrypto.dek,
374
- new TextEncoder().encode(uploadResult.id)
368
+ keyEnvelope = serializeKeyEnvelope(buildKeyEnvelope(wrapperEntries));
369
+ }
370
+ const sdkVersion = package_default.version ?? "unknown";
371
+ const storageResult = await this.config.storageAdapter.store(uploadData, {
372
+ ...tags,
373
+ [TAG_SDK_VERSION]: sdkVersion,
374
+ [TAG_CONTENT_TYPE]: TAG_CONTENT_TYPE_OCTET_STREAM
375
+ });
376
+ if (isPublic) {
377
+ if (this.config.chainAdapter) {
378
+ if (this._encryptionProviders.length === 0) {
379
+ throw new DStorageError(
380
+ CoreErrorCode.PUBLIC_UPLOAD_REQUIRES_SEED_PROVIDER,
381
+ "[dStorage] Cannot store public data without at least one encryption seed provider. Without a provider there is no owner secret, so the reference could never be removed or updated."
382
+ );
383
+ }
384
+ const ownerSeed = generateDek();
385
+ const ownerSeedWrappers = await Promise.all(
386
+ this._encryptionProviders.map(async (provider) => ({
387
+ wrapKeyName: provider.name,
388
+ ...await provider.wrapDek(ownerSeed)
389
+ }))
375
390
  );
376
- } else {
377
- ownerSecret = await deriveOwnerSecret2(
378
- uploadCrypto.ownerSeed,
379
- new TextEncoder().encode(uploadResult.id),
391
+ keyEnvelope = serializeKeyEnvelope(
392
+ buildKeyEnvelope(ownerSeedWrappers)
393
+ );
394
+ ownerSecret = await deriveOwnerSecret(
395
+ ownerSeed,
396
+ new TextEncoder().encode(storageResult.id),
380
397
  "dstorage:owner-secret:public:v1"
381
398
  );
382
399
  }
383
- const chainResult = await this.sdk.chainAdapter.writeReference({
384
- storageId: onChainStorageId,
385
- encryptionScheme: uploadCrypto.encryptionScheme,
386
- storageProvider: uploadResult.provider,
387
- writtenAt: Date.now(),
388
- keyEnvelope: uploadCrypto.keyEnvelope,
389
- ownerSecret
390
- });
391
- const finalChainReceipt = chainResult.paymentReceipt ?? {
392
- amount: "<unknown>",
393
- token: estimate.chainCost.token,
394
- txHash: "",
395
- type: "chain",
396
- paidAt: 0,
397
- serviceFee: "0"
398
- };
399
- chainRefId = chainResult.refId;
400
- chainPayment = finalChainReceipt;
401
- chainToken = finalChainReceipt.token;
402
- this.completeStep(
403
- "chain_reference",
404
- `Reference written \u2192 ${finalChainReceipt.amount} ${finalChainReceipt.token} \u2192 ${truncateId(chainResult.refId)}`
405
- );
400
+ onChainStorageId = storageResult.id;
406
401
  } else {
407
- this.completeStep(
408
- "chain_reference",
409
- "Skipped \u2014 no chain adapter configured"
402
+ onChainStorageId = await uploadScheme.encryptStorageId(
403
+ storageResult.id
404
+ );
405
+ ownerSecret = await deriveOwnerSecret(
406
+ dek,
407
+ new TextEncoder().encode(storageResult.id)
410
408
  );
411
409
  }
412
- const dataId = chainRefId ? `${uploadResult.id}::${chainRefId}` : uploadResult.id;
413
- const totalDurationMs = Date.now() - this.startedAt;
414
- this.completeStep(
415
- "complete",
416
- `Completed in ${(totalDurationMs / 1e3).toFixed(1)}s`
417
- );
418
- const receipt = {
419
- dataId,
420
- storageId: uploadResult.id,
421
- ...chainRefId ? { chainRefId } : {},
422
- file: {
423
- name: file.name,
424
- type: file.type,
425
- sizeBytes: file.size
426
- },
427
- storagePayment: storageReceipt,
428
- ...chainPayment ? { chainPayment, chainToken } : {},
429
- storageToken: storageReceipt.token,
430
- ...config.contractAddress ? {
431
- contract: {
432
- address: config.contractAddress,
433
- name: config.contractName ?? config.contractAddress
434
- }
435
- } : {},
436
- completedAt: Date.now(),
437
- totalDurationMs
410
+ const storageCost = {
411
+ amount: storageResult.cost?.amount ?? "unknown",
412
+ token: storageResult.cost?.token ?? "AR"
438
413
  };
439
- this.emit({ overallStatus: "complete", estimate, receipt });
440
- return receipt;
441
- } catch (err) {
442
- const message = err instanceof Error ? err.message : String(err);
443
- const runningStep = this.steps.find((s) => s.status === "running");
444
- if (runningStep) {
445
- runningStep.status = "error";
446
- runningStep.detail = message;
447
- }
448
- this.emit({ overallStatus: "error", error: message });
449
- throw err;
450
- }
451
- }
452
- // ── Private step helpers ──────────────────────────────────────────────────
453
- startStep(id, detail) {
454
- const step = this.getStep(id);
455
- step.status = "running";
456
- step.detail = detail;
457
- step.startedAt = Date.now();
458
- this.emit({ overallStatus: "running", currentStepId: id });
459
- }
460
- updateStepDetail(id, detail) {
461
- const step = this.getStep(id);
462
- step.detail = detail;
463
- this.emit({ overallStatus: "running", currentStepId: id });
464
- }
465
- completeStep(id, detail) {
466
- const step = this.getStep(id);
467
- step.status = "done";
468
- step.detail = detail;
469
- if (step.startedAt) step.durationMs = Date.now() - step.startedAt;
470
- this.emit({ overallStatus: "running", currentStepId: id });
471
- }
472
- getStep(id) {
473
- const step = this.steps.find((s) => s.id === id);
474
- if (!step)
475
- throw new DStorageError(
476
- CoreErrorCode.META_TX_UNKNOWN_STEP,
477
- `[dStorage] Unknown meta-transaction step: ${id}`
478
- );
479
- return step;
480
- }
481
- emit(overrides) {
482
- if (!this.progressCallback) return;
483
- const currentRunning = this.steps.find((s) => s.status === "running");
484
- this.progressCallback({
485
- steps: [...this.steps.map((s) => ({ ...s }))],
486
- currentStepId: currentRunning?.id ?? overrides.currentStepId ?? null,
487
- overallStatus: "running",
488
- ...overrides
489
- });
490
- }
491
- };
492
-
493
- // ../payment/dist/chunk-IVL4WQ5Y.mjs
494
- var TOKENS = {
495
- AR: { symbol: "AR", name: "Arweave", decimals: 12 },
496
- DUST: { symbol: "DUST", name: "Midnight", decimals: 6 },
497
- MOCK: {
498
- symbol: "MOCK",
499
- name: "Mock Token",
500
- decimals: 6
501
- }
502
- };
503
-
504
- // ../payment/dist/chunk-R6FT3AV4.mjs
505
- var MockPaymentAdapter = class {
506
- constructor(config) {
507
- this.network = "mock";
508
- this.token = config.token ?? "MOCK";
509
- this.type = config.type;
510
- this.quoteValidityMs = config.quoteValidityMs ?? 6e5;
511
- this.logger = config.logger;
512
- }
513
- async estimateCost(dataSizeBytes) {
514
- const kb = dataSizeBytes / 1024;
515
- const amount = Math.max(1e-4, kb * 1e-3).toFixed(6);
516
- this.logger?.log(
517
- "dStorage/mock-payment",
518
- `Estimated cost: ${amount} ${this.token}`
519
- );
520
- return {
521
- token: this.token,
522
- amount,
523
- network: this.network,
524
- type: this.type,
525
- dataSizeBytes,
526
- expiresAt: Date.now() + this.quoteValidityMs
527
- };
528
- }
529
- async pay(instruction, onStatus) {
530
- const tokenName = TOKENS[this.token]?.name ?? this.token;
531
- onStatus?.(`Requesting ${tokenName} wallet signature\u2026`);
532
- onStatus?.(`Broadcasting ${this.type} payment transaction\u2026`);
533
- onStatus?.("Awaiting confirmation\u2026");
534
- const txHash = generateSimulatedTxHash();
535
- const { amount } = await this.estimateCost(instruction.dataSizeBytes);
536
- this.logger?.log(
537
- "dStorage/mock-payment",
538
- `Simulated ${this.type} payment \u2192 txHash: ${txHash}`
539
- );
540
- return {
541
- txHash,
542
- token: this.token,
543
- amount,
544
- type: this.type,
545
- paidAt: Date.now(),
546
- serviceFee: "0"
547
- };
548
- }
549
- };
550
- function generateSimulatedTxHash() {
551
- const hex = Array.from(
552
- { length: 64 },
553
- () => Math.floor(Math.random() * 16).toString(16)
554
- ).join("");
555
- return `0x${hex}`;
556
- }
557
-
558
- // ../payment/dist/index.mjs
559
- import { blake3 } from "@noble/hashes/blake3.js";
560
- var PaymentErrorCode = {
561
- // adapters/arweave.ts (14000–14099)
562
- ARWEAVE_NOT_INSTALLED: 14001,
563
- ARWEAVE_WALLET_KEY_REQUIRED: 14002,
564
- ARWEAVE_CONNECT_NOT_FOUND: 14003,
565
- ARWEAVE_QUOTE_EXPIRED: 14004,
566
- // adapters/managed.ts (14100–14299)
567
- MANAGED_NON_OBJECT_RESPONSE: 14100,
568
- MANAGED_SERVER_FAILURE: 14101,
569
- MANAGED_MISSING_FIELD: 14102,
570
- MANAGED_PUBLIC_KEY_MISMATCH: 14103,
571
- MANAGED_TOKEN_REQUIRED: 14104,
572
- MANAGED_QUOTE_EXPIRED: 14105,
573
- MANAGED_UNBALANCED_TX: 14106,
574
- MANAGED_SERVER_UNREACHABLE: 14107,
575
- MANAGED_KEY_CHANGED: 14108,
576
- MANAGED_SIGN_HTTP_ERROR: 14109,
577
- MANAGED_NON_JSON_RESPONSE: 14110
578
- };
579
- var MAX_ERROR_BODY_LENGTH = 200;
580
- function sanitiseErrorBody(raw) {
581
- return raw.replace(/[\x00-\x1f\x7f]/g, " ").replace(/<[^>]*>/g, "").trim().slice(0, MAX_ERROR_BODY_LENGTH);
582
- }
583
- var DEFAULT_QUOTE_VALIDITY_MS = 12e4;
584
- var CONTENT_HASH_TAG = "X-Content-Hash";
585
- var ArweavePaymentAdapter = class {
586
- constructor(config = {}) {
587
- this.network = "arweave";
588
- this.token = "AR";
589
- this.type = "storage";
590
- this.arweaveClient = null;
591
- this.pendingUpload = null;
592
- this.signedUpload = null;
593
- this.gateway = config.gateway ?? {
594
- host: "arweave.net",
595
- port: 443,
596
- protocol: "https"
597
- };
598
- this.walletKey = config.walletKey;
599
- this.quoteValidityMs = config.quoteValidityMs ?? DEFAULT_QUOTE_VALIDITY_MS;
600
- this.logger = config.logger;
601
- }
602
- // ── Protected: lazy Arweave client ───────────────────────────────────────────
603
- async getClient() {
604
- if (this.arweaveClient) return this.arweaveClient;
605
- try {
606
- const _m = await import("arweave");
607
- const raw = _m.default;
608
- const ctor = typeof raw.init === "function" ? raw : raw.default ?? raw;
609
- this.arweaveClient = ctor.init(this.gateway ?? {});
610
- return this.arweaveClient;
611
- } catch {
612
- throw new DStorageError(
613
- PaymentErrorCode.ARWEAVE_NOT_INSTALLED,
614
- "[dStorage/arweave-payment] arweave package not installed. Run: npm install arweave"
615
- );
616
- }
617
- }
618
- // ── Protected: wallet resolution ─────────────────────────────────────────────
619
- resolveWallet() {
620
- if (this.walletKey && Object.keys(this.walletKey).length > 0) {
621
- return this.walletKey;
622
- }
623
- if (typeof window === "undefined") {
624
- throw new DStorageError(
625
- PaymentErrorCode.ARWEAVE_WALLET_KEY_REQUIRED,
626
- "[dStorage/arweave-payment] Node.js usage requires a walletKey in the config."
627
- );
628
- }
629
- if (typeof window.arweaveWallet === "undefined") {
630
- throw new DStorageError(
631
- PaymentErrorCode.ARWEAVE_CONNECT_NOT_FOUND,
632
- "[dStorage/arweave-payment] ArConnect extension not found. Install it from https://arconnect.io"
633
- );
634
- }
635
- return "use_wallet";
636
- }
637
- // ── PaymentAdapter: provideUploadData ─────────────────────────────────────────
638
- /**
639
- * Store the encrypted upload payload and metadata tags so that pay() can
640
- * build and sign the complete Arweave transaction in one step.
641
- * Called by MetaTransaction after the encrypt step, before pay().
642
- */
643
- provideUploadData(data, metadata = {}) {
644
- const hashHex = Array.from(blake3(data)).map((b) => b.toString(16).padStart(2, "0")).join("");
645
- this.pendingUpload = {
646
- data,
647
- metadata: { [CONTENT_HASH_TAG]: hashHex, ...metadata }
648
- };
649
- this.logger?.log(
650
- "dStorage/arweave-payment",
651
- `Upload data staged: ${data.byteLength} bytes`
652
- );
653
- }
654
- // ── PaymentAdapter: estimateCost ─────────────────────────────────────────────
655
- /**
656
- * Query the Arweave fee oracle for the real upload cost.
657
- */
658
- async estimateCost(dataSizeBytes) {
659
- const arweave = await this.getClient();
660
- const winstons = await arweave.transactions.getPrice(
661
- dataSizeBytes
662
- );
663
- const amount = arweave.ar.winstonToAr(winstons);
664
- this.logger?.log(
665
- "dStorage/arweave-payment",
666
- `Estimated: ${amount} AR (${winstons} Winstons)`
667
- );
668
- return {
669
- token: this.token,
670
- amount,
671
- network: this.network,
672
- type: this.type,
673
- dataSizeBytes,
674
- expiresAt: Date.now() + this.quoteValidityMs
675
- };
676
- }
677
- // ── PaymentAdapter: pay ───────────────────────────────────────────────────────
678
- /**
679
- * Sign-then-store flow (when provideUploadData was called):
680
- * Creates the Arweave transaction with data + tags, prompts the wallet
681
- * for approval (ArConnect popup or JWK signing), and stores the signed
682
- * transaction for ArweaveStorageAdapter.store() to post.
683
- *
684
- * Pass-through fallback (when provideUploadData was NOT called):
685
- * Returns a receipt acknowledging that payment is bundled with the upload.
686
- * ArweaveStorageAdapter.store() performs the full create → sign → post flow.
687
- */
688
- async pay(instruction, onStatus) {
689
- if (Date.now() > instruction.expiresAt) {
690
- throw new DStorageError(
691
- PaymentErrorCode.ARWEAVE_QUOTE_EXPIRED,
692
- `[dStorage/payment] Quote expired at ${new Date(instruction.expiresAt).toISOString()}. Request a fresh quote via estimateCost() before calling pay().`
693
- );
694
- }
695
- if (!this.pendingUpload) {
696
- onStatus?.("AR fee will be deducted as part of the upload transaction.");
697
- return {
698
- txHash: "arweave-bundled-with-upload",
699
- token: this.token,
700
- amount: "0",
701
- type: this.type,
702
- paidAt: Date.now(),
703
- serviceFee: "0"
704
- };
705
- }
706
- const { data, metadata } = this.pendingUpload;
707
- this.pendingUpload = null;
708
- const arweave = await this.getClient();
709
- const wallet = this.resolveWallet();
710
- onStatus?.("Creating Arweave transaction\u2026");
711
- const tx = await arweave.createTransaction({ data }, wallet);
712
- for (const [key, value] of Object.entries(metadata)) {
713
- tx.addTag(key, value);
714
- }
715
- onStatus?.("Requesting AR wallet approval\u2026");
716
- await arweave.transactions.sign(tx, wallet);
717
- const winstons = await arweave.transactions.getPrice(
718
- data.byteLength
719
- );
720
- const arCost = arweave.ar.winstonToAr(winstons);
721
- this.signedUpload = { tx, arCost };
722
- this.logger?.log(
723
- "dStorage/arweave-payment",
724
- `Transaction signed \u2192 txId: ${tx.id} cost: ~${arCost} AR`
725
- );
726
- return {
727
- txHash: tx.id,
728
- token: this.token,
729
- amount: arCost,
730
- type: this.type,
731
- paidAt: Date.now(),
732
- serviceFee: "0"
733
- };
734
- }
735
- // ── Consumed by ArweaveStorageAdapter ─────────────────────────────────────────
736
- /**
737
- * Returns the pre-signed transaction produced by pay(), then clears it.
738
- * Called by ArweaveStorageAdapter.store() to post the already-signed tx.
739
- * Returns null if pay() did not sign a transaction (pass-through mode).
740
- */
741
- consumeSignedUpload() {
742
- const signed = this.signedUpload;
743
- this.signedUpload = null;
744
- return signed;
745
- }
746
- };
747
- var POST_SIGN_TX_API_PATH = "/api/service/sign-tx";
748
- function uint8ArrayToHex(bytes) {
749
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
750
- }
751
- function parseSignedTxBytes(signedTx) {
752
- if (signedTx.startsWith("midnight:")) {
753
- const colonIdx = signedTx.lastIndexOf(":");
754
- return hexToBytes(signedTx.slice(colonIdx + 1), "signedTx");
755
- }
756
- return hexToBytes(signedTx, "signedTx");
757
- }
758
- function parseSignTxResponse(raw) {
759
- if (typeof raw !== "object" || raw === null) {
760
- throw new DStorageError(
761
- PaymentErrorCode.MANAGED_NON_OBJECT_RESPONSE,
762
- "[dStorage/managed-payment] Signing server returned a non-object response."
763
- );
764
- }
765
- const r = raw;
766
- if (r["success"] !== true) {
767
- throw new DStorageError(
768
- PaymentErrorCode.MANAGED_SERVER_FAILURE,
769
- `[dStorage/managed-payment] Signing server reported success:false \u2014 ${sanitiseErrorBody(JSON.stringify(r))}`
770
- );
771
- }
772
- for (const field of [
773
- "txId",
774
- "signature",
775
- "publicKey",
776
- "txAmount",
777
- "serviceFee"
778
- ]) {
779
- if (typeof r[field] !== "string") {
780
- throw new DStorageError(
781
- PaymentErrorCode.MANAGED_MISSING_FIELD,
782
- `[dStorage/managed-payment] Signing server response missing or invalid field: ${field}`
783
- );
784
- }
785
- }
786
- return r;
787
- }
788
- function assertOwnerKeyMatch(responseKey, pinnedKey) {
789
- if (pinnedKey && responseKey !== pinnedKey) {
790
- throw new DStorageError(
791
- PaymentErrorCode.MANAGED_PUBLIC_KEY_MISMATCH,
792
- "[dStorage/managed-payment] Signing server returned a public key that does not match the pinned key from the auth token \u2014 possible server compromise or MITM."
793
- );
794
- }
795
- }
796
- var ManagedPaymentAdapter = class _ManagedPaymentAdapter extends ArweavePaymentAdapter {
797
- /**
798
- * Strip a RSA-4096 modulus suffix from a compound auth token
799
- * (<credential>.<base64url_683_chars>), returning only the credential.
800
- * A plain token (no '.' or wrong suffix length) is returned unchanged.
801
- * This lets ArweaveBundlerStorageAdapter compound tokens be passed directly
802
- * to any adapter that uses ManagedPaymentAdapter without breaking authentication.
803
- *
804
- * lastIndexOf is intentional: the credential part may itself contain dots
805
- * (e.g. a JWT), so we always split at the *last* dot to reach the modulus.
806
- */
807
- static _parseAuthToken(token) {
808
- const dot = token.lastIndexOf(".");
809
- if (dot !== -1) {
810
- const suffix = token.slice(dot + 1);
811
- if (suffix.length === 683 && /^[A-Za-z0-9_-]+$/.test(suffix)) {
812
- return { credential: token.slice(0, dot), ownerPublicKey: suffix };
813
- }
814
- }
815
- return { credential: token, ownerPublicKey: void 0 };
816
- }
817
- constructor(config) {
818
- super({
819
- ...config.gateway !== void 0 ? { gateway: config.gateway } : {},
820
- ...config.quoteValidityMs !== void 0 ? { quoteValidityMs: config.quoteValidityMs } : {},
821
- ...config.logger !== void 0 ? { logger: config.logger } : {}
822
- });
823
- this.signingServerUrl = config.signingServerUrl.replace(/\/$/, "");
824
- if (!config.authToken || config.authToken.trim() === "") {
825
- throw new DStorageError(
826
- PaymentErrorCode.MANAGED_TOKEN_REQUIRED,
827
- "[dStorage/managed-payment] authToken is required and must not be empty or whitespace."
828
- );
829
- }
830
- const { credential, ownerPublicKey } = _ManagedPaymentAdapter._parseAuthToken(config.authToken);
831
- this.authToken = credential;
832
- this.ownerPublicKey = ownerPublicKey;
833
- this.requestTimeoutMs = config.requestTimeoutMs ?? 3e4;
834
- this.network = config.network;
835
- this.type = config.type;
836
- }
837
- // ── PaymentAdapter: estimateCost ─────────────────────────────────────────────
838
- /**
839
- * Query the Arweave fee oracle for the real upload cost.
840
- */
841
- async estimateCost(dataSizeBytes) {
842
- if (this.network === "arweave") {
843
- return await super.estimateCost(dataSizeBytes);
844
- }
845
- const amount = "0.0012345";
846
- this.logger?.log(
847
- "dStorage/managed-payment",
848
- `Estimated: ${amount} ${this.token}`
849
- );
850
- return {
851
- token: this.token,
852
- // FIXME: should be probably obtained from the signing service
853
- amount,
854
- network: this.network,
855
- type: this.type,
856
- dataSizeBytes,
857
- txData: "---",
858
- expiresAt: Date.now() + this.quoteValidityMs
859
- };
860
- }
861
- // ── PaymentAdapter: pay ───────────────────────────────────────────────────────
862
- /**
863
- * Routes payment to the correct network-specific handler based on quote.network.
864
- *
865
- * The Arweave path falls back to the base-class pass-through if
866
- * provideUploadData() was not called. An unrecognised network throws.
867
- */
868
- async pay(instruction, onStatus) {
869
- if (Date.now() > instruction.expiresAt) {
870
- throw new DStorageError(
871
- PaymentErrorCode.MANAGED_QUOTE_EXPIRED,
872
- `[dStorage/payment] Quote expired at ${new Date(instruction.expiresAt).toISOString()}. Request a fresh quote via estimateCost() before calling pay().`
414
+ if (this.config.chainAdapter) {
415
+ const contentHash = await computeBlake3Hex(uploadData);
416
+ const storeRecovery = {
417
+ storageId: storageResult.id,
418
+ storageProvider: storageResult.provider,
419
+ keyEnvelope: isPublic ? void 0 : keyEnvelope,
420
+ contentHash
421
+ };
422
+ onProgress?.({
423
+ phase: "stored",
424
+ chunksUploaded: 1,
425
+ totalChunks: 1,
426
+ bytesUploaded: bytes.length,
427
+ totalBytes: bytes.length,
428
+ recovery: storeRecovery
429
+ });
430
+ const encryptedMetadata = rawMeta ? bytesToBase64url(
431
+ await uploadScheme.encryptPayload(
432
+ new TextEncoder().encode(JSON.stringify(rawMeta)),
433
+ METADATA_AAD
434
+ )
435
+ ) : void 0;
436
+ try {
437
+ const chainResult = await this.config.chainAdapter.writeReference({
438
+ storageId: onChainStorageId,
439
+ encryptionScheme,
440
+ storageProvider: storageResult.provider,
441
+ ...refId !== void 0 ? { refId } : {},
442
+ ...encryptedMetadata !== void 0 ? { encryptedMetadata } : {},
443
+ writtenAt: Date.now(),
444
+ keyEnvelope,
445
+ contentHash,
446
+ ownerSecret
447
+ });
448
+ const resultRefId = chainResult.refId;
449
+ this.logger?.log(
450
+ "dStorage",
451
+ `Upload complete \u2192 refId: ${resultRefId}`
452
+ );
453
+ return {
454
+ chainRefId: resultRefId,
455
+ storageId: storageResult.id,
456
+ storageProvider: storageResult.provider,
457
+ chainProvider: chainResult.provider,
458
+ uploadedAt: storageResult.uploadedAt,
459
+ encryptionScheme,
460
+ costEstimate: {
461
+ storageCost,
462
+ chainCost: {
463
+ amount: chainResult.paymentReceipt?.amount ?? "unknown",
464
+ token: chainResult.paymentReceipt?.token ?? "DUST"
465
+ },
466
+ fileSizeBytes: uploadData.length
467
+ }
468
+ };
469
+ } catch {
470
+ throw new StorePartialError(storeRecovery);
471
+ }
472
+ }
473
+ this.logger?.log(
474
+ "dStorage",
475
+ `Upload complete (storage-only) \u2192 storageId: ${storageResult.id}`
873
476
  );
477
+ return {
478
+ storageId: storageResult.id,
479
+ storageProvider: storageResult.provider,
480
+ uploadedAt: storageResult.uploadedAt,
481
+ encryptionScheme,
482
+ ...uploadScheme ? { cryptoScheme: uploadScheme } : {},
483
+ costEstimate: { storageCost, fileSizeBytes: uploadData.length }
484
+ };
485
+ } catch (err) {
486
+ wrapError(err);
874
487
  }
875
- if (this.network === "managedmock") {
876
- return this._payManagedMock(instruction, onStatus);
877
- }
878
- if (instruction.network === "midnight") {
879
- return this._payMidnight(instruction, onStatus);
880
- } else if (instruction.network === "arweave") {
881
- return this._payArweave(instruction, onStatus);
882
- } else if (instruction.network === "arweave_bundler") {
883
- return this._payArweaveBundler(instruction, onStatus);
884
- }
885
- throw Error(`Unsupported network: ${instruction.network}`);
886
488
  }
887
- // ── Network-specific wallet provider factories ────────────────────────────────
888
489
  /**
889
- * Returns a WalletProvider-compatible object for the Midnight chain whose
890
- * `balanceTx()` is fulfilled by the managed signing server instead of a
891
- * local wallet. The caller supplies the public keys (from whichever source
892
- * they have — HD derivation, a server endpoint, etc.); the SDK only handles
893
- * the signing-server round-trip.
490
+ * Register a pre-existing storageId as a new on-chain reference.
894
491
  *
895
- * The returned object satisfies the @midnight-ntwrk/midnight-js-types
896
- * WalletProvider interface via structural typing no Midnight package
897
- * import is required from the caller's side.
492
+ * Use this when data has already been uploaded to the storage network by
493
+ * other means (e.g. a third-party tool or a separate pipeline). The content
494
+ * is not touched — only the chain reference is created.
898
495
  *
899
- * @param coinPublicKey Midnight coin public key (hex) for the ZK fee circuit.
900
- * @param encryptionPublicKey Midnight encryption public key (hex).
901
- * @param onReceipt Optional callback invoked with the PaymentReceipt
902
- * after each successful balanceTx call. Used internally
903
- * by MidnightChainAdapter to capture receipt metadata;
904
- * external callers can omit it.
496
+ * A fresh DEK is generated solely to encrypt the storageId and derive the
497
+ * ownerSecret. The resulting reference round-trips through retrieveByRefId()
498
+ * exactly like one created by store().
499
+ *
500
+ * Note: chunked manifests are not handled here. For large pre-existing
501
+ * uploads already split into chunks, register the manifest storageId.
905
502
  */
906
- createMidnightWalletProvider(coinPublicKey, encryptionPublicKey, onReceipt) {
907
- return {
908
- getCoinPublicKey: () => coinPublicKey,
909
- getEncryptionPublicKey: () => encryptionPublicKey,
910
- balanceTx: async (tx, _ttl) => {
911
- const txData = uint8ArrayToHex(tx.serialize());
912
- const receipt = await this.pay({
913
- network: "midnight",
914
- txData,
915
- dataSizeBytes: txData.length,
916
- expiresAt: Date.now() + 6e4
917
- });
918
- onReceipt?.(receipt);
919
- if (!receipt.signedTx) {
920
- throw new DStorageError(
921
- PaymentErrorCode.MANAGED_UNBALANCED_TX,
922
- "[dStorage/payment] Midnight managed payment server did not return a balanced transaction."
503
+ async registerReference(options = {}) {
504
+ try {
505
+ this.assertConnected();
506
+ if (!this.config.chainAdapter) {
507
+ throw new DStorageError(
508
+ CoreErrorCode.REGISTER_REF_REQUIRES_CHAIN,
509
+ "[dStorage] registerReference requires a chain adapter."
510
+ );
511
+ }
512
+ if (this._encryptionProviders.length === 0) {
513
+ throw new DStorageError(
514
+ CoreErrorCode.REGISTER_REF_REQUIRES_ENCRYPTION,
515
+ "[dStorage] registerReference requires at least one encryption adapter."
516
+ );
517
+ }
518
+ let dek;
519
+ const recoveryEnvelope = options.keyEnvelope;
520
+ if (recoveryEnvelope) {
521
+ dek = await this._unwrapDek(recoveryEnvelope);
522
+ } else {
523
+ dek = generateDek();
524
+ }
525
+ try {
526
+ const scheme = XChaChaScheme.fromKey(dek);
527
+ const { metadata: rawMeta } = options;
528
+ const encryptedMetadata = rawMeta ? bytesToBase64url(
529
+ await scheme.encryptPayload(
530
+ new TextEncoder().encode(JSON.stringify(rawMeta)),
531
+ METADATA_AAD
532
+ )
533
+ ) : void 0;
534
+ const onChainStorageId = options.storageId !== void 0 ? await scheme.encryptStorageId(options.storageId) : void 0;
535
+ let keyEnvelope;
536
+ if (recoveryEnvelope !== void 0) {
537
+ keyEnvelope = recoveryEnvelope;
538
+ } else {
539
+ const wrapperEntries = await Promise.all(
540
+ this._encryptionProviders.map(async (p) => ({
541
+ wrapKeyName: p.name,
542
+ ...await p.wrapDek(dek)
543
+ }))
923
544
  );
545
+ keyEnvelope = serializeKeyEnvelope(buildKeyEnvelope(wrapperEntries));
924
546
  }
925
- const { Transaction } = await import("@midnight-ntwrk/midnight-js-protocol/ledger");
926
- return Transaction.deserialize(
927
- "signature",
928
- "proof",
929
- "binding",
930
- parseSignedTxBytes(receipt.signedTx)
547
+ const ownerSecret = await deriveOwnerSecret(
548
+ dek,
549
+ options.storageId !== void 0 ? new TextEncoder().encode(options.storageId) : METADATA_ONLY_OWNER_SECRET_SALT
931
550
  );
551
+ const chainResult = await this.config.chainAdapter.writeReference({
552
+ ...onChainStorageId !== void 0 ? { storageId: onChainStorageId } : {},
553
+ encryptionScheme: "xchacha20poly1305-v1",
554
+ storageProvider: options.storageProvider ?? this.config.storageAdapter.name,
555
+ ...options.refId !== void 0 ? { refId: options.refId } : {},
556
+ ...encryptedMetadata !== void 0 ? { encryptedMetadata } : {},
557
+ writtenAt: Date.now(),
558
+ keyEnvelope,
559
+ ...options.contentHash !== void 0 ? { contentHash: options.contentHash } : {},
560
+ ownerSecret
561
+ });
562
+ return { chainRefId: chainResult.refId };
563
+ } finally {
564
+ dek.fill(0);
932
565
  }
933
- };
566
+ } catch (err) {
567
+ wrapError(err);
568
+ }
934
569
  }
935
- // ── Network-specific payment handlers ────────────────────────────────────────
936
570
  /**
937
- * Arweave remote-signing flow:
938
- * 1. Creates the Arweave transaction locally (data + tags, no local signing).
939
- * 2. Calls prepareChunks() to compute data_root (Merkle root) locally.
940
- * 3. Strips the raw data from the transaction before serialising — only the
941
- * data_root and data_size (not the actual bytes) are sent to the server.
942
- * 4. POSTs the stripped transaction JSON to the signing server.
943
- * 5. Applies the returned id, signature, and owner to the transaction.
944
- * 6. Restores the raw data so the chunked uploader can post it directly to
945
- * the Arweave gateway — the server never sees the file content.
571
+ * Store new content and update an existing on-chain reference in place.
946
572
  *
947
- * Falls back to the base-class pass-through if provideUploadData() was not called.
573
+ * Ownership is proved by re-deriving the ownerSecret from the existing
574
+ * reference — the same mechanism used by removeReference(). A fresh DEK is
575
+ * generated for the new content; the previous version is not modified and remains
576
+ * independently decryptable if the caller retained the old storageId.
577
+ *
578
+ * Note: chunking is not applied here. For large files use store() to get a
579
+ * new chunked reference, then removeReference() on the old one.
580
+ *
581
+ * @param refId The chain reference to update (returned by a prior store/registerReference).
582
+ * @param bytes New content to encrypt and store.
583
+ * @param options Optional StoreOptions. isPublic is ignored.
948
584
  */
949
- async _payArweave(instruction, onStatus) {
950
- if (!this.pendingUpload) {
951
- return super.pay(instruction, onStatus);
952
- }
953
- const { data, metadata } = this.pendingUpload;
954
- this.pendingUpload = null;
955
- const arweave = await this.getClient();
956
- onStatus?.("Creating Arweave transaction\u2026");
957
- const tx = await arweave.createTransaction({ data });
958
- if (this.ownerPublicKey) {
959
- tx.owner = this.ownerPublicKey;
960
- }
961
- for (const [key, value] of Object.entries(metadata)) {
962
- tx.addTag(key, value);
585
+ async update(refId, bytes, options) {
586
+ try {
587
+ this.assertConnected();
588
+ if (!this.config.chainAdapter) {
589
+ throw new DStorageError(
590
+ CoreErrorCode.UPDATE_UPLOAD_REQUIRES_CHAIN,
591
+ "[dStorage] update requires a chain adapter."
592
+ );
593
+ }
594
+ if (typeof this.config.chainAdapter.updateReference !== "function") {
595
+ throw new DStorageError(
596
+ CoreErrorCode.UPDATE_UPLOAD_NOT_SUPPORTED,
597
+ "[dStorage] update is not supported by this chain adapter."
598
+ );
599
+ }
600
+ if (this._encryptionProviders.length === 0) {
601
+ throw new DStorageError(
602
+ CoreErrorCode.UPDATE_UPLOAD_REQUIRES_ENCRYPTION,
603
+ "[dStorage] update requires at least one encryption adapter."
604
+ );
605
+ }
606
+ const { tags = {}, metadata: rawMeta } = options ?? {};
607
+ const ownerSecret = await this._computeOwnerSecret(refId);
608
+ const dek = generateDek();
609
+ try {
610
+ const scheme = XChaChaScheme.fromKey(dek);
611
+ const uploadData = await scheme.encryptPayload(bytes, PAYLOAD_AAD);
612
+ const storageResult = await this.config.storageAdapter.store(
613
+ uploadData,
614
+ {
615
+ ...tags,
616
+ [TAG_CONTENT_TYPE]: TAG_CONTENT_TYPE_OCTET_STREAM
617
+ }
618
+ );
619
+ const onChainStorageId = await scheme.encryptStorageId(
620
+ storageResult.id
621
+ );
622
+ const wrapperEntries = await Promise.all(
623
+ this._encryptionProviders.map(async (p) => ({
624
+ wrapKeyName: p.name,
625
+ ...await p.wrapDek(dek)
626
+ }))
627
+ );
628
+ const keyEnvelope = serializeKeyEnvelope(
629
+ buildKeyEnvelope(wrapperEntries)
630
+ );
631
+ const encryptedMetadata = rawMeta ? bytesToBase64url(
632
+ await scheme.encryptPayload(
633
+ new TextEncoder().encode(JSON.stringify(rawMeta)),
634
+ METADATA_AAD
635
+ )
636
+ ) : void 0;
637
+ const contentHash = await computeBlake3Hex(uploadData);
638
+ const rawNewStorageId = new TextEncoder().encode(storageResult.id);
639
+ const newOwnerSecret = await deriveOwnerSecret(dek, rawNewStorageId);
640
+ const storeRecovery = {
641
+ storageId: storageResult.id,
642
+ storageProvider: storageResult.provider,
643
+ keyEnvelope,
644
+ contentHash
645
+ };
646
+ options?.onProgress?.({
647
+ phase: "stored",
648
+ chunksUploaded: 1,
649
+ totalChunks: 1,
650
+ bytesUploaded: bytes.length,
651
+ totalBytes: bytes.length,
652
+ recovery: storeRecovery
653
+ });
654
+ try {
655
+ await this.config.chainAdapter.updateReference(
656
+ refId,
657
+ {
658
+ storageId: onChainStorageId,
659
+ encryptionScheme: "xchacha20poly1305-v1",
660
+ storageProvider: storageResult.provider,
661
+ ...encryptedMetadata !== void 0 ? { encryptedMetadata } : {},
662
+ writtenAt: Date.now(),
663
+ keyEnvelope,
664
+ contentHash
665
+ },
666
+ ownerSecret,
667
+ newOwnerSecret
668
+ );
669
+ } catch {
670
+ throw new StorePartialError(storeRecovery);
671
+ }
672
+ return { chainRefId: refId, storageId: storageResult.id };
673
+ } finally {
674
+ dek.fill(0);
675
+ }
676
+ } catch (err) {
677
+ wrapError(err);
963
678
  }
964
- await tx.prepareChunks(tx.data);
965
- const rawData = tx.data;
966
- tx.data = new Uint8Array(0);
967
- const body = JSON.stringify({
968
- txData: tx.toJSON(),
969
- network: instruction.network
970
- });
971
- const {
972
- txId,
973
- signature,
974
- publicKey,
975
- txAmount: arCost,
976
- serviceFee
977
- } = await this._sendSignTxRequest(body, instruction.network, onStatus);
978
- assertOwnerKeyMatch(publicKey, this.ownerPublicKey);
979
- onStatus?.("Applying remote signature\u2026");
980
- tx.setSignature({ id: txId, signature, owner: publicKey });
981
- tx.data = rawData;
982
- this.signedUpload = { tx, arCost };
983
- this.logger?.log(
984
- "dStorage/managed-payment",
985
- `Transaction signed remotely (${this.signingServerUrl} API service) \u2192 txId: ${txId} cost: ~${arCost} ${this.token} (service fee: ${serviceFee})`
986
- );
987
- return {
988
- txHash: txId,
989
- token: this.token,
990
- amount: arCost,
991
- type: this.type,
992
- paidAt: Date.now(),
993
- serviceFee
994
- };
995
679
  }
996
680
  /**
997
- * Midnight payment flow:
998
- * POSTs the fee amount to the dStorage API service, which holds a funded
999
- * Midnight account and submits the chain transaction on behalf of the user.
681
+ * Retry only the on-chain reference write after a failed `update()` call.
1000
682
  *
1001
- * POST {baseUrl}{POST_SIGN_TX_API_PATH}
1002
- * { txData: string (hex-serialized UnboundTransaction), network: string, userId: string }
1003
- * { txId: string, signature: string (hex-serialized FinalizedTransaction), ... }
683
+ * Use this when `update()` throws a `StorePartialError` — meaning the new
684
+ * content was stored successfully but the chain write failed. Call with the
685
+ * `refId` you passed to `update()` and the `err.recovery` from the caught
686
+ * error (or the `recovery` emitted via `onProgress` at phase `"stored"`).
1004
687
  *
1005
- * The hex-serialized FinalizedTransaction is returned in PaymentReceipt.signedTx
1006
- * for the chain adapter to deserialize and pass to submitTx().
1007
- */
1008
- async _payMidnight(instruction, onStatus) {
1009
- const body = JSON.stringify({
1010
- txData: instruction.txData ?? null,
1011
- network: instruction.network
1012
- });
1013
- const {
1014
- txId,
1015
- signature,
1016
- txAmount: dustCost,
1017
- serviceFee
1018
- } = await this._sendSignTxRequest(body, instruction.network, onStatus);
1019
- this.logger?.log(
1020
- "dStorage/managed-payment",
1021
- `Midnight Tx signed remotely (${this.signingServerUrl} API service) \u2192 txId: ${txId} cost: ~${dustCost} ${this.token} (service fee: ${serviceFee})`
1022
- );
1023
- return {
1024
- txHash: txId,
1025
- token: "DUST",
1026
- amount: dustCost,
1027
- type: this.type,
1028
- signedTx: signature,
1029
- paidAt: Date.now(),
1030
- serviceFee
1031
- };
1032
- }
1033
- /**
1034
- * Arweave Bundler
688
+ * The new content is already in storage; this method re-derives all chain
689
+ * write parameters from the recovery envelope without re-uploading.
1035
690
  */
1036
- async _payArweaveBundler(instruction, onStatus) {
1037
- const body = JSON.stringify({
1038
- txData: instruction.txData ?? null,
1039
- network: instruction.network,
1040
- ...instruction.ownerKey !== void 0 ? { ownerKey: instruction.ownerKey } : {}
1041
- });
1042
- const {
1043
- txId,
1044
- signature,
1045
- txAmount: arCost,
1046
- serviceFee
1047
- } = await this._sendSignTxRequest(body, instruction.network, onStatus);
1048
- this.logger?.log(
1049
- "dStorage/managed-payment",
1050
- `Arweave bundler Tx signed remotely (${this.signingServerUrl} API service) \u2192 txId: ${txId} cost: ~${arCost} ${this.token} (service fee: ${serviceFee})`
1051
- );
1052
- return {
1053
- txHash: txId,
1054
- token: "AR",
1055
- amount: arCost,
1056
- type: this.type,
1057
- signedTx: signature,
1058
- paidAt: Date.now(),
1059
- serviceFee
1060
- };
691
+ async retryUpdate(refId, recovery) {
692
+ try {
693
+ this.assertConnected();
694
+ if (!this.config.chainAdapter) {
695
+ throw new DStorageError(
696
+ CoreErrorCode.UPDATE_UPLOAD_REQUIRES_CHAIN,
697
+ "[dStorage] retryUpdate requires a chain adapter."
698
+ );
699
+ }
700
+ if (typeof this.config.chainAdapter.updateReference !== "function") {
701
+ throw new DStorageError(
702
+ CoreErrorCode.UPDATE_UPLOAD_NOT_SUPPORTED,
703
+ "[dStorage] retryUpdate is not supported by this chain adapter."
704
+ );
705
+ }
706
+ if (this._encryptionProviders.length === 0) {
707
+ throw new DStorageError(
708
+ CoreErrorCode.UPDATE_UPLOAD_REQUIRES_ENCRYPTION,
709
+ "[dStorage] retryUpdate requires at least one encryption adapter."
710
+ );
711
+ }
712
+ if (!recovery.keyEnvelope) {
713
+ throw new DStorageError(
714
+ CoreErrorCode.UNWRAP_DEK_ENVELOPE_EMPTY,
715
+ "[dStorage] retryUpdate requires a key envelope in the recovery object."
716
+ );
717
+ }
718
+ const dek = await this._unwrapDek(recovery.keyEnvelope);
719
+ try {
720
+ const scheme = XChaChaScheme.fromKey(dek);
721
+ const ownerSecret = await this._computeOwnerSecret(refId);
722
+ const onChainStorageId = await scheme.encryptStorageId(
723
+ recovery.storageId
724
+ );
725
+ const newOwnerSecret = await deriveOwnerSecret(
726
+ dek,
727
+ new TextEncoder().encode(recovery.storageId)
728
+ );
729
+ await this.config.chainAdapter.updateReference(
730
+ refId,
731
+ {
732
+ storageId: onChainStorageId,
733
+ encryptionScheme: "xchacha20poly1305-v1",
734
+ storageProvider: recovery.storageProvider,
735
+ writtenAt: Date.now(),
736
+ keyEnvelope: recovery.keyEnvelope,
737
+ ...recovery.contentHash !== void 0 ? { contentHash: recovery.contentHash } : {}
738
+ },
739
+ ownerSecret,
740
+ newOwnerSecret
741
+ );
742
+ return { chainRefId: refId, storageId: recovery.storageId };
743
+ } finally {
744
+ dek.fill(0);
745
+ }
746
+ } catch (err) {
747
+ wrapError(err);
748
+ }
1061
749
  }
1062
750
  /**
1063
- * Managed mock flow:
1064
- * Sends a minimal request to the signing server with the literal network
1065
- * identifier "TEST" — the server treats this as a sandbox/test flow with no
1066
- * real funds involved. The SDK-side network name is "managedmock" to follow
1067
- * the lowercase naming convention; the server-side string is always "TEST".
751
+ * Rotate the encryption adapters protecting an existing reference without
752
+ * re-uploading the content.
753
+ *
754
+ * Useful when you want to:
755
+ * - Change a password: configure [newPassword, mnemonic] and call rotateKeys().
756
+ * The mnemonic unwraps the old DEK, the new envelope is wrapped under both
757
+ * new adapters, and the old password wrapper is dropped.
758
+ * - Add or remove adapters: any single adapter that matches a wrapper in the
759
+ * existing key envelope can authorise the rotation. The resulting envelope
760
+ * is wrapped under exactly the adapters currently configured on this SDK.
761
+ *
762
+ * The content in storage is not touched — only the on-chain key envelope is
763
+ * updated. writtenAt is preserved to reflect the original upload time.
1068
764
  */
1069
- async _payManagedMock(instruction, onStatus) {
1070
- const body = JSON.stringify({
1071
- txData: instruction.txData || "mock",
1072
- network: "TEST"
1073
- });
1074
- const { txId, txAmount, serviceFee } = await this._sendSignTxRequest(
1075
- body,
1076
- "TEST",
1077
- onStatus
1078
- );
1079
- this.logger?.log(
1080
- "dStorage/managed-payment",
1081
- `ManagedMock Tx signed remotely (${this.signingServerUrl}) \u2192 txId: ${txId} cost: ~${txAmount} MOCK (service fee: ${serviceFee})`
1082
- );
1083
- return {
1084
- txHash: txId,
1085
- token: "MOCK",
1086
- amount: txAmount,
1087
- type: this.type,
1088
- paidAt: Date.now(),
1089
- serviceFee
1090
- };
765
+ async rotateKeys(refId) {
766
+ try {
767
+ this.assertConnected();
768
+ if (!this.config.chainAdapter) {
769
+ throw new DStorageError(
770
+ CoreErrorCode.ROTATE_KEYS_REQUIRES_CHAIN,
771
+ "[dStorage] rotateKeys requires a chain adapter."
772
+ );
773
+ }
774
+ if (typeof this.config.chainAdapter.updateReference !== "function") {
775
+ throw new DStorageError(
776
+ CoreErrorCode.ROTATE_KEYS_NOT_SUPPORTED,
777
+ "[dStorage] rotateKeys is not supported by this chain adapter."
778
+ );
779
+ }
780
+ if (this._encryptionProviders.length === 0) {
781
+ throw new DStorageError(
782
+ CoreErrorCode.ROTATE_KEYS_REQUIRES_ENCRYPTION,
783
+ "[dStorage] rotateKeys requires at least one encryption adapter."
784
+ );
785
+ }
786
+ const ref = await this.config.chainAdapter.readReference(refId);
787
+ const ownerSecret = await this._computeOwnerSecret(refId);
788
+ const seed = await this._unwrapDek(ref.keyEnvelope);
789
+ try {
790
+ const wrapperEntries = await Promise.all(
791
+ this._encryptionProviders.map(async (p) => ({
792
+ wrapKeyName: p.name,
793
+ ...await p.wrapDek(seed)
794
+ }))
795
+ );
796
+ const newKeyEnvelope = serializeKeyEnvelope(
797
+ buildKeyEnvelope(wrapperEntries)
798
+ );
799
+ await this.config.chainAdapter.updateReference(
800
+ refId,
801
+ {
802
+ storageId: ref.storageId ?? "",
803
+ encryptionScheme: ref.encryptionScheme ?? "",
804
+ storageProvider: ref.storageProvider,
805
+ writtenAt: ref.writtenAt,
806
+ keyEnvelope: newKeyEnvelope,
807
+ ...ref.encryptedMetadata !== void 0 ? { encryptedMetadata: ref.encryptedMetadata } : {},
808
+ ...ref.contentHash !== void 0 ? { contentHash: ref.contentHash } : {}
809
+ },
810
+ ownerSecret,
811
+ ownerSecret
812
+ );
813
+ } finally {
814
+ seed.fill(0);
815
+ }
816
+ } catch (err) {
817
+ wrapError(err);
818
+ }
1091
819
  }
1092
820
  /**
1093
- * Shared HTTP helper: POSTs a transaction payload to the signing server and
1094
- * returns the parsed SignTxResponse.
821
+ * Retrieve and decrypt data by its storageId.
822
+ * Decryption happens locally — the SDK never sends plaintext anywhere.
1095
823
  *
1096
- * - Arweave: txJson is the Arweave tx JSON object (from tx.toJSON()); the server
1097
- * signs it and returns the signature + owner in the response fields.
1098
- * - Midnight: txJson is the hex-serialized UnboundTransaction; the server balances
1099
- * and signs it, returning the hex-serialized FinalizedTransaction in the
1100
- * `signature` response field.
824
+ * Automatically detects chunked manifests and reassembles the original file.
1101
825
  *
1102
- * Throws on network errors or non-2xx HTTP responses.
826
+ * @param storageId The storageId returned from store()
1103
827
  */
1104
- async _sendSignTxRequest(body, network, onStatus) {
1105
- onStatus?.(`Sending ${network} transaction to signing server\u2026`);
1106
- let signResp;
828
+ async retrieveByStorageId(storageId, dekScheme, contentHash) {
1107
829
  try {
1108
- this.logger?.log(
1109
- "dStorage/managed-payment",
1110
- `${network}: delegating to managed payment service (serialized req length=${body.length})`
1111
- );
1112
- const signal = this.requestTimeoutMs > 0 ? AbortSignal.timeout(this.requestTimeoutMs) : void 0;
1113
- signResp = await fetch(
1114
- `${this.signingServerUrl}${POST_SIGN_TX_API_PATH}`,
1115
- {
1116
- method: "POST",
1117
- headers: {
1118
- "Content-Type": "application/json",
1119
- Authorization: `Bearer ${this.authToken}`
1120
- },
1121
- body,
1122
- ...signal !== void 0 ? { signal } : {}
830
+ this.assertConnected();
831
+ const { bytes: rawBytes, tags } = await this.config.storageAdapter.retrieve(storageId);
832
+ if (contentHash) {
833
+ const computed = await computeBlake3Hex(rawBytes);
834
+ if (computed !== contentHash) {
835
+ throw new DStorageError(
836
+ CoreErrorCode.CONTENT_HASH_MISMATCH,
837
+ `[dStorage] Content hash mismatch for storageId ${storageId}. Expected ${contentHash}, got ${computed}. The retrieved bytes do not match the hash committed on-chain \u2014 the storage content may have been tampered with.`
838
+ );
1123
839
  }
1124
- );
840
+ }
841
+ return this._decryptAndReassemble(rawBytes, storageId, tags, dekScheme);
1125
842
  } catch (err) {
843
+ wrapError(err);
844
+ }
845
+ }
846
+ /**
847
+ * Unwrap the data DEK from a key envelope and return an XChaChaScheme built from it.
848
+ * Throws if the KEK is not available or the envelope cannot be parsed.
849
+ * Only valid for private uploads (envelope.wrappers must be present).
850
+ */
851
+ async _getDekScheme(keyEnvelope) {
852
+ if (this._encryptionProviders.length === 0) {
1126
853
  throw new DStorageError(
1127
- PaymentErrorCode.MANAGED_SERVER_UNREACHABLE,
1128
- `[dStorage/managed-payment] Could not reach signing server: ${err}`,
1129
- { cause: err }
854
+ CoreErrorCode.DECRYPT_NO_PROVIDERS,
855
+ "[dStorage] Cannot decrypt: no encryption providers. Call init() before retrieving private data."
1130
856
  );
1131
857
  }
1132
- if (signResp.status === 409) {
858
+ if (!keyEnvelope) {
1133
859
  throw new DStorageError(
1134
- PaymentErrorCode.MANAGED_KEY_CHANGED,
1135
- "[dStorage/managed-payment] Signing server key has changed \u2014 re-issue your API token to obtain the updated public key."
860
+ CoreErrorCode.DECRYPT_ENVELOPE_EMPTY,
861
+ "[dStorage] Cannot decrypt: key envelope is empty.",
862
+ { cause: { code: "envelope_empty" } }
1136
863
  );
1137
864
  }
1138
- if (!signResp.ok) {
1139
- const rawBody = await signResp.text().catch(() => "");
1140
- const body2 = sanitiseErrorBody(rawBody);
865
+ const envelope = parseKeyEnvelope(keyEnvelope);
866
+ if (!envelope) {
1141
867
  throw new DStorageError(
1142
- PaymentErrorCode.MANAGED_SIGN_HTTP_ERROR,
1143
- `[dStorage/managed-payment] POST ${POST_SIGN_TX_API_PATH} failed: HTTP ${signResp.status}${body2 ? ` \u2014 ${body2}` : ""}`
868
+ CoreErrorCode.DECRYPT_ENVELOPE_MALFORMED,
869
+ "[dStorage] Cannot decrypt: key envelope is malformed or has an invalid structure. The stored reference may be corrupted or tampered with.",
870
+ { cause: { code: "envelope_malformed" } }
1144
871
  );
1145
872
  }
1146
- let parsed;
1147
- try {
1148
- parsed = await signResp.json();
1149
- } catch {
1150
- throw new DStorageError(
1151
- PaymentErrorCode.MANAGED_NON_JSON_RESPONSE,
1152
- `[dStorage/managed-payment] Managed payment service returned a non-JSON response \u2014 check that the service URL is correct and reachable: ${this.signingServerUrl}${POST_SIGN_TX_API_PATH}`
873
+ for (const wrapper of envelope.wrappers) {
874
+ const provider = this._encryptionProviders.find(
875
+ (p) => p.name === wrapper.wrapKeyName
1153
876
  );
877
+ if (!provider) continue;
878
+ try {
879
+ const dek = await provider.unwrapDek(wrapper);
880
+ return XChaChaScheme.fromKey(dek);
881
+ } catch {
882
+ }
1154
883
  }
1155
- return parseSignTxResponse(parsed);
1156
- }
1157
- };
1158
-
1159
- // src/chunked.ts
1160
- var CHUNK_SIZE = 10 * 1024 * 1024;
1161
- var MAX_CHUNK_COUNT = 1e4;
1162
- var MAX_REASSEMBLY_BYTES = MAX_CHUNK_COUNT * CHUNK_SIZE;
1163
- function isManifest(payload) {
1164
- if (typeof payload !== "object" || payload === null) return false;
1165
- const m = payload;
1166
- if (m["type"] !== "dstorage-chunked-manifest") return false;
1167
- return Number.isInteger(m["totalSize"]) && m["totalSize"] >= 0 && m["totalSize"] <= MAX_REASSEMBLY_BYTES && Number.isInteger(m["chunkSize"]) && m["chunkSize"] > 0 && Number.isInteger(m["chunkCount"]) && m["chunkCount"] >= 0 && m["chunkCount"] <= MAX_CHUNK_COUNT;
1168
- }
1169
-
1170
- // src/dStorage.ts
1171
- var TAG_SDK_VERSION = "dStorage-Version";
1172
- var TAG_CONTENT_TYPE = "Content-Type";
1173
- var TAG_CONTENT_TYPE_OCTET_STREAM = "application/octet-stream";
1174
- var TAG_CONTENT_TYPE_JSON = "application/json";
1175
- var TAG_CHUNK_INDEX = "dStorage-Chunk-Index";
1176
- var TAG_CHUNK_TOTAL = "dStorage-Chunk-Total";
1177
- var TAG_MANIFEST_UPLOAD = "dStorage-Manifest-Upload";
1178
- function wrapError(err) {
1179
- if (err instanceof DStorageError || isDStorageError(err)) throw err;
1180
- throw new DStorageError(
1181
- CoreErrorCode.UNKNOWN_ERROR,
1182
- `[dStorage] Unexpected error: ${err instanceof Error ? err.message : String(err)}`,
1183
- { cause: err }
1184
- );
1185
- }
1186
- var DStorage = class {
1187
- constructor(config) {
1188
- this.wallet = null;
1189
- /** Encryption adapters — each wraps/unwraps per-upload DEKs using its own algorithm. */
1190
- this._encryptionProviders = [];
1191
- this._initialized = false;
1192
- this.config = config;
1193
- this.logger = config.logger;
1194
- }
1195
- // ── DStorageContext interface — public accessors ───────────────────────────
1196
- get storageAdapter() {
1197
- return this.config.storageAdapter;
884
+ throw new DStorageError(
885
+ CoreErrorCode.DECRYPT_NO_WRAPPER_MATCHED,
886
+ "[dStorage] Cannot decrypt: this reference was not encrypted with any of your configured keys. Check that you are using the correct encryption adapter, password, seed phrase, or keypair.",
887
+ { cause: { code: "no_wrapper_matched" } }
888
+ );
1198
889
  }
1199
- get chainAdapter() {
1200
- return this.config.chainAdapter;
890
+ /**
891
+ * Unwrap the raw DEK bytes from a key envelope using the available KEKs.
892
+ * The ownerSecret for the reference can be re-derived as HKDF(dek, rawStorageId).
893
+ * Throws if no wrapper matches any available KEK.
894
+ */
895
+ async _unwrapDek(keyEnvelope) {
896
+ if (this._encryptionProviders.length === 0) {
897
+ throw new DStorageError(
898
+ CoreErrorCode.UNWRAP_DEK_NO_PROVIDERS,
899
+ "[dStorage] Cannot unwrap DEK: no encryption providers. Call init() first."
900
+ );
901
+ }
902
+ if (!keyEnvelope) {
903
+ throw new DStorageError(
904
+ CoreErrorCode.UNWRAP_DEK_ENVELOPE_EMPTY,
905
+ "[dStorage] Cannot unwrap DEK: key envelope is empty.",
906
+ { cause: { code: "envelope_empty" } }
907
+ );
908
+ }
909
+ const envelope = parseKeyEnvelope(keyEnvelope);
910
+ if (!envelope) {
911
+ throw new DStorageError(
912
+ CoreErrorCode.UNWRAP_DEK_ENVELOPE_MALFORMED,
913
+ "[dStorage] Cannot unwrap DEK: key envelope is malformed or has an invalid structure. The stored reference may be corrupted or tampered with.",
914
+ { cause: { code: "envelope_malformed" } }
915
+ );
916
+ }
917
+ for (const wrapper of envelope.wrappers) {
918
+ const provider = this._encryptionProviders.find(
919
+ (p) => p.name === wrapper.wrapKeyName
920
+ );
921
+ if (!provider) continue;
922
+ try {
923
+ return await provider.unwrapDek(wrapper);
924
+ } catch {
925
+ }
926
+ }
927
+ throw new DStorageError(
928
+ CoreErrorCode.UNWRAP_DEK_NO_WRAPPER_MATCHED,
929
+ "[dStorage] Cannot unwrap DEK: this reference was not encrypted with any of your configured keys. Check that you are using the correct encryption adapter, password, seed phrase, or keypair.",
930
+ { cause: { code: "no_wrapper_matched" } }
931
+ );
1201
932
  }
1202
- // ── Lifecycle ─────────────────────────────────────────────────────────────
1203
933
  /**
1204
- * Initialize the SDK:
1205
- * 1. Initialize the chain adapter wallet (if supported), and
1206
- * deploy a new DataRegistry contract, or connect to an existing one.
1207
- * 2. Derive an encryption key from the chain adapter's credentials (if supported).
934
+ * Compute the ownerSecret for an existing on-chain reference.
935
+ * Used by removeReference/updateReference to derive the ZK witness secret.
1208
936
  *
1209
- * Returns the address of the DataRegistry contract in use.
1210
- * Call this instead of connect() when using MidnightChainAdapter directly.
937
+ * Flow:
938
+ * 1. Read the reference from chain to get keyEnvelope + storageId
939
+ * 2. Unwrap the DEK from keyEnvelope using available KEKs
940
+ * 3. Recover raw storageId (decrypt with DEK if private, plaintext if public)
941
+ * 4. Return HKDF-SHA256(dek, rawStorageId) for private, or HKDF-SHA256(ownerSeed, rawStorageId) for public
1211
942
  */
1212
- async init() {
943
+ async _computeOwnerSecret(refId) {
944
+ const ref = await this.config.chainAdapter.readReference(refId);
945
+ const isPublic = !ref.encryptionScheme;
946
+ if (isPublic) {
947
+ const ownerSeed = await this._unwrapDek(ref.keyEnvelope);
948
+ return deriveOwnerSecret(
949
+ ownerSeed,
950
+ new TextEncoder().encode(ref.storageId),
951
+ "dstorage:owner-secret:public:v1"
952
+ );
953
+ }
954
+ const dek = await this._unwrapDek(ref.keyEnvelope);
955
+ const dekScheme = XChaChaScheme.fromKey(dek);
956
+ const rawStorageIdBytes = ref.storageId ? new TextEncoder().encode(
957
+ await dekScheme.decryptStorageId(ref.storageId)
958
+ ) : METADATA_ONLY_OWNER_SECRET_SALT;
959
+ return deriveOwnerSecret(dek, rawStorageIdBytes);
960
+ }
961
+ // Accepts an optional pre-built CryptoScheme (unwrapped from a key envelope).
962
+ // If no dekScheme is provided, content is treated as public (no decryption).
963
+ async _decryptAndReassemble(rawBytes, storageId, tags, dekScheme) {
964
+ const scheme = dekScheme ?? null;
965
+ if (scheme) {
966
+ let manifestBytes = null;
967
+ try {
968
+ manifestBytes = await scheme.decryptPayload(rawBytes, MANIFEST_AAD);
969
+ } catch {
970
+ }
971
+ if (manifestBytes !== null) {
972
+ const parsed = JSON.parse(
973
+ new TextDecoder().decode(manifestBytes)
974
+ );
975
+ if (!isManifest(parsed)) {
976
+ throw new DStorageError(
977
+ CoreErrorCode.MANIFEST_INVALID_STRUCTURE,
978
+ "[dStorage] Payload authenticated as manifest but has invalid structure"
979
+ );
980
+ }
981
+ this.logger?.log(
982
+ "dStorage",
983
+ `Manifest detected: reassembling ${parsed.chunkCount} chunks`
984
+ );
985
+ return this._retrieveChunked(parsed, scheme, tags);
986
+ }
987
+ let decryptedBytes;
988
+ try {
989
+ decryptedBytes = await scheme.decryptPayload(rawBytes, PAYLOAD_AAD);
990
+ } catch (err) {
991
+ throw new DStorageError(
992
+ CoreErrorCode.DECRYPT_PAYLOAD_FAILED,
993
+ `[dStorage] Failed to decrypt payload: ${err}`,
994
+ { cause: err }
995
+ );
996
+ }
997
+ this.logger?.log(
998
+ "dStorage",
999
+ `Retrieved by refId \u2192 storageId: ${storageId}`
1000
+ );
1001
+ return { bytes: decryptedBytes, metadata: {}, tags };
1002
+ }
1213
1003
  try {
1214
- if (this.config.chainAdapter?.init) {
1215
- await this.config.chainAdapter.init();
1004
+ const parsed = JSON.parse(new TextDecoder().decode(rawBytes));
1005
+ if (isManifest(parsed)) {
1006
+ this.logger?.log(
1007
+ "dStorage",
1008
+ `Manifest detected: reassembling ${parsed.chunkCount} chunks`
1009
+ );
1010
+ return this._retrieveChunked(parsed, null, tags);
1216
1011
  }
1217
- this._encryptionProviders = this.config.encryptionAdapters ?? [];
1218
- this._initialized = true;
1219
- return this.config.chainAdapter?.getContractAddress();
1220
- } catch (err) {
1221
- wrapError(err);
1012
+ } catch {
1222
1013
  }
1223
- }
1224
- /** Tear down active state and clear all in-memory key material. */
1225
- destroy() {
1226
- this.wallet = null;
1227
- this._encryptionProviders = [];
1228
- this._initialized = false;
1229
1014
  this.logger?.log(
1230
1015
  "dStorage",
1231
- "Destroyed \u2014 encryption key cleared from memory."
1016
+ `Retrieved by refId \u2192 storageId: ${storageId}`
1232
1017
  );
1018
+ return {
1019
+ bytes: rawBytes,
1020
+ metadata: {},
1021
+ tags
1022
+ };
1233
1023
  }
1234
- get isConnected() {
1235
- return this._initialized || this.wallet !== null;
1236
- }
1237
- get connectedAddress() {
1238
- return this.wallet?.address ?? null;
1239
- }
1240
- // ── Core operations ───────────────────────────────────────────────────────
1241
1024
  /**
1242
- * Encrypt content client-side, store to decentralised storage,
1243
- * and write a cryptographic reference on-chain.
1025
+ * Retrieve and decrypt data using only its on-chain reference ID.
1244
1026
  *
1245
- * Files larger than CHUNK_SIZE (10 MB) are automatically split into chunks,
1246
- * each encrypted and stored independently. A manifest file ties them together.
1247
- * Only the manifest's storageId is written on-chain.
1027
+ * Flow:
1028
+ * 1. Look up the reference on-chain by refId to obtain the storage network ID.
1029
+ * 2. Fetch the blob from the storage network.
1030
+ * 3. Decrypt locally if an encryption key is available.
1248
1031
  *
1249
- * @param bytes Uint8Array to store
1250
- * @param options Optional StoreOptions (metadata, onProgress, isPublic, refId)
1251
- * @returns StoreResult containing the chainRefId/storageId needed to retrieve later
1032
+ * Requires the chain adapter to implement readReference() (part of the
1033
+ * ChainAdapter interface shared by MidnightChainAdapter, MockChainAdapter, etc.).
1034
+ *
1035
+ * @param refId The chainRefId returned from store()
1252
1036
  */
1253
- async store(bytes, options) {
1037
+ async retrieveByRefId(refId) {
1254
1038
  try {
1255
1039
  this.assertConnected();
1256
- const {
1257
- tags = {},
1258
- metadata: rawMeta,
1259
- onProgress,
1260
- isPublic = false,
1261
- refId
1262
- } = options ?? {};
1263
- if (rawMeta !== void 0 && isPublic) {
1040
+ if (!this.config.chainAdapter) {
1264
1041
  throw new DStorageError(
1265
- CoreErrorCode.METADATA_WITH_PUBLIC_UPLOAD,
1266
- "[dStorage] metadata cannot be used with isPublic uploads \u2014 there is no DEK to encrypt it with."
1042
+ CoreErrorCode.RETRIEVE_BY_REF_ID_REQUIRES_CHAIN,
1043
+ "[dStorage] retrieveByRefId() requires a chain adapter. Configure one in DStorageConfig or use retrieveByStorageId() instead."
1267
1044
  );
1268
1045
  }
1269
- if (bytes.length > CHUNK_SIZE) {
1270
- return this._uploadChunked(
1271
- bytes,
1272
- tags,
1273
- onProgress,
1274
- isPublic,
1275
- rawMeta,
1276
- refId
1046
+ const ref = await this.config.chainAdapter.readReference(refId);
1047
+ if (!ref)
1048
+ throw new DStorageError(
1049
+ CoreErrorCode.REF_NOT_FOUND,
1050
+ `[dStorage] No on-chain reference found for refId: ${refId}`
1051
+ );
1052
+ if (ref.storageProvider && ref.storageProvider !== this.config.storageAdapter.name) {
1053
+ throw new DStorageError(
1054
+ CoreErrorCode.STORAGE_ADAPTER_MISMATCH,
1055
+ `[dStorage] Storage adapter mismatch: reference "${refId}" was stored on "${ref.storageProvider}" but the configured adapter is "${this.config.storageAdapter.name}". Configure a "${ref.storageProvider}" storage adapter to retrieve this reference.`
1277
1056
  );
1278
1057
  }
1279
- let uploadData;
1280
- let onChainStorageId;
1281
- const encryptionScheme = isPublic ? "" : "xchacha20poly1305-v1";
1282
- let uploadScheme = null;
1283
- let keyEnvelope = "";
1284
- let dek = null;
1285
- let ownerSecret = new Uint8Array(0);
1286
- if (isPublic) {
1287
- uploadData = bytes;
1058
+ const isPublic = !ref.encryptionScheme;
1059
+ let storageId;
1060
+ let dekScheme;
1061
+ if (!ref.storageId) {
1062
+ if (!isPublic && ref.keyEnvelope) {
1063
+ dekScheme = await this._getDekScheme(ref.keyEnvelope);
1064
+ }
1065
+ } else if (isPublic) {
1066
+ storageId = ref.storageId;
1288
1067
  } else {
1289
- if (this._encryptionProviders.length === 0) {
1068
+ if (!ref.keyEnvelope) {
1290
1069
  throw new DStorageError(
1291
- CoreErrorCode.UPLOAD_NO_PROVIDERS,
1292
- "[dStorage] No encryption providers available. Call init() before uploading."
1070
+ CoreErrorCode.RETRIEVE_BY_REF_ID_ENVELOPE_EMPTY,
1071
+ `[dStorage] Cannot decrypt refId ${refId}: key envelope is empty.`,
1072
+ { cause: { code: "envelope_empty" } }
1293
1073
  );
1294
1074
  }
1295
- dek = generateDek();
1296
- uploadScheme = XChaChaScheme.fromKey(dek);
1297
- this.logger?.log(
1298
- "dStorage",
1299
- `Encrypting content using scheme: ${encryptionScheme}`
1300
- );
1301
- uploadData = await uploadScheme.encryptPayload(bytes, PAYLOAD_AAD);
1302
- const wrapperEntries = await Promise.all(
1303
- this._encryptionProviders.map(async (provider) => ({
1304
- wrapKeyName: provider.name,
1305
- ...await provider.wrapDek(dek)
1306
- }))
1307
- );
1308
- keyEnvelope = serializeKeyEnvelope(buildKeyEnvelope(wrapperEntries));
1075
+ dekScheme = await this._getDekScheme(ref.keyEnvelope);
1076
+ storageId = await dekScheme.decryptStorageId(ref.storageId);
1309
1077
  }
1310
- const sdkVersion = package_default.version ?? "unknown";
1311
- const storageResult = await this.config.storageAdapter.store(uploadData, {
1312
- ...tags,
1313
- [TAG_SDK_VERSION]: sdkVersion,
1314
- [TAG_CONTENT_TYPE]: TAG_CONTENT_TYPE_OCTET_STREAM
1315
- });
1316
- if (isPublic) {
1317
- if (this.config.chainAdapter) {
1318
- if (this._encryptionProviders.length === 0) {
1319
- throw new DStorageError(
1320
- CoreErrorCode.PUBLIC_UPLOAD_REQUIRES_SEED_PROVIDER,
1321
- "[dStorage] Cannot store public data without at least one encryption seed provider. Without a provider there is no owner secret, so the reference could never be removed or updated."
1322
- );
1323
- }
1324
- const ownerSeed = generateDek();
1325
- const ownerSeedWrappers = await Promise.all(
1326
- this._encryptionProviders.map(async (provider) => ({
1327
- wrapKeyName: provider.name,
1328
- ...await provider.wrapDek(ownerSeed)
1329
- }))
1330
- );
1331
- keyEnvelope = serializeKeyEnvelope(
1332
- buildKeyEnvelope(ownerSeedWrappers)
1333
- );
1334
- ownerSecret = await deriveOwnerSecret(
1335
- ownerSeed,
1336
- new TextEncoder().encode(storageResult.id),
1337
- "dstorage:owner-secret:public:v1"
1338
- );
1339
- }
1340
- onChainStorageId = storageResult.id;
1078
+ let contentBytes;
1079
+ let contentTags;
1080
+ if (storageId !== void 0) {
1081
+ const r = await this.retrieveByStorageId(
1082
+ storageId,
1083
+ dekScheme,
1084
+ ref.contentHash
1085
+ );
1086
+ contentBytes = r.bytes;
1087
+ contentTags = r.tags;
1341
1088
  } else {
1342
- onChainStorageId = await uploadScheme.encryptStorageId(
1343
- storageResult.id
1089
+ contentBytes = new Uint8Array(0);
1090
+ contentTags = {};
1091
+ }
1092
+ let metadata = {};
1093
+ if (ref.encryptedMetadata && dekScheme) {
1094
+ const raw = await dekScheme.decryptPayload(
1095
+ base64urlToBytes(ref.encryptedMetadata),
1096
+ METADATA_AAD
1344
1097
  );
1345
- ownerSecret = await deriveOwnerSecret(
1346
- dek,
1347
- new TextEncoder().encode(storageResult.id)
1098
+ metadata = JSON.parse(new TextDecoder().decode(raw));
1099
+ }
1100
+ return { bytes: contentBytes, tags: contentTags, metadata };
1101
+ } catch (err) {
1102
+ wrapError(err);
1103
+ }
1104
+ }
1105
+ /**
1106
+ * List references currently available from the configured chain adapter.
1107
+ */
1108
+ async listReferences(options) {
1109
+ try {
1110
+ if (!this.config.chainAdapter) {
1111
+ throw new DStorageError(
1112
+ CoreErrorCode.LIST_REFS_REQUIRES_CHAIN,
1113
+ "[dStorage] listReferences() requires a chain adapter."
1348
1114
  );
1349
1115
  }
1350
- const storageCost = {
1351
- amount: storageResult.cost?.amount ?? "unknown",
1352
- token: storageResult.cost?.token ?? "AR"
1353
- };
1354
- if (this.config.chainAdapter) {
1355
- const contentHash = await computeBlake3Hex(uploadData);
1356
- const storeRecovery = {
1357
- storageId: storageResult.id,
1358
- storageProvider: storageResult.provider,
1359
- keyEnvelope: isPublic ? void 0 : keyEnvelope,
1360
- contentHash
1361
- };
1362
- onProgress?.({
1363
- phase: "stored",
1364
- chunksUploaded: 1,
1365
- totalChunks: 1,
1366
- bytesUploaded: bytes.length,
1367
- totalBytes: bytes.length,
1368
- recovery: storeRecovery
1369
- });
1370
- const encryptedMetadata = rawMeta ? bytesToBase64url(
1371
- await uploadScheme.encryptPayload(
1372
- new TextEncoder().encode(JSON.stringify(rawMeta)),
1373
- METADATA_AAD
1374
- )
1375
- ) : void 0;
1376
- try {
1377
- const chainResult = await this.config.chainAdapter.writeReference({
1378
- storageId: onChainStorageId,
1379
- encryptionScheme,
1380
- storageProvider: storageResult.provider,
1381
- ...refId !== void 0 ? { refId } : {},
1382
- ...encryptedMetadata !== void 0 ? { encryptedMetadata } : {},
1383
- writtenAt: Date.now(),
1384
- keyEnvelope,
1385
- contentHash,
1386
- ownerSecret
1387
- });
1388
- const resultRefId = chainResult.refId;
1389
- this.logger?.log(
1390
- "dStorage",
1391
- `Upload complete \u2192 refId: ${resultRefId}`
1116
+ let rawItems;
1117
+ if (options?.refIds && options.refIds.length > 0) {
1118
+ rawItems = await Promise.all(
1119
+ options.refIds.map(async (refId) => {
1120
+ const ref = await this.config.chainAdapter.readReference(refId);
1121
+ return {
1122
+ refId,
1123
+ storageId: ref.storageId ?? null,
1124
+ encryptionScheme: ref.encryptionScheme ?? null,
1125
+ keyEnvelope: ref.keyEnvelope,
1126
+ storageProvider: ref.storageProvider,
1127
+ ownerCommitment: "",
1128
+ ...ref.encryptedMetadata !== void 0 ? { encryptedMetadata: ref.encryptedMetadata } : {}
1129
+ };
1130
+ })
1131
+ );
1132
+ } else {
1133
+ if (typeof this.config.chainAdapter.listReferences !== "function") {
1134
+ throw new DStorageError(
1135
+ CoreErrorCode.LIST_REFS_NOT_SUPPORTED,
1136
+ "[dStorage] listReferences() is not supported by this chain adapter."
1392
1137
  );
1393
- return {
1394
- chainRefId: resultRefId,
1395
- storageId: storageResult.id,
1396
- storageProvider: storageResult.provider,
1397
- chainProvider: chainResult.provider,
1398
- uploadedAt: storageResult.uploadedAt,
1399
- encryptionScheme,
1400
- costEstimate: {
1401
- storageCost,
1402
- chainCost: {
1403
- amount: chainResult.paymentReceipt?.amount ?? "unknown",
1404
- token: chainResult.paymentReceipt?.token ?? "DUST"
1405
- },
1406
- fileSizeBytes: uploadData.length
1407
- }
1408
- };
1409
- } catch {
1410
- throw new StorePartialError(storeRecovery);
1411
1138
  }
1139
+ rawItems = await this.config.chainAdapter.listReferences();
1412
1140
  }
1413
- this.logger?.log(
1414
- "dStorage",
1415
- `Upload complete (storage-only) \u2192 storageId: ${storageResult.id}`
1141
+ return Promise.all(
1142
+ rawItems.map(async (item) => {
1143
+ let storageId = null;
1144
+ let decryptionError;
1145
+ const isPublic = !item.encryptionScheme;
1146
+ let dekScheme;
1147
+ if (item.storageId) {
1148
+ if (isPublic) {
1149
+ storageId = item.storageId;
1150
+ } else {
1151
+ try {
1152
+ dekScheme = await this._getDekScheme(item.keyEnvelope);
1153
+ storageId = await dekScheme.decryptStorageId(item.storageId);
1154
+ } catch (err) {
1155
+ decryptionError = err instanceof Error ? err.message : String(err);
1156
+ }
1157
+ }
1158
+ }
1159
+ if (!decryptionError) {
1160
+ if (isPublic) {
1161
+ try {
1162
+ await this._unwrapDek(item.keyEnvelope);
1163
+ } catch (err) {
1164
+ decryptionError = err instanceof Error ? err.message : String(err);
1165
+ }
1166
+ }
1167
+ }
1168
+ let metadata;
1169
+ if (item.encryptedMetadata && dekScheme) {
1170
+ try {
1171
+ const raw = await dekScheme.decryptPayload(
1172
+ base64urlToBytes(item.encryptedMetadata),
1173
+ METADATA_AAD
1174
+ );
1175
+ metadata = JSON.parse(new TextDecoder().decode(raw));
1176
+ } catch {
1177
+ }
1178
+ }
1179
+ return {
1180
+ refId: item.refId,
1181
+ storageId,
1182
+ storageProvider: item.storageProvider,
1183
+ encryptionScheme: item.encryptionScheme ?? null,
1184
+ keyEnvelope: item.keyEnvelope,
1185
+ ownerCommitment: item.ownerCommitment,
1186
+ ...decryptionError !== void 0 ? { decryptionError } : {},
1187
+ ...metadata !== void 0 ? { metadata } : {}
1188
+ };
1189
+ })
1416
1190
  );
1417
- return {
1418
- storageId: storageResult.id,
1419
- storageProvider: storageResult.provider,
1420
- uploadedAt: storageResult.uploadedAt,
1421
- encryptionScheme,
1422
- ...uploadScheme ? { cryptoScheme: uploadScheme } : {},
1423
- costEstimate: { storageCost, fileSizeBytes: uploadData.length }
1424
- };
1425
1191
  } catch (err) {
1426
1192
  wrapError(err);
1427
1193
  }
1428
1194
  }
1429
1195
  /**
1430
- * Register a pre-existing storageId as a new on-chain reference.
1431
- *
1432
- * Use this when data has already been uploaded to the storage network by
1433
- * other means (e.g. a third-party tool or a separate pipeline). The content
1434
- * is not touched — only the chain reference is created.
1435
- *
1436
- * A fresh DEK is generated solely to encrypt the storageId and derive the
1437
- * ownerSecret. The resulting reference round-trips through retrieveByRefId()
1438
- * exactly like one created by store().
1439
- *
1440
- * Note: chunked manifests are not handled here. For large pre-existing
1441
- * uploads already split into chunks, register the manifest storageId.
1196
+ * Remove a reference from the on-chain registry.
1197
+ * Only the owner of the reference can successfully call this — the chain
1198
+ * enforces ownership via ZK proof using the witness-derived ownerSecret.
1442
1199
  */
1443
- async registerReference(options = {}) {
1200
+ async removeReference(refId) {
1444
1201
  try {
1445
- this.assertConnected();
1446
1202
  if (!this.config.chainAdapter) {
1447
1203
  throw new DStorageError(
1448
- CoreErrorCode.REGISTER_REF_REQUIRES_CHAIN,
1449
- "[dStorage] registerReference requires a chain adapter."
1204
+ CoreErrorCode.REMOVE_REF_REQUIRES_CHAIN,
1205
+ "[dStorage] removeReference() requires a chain adapter."
1450
1206
  );
1451
1207
  }
1452
- if (this._encryptionProviders.length === 0) {
1208
+ if (typeof this.config.chainAdapter.removeReference !== "function") {
1453
1209
  throw new DStorageError(
1454
- CoreErrorCode.REGISTER_REF_REQUIRES_ENCRYPTION,
1455
- "[dStorage] registerReference requires at least one encryption adapter."
1210
+ CoreErrorCode.REMOVE_REF_NOT_SUPPORTED,
1211
+ "[dStorage] removeReference() is not supported by this chain adapter."
1456
1212
  );
1457
1213
  }
1458
- let dek;
1459
- const recoveryEnvelope = options.keyEnvelope;
1460
- if (recoveryEnvelope) {
1461
- dek = await this._unwrapDek(recoveryEnvelope);
1214
+ const ownerSecret = await this._computeOwnerSecret(refId);
1215
+ await this.config.chainAdapter.removeReference(refId, ownerSecret);
1216
+ } catch (err) {
1217
+ wrapError(err);
1218
+ }
1219
+ }
1220
+ /**
1221
+ * Estimate the full cost (storage + chain) for a given payload size before committing.
1222
+ */
1223
+ async estimateCost(sizeBytes) {
1224
+ try {
1225
+ const [storageQuote, chainQuote] = await Promise.all([
1226
+ this.config.storageAdapter.estimateCost(sizeBytes),
1227
+ this.config.chainAdapter?.estimateCost()
1228
+ ]);
1229
+ return {
1230
+ storageCost: { amount: storageQuote.amount, token: storageQuote.token },
1231
+ ...chainQuote ? {
1232
+ chainCost: { amount: chainQuote.amount, token: chainQuote.token }
1233
+ } : {},
1234
+ fileSizeBytes: sizeBytes
1235
+ };
1236
+ } catch (err) {
1237
+ wrapError(err);
1238
+ }
1239
+ }
1240
+ // ── Private helpers ────────────────────────────────────────────────────────
1241
+ /**
1242
+ * Upload large content by splitting into CHUNK_SIZE pieces, encrypting each
1243
+ * independently, and storing a manifest that ties them together.
1244
+ * Only the manifest's storageId is written on-chain.
1245
+ */
1246
+ async _uploadChunked(bytes, tags, onProgress, isPublic = false, rawMeta, refId) {
1247
+ if (this._encryptionProviders.length === 0) {
1248
+ throw new DStorageError(
1249
+ CoreErrorCode.UPLOAD_NO_PROVIDERS,
1250
+ "[dStorage] No encryption providers available. Call init() before uploading."
1251
+ );
1252
+ }
1253
+ const totalChunks = Math.ceil(bytes.length / CHUNK_SIZE);
1254
+ const sdkVersion = package_default.version ?? "unknown";
1255
+ const encryptionScheme = isPublic ? "" : "xchacha20poly1305-v1";
1256
+ this.logger?.log(
1257
+ "dStorage",
1258
+ `Chunked upload: ${bytes.length} bytes \u2192 ${totalChunks} chunks${isPublic ? "" : `, encryption scheme: ${encryptionScheme}`}`
1259
+ );
1260
+ let uploadScheme = null;
1261
+ let keyEnvelope;
1262
+ let chunkDek;
1263
+ let ownerSeed;
1264
+ if (!isPublic) {
1265
+ chunkDek = generateDek();
1266
+ uploadScheme = XChaChaScheme.fromKey(chunkDek);
1267
+ const wrapperEntries = await Promise.all(
1268
+ this._encryptionProviders.map(async (provider) => ({
1269
+ wrapKeyName: provider.name,
1270
+ ...await provider.wrapDek(chunkDek)
1271
+ }))
1272
+ );
1273
+ keyEnvelope = serializeKeyEnvelope(buildKeyEnvelope(wrapperEntries));
1274
+ } else {
1275
+ ownerSeed = generateDek();
1276
+ const ownerSeedWrappers = await Promise.all(
1277
+ this._encryptionProviders.map(async (provider) => ({
1278
+ wrapKeyName: provider.name,
1279
+ ...await provider.wrapDek(ownerSeed)
1280
+ }))
1281
+ );
1282
+ keyEnvelope = serializeKeyEnvelope(buildKeyEnvelope(ownerSeedWrappers));
1283
+ }
1284
+ const chunkEntries = [];
1285
+ let bytesUploaded = 0;
1286
+ for (let i = 0; i < totalChunks; i++) {
1287
+ const start = i * CHUNK_SIZE;
1288
+ const chunk = bytes.subarray(start, start + CHUNK_SIZE);
1289
+ onProgress?.({
1290
+ phase: "encrypting",
1291
+ chunksUploaded: i,
1292
+ totalChunks,
1293
+ bytesUploaded,
1294
+ totalBytes: bytes.length
1295
+ });
1296
+ let chunkData;
1297
+ if (isPublic) {
1298
+ chunkData = chunk;
1462
1299
  } else {
1463
- dek = generateDek();
1300
+ chunkData = await uploadScheme.encryptPayload(
1301
+ chunk,
1302
+ chunkAad(i, totalChunks)
1303
+ );
1304
+ }
1305
+ onProgress?.({
1306
+ phase: "uploading",
1307
+ chunksUploaded: i,
1308
+ totalChunks,
1309
+ bytesUploaded,
1310
+ totalBytes: bytes.length
1311
+ });
1312
+ const chunkResult = await this.config.storageAdapter.store(chunkData, {
1313
+ ...tags,
1314
+ [TAG_SDK_VERSION]: sdkVersion,
1315
+ [TAG_CONTENT_TYPE]: TAG_CONTENT_TYPE_OCTET_STREAM,
1316
+ [TAG_CHUNK_INDEX]: String(i),
1317
+ [TAG_CHUNK_TOTAL]: String(totalChunks)
1318
+ });
1319
+ chunkEntries.push({
1320
+ index: i,
1321
+ storageId: chunkResult.id,
1322
+ size: chunk.length
1323
+ });
1324
+ bytesUploaded += chunk.length;
1325
+ onProgress?.({
1326
+ phase: "uploading",
1327
+ chunksUploaded: i + 1,
1328
+ totalChunks,
1329
+ bytesUploaded,
1330
+ totalBytes: bytes.length
1331
+ });
1332
+ }
1333
+ onProgress?.({
1334
+ phase: "finalizing",
1335
+ chunksUploaded: totalChunks,
1336
+ totalChunks,
1337
+ bytesUploaded,
1338
+ totalBytes: bytes.length
1339
+ });
1340
+ const manifest = {
1341
+ v: 1,
1342
+ type: "dstorage-chunked-manifest",
1343
+ totalSize: bytes.length,
1344
+ chunkSize: CHUNK_SIZE,
1345
+ chunkCount: totalChunks,
1346
+ provider: this.config.storageAdapter.name,
1347
+ chunks: chunkEntries
1348
+ };
1349
+ const manifestBytes = new TextEncoder().encode(JSON.stringify(manifest));
1350
+ let manifestData;
1351
+ if (isPublic) {
1352
+ manifestData = manifestBytes;
1353
+ } else {
1354
+ manifestData = await uploadScheme.encryptPayload(
1355
+ manifestBytes,
1356
+ MANIFEST_AAD
1357
+ );
1358
+ }
1359
+ const manifestResult = await this.config.storageAdapter.store(
1360
+ manifestData,
1361
+ {
1362
+ [TAG_SDK_VERSION]: sdkVersion,
1363
+ [TAG_CONTENT_TYPE]: isPublic ? TAG_CONTENT_TYPE_JSON : TAG_CONTENT_TYPE_OCTET_STREAM,
1364
+ [TAG_MANIFEST_UPLOAD]: "true"
1464
1365
  }
1366
+ );
1367
+ let onChainManifestId;
1368
+ if (isPublic) {
1369
+ onChainManifestId = manifestResult.id;
1370
+ } else {
1371
+ onChainManifestId = await uploadScheme.encryptStorageId(
1372
+ manifestResult.id
1373
+ );
1374
+ }
1375
+ const ownerSecret = chunkDek ? await deriveOwnerSecret(
1376
+ chunkDek,
1377
+ new TextEncoder().encode(manifestResult.id)
1378
+ ) : await deriveOwnerSecret(
1379
+ ownerSeed,
1380
+ new TextEncoder().encode(manifestResult.id),
1381
+ "dstorage:owner-secret:public:v1"
1382
+ );
1383
+ if (this.config.chainAdapter) {
1384
+ const contentHash = await computeBlake3Hex(manifestData);
1385
+ const storeRecovery = {
1386
+ storageId: manifestResult.id,
1387
+ storageProvider: manifestResult.provider,
1388
+ keyEnvelope: isPublic ? void 0 : keyEnvelope,
1389
+ contentHash
1390
+ };
1391
+ onProgress?.({
1392
+ phase: "stored",
1393
+ chunksUploaded: totalChunks,
1394
+ totalChunks,
1395
+ bytesUploaded: bytes.length,
1396
+ totalBytes: bytes.length,
1397
+ recovery: storeRecovery
1398
+ });
1399
+ const encryptedMetadata = rawMeta && uploadScheme ? bytesToBase64url(
1400
+ await uploadScheme.encryptPayload(
1401
+ new TextEncoder().encode(JSON.stringify(rawMeta)),
1402
+ METADATA_AAD
1403
+ )
1404
+ ) : void 0;
1465
1405
  try {
1466
- const scheme = XChaChaScheme.fromKey(dek);
1467
- const { metadata: rawMeta } = options;
1468
- const encryptedMetadata = rawMeta ? bytesToBase64url(
1469
- await scheme.encryptPayload(
1470
- new TextEncoder().encode(JSON.stringify(rawMeta)),
1471
- METADATA_AAD
1472
- )
1473
- ) : void 0;
1474
- const onChainStorageId = options.storageId !== void 0 ? await scheme.encryptStorageId(options.storageId) : void 0;
1475
- let keyEnvelope;
1476
- if (recoveryEnvelope !== void 0) {
1477
- keyEnvelope = recoveryEnvelope;
1478
- } else {
1479
- const wrapperEntries = await Promise.all(
1480
- this._encryptionProviders.map(async (p) => ({
1481
- wrapKeyName: p.name,
1482
- ...await p.wrapDek(dek)
1483
- }))
1484
- );
1485
- keyEnvelope = serializeKeyEnvelope(buildKeyEnvelope(wrapperEntries));
1486
- }
1487
- const ownerSecret = await deriveOwnerSecret(
1488
- dek,
1489
- options.storageId !== void 0 ? new TextEncoder().encode(options.storageId) : METADATA_ONLY_OWNER_SECRET_SALT
1490
- );
1491
1406
  const chainResult = await this.config.chainAdapter.writeReference({
1492
- ...onChainStorageId !== void 0 ? { storageId: onChainStorageId } : {},
1493
- encryptionScheme: "xchacha20poly1305-v1",
1494
- storageProvider: options.storageProvider ?? this.config.storageAdapter.name,
1495
- ...options.refId !== void 0 ? { refId: options.refId } : {},
1407
+ storageId: onChainManifestId,
1408
+ encryptionScheme,
1409
+ storageProvider: manifestResult.provider,
1410
+ ...refId !== void 0 ? { refId } : {},
1496
1411
  ...encryptedMetadata !== void 0 ? { encryptedMetadata } : {},
1497
1412
  writtenAt: Date.now(),
1498
1413
  keyEnvelope,
1499
- ...options.contentHash !== void 0 ? { contentHash: options.contentHash } : {},
1414
+ contentHash,
1500
1415
  ownerSecret
1501
1416
  });
1502
- return { chainRefId: chainResult.refId };
1503
- } finally {
1504
- dek.fill(0);
1505
- }
1506
- } catch (err) {
1507
- wrapError(err);
1508
- }
1509
- }
1510
- /**
1511
- * Store new content and update an existing on-chain reference in place.
1512
- *
1513
- * Ownership is proved by re-deriving the ownerSecret from the existing
1514
- * reference — the same mechanism used by removeReference(). A fresh DEK is
1515
- * generated for the new content; the previous version is not modified and remains
1516
- * independently decryptable if the caller retained the old storageId.
1517
- *
1518
- * Note: chunking is not applied here. For large files use store() to get a
1519
- * new chunked reference, then removeReference() on the old one.
1520
- *
1521
- * @param refId The chain reference to update (returned by a prior store/registerReference).
1522
- * @param bytes New content to encrypt and store.
1523
- * @param options Optional StoreOptions. isPublic is ignored.
1524
- */
1525
- async update(refId, bytes, options) {
1526
- try {
1527
- this.assertConnected();
1528
- if (!this.config.chainAdapter) {
1529
- throw new DStorageError(
1530
- CoreErrorCode.UPDATE_UPLOAD_REQUIRES_CHAIN,
1531
- "[dStorage] update requires a chain adapter."
1532
- );
1533
- }
1534
- if (typeof this.config.chainAdapter.updateReference !== "function") {
1535
- throw new DStorageError(
1536
- CoreErrorCode.UPDATE_UPLOAD_NOT_SUPPORTED,
1537
- "[dStorage] update is not supported by this chain adapter."
1538
- );
1539
- }
1540
- if (this._encryptionProviders.length === 0) {
1541
- throw new DStorageError(
1542
- CoreErrorCode.UPDATE_UPLOAD_REQUIRES_ENCRYPTION,
1543
- "[dStorage] update requires at least one encryption adapter."
1417
+ const resultRefId = chainResult.refId;
1418
+ this.logger?.log(
1419
+ "dStorage",
1420
+ `Chunked upload complete \u2192 ${totalChunks} chunks + manifest, refId: ${resultRefId}`
1544
1421
  );
1545
- }
1546
- const { tags = {}, metadata: rawMeta } = options ?? {};
1547
- const ownerSecret = await this._computeOwnerSecret(refId);
1548
- const dek = generateDek();
1549
- try {
1550
- const scheme = XChaChaScheme.fromKey(dek);
1551
- const uploadData = await scheme.encryptPayload(bytes, PAYLOAD_AAD);
1552
- const storageResult = await this.config.storageAdapter.store(
1553
- uploadData,
1554
- {
1555
- ...tags,
1556
- [TAG_CONTENT_TYPE]: TAG_CONTENT_TYPE_OCTET_STREAM
1422
+ return {
1423
+ chainRefId: resultRefId,
1424
+ storageId: manifestResult.id,
1425
+ storageProvider: manifestResult.provider,
1426
+ chainProvider: chainResult.provider,
1427
+ uploadedAt: manifestResult.uploadedAt,
1428
+ encryptionScheme,
1429
+ costEstimate: {
1430
+ storageCost: {
1431
+ amount: manifestResult.cost?.amount ?? "unknown",
1432
+ token: manifestResult.cost?.token ?? "AR"
1433
+ },
1434
+ chainCost: {
1435
+ amount: chainResult.paymentReceipt?.amount ?? "unknown",
1436
+ token: chainResult.paymentReceipt?.token ?? "DUST"
1437
+ },
1438
+ fileSizeBytes: bytes.length
1557
1439
  }
1558
- );
1559
- const onChainStorageId = await scheme.encryptStorageId(
1560
- storageResult.id
1561
- );
1562
- const wrapperEntries = await Promise.all(
1563
- this._encryptionProviders.map(async (p) => ({
1564
- wrapKeyName: p.name,
1565
- ...await p.wrapDek(dek)
1566
- }))
1567
- );
1568
- const keyEnvelope = serializeKeyEnvelope(
1569
- buildKeyEnvelope(wrapperEntries)
1570
- );
1571
- const encryptedMetadata = rawMeta ? bytesToBase64url(
1572
- await scheme.encryptPayload(
1573
- new TextEncoder().encode(JSON.stringify(rawMeta)),
1574
- METADATA_AAD
1575
- )
1576
- ) : void 0;
1577
- const contentHash = await computeBlake3Hex(uploadData);
1578
- const rawNewStorageId = new TextEncoder().encode(storageResult.id);
1579
- const newOwnerSecret = await deriveOwnerSecret(dek, rawNewStorageId);
1580
- const storeRecovery = {
1581
- storageId: storageResult.id,
1582
- storageProvider: storageResult.provider,
1583
- keyEnvelope,
1584
- contentHash
1585
1440
  };
1586
- options?.onProgress?.({
1587
- phase: "stored",
1588
- chunksUploaded: 1,
1589
- totalChunks: 1,
1590
- bytesUploaded: bytes.length,
1591
- totalBytes: bytes.length,
1592
- recovery: storeRecovery
1593
- });
1594
- try {
1595
- await this.config.chainAdapter.updateReference(
1596
- refId,
1597
- {
1598
- storageId: onChainStorageId,
1599
- encryptionScheme: "xchacha20poly1305-v1",
1600
- storageProvider: storageResult.provider,
1601
- ...encryptedMetadata !== void 0 ? { encryptedMetadata } : {},
1602
- writtenAt: Date.now(),
1603
- keyEnvelope,
1604
- contentHash
1605
- },
1606
- ownerSecret,
1607
- newOwnerSecret
1608
- );
1609
- } catch {
1610
- throw new StorePartialError(storeRecovery);
1611
- }
1612
- return { chainRefId: refId, storageId: storageResult.id };
1613
- } finally {
1614
- dek.fill(0);
1441
+ } catch {
1442
+ throw new StorePartialError(storeRecovery);
1615
1443
  }
1616
- } catch (err) {
1617
- wrapError(err);
1618
1444
  }
1619
- }
1620
- /**
1621
- * Retry only the on-chain reference write after a failed `update()` call.
1622
- *
1623
- * Use this when `update()` throws a `StorePartialError` — meaning the new
1624
- * content was stored successfully but the chain write failed. Call with the
1625
- * `refId` you passed to `update()` and the `err.recovery` from the caught
1626
- * error (or the `recovery` emitted via `onProgress` at phase `"stored"`).
1627
- *
1628
- * The new content is already in storage; this method re-derives all chain
1629
- * write parameters from the recovery envelope without re-uploading.
1630
- */
1631
- async retryUpdate(refId, recovery) {
1632
- try {
1633
- this.assertConnected();
1634
- if (!this.config.chainAdapter) {
1635
- throw new DStorageError(
1636
- CoreErrorCode.UPDATE_UPLOAD_REQUIRES_CHAIN,
1637
- "[dStorage] retryUpdate requires a chain adapter."
1638
- );
1445
+ this.logger?.log(
1446
+ "dStorage",
1447
+ `Chunked upload complete (storage-only) \u2192 ${totalChunks} chunks + manifest, storageId: ${manifestResult.id}`
1448
+ );
1449
+ return {
1450
+ storageId: manifestResult.id,
1451
+ storageProvider: manifestResult.provider,
1452
+ uploadedAt: manifestResult.uploadedAt,
1453
+ encryptionScheme,
1454
+ costEstimate: {
1455
+ storageCost: {
1456
+ amount: manifestResult.cost?.amount ?? "unknown",
1457
+ token: manifestResult.cost?.token ?? "AR"
1458
+ },
1459
+ fileSizeBytes: bytes.length
1639
1460
  }
1640
- if (typeof this.config.chainAdapter.updateReference !== "function") {
1461
+ };
1462
+ }
1463
+ /**
1464
+ * Reassemble a chunked file from its manifest.
1465
+ * Fetches and decrypts each chunk sequentially to bound memory usage.
1466
+ */
1467
+ async _retrieveChunked(manifest, scheme = null, tags = {}) {
1468
+ if (manifest.chunks.length !== manifest.chunkCount) {
1469
+ throw new DStorageError(
1470
+ CoreErrorCode.MANIFEST_CHUNK_COUNT_MISMATCH,
1471
+ `[dStorage] Manifest declares ${manifest.chunkCount} chunks but contains ${manifest.chunks.length} entries`
1472
+ );
1473
+ }
1474
+ const sorted = [...manifest.chunks].sort((a, b) => a.index - b.index);
1475
+ for (let i = 0; i < sorted.length; i++) {
1476
+ if (sorted[i].index !== i) {
1641
1477
  throw new DStorageError(
1642
- CoreErrorCode.UPDATE_UPLOAD_NOT_SUPPORTED,
1643
- "[dStorage] retryUpdate is not supported by this chain adapter."
1478
+ CoreErrorCode.MANIFEST_NOT_CONTIGUOUS,
1479
+ `[dStorage] Manifest chunk list is not contiguous: expected index ${i}, got ${sorted[i].index}`
1644
1480
  );
1645
1481
  }
1646
- if (this._encryptionProviders.length === 0) {
1647
- throw new DStorageError(
1648
- CoreErrorCode.UPDATE_UPLOAD_REQUIRES_ENCRYPTION,
1649
- "[dStorage] retryUpdate requires at least one encryption adapter."
1650
- );
1482
+ }
1483
+ const result = new Uint8Array(manifest.totalSize);
1484
+ let offset = 0;
1485
+ for (const entry of sorted) {
1486
+ const { bytes: rawChunkBytes } = await this.config.storageAdapter.retrieve(entry.storageId);
1487
+ let chunkBytes = rawChunkBytes;
1488
+ if (scheme) {
1489
+ try {
1490
+ chunkBytes = await scheme.decryptPayload(
1491
+ rawChunkBytes,
1492
+ chunkAad(entry.index, manifest.chunkCount)
1493
+ );
1494
+ } catch (err) {
1495
+ throw new DStorageError(
1496
+ CoreErrorCode.DECRYPT_CHUNK_FAILED,
1497
+ `[dStorage] Failed to decrypt chunk ${entry.index}: ${err}`,
1498
+ { cause: err }
1499
+ );
1500
+ }
1651
1501
  }
1652
- if (!recovery.keyEnvelope) {
1502
+ if (offset + chunkBytes.length > manifest.totalSize) {
1653
1503
  throw new DStorageError(
1654
- CoreErrorCode.UNWRAP_DEK_ENVELOPE_EMPTY,
1655
- "[dStorage] retryUpdate requires a key envelope in the recovery object."
1656
- );
1657
- }
1658
- const dek = await this._unwrapDek(recovery.keyEnvelope);
1659
- try {
1660
- const scheme = XChaChaScheme.fromKey(dek);
1661
- const ownerSecret = await this._computeOwnerSecret(refId);
1662
- const onChainStorageId = await scheme.encryptStorageId(
1663
- recovery.storageId
1664
- );
1665
- const newOwnerSecret = await deriveOwnerSecret(
1666
- dek,
1667
- new TextEncoder().encode(recovery.storageId)
1668
- );
1669
- await this.config.chainAdapter.updateReference(
1670
- refId,
1671
- {
1672
- storageId: onChainStorageId,
1673
- encryptionScheme: "xchacha20poly1305-v1",
1674
- storageProvider: recovery.storageProvider,
1675
- writtenAt: Date.now(),
1676
- keyEnvelope: recovery.keyEnvelope,
1677
- ...recovery.contentHash !== void 0 ? { contentHash: recovery.contentHash } : {}
1678
- },
1679
- ownerSecret,
1680
- newOwnerSecret
1504
+ CoreErrorCode.CHUNK_OVERFLOWS_TOTAL_SIZE,
1505
+ `[dStorage] Chunk ${entry.index} overflows manifest totalSize`
1681
1506
  );
1682
- return { chainRefId: refId, storageId: recovery.storageId };
1683
- } finally {
1684
- dek.fill(0);
1685
1507
  }
1686
- } catch (err) {
1687
- wrapError(err);
1508
+ result.set(chunkBytes, offset);
1509
+ offset += chunkBytes.length;
1510
+ this.logger?.log(
1511
+ "dStorage",
1512
+ `Retrieved chunk ${entry.index + 1}/${manifest.chunkCount}: ${chunkBytes.length} bytes`
1513
+ );
1514
+ }
1515
+ if (offset !== manifest.totalSize) {
1516
+ throw new DStorageError(
1517
+ CoreErrorCode.REASSEMBLY_SIZE_MISMATCH,
1518
+ `[dStorage] Reassembled ${offset} bytes but manifest declares totalSize ${manifest.totalSize}`
1519
+ );
1520
+ }
1521
+ return {
1522
+ bytes: result,
1523
+ metadata: {},
1524
+ tags
1525
+ };
1526
+ }
1527
+ assertConnected() {
1528
+ if (!this._initialized && this.wallet === null) {
1529
+ throw new DStorageError(
1530
+ CoreErrorCode.NOT_INITIALIZED,
1531
+ "[dStorage] Not connected. Call dStorage.init() first."
1532
+ );
1688
1533
  }
1689
1534
  }
1690
- /**
1691
- * Rotate the encryption adapters protecting an existing reference without
1692
- * re-uploading the content.
1693
- *
1694
- * Useful when you want to:
1695
- * - Change a password: configure [newPassword, mnemonic] and call rotateKeys().
1696
- * The mnemonic unwraps the old DEK, the new envelope is wrapped under both
1697
- * new adapters, and the old password wrapper is dropped.
1698
- * - Add or remove adapters: any single adapter that matches a wrapper in the
1699
- * existing key envelope can authorise the rotation. The resulting envelope
1700
- * is wrapped under exactly the adapters currently configured on this SDK.
1701
- *
1702
- * The content in storage is not touched — only the on-chain key envelope is
1703
- * updated. writtenAt is preserved to reflect the original upload time.
1704
- */
1705
- async rotateKeys(refId) {
1535
+ };
1536
+
1537
+ // ../storage/dist/chunk-URI7FNOW.mjs
1538
+ var StorageErrorCode = {
1539
+ // adapters/mock.ts (15000–15049)
1540
+ MOCK_AUTH_TOKEN_REQUIRED: 15001,
1541
+ MOCK_DATA_NOT_FOUND: 15002,
1542
+ // adapters/integrity.ts (15050–15149)
1543
+ CONTENT_HASH_MISMATCH: 15050,
1544
+ // adapters/arweave-local.ts (15150–15199)
1545
+ ARWEAVE_LOCAL_INVALID_ADDRESS: 15150,
1546
+ ARWEAVE_LOCAL_NOT_INSTALLED: 15151,
1547
+ // adapters/arweave.ts (15200–15399)
1548
+ ARWEAVE_AUTH_TOKEN_REQUIRED: 15200,
1549
+ ARWEAVE_NOT_INSTALLED: 15201,
1550
+ ARWEAVE_NO_SIGNED_TX: 15202,
1551
+ ARWEAVE_INVALID_TX_ID: 15203,
1552
+ ARWEAVE_TX_TOO_LARGE: 15204,
1553
+ ARWEAVE_TX_METADATA_FAILED: 15205,
1554
+ ARWEAVE_NO_DATA_RETURNED: 15206,
1555
+ ARWEAVE_SIZE_MISMATCH: 15207,
1556
+ ARWEAVE_HASH_TAG_MISSING: 15208,
1557
+ ARWEAVE_UPLOADER_INIT_FAILED: 15209,
1558
+ ARWEAVE_CHUNK_UPLOAD_FAILED: 15210,
1559
+ ARWEAVE_CONFIRM_TIMEOUT: 15211,
1560
+ ARWEAVE_GET_DATA_FAILED: 15212,
1561
+ // adapters/arweave-bundler.ts (15400–15599)
1562
+ ARWEAVE_BUNDLER_AUTH_TOKEN_REQUIRED: 15400,
1563
+ ARWEAVE_BUNDLER_LOCAL_NOT_SUPPORTED: 15401,
1564
+ ARWEAVE_BUNDLER_INVALID_TX_ID: 15402,
1565
+ ARWEAVE_BUNDLER_TX_METADATA_FAILED: 15403,
1566
+ ARWEAVE_BUNDLER_HASH_TAG_MISSING: 15404,
1567
+ ARWEAVE_BUNDLER_RETRIEVE_HTTP_ERROR: 15405,
1568
+ ARWEAVE_BUNDLER_SIZE_LIMIT_EXCEEDED: 15406,
1569
+ ARWEAVE_BUNDLER_SIZE_MISMATCH: 15407,
1570
+ ARWEAVE_BUNDLER_COST_ESTIMATE_FAILED: 15408,
1571
+ ARWEAVE_BUNDLER_NO_COST_ESTIMATE: 15409,
1572
+ ARWEAVE_BUNDLER_INVALID_SIGN_TX_RESPONSE: 15410,
1573
+ ARWEAVE_BUNDLER_INVALID_AUTH_TOKEN_FORMAT: 15411,
1574
+ ARWEAVE_BUNDLER_INVALID_AUTH_TOKEN_KEY: 15412,
1575
+ ARWEAVE_BUNDLER_NO_TX_ID_RETURNED: 15413,
1576
+ ARWEAVE_BUNDLER_UPLOAD_FAILED: 15414
1577
+ };
1578
+
1579
+ // ../payment/dist/chunk-IVL4WQ5Y.mjs
1580
+ var TOKENS = {
1581
+ AR: { symbol: "AR", name: "Arweave", decimals: 12 },
1582
+ DUST: { symbol: "DUST", name: "Midnight", decimals: 6 },
1583
+ MOCK: {
1584
+ symbol: "MOCK",
1585
+ name: "Mock Token",
1586
+ decimals: 6
1587
+ }
1588
+ };
1589
+
1590
+ // ../payment/dist/chunk-R6FT3AV4.mjs
1591
+ var MockPaymentAdapter = class {
1592
+ constructor(config) {
1593
+ this.network = "mock";
1594
+ this.token = config.token ?? "MOCK";
1595
+ this.type = config.type;
1596
+ this.quoteValidityMs = config.quoteValidityMs ?? 6e5;
1597
+ this.logger = config.logger;
1598
+ }
1599
+ async estimateCost(dataSizeBytes) {
1600
+ const kb = dataSizeBytes / 1024;
1601
+ const amount = Math.max(1e-4, kb * 1e-3).toFixed(6);
1602
+ this.logger?.log(
1603
+ "dStorage/mock-payment",
1604
+ `Estimated cost: ${amount} ${this.token}`
1605
+ );
1606
+ return {
1607
+ token: this.token,
1608
+ amount,
1609
+ network: this.network,
1610
+ type: this.type,
1611
+ dataSizeBytes,
1612
+ expiresAt: Date.now() + this.quoteValidityMs
1613
+ };
1614
+ }
1615
+ async pay(instruction, onStatus) {
1616
+ const tokenName = TOKENS[this.token]?.name ?? this.token;
1617
+ onStatus?.(`Requesting ${tokenName} wallet signature\u2026`);
1618
+ onStatus?.(`Broadcasting ${this.type} payment transaction\u2026`);
1619
+ onStatus?.("Awaiting confirmation\u2026");
1620
+ const txHash = generateSimulatedTxHash();
1621
+ const { amount } = await this.estimateCost(instruction.dataSizeBytes);
1622
+ this.logger?.log(
1623
+ "dStorage/mock-payment",
1624
+ `Simulated ${this.type} payment \u2192 txHash: ${txHash}`
1625
+ );
1626
+ return {
1627
+ txHash,
1628
+ token: this.token,
1629
+ amount,
1630
+ type: this.type,
1631
+ paidAt: Date.now(),
1632
+ serviceFee: "0"
1633
+ };
1634
+ }
1635
+ };
1636
+ function generateSimulatedTxHash() {
1637
+ const hex = Array.from(
1638
+ { length: 64 },
1639
+ () => Math.floor(Math.random() * 16).toString(16)
1640
+ ).join("");
1641
+ return `0x${hex}`;
1642
+ }
1643
+
1644
+ // ../payment/dist/index.mjs
1645
+ import { blake3 } from "@noble/hashes/blake3.js";
1646
+ var PaymentErrorCode = {
1647
+ // adapters/arweave.ts (14000–14099)
1648
+ ARWEAVE_NOT_INSTALLED: 14001,
1649
+ ARWEAVE_WALLET_KEY_REQUIRED: 14002,
1650
+ ARWEAVE_CONNECT_NOT_FOUND: 14003,
1651
+ ARWEAVE_QUOTE_EXPIRED: 14004,
1652
+ // adapters/managed.ts (14100–14299)
1653
+ MANAGED_NON_OBJECT_RESPONSE: 14100,
1654
+ MANAGED_SERVER_FAILURE: 14101,
1655
+ MANAGED_MISSING_FIELD: 14102,
1656
+ MANAGED_PUBLIC_KEY_MISMATCH: 14103,
1657
+ MANAGED_TOKEN_REQUIRED: 14104,
1658
+ MANAGED_QUOTE_EXPIRED: 14105,
1659
+ MANAGED_UNBALANCED_TX: 14106,
1660
+ MANAGED_SERVER_UNREACHABLE: 14107,
1661
+ MANAGED_KEY_CHANGED: 14108,
1662
+ MANAGED_SIGN_HTTP_ERROR: 14109,
1663
+ MANAGED_NON_JSON_RESPONSE: 14110
1664
+ };
1665
+ var MAX_ERROR_BODY_LENGTH = 200;
1666
+ function sanitiseErrorBody(raw) {
1667
+ return raw.replace(/[\x00-\x1f\x7f]/g, " ").replace(/<[^>]*>/g, "").trim().slice(0, MAX_ERROR_BODY_LENGTH);
1668
+ }
1669
+ var DEFAULT_QUOTE_VALIDITY_MS = 12e4;
1670
+ var CONTENT_HASH_TAG = "X-Content-Hash";
1671
+ var ArweavePaymentAdapter = class {
1672
+ constructor(config = {}) {
1673
+ this.network = "arweave";
1674
+ this.token = "AR";
1675
+ this.type = "storage";
1676
+ this.arweaveClient = null;
1677
+ this.pendingUpload = null;
1678
+ this.signedUpload = null;
1679
+ this.gateway = config.gateway ?? {
1680
+ host: "arweave.net",
1681
+ port: 443,
1682
+ protocol: "https"
1683
+ };
1684
+ this.walletKey = config.walletKey;
1685
+ this.quoteValidityMs = config.quoteValidityMs ?? DEFAULT_QUOTE_VALIDITY_MS;
1686
+ this.logger = config.logger;
1687
+ }
1688
+ // ── Protected: lazy Arweave client ───────────────────────────────────────────
1689
+ async getClient() {
1690
+ if (this.arweaveClient) return this.arweaveClient;
1706
1691
  try {
1707
- this.assertConnected();
1708
- if (!this.config.chainAdapter) {
1709
- throw new DStorageError(
1710
- CoreErrorCode.ROTATE_KEYS_REQUIRES_CHAIN,
1711
- "[dStorage] rotateKeys requires a chain adapter."
1712
- );
1713
- }
1714
- if (typeof this.config.chainAdapter.updateReference !== "function") {
1715
- throw new DStorageError(
1716
- CoreErrorCode.ROTATE_KEYS_NOT_SUPPORTED,
1717
- "[dStorage] rotateKeys is not supported by this chain adapter."
1718
- );
1719
- }
1720
- if (this._encryptionProviders.length === 0) {
1721
- throw new DStorageError(
1722
- CoreErrorCode.ROTATE_KEYS_REQUIRES_ENCRYPTION,
1723
- "[dStorage] rotateKeys requires at least one encryption adapter."
1724
- );
1725
- }
1726
- const ref = await this.config.chainAdapter.readReference(refId);
1727
- const ownerSecret = await this._computeOwnerSecret(refId);
1728
- const seed = await this._unwrapDek(ref.keyEnvelope);
1729
- try {
1730
- const wrapperEntries = await Promise.all(
1731
- this._encryptionProviders.map(async (p) => ({
1732
- wrapKeyName: p.name,
1733
- ...await p.wrapDek(seed)
1734
- }))
1735
- );
1736
- const newKeyEnvelope = serializeKeyEnvelope(
1737
- buildKeyEnvelope(wrapperEntries)
1738
- );
1739
- await this.config.chainAdapter.updateReference(
1740
- refId,
1741
- {
1742
- storageId: ref.storageId ?? "",
1743
- encryptionScheme: ref.encryptionScheme ?? "",
1744
- storageProvider: ref.storageProvider,
1745
- writtenAt: ref.writtenAt,
1746
- keyEnvelope: newKeyEnvelope,
1747
- ...ref.encryptedMetadata !== void 0 ? { encryptedMetadata: ref.encryptedMetadata } : {},
1748
- ...ref.contentHash !== void 0 ? { contentHash: ref.contentHash } : {}
1749
- },
1750
- ownerSecret,
1751
- ownerSecret
1752
- );
1753
- } finally {
1754
- seed.fill(0);
1755
- }
1756
- } catch (err) {
1757
- wrapError(err);
1692
+ const _m = await import("arweave");
1693
+ const raw = _m.default;
1694
+ const ctor = typeof raw.init === "function" ? raw : raw.default ?? raw;
1695
+ this.arweaveClient = ctor.init(this.gateway ?? {});
1696
+ return this.arweaveClient;
1697
+ } catch {
1698
+ throw new DStorageError(
1699
+ PaymentErrorCode.ARWEAVE_NOT_INSTALLED,
1700
+ "[dStorage/arweave-payment] arweave package not installed. Run: npm install arweave"
1701
+ );
1758
1702
  }
1759
1703
  }
1760
- /**
1761
- * Retrieve and decrypt data by its storageId.
1762
- * Decryption happens locally the SDK never sends plaintext anywhere.
1763
- *
1764
- * Automatically detects chunked manifests and reassembles the original file.
1765
- *
1766
- * @param storageId The storageId returned from store()
1767
- */
1768
- async retrieveByStorageId(storageId, dekScheme, contentHash) {
1769
- try {
1770
- this.assertConnected();
1771
- const { bytes: rawBytes, tags } = await this.config.storageAdapter.retrieve(storageId);
1772
- if (contentHash) {
1773
- const computed = await computeBlake3Hex(rawBytes);
1774
- if (computed !== contentHash) {
1775
- throw new DStorageError(
1776
- CoreErrorCode.CONTENT_HASH_MISMATCH,
1777
- `[dStorage] Content hash mismatch for storageId ${storageId}. Expected ${contentHash}, got ${computed}. The retrieved bytes do not match the hash committed on-chain \u2014 the storage content may have been tampered with.`
1778
- );
1779
- }
1780
- }
1781
- return this._decryptAndReassemble(rawBytes, storageId, tags, dekScheme);
1782
- } catch (err) {
1783
- wrapError(err);
1704
+ // ── Protected: wallet resolution ─────────────────────────────────────────────
1705
+ resolveWallet() {
1706
+ if (this.walletKey && Object.keys(this.walletKey).length > 0) {
1707
+ return this.walletKey;
1784
1708
  }
1785
- }
1786
- /**
1787
- * Unwrap the data DEK from a key envelope and return an XChaChaScheme built from it.
1788
- * Throws if the KEK is not available or the envelope cannot be parsed.
1789
- * Only valid for private uploads (envelope.wrappers must be present).
1790
- */
1791
- async _getDekScheme(keyEnvelope) {
1792
- if (this._encryptionProviders.length === 0) {
1709
+ if (typeof window === "undefined") {
1793
1710
  throw new DStorageError(
1794
- CoreErrorCode.DECRYPT_NO_PROVIDERS,
1795
- "[dStorage] Cannot decrypt: no encryption providers. Call init() before retrieving private data."
1711
+ PaymentErrorCode.ARWEAVE_WALLET_KEY_REQUIRED,
1712
+ "[dStorage/arweave-payment] Node.js usage requires a walletKey in the config."
1796
1713
  );
1797
1714
  }
1798
- if (!keyEnvelope) {
1715
+ if (typeof window.arweaveWallet === "undefined") {
1799
1716
  throw new DStorageError(
1800
- CoreErrorCode.DECRYPT_ENVELOPE_EMPTY,
1801
- "[dStorage] Cannot decrypt: key envelope is empty.",
1802
- { cause: { code: "envelope_empty" } }
1717
+ PaymentErrorCode.ARWEAVE_CONNECT_NOT_FOUND,
1718
+ "[dStorage/arweave-payment] ArConnect extension not found. Install it from https://arconnect.io"
1803
1719
  );
1804
1720
  }
1805
- const envelope = parseKeyEnvelope(keyEnvelope);
1806
- if (!envelope) {
1721
+ return "use_wallet";
1722
+ }
1723
+ // ── PaymentAdapter: provideUploadData ─────────────────────────────────────────
1724
+ /**
1725
+ * Store the encrypted upload payload and metadata tags so that pay() can
1726
+ * build and sign the complete Arweave transaction in one step.
1727
+ * Called by MetaTransaction after the encrypt step, before pay().
1728
+ */
1729
+ provideUploadData(data, metadata = {}) {
1730
+ const hashHex = Array.from(blake3(data)).map((b) => b.toString(16).padStart(2, "0")).join("");
1731
+ this.pendingUpload = {
1732
+ data,
1733
+ metadata: { [CONTENT_HASH_TAG]: hashHex, ...metadata }
1734
+ };
1735
+ this.logger?.log(
1736
+ "dStorage/arweave-payment",
1737
+ `Upload data staged: ${data.byteLength} bytes`
1738
+ );
1739
+ }
1740
+ // ── PaymentAdapter: estimateCost ─────────────────────────────────────────────
1741
+ /**
1742
+ * Query the Arweave fee oracle for the real upload cost.
1743
+ */
1744
+ async estimateCost(dataSizeBytes) {
1745
+ const arweave = await this.getClient();
1746
+ const winstons = await arweave.transactions.getPrice(
1747
+ dataSizeBytes
1748
+ );
1749
+ const amount = arweave.ar.winstonToAr(winstons);
1750
+ this.logger?.log(
1751
+ "dStorage/arweave-payment",
1752
+ `Estimated: ${amount} AR (${winstons} Winstons)`
1753
+ );
1754
+ return {
1755
+ token: this.token,
1756
+ amount,
1757
+ network: this.network,
1758
+ type: this.type,
1759
+ dataSizeBytes,
1760
+ expiresAt: Date.now() + this.quoteValidityMs
1761
+ };
1762
+ }
1763
+ // ── PaymentAdapter: pay ───────────────────────────────────────────────────────
1764
+ /**
1765
+ * Sign-then-store flow (when provideUploadData was called):
1766
+ * Creates the Arweave transaction with data + tags, prompts the wallet
1767
+ * for approval (ArConnect popup or JWK signing), and stores the signed
1768
+ * transaction for ArweaveStorageAdapter.store() to post.
1769
+ *
1770
+ * Pass-through fallback (when provideUploadData was NOT called):
1771
+ * Returns a receipt acknowledging that payment is bundled with the upload.
1772
+ * ArweaveStorageAdapter.store() performs the full create → sign → post flow.
1773
+ */
1774
+ async pay(instruction, onStatus) {
1775
+ if (Date.now() > instruction.expiresAt) {
1807
1776
  throw new DStorageError(
1808
- CoreErrorCode.DECRYPT_ENVELOPE_MALFORMED,
1809
- "[dStorage] Cannot decrypt: key envelope is malformed or has an invalid structure. The stored reference may be corrupted or tampered with.",
1810
- { cause: { code: "envelope_malformed" } }
1777
+ PaymentErrorCode.ARWEAVE_QUOTE_EXPIRED,
1778
+ `[dStorage/payment] Quote expired at ${new Date(instruction.expiresAt).toISOString()}. Request a fresh quote via estimateCost() before calling pay().`
1811
1779
  );
1812
1780
  }
1813
- for (const wrapper of envelope.wrappers) {
1814
- const provider = this._encryptionProviders.find(
1815
- (p) => p.name === wrapper.wrapKeyName
1816
- );
1817
- if (!provider) continue;
1818
- try {
1819
- const dek = await provider.unwrapDek(wrapper);
1820
- return XChaChaScheme.fromKey(dek);
1821
- } catch {
1822
- }
1781
+ if (!this.pendingUpload) {
1782
+ onStatus?.("AR fee will be deducted as part of the upload transaction.");
1783
+ return {
1784
+ txHash: "arweave-bundled-with-upload",
1785
+ token: this.token,
1786
+ amount: "0",
1787
+ type: this.type,
1788
+ paidAt: Date.now(),
1789
+ serviceFee: "0"
1790
+ };
1791
+ }
1792
+ const { data, metadata } = this.pendingUpload;
1793
+ this.pendingUpload = null;
1794
+ const arweave = await this.getClient();
1795
+ const wallet = this.resolveWallet();
1796
+ onStatus?.("Creating Arweave transaction\u2026");
1797
+ const tx = await arweave.createTransaction({ data }, wallet);
1798
+ for (const [key, value] of Object.entries(metadata)) {
1799
+ tx.addTag(key, value);
1823
1800
  }
1801
+ onStatus?.("Requesting AR wallet approval\u2026");
1802
+ await arweave.transactions.sign(tx, wallet);
1803
+ const winstons = await arweave.transactions.getPrice(
1804
+ data.byteLength
1805
+ );
1806
+ const arCost = arweave.ar.winstonToAr(winstons);
1807
+ this.signedUpload = { tx, arCost };
1808
+ this.logger?.log(
1809
+ "dStorage/arweave-payment",
1810
+ `Transaction signed \u2192 txId: ${tx.id} cost: ~${arCost} AR`
1811
+ );
1812
+ return {
1813
+ txHash: tx.id,
1814
+ token: this.token,
1815
+ amount: arCost,
1816
+ type: this.type,
1817
+ paidAt: Date.now(),
1818
+ serviceFee: "0"
1819
+ };
1820
+ }
1821
+ // ── Consumed by ArweaveStorageAdapter ─────────────────────────────────────────
1822
+ /**
1823
+ * Returns the pre-signed transaction produced by pay(), then clears it.
1824
+ * Called by ArweaveStorageAdapter.store() to post the already-signed tx.
1825
+ * Returns null if pay() did not sign a transaction (pass-through mode).
1826
+ */
1827
+ consumeSignedUpload() {
1828
+ const signed = this.signedUpload;
1829
+ this.signedUpload = null;
1830
+ return signed;
1831
+ }
1832
+ };
1833
+ var POST_SIGN_TX_API_PATH = "/api/service/sign-tx";
1834
+ function uint8ArrayToHex(bytes) {
1835
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1836
+ }
1837
+ function parseSignedTxBytes(signedTx) {
1838
+ if (signedTx.startsWith("midnight:")) {
1839
+ const colonIdx = signedTx.lastIndexOf(":");
1840
+ return hexToBytes(signedTx.slice(colonIdx + 1), "signedTx");
1841
+ }
1842
+ return hexToBytes(signedTx, "signedTx");
1843
+ }
1844
+ function parseSignTxResponse(raw) {
1845
+ if (typeof raw !== "object" || raw === null) {
1824
1846
  throw new DStorageError(
1825
- CoreErrorCode.DECRYPT_NO_WRAPPER_MATCHED,
1826
- "[dStorage] Cannot decrypt: this reference was not encrypted with any of your configured keys. Check that you are using the correct encryption adapter, password, seed phrase, or keypair.",
1827
- { cause: { code: "no_wrapper_matched" } }
1847
+ PaymentErrorCode.MANAGED_NON_OBJECT_RESPONSE,
1848
+ "[dStorage/managed-payment] Signing server returned a non-object response."
1828
1849
  );
1829
1850
  }
1830
- /**
1831
- * Unwrap the raw DEK bytes from a key envelope using the available KEKs.
1832
- * The ownerSecret for the reference can be re-derived as HKDF(dek, rawStorageId).
1833
- * Throws if no wrapper matches any available KEK.
1834
- */
1835
- async _unwrapDek(keyEnvelope) {
1836
- if (this._encryptionProviders.length === 0) {
1837
- throw new DStorageError(
1838
- CoreErrorCode.UNWRAP_DEK_NO_PROVIDERS,
1839
- "[dStorage] Cannot unwrap DEK: no encryption providers. Call init() first."
1840
- );
1841
- }
1842
- if (!keyEnvelope) {
1843
- throw new DStorageError(
1844
- CoreErrorCode.UNWRAP_DEK_ENVELOPE_EMPTY,
1845
- "[dStorage] Cannot unwrap DEK: key envelope is empty.",
1846
- { cause: { code: "envelope_empty" } }
1847
- );
1848
- }
1849
- const envelope = parseKeyEnvelope(keyEnvelope);
1850
- if (!envelope) {
1851
- throw new DStorageError(
1852
- CoreErrorCode.UNWRAP_DEK_ENVELOPE_MALFORMED,
1853
- "[dStorage] Cannot unwrap DEK: key envelope is malformed or has an invalid structure. The stored reference may be corrupted or tampered with.",
1854
- { cause: { code: "envelope_malformed" } }
1855
- );
1856
- }
1857
- for (const wrapper of envelope.wrappers) {
1858
- const provider = this._encryptionProviders.find(
1859
- (p) => p.name === wrapper.wrapKeyName
1860
- );
1861
- if (!provider) continue;
1862
- try {
1863
- return await provider.unwrapDek(wrapper);
1864
- } catch {
1865
- }
1866
- }
1851
+ const r = raw;
1852
+ if (r["success"] !== true) {
1867
1853
  throw new DStorageError(
1868
- CoreErrorCode.UNWRAP_DEK_NO_WRAPPER_MATCHED,
1869
- "[dStorage] Cannot unwrap DEK: this reference was not encrypted with any of your configured keys. Check that you are using the correct encryption adapter, password, seed phrase, or keypair.",
1870
- { cause: { code: "no_wrapper_matched" } }
1854
+ PaymentErrorCode.MANAGED_SERVER_FAILURE,
1855
+ `[dStorage/managed-payment] Signing server reported success:false \u2014 ${sanitiseErrorBody(JSON.stringify(r))}`
1871
1856
  );
1872
1857
  }
1873
- /**
1874
- * Compute the ownerSecret for an existing on-chain reference.
1875
- * Used by removeReference/updateReference to derive the ZK witness secret.
1876
- *
1877
- * Flow:
1878
- * 1. Read the reference from chain to get keyEnvelope + storageId
1879
- * 2. Unwrap the DEK from keyEnvelope using available KEKs
1880
- * 3. Recover raw storageId (decrypt with DEK if private, plaintext if public)
1881
- * 4. Return HKDF-SHA256(dek, rawStorageId) for private, or HKDF-SHA256(ownerSeed, rawStorageId) for public
1882
- */
1883
- async _computeOwnerSecret(refId) {
1884
- const ref = await this.config.chainAdapter.readReference(refId);
1885
- const isPublic = !ref.encryptionScheme;
1886
- if (isPublic) {
1887
- const ownerSeed = await this._unwrapDek(ref.keyEnvelope);
1888
- return deriveOwnerSecret(
1889
- ownerSeed,
1890
- new TextEncoder().encode(ref.storageId),
1891
- "dstorage:owner-secret:public:v1"
1858
+ for (const field of [
1859
+ "txId",
1860
+ "signature",
1861
+ "publicKey",
1862
+ "txAmount",
1863
+ "serviceFee"
1864
+ ]) {
1865
+ if (typeof r[field] !== "string") {
1866
+ throw new DStorageError(
1867
+ PaymentErrorCode.MANAGED_MISSING_FIELD,
1868
+ `[dStorage/managed-payment] Signing server response missing or invalid field: ${field}`
1892
1869
  );
1893
1870
  }
1894
- const dek = await this._unwrapDek(ref.keyEnvelope);
1895
- const dekScheme = XChaChaScheme.fromKey(dek);
1896
- const rawStorageIdBytes = ref.storageId ? new TextEncoder().encode(
1897
- await dekScheme.decryptStorageId(ref.storageId)
1898
- ) : METADATA_ONLY_OWNER_SECRET_SALT;
1899
- return deriveOwnerSecret(dek, rawStorageIdBytes);
1900
1871
  }
1901
- // Accepts an optional pre-built CryptoScheme (unwrapped from a key envelope).
1902
- // If no dekScheme is provided, content is treated as public (no decryption).
1903
- async _decryptAndReassemble(rawBytes, storageId, tags, dekScheme) {
1904
- const scheme = dekScheme ?? null;
1905
- if (scheme) {
1906
- let manifestBytes = null;
1907
- try {
1908
- manifestBytes = await scheme.decryptPayload(rawBytes, MANIFEST_AAD);
1909
- } catch {
1910
- }
1911
- if (manifestBytes !== null) {
1912
- const parsed = JSON.parse(
1913
- new TextDecoder().decode(manifestBytes)
1914
- );
1915
- if (!isManifest(parsed)) {
1916
- throw new DStorageError(
1917
- CoreErrorCode.MANIFEST_INVALID_STRUCTURE,
1918
- "[dStorage] Payload authenticated as manifest but has invalid structure"
1919
- );
1920
- }
1921
- this.logger?.log(
1922
- "dStorage",
1923
- `Manifest detected: reassembling ${parsed.chunkCount} chunks`
1924
- );
1925
- return this._retrieveChunked(parsed, scheme, tags);
1926
- }
1927
- let decryptedBytes;
1928
- try {
1929
- decryptedBytes = await scheme.decryptPayload(rawBytes, PAYLOAD_AAD);
1930
- } catch (err) {
1931
- throw new DStorageError(
1932
- CoreErrorCode.DECRYPT_PAYLOAD_FAILED,
1933
- `[dStorage] Failed to decrypt payload: ${err}`,
1934
- { cause: err }
1935
- );
1936
- }
1937
- this.logger?.log(
1938
- "dStorage",
1939
- `Retrieved by refId \u2192 storageId: ${storageId}`
1940
- );
1941
- return { bytes: decryptedBytes, metadata: {}, tags };
1942
- }
1943
- try {
1944
- const parsed = JSON.parse(new TextDecoder().decode(rawBytes));
1945
- if (isManifest(parsed)) {
1946
- this.logger?.log(
1947
- "dStorage",
1948
- `Manifest detected: reassembling ${parsed.chunkCount} chunks`
1949
- );
1950
- return this._retrieveChunked(parsed, null, tags);
1951
- }
1952
- } catch {
1953
- }
1954
- this.logger?.log(
1955
- "dStorage",
1956
- `Retrieved by refId \u2192 storageId: ${storageId}`
1872
+ return r;
1873
+ }
1874
+ function assertOwnerKeyMatch(responseKey, pinnedKey) {
1875
+ if (pinnedKey && responseKey !== pinnedKey) {
1876
+ throw new DStorageError(
1877
+ PaymentErrorCode.MANAGED_PUBLIC_KEY_MISMATCH,
1878
+ "[dStorage/managed-payment] Signing server returned a public key that does not match the pinned key from the auth token \u2014 possible server compromise or MITM."
1957
1879
  );
1958
- return {
1959
- bytes: rawBytes,
1960
- metadata: {},
1961
- tags
1962
- };
1963
1880
  }
1881
+ }
1882
+ var ManagedPaymentAdapter = class _ManagedPaymentAdapter extends ArweavePaymentAdapter {
1964
1883
  /**
1965
- * Retrieve and decrypt data using only its on-chain reference ID.
1966
- *
1967
- * Flow:
1968
- * 1. Look up the reference on-chain by refId to obtain the storage network ID.
1969
- * 2. Fetch the blob from the storage network.
1970
- * 3. Decrypt locally if an encryption key is available.
1971
- *
1972
- * Requires the chain adapter to implement readReference() (part of the
1973
- * ChainAdapter interface — shared by MidnightChainAdapter, MockChainAdapter, etc.).
1884
+ * Strip a RSA-4096 modulus suffix from a compound auth token
1885
+ * (<credential>.<base64url_683_chars>), returning only the credential.
1886
+ * A plain token (no '.' or wrong suffix length) is returned unchanged.
1887
+ * This lets ArweaveBundlerStorageAdapter compound tokens be passed directly
1888
+ * to any adapter that uses ManagedPaymentAdapter without breaking authentication.
1974
1889
  *
1975
- * @param refId The chainRefId returned from store()
1890
+ * lastIndexOf is intentional: the credential part may itself contain dots
1891
+ * (e.g. a JWT), so we always split at the *last* dot to reach the modulus.
1976
1892
  */
1977
- async retrieveByRefId(refId) {
1978
- try {
1979
- this.assertConnected();
1980
- if (!this.config.chainAdapter) {
1981
- throw new DStorageError(
1982
- CoreErrorCode.RETRIEVE_BY_REF_ID_REQUIRES_CHAIN,
1983
- "[dStorage] retrieveByRefId() requires a chain adapter. Configure one in DStorageConfig or use retrieveByStorageId() instead."
1984
- );
1985
- }
1986
- const ref = await this.config.chainAdapter.readReference(refId);
1987
- if (!ref)
1988
- throw new DStorageError(
1989
- CoreErrorCode.REF_NOT_FOUND,
1990
- `[dStorage] No on-chain reference found for refId: ${refId}`
1991
- );
1992
- if (ref.storageProvider && ref.storageProvider !== this.config.storageAdapter.name) {
1993
- throw new DStorageError(
1994
- CoreErrorCode.STORAGE_ADAPTER_MISMATCH,
1995
- `[dStorage] Storage adapter mismatch: reference "${refId}" was stored on "${ref.storageProvider}" but the configured adapter is "${this.config.storageAdapter.name}". Configure a "${ref.storageProvider}" storage adapter to retrieve this reference.`
1996
- );
1997
- }
1998
- const isPublic = !ref.encryptionScheme;
1999
- let storageId;
2000
- let dekScheme;
2001
- if (!ref.storageId) {
2002
- if (!isPublic && ref.keyEnvelope) {
2003
- dekScheme = await this._getDekScheme(ref.keyEnvelope);
2004
- }
2005
- } else if (isPublic) {
2006
- storageId = ref.storageId;
2007
- } else {
2008
- if (!ref.keyEnvelope) {
2009
- throw new DStorageError(
2010
- CoreErrorCode.RETRIEVE_BY_REF_ID_ENVELOPE_EMPTY,
2011
- `[dStorage] Cannot decrypt refId ${refId}: key envelope is empty.`,
2012
- { cause: { code: "envelope_empty" } }
2013
- );
2014
- }
2015
- dekScheme = await this._getDekScheme(ref.keyEnvelope);
2016
- storageId = await dekScheme.decryptStorageId(ref.storageId);
2017
- }
2018
- let contentBytes;
2019
- let contentTags;
2020
- if (storageId !== void 0) {
2021
- const r = await this.retrieveByStorageId(
2022
- storageId,
2023
- dekScheme,
2024
- ref.contentHash
2025
- );
2026
- contentBytes = r.bytes;
2027
- contentTags = r.tags;
2028
- } else {
2029
- contentBytes = new Uint8Array(0);
2030
- contentTags = {};
2031
- }
2032
- let metadata = {};
2033
- if (ref.encryptedMetadata && dekScheme) {
2034
- const raw = await dekScheme.decryptPayload(
2035
- base64urlToBytes(ref.encryptedMetadata),
2036
- METADATA_AAD
2037
- );
2038
- metadata = JSON.parse(new TextDecoder().decode(raw));
1893
+ static _parseAuthToken(token) {
1894
+ const dot = token.lastIndexOf(".");
1895
+ if (dot !== -1) {
1896
+ const suffix = token.slice(dot + 1);
1897
+ if (suffix.length === 683 && /^[A-Za-z0-9_-]+$/.test(suffix)) {
1898
+ return { credential: token.slice(0, dot), ownerPublicKey: suffix };
2039
1899
  }
2040
- return { bytes: contentBytes, tags: contentTags, metadata };
2041
- } catch (err) {
2042
- wrapError(err);
2043
1900
  }
1901
+ return { credential: token, ownerPublicKey: void 0 };
2044
1902
  }
2045
- /**
2046
- * List references currently available from the configured chain adapter.
2047
- */
2048
- async listReferences(options) {
2049
- try {
2050
- if (!this.config.chainAdapter) {
2051
- throw new DStorageError(
2052
- CoreErrorCode.LIST_REFS_REQUIRES_CHAIN,
2053
- "[dStorage] listReferences() requires a chain adapter."
2054
- );
2055
- }
2056
- let rawItems;
2057
- if (options?.refIds && options.refIds.length > 0) {
2058
- rawItems = await Promise.all(
2059
- options.refIds.map(async (refId) => {
2060
- const ref = await this.config.chainAdapter.readReference(refId);
2061
- return {
2062
- refId,
2063
- storageId: ref.storageId ?? null,
2064
- encryptionScheme: ref.encryptionScheme ?? null,
2065
- keyEnvelope: ref.keyEnvelope,
2066
- storageProvider: ref.storageProvider,
2067
- ownerCommitment: "",
2068
- ...ref.encryptedMetadata !== void 0 ? { encryptedMetadata: ref.encryptedMetadata } : {}
2069
- };
2070
- })
2071
- );
2072
- } else {
2073
- if (typeof this.config.chainAdapter.listReferences !== "function") {
2074
- throw new DStorageError(
2075
- CoreErrorCode.LIST_REFS_NOT_SUPPORTED,
2076
- "[dStorage] listReferences() is not supported by this chain adapter."
2077
- );
2078
- }
2079
- rawItems = await this.config.chainAdapter.listReferences();
2080
- }
2081
- return Promise.all(
2082
- rawItems.map(async (item) => {
2083
- let storageId = null;
2084
- let decryptionError;
2085
- const isPublic = !item.encryptionScheme;
2086
- let dekScheme;
2087
- if (item.storageId) {
2088
- if (isPublic) {
2089
- storageId = item.storageId;
2090
- } else {
2091
- try {
2092
- dekScheme = await this._getDekScheme(item.keyEnvelope);
2093
- storageId = await dekScheme.decryptStorageId(item.storageId);
2094
- } catch (err) {
2095
- decryptionError = err instanceof Error ? err.message : String(err);
2096
- }
2097
- }
2098
- }
2099
- if (!decryptionError) {
2100
- if (isPublic) {
2101
- try {
2102
- await this._unwrapDek(item.keyEnvelope);
2103
- } catch (err) {
2104
- decryptionError = err instanceof Error ? err.message : String(err);
2105
- }
2106
- }
2107
- }
2108
- let metadata;
2109
- if (item.encryptedMetadata && dekScheme) {
2110
- try {
2111
- const raw = await dekScheme.decryptPayload(
2112
- base64urlToBytes(item.encryptedMetadata),
2113
- METADATA_AAD
2114
- );
2115
- metadata = JSON.parse(new TextDecoder().decode(raw));
2116
- } catch {
2117
- }
2118
- }
2119
- return {
2120
- refId: item.refId,
2121
- storageId,
2122
- storageProvider: item.storageProvider,
2123
- encryptionScheme: item.encryptionScheme ?? null,
2124
- keyEnvelope: item.keyEnvelope,
2125
- ownerCommitment: item.ownerCommitment,
2126
- ...decryptionError !== void 0 ? { decryptionError } : {},
2127
- ...metadata !== void 0 ? { metadata } : {}
2128
- };
2129
- })
1903
+ constructor(config) {
1904
+ super({
1905
+ ...config.gateway !== void 0 ? { gateway: config.gateway } : {},
1906
+ ...config.quoteValidityMs !== void 0 ? { quoteValidityMs: config.quoteValidityMs } : {},
1907
+ ...config.logger !== void 0 ? { logger: config.logger } : {}
1908
+ });
1909
+ this.signingServerUrl = config.signingServerUrl.replace(/\/$/, "");
1910
+ if (!config.authToken || config.authToken.trim() === "") {
1911
+ throw new DStorageError(
1912
+ PaymentErrorCode.MANAGED_TOKEN_REQUIRED,
1913
+ "[dStorage/managed-payment] authToken is required and must not be empty or whitespace."
2130
1914
  );
2131
- } catch (err) {
2132
- wrapError(err);
2133
1915
  }
1916
+ const { credential, ownerPublicKey } = _ManagedPaymentAdapter._parseAuthToken(config.authToken);
1917
+ this.authToken = credential;
1918
+ this.ownerPublicKey = ownerPublicKey;
1919
+ this.requestTimeoutMs = config.requestTimeoutMs ?? 3e4;
1920
+ this.network = config.network;
1921
+ this.type = config.type;
2134
1922
  }
1923
+ // ── PaymentAdapter: estimateCost ─────────────────────────────────────────────
2135
1924
  /**
2136
- * Remove a reference from the on-chain registry.
2137
- * Only the owner of the reference can successfully call this — the chain
2138
- * enforces ownership via ZK proof using the witness-derived ownerSecret.
1925
+ * Query the Arweave fee oracle for the real upload cost.
2139
1926
  */
2140
- async removeReference(refId) {
2141
- try {
2142
- if (!this.config.chainAdapter) {
2143
- throw new DStorageError(
2144
- CoreErrorCode.REMOVE_REF_REQUIRES_CHAIN,
2145
- "[dStorage] removeReference() requires a chain adapter."
2146
- );
2147
- }
2148
- if (typeof this.config.chainAdapter.removeReference !== "function") {
2149
- throw new DStorageError(
2150
- CoreErrorCode.REMOVE_REF_NOT_SUPPORTED,
2151
- "[dStorage] removeReference() is not supported by this chain adapter."
2152
- );
2153
- }
2154
- const ownerSecret = await this._computeOwnerSecret(refId);
2155
- await this.config.chainAdapter.removeReference(refId, ownerSecret);
2156
- } catch (err) {
2157
- wrapError(err);
1927
+ async estimateCost(dataSizeBytes) {
1928
+ if (this.network === "arweave") {
1929
+ return await super.estimateCost(dataSizeBytes);
2158
1930
  }
1931
+ const amount = "0.0012345";
1932
+ this.logger?.log(
1933
+ "dStorage/managed-payment",
1934
+ `Estimated: ${amount} ${this.token}`
1935
+ );
1936
+ return {
1937
+ token: this.token,
1938
+ // FIXME: should be probably obtained from the signing service
1939
+ amount,
1940
+ network: this.network,
1941
+ type: this.type,
1942
+ dataSizeBytes,
1943
+ txData: "---",
1944
+ expiresAt: Date.now() + this.quoteValidityMs
1945
+ };
2159
1946
  }
1947
+ // ── PaymentAdapter: pay ───────────────────────────────────────────────────────
2160
1948
  /**
2161
- * Estimate the full cost (storage + chain) for a given payload size before committing.
1949
+ * Routes payment to the correct network-specific handler based on quote.network.
1950
+ *
1951
+ * The Arweave path falls back to the base-class pass-through if
1952
+ * provideUploadData() was not called. An unrecognised network throws.
2162
1953
  */
2163
- async estimateCost(sizeBytes) {
2164
- try {
2165
- const [storageQuote, chainQuote] = await Promise.all([
2166
- this.config.storageAdapter.estimateCost(sizeBytes),
2167
- this.config.chainAdapter?.estimateCost()
2168
- ]);
2169
- return {
2170
- storageCost: { amount: storageQuote.amount, token: storageQuote.token },
2171
- ...chainQuote ? {
2172
- chainCost: { amount: chainQuote.amount, token: chainQuote.token }
2173
- } : {},
2174
- fileSizeBytes: sizeBytes
2175
- };
2176
- } catch (err) {
2177
- wrapError(err);
1954
+ async pay(instruction, onStatus) {
1955
+ if (Date.now() > instruction.expiresAt) {
1956
+ throw new DStorageError(
1957
+ PaymentErrorCode.MANAGED_QUOTE_EXPIRED,
1958
+ `[dStorage/payment] Quote expired at ${new Date(instruction.expiresAt).toISOString()}. Request a fresh quote via estimateCost() before calling pay().`
1959
+ );
1960
+ }
1961
+ if (this.network === "managedmock") {
1962
+ return this._payManagedMock(instruction, onStatus);
1963
+ }
1964
+ if (instruction.network === "midnight") {
1965
+ return this._payMidnight(instruction, onStatus);
1966
+ } else if (instruction.network === "arweave") {
1967
+ return this._payArweave(instruction, onStatus);
1968
+ } else if (instruction.network === "arweave_bundler") {
1969
+ return this._payArweaveBundler(instruction, onStatus);
2178
1970
  }
1971
+ throw Error(`Unsupported network: ${instruction.network}`);
2179
1972
  }
1973
+ // ── Network-specific wallet provider factories ────────────────────────────────
2180
1974
  /**
2181
- * Execute a full meta-transaction for a file:
2182
- * estimate encrypt pay storage upload pay chain → write reference
1975
+ * Returns a WalletProvider-compatible object for the Midnight chain whose
1976
+ * `balanceTx()` is fulfilled by the managed signing server instead of a
1977
+ * local wallet. The caller supplies the public keys (from whichever source
1978
+ * they have — HD derivation, a server endpoint, etc.); the SDK only handles
1979
+ * the signing-server round-trip.
2183
1980
  *
2184
- * Presents the entire flow as a single action with step-by-step progress.
1981
+ * The returned object satisfies the @midnight-ntwrk/midnight-js-types
1982
+ * WalletProvider interface via structural typing — no Midnight package
1983
+ * import is required from the caller's side.
2185
1984
  *
2186
- * @param file The File to encrypt and store
2187
- * @param onProgress Callback fired on every step state change
2188
- * @param config Optional token overrides and metadata
1985
+ * @param coinPublicKey Midnight coin public key (hex) for the ZK fee circuit.
1986
+ * @param encryptionPublicKey Midnight encryption public key (hex).
1987
+ * @param onReceipt Optional callback invoked with the PaymentReceipt
1988
+ * after each successful balanceTx call. Used internally
1989
+ * by MidnightChainAdapter to capture receipt metadata;
1990
+ * external callers can omit it.
2189
1991
  */
2190
- async executeMetaTransaction(file, onProgress, config = {}) {
2191
- try {
2192
- this.assertConnected();
2193
- if (this._encryptionProviders.length === 0) {
2194
- throw new DStorageError(
2195
- CoreErrorCode.META_TX_REQUIRES_ENCRYPTION,
2196
- "[dStorage] executeMetaTransaction() requires at least one encryption provider."
1992
+ createMidnightWalletProvider(coinPublicKey, encryptionPublicKey, onReceipt) {
1993
+ return {
1994
+ getCoinPublicKey: () => coinPublicKey,
1995
+ getEncryptionPublicKey: () => encryptionPublicKey,
1996
+ balanceTx: async (tx, _ttl) => {
1997
+ const txData = uint8ArrayToHex(tx.serialize());
1998
+ const receipt = await this.pay({
1999
+ network: "midnight",
2000
+ txData,
2001
+ dataSizeBytes: txData.length,
2002
+ expiresAt: Date.now() + 6e4
2003
+ });
2004
+ onReceipt?.(receipt);
2005
+ if (!receipt.signedTx) {
2006
+ throw new DStorageError(
2007
+ PaymentErrorCode.MANAGED_UNBALANCED_TX,
2008
+ "[dStorage/payment] Midnight managed payment server did not return a balanced transaction."
2009
+ );
2010
+ }
2011
+ const { Transaction } = await import("@midnight-ntwrk/midnight-js-protocol/ledger");
2012
+ return Transaction.deserialize(
2013
+ "signature",
2014
+ "proof",
2015
+ "binding",
2016
+ parseSignedTxBytes(receipt.signedTx)
2197
2017
  );
2198
2018
  }
2199
- const metaTx = new MetaTx(this);
2200
- metaTx.onProgress(onProgress);
2201
- return metaTx.execute(file, config);
2202
- } catch (err) {
2203
- wrapError(err);
2019
+ };
2020
+ }
2021
+ // ── Network-specific payment handlers ────────────────────────────────────────
2022
+ /**
2023
+ * Arweave remote-signing flow:
2024
+ * 1. Creates the Arweave transaction locally (data + tags, no local signing).
2025
+ * 2. Calls prepareChunks() to compute data_root (Merkle root) locally.
2026
+ * 3. Strips the raw data from the transaction before serialising — only the
2027
+ * data_root and data_size (not the actual bytes) are sent to the server.
2028
+ * 4. POSTs the stripped transaction JSON to the signing server.
2029
+ * 5. Applies the returned id, signature, and owner to the transaction.
2030
+ * 6. Restores the raw data so the chunked uploader can post it directly to
2031
+ * the Arweave gateway — the server never sees the file content.
2032
+ *
2033
+ * Falls back to the base-class pass-through if provideUploadData() was not called.
2034
+ */
2035
+ async _payArweave(instruction, onStatus) {
2036
+ if (!this.pendingUpload) {
2037
+ return super.pay(instruction, onStatus);
2038
+ }
2039
+ const { data, metadata } = this.pendingUpload;
2040
+ this.pendingUpload = null;
2041
+ const arweave = await this.getClient();
2042
+ onStatus?.("Creating Arweave transaction\u2026");
2043
+ const tx = await arweave.createTransaction({ data });
2044
+ if (this.ownerPublicKey) {
2045
+ tx.owner = this.ownerPublicKey;
2046
+ }
2047
+ for (const [key, value] of Object.entries(metadata)) {
2048
+ tx.addTag(key, value);
2204
2049
  }
2050
+ await tx.prepareChunks(tx.data);
2051
+ const rawData = tx.data;
2052
+ tx.data = new Uint8Array(0);
2053
+ const body = JSON.stringify({
2054
+ txData: tx.toJSON(),
2055
+ network: instruction.network
2056
+ });
2057
+ const {
2058
+ txId,
2059
+ signature,
2060
+ publicKey,
2061
+ txAmount: arCost,
2062
+ serviceFee
2063
+ } = await this._sendSignTxRequest(body, instruction.network, onStatus);
2064
+ assertOwnerKeyMatch(publicKey, this.ownerPublicKey);
2065
+ onStatus?.("Applying remote signature\u2026");
2066
+ tx.setSignature({ id: txId, signature, owner: publicKey });
2067
+ tx.data = rawData;
2068
+ this.signedUpload = { tx, arCost };
2069
+ this.logger?.log(
2070
+ "dStorage/managed-payment",
2071
+ `Transaction signed remotely (${this.signingServerUrl} API service) \u2192 txId: ${txId} cost: ~${arCost} ${this.token} (service fee: ${serviceFee})`
2072
+ );
2073
+ return {
2074
+ txHash: txId,
2075
+ token: this.token,
2076
+ amount: arCost,
2077
+ type: this.type,
2078
+ paidAt: Date.now(),
2079
+ serviceFee
2080
+ };
2205
2081
  }
2206
2082
  /**
2207
- * Generate a fresh random DEK, wrap it under the KEK(s), and return the crypto
2208
- * materials needed by MetaTx to encrypt payload and storageId.
2083
+ * Midnight payment flow:
2084
+ * POSTs the fee amount to the dStorage API service, which holds a funded
2085
+ * Midnight account and submits the chain transaction on behalf of the user.
2086
+ *
2087
+ * POST {baseUrl}{POST_SIGN_TX_API_PATH}
2088
+ * → { txData: string (hex-serialized UnboundTransaction), network: string, userId: string }
2089
+ * ← { txId: string, signature: string (hex-serialized FinalizedTransaction), ... }
2209
2090
  *
2210
- * For private stores: the DEK is wrapped under every available KEK. The ownerSecret
2211
- * can later be re-derived as HKDF(dek, rawStorageId) by any KEK holder.
2212
- * For public stores: a random ownerSeed is generated and wrapped under all KEKs (same pattern).
2091
+ * The hex-serialized FinalizedTransaction is returned in PaymentReceipt.signedTx
2092
+ * for the chain adapter to deserialize and pass to submitTx().
2213
2093
  */
2214
- async prepareUploadCrypto(isPublic = false) {
2215
- try {
2216
- if (this._encryptionProviders.length === 0) {
2217
- throw new DStorageError(
2218
- CoreErrorCode.PREPARE_UPLOAD_NO_PROVIDERS,
2219
- "[dStorage] Cannot prepare upload crypto: no encryption providers. Call init() first."
2220
- );
2221
- }
2222
- if (isPublic) {
2223
- const ownerSeed = generateDek();
2224
- const ownerSeedWrappers = await Promise.all(
2225
- this._encryptionProviders.map(async (provider) => ({
2226
- wrapKeyName: provider.name,
2227
- ...await provider.wrapDek(ownerSeed)
2228
- }))
2229
- );
2230
- const keyEnvelope2 = serializeKeyEnvelope(
2231
- buildKeyEnvelope(ownerSeedWrappers)
2232
- );
2233
- return {
2234
- uploadScheme: null,
2235
- keyEnvelope: keyEnvelope2,
2236
- encryptionScheme: "",
2237
- ownerSeed
2238
- };
2239
- }
2240
- const dek = generateDek();
2241
- const uploadScheme = XChaChaScheme.fromKey(dek);
2242
- const wrapperEntries = await Promise.all(
2243
- this._encryptionProviders.map(async (provider) => ({
2244
- wrapKeyName: provider.name,
2245
- ...await provider.wrapDek(dek)
2246
- }))
2247
- );
2248
- const keyEnvelope = serializeKeyEnvelope(
2249
- buildKeyEnvelope(wrapperEntries)
2250
- );
2251
- return {
2252
- uploadScheme,
2253
- keyEnvelope,
2254
- encryptionScheme: "xchacha20poly1305-v1",
2255
- dek
2256
- };
2257
- } catch (err) {
2258
- wrapError(err);
2259
- }
2094
+ async _payMidnight(instruction, onStatus) {
2095
+ const body = JSON.stringify({
2096
+ txData: instruction.txData ?? null,
2097
+ network: instruction.network
2098
+ });
2099
+ const {
2100
+ txId,
2101
+ signature,
2102
+ txAmount: dustCost,
2103
+ serviceFee
2104
+ } = await this._sendSignTxRequest(body, instruction.network, onStatus);
2105
+ this.logger?.log(
2106
+ "dStorage/managed-payment",
2107
+ `Midnight Tx signed remotely (${this.signingServerUrl} API service) \u2192 txId: ${txId} cost: ~${dustCost} ${this.token} (service fee: ${serviceFee})`
2108
+ );
2109
+ return {
2110
+ txHash: txId,
2111
+ token: "DUST",
2112
+ amount: dustCost,
2113
+ type: this.type,
2114
+ signedTx: signature,
2115
+ paidAt: Date.now(),
2116
+ serviceFee
2117
+ };
2260
2118
  }
2261
- // ── Private helpers ────────────────────────────────────────────────────────
2262
2119
  /**
2263
- * Upload large content by splitting into CHUNK_SIZE pieces, encrypting each
2264
- * independently, and storing a manifest that ties them together.
2265
- * Only the manifest's storageId is written on-chain.
2120
+ * Arweave Bundler
2266
2121
  */
2267
- async _uploadChunked(bytes, tags, onProgress, isPublic = false, rawMeta, refId) {
2268
- if (this._encryptionProviders.length === 0) {
2269
- throw new DStorageError(
2270
- CoreErrorCode.UPLOAD_NO_PROVIDERS,
2271
- "[dStorage] No encryption providers available. Call init() before uploading."
2272
- );
2273
- }
2274
- const totalChunks = Math.ceil(bytes.length / CHUNK_SIZE);
2275
- const sdkVersion = package_default.version ?? "unknown";
2276
- const encryptionScheme = isPublic ? "" : "xchacha20poly1305-v1";
2277
- this.logger?.log(
2278
- "dStorage",
2279
- `Chunked upload: ${bytes.length} bytes \u2192 ${totalChunks} chunks${isPublic ? "" : `, encryption scheme: ${encryptionScheme}`}`
2280
- );
2281
- let uploadScheme = null;
2282
- let keyEnvelope;
2283
- let chunkDek;
2284
- let ownerSeed;
2285
- if (!isPublic) {
2286
- chunkDek = generateDek();
2287
- uploadScheme = XChaChaScheme.fromKey(chunkDek);
2288
- const wrapperEntries = await Promise.all(
2289
- this._encryptionProviders.map(async (provider) => ({
2290
- wrapKeyName: provider.name,
2291
- ...await provider.wrapDek(chunkDek)
2292
- }))
2293
- );
2294
- keyEnvelope = serializeKeyEnvelope(buildKeyEnvelope(wrapperEntries));
2295
- } else {
2296
- ownerSeed = generateDek();
2297
- const ownerSeedWrappers = await Promise.all(
2298
- this._encryptionProviders.map(async (provider) => ({
2299
- wrapKeyName: provider.name,
2300
- ...await provider.wrapDek(ownerSeed)
2301
- }))
2302
- );
2303
- keyEnvelope = serializeKeyEnvelope(buildKeyEnvelope(ownerSeedWrappers));
2304
- }
2305
- const chunkEntries = [];
2306
- let bytesUploaded = 0;
2307
- for (let i = 0; i < totalChunks; i++) {
2308
- const start = i * CHUNK_SIZE;
2309
- const chunk = bytes.subarray(start, start + CHUNK_SIZE);
2310
- onProgress?.({
2311
- phase: "encrypting",
2312
- chunksUploaded: i,
2313
- totalChunks,
2314
- bytesUploaded,
2315
- totalBytes: bytes.length
2316
- });
2317
- let chunkData;
2318
- if (isPublic) {
2319
- chunkData = chunk;
2320
- } else {
2321
- chunkData = await uploadScheme.encryptPayload(
2322
- chunk,
2323
- chunkAad(i, totalChunks)
2324
- );
2325
- }
2326
- onProgress?.({
2327
- phase: "uploading",
2328
- chunksUploaded: i,
2329
- totalChunks,
2330
- bytesUploaded,
2331
- totalBytes: bytes.length
2332
- });
2333
- const chunkResult = await this.config.storageAdapter.store(chunkData, {
2334
- ...tags,
2335
- [TAG_SDK_VERSION]: sdkVersion,
2336
- [TAG_CONTENT_TYPE]: TAG_CONTENT_TYPE_OCTET_STREAM,
2337
- [TAG_CHUNK_INDEX]: String(i),
2338
- [TAG_CHUNK_TOTAL]: String(totalChunks)
2339
- });
2340
- chunkEntries.push({
2341
- index: i,
2342
- storageId: chunkResult.id,
2343
- size: chunk.length
2344
- });
2345
- bytesUploaded += chunk.length;
2346
- onProgress?.({
2347
- phase: "uploading",
2348
- chunksUploaded: i + 1,
2349
- totalChunks,
2350
- bytesUploaded,
2351
- totalBytes: bytes.length
2352
- });
2353
- }
2354
- onProgress?.({
2355
- phase: "finalizing",
2356
- chunksUploaded: totalChunks,
2357
- totalChunks,
2358
- bytesUploaded,
2359
- totalBytes: bytes.length
2122
+ async _payArweaveBundler(instruction, onStatus) {
2123
+ const body = JSON.stringify({
2124
+ txData: instruction.txData ?? null,
2125
+ network: instruction.network,
2126
+ ...instruction.ownerKey !== void 0 ? { ownerKey: instruction.ownerKey } : {}
2360
2127
  });
2361
- const manifest = {
2362
- v: 1,
2363
- type: "dstorage-chunked-manifest",
2364
- totalSize: bytes.length,
2365
- chunkSize: CHUNK_SIZE,
2366
- chunkCount: totalChunks,
2367
- provider: this.config.storageAdapter.name,
2368
- chunks: chunkEntries
2369
- };
2370
- const manifestBytes = new TextEncoder().encode(JSON.stringify(manifest));
2371
- let manifestData;
2372
- if (isPublic) {
2373
- manifestData = manifestBytes;
2374
- } else {
2375
- manifestData = await uploadScheme.encryptPayload(
2376
- manifestBytes,
2377
- MANIFEST_AAD
2378
- );
2379
- }
2380
- const manifestResult = await this.config.storageAdapter.store(
2381
- manifestData,
2382
- {
2383
- [TAG_SDK_VERSION]: sdkVersion,
2384
- [TAG_CONTENT_TYPE]: isPublic ? TAG_CONTENT_TYPE_JSON : TAG_CONTENT_TYPE_OCTET_STREAM,
2385
- [TAG_MANIFEST_UPLOAD]: "true"
2386
- }
2128
+ const {
2129
+ txId,
2130
+ signature,
2131
+ txAmount: arCost,
2132
+ serviceFee
2133
+ } = await this._sendSignTxRequest(body, instruction.network, onStatus);
2134
+ this.logger?.log(
2135
+ "dStorage/managed-payment",
2136
+ `Arweave bundler Tx signed remotely (${this.signingServerUrl} API service) \u2192 txId: ${txId} cost: ~${arCost} ${this.token} (service fee: ${serviceFee})`
2387
2137
  );
2388
- let onChainManifestId;
2389
- if (isPublic) {
2390
- onChainManifestId = manifestResult.id;
2391
- } else {
2392
- onChainManifestId = await uploadScheme.encryptStorageId(
2393
- manifestResult.id
2394
- );
2395
- }
2396
- const ownerSecret = chunkDek ? await deriveOwnerSecret(
2397
- chunkDek,
2398
- new TextEncoder().encode(manifestResult.id)
2399
- ) : await deriveOwnerSecret(
2400
- ownerSeed,
2401
- new TextEncoder().encode(manifestResult.id),
2402
- "dstorage:owner-secret:public:v1"
2138
+ return {
2139
+ txHash: txId,
2140
+ token: "AR",
2141
+ amount: arCost,
2142
+ type: this.type,
2143
+ signedTx: signature,
2144
+ paidAt: Date.now(),
2145
+ serviceFee
2146
+ };
2147
+ }
2148
+ /**
2149
+ * Managed mock flow:
2150
+ * Sends a minimal request to the signing server with the literal network
2151
+ * identifier "TEST" — the server treats this as a sandbox/test flow with no
2152
+ * real funds involved. The SDK-side network name is "managedmock" to follow
2153
+ * the lowercase naming convention; the server-side string is always "TEST".
2154
+ */
2155
+ async _payManagedMock(instruction, onStatus) {
2156
+ const body = JSON.stringify({
2157
+ txData: instruction.txData || "mock",
2158
+ network: "TEST"
2159
+ });
2160
+ const { txId, txAmount, serviceFee } = await this._sendSignTxRequest(
2161
+ body,
2162
+ "TEST",
2163
+ onStatus
2403
2164
  );
2404
- if (this.config.chainAdapter) {
2405
- const contentHash = await computeBlake3Hex(manifestData);
2406
- const storeRecovery = {
2407
- storageId: manifestResult.id,
2408
- storageProvider: manifestResult.provider,
2409
- keyEnvelope: isPublic ? void 0 : keyEnvelope,
2410
- contentHash
2411
- };
2412
- onProgress?.({
2413
- phase: "stored",
2414
- chunksUploaded: totalChunks,
2415
- totalChunks,
2416
- bytesUploaded: bytes.length,
2417
- totalBytes: bytes.length,
2418
- recovery: storeRecovery
2419
- });
2420
- const encryptedMetadata = rawMeta && uploadScheme ? bytesToBase64url(
2421
- await uploadScheme.encryptPayload(
2422
- new TextEncoder().encode(JSON.stringify(rawMeta)),
2423
- METADATA_AAD
2424
- )
2425
- ) : void 0;
2426
- try {
2427
- const chainResult = await this.config.chainAdapter.writeReference({
2428
- storageId: onChainManifestId,
2429
- encryptionScheme,
2430
- storageProvider: manifestResult.provider,
2431
- ...refId !== void 0 ? { refId } : {},
2432
- ...encryptedMetadata !== void 0 ? { encryptedMetadata } : {},
2433
- writtenAt: Date.now(),
2434
- keyEnvelope,
2435
- contentHash,
2436
- ownerSecret
2437
- });
2438
- const resultRefId = chainResult.refId;
2439
- this.logger?.log(
2440
- "dStorage",
2441
- `Chunked upload complete \u2192 ${totalChunks} chunks + manifest, refId: ${resultRefId}`
2442
- );
2443
- return {
2444
- chainRefId: resultRefId,
2445
- storageId: manifestResult.id,
2446
- storageProvider: manifestResult.provider,
2447
- chainProvider: chainResult.provider,
2448
- uploadedAt: manifestResult.uploadedAt,
2449
- encryptionScheme,
2450
- costEstimate: {
2451
- storageCost: {
2452
- amount: manifestResult.cost?.amount ?? "unknown",
2453
- token: manifestResult.cost?.token ?? "AR"
2454
- },
2455
- chainCost: {
2456
- amount: chainResult.paymentReceipt?.amount ?? "unknown",
2457
- token: chainResult.paymentReceipt?.token ?? "DUST"
2458
- },
2459
- fileSizeBytes: bytes.length
2460
- }
2461
- };
2462
- } catch {
2463
- throw new StorePartialError(storeRecovery);
2464
- }
2465
- }
2466
2165
  this.logger?.log(
2467
- "dStorage",
2468
- `Chunked upload complete (storage-only) \u2192 ${totalChunks} chunks + manifest, storageId: ${manifestResult.id}`
2166
+ "dStorage/managed-payment",
2167
+ `ManagedMock Tx signed remotely (${this.signingServerUrl}) \u2192 txId: ${txId} cost: ~${txAmount} MOCK (service fee: ${serviceFee})`
2469
2168
  );
2470
2169
  return {
2471
- storageId: manifestResult.id,
2472
- storageProvider: manifestResult.provider,
2473
- uploadedAt: manifestResult.uploadedAt,
2474
- encryptionScheme,
2475
- costEstimate: {
2476
- storageCost: {
2477
- amount: manifestResult.cost?.amount ?? "unknown",
2478
- token: manifestResult.cost?.token ?? "AR"
2479
- },
2480
- fileSizeBytes: bytes.length
2481
- }
2170
+ txHash: txId,
2171
+ token: "MOCK",
2172
+ amount: txAmount,
2173
+ type: this.type,
2174
+ paidAt: Date.now(),
2175
+ serviceFee
2482
2176
  };
2483
2177
  }
2484
2178
  /**
2485
- * Reassemble a chunked file from its manifest.
2486
- * Fetches and decrypts each chunk sequentially to bound memory usage.
2179
+ * Shared HTTP helper: POSTs a transaction payload to the signing server and
2180
+ * returns the parsed SignTxResponse.
2181
+ *
2182
+ * - Arweave: txJson is the Arweave tx JSON object (from tx.toJSON()); the server
2183
+ * signs it and returns the signature + owner in the response fields.
2184
+ * - Midnight: txJson is the hex-serialized UnboundTransaction; the server balances
2185
+ * and signs it, returning the hex-serialized FinalizedTransaction in the
2186
+ * `signature` response field.
2187
+ *
2188
+ * Throws on network errors or non-2xx HTTP responses.
2487
2189
  */
2488
- async _retrieveChunked(manifest, scheme = null, tags = {}) {
2489
- if (manifest.chunks.length !== manifest.chunkCount) {
2190
+ async _sendSignTxRequest(body, network, onStatus) {
2191
+ onStatus?.(`Sending ${network} transaction to signing server\u2026`);
2192
+ let signResp;
2193
+ try {
2194
+ this.logger?.log(
2195
+ "dStorage/managed-payment",
2196
+ `${network}: delegating to managed payment service (serialized req length=${body.length})`
2197
+ );
2198
+ const signal = this.requestTimeoutMs > 0 ? AbortSignal.timeout(this.requestTimeoutMs) : void 0;
2199
+ signResp = await fetch(
2200
+ `${this.signingServerUrl}${POST_SIGN_TX_API_PATH}`,
2201
+ {
2202
+ method: "POST",
2203
+ headers: {
2204
+ "Content-Type": "application/json",
2205
+ Authorization: `Bearer ${this.authToken}`
2206
+ },
2207
+ body,
2208
+ ...signal !== void 0 ? { signal } : {}
2209
+ }
2210
+ );
2211
+ } catch (err) {
2490
2212
  throw new DStorageError(
2491
- CoreErrorCode.MANIFEST_CHUNK_COUNT_MISMATCH,
2492
- `[dStorage] Manifest declares ${manifest.chunkCount} chunks but contains ${manifest.chunks.length} entries`
2213
+ PaymentErrorCode.MANAGED_SERVER_UNREACHABLE,
2214
+ `[dStorage/managed-payment] Could not reach signing server: ${err}`,
2215
+ { cause: err }
2493
2216
  );
2494
2217
  }
2495
- const sorted = [...manifest.chunks].sort((a, b) => a.index - b.index);
2496
- for (let i = 0; i < sorted.length; i++) {
2497
- if (sorted[i].index !== i) {
2498
- throw new DStorageError(
2499
- CoreErrorCode.MANIFEST_NOT_CONTIGUOUS,
2500
- `[dStorage] Manifest chunk list is not contiguous: expected index ${i}, got ${sorted[i].index}`
2501
- );
2502
- }
2503
- }
2504
- const result = new Uint8Array(manifest.totalSize);
2505
- let offset = 0;
2506
- for (const entry of sorted) {
2507
- const { bytes: rawChunkBytes } = await this.config.storageAdapter.retrieve(entry.storageId);
2508
- let chunkBytes = rawChunkBytes;
2509
- if (scheme) {
2510
- try {
2511
- chunkBytes = await scheme.decryptPayload(
2512
- rawChunkBytes,
2513
- chunkAad(entry.index, manifest.chunkCount)
2514
- );
2515
- } catch (err) {
2516
- throw new DStorageError(
2517
- CoreErrorCode.DECRYPT_CHUNK_FAILED,
2518
- `[dStorage] Failed to decrypt chunk ${entry.index}: ${err}`,
2519
- { cause: err }
2520
- );
2521
- }
2522
- }
2523
- if (offset + chunkBytes.length > manifest.totalSize) {
2524
- throw new DStorageError(
2525
- CoreErrorCode.CHUNK_OVERFLOWS_TOTAL_SIZE,
2526
- `[dStorage] Chunk ${entry.index} overflows manifest totalSize`
2527
- );
2528
- }
2529
- result.set(chunkBytes, offset);
2530
- offset += chunkBytes.length;
2531
- this.logger?.log(
2532
- "dStorage",
2533
- `Retrieved chunk ${entry.index + 1}/${manifest.chunkCount}: ${chunkBytes.length} bytes`
2218
+ if (signResp.status === 409) {
2219
+ throw new DStorageError(
2220
+ PaymentErrorCode.MANAGED_KEY_CHANGED,
2221
+ "[dStorage/managed-payment] Signing server key has changed \u2014 re-issue your API token to obtain the updated public key."
2534
2222
  );
2535
2223
  }
2536
- if (offset !== manifest.totalSize) {
2224
+ if (!signResp.ok) {
2225
+ const rawBody = await signResp.text().catch(() => "");
2226
+ const body2 = sanitiseErrorBody(rawBody);
2537
2227
  throw new DStorageError(
2538
- CoreErrorCode.REASSEMBLY_SIZE_MISMATCH,
2539
- `[dStorage] Reassembled ${offset} bytes but manifest declares totalSize ${manifest.totalSize}`
2228
+ PaymentErrorCode.MANAGED_SIGN_HTTP_ERROR,
2229
+ `[dStorage/managed-payment] POST ${POST_SIGN_TX_API_PATH} failed: HTTP ${signResp.status}${body2 ? ` \u2014 ${body2}` : ""}`
2540
2230
  );
2541
2231
  }
2542
- return {
2543
- bytes: result,
2544
- metadata: {},
2545
- tags
2546
- };
2547
- }
2548
- assertConnected() {
2549
- if (!this._initialized && this.wallet === null) {
2232
+ let parsed;
2233
+ try {
2234
+ parsed = await signResp.json();
2235
+ } catch {
2550
2236
  throw new DStorageError(
2551
- CoreErrorCode.NOT_INITIALIZED,
2552
- "[dStorage] Not connected. Call dStorage.init() first."
2237
+ PaymentErrorCode.MANAGED_NON_JSON_RESPONSE,
2238
+ `[dStorage/managed-payment] Managed payment service returned a non-JSON response \u2014 check that the service URL is correct and reachable: ${this.signingServerUrl}${POST_SIGN_TX_API_PATH}`
2553
2239
  );
2554
2240
  }
2241
+ return parseSignTxResponse(parsed);
2555
2242
  }
2556
2243
  };
2557
2244
 
2558
- // ../storage/dist/chunk-URI7FNOW.mjs
2559
- var StorageErrorCode = {
2560
- // adapters/mock.ts (15000–15049)
2561
- MOCK_AUTH_TOKEN_REQUIRED: 15001,
2562
- MOCK_DATA_NOT_FOUND: 15002,
2563
- // adapters/integrity.ts (15050–15149)
2564
- CONTENT_HASH_MISMATCH: 15050,
2565
- // adapters/arweave-local.ts (15150–15199)
2566
- ARWEAVE_LOCAL_INVALID_ADDRESS: 15150,
2567
- ARWEAVE_LOCAL_NOT_INSTALLED: 15151,
2568
- // adapters/arweave.ts (15200–15399)
2569
- ARWEAVE_AUTH_TOKEN_REQUIRED: 15200,
2570
- ARWEAVE_NOT_INSTALLED: 15201,
2571
- ARWEAVE_NO_SIGNED_TX: 15202,
2572
- ARWEAVE_INVALID_TX_ID: 15203,
2573
- ARWEAVE_TX_TOO_LARGE: 15204,
2574
- ARWEAVE_TX_METADATA_FAILED: 15205,
2575
- ARWEAVE_NO_DATA_RETURNED: 15206,
2576
- ARWEAVE_SIZE_MISMATCH: 15207,
2577
- ARWEAVE_HASH_TAG_MISSING: 15208,
2578
- ARWEAVE_UPLOADER_INIT_FAILED: 15209,
2579
- ARWEAVE_CHUNK_UPLOAD_FAILED: 15210,
2580
- ARWEAVE_CONFIRM_TIMEOUT: 15211,
2581
- ARWEAVE_GET_DATA_FAILED: 15212,
2582
- // adapters/arweave-bundler.ts (15400–15599)
2583
- ARWEAVE_BUNDLER_AUTH_TOKEN_REQUIRED: 15400,
2584
- ARWEAVE_BUNDLER_LOCAL_NOT_SUPPORTED: 15401,
2585
- ARWEAVE_BUNDLER_INVALID_TX_ID: 15402,
2586
- ARWEAVE_BUNDLER_TX_METADATA_FAILED: 15403,
2587
- ARWEAVE_BUNDLER_HASH_TAG_MISSING: 15404,
2588
- ARWEAVE_BUNDLER_RETRIEVE_HTTP_ERROR: 15405,
2589
- ARWEAVE_BUNDLER_SIZE_LIMIT_EXCEEDED: 15406,
2590
- ARWEAVE_BUNDLER_SIZE_MISMATCH: 15407,
2591
- ARWEAVE_BUNDLER_COST_ESTIMATE_FAILED: 15408,
2592
- ARWEAVE_BUNDLER_NO_COST_ESTIMATE: 15409,
2593
- ARWEAVE_BUNDLER_INVALID_SIGN_TX_RESPONSE: 15410,
2594
- ARWEAVE_BUNDLER_INVALID_AUTH_TOKEN_FORMAT: 15411,
2595
- ARWEAVE_BUNDLER_INVALID_AUTH_TOKEN_KEY: 15412,
2596
- ARWEAVE_BUNDLER_NO_TX_ID_RETURNED: 15413,
2597
- ARWEAVE_BUNDLER_UPLOAD_FAILED: 15414
2598
- };
2599
-
2600
2245
  // ../storage/dist/chunk-BIT7XMIA.mjs
2601
2246
  var memoryStore = /* @__PURE__ */ new Map();
2602
2247
  function bytesToBase64url2(bytes) {
@@ -5132,7 +4777,6 @@ export {
5132
4777
  METADATA_AAD,
5133
4778
  METADATA_ONLY_OWNER_SECRET_SALT,
5134
4779
  ManagedPaymentAdapter,
5135
- MetaTx,
5136
4780
  MidnightChainAdapter,
5137
4781
  MidnightSimulatorChainAdapter,
5138
4782
  MnemonicEncryptionAdapter,
@@ -5155,7 +4799,6 @@ export {
5155
4799
  computeBlake3Hex,
5156
4800
  decryptStorageIdXChaCha,
5157
4801
  decryptXChaCha,
5158
- defaultChainToken,
5159
4802
  deriveKek,
5160
4803
  deriveOwnerSecret,
5161
4804
  deriveSymmetricKeyFromSeed,