mnemospark 0.1.2
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/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1643 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +595 -0
- package/dist/index.js +3217 -0
- package/dist/index.js.map +1 -0
- package/openclaw.plugin.json +22 -0
- package/package.json +91 -0
- package/scripts/README.md +61 -0
- package/scripts/install-aws-cli.sh +28 -0
- package/scripts/install-jq.sh +13 -0
- package/scripts/install-pnpm.sh +20 -0
- package/scripts/reinstall.sh +102 -0
- package/scripts/seed-mnemospark-backend.sh +96 -0
- package/scripts/sync-plugin-version.js +24 -0
- package/scripts/uninstall.sh +67 -0
- package/scripts/verify-dev-tools.sh +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3217 @@
|
|
|
1
|
+
// src/proxy.ts
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
4
|
+
|
|
5
|
+
// src/balance.ts
|
|
6
|
+
import { createPublicClient, http, erc20Abi } from "viem";
|
|
7
|
+
import { base } from "viem/chains";
|
|
8
|
+
|
|
9
|
+
// src/errors.ts
|
|
10
|
+
var InsufficientFundsError = class extends Error {
|
|
11
|
+
code = "INSUFFICIENT_FUNDS";
|
|
12
|
+
currentBalanceUSD;
|
|
13
|
+
requiredUSD;
|
|
14
|
+
walletAddress;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
const msg = [
|
|
17
|
+
`Insufficient balance. Current: ${opts.currentBalanceUSD}, Required: ${opts.requiredUSD}`,
|
|
18
|
+
`Options:`,
|
|
19
|
+
` 1. Fund wallet: ${opts.walletAddress}`,
|
|
20
|
+
` 2. Use free model: /model free`
|
|
21
|
+
].join("\n");
|
|
22
|
+
super(msg);
|
|
23
|
+
this.name = "InsufficientFundsError";
|
|
24
|
+
this.currentBalanceUSD = opts.currentBalanceUSD;
|
|
25
|
+
this.requiredUSD = opts.requiredUSD;
|
|
26
|
+
this.walletAddress = opts.walletAddress;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var EmptyWalletError = class extends Error {
|
|
30
|
+
code = "EMPTY_WALLET";
|
|
31
|
+
walletAddress;
|
|
32
|
+
constructor(walletAddress) {
|
|
33
|
+
const msg = [
|
|
34
|
+
`No USDC balance.`,
|
|
35
|
+
`Options:`,
|
|
36
|
+
` 1. Fund wallet: ${walletAddress}`,
|
|
37
|
+
` 2. Use free model: /model free`,
|
|
38
|
+
` 3. Uninstall: bash ~/.openclaw/extensions/mnemospark/scripts/uninstall.sh`
|
|
39
|
+
].join("\n");
|
|
40
|
+
super(msg);
|
|
41
|
+
this.name = "EmptyWalletError";
|
|
42
|
+
this.walletAddress = walletAddress;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function isInsufficientFundsError(error) {
|
|
46
|
+
return error instanceof Error && error.code === "INSUFFICIENT_FUNDS";
|
|
47
|
+
}
|
|
48
|
+
function isEmptyWalletError(error) {
|
|
49
|
+
return error instanceof Error && error.code === "EMPTY_WALLET";
|
|
50
|
+
}
|
|
51
|
+
function isBalanceError(error) {
|
|
52
|
+
return isInsufficientFundsError(error) || isEmptyWalletError(error);
|
|
53
|
+
}
|
|
54
|
+
var RpcError = class extends Error {
|
|
55
|
+
code = "RPC_ERROR";
|
|
56
|
+
originalError;
|
|
57
|
+
constructor(message, originalError) {
|
|
58
|
+
super(`RPC error: ${message}. Check network connectivity.`);
|
|
59
|
+
this.name = "RpcError";
|
|
60
|
+
this.originalError = originalError;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
function isRpcError(error) {
|
|
64
|
+
return error instanceof Error && error.code === "RPC_ERROR";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/balance.ts
|
|
68
|
+
var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
69
|
+
var CACHE_TTL_MS = 3e4;
|
|
70
|
+
var BALANCE_THRESHOLDS = {
|
|
71
|
+
/** Low balance warning threshold: $1.00 */
|
|
72
|
+
LOW_BALANCE_MICROS: 1000000n,
|
|
73
|
+
/** Effectively zero threshold: $0.0001 (covers dust/rounding) */
|
|
74
|
+
ZERO_THRESHOLD: 100n
|
|
75
|
+
};
|
|
76
|
+
var BalanceMonitor = class {
|
|
77
|
+
client;
|
|
78
|
+
walletAddress;
|
|
79
|
+
/** Cached balance (null = not yet fetched) */
|
|
80
|
+
cachedBalance = null;
|
|
81
|
+
/** Timestamp when cache was last updated */
|
|
82
|
+
cachedAt = 0;
|
|
83
|
+
constructor(walletAddress) {
|
|
84
|
+
this.walletAddress = walletAddress;
|
|
85
|
+
this.client = createPublicClient({
|
|
86
|
+
chain: base,
|
|
87
|
+
transport: http(void 0, {
|
|
88
|
+
timeout: 1e4
|
|
89
|
+
// 10 second timeout to prevent hanging on slow RPC
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check current USDC balance.
|
|
95
|
+
* Uses cache if valid, otherwise fetches from RPC.
|
|
96
|
+
*/
|
|
97
|
+
async checkBalance() {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
if (this.cachedBalance !== null && now - this.cachedAt < CACHE_TTL_MS) {
|
|
100
|
+
return this.buildInfo(this.cachedBalance);
|
|
101
|
+
}
|
|
102
|
+
const balance = await this.fetchBalance();
|
|
103
|
+
this.cachedBalance = balance;
|
|
104
|
+
this.cachedAt = now;
|
|
105
|
+
return this.buildInfo(balance);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if balance is sufficient for an estimated cost.
|
|
109
|
+
*
|
|
110
|
+
* @param estimatedCostMicros - Estimated cost in USDC smallest unit (6 decimals)
|
|
111
|
+
*/
|
|
112
|
+
async checkSufficient(estimatedCostMicros) {
|
|
113
|
+
const info = await this.checkBalance();
|
|
114
|
+
if (info.balance >= estimatedCostMicros) {
|
|
115
|
+
return { sufficient: true, info };
|
|
116
|
+
}
|
|
117
|
+
const shortfall = estimatedCostMicros - info.balance;
|
|
118
|
+
return {
|
|
119
|
+
sufficient: false,
|
|
120
|
+
info,
|
|
121
|
+
shortfall: this.formatUSDC(shortfall)
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Optimistically deduct estimated cost from cached balance.
|
|
126
|
+
* Call this after a successful payment to keep cache accurate.
|
|
127
|
+
*
|
|
128
|
+
* @param amountMicros - Amount to deduct in USDC smallest unit
|
|
129
|
+
*/
|
|
130
|
+
deductEstimated(amountMicros) {
|
|
131
|
+
if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
|
|
132
|
+
this.cachedBalance -= amountMicros;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Invalidate cache, forcing next checkBalance() to fetch from RPC.
|
|
137
|
+
* Call this after a payment failure to get accurate balance.
|
|
138
|
+
*/
|
|
139
|
+
invalidate() {
|
|
140
|
+
this.cachedBalance = null;
|
|
141
|
+
this.cachedAt = 0;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Force refresh balance from RPC (ignores cache).
|
|
145
|
+
*/
|
|
146
|
+
async refresh() {
|
|
147
|
+
this.invalidate();
|
|
148
|
+
return this.checkBalance();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Format USDC amount (in micros) as "$X.XX".
|
|
152
|
+
*/
|
|
153
|
+
formatUSDC(amountMicros) {
|
|
154
|
+
const dollars = Number(amountMicros) / 1e6;
|
|
155
|
+
return `$${dollars.toFixed(2)}`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get the wallet address being monitored.
|
|
159
|
+
*/
|
|
160
|
+
getWalletAddress() {
|
|
161
|
+
return this.walletAddress;
|
|
162
|
+
}
|
|
163
|
+
/** Fetch balance from RPC */
|
|
164
|
+
async fetchBalance() {
|
|
165
|
+
try {
|
|
166
|
+
const balance = await this.client.readContract({
|
|
167
|
+
address: USDC_BASE,
|
|
168
|
+
abi: erc20Abi,
|
|
169
|
+
functionName: "balanceOf",
|
|
170
|
+
args: [this.walletAddress]
|
|
171
|
+
});
|
|
172
|
+
return balance;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw new RpcError(error instanceof Error ? error.message : "Unknown error", error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Build BalanceInfo from raw balance */
|
|
178
|
+
buildInfo(balance) {
|
|
179
|
+
return {
|
|
180
|
+
balance,
|
|
181
|
+
balanceUSD: this.formatUSDC(balance),
|
|
182
|
+
isLow: balance < BALANCE_THRESHOLDS.LOW_BALANCE_MICROS,
|
|
183
|
+
isEmpty: balance < BALANCE_THRESHOLDS.ZERO_THRESHOLD,
|
|
184
|
+
walletAddress: this.walletAddress
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// src/config.ts
|
|
190
|
+
var DEFAULT_PORT = 7120;
|
|
191
|
+
var PROXY_PORT = (() => {
|
|
192
|
+
const envPort = process.env.MNEMOSPARK_PROXY_PORT;
|
|
193
|
+
if (envPort) {
|
|
194
|
+
const parsed = parseInt(envPort, 10);
|
|
195
|
+
if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
|
|
196
|
+
return parsed;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return DEFAULT_PORT;
|
|
200
|
+
})();
|
|
201
|
+
var MNEMOSPARK_BACKEND_API_BASE_URL = (process.env.MNEMOSPARK_BACKEND_API_BASE_URL ?? "").trim();
|
|
202
|
+
|
|
203
|
+
// src/mnemospark-request-sign.ts
|
|
204
|
+
import { getAddress } from "viem";
|
|
205
|
+
import { privateKeyToAccount, signTypedData } from "viem/accounts";
|
|
206
|
+
|
|
207
|
+
// src/nonce.ts
|
|
208
|
+
function createNonce() {
|
|
209
|
+
const bytes = new Uint8Array(32);
|
|
210
|
+
crypto.getRandomValues(bytes);
|
|
211
|
+
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/mnemospark-request-sign.ts
|
|
215
|
+
var MNEMOSPARK_DOMAIN_NAME = "Mnemospark";
|
|
216
|
+
var MNEMOSPARK_DOMAIN_VERSION = "1";
|
|
217
|
+
var MNEMOSPARK_VERIFYING_CONTRACT = "0x0000000000000000000000000000000000000001";
|
|
218
|
+
var BASE_MAINNET_CHAIN_ID = 8453;
|
|
219
|
+
var BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
220
|
+
var MNEMOSPARK_REQUEST_TYPES = {
|
|
221
|
+
MnemosparkRequest: [
|
|
222
|
+
{ name: "method", type: "string" },
|
|
223
|
+
{ name: "path", type: "string" },
|
|
224
|
+
{ name: "walletAddress", type: "string" },
|
|
225
|
+
{ name: "nonce", type: "string" },
|
|
226
|
+
{ name: "timestamp", type: "string" }
|
|
227
|
+
]
|
|
228
|
+
};
|
|
229
|
+
function encodeBase64Json(value) {
|
|
230
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
231
|
+
}
|
|
232
|
+
function normalizeMethod(method) {
|
|
233
|
+
const normalized = method.trim().toUpperCase();
|
|
234
|
+
if (!normalized) {
|
|
235
|
+
throw new Error("Request signing requires a non-empty HTTP method.");
|
|
236
|
+
}
|
|
237
|
+
return normalized;
|
|
238
|
+
}
|
|
239
|
+
function normalizePath(path) {
|
|
240
|
+
const trimmed = path.trim();
|
|
241
|
+
if (!trimmed) {
|
|
242
|
+
throw new Error("Request signing requires a non-empty path.");
|
|
243
|
+
}
|
|
244
|
+
let parsedPath;
|
|
245
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
246
|
+
parsedPath = new URL(trimmed).pathname;
|
|
247
|
+
} else {
|
|
248
|
+
parsedPath = trimmed.split("?")[0]?.split("#")[0] ?? "";
|
|
249
|
+
}
|
|
250
|
+
if (!parsedPath) {
|
|
251
|
+
throw new Error("Request signing requires a valid request path.");
|
|
252
|
+
}
|
|
253
|
+
const prefixed = parsedPath.startsWith("/") ? parsedPath : `/${parsedPath}`;
|
|
254
|
+
const deduplicated = prefixed.replace(/\/{2,}/g, "/");
|
|
255
|
+
return deduplicated.length > 1 && deduplicated.endsWith("/") ? deduplicated.slice(0, -1) : deduplicated;
|
|
256
|
+
}
|
|
257
|
+
function normalizeTimestamp(value) {
|
|
258
|
+
const timestamp = value ?? Math.floor(Date.now() / 1e3).toString();
|
|
259
|
+
if (!/^\d+$/.test(timestamp)) {
|
|
260
|
+
throw new Error("Request signing timestamp must be a Unix timestamp in seconds.");
|
|
261
|
+
}
|
|
262
|
+
return timestamp;
|
|
263
|
+
}
|
|
264
|
+
function normalizeNonce(value) {
|
|
265
|
+
const nonce = value ?? createNonce();
|
|
266
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(nonce)) {
|
|
267
|
+
throw new Error("Request signing nonce must be a 32-byte hex value.");
|
|
268
|
+
}
|
|
269
|
+
return nonce;
|
|
270
|
+
}
|
|
271
|
+
function normalizeChainId(chainId) {
|
|
272
|
+
const selected = chainId ?? BASE_MAINNET_CHAIN_ID;
|
|
273
|
+
if (selected !== BASE_MAINNET_CHAIN_ID && selected !== BASE_SEPOLIA_CHAIN_ID) {
|
|
274
|
+
throw new Error(`Unsupported chainId for request signing: ${selected}`);
|
|
275
|
+
}
|
|
276
|
+
return selected;
|
|
277
|
+
}
|
|
278
|
+
function createMnemosparkRequestDomain(chainId) {
|
|
279
|
+
return {
|
|
280
|
+
name: MNEMOSPARK_DOMAIN_NAME,
|
|
281
|
+
version: MNEMOSPARK_DOMAIN_VERSION,
|
|
282
|
+
chainId: normalizeChainId(chainId),
|
|
283
|
+
verifyingContract: MNEMOSPARK_VERIFYING_CONTRACT
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function createMnemosparkRequestPayload(method, path, walletAddress, options) {
|
|
287
|
+
return {
|
|
288
|
+
method: normalizeMethod(method),
|
|
289
|
+
path: normalizePath(path),
|
|
290
|
+
walletAddress: getAddress(walletAddress),
|
|
291
|
+
nonce: normalizeNonce(options?.nonce),
|
|
292
|
+
timestamp: normalizeTimestamp(options?.timestamp)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async function createWalletSignatureHeaderValue(method, path, walletAddress, walletPrivateKey, options) {
|
|
296
|
+
const payload = createMnemosparkRequestPayload(method, path, walletAddress, {
|
|
297
|
+
nonce: options?.nonce,
|
|
298
|
+
timestamp: options?.timestamp
|
|
299
|
+
});
|
|
300
|
+
const signer = privateKeyToAccount(walletPrivateKey);
|
|
301
|
+
if (signer.address.toLowerCase() !== payload.walletAddress.toLowerCase()) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Wallet address ${payload.walletAddress} does not match signer address ${signer.address}.`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const signature = await signTypedData({
|
|
307
|
+
privateKey: walletPrivateKey,
|
|
308
|
+
domain: createMnemosparkRequestDomain(options?.chainId),
|
|
309
|
+
types: MNEMOSPARK_REQUEST_TYPES,
|
|
310
|
+
primaryType: "MnemosparkRequest",
|
|
311
|
+
message: payload
|
|
312
|
+
});
|
|
313
|
+
const headerEnvelope = {
|
|
314
|
+
payloadB64: encodeBase64Json(payload),
|
|
315
|
+
signature,
|
|
316
|
+
address: signer.address
|
|
317
|
+
};
|
|
318
|
+
return encodeBase64Json(headerEnvelope);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/cloud-utils.ts
|
|
322
|
+
function normalizeBaseUrl(baseUrl) {
|
|
323
|
+
return baseUrl.replace(/\/+$/, "");
|
|
324
|
+
}
|
|
325
|
+
function asRecord(value) {
|
|
326
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
return value;
|
|
330
|
+
}
|
|
331
|
+
function asNumber(value) {
|
|
332
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
336
|
+
const parsed = Number.parseFloat(value);
|
|
337
|
+
if (Number.isFinite(parsed)) {
|
|
338
|
+
return parsed;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
function asNonEmptyString(value) {
|
|
344
|
+
if (typeof value !== "string") {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const trimmed = value.trim();
|
|
348
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
349
|
+
}
|
|
350
|
+
function normalizePaymentRequired(headers) {
|
|
351
|
+
return headers.get("PAYMENT-REQUIRED") ?? headers.get("x-payment-required") ?? void 0;
|
|
352
|
+
}
|
|
353
|
+
function normalizePaymentResponse(headers) {
|
|
354
|
+
return headers.get("PAYMENT-RESPONSE") ?? headers.get("x-payment-response") ?? void 0;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/wallet-signature.ts
|
|
358
|
+
function normalizeWalletSignature(value) {
|
|
359
|
+
const trimmed = value?.trim();
|
|
360
|
+
return trimmed && trimmed.length > 0 ? trimmed : void 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/cloud-price-storage.ts
|
|
364
|
+
var PRICE_STORAGE_PROXY_PATH = "/mnemospark/price-storage";
|
|
365
|
+
var UPLOAD_PROXY_PATH = "/mnemospark/upload";
|
|
366
|
+
function asStringRecord(value) {
|
|
367
|
+
const record = asRecord(value);
|
|
368
|
+
if (!record) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const output = {};
|
|
372
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
373
|
+
if (typeof entry !== "string") {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
output[key] = entry;
|
|
377
|
+
}
|
|
378
|
+
return output;
|
|
379
|
+
}
|
|
380
|
+
function parsePriceStorageQuoteRequest(payload) {
|
|
381
|
+
const record = asRecord(payload);
|
|
382
|
+
if (!record) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
const walletAddress = asNonEmptyString(record.wallet_address);
|
|
386
|
+
const objectId = asNonEmptyString(record.object_id);
|
|
387
|
+
const objectIdHash = asNonEmptyString(record.object_id_hash);
|
|
388
|
+
const gb = asNumber(record.gb);
|
|
389
|
+
const provider = asNonEmptyString(record.provider);
|
|
390
|
+
const region = asNonEmptyString(record.region);
|
|
391
|
+
if (!walletAddress || !objectId || !objectIdHash || gb === null || !provider || !region) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
wallet_address: walletAddress,
|
|
396
|
+
object_id: objectId,
|
|
397
|
+
object_id_hash: objectIdHash,
|
|
398
|
+
gb,
|
|
399
|
+
provider,
|
|
400
|
+
region
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function parsePriceStorageQuoteResponse(payload) {
|
|
404
|
+
const record = asRecord(payload);
|
|
405
|
+
if (!record) {
|
|
406
|
+
throw new Error("Invalid price-storage response payload");
|
|
407
|
+
}
|
|
408
|
+
const timestamp = asNonEmptyString(record.timestamp);
|
|
409
|
+
const quoteId = asNonEmptyString(record.quote_id);
|
|
410
|
+
const storagePrice = asNumber(record.storage_price);
|
|
411
|
+
const addr = asNonEmptyString(record.addr);
|
|
412
|
+
const objectId = asNonEmptyString(record.object_id);
|
|
413
|
+
const objectIdHash = asNonEmptyString(record.object_id_hash);
|
|
414
|
+
const objectSizeGb = asNumber(record.object_size_gb);
|
|
415
|
+
const provider = asNonEmptyString(record.provider);
|
|
416
|
+
const location = asNonEmptyString(record.location);
|
|
417
|
+
if (!timestamp || !quoteId || storagePrice === null || !addr || !objectId || !objectIdHash || objectSizeGb === null || !provider || !location) {
|
|
418
|
+
throw new Error("Price-storage response is missing required fields");
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
timestamp,
|
|
422
|
+
quote_id: quoteId,
|
|
423
|
+
storage_price: storagePrice,
|
|
424
|
+
addr,
|
|
425
|
+
object_id: objectId,
|
|
426
|
+
object_id_hash: objectIdHash,
|
|
427
|
+
object_size_gb: objectSizeGb,
|
|
428
|
+
provider,
|
|
429
|
+
location
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function parseStorageUploadRequest(payload) {
|
|
433
|
+
const record = asRecord(payload);
|
|
434
|
+
if (!record) {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
const quoteId = asNonEmptyString(record.quote_id);
|
|
438
|
+
const walletAddress = asNonEmptyString(record.wallet_address);
|
|
439
|
+
const objectId = asNonEmptyString(record.object_id);
|
|
440
|
+
const objectIdHash = asNonEmptyString(record.object_id_hash);
|
|
441
|
+
const quotedStoragePrice = asNumber(record.quoted_storage_price);
|
|
442
|
+
const payloadRecord = asRecord(record.payload);
|
|
443
|
+
if (!payloadRecord) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const modeRaw = asNonEmptyString(payloadRecord.mode);
|
|
447
|
+
const mode = modeRaw === "inline" || modeRaw === "presigned" ? modeRaw : null;
|
|
448
|
+
const contentBase64 = payloadRecord.content_base64 === void 0 ? void 0 : asNonEmptyString(payloadRecord.content_base64);
|
|
449
|
+
const contentSha256 = asNonEmptyString(payloadRecord.content_sha256);
|
|
450
|
+
const contentLengthBytes = asNumber(payloadRecord.content_length_bytes);
|
|
451
|
+
const wrappedDek = asNonEmptyString(payloadRecord.wrapped_dek);
|
|
452
|
+
const encryptionAlgorithm = asNonEmptyString(payloadRecord.encryption_algorithm);
|
|
453
|
+
const bucketNameHint = asNonEmptyString(payloadRecord.bucket_name_hint);
|
|
454
|
+
const keyStorePathHint = asNonEmptyString(payloadRecord.key_store_path_hint);
|
|
455
|
+
if (!quoteId || !walletAddress || !objectId || !objectIdHash || quotedStoragePrice === null || !mode || !contentSha256 || contentLengthBytes === null || !wrappedDek || encryptionAlgorithm !== "AES-256-GCM" || !bucketNameHint || !keyStorePathHint) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
if (mode === "inline" && !contentBase64) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
quote_id: quoteId,
|
|
463
|
+
wallet_address: walletAddress,
|
|
464
|
+
object_id: objectId,
|
|
465
|
+
object_id_hash: objectIdHash,
|
|
466
|
+
quoted_storage_price: quotedStoragePrice,
|
|
467
|
+
payload: {
|
|
468
|
+
mode,
|
|
469
|
+
content_base64: contentBase64 ?? void 0,
|
|
470
|
+
content_sha256: contentSha256,
|
|
471
|
+
content_length_bytes: contentLengthBytes,
|
|
472
|
+
wrapped_dek: wrappedDek,
|
|
473
|
+
encryption_algorithm: "AES-256-GCM",
|
|
474
|
+
bucket_name_hint: bucketNameHint,
|
|
475
|
+
key_store_path_hint: keyStorePathHint
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function parseStorageUploadResponse(payload) {
|
|
480
|
+
const record = asRecord(payload);
|
|
481
|
+
if (!record) {
|
|
482
|
+
throw new Error("Invalid upload response payload");
|
|
483
|
+
}
|
|
484
|
+
const quoteId = asNonEmptyString(record.quote_id);
|
|
485
|
+
const addr = asNonEmptyString(record.addr);
|
|
486
|
+
const addrHash = asNonEmptyString(record.addr_hash);
|
|
487
|
+
const transId = asNonEmptyString(record.trans_id);
|
|
488
|
+
const storagePrice = asNumber(record.storage_price);
|
|
489
|
+
const objectId = asNonEmptyString(record.object_id);
|
|
490
|
+
const objectKey = asNonEmptyString(record.object_key);
|
|
491
|
+
const provider = asNonEmptyString(record.provider);
|
|
492
|
+
const bucketName = asNonEmptyString(record.bucket_name);
|
|
493
|
+
const location = asNonEmptyString(record.location);
|
|
494
|
+
const uploadUrl = asNonEmptyString(record.upload_url);
|
|
495
|
+
const uploadHeaders = record.upload_headers === void 0 ? void 0 : asStringRecord(record.upload_headers);
|
|
496
|
+
if (!quoteId || !addr || !objectId || !objectKey || !provider || !bucketName || !location) {
|
|
497
|
+
throw new Error("Upload response is missing required fields");
|
|
498
|
+
}
|
|
499
|
+
if (record.upload_headers !== void 0 && !uploadHeaders) {
|
|
500
|
+
throw new Error("Upload response has invalid upload_headers");
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
quote_id: quoteId,
|
|
504
|
+
addr,
|
|
505
|
+
addr_hash: addrHash ?? void 0,
|
|
506
|
+
trans_id: transId ?? void 0,
|
|
507
|
+
storage_price: storagePrice ?? void 0,
|
|
508
|
+
object_id: objectId,
|
|
509
|
+
object_key: objectKey,
|
|
510
|
+
provider,
|
|
511
|
+
bucket_name: bucketName,
|
|
512
|
+
location,
|
|
513
|
+
upload_url: uploadUrl ?? void 0,
|
|
514
|
+
upload_headers: uploadHeaders ?? void 0
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
async function requestPriceStorageViaProxy(request, options = {}) {
|
|
518
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
519
|
+
const baseUrl = normalizeBaseUrl(
|
|
520
|
+
options.proxyBaseUrl ?? `http://127.0.0.1:${PROXY_PORT.toString()}`
|
|
521
|
+
);
|
|
522
|
+
const response = await fetchImpl(`${baseUrl}${PRICE_STORAGE_PROXY_PATH}`, {
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers: {
|
|
525
|
+
"Content-Type": "application/json"
|
|
526
|
+
},
|
|
527
|
+
body: JSON.stringify(request)
|
|
528
|
+
});
|
|
529
|
+
const responseBody = await response.text();
|
|
530
|
+
if (!response.ok) {
|
|
531
|
+
throw new Error(responseBody || `Price-storage proxy failed with status ${response.status}`);
|
|
532
|
+
}
|
|
533
|
+
let payload;
|
|
534
|
+
try {
|
|
535
|
+
payload = JSON.parse(responseBody);
|
|
536
|
+
} catch {
|
|
537
|
+
throw new Error("Price-storage proxy returned invalid JSON");
|
|
538
|
+
}
|
|
539
|
+
return parsePriceStorageQuoteResponse(payload);
|
|
540
|
+
}
|
|
541
|
+
async function requestStorageUploadViaProxy(request, options = {}) {
|
|
542
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
543
|
+
const baseUrl = normalizeBaseUrl(
|
|
544
|
+
options.proxyBaseUrl ?? `http://127.0.0.1:${PROXY_PORT.toString()}`
|
|
545
|
+
);
|
|
546
|
+
const requestHeaders = {
|
|
547
|
+
"Content-Type": "application/json"
|
|
548
|
+
};
|
|
549
|
+
if (options.idempotencyKey && options.idempotencyKey.trim().length > 0) {
|
|
550
|
+
requestHeaders["Idempotency-Key"] = options.idempotencyKey.trim();
|
|
551
|
+
}
|
|
552
|
+
const response = await fetchImpl(`${baseUrl}${UPLOAD_PROXY_PATH}`, {
|
|
553
|
+
method: "POST",
|
|
554
|
+
headers: requestHeaders,
|
|
555
|
+
body: JSON.stringify(request)
|
|
556
|
+
});
|
|
557
|
+
const responseBody = await response.text();
|
|
558
|
+
if (!response.ok) {
|
|
559
|
+
throw new Error(responseBody || `Upload proxy failed with status ${response.status}`);
|
|
560
|
+
}
|
|
561
|
+
let payload;
|
|
562
|
+
try {
|
|
563
|
+
payload = JSON.parse(responseBody);
|
|
564
|
+
} catch {
|
|
565
|
+
throw new Error("Upload proxy returned invalid JSON");
|
|
566
|
+
}
|
|
567
|
+
return parseStorageUploadResponse(payload);
|
|
568
|
+
}
|
|
569
|
+
async function forwardPriceStorageToBackend(request, options = {}) {
|
|
570
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
571
|
+
const backendBaseUrl = (options.backendBaseUrl ?? "").trim();
|
|
572
|
+
const walletSignature = normalizeWalletSignature(options.walletSignature);
|
|
573
|
+
if (!backendBaseUrl) {
|
|
574
|
+
throw new Error("MNEMOSPARK_BACKEND_API_BASE_URL is not configured");
|
|
575
|
+
}
|
|
576
|
+
const headers = {
|
|
577
|
+
"Content-Type": "application/json"
|
|
578
|
+
};
|
|
579
|
+
if (walletSignature) {
|
|
580
|
+
headers["X-Wallet-Signature"] = walletSignature;
|
|
581
|
+
}
|
|
582
|
+
const targetUrl = `${normalizeBaseUrl(backendBaseUrl)}/price-storage`;
|
|
583
|
+
const response = await fetchImpl(targetUrl, {
|
|
584
|
+
method: "POST",
|
|
585
|
+
headers,
|
|
586
|
+
body: JSON.stringify(request)
|
|
587
|
+
});
|
|
588
|
+
return {
|
|
589
|
+
status: response.status,
|
|
590
|
+
bodyText: await response.text(),
|
|
591
|
+
contentType: response.headers.get("content-type") ?? "application/json",
|
|
592
|
+
paymentRequired: normalizePaymentRequired(response.headers),
|
|
593
|
+
paymentResponse: normalizePaymentResponse(response.headers)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async function forwardStorageUploadToBackend(request, options = {}) {
|
|
597
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
598
|
+
const backendBaseUrl = (options.backendBaseUrl ?? "").trim();
|
|
599
|
+
const walletSignature = normalizeWalletSignature(options.walletSignature);
|
|
600
|
+
if (!backendBaseUrl) {
|
|
601
|
+
throw new Error("MNEMOSPARK_BACKEND_API_BASE_URL is not configured");
|
|
602
|
+
}
|
|
603
|
+
if (!walletSignature) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
"Wallet required for storage endpoints: wallet key must be present to sign requests."
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const requestHeaders = {
|
|
609
|
+
"Content-Type": "application/json",
|
|
610
|
+
"X-Wallet-Signature": walletSignature
|
|
611
|
+
};
|
|
612
|
+
if (options.idempotencyKey && options.idempotencyKey.trim().length > 0) {
|
|
613
|
+
requestHeaders["Idempotency-Key"] = options.idempotencyKey.trim();
|
|
614
|
+
}
|
|
615
|
+
const paymentSignature = options.paymentSignature?.trim();
|
|
616
|
+
const legacyPayment = options.legacyPayment?.trim();
|
|
617
|
+
if (paymentSignature) {
|
|
618
|
+
requestHeaders["PAYMENT-SIGNATURE"] = paymentSignature;
|
|
619
|
+
requestHeaders["x-payment"] = paymentSignature;
|
|
620
|
+
}
|
|
621
|
+
if (legacyPayment) {
|
|
622
|
+
requestHeaders["x-payment"] = legacyPayment;
|
|
623
|
+
requestHeaders["PAYMENT-SIGNATURE"] = requestHeaders["PAYMENT-SIGNATURE"] ?? legacyPayment;
|
|
624
|
+
}
|
|
625
|
+
const targetUrl = `${normalizeBaseUrl(backendBaseUrl)}/storage/upload`;
|
|
626
|
+
const response = await fetchImpl(targetUrl, {
|
|
627
|
+
method: "POST",
|
|
628
|
+
headers: requestHeaders,
|
|
629
|
+
body: JSON.stringify(request)
|
|
630
|
+
});
|
|
631
|
+
return {
|
|
632
|
+
status: response.status,
|
|
633
|
+
bodyText: await response.text(),
|
|
634
|
+
contentType: response.headers.get("content-type") ?? "application/json",
|
|
635
|
+
paymentRequired: normalizePaymentRequired(response.headers),
|
|
636
|
+
paymentResponse: normalizePaymentResponse(response.headers)
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/cloud-storage.ts
|
|
641
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
642
|
+
import { dirname, join, resolve, sep } from "path";
|
|
643
|
+
var STORAGE_LS_PROXY_PATH = "/mnemospark/storage/ls";
|
|
644
|
+
var STORAGE_DOWNLOAD_PROXY_PATH = "/mnemospark/storage/download";
|
|
645
|
+
var STORAGE_DELETE_PROXY_PATH = "/mnemospark/storage/delete";
|
|
646
|
+
function asBooleanOrDefault(value, defaultValue) {
|
|
647
|
+
if (typeof value === "boolean") {
|
|
648
|
+
return value;
|
|
649
|
+
}
|
|
650
|
+
return defaultValue;
|
|
651
|
+
}
|
|
652
|
+
function parseJsonText(text, errorMessage) {
|
|
653
|
+
let parsed;
|
|
654
|
+
try {
|
|
655
|
+
parsed = JSON.parse(text);
|
|
656
|
+
} catch {
|
|
657
|
+
throw new Error(errorMessage);
|
|
658
|
+
}
|
|
659
|
+
const record = asRecord(parsed);
|
|
660
|
+
if (!record) {
|
|
661
|
+
throw new Error(errorMessage);
|
|
662
|
+
}
|
|
663
|
+
return record;
|
|
664
|
+
}
|
|
665
|
+
function sanitizeObjectKeyToRelativePath(objectKey) {
|
|
666
|
+
const normalized = objectKey.replace(/\\/g, "/").trim().replace(/^\/+/, "");
|
|
667
|
+
const segments = normalized.split("/").filter((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
668
|
+
if (segments.length === 0) {
|
|
669
|
+
return "downloaded-object";
|
|
670
|
+
}
|
|
671
|
+
return join(...segments);
|
|
672
|
+
}
|
|
673
|
+
function resolveDownloadPath(outputDir, objectKey) {
|
|
674
|
+
const resolvedOutputDir = resolve(outputDir);
|
|
675
|
+
const relativeObjectPath = sanitizeObjectKeyToRelativePath(objectKey);
|
|
676
|
+
const resolvedTargetPath = resolve(resolvedOutputDir, relativeObjectPath);
|
|
677
|
+
if (resolvedTargetPath !== resolvedOutputDir && !resolvedTargetPath.startsWith(`${resolvedOutputDir}${sep}`)) {
|
|
678
|
+
throw new Error("Resolved download target escapes output directory");
|
|
679
|
+
}
|
|
680
|
+
return resolvedTargetPath;
|
|
681
|
+
}
|
|
682
|
+
function parseFilenameFromContentDisposition(contentDisposition) {
|
|
683
|
+
if (!contentDisposition) {
|
|
684
|
+
return void 0;
|
|
685
|
+
}
|
|
686
|
+
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
|
687
|
+
if (utf8Match?.[1]) {
|
|
688
|
+
try {
|
|
689
|
+
return decodeURIComponent(utf8Match[1]);
|
|
690
|
+
} catch {
|
|
691
|
+
return utf8Match[1];
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const quotedMatch = contentDisposition.match(/filename="([^"]+)"/i);
|
|
695
|
+
if (quotedMatch?.[1]) {
|
|
696
|
+
return quotedMatch[1];
|
|
697
|
+
}
|
|
698
|
+
const plainMatch = contentDisposition.match(/filename=([^;]+)/i);
|
|
699
|
+
if (plainMatch?.[1]) {
|
|
700
|
+
return plainMatch[1].trim();
|
|
701
|
+
}
|
|
702
|
+
return void 0;
|
|
703
|
+
}
|
|
704
|
+
async function requestJsonViaProxy(proxyPath, request, parser, options = {}) {
|
|
705
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
706
|
+
const baseUrl = normalizeBaseUrl(
|
|
707
|
+
options.proxyBaseUrl ?? `http://127.0.0.1:${PROXY_PORT.toString()}`
|
|
708
|
+
);
|
|
709
|
+
const response = await fetchImpl(`${baseUrl}${proxyPath}`, {
|
|
710
|
+
method: "POST",
|
|
711
|
+
headers: {
|
|
712
|
+
"Content-Type": "application/json"
|
|
713
|
+
},
|
|
714
|
+
body: JSON.stringify(request)
|
|
715
|
+
});
|
|
716
|
+
const bodyText = await response.text();
|
|
717
|
+
if (!response.ok) {
|
|
718
|
+
throw new Error(bodyText || `Cloud storage proxy failed with status ${response.status}`);
|
|
719
|
+
}
|
|
720
|
+
let payload;
|
|
721
|
+
try {
|
|
722
|
+
payload = JSON.parse(bodyText);
|
|
723
|
+
} catch {
|
|
724
|
+
throw new Error("Cloud storage proxy returned invalid JSON");
|
|
725
|
+
}
|
|
726
|
+
return parser(payload);
|
|
727
|
+
}
|
|
728
|
+
async function forwardStorageToBackend(path, method, request, options = {}) {
|
|
729
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
730
|
+
const backendBaseUrl = (options.backendBaseUrl ?? "").trim();
|
|
731
|
+
const walletSignature = normalizeWalletSignature(options.walletSignature);
|
|
732
|
+
if (!backendBaseUrl) {
|
|
733
|
+
throw new Error("MNEMOSPARK_BACKEND_API_BASE_URL is not configured");
|
|
734
|
+
}
|
|
735
|
+
if (!walletSignature) {
|
|
736
|
+
throw new Error(
|
|
737
|
+
"Wallet required for storage endpoints: wallet key must be present to sign requests."
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const targetUrl = `${normalizeBaseUrl(backendBaseUrl)}${path}`;
|
|
741
|
+
const response = await fetchImpl(targetUrl, {
|
|
742
|
+
method,
|
|
743
|
+
headers: {
|
|
744
|
+
"Content-Type": "application/json",
|
|
745
|
+
"X-Wallet-Signature": walletSignature
|
|
746
|
+
},
|
|
747
|
+
body: JSON.stringify(request)
|
|
748
|
+
});
|
|
749
|
+
const bodyBuffer = Buffer.from(await response.arrayBuffer());
|
|
750
|
+
return {
|
|
751
|
+
status: response.status,
|
|
752
|
+
bodyText: bodyBuffer.toString("utf-8"),
|
|
753
|
+
bodyBuffer,
|
|
754
|
+
contentType: response.headers.get("content-type") ?? "application/octet-stream",
|
|
755
|
+
contentDisposition: response.headers.get("content-disposition") ?? void 0,
|
|
756
|
+
paymentRequired: normalizePaymentRequired(response.headers),
|
|
757
|
+
paymentResponse: normalizePaymentResponse(response.headers)
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function parseStorageObjectRequest(payload) {
|
|
761
|
+
const record = asRecord(payload);
|
|
762
|
+
if (!record) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
const walletAddress = asNonEmptyString(record.wallet_address);
|
|
766
|
+
const objectKey = asNonEmptyString(record.object_key);
|
|
767
|
+
const location = asNonEmptyString(record.location) ?? void 0;
|
|
768
|
+
if (!walletAddress || !objectKey) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
wallet_address: walletAddress,
|
|
773
|
+
object_key: objectKey,
|
|
774
|
+
location
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function parseStorageLsResponse(payload) {
|
|
778
|
+
const record = asRecord(payload);
|
|
779
|
+
if (!record) {
|
|
780
|
+
throw new Error("Invalid ls response payload");
|
|
781
|
+
}
|
|
782
|
+
const key = asNonEmptyString(record.key) ?? asNonEmptyString(record.object_key);
|
|
783
|
+
const sizeBytes = asNumber(record.size_bytes);
|
|
784
|
+
const bucket = asNonEmptyString(record.bucket) ?? asNonEmptyString(record.bucket_name);
|
|
785
|
+
const objectId = asNonEmptyString(record.object_id) ?? void 0;
|
|
786
|
+
if (!key || sizeBytes === null || !bucket) {
|
|
787
|
+
throw new Error("ls response is missing required fields");
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
success: asBooleanOrDefault(record.success, true),
|
|
791
|
+
key,
|
|
792
|
+
size_bytes: sizeBytes,
|
|
793
|
+
bucket,
|
|
794
|
+
object_id: objectId
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function parseStorageDeleteResponse(payload) {
|
|
798
|
+
const record = asRecord(payload);
|
|
799
|
+
if (!record) {
|
|
800
|
+
throw new Error("Invalid delete response payload");
|
|
801
|
+
}
|
|
802
|
+
const key = asNonEmptyString(record.key) ?? asNonEmptyString(record.object_key);
|
|
803
|
+
const bucket = asNonEmptyString(record.bucket) ?? asNonEmptyString(record.bucket_name);
|
|
804
|
+
const bucketDeleted = asBooleanOrDefault(record.bucket_deleted, false);
|
|
805
|
+
if (!key || !bucket) {
|
|
806
|
+
throw new Error("delete response is missing required fields");
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
success: asBooleanOrDefault(record.success, true),
|
|
810
|
+
key,
|
|
811
|
+
bucket,
|
|
812
|
+
bucket_deleted: bucketDeleted
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
function parseStorageDownloadProxyResponse(payload) {
|
|
816
|
+
const record = asRecord(payload);
|
|
817
|
+
if (!record) {
|
|
818
|
+
throw new Error("Invalid download response payload");
|
|
819
|
+
}
|
|
820
|
+
const key = asNonEmptyString(record.key) ?? asNonEmptyString(record.object_key);
|
|
821
|
+
const filePath = asNonEmptyString(record.file_path);
|
|
822
|
+
if (!key || !filePath) {
|
|
823
|
+
throw new Error("download response is missing required fields");
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
success: asBooleanOrDefault(record.success, true),
|
|
827
|
+
key,
|
|
828
|
+
file_path: filePath
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
async function requestStorageLsViaProxy(request, options = {}) {
|
|
832
|
+
return requestJsonViaProxy(STORAGE_LS_PROXY_PATH, request, parseStorageLsResponse, options);
|
|
833
|
+
}
|
|
834
|
+
async function requestStorageDownloadViaProxy(request, options = {}) {
|
|
835
|
+
return requestJsonViaProxy(
|
|
836
|
+
STORAGE_DOWNLOAD_PROXY_PATH,
|
|
837
|
+
request,
|
|
838
|
+
parseStorageDownloadProxyResponse,
|
|
839
|
+
options
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
async function requestStorageDeleteViaProxy(request, options = {}) {
|
|
843
|
+
return requestJsonViaProxy(
|
|
844
|
+
STORAGE_DELETE_PROXY_PATH,
|
|
845
|
+
request,
|
|
846
|
+
parseStorageDeleteResponse,
|
|
847
|
+
options
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
async function forwardStorageLsToBackend(request, options = {}) {
|
|
851
|
+
return forwardStorageToBackend("/storage/ls", "POST", request, options);
|
|
852
|
+
}
|
|
853
|
+
async function forwardStorageDownloadToBackend(request, options = {}) {
|
|
854
|
+
return forwardStorageToBackend("/storage/download", "POST", request, options);
|
|
855
|
+
}
|
|
856
|
+
async function forwardStorageDeleteToBackend(request, options = {}) {
|
|
857
|
+
return forwardStorageToBackend("/storage/delete", "POST", request, options);
|
|
858
|
+
}
|
|
859
|
+
async function downloadStorageToDisk(request, backendResponse, options = {}) {
|
|
860
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
861
|
+
const outputDir = options.outputDir ?? process.cwd();
|
|
862
|
+
let objectKey = request.object_key;
|
|
863
|
+
let bytes = backendResponse.bodyBuffer;
|
|
864
|
+
const contentType = backendResponse.contentType.toLowerCase();
|
|
865
|
+
if (contentType.includes("application/json")) {
|
|
866
|
+
const payload = parseJsonText(
|
|
867
|
+
backendResponse.bodyText,
|
|
868
|
+
"Download response was JSON but not parseable"
|
|
869
|
+
);
|
|
870
|
+
const payloadObjectKey = asNonEmptyString(payload.object_key) ?? asNonEmptyString(payload.key) ?? asNonEmptyString(payload.object_id);
|
|
871
|
+
const downloadUrl = asNonEmptyString(payload.download_url);
|
|
872
|
+
const inlineContent = asNonEmptyString(payload.content) ?? asNonEmptyString(payload.body_base64) ?? asNonEmptyString(payload.data);
|
|
873
|
+
if (payloadObjectKey) {
|
|
874
|
+
objectKey = payloadObjectKey;
|
|
875
|
+
}
|
|
876
|
+
if (downloadUrl) {
|
|
877
|
+
const fileResponse = await fetchImpl(downloadUrl, { method: "GET" });
|
|
878
|
+
if (!fileResponse.ok) {
|
|
879
|
+
throw new Error(`Presigned download failed with status ${fileResponse.status}`);
|
|
880
|
+
}
|
|
881
|
+
bytes = Buffer.from(await fileResponse.arrayBuffer());
|
|
882
|
+
} else if (inlineContent) {
|
|
883
|
+
bytes = Buffer.from(inlineContent, "base64");
|
|
884
|
+
} else {
|
|
885
|
+
throw new Error("Download response did not include download_url or inline content");
|
|
886
|
+
}
|
|
887
|
+
} else {
|
|
888
|
+
const filenameFromHeader = parseFilenameFromContentDisposition(
|
|
889
|
+
backendResponse.contentDisposition
|
|
890
|
+
);
|
|
891
|
+
if (filenameFromHeader) {
|
|
892
|
+
objectKey = filenameFromHeader;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const filePath = resolveDownloadPath(outputDir, objectKey);
|
|
896
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
897
|
+
await writeFile(filePath, bytes);
|
|
898
|
+
return {
|
|
899
|
+
key: objectKey,
|
|
900
|
+
filePath,
|
|
901
|
+
bytesWritten: bytes.length
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/proxy.ts
|
|
906
|
+
var HEALTH_CHECK_TIMEOUT_MS = 2e3;
|
|
907
|
+
var PORT_RETRY_ATTEMPTS = 5;
|
|
908
|
+
var PORT_RETRY_DELAY_MS = 1e3;
|
|
909
|
+
function matchesProxyPath(url, path) {
|
|
910
|
+
return url === path || url?.startsWith(`${path}?`) === true;
|
|
911
|
+
}
|
|
912
|
+
function readHeaderValue(value) {
|
|
913
|
+
if (typeof value === "string") {
|
|
914
|
+
const trimmed = value.trim();
|
|
915
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
916
|
+
}
|
|
917
|
+
if (Array.isArray(value)) {
|
|
918
|
+
for (const candidate of value) {
|
|
919
|
+
const trimmed = candidate.trim();
|
|
920
|
+
if (trimmed.length > 0) {
|
|
921
|
+
return trimmed;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return void 0;
|
|
926
|
+
}
|
|
927
|
+
async function readProxyJsonBody(req) {
|
|
928
|
+
const bodyChunks = [];
|
|
929
|
+
for await (const chunk of req) {
|
|
930
|
+
bodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
931
|
+
}
|
|
932
|
+
const bodyText = Buffer.concat(bodyChunks).toString("utf-8").trim();
|
|
933
|
+
if (bodyText.length === 0) {
|
|
934
|
+
return {};
|
|
935
|
+
}
|
|
936
|
+
return JSON.parse(bodyText);
|
|
937
|
+
}
|
|
938
|
+
function sendJson(res, status, body) {
|
|
939
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
940
|
+
res.end(JSON.stringify(body));
|
|
941
|
+
}
|
|
942
|
+
function createBackendForwardHeaders(response) {
|
|
943
|
+
const responseHeaders = {
|
|
944
|
+
"Content-Type": response.contentType
|
|
945
|
+
};
|
|
946
|
+
if (response.paymentRequired) {
|
|
947
|
+
responseHeaders["PAYMENT-REQUIRED"] = response.paymentRequired;
|
|
948
|
+
responseHeaders["x-payment-required"] = response.paymentRequired;
|
|
949
|
+
}
|
|
950
|
+
if (response.paymentResponse) {
|
|
951
|
+
responseHeaders["PAYMENT-RESPONSE"] = response.paymentResponse;
|
|
952
|
+
responseHeaders["x-payment-response"] = response.paymentResponse;
|
|
953
|
+
}
|
|
954
|
+
return responseHeaders;
|
|
955
|
+
}
|
|
956
|
+
function isLikelyWalletProofFailure(bodyText) {
|
|
957
|
+
return /(wallet|signature|proof|nonce|timestamp|expired|authoriz)/i.test(bodyText);
|
|
958
|
+
}
|
|
959
|
+
function normalizeBackendAuthFailure(status, bodyText) {
|
|
960
|
+
if (status !== 401 && status !== 403) {
|
|
961
|
+
return void 0;
|
|
962
|
+
}
|
|
963
|
+
const message = isLikelyWalletProofFailure(bodyText) ? "wallet proof invalid" : "unauthorized";
|
|
964
|
+
return {
|
|
965
|
+
status,
|
|
966
|
+
contentType: "application/json",
|
|
967
|
+
bodyText: createAuthErrorBody(message)
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function createAuthErrorBody(message) {
|
|
971
|
+
return JSON.stringify({
|
|
972
|
+
error: message.replace(/\s+/g, "_"),
|
|
973
|
+
message
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
function createWalletRequiredBody() {
|
|
977
|
+
return JSON.stringify({
|
|
978
|
+
error: "wallet_required",
|
|
979
|
+
message: "wallet required for storage endpoints"
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
function getProxyPort() {
|
|
983
|
+
return PROXY_PORT;
|
|
984
|
+
}
|
|
985
|
+
async function checkExistingProxy(port) {
|
|
986
|
+
const controller = new AbortController();
|
|
987
|
+
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
|
|
988
|
+
try {
|
|
989
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
990
|
+
signal: controller.signal
|
|
991
|
+
});
|
|
992
|
+
clearTimeout(timeoutId);
|
|
993
|
+
if (response.ok) {
|
|
994
|
+
const data = await response.json();
|
|
995
|
+
if (data.status === "ok" && data.wallet) {
|
|
996
|
+
return data.wallet;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return void 0;
|
|
1000
|
+
} catch {
|
|
1001
|
+
clearTimeout(timeoutId);
|
|
1002
|
+
return void 0;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
async function startProxy(options) {
|
|
1006
|
+
const listenPort = options.port ?? getProxyPort();
|
|
1007
|
+
const existingWallet = await checkExistingProxy(listenPort);
|
|
1008
|
+
if (existingWallet) {
|
|
1009
|
+
const account2 = privateKeyToAccount2(options.walletKey);
|
|
1010
|
+
const balanceMonitor2 = new BalanceMonitor(account2.address);
|
|
1011
|
+
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
1012
|
+
if (existingWallet !== account2.address) {
|
|
1013
|
+
console.warn(
|
|
1014
|
+
`[mnemospark] Existing proxy on port ${listenPort} uses wallet ${existingWallet}, but current config uses ${account2.address}. Reusing existing proxy.`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
options.onReady?.(listenPort);
|
|
1018
|
+
return {
|
|
1019
|
+
port: listenPort,
|
|
1020
|
+
baseUrl: baseUrl2,
|
|
1021
|
+
walletAddress: existingWallet,
|
|
1022
|
+
balanceMonitor: balanceMonitor2,
|
|
1023
|
+
close: async () => {
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
const walletPrivateKey = options.walletKey.trim();
|
|
1028
|
+
const account = privateKeyToAccount2(walletPrivateKey);
|
|
1029
|
+
const balanceMonitor = new BalanceMonitor(account.address);
|
|
1030
|
+
const proxyWalletAddressLower = account.address.toLowerCase();
|
|
1031
|
+
const connections = /* @__PURE__ */ new Set();
|
|
1032
|
+
const createBackendWalletSignature = async (method, path, walletAddress) => {
|
|
1033
|
+
if (walletAddress.toLowerCase() !== proxyWalletAddressLower) {
|
|
1034
|
+
return void 0;
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
return await createWalletSignatureHeaderValue(method, path, walletAddress, walletPrivateKey);
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
console.warn(
|
|
1040
|
+
`[mnemospark] Failed to create wallet proof for ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
1041
|
+
);
|
|
1042
|
+
return void 0;
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
const server = createServer(async (req, res) => {
|
|
1046
|
+
req.on("error", (err) => {
|
|
1047
|
+
console.error(`[mnemospark] Request stream error: ${err.message}`);
|
|
1048
|
+
});
|
|
1049
|
+
res.on("error", (err) => {
|
|
1050
|
+
console.error(`[mnemospark] Response stream error: ${err.message}`);
|
|
1051
|
+
});
|
|
1052
|
+
if (req.method === "POST" && matchesProxyPath(req.url, PRICE_STORAGE_PROXY_PATH)) {
|
|
1053
|
+
try {
|
|
1054
|
+
let payload;
|
|
1055
|
+
try {
|
|
1056
|
+
payload = await readProxyJsonBody(req);
|
|
1057
|
+
} catch {
|
|
1058
|
+
sendJson(res, 400, {
|
|
1059
|
+
error: "Bad request",
|
|
1060
|
+
message: "Invalid JSON body for /cloud price-storage"
|
|
1061
|
+
});
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const requestPayload = parsePriceStorageQuoteRequest(payload);
|
|
1065
|
+
if (!requestPayload) {
|
|
1066
|
+
sendJson(res, 400, {
|
|
1067
|
+
error: "Bad request",
|
|
1068
|
+
message: "Missing required fields: wallet_address, object_id, object_id_hash, gb, provider, region"
|
|
1069
|
+
});
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const walletSignature = await createBackendWalletSignature(
|
|
1073
|
+
"POST",
|
|
1074
|
+
"/price-storage",
|
|
1075
|
+
requestPayload.wallet_address
|
|
1076
|
+
);
|
|
1077
|
+
const backendResponse = await forwardPriceStorageToBackend(requestPayload, {
|
|
1078
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
1079
|
+
walletSignature
|
|
1080
|
+
});
|
|
1081
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
1082
|
+
backendResponse.status,
|
|
1083
|
+
backendResponse.bodyText
|
|
1084
|
+
);
|
|
1085
|
+
if (authFailure) {
|
|
1086
|
+
const responseHeaders2 = createBackendForwardHeaders({
|
|
1087
|
+
contentType: authFailure.contentType,
|
|
1088
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
1089
|
+
paymentResponse: backendResponse.paymentResponse
|
|
1090
|
+
});
|
|
1091
|
+
res.writeHead(authFailure.status, responseHeaders2);
|
|
1092
|
+
res.end(authFailure.bodyText);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
1096
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
1097
|
+
res.end(backendResponse.bodyText);
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
sendJson(res, 502, {
|
|
1100
|
+
error: "proxy_error",
|
|
1101
|
+
message: `Failed to forward /cloud price-storage: ${err instanceof Error ? err.message : String(err)}`
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (req.method === "POST" && matchesProxyPath(req.url, UPLOAD_PROXY_PATH)) {
|
|
1107
|
+
try {
|
|
1108
|
+
let payload;
|
|
1109
|
+
try {
|
|
1110
|
+
payload = await readProxyJsonBody(req);
|
|
1111
|
+
} catch {
|
|
1112
|
+
sendJson(res, 400, {
|
|
1113
|
+
error: "Bad request",
|
|
1114
|
+
message: "Invalid JSON body for /cloud upload"
|
|
1115
|
+
});
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const requestPayload = parseStorageUploadRequest(payload);
|
|
1119
|
+
if (!requestPayload) {
|
|
1120
|
+
sendJson(res, 400, {
|
|
1121
|
+
error: "Bad request",
|
|
1122
|
+
message: "Missing required fields: quote_id, wallet_address, object_id, object_id_hash, quoted_storage_price, payload"
|
|
1123
|
+
});
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (requestPayload.wallet_address.toLowerCase() !== proxyWalletAddressLower) {
|
|
1127
|
+
sendJson(res, 403, {
|
|
1128
|
+
error: "wallet_proof_invalid",
|
|
1129
|
+
message: "wallet proof invalid"
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const walletSignature = await createBackendWalletSignature(
|
|
1134
|
+
"POST",
|
|
1135
|
+
"/storage/upload",
|
|
1136
|
+
requestPayload.wallet_address
|
|
1137
|
+
);
|
|
1138
|
+
if (!walletSignature) {
|
|
1139
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1140
|
+
res.end(createWalletRequiredBody());
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const requiredMicros = BigInt(
|
|
1144
|
+
Math.max(1, Math.ceil(requestPayload.quoted_storage_price * 1e6))
|
|
1145
|
+
);
|
|
1146
|
+
const uploadBalanceMonitor = requestPayload.wallet_address.toLowerCase() === account.address.toLowerCase() ? balanceMonitor : new BalanceMonitor(requestPayload.wallet_address);
|
|
1147
|
+
const sufficiency = await uploadBalanceMonitor.checkSufficient(requiredMicros);
|
|
1148
|
+
const requiredUSD = uploadBalanceMonitor.formatUSDC(requiredMicros);
|
|
1149
|
+
if (!sufficiency.sufficient) {
|
|
1150
|
+
options.onInsufficientFunds?.({
|
|
1151
|
+
balanceUSD: sufficiency.info.balanceUSD,
|
|
1152
|
+
requiredUSD,
|
|
1153
|
+
walletAddress: requestPayload.wallet_address
|
|
1154
|
+
});
|
|
1155
|
+
sendJson(res, 400, {
|
|
1156
|
+
error: "insufficient_balance",
|
|
1157
|
+
message: `Insufficient USDC balance. Current: ${sufficiency.info.balanceUSD}, Required: ${requiredUSD}`,
|
|
1158
|
+
wallet: requestPayload.wallet_address,
|
|
1159
|
+
help: `Fund wallet ${requestPayload.wallet_address} on Base before running /cloud upload`
|
|
1160
|
+
});
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (sufficiency.info.isLow) {
|
|
1164
|
+
options.onLowBalance?.({
|
|
1165
|
+
balanceUSD: sufficiency.info.balanceUSD,
|
|
1166
|
+
walletAddress: requestPayload.wallet_address
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
const backendResponse = await forwardStorageUploadToBackend(requestPayload, {
|
|
1170
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
1171
|
+
walletSignature,
|
|
1172
|
+
paymentSignature: readHeaderValue(req.headers["payment-signature"]),
|
|
1173
|
+
legacyPayment: readHeaderValue(req.headers["x-payment"]),
|
|
1174
|
+
idempotencyKey: readHeaderValue(req.headers["idempotency-key"])
|
|
1175
|
+
});
|
|
1176
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
1177
|
+
backendResponse.status,
|
|
1178
|
+
backendResponse.bodyText
|
|
1179
|
+
);
|
|
1180
|
+
if (authFailure) {
|
|
1181
|
+
const responseHeaders2 = createBackendForwardHeaders({
|
|
1182
|
+
contentType: authFailure.contentType,
|
|
1183
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
1184
|
+
paymentResponse: backendResponse.paymentResponse
|
|
1185
|
+
});
|
|
1186
|
+
res.writeHead(authFailure.status, responseHeaders2);
|
|
1187
|
+
res.end(authFailure.bodyText);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
1191
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
1192
|
+
res.end(backendResponse.bodyText);
|
|
1193
|
+
} catch (err) {
|
|
1194
|
+
sendJson(res, 502, {
|
|
1195
|
+
error: "proxy_error",
|
|
1196
|
+
message: `Failed to forward /cloud upload: ${err instanceof Error ? err.message : String(err)}`
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (req.method === "POST" && matchesProxyPath(req.url, STORAGE_LS_PROXY_PATH)) {
|
|
1202
|
+
try {
|
|
1203
|
+
let payload;
|
|
1204
|
+
try {
|
|
1205
|
+
payload = await readProxyJsonBody(req);
|
|
1206
|
+
} catch {
|
|
1207
|
+
sendJson(res, 400, {
|
|
1208
|
+
error: "Bad request",
|
|
1209
|
+
message: "Invalid JSON body for /cloud ls"
|
|
1210
|
+
});
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const requestPayload = parseStorageObjectRequest(payload);
|
|
1214
|
+
if (!requestPayload) {
|
|
1215
|
+
sendJson(res, 400, {
|
|
1216
|
+
error: "Bad request",
|
|
1217
|
+
message: "Missing required fields: wallet_address, object_key"
|
|
1218
|
+
});
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (requestPayload.wallet_address.toLowerCase() !== proxyWalletAddressLower) {
|
|
1222
|
+
sendJson(res, 403, {
|
|
1223
|
+
error: "wallet_proof_invalid",
|
|
1224
|
+
message: "wallet proof invalid"
|
|
1225
|
+
});
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
const walletSignature = await createBackendWalletSignature(
|
|
1229
|
+
"POST",
|
|
1230
|
+
"/storage/ls",
|
|
1231
|
+
requestPayload.wallet_address
|
|
1232
|
+
);
|
|
1233
|
+
if (!walletSignature) {
|
|
1234
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1235
|
+
res.end(createWalletRequiredBody());
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
const backendResponse = await forwardStorageLsToBackend(requestPayload, {
|
|
1239
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
1240
|
+
walletSignature
|
|
1241
|
+
});
|
|
1242
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
1243
|
+
backendResponse.status,
|
|
1244
|
+
backendResponse.bodyText
|
|
1245
|
+
);
|
|
1246
|
+
if (authFailure) {
|
|
1247
|
+
const responseHeaders2 = createBackendForwardHeaders({
|
|
1248
|
+
contentType: authFailure.contentType,
|
|
1249
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
1250
|
+
paymentResponse: backendResponse.paymentResponse
|
|
1251
|
+
});
|
|
1252
|
+
res.writeHead(authFailure.status, responseHeaders2);
|
|
1253
|
+
res.end(authFailure.bodyText);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
1257
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
1258
|
+
res.end(backendResponse.bodyText);
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
sendJson(res, 502, {
|
|
1261
|
+
error: "proxy_error",
|
|
1262
|
+
message: `Failed to forward /cloud ls: ${err instanceof Error ? err.message : String(err)}`
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (req.method === "POST" && matchesProxyPath(req.url, STORAGE_DOWNLOAD_PROXY_PATH)) {
|
|
1268
|
+
try {
|
|
1269
|
+
let payload;
|
|
1270
|
+
try {
|
|
1271
|
+
payload = await readProxyJsonBody(req);
|
|
1272
|
+
} catch {
|
|
1273
|
+
sendJson(res, 400, {
|
|
1274
|
+
error: "Bad request",
|
|
1275
|
+
message: "Invalid JSON body for /cloud download"
|
|
1276
|
+
});
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const requestPayload = parseStorageObjectRequest(payload);
|
|
1280
|
+
if (!requestPayload) {
|
|
1281
|
+
sendJson(res, 400, {
|
|
1282
|
+
error: "Bad request",
|
|
1283
|
+
message: "Missing required fields: wallet_address, object_key"
|
|
1284
|
+
});
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (requestPayload.wallet_address.toLowerCase() !== proxyWalletAddressLower) {
|
|
1288
|
+
sendJson(res, 403, {
|
|
1289
|
+
error: "wallet_proof_invalid",
|
|
1290
|
+
message: "wallet proof invalid"
|
|
1291
|
+
});
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const walletSignature = await createBackendWalletSignature(
|
|
1295
|
+
"POST",
|
|
1296
|
+
"/storage/download",
|
|
1297
|
+
requestPayload.wallet_address
|
|
1298
|
+
);
|
|
1299
|
+
if (!walletSignature) {
|
|
1300
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1301
|
+
res.end(createWalletRequiredBody());
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const backendResponse = await forwardStorageDownloadToBackend(requestPayload, {
|
|
1305
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
1306
|
+
walletSignature
|
|
1307
|
+
});
|
|
1308
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
1309
|
+
backendResponse.status,
|
|
1310
|
+
backendResponse.bodyText
|
|
1311
|
+
);
|
|
1312
|
+
if (authFailure) {
|
|
1313
|
+
const responseHeaders = createBackendForwardHeaders({
|
|
1314
|
+
contentType: authFailure.contentType,
|
|
1315
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
1316
|
+
paymentResponse: backendResponse.paymentResponse
|
|
1317
|
+
});
|
|
1318
|
+
res.writeHead(authFailure.status, responseHeaders);
|
|
1319
|
+
res.end(authFailure.bodyText);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
if (backendResponse.status < 200 || backendResponse.status >= 300) {
|
|
1323
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
1324
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
1325
|
+
res.end(backendResponse.bodyText);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
const downloadResult = await downloadStorageToDisk(requestPayload, backendResponse);
|
|
1329
|
+
sendJson(res, 200, {
|
|
1330
|
+
success: true,
|
|
1331
|
+
key: downloadResult.key,
|
|
1332
|
+
file_path: downloadResult.filePath,
|
|
1333
|
+
bytes_written: downloadResult.bytesWritten
|
|
1334
|
+
});
|
|
1335
|
+
} catch (err) {
|
|
1336
|
+
sendJson(res, 502, {
|
|
1337
|
+
error: "proxy_error",
|
|
1338
|
+
message: `Failed to forward /cloud download: ${err instanceof Error ? err.message : String(err)}`
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (req.method === "POST" && matchesProxyPath(req.url, STORAGE_DELETE_PROXY_PATH)) {
|
|
1344
|
+
try {
|
|
1345
|
+
let payload;
|
|
1346
|
+
try {
|
|
1347
|
+
payload = await readProxyJsonBody(req);
|
|
1348
|
+
} catch {
|
|
1349
|
+
sendJson(res, 400, {
|
|
1350
|
+
error: "Bad request",
|
|
1351
|
+
message: "Invalid JSON body for /cloud delete"
|
|
1352
|
+
});
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
const requestPayload = parseStorageObjectRequest(payload);
|
|
1356
|
+
if (!requestPayload) {
|
|
1357
|
+
sendJson(res, 400, {
|
|
1358
|
+
error: "Bad request",
|
|
1359
|
+
message: "Missing required fields: wallet_address, object_key"
|
|
1360
|
+
});
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (requestPayload.wallet_address.toLowerCase() !== proxyWalletAddressLower) {
|
|
1364
|
+
sendJson(res, 403, {
|
|
1365
|
+
error: "wallet_proof_invalid",
|
|
1366
|
+
message: "wallet proof invalid"
|
|
1367
|
+
});
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const walletSignature = await createBackendWalletSignature(
|
|
1371
|
+
"POST",
|
|
1372
|
+
"/storage/delete",
|
|
1373
|
+
requestPayload.wallet_address
|
|
1374
|
+
);
|
|
1375
|
+
if (!walletSignature) {
|
|
1376
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1377
|
+
res.end(createWalletRequiredBody());
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
const backendResponse = await forwardStorageDeleteToBackend(requestPayload, {
|
|
1381
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
1382
|
+
walletSignature
|
|
1383
|
+
});
|
|
1384
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
1385
|
+
backendResponse.status,
|
|
1386
|
+
backendResponse.bodyText
|
|
1387
|
+
);
|
|
1388
|
+
if (authFailure) {
|
|
1389
|
+
const responseHeaders2 = createBackendForwardHeaders({
|
|
1390
|
+
contentType: authFailure.contentType,
|
|
1391
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
1392
|
+
paymentResponse: backendResponse.paymentResponse
|
|
1393
|
+
});
|
|
1394
|
+
res.writeHead(authFailure.status, responseHeaders2);
|
|
1395
|
+
res.end(authFailure.bodyText);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
1399
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
1400
|
+
res.end(backendResponse.bodyText);
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
sendJson(res, 502, {
|
|
1403
|
+
error: "proxy_error",
|
|
1404
|
+
message: `Failed to forward /cloud delete: ${err instanceof Error ? err.message : String(err)}`
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
if (req.url === "/health" || req.url?.startsWith("/health?")) {
|
|
1410
|
+
const url = new URL(req.url, "http://localhost");
|
|
1411
|
+
const full = url.searchParams.get("full") === "true";
|
|
1412
|
+
const response = {
|
|
1413
|
+
status: "ok",
|
|
1414
|
+
wallet: account.address
|
|
1415
|
+
};
|
|
1416
|
+
if (full) {
|
|
1417
|
+
try {
|
|
1418
|
+
const balanceInfo = await balanceMonitor.checkBalance();
|
|
1419
|
+
response.balance = balanceInfo.balanceUSD;
|
|
1420
|
+
response.isLow = balanceInfo.isLow;
|
|
1421
|
+
response.isEmpty = balanceInfo.isEmpty;
|
|
1422
|
+
} catch {
|
|
1423
|
+
response.balanceError = "Could not fetch balance";
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
sendJson(res, 200, response);
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
sendJson(res, 404, {
|
|
1430
|
+
error: "Not found",
|
|
1431
|
+
message: "Supported paths: /health and /mnemospark/* storage endpoints"
|
|
1432
|
+
});
|
|
1433
|
+
});
|
|
1434
|
+
const tryListen = (attempt) => {
|
|
1435
|
+
return new Promise((resolveAttempt, rejectAttempt) => {
|
|
1436
|
+
const onError = async (err) => {
|
|
1437
|
+
server.removeListener("error", onError);
|
|
1438
|
+
if (err.code === "EADDRINUSE") {
|
|
1439
|
+
const existingWallet2 = await checkExistingProxy(listenPort);
|
|
1440
|
+
if (existingWallet2) {
|
|
1441
|
+
console.log(`[mnemospark] Existing proxy detected on port ${listenPort}, reusing`);
|
|
1442
|
+
rejectAttempt({ code: "REUSE_EXISTING", wallet: existingWallet2 });
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
if (attempt < PORT_RETRY_ATTEMPTS) {
|
|
1446
|
+
console.log(
|
|
1447
|
+
`[mnemospark] Port ${listenPort} in TIME_WAIT, retrying in ${PORT_RETRY_DELAY_MS}ms (attempt ${attempt}/${PORT_RETRY_ATTEMPTS})`
|
|
1448
|
+
);
|
|
1449
|
+
rejectAttempt({ code: "RETRY", attempt });
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
console.error(
|
|
1453
|
+
`[mnemospark] Port ${listenPort} still in use after ${PORT_RETRY_ATTEMPTS} attempts`
|
|
1454
|
+
);
|
|
1455
|
+
rejectAttempt(err);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
rejectAttempt(err);
|
|
1459
|
+
};
|
|
1460
|
+
server.once("error", onError);
|
|
1461
|
+
server.listen(listenPort, "127.0.0.1", () => {
|
|
1462
|
+
server.removeListener("error", onError);
|
|
1463
|
+
resolveAttempt();
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
};
|
|
1467
|
+
let lastError;
|
|
1468
|
+
for (let attempt = 1; attempt <= PORT_RETRY_ATTEMPTS; attempt++) {
|
|
1469
|
+
try {
|
|
1470
|
+
await tryListen(attempt);
|
|
1471
|
+
break;
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
const error = err;
|
|
1474
|
+
if (error.code === "REUSE_EXISTING" && error.wallet) {
|
|
1475
|
+
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
1476
|
+
options.onReady?.(listenPort);
|
|
1477
|
+
return {
|
|
1478
|
+
port: listenPort,
|
|
1479
|
+
baseUrl: baseUrl2,
|
|
1480
|
+
walletAddress: error.wallet,
|
|
1481
|
+
balanceMonitor,
|
|
1482
|
+
close: async () => {
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
if (error.code === "RETRY") {
|
|
1487
|
+
await new Promise((r) => setTimeout(r, PORT_RETRY_DELAY_MS));
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
lastError = err;
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (lastError) {
|
|
1495
|
+
throw lastError;
|
|
1496
|
+
}
|
|
1497
|
+
const addr = server.address();
|
|
1498
|
+
const port = addr.port;
|
|
1499
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
1500
|
+
options.onReady?.(port);
|
|
1501
|
+
server.on("error", (err) => {
|
|
1502
|
+
console.error(`[mnemospark] Server runtime error: ${err.message}`);
|
|
1503
|
+
options.onError?.(err);
|
|
1504
|
+
});
|
|
1505
|
+
server.on("clientError", (err, socket) => {
|
|
1506
|
+
console.error(`[mnemospark] Client error: ${err.message}`);
|
|
1507
|
+
if (socket.writable && !socket.destroyed) {
|
|
1508
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
server.on("connection", (socket) => {
|
|
1512
|
+
connections.add(socket);
|
|
1513
|
+
socket.setTimeout(3e5);
|
|
1514
|
+
socket.on("timeout", () => {
|
|
1515
|
+
console.error(`[mnemospark] Socket timeout, destroying connection`);
|
|
1516
|
+
socket.destroy();
|
|
1517
|
+
});
|
|
1518
|
+
socket.on("error", (err) => {
|
|
1519
|
+
console.error(`[mnemospark] Socket error: ${err.message}`);
|
|
1520
|
+
});
|
|
1521
|
+
socket.on("close", () => {
|
|
1522
|
+
connections.delete(socket);
|
|
1523
|
+
});
|
|
1524
|
+
});
|
|
1525
|
+
return {
|
|
1526
|
+
port,
|
|
1527
|
+
baseUrl,
|
|
1528
|
+
walletAddress: account.address,
|
|
1529
|
+
balanceMonitor,
|
|
1530
|
+
close: () => new Promise((res, rej) => {
|
|
1531
|
+
const timeout = setTimeout(() => {
|
|
1532
|
+
rej(new Error("[mnemospark] Close timeout after 4s"));
|
|
1533
|
+
}, 4e3);
|
|
1534
|
+
for (const socket of connections) {
|
|
1535
|
+
socket.destroy();
|
|
1536
|
+
}
|
|
1537
|
+
connections.clear();
|
|
1538
|
+
server.close((err) => {
|
|
1539
|
+
clearTimeout(timeout);
|
|
1540
|
+
if (err) {
|
|
1541
|
+
rej(err);
|
|
1542
|
+
} else {
|
|
1543
|
+
res();
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
})
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/auth.ts
|
|
1551
|
+
import { writeFile as writeFile2, readFile, mkdir as mkdir2 } from "fs/promises";
|
|
1552
|
+
import { join as join2 } from "path";
|
|
1553
|
+
import { homedir } from "os";
|
|
1554
|
+
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
1555
|
+
var LEGACY_WALLET_DIR = join2(homedir(), ".openclaw", "blockrun");
|
|
1556
|
+
var LEGACY_WALLET_FILE = join2(LEGACY_WALLET_DIR, "wallet.key");
|
|
1557
|
+
var WALLET_DIR = join2(homedir(), ".openclaw", "mnemospark", "wallet");
|
|
1558
|
+
var WALLET_FILE = join2(WALLET_DIR, "wallet.key");
|
|
1559
|
+
async function loadSavedWallet() {
|
|
1560
|
+
for (const path of [WALLET_FILE, LEGACY_WALLET_FILE]) {
|
|
1561
|
+
try {
|
|
1562
|
+
const key = (await readFile(path, "utf-8")).trim();
|
|
1563
|
+
if (key.startsWith("0x") && key.length === 66) {
|
|
1564
|
+
console.log(`[mnemospark] \u2713 Loaded existing wallet from ${path}`);
|
|
1565
|
+
return key;
|
|
1566
|
+
}
|
|
1567
|
+
console.warn(`[mnemospark] \u26A0 Wallet file exists but is invalid (wrong format): ${path}`);
|
|
1568
|
+
} catch (err) {
|
|
1569
|
+
if (err.code !== "ENOENT") {
|
|
1570
|
+
console.error(
|
|
1571
|
+
`[mnemospark] \u2717 Failed to read wallet file ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return void 0;
|
|
1577
|
+
}
|
|
1578
|
+
async function generateAndSaveWallet() {
|
|
1579
|
+
const key = generatePrivateKey();
|
|
1580
|
+
const account = privateKeyToAccount3(key);
|
|
1581
|
+
await mkdir2(WALLET_DIR, { recursive: true });
|
|
1582
|
+
await writeFile2(WALLET_FILE, key + "\n", { mode: 384 });
|
|
1583
|
+
try {
|
|
1584
|
+
const verification = (await readFile(WALLET_FILE, "utf-8")).trim();
|
|
1585
|
+
if (verification !== key) {
|
|
1586
|
+
throw new Error("Wallet file verification failed - content mismatch");
|
|
1587
|
+
}
|
|
1588
|
+
console.log(`[mnemospark] \u2713 Wallet saved and verified at ${WALLET_FILE}`);
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
throw new Error(
|
|
1591
|
+
`Failed to verify wallet file after creation: ${err instanceof Error ? err.message : String(err)}`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
return { key, address: account.address };
|
|
1595
|
+
}
|
|
1596
|
+
async function resolveOrGenerateWalletKey() {
|
|
1597
|
+
const saved = await loadSavedWallet();
|
|
1598
|
+
if (saved) {
|
|
1599
|
+
const account = privateKeyToAccount3(saved);
|
|
1600
|
+
return { key: saved, address: account.address, source: "saved" };
|
|
1601
|
+
}
|
|
1602
|
+
const envKey = process.env.BLOCKRUN_WALLET_KEY;
|
|
1603
|
+
if (typeof envKey === "string" && envKey.startsWith("0x") && envKey.length === 66) {
|
|
1604
|
+
const account = privateKeyToAccount3(envKey);
|
|
1605
|
+
return { key: envKey, address: account.address, source: "env" };
|
|
1606
|
+
}
|
|
1607
|
+
const { key, address } = await generateAndSaveWallet();
|
|
1608
|
+
return { key, address, source: "generated" };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/index.ts
|
|
1612
|
+
import { existsSync, readFileSync } from "fs";
|
|
1613
|
+
|
|
1614
|
+
// src/version.ts
|
|
1615
|
+
import { createRequire } from "module";
|
|
1616
|
+
import { fileURLToPath } from "url";
|
|
1617
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1618
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1619
|
+
var __dirname = dirname2(__filename);
|
|
1620
|
+
var require2 = createRequire(import.meta.url);
|
|
1621
|
+
var pkg = require2(join3(__dirname, "..", "package.json"));
|
|
1622
|
+
var VERSION = pkg.version;
|
|
1623
|
+
var USER_AGENT = `mnemospark/${VERSION}`;
|
|
1624
|
+
|
|
1625
|
+
// src/index.ts
|
|
1626
|
+
import { privateKeyToAccount as privateKeyToAccount6 } from "viem/accounts";
|
|
1627
|
+
|
|
1628
|
+
// src/cloud-command.ts
|
|
1629
|
+
import { spawn } from "child_process";
|
|
1630
|
+
import {
|
|
1631
|
+
createCipheriv,
|
|
1632
|
+
createHash,
|
|
1633
|
+
randomBytes as randomBytesNode,
|
|
1634
|
+
randomUUID
|
|
1635
|
+
} from "crypto";
|
|
1636
|
+
import { createReadStream, statfsSync } from "fs";
|
|
1637
|
+
import { appendFile, lstat, mkdir as mkdir3, readFile as readFile2, readdir, rm, stat, writeFile as writeFile3 } from "fs/promises";
|
|
1638
|
+
import { homedir as homedir2 } from "os";
|
|
1639
|
+
import { basename, dirname as dirname3, join as join4, resolve as resolve2 } from "path";
|
|
1640
|
+
import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
|
|
1641
|
+
|
|
1642
|
+
// src/x402.ts
|
|
1643
|
+
import { signTypedData as signTypedData2, privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
|
|
1644
|
+
|
|
1645
|
+
// src/payment-cache.ts
|
|
1646
|
+
var DEFAULT_TTL_MS = 36e5;
|
|
1647
|
+
var PaymentCache = class {
|
|
1648
|
+
cache = /* @__PURE__ */ new Map();
|
|
1649
|
+
ttlMs;
|
|
1650
|
+
constructor(ttlMs = DEFAULT_TTL_MS) {
|
|
1651
|
+
this.ttlMs = ttlMs;
|
|
1652
|
+
}
|
|
1653
|
+
/** Get cached payment params for an endpoint path. */
|
|
1654
|
+
get(endpointPath) {
|
|
1655
|
+
const entry = this.cache.get(endpointPath);
|
|
1656
|
+
if (!entry) return void 0;
|
|
1657
|
+
if (Date.now() - entry.cachedAt > this.ttlMs) {
|
|
1658
|
+
this.cache.delete(endpointPath);
|
|
1659
|
+
return void 0;
|
|
1660
|
+
}
|
|
1661
|
+
return entry;
|
|
1662
|
+
}
|
|
1663
|
+
/** Cache payment params from a 402 response. */
|
|
1664
|
+
set(endpointPath, params) {
|
|
1665
|
+
this.cache.set(endpointPath, { ...params, cachedAt: Date.now() });
|
|
1666
|
+
}
|
|
1667
|
+
/** Invalidate cache for an endpoint (e.g., if payTo changed). */
|
|
1668
|
+
invalidate(endpointPath) {
|
|
1669
|
+
this.cache.delete(endpointPath);
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
// src/x402.ts
|
|
1674
|
+
var BASE_CHAIN_ID = 8453;
|
|
1675
|
+
var BASE_SEPOLIA_CHAIN_ID2 = 84532;
|
|
1676
|
+
var DEFAULT_TOKEN_NAME = "USD Coin";
|
|
1677
|
+
var DEFAULT_TOKEN_VERSION = "2";
|
|
1678
|
+
var DEFAULT_NETWORK = "eip155:8453";
|
|
1679
|
+
var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
|
|
1680
|
+
var TRANSFER_TYPES = {
|
|
1681
|
+
TransferWithAuthorization: [
|
|
1682
|
+
{ name: "from", type: "address" },
|
|
1683
|
+
{ name: "to", type: "address" },
|
|
1684
|
+
{ name: "value", type: "uint256" },
|
|
1685
|
+
{ name: "validAfter", type: "uint256" },
|
|
1686
|
+
{ name: "validBefore", type: "uint256" },
|
|
1687
|
+
{ name: "nonce", type: "bytes32" }
|
|
1688
|
+
]
|
|
1689
|
+
};
|
|
1690
|
+
function decodeBase64Json(value) {
|
|
1691
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
1692
|
+
const padding = (4 - normalized.length % 4) % 4;
|
|
1693
|
+
const padded = normalized + "=".repeat(padding);
|
|
1694
|
+
const decoded = Buffer.from(padded, "base64").toString("utf8");
|
|
1695
|
+
return JSON.parse(decoded);
|
|
1696
|
+
}
|
|
1697
|
+
function encodeBase64Json2(value) {
|
|
1698
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
1699
|
+
}
|
|
1700
|
+
function parsePaymentRequired(headerValue) {
|
|
1701
|
+
return decodeBase64Json(headerValue);
|
|
1702
|
+
}
|
|
1703
|
+
function normalizeNetwork(network) {
|
|
1704
|
+
if (!network || network.trim().length === 0) {
|
|
1705
|
+
return DEFAULT_NETWORK;
|
|
1706
|
+
}
|
|
1707
|
+
return network.trim().toLowerCase();
|
|
1708
|
+
}
|
|
1709
|
+
function resolveChainId(network) {
|
|
1710
|
+
const eip155Match = network.match(/^eip155:(\d+)$/i);
|
|
1711
|
+
if (eip155Match) {
|
|
1712
|
+
const parsed = Number.parseInt(eip155Match[1], 10);
|
|
1713
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1714
|
+
return parsed;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (network === "base") return BASE_CHAIN_ID;
|
|
1718
|
+
if (network === "base-sepolia") return BASE_SEPOLIA_CHAIN_ID2;
|
|
1719
|
+
return BASE_CHAIN_ID;
|
|
1720
|
+
}
|
|
1721
|
+
function parseHexAddress(value) {
|
|
1722
|
+
if (!value) return void 0;
|
|
1723
|
+
const direct = value.match(/^0x[a-fA-F0-9]{40}$/);
|
|
1724
|
+
if (direct) {
|
|
1725
|
+
return direct[0];
|
|
1726
|
+
}
|
|
1727
|
+
const caipSuffix = value.match(/0x[a-fA-F0-9]{40}$/);
|
|
1728
|
+
if (caipSuffix) {
|
|
1729
|
+
return caipSuffix[0];
|
|
1730
|
+
}
|
|
1731
|
+
return void 0;
|
|
1732
|
+
}
|
|
1733
|
+
function requireHexAddress(value, field) {
|
|
1734
|
+
const parsed = parseHexAddress(value);
|
|
1735
|
+
if (!parsed) {
|
|
1736
|
+
throw new Error(`Invalid ${field} in payment requirements: ${String(value)}`);
|
|
1737
|
+
}
|
|
1738
|
+
return parsed;
|
|
1739
|
+
}
|
|
1740
|
+
function setPaymentHeaders(headers, payload) {
|
|
1741
|
+
headers.set("payment-signature", payload);
|
|
1742
|
+
headers.set("x-payment", payload);
|
|
1743
|
+
}
|
|
1744
|
+
async function createPaymentPayload(privateKey, fromAddress, option, amount, requestUrl, resource) {
|
|
1745
|
+
const network = normalizeNetwork(option.network);
|
|
1746
|
+
const chainId = resolveChainId(network);
|
|
1747
|
+
const recipient = requireHexAddress(option.payTo, "payTo");
|
|
1748
|
+
const verifyingContract = requireHexAddress(option.asset, "asset");
|
|
1749
|
+
const maxTimeoutSeconds = typeof option.maxTimeoutSeconds === "number" && option.maxTimeoutSeconds > 0 ? Math.floor(option.maxTimeoutSeconds) : DEFAULT_MAX_TIMEOUT_SECONDS;
|
|
1750
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1751
|
+
const validAfter = now - 600;
|
|
1752
|
+
const validBefore = now + maxTimeoutSeconds;
|
|
1753
|
+
const nonce = createNonce();
|
|
1754
|
+
const signature = await signTypedData2({
|
|
1755
|
+
privateKey,
|
|
1756
|
+
domain: {
|
|
1757
|
+
name: option.extra?.name || DEFAULT_TOKEN_NAME,
|
|
1758
|
+
version: option.extra?.version || DEFAULT_TOKEN_VERSION,
|
|
1759
|
+
chainId,
|
|
1760
|
+
verifyingContract
|
|
1761
|
+
},
|
|
1762
|
+
types: TRANSFER_TYPES,
|
|
1763
|
+
primaryType: "TransferWithAuthorization",
|
|
1764
|
+
message: {
|
|
1765
|
+
from: fromAddress,
|
|
1766
|
+
to: recipient,
|
|
1767
|
+
value: BigInt(amount),
|
|
1768
|
+
validAfter: BigInt(validAfter),
|
|
1769
|
+
validBefore: BigInt(validBefore),
|
|
1770
|
+
nonce
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
const paymentData = {
|
|
1774
|
+
x402Version: 2,
|
|
1775
|
+
resource: {
|
|
1776
|
+
url: resource?.url || requestUrl,
|
|
1777
|
+
description: resource?.description || "BlockRun AI API call",
|
|
1778
|
+
mimeType: "application/json"
|
|
1779
|
+
},
|
|
1780
|
+
accepted: {
|
|
1781
|
+
scheme: option.scheme,
|
|
1782
|
+
network,
|
|
1783
|
+
amount,
|
|
1784
|
+
asset: option.asset,
|
|
1785
|
+
payTo: option.payTo,
|
|
1786
|
+
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
1787
|
+
extra: option.extra
|
|
1788
|
+
},
|
|
1789
|
+
payload: {
|
|
1790
|
+
signature,
|
|
1791
|
+
authorization: {
|
|
1792
|
+
from: fromAddress,
|
|
1793
|
+
to: recipient,
|
|
1794
|
+
value: amount,
|
|
1795
|
+
validAfter: validAfter.toString(),
|
|
1796
|
+
validBefore: validBefore.toString(),
|
|
1797
|
+
nonce
|
|
1798
|
+
}
|
|
1799
|
+
},
|
|
1800
|
+
extensions: {}
|
|
1801
|
+
};
|
|
1802
|
+
return encodeBase64Json2(paymentData);
|
|
1803
|
+
}
|
|
1804
|
+
function createPaymentFetch(privateKey) {
|
|
1805
|
+
const account = privateKeyToAccount4(privateKey);
|
|
1806
|
+
const walletAddress = account.address;
|
|
1807
|
+
const paymentCache = new PaymentCache();
|
|
1808
|
+
const payFetch = async (input, init, preAuth) => {
|
|
1809
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
1810
|
+
const endpointPath = new URL(url).pathname;
|
|
1811
|
+
const cached = paymentCache.get(endpointPath);
|
|
1812
|
+
if (cached && preAuth?.estimatedAmount) {
|
|
1813
|
+
const paymentPayload = await createPaymentPayload(
|
|
1814
|
+
privateKey,
|
|
1815
|
+
walletAddress,
|
|
1816
|
+
{
|
|
1817
|
+
scheme: cached.scheme,
|
|
1818
|
+
network: cached.network,
|
|
1819
|
+
asset: cached.asset,
|
|
1820
|
+
payTo: cached.payTo,
|
|
1821
|
+
maxTimeoutSeconds: cached.maxTimeoutSeconds,
|
|
1822
|
+
extra: cached.extra
|
|
1823
|
+
},
|
|
1824
|
+
preAuth.estimatedAmount,
|
|
1825
|
+
url,
|
|
1826
|
+
{
|
|
1827
|
+
url: cached.resourceUrl,
|
|
1828
|
+
description: cached.resourceDescription
|
|
1829
|
+
}
|
|
1830
|
+
);
|
|
1831
|
+
const preAuthHeaders = new Headers(init?.headers);
|
|
1832
|
+
setPaymentHeaders(preAuthHeaders, paymentPayload);
|
|
1833
|
+
const response2 = await fetch(input, { ...init, headers: preAuthHeaders });
|
|
1834
|
+
if (response2.status !== 402) {
|
|
1835
|
+
return response2;
|
|
1836
|
+
}
|
|
1837
|
+
const paymentHeader2 = response2.headers.get("payment-required") ?? response2.headers.get("x-payment-required");
|
|
1838
|
+
if (paymentHeader2) {
|
|
1839
|
+
return handle402(input, init, url, endpointPath, paymentHeader2);
|
|
1840
|
+
}
|
|
1841
|
+
paymentCache.invalidate(endpointPath);
|
|
1842
|
+
const cleanResponse = await fetch(input, init);
|
|
1843
|
+
if (cleanResponse.status !== 402) {
|
|
1844
|
+
return cleanResponse;
|
|
1845
|
+
}
|
|
1846
|
+
const cleanHeader = cleanResponse.headers.get("payment-required") ?? cleanResponse.headers.get("x-payment-required");
|
|
1847
|
+
if (!cleanHeader) {
|
|
1848
|
+
throw new Error("402 response missing PAYMENT-REQUIRED or x-payment-required header");
|
|
1849
|
+
}
|
|
1850
|
+
return handle402(input, init, url, endpointPath, cleanHeader);
|
|
1851
|
+
}
|
|
1852
|
+
const response = await fetch(input, init);
|
|
1853
|
+
if (response.status !== 402) {
|
|
1854
|
+
return response;
|
|
1855
|
+
}
|
|
1856
|
+
const paymentHeader = response.headers.get("payment-required") ?? response.headers.get("x-payment-required");
|
|
1857
|
+
if (!paymentHeader) {
|
|
1858
|
+
throw new Error("402 response missing PAYMENT-REQUIRED or x-payment-required header");
|
|
1859
|
+
}
|
|
1860
|
+
return handle402(input, init, url, endpointPath, paymentHeader);
|
|
1861
|
+
};
|
|
1862
|
+
async function handle402(input, init, url, endpointPath, paymentHeader) {
|
|
1863
|
+
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
1864
|
+
const option = paymentRequired.accepts?.[0];
|
|
1865
|
+
if (!option) {
|
|
1866
|
+
throw new Error("No payment options in 402 response");
|
|
1867
|
+
}
|
|
1868
|
+
const amount = option.amount || option.maxAmountRequired;
|
|
1869
|
+
if (!amount) {
|
|
1870
|
+
throw new Error("No amount in payment requirements");
|
|
1871
|
+
}
|
|
1872
|
+
paymentCache.set(endpointPath, {
|
|
1873
|
+
payTo: option.payTo,
|
|
1874
|
+
asset: option.asset,
|
|
1875
|
+
scheme: option.scheme,
|
|
1876
|
+
network: option.network,
|
|
1877
|
+
extra: option.extra,
|
|
1878
|
+
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
1879
|
+
resourceUrl: paymentRequired.resource?.url,
|
|
1880
|
+
resourceDescription: paymentRequired.resource?.description
|
|
1881
|
+
});
|
|
1882
|
+
const paymentPayload = await createPaymentPayload(
|
|
1883
|
+
privateKey,
|
|
1884
|
+
walletAddress,
|
|
1885
|
+
option,
|
|
1886
|
+
amount,
|
|
1887
|
+
url,
|
|
1888
|
+
paymentRequired.resource
|
|
1889
|
+
);
|
|
1890
|
+
const retryHeaders = new Headers(init?.headers);
|
|
1891
|
+
setPaymentHeaders(retryHeaders, paymentPayload);
|
|
1892
|
+
return fetch(input, {
|
|
1893
|
+
...init,
|
|
1894
|
+
headers: retryHeaders
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
return { fetch: payFetch, cache: paymentCache };
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/cloud-command.ts
|
|
1901
|
+
var SUPPORTED_BACKUP_PLATFORMS = /* @__PURE__ */ new Set(["darwin", "linux"]);
|
|
1902
|
+
var BACKUP_DIR_SUBPATH = join4(".openclaw", "mnemospark", "backup");
|
|
1903
|
+
var DEFAULT_BACKUP_DIR = join4(homedir2(), BACKUP_DIR_SUBPATH);
|
|
1904
|
+
var OBJECT_LOG_SUBPATH = join4(".openclaw", "mnemospark", "object.log");
|
|
1905
|
+
var CRON_TABLE_SUBPATH = join4(".openclaw", "mnemospark", "crontab.txt");
|
|
1906
|
+
var BLOCKRUN_WALLET_KEY_SUBPATH = join4(".openclaw", "blockrun", "wallet.key");
|
|
1907
|
+
var MNEMOSPARK_WALLET_KEY_SUBPATH = join4(".openclaw", "mnemospark", "wallet", "wallet.key");
|
|
1908
|
+
var KEY_STORE_SUBPATH = join4(".openclaw", "mnemospark", "keys");
|
|
1909
|
+
var INLINE_UPLOAD_MAX_BYTES = 45e5;
|
|
1910
|
+
var AES_GCM_NONCE_BYTES = 12;
|
|
1911
|
+
var PAYMENT_REMINDER_INTERVAL_DAYS = 30;
|
|
1912
|
+
var PAYMENT_DELETE_DEADLINE_DAYS = 32;
|
|
1913
|
+
var PAYMENT_CRON_SCHEDULE = "0 0 1 * *";
|
|
1914
|
+
var CRON_LOG_ROW_PREFIX = "cron";
|
|
1915
|
+
var TAR_OVERHEAD_BYTES = 10 * 1024 * 1024;
|
|
1916
|
+
var REQUIRED_PRICE_STORAGE = "--wallet-address, --object-id, --object-id-hash, --gb, --provider, --region";
|
|
1917
|
+
var REQUIRED_UPLOAD = "--quote-id, --wallet-address, --object-id, --object-id-hash";
|
|
1918
|
+
var REQUIRED_STORAGE_OBJECT = "--wallet-address, --object-key";
|
|
1919
|
+
var CLOUD_HELP_TEXT = [
|
|
1920
|
+
"\u2601\uFE0F **mnemospark Cloud Commands**",
|
|
1921
|
+
"",
|
|
1922
|
+
"\u2022 `/cloud` or `/cloud help` \u2014 show this message",
|
|
1923
|
+
"",
|
|
1924
|
+
"\u2022 `/cloud backup <file>` or `/cloud backup <directory>`",
|
|
1925
|
+
" Required: <file> or <directory> (path to back up)",
|
|
1926
|
+
"",
|
|
1927
|
+
"\u2022 `/cloud price-storage --wallet-address <addr> --object-id <id> --object-id-hash <hash> --gb <gb> --provider <provider> --region <region>`",
|
|
1928
|
+
" Required: " + REQUIRED_PRICE_STORAGE,
|
|
1929
|
+
"",
|
|
1930
|
+
"\u2022 `/cloud upload --quote-id <quote-id> --wallet-address <addr> --object-id <id> --object-id-hash <hash>`",
|
|
1931
|
+
" Required: " + REQUIRED_UPLOAD,
|
|
1932
|
+
"",
|
|
1933
|
+
"\u2022 `/cloud ls --wallet-address <addr> --object-key <object-key>`",
|
|
1934
|
+
" Required: " + REQUIRED_STORAGE_OBJECT,
|
|
1935
|
+
"",
|
|
1936
|
+
"\u2022 `/cloud download --wallet-address <addr> --object-key <object-key>`",
|
|
1937
|
+
" Required: " + REQUIRED_STORAGE_OBJECT,
|
|
1938
|
+
"",
|
|
1939
|
+
"\u2022 `/cloud delete --wallet-address <addr> --object-key <object-key>`",
|
|
1940
|
+
" Required: " + REQUIRED_STORAGE_OBJECT,
|
|
1941
|
+
"",
|
|
1942
|
+
"Backup creates a tar+gzip object in ~/.openclaw/mnemospark/backup and appends object metadata to ~/.openclaw/mnemospark/object.log. Upload appends storage rows and cron-tracking rows to object.log, and keeps job entries in ~/.openclaw/mnemospark/crontab.txt. All storage commands (price-storage, upload, ls, download, delete) require --wallet-address."
|
|
1943
|
+
].join("\n");
|
|
1944
|
+
var UnsupportedBackupPlatformError = class extends Error {
|
|
1945
|
+
constructor(platform) {
|
|
1946
|
+
super(`Unsupported platform for backup: ${platform}`);
|
|
1947
|
+
this.name = "UnsupportedBackupPlatformError";
|
|
1948
|
+
}
|
|
1949
|
+
};
|
|
1950
|
+
function toGbString(bytes) {
|
|
1951
|
+
const gb = bytes / 1e9;
|
|
1952
|
+
const fixed = gb.toFixed(9).replace(/\.?0+$/, "");
|
|
1953
|
+
if (!fixed) return "0";
|
|
1954
|
+
return fixed.includes(".") ? fixed : `${fixed}.0`;
|
|
1955
|
+
}
|
|
1956
|
+
function stripWrappingQuotes(input) {
|
|
1957
|
+
const trimmed = input.trim();
|
|
1958
|
+
if (trimmed.length < 2) return trimmed;
|
|
1959
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
1960
|
+
return trimmed.slice(1, -1);
|
|
1961
|
+
}
|
|
1962
|
+
return trimmed;
|
|
1963
|
+
}
|
|
1964
|
+
function tokenizeArgs(input) {
|
|
1965
|
+
const tokens = input.match(/"[^"]*"|'[^']*'|\S+/g);
|
|
1966
|
+
if (!tokens) {
|
|
1967
|
+
return [];
|
|
1968
|
+
}
|
|
1969
|
+
return tokens.map((token) => stripWrappingQuotes(token));
|
|
1970
|
+
}
|
|
1971
|
+
function parseNamedFlags(input) {
|
|
1972
|
+
const tokens = tokenizeArgs(input);
|
|
1973
|
+
if (tokens.length === 0) {
|
|
1974
|
+
return null;
|
|
1975
|
+
}
|
|
1976
|
+
const parsed = {};
|
|
1977
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
1978
|
+
const keyToken = tokens[i];
|
|
1979
|
+
if (!keyToken.startsWith("--")) {
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
const key = keyToken.slice(2).toLowerCase();
|
|
1983
|
+
const value = tokens[i + 1];
|
|
1984
|
+
if (!value || value.startsWith("--")) {
|
|
1985
|
+
return null;
|
|
1986
|
+
}
|
|
1987
|
+
parsed[key] = value;
|
|
1988
|
+
i += 1;
|
|
1989
|
+
}
|
|
1990
|
+
return parsed;
|
|
1991
|
+
}
|
|
1992
|
+
function parseCloudArgs(args) {
|
|
1993
|
+
const trimmed = args?.trim() ?? "";
|
|
1994
|
+
if (!trimmed) {
|
|
1995
|
+
return { mode: "help" };
|
|
1996
|
+
}
|
|
1997
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
1998
|
+
const subcommand = (spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)).toLowerCase();
|
|
1999
|
+
const rest = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1);
|
|
2000
|
+
if (subcommand === "help") {
|
|
2001
|
+
return { mode: "help" };
|
|
2002
|
+
}
|
|
2003
|
+
if (subcommand === "backup") {
|
|
2004
|
+
const backupTarget = stripWrappingQuotes(rest);
|
|
2005
|
+
if (!backupTarget) {
|
|
2006
|
+
return { mode: "unknown" };
|
|
2007
|
+
}
|
|
2008
|
+
return { mode: "backup", backupTarget };
|
|
2009
|
+
}
|
|
2010
|
+
if (subcommand === "price-storage") {
|
|
2011
|
+
const flags = parseNamedFlags(rest);
|
|
2012
|
+
if (!flags) {
|
|
2013
|
+
return { mode: "price-storage-invalid" };
|
|
2014
|
+
}
|
|
2015
|
+
const gb = Number.parseFloat(flags.gb ?? "");
|
|
2016
|
+
const request = parsePriceStorageQuoteRequest({
|
|
2017
|
+
wallet_address: flags["wallet-address"],
|
|
2018
|
+
object_id: flags["object-id"],
|
|
2019
|
+
object_id_hash: flags["object-id-hash"],
|
|
2020
|
+
gb,
|
|
2021
|
+
provider: flags.provider,
|
|
2022
|
+
region: flags.region
|
|
2023
|
+
});
|
|
2024
|
+
if (!request) {
|
|
2025
|
+
return { mode: "price-storage-invalid" };
|
|
2026
|
+
}
|
|
2027
|
+
return { mode: "price-storage", priceStorageRequest: request };
|
|
2028
|
+
}
|
|
2029
|
+
if (subcommand === "upload") {
|
|
2030
|
+
const flags = parseNamedFlags(rest);
|
|
2031
|
+
if (!flags) {
|
|
2032
|
+
return { mode: "upload-invalid" };
|
|
2033
|
+
}
|
|
2034
|
+
const quoteId = flags["quote-id"]?.trim();
|
|
2035
|
+
const walletAddress = flags["wallet-address"]?.trim();
|
|
2036
|
+
const objectId = flags["object-id"]?.trim();
|
|
2037
|
+
const objectIdHash = flags["object-id-hash"]?.trim();
|
|
2038
|
+
if (!quoteId || !walletAddress || !objectId || !objectIdHash) {
|
|
2039
|
+
return { mode: "upload-invalid" };
|
|
2040
|
+
}
|
|
2041
|
+
return {
|
|
2042
|
+
mode: "upload",
|
|
2043
|
+
uploadRequest: {
|
|
2044
|
+
quote_id: quoteId,
|
|
2045
|
+
wallet_address: walletAddress,
|
|
2046
|
+
object_id: objectId,
|
|
2047
|
+
object_id_hash: objectIdHash
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
if (subcommand === "ls") {
|
|
2052
|
+
const flags = parseNamedFlags(rest);
|
|
2053
|
+
if (!flags) {
|
|
2054
|
+
return { mode: "ls-invalid" };
|
|
2055
|
+
}
|
|
2056
|
+
const request = parseStorageObjectRequest({
|
|
2057
|
+
wallet_address: flags["wallet-address"],
|
|
2058
|
+
object_key: flags["object-key"],
|
|
2059
|
+
location: flags.location ?? flags.region
|
|
2060
|
+
});
|
|
2061
|
+
if (!request) {
|
|
2062
|
+
return { mode: "ls-invalid" };
|
|
2063
|
+
}
|
|
2064
|
+
return { mode: "ls", storageObjectRequest: request };
|
|
2065
|
+
}
|
|
2066
|
+
if (subcommand === "download") {
|
|
2067
|
+
const flags = parseNamedFlags(rest);
|
|
2068
|
+
if (!flags) {
|
|
2069
|
+
return { mode: "download-invalid" };
|
|
2070
|
+
}
|
|
2071
|
+
const request = parseStorageObjectRequest({
|
|
2072
|
+
wallet_address: flags["wallet-address"],
|
|
2073
|
+
object_key: flags["object-key"],
|
|
2074
|
+
location: flags.location ?? flags.region
|
|
2075
|
+
});
|
|
2076
|
+
if (!request) {
|
|
2077
|
+
return { mode: "download-invalid" };
|
|
2078
|
+
}
|
|
2079
|
+
return { mode: "download", storageObjectRequest: request };
|
|
2080
|
+
}
|
|
2081
|
+
if (subcommand === "delete") {
|
|
2082
|
+
const flags = parseNamedFlags(rest);
|
|
2083
|
+
if (!flags) {
|
|
2084
|
+
return { mode: "delete-invalid" };
|
|
2085
|
+
}
|
|
2086
|
+
const request = parseStorageObjectRequest({
|
|
2087
|
+
wallet_address: flags["wallet-address"],
|
|
2088
|
+
object_key: flags["object-key"],
|
|
2089
|
+
location: flags.location ?? flags.region
|
|
2090
|
+
});
|
|
2091
|
+
if (!request) {
|
|
2092
|
+
return { mode: "delete-invalid" };
|
|
2093
|
+
}
|
|
2094
|
+
return { mode: "delete", storageObjectRequest: request };
|
|
2095
|
+
}
|
|
2096
|
+
return { mode: "unknown" };
|
|
2097
|
+
}
|
|
2098
|
+
function resolveObjectLogPath(homeDir) {
|
|
2099
|
+
return join4(homeDir ?? homedir2(), OBJECT_LOG_SUBPATH);
|
|
2100
|
+
}
|
|
2101
|
+
function resolveCronTablePath(homeDir) {
|
|
2102
|
+
return join4(homeDir ?? homedir2(), CRON_TABLE_SUBPATH);
|
|
2103
|
+
}
|
|
2104
|
+
async function appendObjectLogLine(line, homeDir) {
|
|
2105
|
+
const objectLogPath = resolveObjectLogPath(homeDir);
|
|
2106
|
+
await mkdir3(dirname3(objectLogPath), { recursive: true });
|
|
2107
|
+
await appendFile(objectLogPath, `${line}
|
|
2108
|
+
`, "utf-8");
|
|
2109
|
+
return objectLogPath;
|
|
2110
|
+
}
|
|
2111
|
+
async function calculateInputSizeBytes(targetPath) {
|
|
2112
|
+
const targetStats = await lstat(targetPath);
|
|
2113
|
+
if (targetStats.isFile() || targetStats.isSymbolicLink()) {
|
|
2114
|
+
return targetStats.size;
|
|
2115
|
+
}
|
|
2116
|
+
if (!targetStats.isDirectory()) {
|
|
2117
|
+
throw new Error("Backup target must be a file or directory");
|
|
2118
|
+
}
|
|
2119
|
+
let total = 0;
|
|
2120
|
+
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
2121
|
+
for (const entry of entries) {
|
|
2122
|
+
total += await calculateInputSizeBytes(join4(targetPath, entry.name));
|
|
2123
|
+
}
|
|
2124
|
+
return total;
|
|
2125
|
+
}
|
|
2126
|
+
function getAvailableDiskBytes(tmpDir, options) {
|
|
2127
|
+
if (typeof options.availableDiskBytes === "number") {
|
|
2128
|
+
return options.availableDiskBytes;
|
|
2129
|
+
}
|
|
2130
|
+
const stats = statfsSync(tmpDir);
|
|
2131
|
+
return stats.bavail * stats.bsize;
|
|
2132
|
+
}
|
|
2133
|
+
async function runTarGzip(archivePath, sourcePath) {
|
|
2134
|
+
const sourceDir = dirname3(sourcePath);
|
|
2135
|
+
const sourceName = basename(sourcePath);
|
|
2136
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
2137
|
+
let stderr = "";
|
|
2138
|
+
const child = spawn("tar", ["-czf", archivePath, "-C", sourceDir, sourceName], {
|
|
2139
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
2140
|
+
});
|
|
2141
|
+
child.stderr.on("data", (chunk) => {
|
|
2142
|
+
stderr += chunk.toString();
|
|
2143
|
+
});
|
|
2144
|
+
child.on("error", rejectPromise);
|
|
2145
|
+
child.on("close", (code) => {
|
|
2146
|
+
if (code === 0) {
|
|
2147
|
+
resolvePromise();
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
rejectPromise(new Error(stderr.trim() || `tar exited with code ${code ?? "unknown"}`));
|
|
2151
|
+
});
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2154
|
+
async function sha256File(filePath) {
|
|
2155
|
+
const hash = createHash("sha256");
|
|
2156
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
2157
|
+
const stream = createReadStream(filePath);
|
|
2158
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
2159
|
+
stream.on("error", rejectPromise);
|
|
2160
|
+
stream.on("end", () => resolvePromise());
|
|
2161
|
+
});
|
|
2162
|
+
return hash.digest("hex");
|
|
2163
|
+
}
|
|
2164
|
+
function createObjectId(options) {
|
|
2165
|
+
const nowFn = options.now ?? Date.now;
|
|
2166
|
+
const randomFn = options.randomBytes ?? randomBytesNode;
|
|
2167
|
+
return `${nowFn()}-${randomFn(8).toString("hex")}`;
|
|
2168
|
+
}
|
|
2169
|
+
async function buildBackupObject(targetPathArg, options = {}) {
|
|
2170
|
+
const platform = options.platform ?? process.platform;
|
|
2171
|
+
if (!SUPPORTED_BACKUP_PLATFORMS.has(platform)) {
|
|
2172
|
+
throw new UnsupportedBackupPlatformError(platform);
|
|
2173
|
+
}
|
|
2174
|
+
const targetPath = resolve2(targetPathArg);
|
|
2175
|
+
const targetStats = await lstat(targetPath);
|
|
2176
|
+
if (!targetStats.isFile() && !targetStats.isDirectory()) {
|
|
2177
|
+
throw new Error("Backup target must be a file or directory");
|
|
2178
|
+
}
|
|
2179
|
+
const tmpDir = options.tmpDir ?? DEFAULT_BACKUP_DIR;
|
|
2180
|
+
let tmpStats;
|
|
2181
|
+
try {
|
|
2182
|
+
tmpStats = await stat(tmpDir);
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
if (error.code === "ENOENT") {
|
|
2185
|
+
await mkdir3(tmpDir, { recursive: true });
|
|
2186
|
+
tmpStats = await stat(tmpDir);
|
|
2187
|
+
} else {
|
|
2188
|
+
throw error;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
if (!tmpStats.isDirectory()) {
|
|
2192
|
+
throw new Error("Backup path is not a directory");
|
|
2193
|
+
}
|
|
2194
|
+
const inputSizeBytes = await calculateInputSizeBytes(targetPath);
|
|
2195
|
+
const availableDiskBytes = getAvailableDiskBytes(tmpDir, options);
|
|
2196
|
+
const requiredDiskBytes = inputSizeBytes + TAR_OVERHEAD_BYTES;
|
|
2197
|
+
if (availableDiskBytes < requiredDiskBytes) {
|
|
2198
|
+
throw new Error("Insufficient disk space for backup object");
|
|
2199
|
+
}
|
|
2200
|
+
const objectId = createObjectId(options);
|
|
2201
|
+
const archivePath = join4(tmpDir, objectId);
|
|
2202
|
+
try {
|
|
2203
|
+
await runTarGzip(archivePath, targetPath);
|
|
2204
|
+
const archiveStats = await stat(archivePath);
|
|
2205
|
+
const objectIdHash = await sha256File(archivePath);
|
|
2206
|
+
const objectSizeGb = toGbString(archiveStats.size);
|
|
2207
|
+
const objectLogPath = await appendObjectLogLine(
|
|
2208
|
+
`${objectId},${objectIdHash},${objectSizeGb}`,
|
|
2209
|
+
options.homeDir
|
|
2210
|
+
);
|
|
2211
|
+
return {
|
|
2212
|
+
objectId,
|
|
2213
|
+
objectIdHash,
|
|
2214
|
+
objectSizeGb,
|
|
2215
|
+
archivePath,
|
|
2216
|
+
objectLogPath
|
|
2217
|
+
};
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
await rm(archivePath, { force: true }).catch(() => void 0);
|
|
2220
|
+
throw error;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
async function appendPriceStorageQuoteLog(quote, homeDir) {
|
|
2224
|
+
return appendObjectLogLine(
|
|
2225
|
+
[
|
|
2226
|
+
quote.timestamp,
|
|
2227
|
+
quote.quote_id,
|
|
2228
|
+
quote.storage_price.toString(),
|
|
2229
|
+
quote.addr,
|
|
2230
|
+
quote.object_id,
|
|
2231
|
+
quote.object_id_hash,
|
|
2232
|
+
quote.object_size_gb.toString(),
|
|
2233
|
+
quote.provider,
|
|
2234
|
+
quote.location
|
|
2235
|
+
].join(","),
|
|
2236
|
+
homeDir
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
function formatTimestamp(date) {
|
|
2240
|
+
const pad = (value) => value.toString().padStart(2, "0");
|
|
2241
|
+
return [
|
|
2242
|
+
date.getFullYear().toString(),
|
|
2243
|
+
"-",
|
|
2244
|
+
pad(date.getMonth() + 1),
|
|
2245
|
+
"-",
|
|
2246
|
+
pad(date.getDate()),
|
|
2247
|
+
" ",
|
|
2248
|
+
pad(date.getHours()),
|
|
2249
|
+
":",
|
|
2250
|
+
pad(date.getMinutes()),
|
|
2251
|
+
":",
|
|
2252
|
+
pad(date.getSeconds())
|
|
2253
|
+
].join("");
|
|
2254
|
+
}
|
|
2255
|
+
function parseLoggedPriceStorageQuote(line) {
|
|
2256
|
+
const parts = line.split(",");
|
|
2257
|
+
if (parts.length < 9) {
|
|
2258
|
+
return null;
|
|
2259
|
+
}
|
|
2260
|
+
const quoteId = parts[1]?.trim() ?? "";
|
|
2261
|
+
const storagePriceRaw = parts[2]?.trim() ?? "";
|
|
2262
|
+
const walletAddress = parts[3]?.trim() ?? "";
|
|
2263
|
+
const objectId = parts[4]?.trim() ?? "";
|
|
2264
|
+
const objectIdHash = parts[5]?.trim() ?? "";
|
|
2265
|
+
const provider = parts[7]?.trim() ?? "";
|
|
2266
|
+
const location = parts[8]?.trim() ?? "";
|
|
2267
|
+
const storagePrice = Number.parseFloat(storagePriceRaw);
|
|
2268
|
+
if (!quoteId || !walletAddress || !objectId || !objectIdHash || !provider || !location) {
|
|
2269
|
+
return null;
|
|
2270
|
+
}
|
|
2271
|
+
if (!Number.isFinite(storagePrice) || storagePrice <= 0) {
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
return {
|
|
2275
|
+
quoteId,
|
|
2276
|
+
storagePrice,
|
|
2277
|
+
walletAddress,
|
|
2278
|
+
objectId,
|
|
2279
|
+
objectIdHash,
|
|
2280
|
+
provider,
|
|
2281
|
+
location
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
function parseLoggedStoragePaymentCron(line) {
|
|
2285
|
+
const parts = line.split(",");
|
|
2286
|
+
if (parts.length < 5) {
|
|
2287
|
+
return null;
|
|
2288
|
+
}
|
|
2289
|
+
if ((parts[0]?.trim() ?? "").toLowerCase() !== CRON_LOG_ROW_PREFIX) {
|
|
2290
|
+
return null;
|
|
2291
|
+
}
|
|
2292
|
+
const cronId = parts[2]?.trim() ?? "";
|
|
2293
|
+
const objectId = parts[3]?.trim() ?? "";
|
|
2294
|
+
const objectKey = parts[4]?.trim() ?? "";
|
|
2295
|
+
if (!cronId || !objectId || !objectKey) {
|
|
2296
|
+
return null;
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
cronId,
|
|
2300
|
+
objectId,
|
|
2301
|
+
objectKey
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
function parseStoragePaymentCronJobLine(line) {
|
|
2305
|
+
const trimmed = line.trim();
|
|
2306
|
+
if (!trimmed) {
|
|
2307
|
+
return null;
|
|
2308
|
+
}
|
|
2309
|
+
let payload;
|
|
2310
|
+
try {
|
|
2311
|
+
payload = JSON.parse(trimmed);
|
|
2312
|
+
} catch {
|
|
2313
|
+
return null;
|
|
2314
|
+
}
|
|
2315
|
+
if (!payload || typeof payload !== "object") {
|
|
2316
|
+
return null;
|
|
2317
|
+
}
|
|
2318
|
+
const record = payload;
|
|
2319
|
+
const cronId = typeof record.cronId === "string" ? record.cronId.trim() : "";
|
|
2320
|
+
const createdAt = typeof record.createdAt === "string" ? record.createdAt.trim() : "";
|
|
2321
|
+
const schedule = typeof record.schedule === "string" ? record.schedule.trim() : "";
|
|
2322
|
+
const command = typeof record.command === "string" ? record.command.trim() : "";
|
|
2323
|
+
const quoteId = typeof record.quoteId === "string" ? record.quoteId.trim() : "";
|
|
2324
|
+
const storagePrice = typeof record.storagePrice === "number" ? record.storagePrice : Number.NaN;
|
|
2325
|
+
const walletAddress = typeof record.walletAddress === "string" ? record.walletAddress.trim() : "";
|
|
2326
|
+
const objectId = typeof record.objectId === "string" ? record.objectId.trim() : "";
|
|
2327
|
+
const objectKey = typeof record.objectKey === "string" ? record.objectKey.trim() : "";
|
|
2328
|
+
const provider = typeof record.provider === "string" ? record.provider.trim() : "";
|
|
2329
|
+
const bucketName = typeof record.bucketName === "string" ? record.bucketName.trim() : "";
|
|
2330
|
+
const location = typeof record.location === "string" ? record.location.trim() : "";
|
|
2331
|
+
if (!cronId || !createdAt || !schedule || !command || !quoteId || !Number.isFinite(storagePrice) || storagePrice <= 0 || !walletAddress || !objectId || !objectKey || !provider || !bucketName || !location) {
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
return {
|
|
2335
|
+
cronId,
|
|
2336
|
+
createdAt,
|
|
2337
|
+
schedule,
|
|
2338
|
+
command,
|
|
2339
|
+
quoteId,
|
|
2340
|
+
storagePrice,
|
|
2341
|
+
walletAddress,
|
|
2342
|
+
objectId,
|
|
2343
|
+
objectKey,
|
|
2344
|
+
provider,
|
|
2345
|
+
bucketName,
|
|
2346
|
+
location
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
async function findLoggedPriceStorageQuote(quoteId, homeDir) {
|
|
2350
|
+
const objectLogPath = resolveObjectLogPath(homeDir);
|
|
2351
|
+
let content;
|
|
2352
|
+
try {
|
|
2353
|
+
content = await readFile2(objectLogPath, "utf-8");
|
|
2354
|
+
} catch (error) {
|
|
2355
|
+
if (error.code === "ENOENT") {
|
|
2356
|
+
return null;
|
|
2357
|
+
}
|
|
2358
|
+
throw error;
|
|
2359
|
+
}
|
|
2360
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2361
|
+
for (let idx = lines.length - 1; idx >= 0; idx -= 1) {
|
|
2362
|
+
const parsed = parseLoggedPriceStorageQuote(lines[idx]);
|
|
2363
|
+
if (parsed && parsed.quoteId === quoteId) {
|
|
2364
|
+
return parsed;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
return null;
|
|
2368
|
+
}
|
|
2369
|
+
async function findLoggedStoragePaymentCronByObjectKey(objectKey, homeDir) {
|
|
2370
|
+
const objectLogPath = resolveObjectLogPath(homeDir);
|
|
2371
|
+
let content;
|
|
2372
|
+
try {
|
|
2373
|
+
content = await readFile2(objectLogPath, "utf-8");
|
|
2374
|
+
} catch (error) {
|
|
2375
|
+
if (error.code === "ENOENT") {
|
|
2376
|
+
return null;
|
|
2377
|
+
}
|
|
2378
|
+
throw error;
|
|
2379
|
+
}
|
|
2380
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
2381
|
+
for (let idx = lines.length - 1; idx >= 0; idx -= 1) {
|
|
2382
|
+
const parsed = parseLoggedStoragePaymentCron(lines[idx]);
|
|
2383
|
+
if (parsed && parsed.objectKey === objectKey) {
|
|
2384
|
+
return parsed;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return null;
|
|
2388
|
+
}
|
|
2389
|
+
function quoteCronArgument(value) {
|
|
2390
|
+
return JSON.stringify(String(value));
|
|
2391
|
+
}
|
|
2392
|
+
function buildStoragePaymentCronCommand(job) {
|
|
2393
|
+
return [
|
|
2394
|
+
"mnemospark-pay-storage",
|
|
2395
|
+
"--quote-id",
|
|
2396
|
+
quoteCronArgument(job.quoteId),
|
|
2397
|
+
"--wallet-address",
|
|
2398
|
+
quoteCronArgument(job.walletAddress),
|
|
2399
|
+
"--object-id",
|
|
2400
|
+
quoteCronArgument(job.objectId),
|
|
2401
|
+
"--object-key",
|
|
2402
|
+
quoteCronArgument(job.objectKey),
|
|
2403
|
+
"--storage-price",
|
|
2404
|
+
quoteCronArgument(job.storagePrice)
|
|
2405
|
+
].join(" ");
|
|
2406
|
+
}
|
|
2407
|
+
async function appendStoragePaymentCronLog(cronJob, homeDir) {
|
|
2408
|
+
return appendObjectLogLine(
|
|
2409
|
+
[
|
|
2410
|
+
CRON_LOG_ROW_PREFIX,
|
|
2411
|
+
cronJob.createdAt,
|
|
2412
|
+
cronJob.cronId,
|
|
2413
|
+
cronJob.objectId,
|
|
2414
|
+
cronJob.objectKey,
|
|
2415
|
+
cronJob.quoteId,
|
|
2416
|
+
cronJob.storagePrice.toString()
|
|
2417
|
+
].join(","),
|
|
2418
|
+
homeDir
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
async function appendStoragePaymentCronJob(cronJob, homeDir) {
|
|
2422
|
+
const cronTablePath = resolveCronTablePath(homeDir);
|
|
2423
|
+
await mkdir3(dirname3(cronTablePath), { recursive: true });
|
|
2424
|
+
await appendFile(cronTablePath, `${JSON.stringify(cronJob)}
|
|
2425
|
+
`, "utf-8");
|
|
2426
|
+
return cronTablePath;
|
|
2427
|
+
}
|
|
2428
|
+
async function removeStoragePaymentCronJob(cronId, homeDir) {
|
|
2429
|
+
const cronTablePath = resolveCronTablePath(homeDir);
|
|
2430
|
+
let content;
|
|
2431
|
+
try {
|
|
2432
|
+
content = await readFile2(cronTablePath, "utf-8");
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
if (error.code === "ENOENT") {
|
|
2435
|
+
return false;
|
|
2436
|
+
}
|
|
2437
|
+
throw error;
|
|
2438
|
+
}
|
|
2439
|
+
const lines = content.split(/\r?\n/);
|
|
2440
|
+
let removed = false;
|
|
2441
|
+
const keptLines = [];
|
|
2442
|
+
for (const line of lines) {
|
|
2443
|
+
const trimmed = line.trim();
|
|
2444
|
+
if (!trimmed) {
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
const parsed = parseStoragePaymentCronJobLine(trimmed);
|
|
2448
|
+
if (parsed && parsed.cronId === cronId) {
|
|
2449
|
+
removed = true;
|
|
2450
|
+
continue;
|
|
2451
|
+
}
|
|
2452
|
+
keptLines.push(trimmed);
|
|
2453
|
+
}
|
|
2454
|
+
if (!removed) {
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
await mkdir3(dirname3(cronTablePath), { recursive: true });
|
|
2458
|
+
const nextContent = keptLines.length > 0 ? `${keptLines.join("\n")}
|
|
2459
|
+
` : "";
|
|
2460
|
+
await writeFile3(cronTablePath, nextContent, "utf-8");
|
|
2461
|
+
return true;
|
|
2462
|
+
}
|
|
2463
|
+
async function createStoragePaymentCronJob(upload, storagePrice, homeDir, nowDateFn = () => /* @__PURE__ */ new Date()) {
|
|
2464
|
+
const cronId = randomUUID();
|
|
2465
|
+
const createdAt = formatTimestamp(nowDateFn());
|
|
2466
|
+
const cronJob = {
|
|
2467
|
+
cronId,
|
|
2468
|
+
createdAt,
|
|
2469
|
+
schedule: PAYMENT_CRON_SCHEDULE,
|
|
2470
|
+
command: buildStoragePaymentCronCommand({
|
|
2471
|
+
quoteId: upload.quote_id,
|
|
2472
|
+
walletAddress: upload.addr,
|
|
2473
|
+
objectId: upload.object_id,
|
|
2474
|
+
objectKey: upload.object_key,
|
|
2475
|
+
storagePrice
|
|
2476
|
+
}),
|
|
2477
|
+
quoteId: upload.quote_id,
|
|
2478
|
+
storagePrice,
|
|
2479
|
+
walletAddress: upload.addr,
|
|
2480
|
+
objectId: upload.object_id,
|
|
2481
|
+
objectKey: upload.object_key,
|
|
2482
|
+
provider: upload.provider,
|
|
2483
|
+
bucketName: upload.bucket_name,
|
|
2484
|
+
location: upload.location
|
|
2485
|
+
};
|
|
2486
|
+
await appendStoragePaymentCronJob(cronJob, homeDir);
|
|
2487
|
+
await appendStoragePaymentCronLog(cronJob, homeDir);
|
|
2488
|
+
return cronJob;
|
|
2489
|
+
}
|
|
2490
|
+
function isValidWalletPrivateKey(value) {
|
|
2491
|
+
return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value.trim());
|
|
2492
|
+
}
|
|
2493
|
+
async function readWalletKeyIfPresent(walletPath) {
|
|
2494
|
+
try {
|
|
2495
|
+
const key = (await readFile2(walletPath, "utf-8")).trim();
|
|
2496
|
+
return isValidWalletPrivateKey(key) ? key : null;
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
if (error.code === "ENOENT") {
|
|
2499
|
+
return null;
|
|
2500
|
+
}
|
|
2501
|
+
throw error;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
async function resolveWalletPrivateKey(homeDir) {
|
|
2505
|
+
const envKey = process.env.BLOCKRUN_WALLET_KEY?.trim();
|
|
2506
|
+
if (isValidWalletPrivateKey(envKey)) {
|
|
2507
|
+
return envKey;
|
|
2508
|
+
}
|
|
2509
|
+
const baseHome = homeDir ?? homedir2();
|
|
2510
|
+
const primaryWalletPath = join4(baseHome, BLOCKRUN_WALLET_KEY_SUBPATH);
|
|
2511
|
+
const fallbackWalletPath = join4(baseHome, MNEMOSPARK_WALLET_KEY_SUBPATH);
|
|
2512
|
+
const fromPrimary = await readWalletKeyIfPresent(primaryWalletPath);
|
|
2513
|
+
if (fromPrimary) {
|
|
2514
|
+
return fromPrimary;
|
|
2515
|
+
}
|
|
2516
|
+
const fromFallback = await readWalletKeyIfPresent(fallbackWalletPath);
|
|
2517
|
+
if (fromFallback) {
|
|
2518
|
+
return fromFallback;
|
|
2519
|
+
}
|
|
2520
|
+
throw new Error("Wallet key not found. Configure BLOCKRUN_WALLET_KEY or run /wallet first.");
|
|
2521
|
+
}
|
|
2522
|
+
function sha256Buffer(content) {
|
|
2523
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2524
|
+
}
|
|
2525
|
+
function walletShortHash(walletAddress) {
|
|
2526
|
+
return sha256Buffer(Buffer.from(walletAddress.trim().toLowerCase(), "utf-8")).slice(0, 16);
|
|
2527
|
+
}
|
|
2528
|
+
function bucketNameForWallet(walletAddress) {
|
|
2529
|
+
return `mnemospark-${walletShortHash(walletAddress)}`;
|
|
2530
|
+
}
|
|
2531
|
+
function encryptAesGcm(plaintext, key, randomFn = randomBytesNode) {
|
|
2532
|
+
if (key.length !== 32) {
|
|
2533
|
+
throw new Error("Expected 32-byte AES key");
|
|
2534
|
+
}
|
|
2535
|
+
const nonce = randomFn(AES_GCM_NONCE_BYTES);
|
|
2536
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
2537
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
2538
|
+
const tag = cipher.getAuthTag();
|
|
2539
|
+
return Buffer.concat([nonce, ciphertext, tag]);
|
|
2540
|
+
}
|
|
2541
|
+
async function loadOrCreateKek(walletAddress, homeDir) {
|
|
2542
|
+
const keyPath = join4(
|
|
2543
|
+
homeDir ?? homedir2(),
|
|
2544
|
+
KEY_STORE_SUBPATH,
|
|
2545
|
+
`${walletShortHash(walletAddress)}.key`
|
|
2546
|
+
);
|
|
2547
|
+
await mkdir3(dirname3(keyPath), { recursive: true });
|
|
2548
|
+
try {
|
|
2549
|
+
const existing = await readFile2(keyPath);
|
|
2550
|
+
if (existing.length === 32) {
|
|
2551
|
+
return { kek: existing, keyPath };
|
|
2552
|
+
}
|
|
2553
|
+
const decoded = Buffer.from(existing.toString("utf-8").trim(), "base64");
|
|
2554
|
+
if (decoded.length === 32) {
|
|
2555
|
+
return { kek: decoded, keyPath };
|
|
2556
|
+
}
|
|
2557
|
+
throw new Error("Invalid key file format");
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
if (error.code !== "ENOENT") {
|
|
2560
|
+
throw error;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
const generated = randomBytesNode(32);
|
|
2564
|
+
await writeFile3(keyPath, generated, { mode: 384 });
|
|
2565
|
+
return { kek: generated, keyPath };
|
|
2566
|
+
}
|
|
2567
|
+
async function prepareUploadPayload(archivePath, walletAddress, homeDir) {
|
|
2568
|
+
const plaintext = await readFile2(archivePath);
|
|
2569
|
+
const { kek, keyPath } = await loadOrCreateKek(walletAddress, homeDir);
|
|
2570
|
+
const dek = randomBytesNode(32);
|
|
2571
|
+
const encryptedContent = encryptAesGcm(plaintext, dek);
|
|
2572
|
+
const wrappedDek = encryptAesGcm(dek, kek);
|
|
2573
|
+
const payloadHash = sha256Buffer(encryptedContent);
|
|
2574
|
+
const payload = {
|
|
2575
|
+
mode: encryptedContent.length <= INLINE_UPLOAD_MAX_BYTES ? "inline" : "presigned",
|
|
2576
|
+
content_base64: encryptedContent.length <= INLINE_UPLOAD_MAX_BYTES ? encryptedContent.toString("base64") : void 0,
|
|
2577
|
+
content_sha256: payloadHash,
|
|
2578
|
+
content_length_bytes: encryptedContent.length,
|
|
2579
|
+
wrapped_dek: wrappedDek.toString("base64"),
|
|
2580
|
+
encryption_algorithm: "AES-256-GCM",
|
|
2581
|
+
bucket_name_hint: bucketNameForWallet(walletAddress),
|
|
2582
|
+
key_store_path_hint: keyPath
|
|
2583
|
+
};
|
|
2584
|
+
return {
|
|
2585
|
+
payload,
|
|
2586
|
+
encryptedContent
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
async function uploadPresignedObjectIfNeeded(uploadResponse, uploadMode, encryptedContent, fetchImpl = fetch) {
|
|
2590
|
+
if (!uploadResponse.upload_url) {
|
|
2591
|
+
if (uploadMode === "presigned") {
|
|
2592
|
+
throw new Error("Cannot upload storage object: missing presigned upload URL.");
|
|
2593
|
+
}
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
const headers = new Headers(uploadResponse.upload_headers ?? {});
|
|
2597
|
+
if (!headers.has("content-type")) {
|
|
2598
|
+
headers.set("content-type", "application/octet-stream");
|
|
2599
|
+
}
|
|
2600
|
+
const response = await fetchImpl(uploadResponse.upload_url, {
|
|
2601
|
+
method: "PUT",
|
|
2602
|
+
headers,
|
|
2603
|
+
body: new Uint8Array(encryptedContent)
|
|
2604
|
+
});
|
|
2605
|
+
if (!response.ok) {
|
|
2606
|
+
const details = (await response.text()).trim();
|
|
2607
|
+
throw new Error(
|
|
2608
|
+
`Presigned upload failed with status ${response.status}${details ? `: ${details}` : ""}`
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
async function appendStorageUploadLog(upload, homeDir, nowDateFn = () => /* @__PURE__ */ new Date()) {
|
|
2613
|
+
return appendObjectLogLine(
|
|
2614
|
+
[
|
|
2615
|
+
formatTimestamp(nowDateFn()),
|
|
2616
|
+
upload.quote_id,
|
|
2617
|
+
upload.addr,
|
|
2618
|
+
upload.addr_hash ?? "",
|
|
2619
|
+
upload.trans_id ?? "",
|
|
2620
|
+
upload.storage_price?.toString() ?? "",
|
|
2621
|
+
upload.object_id,
|
|
2622
|
+
upload.object_key,
|
|
2623
|
+
upload.provider,
|
|
2624
|
+
upload.bucket_name,
|
|
2625
|
+
upload.location
|
|
2626
|
+
].join(","),
|
|
2627
|
+
homeDir
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
async function maybeCleanupLocalBackupArchive(archivePath) {
|
|
2631
|
+
const flag = process.env.MNEMOSPARK_DELETE_BACKUP_AFTER_UPLOAD;
|
|
2632
|
+
if (!flag) {
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
const normalized = flag.trim().toLowerCase();
|
|
2636
|
+
if (normalized !== "1" && normalized !== "true" && normalized !== "yes" && normalized !== "y") {
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
try {
|
|
2640
|
+
await rm(archivePath, { force: true });
|
|
2641
|
+
} catch {
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
function formatStorageUploadUserMessage(upload, cronJobId) {
|
|
2645
|
+
return [
|
|
2646
|
+
`Your file \`${upload.object_id}\` with key \`${upload.object_key}\` has been stored using \`${upload.provider}\` in \`${upload.bucket_name}\` \`${upload.location}\``,
|
|
2647
|
+
`A cron job \`${cronJobId}\` has been configured to send payment monthly (on the 1st) for storage services. If payment is not sent, your \`${upload.object_id}\` will be deleted after the **${PAYMENT_DELETE_DEADLINE_DAYS}-day deadline** (${PAYMENT_REMINDER_INTERVAL_DAYS}-day billing interval + 2-day grace period).`,
|
|
2648
|
+
"Thank you for using mnemospark!"
|
|
2649
|
+
].join("\n");
|
|
2650
|
+
}
|
|
2651
|
+
function formatStorageDeleteUserMessage(objectKey, cronId, cronDeleted) {
|
|
2652
|
+
const statusLine = cronId ? cronDeleted ? `File \`${objectKey}\` has been deleted from the cloud and the cron job \`${cronId}\` has been deleted from your system.` : `File \`${objectKey}\` has been deleted from the cloud and the cron job \`${cronId}\` was not found in your system.` : `File \`${objectKey}\` has been deleted from the cloud and no matching cron job was found in your system.`;
|
|
2653
|
+
return [statusLine, "Thank you for using mnemospark!"].join("\n");
|
|
2654
|
+
}
|
|
2655
|
+
function extractUploadErrorMessage(error) {
|
|
2656
|
+
if (!(error instanceof Error)) {
|
|
2657
|
+
return null;
|
|
2658
|
+
}
|
|
2659
|
+
const message = error.message.trim();
|
|
2660
|
+
if (!message) {
|
|
2661
|
+
return null;
|
|
2662
|
+
}
|
|
2663
|
+
try {
|
|
2664
|
+
const payload = JSON.parse(message);
|
|
2665
|
+
if (typeof payload.message === "string" && payload.message.trim().length > 0) {
|
|
2666
|
+
return payload.message.trim();
|
|
2667
|
+
}
|
|
2668
|
+
if (typeof payload.error === "string" && payload.error.trim().length > 0) {
|
|
2669
|
+
return payload.error.trim();
|
|
2670
|
+
}
|
|
2671
|
+
if (payload.error && typeof payload.error === "object" && typeof payload.error.message === "string" && payload.error.message.trim().length > 0) {
|
|
2672
|
+
return payload.error.message.trim();
|
|
2673
|
+
}
|
|
2674
|
+
} catch {
|
|
2675
|
+
}
|
|
2676
|
+
return message;
|
|
2677
|
+
}
|
|
2678
|
+
function formatPriceStorageUserMessage(quote) {
|
|
2679
|
+
return [
|
|
2680
|
+
`Your storage quote \`${quote.quote_id}\` is valid for 1 hour, the storage price is \`${quote.storage_price}\` for \`${quote.object_id}\` with file size of \`${quote.object_size_gb}\` in \`${quote.provider}\` \`${quote.location}\``,
|
|
2681
|
+
`If you accept this quote run the command /cloud upload --quote-id \`${quote.quote_id}\` --wallet-address \`${quote.addr}\` --object-id \`${quote.object_id}\` --object-id-hash \`${quote.object_id_hash}\``
|
|
2682
|
+
].join("\n");
|
|
2683
|
+
}
|
|
2684
|
+
function formatStorageLsUserMessage(result, requestedObjectKey) {
|
|
2685
|
+
const objectId = result.object_id ?? result.key;
|
|
2686
|
+
return `${objectId} with ${requestedObjectKey} is ${result.size_bytes} in ${result.bucket}`;
|
|
2687
|
+
}
|
|
2688
|
+
function createCloudCommand(options = {}) {
|
|
2689
|
+
const backupBuilder = options.buildBackupObjectFn ?? buildBackupObject;
|
|
2690
|
+
const requestPriceStorageQuote = options.requestPriceStorageQuoteFn ?? requestPriceStorageViaProxy;
|
|
2691
|
+
const requestStorageUpload = options.requestStorageUploadFn ?? requestStorageUploadViaProxy;
|
|
2692
|
+
const resolveWalletKey = options.resolveWalletPrivateKeyFn ?? resolveWalletPrivateKey;
|
|
2693
|
+
const createPayment = options.createPaymentFetchFn ?? createPaymentFetch;
|
|
2694
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
2695
|
+
const nowDateFn = options.nowDateFn ?? (() => /* @__PURE__ */ new Date());
|
|
2696
|
+
const idempotencyKeyFn = options.idempotencyKeyFn ?? randomUUID;
|
|
2697
|
+
const requestStorageLs = options.requestStorageLsFn ?? requestStorageLsViaProxy;
|
|
2698
|
+
const requestStorageDownload = options.requestStorageDownloadFn ?? requestStorageDownloadViaProxy;
|
|
2699
|
+
const requestStorageDelete = options.requestStorageDeleteFn ?? requestStorageDeleteViaProxy;
|
|
2700
|
+
const objectLogHomeDir = options.objectLogHomeDir ?? options.backupOptions?.homeDir;
|
|
2701
|
+
return {
|
|
2702
|
+
name: "cloud",
|
|
2703
|
+
description: "Manage mnemospark cloud storage workflow commands",
|
|
2704
|
+
acceptsArgs: true,
|
|
2705
|
+
requireAuth: true,
|
|
2706
|
+
handler: async (ctx) => {
|
|
2707
|
+
const parsed = parseCloudArgs(ctx.args);
|
|
2708
|
+
if (parsed.mode === "help" || parsed.mode === "unknown") {
|
|
2709
|
+
return {
|
|
2710
|
+
text: CLOUD_HELP_TEXT,
|
|
2711
|
+
isError: parsed.mode === "unknown"
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
if (parsed.mode === "price-storage-invalid") {
|
|
2715
|
+
return {
|
|
2716
|
+
text: `Cannot price storage: required arguments are ${REQUIRED_PRICE_STORAGE}.`,
|
|
2717
|
+
isError: true
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
if (parsed.mode === "upload-invalid") {
|
|
2721
|
+
return {
|
|
2722
|
+
text: `Cannot upload storage object: required arguments are ${REQUIRED_UPLOAD}.`,
|
|
2723
|
+
isError: true
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
if (parsed.mode === "ls-invalid") {
|
|
2727
|
+
return {
|
|
2728
|
+
text: `Cannot list storage object: required arguments are ${REQUIRED_STORAGE_OBJECT}.`,
|
|
2729
|
+
isError: true
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
if (parsed.mode === "download-invalid") {
|
|
2733
|
+
return {
|
|
2734
|
+
text: `Cannot download file: required arguments are ${REQUIRED_STORAGE_OBJECT}.`,
|
|
2735
|
+
isError: true
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
if (parsed.mode === "delete-invalid") {
|
|
2739
|
+
return {
|
|
2740
|
+
text: `Cannot delete file: required arguments are ${REQUIRED_STORAGE_OBJECT}.`,
|
|
2741
|
+
isError: true
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
if (parsed.mode === "backup") {
|
|
2745
|
+
try {
|
|
2746
|
+
const result = await backupBuilder(parsed.backupTarget, options.backupOptions);
|
|
2747
|
+
return {
|
|
2748
|
+
text: `Your object-id is ${result.objectId} your object-id-hash is ${result.objectIdHash} and your object-size is ${result.objectSizeGb}`
|
|
2749
|
+
};
|
|
2750
|
+
} catch (err) {
|
|
2751
|
+
if (err instanceof UnsupportedBackupPlatformError) {
|
|
2752
|
+
return {
|
|
2753
|
+
text: "Cloud backup is only supported on macOS and Linux.",
|
|
2754
|
+
isError: true
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
return {
|
|
2758
|
+
text: "Cannot build storage object",
|
|
2759
|
+
isError: true
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
if (parsed.mode === "price-storage") {
|
|
2764
|
+
try {
|
|
2765
|
+
const quote = await requestPriceStorageQuote(
|
|
2766
|
+
parsed.priceStorageRequest,
|
|
2767
|
+
options.proxyQuoteOptions
|
|
2768
|
+
);
|
|
2769
|
+
await appendPriceStorageQuoteLog(quote, objectLogHomeDir);
|
|
2770
|
+
return {
|
|
2771
|
+
text: formatPriceStorageUserMessage(quote)
|
|
2772
|
+
};
|
|
2773
|
+
} catch {
|
|
2774
|
+
return {
|
|
2775
|
+
text: "Cannot price storage",
|
|
2776
|
+
isError: true
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
if (parsed.mode === "upload") {
|
|
2781
|
+
try {
|
|
2782
|
+
const loggedQuote = await findLoggedPriceStorageQuote(
|
|
2783
|
+
parsed.uploadRequest.quote_id,
|
|
2784
|
+
objectLogHomeDir
|
|
2785
|
+
);
|
|
2786
|
+
if (!loggedQuote) {
|
|
2787
|
+
return {
|
|
2788
|
+
text: "Cannot upload storage object: quote-id not found in object.log. Run /cloud price-storage first.",
|
|
2789
|
+
isError: true
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
if (loggedQuote.walletAddress.toLowerCase() !== parsed.uploadRequest.wallet_address.toLowerCase() || loggedQuote.objectId !== parsed.uploadRequest.object_id || loggedQuote.objectIdHash.toLowerCase() !== parsed.uploadRequest.object_id_hash.toLowerCase()) {
|
|
2793
|
+
return {
|
|
2794
|
+
text: "Cannot upload storage object: quote details do not match wallet/object arguments.",
|
|
2795
|
+
isError: true
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
const archivePath = join4(
|
|
2799
|
+
options.backupOptions?.tmpDir ?? DEFAULT_BACKUP_DIR,
|
|
2800
|
+
parsed.uploadRequest.object_id
|
|
2801
|
+
);
|
|
2802
|
+
let archiveStats;
|
|
2803
|
+
try {
|
|
2804
|
+
archiveStats = await stat(archivePath);
|
|
2805
|
+
} catch {
|
|
2806
|
+
return {
|
|
2807
|
+
text: `Cannot upload storage object: local archive not found at ${archivePath}. Run /cloud backup first.`,
|
|
2808
|
+
isError: true
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
if (!archiveStats.isFile()) {
|
|
2812
|
+
return {
|
|
2813
|
+
text: `Cannot upload storage object: local archive path is not a file (${archivePath}).`,
|
|
2814
|
+
isError: true
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
const archiveHash = await sha256File(archivePath);
|
|
2818
|
+
if (archiveHash.toLowerCase() !== parsed.uploadRequest.object_id_hash.toLowerCase()) {
|
|
2819
|
+
return {
|
|
2820
|
+
text: "Cannot upload storage object: object-id-hash does not match local archive.",
|
|
2821
|
+
isError: true
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
const walletKey = await resolveWalletKey(objectLogHomeDir);
|
|
2825
|
+
const walletAccount = privateKeyToAccount5(walletKey);
|
|
2826
|
+
if (walletAccount.address.toLowerCase() !== parsed.uploadRequest.wallet_address.toLowerCase()) {
|
|
2827
|
+
return {
|
|
2828
|
+
text: `Cannot upload storage object: wallet key address ${walletAccount.address} does not match --wallet-address ${parsed.uploadRequest.wallet_address}.`,
|
|
2829
|
+
isError: true
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
const preparedPayload = await prepareUploadPayload(
|
|
2833
|
+
archivePath,
|
|
2834
|
+
parsed.uploadRequest.wallet_address,
|
|
2835
|
+
objectLogHomeDir
|
|
2836
|
+
);
|
|
2837
|
+
const paymentFetch = createPayment(walletKey).fetch;
|
|
2838
|
+
const idempotencyKey = idempotencyKeyFn();
|
|
2839
|
+
const uploadResponse = await requestStorageUpload(
|
|
2840
|
+
{
|
|
2841
|
+
quote_id: parsed.uploadRequest.quote_id,
|
|
2842
|
+
wallet_address: parsed.uploadRequest.wallet_address,
|
|
2843
|
+
object_id: parsed.uploadRequest.object_id,
|
|
2844
|
+
object_id_hash: parsed.uploadRequest.object_id_hash,
|
|
2845
|
+
quoted_storage_price: loggedQuote.storagePrice,
|
|
2846
|
+
payload: preparedPayload.payload
|
|
2847
|
+
},
|
|
2848
|
+
{
|
|
2849
|
+
...options.proxyUploadOptions,
|
|
2850
|
+
idempotencyKey,
|
|
2851
|
+
fetchImpl: (input, init) => paymentFetch(input, init)
|
|
2852
|
+
}
|
|
2853
|
+
);
|
|
2854
|
+
await uploadPresignedObjectIfNeeded(
|
|
2855
|
+
uploadResponse,
|
|
2856
|
+
preparedPayload.payload.mode,
|
|
2857
|
+
preparedPayload.encryptedContent,
|
|
2858
|
+
fetchImpl
|
|
2859
|
+
);
|
|
2860
|
+
await appendStorageUploadLog(uploadResponse, objectLogHomeDir, nowDateFn);
|
|
2861
|
+
const cronStoragePriceCandidate = uploadResponse.storage_price ?? loggedQuote.storagePrice;
|
|
2862
|
+
const cronStoragePrice = Number.isFinite(cronStoragePriceCandidate) && cronStoragePriceCandidate > 0 ? cronStoragePriceCandidate : loggedQuote.storagePrice;
|
|
2863
|
+
const cronJob = await createStoragePaymentCronJob(
|
|
2864
|
+
uploadResponse,
|
|
2865
|
+
cronStoragePrice,
|
|
2866
|
+
objectLogHomeDir,
|
|
2867
|
+
nowDateFn
|
|
2868
|
+
);
|
|
2869
|
+
await maybeCleanupLocalBackupArchive(archivePath);
|
|
2870
|
+
return {
|
|
2871
|
+
text: formatStorageUploadUserMessage(uploadResponse, cronJob.cronId)
|
|
2872
|
+
};
|
|
2873
|
+
} catch (error) {
|
|
2874
|
+
return {
|
|
2875
|
+
text: extractUploadErrorMessage(error) ?? "Cannot upload storage object",
|
|
2876
|
+
isError: true
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
if (parsed.mode === "ls") {
|
|
2881
|
+
try {
|
|
2882
|
+
const lsResult = await requestStorageLs(
|
|
2883
|
+
parsed.storageObjectRequest,
|
|
2884
|
+
options.proxyStorageOptions
|
|
2885
|
+
);
|
|
2886
|
+
if (!lsResult.success) {
|
|
2887
|
+
throw new Error("ls failed");
|
|
2888
|
+
}
|
|
2889
|
+
return {
|
|
2890
|
+
text: formatStorageLsUserMessage(lsResult, parsed.storageObjectRequest.object_key)
|
|
2891
|
+
};
|
|
2892
|
+
} catch {
|
|
2893
|
+
return {
|
|
2894
|
+
text: "Cannot list storage object",
|
|
2895
|
+
isError: true
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
if (parsed.mode === "download") {
|
|
2900
|
+
try {
|
|
2901
|
+
const downloadResult = await requestStorageDownload(
|
|
2902
|
+
parsed.storageObjectRequest,
|
|
2903
|
+
options.proxyStorageOptions
|
|
2904
|
+
);
|
|
2905
|
+
if (!downloadResult.success) {
|
|
2906
|
+
throw new Error("download failed");
|
|
2907
|
+
}
|
|
2908
|
+
return {
|
|
2909
|
+
text: `File ${parsed.storageObjectRequest.object_key} downloaded`
|
|
2910
|
+
};
|
|
2911
|
+
} catch {
|
|
2912
|
+
return {
|
|
2913
|
+
text: "Cannot download file",
|
|
2914
|
+
isError: true
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
if (parsed.mode === "delete") {
|
|
2919
|
+
try {
|
|
2920
|
+
const deleteResult = await requestStorageDelete(
|
|
2921
|
+
parsed.storageObjectRequest,
|
|
2922
|
+
options.proxyStorageOptions
|
|
2923
|
+
);
|
|
2924
|
+
if (!deleteResult.success) {
|
|
2925
|
+
throw new Error("delete failed");
|
|
2926
|
+
}
|
|
2927
|
+
} catch {
|
|
2928
|
+
return {
|
|
2929
|
+
text: "Cannot delete file",
|
|
2930
|
+
isError: true
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2933
|
+
let cronEntry = null;
|
|
2934
|
+
let cronDeleted = false;
|
|
2935
|
+
try {
|
|
2936
|
+
cronEntry = await findLoggedStoragePaymentCronByObjectKey(
|
|
2937
|
+
parsed.storageObjectRequest.object_key,
|
|
2938
|
+
objectLogHomeDir
|
|
2939
|
+
);
|
|
2940
|
+
cronDeleted = cronEntry ? await removeStoragePaymentCronJob(cronEntry.cronId, objectLogHomeDir) : false;
|
|
2941
|
+
} catch {
|
|
2942
|
+
}
|
|
2943
|
+
return {
|
|
2944
|
+
text: formatStorageDeleteUserMessage(
|
|
2945
|
+
parsed.storageObjectRequest.object_key,
|
|
2946
|
+
cronEntry?.cronId ?? null,
|
|
2947
|
+
cronDeleted
|
|
2948
|
+
)
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
return {
|
|
2952
|
+
text: CLOUD_HELP_TEXT,
|
|
2953
|
+
isError: true
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// src/retry.ts
|
|
2960
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
2961
|
+
maxRetries: 2,
|
|
2962
|
+
baseDelayMs: 500,
|
|
2963
|
+
retryableCodes: [429, 502, 503, 504]
|
|
2964
|
+
};
|
|
2965
|
+
function sleep(ms) {
|
|
2966
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2967
|
+
}
|
|
2968
|
+
async function fetchWithRetry(fetchFn, url, init, config) {
|
|
2969
|
+
const cfg = {
|
|
2970
|
+
...DEFAULT_RETRY_CONFIG,
|
|
2971
|
+
...config
|
|
2972
|
+
};
|
|
2973
|
+
let lastError;
|
|
2974
|
+
let lastResponse;
|
|
2975
|
+
for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
|
|
2976
|
+
try {
|
|
2977
|
+
const response = await fetchFn(url, init);
|
|
2978
|
+
if (!cfg.retryableCodes.includes(response.status)) {
|
|
2979
|
+
return response;
|
|
2980
|
+
}
|
|
2981
|
+
lastResponse = response;
|
|
2982
|
+
const retryAfter = response.headers.get("retry-after");
|
|
2983
|
+
let delay;
|
|
2984
|
+
if (retryAfter) {
|
|
2985
|
+
const seconds = parseInt(retryAfter, 10);
|
|
2986
|
+
delay = isNaN(seconds) ? cfg.baseDelayMs * Math.pow(2, attempt) : seconds * 1e3;
|
|
2987
|
+
} else {
|
|
2988
|
+
delay = cfg.baseDelayMs * Math.pow(2, attempt);
|
|
2989
|
+
}
|
|
2990
|
+
if (attempt < cfg.maxRetries) {
|
|
2991
|
+
await sleep(delay);
|
|
2992
|
+
}
|
|
2993
|
+
} catch (err) {
|
|
2994
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2995
|
+
if (attempt < cfg.maxRetries) {
|
|
2996
|
+
const delay = cfg.baseDelayMs * Math.pow(2, attempt);
|
|
2997
|
+
await sleep(delay);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
if (lastResponse) {
|
|
3002
|
+
return lastResponse;
|
|
3003
|
+
}
|
|
3004
|
+
throw lastError ?? new Error("Max retries exceeded");
|
|
3005
|
+
}
|
|
3006
|
+
function isRetryable(errorOrResponse, config) {
|
|
3007
|
+
const retryableCodes = config?.retryableCodes ?? DEFAULT_RETRY_CONFIG.retryableCodes;
|
|
3008
|
+
if (errorOrResponse instanceof Response) {
|
|
3009
|
+
return retryableCodes.includes(errorOrResponse.status);
|
|
3010
|
+
}
|
|
3011
|
+
const message = errorOrResponse.message.toLowerCase();
|
|
3012
|
+
return message.includes("network") || message.includes("timeout") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("socket hang up");
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// src/index.ts
|
|
3016
|
+
function isCompletionMode() {
|
|
3017
|
+
const args = process.argv;
|
|
3018
|
+
return args.some((arg, i) => arg === "completion" && i >= 1 && i <= 3);
|
|
3019
|
+
}
|
|
3020
|
+
function isGatewayMode() {
|
|
3021
|
+
const args = process.argv;
|
|
3022
|
+
return args.includes("gateway");
|
|
3023
|
+
}
|
|
3024
|
+
var activeProxyHandle = null;
|
|
3025
|
+
async function startProxyInBackground(api) {
|
|
3026
|
+
const { key: walletKey, address, source } = await resolveOrGenerateWalletKey();
|
|
3027
|
+
if (source === "generated") {
|
|
3028
|
+
api.logger.info(`Generated new wallet: ${address}`);
|
|
3029
|
+
} else if (source === "saved") {
|
|
3030
|
+
api.logger.info(`Using saved wallet: ${address}`);
|
|
3031
|
+
} else {
|
|
3032
|
+
api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
|
|
3033
|
+
}
|
|
3034
|
+
const proxy = await startProxy({
|
|
3035
|
+
walletKey,
|
|
3036
|
+
onReady: (port) => {
|
|
3037
|
+
api.logger.info(`mnemospark proxy listening on port ${port}`);
|
|
3038
|
+
},
|
|
3039
|
+
onError: (error) => {
|
|
3040
|
+
api.logger.error(`mnemospark proxy error: ${error.message}`);
|
|
3041
|
+
},
|
|
3042
|
+
onLowBalance: (info) => {
|
|
3043
|
+
api.logger.warn(`[!] Low balance: ${info.balanceUSD}. Fund wallet: ${info.walletAddress}`);
|
|
3044
|
+
},
|
|
3045
|
+
onInsufficientFunds: (info) => {
|
|
3046
|
+
api.logger.error(
|
|
3047
|
+
`[!] Insufficient funds. Balance: ${info.balanceUSD}, Needed: ${info.requiredUSD}. Fund wallet: ${info.walletAddress}`
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
});
|
|
3051
|
+
activeProxyHandle = proxy;
|
|
3052
|
+
api.logger.info("mnemospark ready");
|
|
3053
|
+
const startupMonitor = new BalanceMonitor(address);
|
|
3054
|
+
startupMonitor.checkBalance().then((balance) => {
|
|
3055
|
+
if (balance.isEmpty) {
|
|
3056
|
+
api.logger.info(`Wallet: ${address} | Balance: $0.00`);
|
|
3057
|
+
} else if (balance.isLow) {
|
|
3058
|
+
api.logger.info(`Wallet: ${address} | Balance: ${balance.balanceUSD} (low)`);
|
|
3059
|
+
} else {
|
|
3060
|
+
api.logger.info(`Wallet: ${address} | Balance: ${balance.balanceUSD}`);
|
|
3061
|
+
}
|
|
3062
|
+
}).catch(() => {
|
|
3063
|
+
api.logger.info(`Wallet: ${address} | Balance: (checking...)`);
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
async function createWalletCommand() {
|
|
3067
|
+
return {
|
|
3068
|
+
name: "wallet",
|
|
3069
|
+
description: "Show mnemospark wallet info or export private key for backup",
|
|
3070
|
+
acceptsArgs: true,
|
|
3071
|
+
requireAuth: true,
|
|
3072
|
+
handler: async (ctx) => {
|
|
3073
|
+
const subcommand = ctx.args?.trim().toLowerCase() || "status";
|
|
3074
|
+
let walletKey;
|
|
3075
|
+
let address;
|
|
3076
|
+
try {
|
|
3077
|
+
if (existsSync(WALLET_FILE)) {
|
|
3078
|
+
walletKey = readFileSync(WALLET_FILE, "utf-8").trim();
|
|
3079
|
+
if (walletKey.startsWith("0x") && walletKey.length === 66) {
|
|
3080
|
+
const account = privateKeyToAccount6(walletKey);
|
|
3081
|
+
address = account.address;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
} catch {
|
|
3085
|
+
}
|
|
3086
|
+
if (!walletKey || !address) {
|
|
3087
|
+
return {
|
|
3088
|
+
text: "No mnemospark wallet found.\n\nRun `openclaw plugins install mnemospark` to generate a wallet.",
|
|
3089
|
+
isError: true
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
if (subcommand === "export") {
|
|
3093
|
+
return {
|
|
3094
|
+
text: [
|
|
3095
|
+
"\u{1F510} **mnemospark Wallet Export**",
|
|
3096
|
+
"",
|
|
3097
|
+
"\u26A0\uFE0F **SECURITY WARNING**: Your private key controls your wallet funds.",
|
|
3098
|
+
"Never share this key. Anyone with this key can spend your USDC.",
|
|
3099
|
+
"",
|
|
3100
|
+
`**Address:** \`${address}\``,
|
|
3101
|
+
"",
|
|
3102
|
+
"**Private Key:**",
|
|
3103
|
+
`\`${walletKey}\``,
|
|
3104
|
+
"",
|
|
3105
|
+
"**To restore on a new machine:**",
|
|
3106
|
+
"1. Set the environment variable before running OpenClaw:",
|
|
3107
|
+
` \`export BLOCKRUN_WALLET_KEY=${walletKey}\``,
|
|
3108
|
+
"2. Or save to file:",
|
|
3109
|
+
` \`mkdir -p ~/.openclaw/blockrun && echo "${walletKey}" > ~/.openclaw/blockrun/wallet.key && chmod 600 ~/.openclaw/blockrun/wallet.key\``
|
|
3110
|
+
].join("\n")
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
let balanceText = "Balance: (checking...)";
|
|
3114
|
+
try {
|
|
3115
|
+
const monitor = new BalanceMonitor(address);
|
|
3116
|
+
const balance = await monitor.checkBalance();
|
|
3117
|
+
balanceText = `Balance: ${balance.balanceUSD}`;
|
|
3118
|
+
} catch {
|
|
3119
|
+
balanceText = "Balance: (could not check)";
|
|
3120
|
+
}
|
|
3121
|
+
return {
|
|
3122
|
+
text: [
|
|
3123
|
+
"\u{1F99E} **mnemospark Wallet**",
|
|
3124
|
+
"",
|
|
3125
|
+
`**Address:** \`${address}\``,
|
|
3126
|
+
`**${balanceText}**`,
|
|
3127
|
+
`**Key File:** \`${WALLET_FILE}\``,
|
|
3128
|
+
"",
|
|
3129
|
+
"**Commands:**",
|
|
3130
|
+
"\u2022 `/wallet` - Show this status",
|
|
3131
|
+
"\u2022 `/wallet export` - Export private key for backup",
|
|
3132
|
+
"",
|
|
3133
|
+
`**Fund with USDC on Base:** https://basescan.org/address/${address}`
|
|
3134
|
+
].join("\n")
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
var plugin = {
|
|
3140
|
+
id: "mnemospark",
|
|
3141
|
+
name: "mnemospark",
|
|
3142
|
+
description: "mnemospark storage and wallet plugin",
|
|
3143
|
+
version: VERSION,
|
|
3144
|
+
async register(api) {
|
|
3145
|
+
const isDisabled = process.env.MNEMOSPARK_DISABLED === "true" || process.env.MNEMOSPARK_DISABLED === "1";
|
|
3146
|
+
if (isDisabled) {
|
|
3147
|
+
api.logger.info("mnemospark disabled (MNEMOSPARK_DISABLED=true).");
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
if (isCompletionMode()) {
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
createWalletCommand().then((walletCommand) => {
|
|
3154
|
+
api.registerCommand(walletCommand);
|
|
3155
|
+
}).catch((err) => {
|
|
3156
|
+
api.logger.warn(
|
|
3157
|
+
`Failed to register /wallet command: ${err instanceof Error ? err.message : String(err)}`
|
|
3158
|
+
);
|
|
3159
|
+
});
|
|
3160
|
+
try {
|
|
3161
|
+
api.registerCommand(createCloudCommand());
|
|
3162
|
+
} catch (err) {
|
|
3163
|
+
api.logger.warn(
|
|
3164
|
+
`Failed to register /cloud command: ${err instanceof Error ? err.message : String(err)}`
|
|
3165
|
+
);
|
|
3166
|
+
}
|
|
3167
|
+
api.registerService({
|
|
3168
|
+
id: "mnemospark-proxy",
|
|
3169
|
+
start: () => {
|
|
3170
|
+
},
|
|
3171
|
+
stop: async () => {
|
|
3172
|
+
if (activeProxyHandle) {
|
|
3173
|
+
try {
|
|
3174
|
+
await activeProxyHandle.close();
|
|
3175
|
+
api.logger.info("mnemospark proxy closed");
|
|
3176
|
+
} catch (err) {
|
|
3177
|
+
api.logger.warn(
|
|
3178
|
+
`Error closing proxy: ${err instanceof Error ? err.message : String(err)}`
|
|
3179
|
+
);
|
|
3180
|
+
} finally {
|
|
3181
|
+
activeProxyHandle = null;
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
});
|
|
3186
|
+
if (!isGatewayMode()) {
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
startProxyInBackground(api).catch((err) => {
|
|
3190
|
+
api.logger.error(
|
|
3191
|
+
`Failed to start mnemospark proxy: ${err instanceof Error ? err.message : String(err)}`
|
|
3192
|
+
);
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
};
|
|
3196
|
+
var index_default = plugin;
|
|
3197
|
+
export {
|
|
3198
|
+
BALANCE_THRESHOLDS,
|
|
3199
|
+
BalanceMonitor,
|
|
3200
|
+
DEFAULT_RETRY_CONFIG,
|
|
3201
|
+
EmptyWalletError,
|
|
3202
|
+
InsufficientFundsError,
|
|
3203
|
+
PaymentCache,
|
|
3204
|
+
RpcError,
|
|
3205
|
+
createCloudCommand,
|
|
3206
|
+
createPaymentFetch,
|
|
3207
|
+
index_default as default,
|
|
3208
|
+
fetchWithRetry,
|
|
3209
|
+
getProxyPort,
|
|
3210
|
+
isBalanceError,
|
|
3211
|
+
isEmptyWalletError,
|
|
3212
|
+
isInsufficientFundsError,
|
|
3213
|
+
isRetryable,
|
|
3214
|
+
isRpcError,
|
|
3215
|
+
startProxy
|
|
3216
|
+
};
|
|
3217
|
+
//# sourceMappingURL=index.js.map
|