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