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