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