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