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/cli.js
ADDED
|
@@ -0,0 +1,1643 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/proxy.ts
|
|
4
|
+
import { createServer } from "http";
|
|
5
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
6
|
+
|
|
7
|
+
// src/balance.ts
|
|
8
|
+
import { createPublicClient, http, erc20Abi } from "viem";
|
|
9
|
+
import { base } from "viem/chains";
|
|
10
|
+
|
|
11
|
+
// src/errors.ts
|
|
12
|
+
var RpcError = class extends Error {
|
|
13
|
+
code = "RPC_ERROR";
|
|
14
|
+
originalError;
|
|
15
|
+
constructor(message, originalError) {
|
|
16
|
+
super(`RPC error: ${message}. Check network connectivity.`);
|
|
17
|
+
this.name = "RpcError";
|
|
18
|
+
this.originalError = originalError;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/balance.ts
|
|
23
|
+
var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
24
|
+
var CACHE_TTL_MS = 3e4;
|
|
25
|
+
var BALANCE_THRESHOLDS = {
|
|
26
|
+
/** Low balance warning threshold: $1.00 */
|
|
27
|
+
LOW_BALANCE_MICROS: 1000000n,
|
|
28
|
+
/** Effectively zero threshold: $0.0001 (covers dust/rounding) */
|
|
29
|
+
ZERO_THRESHOLD: 100n
|
|
30
|
+
};
|
|
31
|
+
var BalanceMonitor = class {
|
|
32
|
+
client;
|
|
33
|
+
walletAddress;
|
|
34
|
+
/** Cached balance (null = not yet fetched) */
|
|
35
|
+
cachedBalance = null;
|
|
36
|
+
/** Timestamp when cache was last updated */
|
|
37
|
+
cachedAt = 0;
|
|
38
|
+
constructor(walletAddress) {
|
|
39
|
+
this.walletAddress = walletAddress;
|
|
40
|
+
this.client = createPublicClient({
|
|
41
|
+
chain: base,
|
|
42
|
+
transport: http(void 0, {
|
|
43
|
+
timeout: 1e4
|
|
44
|
+
// 10 second timeout to prevent hanging on slow RPC
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check current USDC balance.
|
|
50
|
+
* Uses cache if valid, otherwise fetches from RPC.
|
|
51
|
+
*/
|
|
52
|
+
async checkBalance() {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
if (this.cachedBalance !== null && now - this.cachedAt < CACHE_TTL_MS) {
|
|
55
|
+
return this.buildInfo(this.cachedBalance);
|
|
56
|
+
}
|
|
57
|
+
const balance = await this.fetchBalance();
|
|
58
|
+
this.cachedBalance = balance;
|
|
59
|
+
this.cachedAt = now;
|
|
60
|
+
return this.buildInfo(balance);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Check if balance is sufficient for an estimated cost.
|
|
64
|
+
*
|
|
65
|
+
* @param estimatedCostMicros - Estimated cost in USDC smallest unit (6 decimals)
|
|
66
|
+
*/
|
|
67
|
+
async checkSufficient(estimatedCostMicros) {
|
|
68
|
+
const info = await this.checkBalance();
|
|
69
|
+
if (info.balance >= estimatedCostMicros) {
|
|
70
|
+
return { sufficient: true, info };
|
|
71
|
+
}
|
|
72
|
+
const shortfall = estimatedCostMicros - info.balance;
|
|
73
|
+
return {
|
|
74
|
+
sufficient: false,
|
|
75
|
+
info,
|
|
76
|
+
shortfall: this.formatUSDC(shortfall)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Optimistically deduct estimated cost from cached balance.
|
|
81
|
+
* Call this after a successful payment to keep cache accurate.
|
|
82
|
+
*
|
|
83
|
+
* @param amountMicros - Amount to deduct in USDC smallest unit
|
|
84
|
+
*/
|
|
85
|
+
deductEstimated(amountMicros) {
|
|
86
|
+
if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
|
|
87
|
+
this.cachedBalance -= amountMicros;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Invalidate cache, forcing next checkBalance() to fetch from RPC.
|
|
92
|
+
* Call this after a payment failure to get accurate balance.
|
|
93
|
+
*/
|
|
94
|
+
invalidate() {
|
|
95
|
+
this.cachedBalance = null;
|
|
96
|
+
this.cachedAt = 0;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Force refresh balance from RPC (ignores cache).
|
|
100
|
+
*/
|
|
101
|
+
async refresh() {
|
|
102
|
+
this.invalidate();
|
|
103
|
+
return this.checkBalance();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Format USDC amount (in micros) as "$X.XX".
|
|
107
|
+
*/
|
|
108
|
+
formatUSDC(amountMicros) {
|
|
109
|
+
const dollars = Number(amountMicros) / 1e6;
|
|
110
|
+
return `$${dollars.toFixed(2)}`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get the wallet address being monitored.
|
|
114
|
+
*/
|
|
115
|
+
getWalletAddress() {
|
|
116
|
+
return this.walletAddress;
|
|
117
|
+
}
|
|
118
|
+
/** Fetch balance from RPC */
|
|
119
|
+
async fetchBalance() {
|
|
120
|
+
try {
|
|
121
|
+
const balance = await this.client.readContract({
|
|
122
|
+
address: USDC_BASE,
|
|
123
|
+
abi: erc20Abi,
|
|
124
|
+
functionName: "balanceOf",
|
|
125
|
+
args: [this.walletAddress]
|
|
126
|
+
});
|
|
127
|
+
return balance;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new RpcError(error instanceof Error ? error.message : "Unknown error", error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Build BalanceInfo from raw balance */
|
|
133
|
+
buildInfo(balance) {
|
|
134
|
+
return {
|
|
135
|
+
balance,
|
|
136
|
+
balanceUSD: this.formatUSDC(balance),
|
|
137
|
+
isLow: balance < BALANCE_THRESHOLDS.LOW_BALANCE_MICROS,
|
|
138
|
+
isEmpty: balance < BALANCE_THRESHOLDS.ZERO_THRESHOLD,
|
|
139
|
+
walletAddress: this.walletAddress
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/config.ts
|
|
145
|
+
var DEFAULT_PORT = 7120;
|
|
146
|
+
var PROXY_PORT = (() => {
|
|
147
|
+
const envPort = process.env.MNEMOSPARK_PROXY_PORT;
|
|
148
|
+
if (envPort) {
|
|
149
|
+
const parsed = parseInt(envPort, 10);
|
|
150
|
+
if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
|
|
151
|
+
return parsed;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return DEFAULT_PORT;
|
|
155
|
+
})();
|
|
156
|
+
var MNEMOSPARK_BACKEND_API_BASE_URL = (process.env.MNEMOSPARK_BACKEND_API_BASE_URL ?? "").trim();
|
|
157
|
+
|
|
158
|
+
// src/mnemospark-request-sign.ts
|
|
159
|
+
import { getAddress } from "viem";
|
|
160
|
+
import { privateKeyToAccount, signTypedData } from "viem/accounts";
|
|
161
|
+
|
|
162
|
+
// src/nonce.ts
|
|
163
|
+
function createNonce() {
|
|
164
|
+
const bytes = new Uint8Array(32);
|
|
165
|
+
crypto.getRandomValues(bytes);
|
|
166
|
+
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/mnemospark-request-sign.ts
|
|
170
|
+
var MNEMOSPARK_DOMAIN_NAME = "Mnemospark";
|
|
171
|
+
var MNEMOSPARK_DOMAIN_VERSION = "1";
|
|
172
|
+
var MNEMOSPARK_VERIFYING_CONTRACT = "0x0000000000000000000000000000000000000001";
|
|
173
|
+
var BASE_MAINNET_CHAIN_ID = 8453;
|
|
174
|
+
var BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
175
|
+
var MNEMOSPARK_REQUEST_TYPES = {
|
|
176
|
+
MnemosparkRequest: [
|
|
177
|
+
{ name: "method", type: "string" },
|
|
178
|
+
{ name: "path", type: "string" },
|
|
179
|
+
{ name: "walletAddress", type: "string" },
|
|
180
|
+
{ name: "nonce", type: "string" },
|
|
181
|
+
{ name: "timestamp", type: "string" }
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
function encodeBase64Json(value) {
|
|
185
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
186
|
+
}
|
|
187
|
+
function normalizeMethod(method) {
|
|
188
|
+
const normalized = method.trim().toUpperCase();
|
|
189
|
+
if (!normalized) {
|
|
190
|
+
throw new Error("Request signing requires a non-empty HTTP method.");
|
|
191
|
+
}
|
|
192
|
+
return normalized;
|
|
193
|
+
}
|
|
194
|
+
function normalizePath(path) {
|
|
195
|
+
const trimmed = path.trim();
|
|
196
|
+
if (!trimmed) {
|
|
197
|
+
throw new Error("Request signing requires a non-empty path.");
|
|
198
|
+
}
|
|
199
|
+
let parsedPath;
|
|
200
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
201
|
+
parsedPath = new URL(trimmed).pathname;
|
|
202
|
+
} else {
|
|
203
|
+
parsedPath = trimmed.split("?")[0]?.split("#")[0] ?? "";
|
|
204
|
+
}
|
|
205
|
+
if (!parsedPath) {
|
|
206
|
+
throw new Error("Request signing requires a valid request path.");
|
|
207
|
+
}
|
|
208
|
+
const prefixed = parsedPath.startsWith("/") ? parsedPath : `/${parsedPath}`;
|
|
209
|
+
const deduplicated = prefixed.replace(/\/{2,}/g, "/");
|
|
210
|
+
return deduplicated.length > 1 && deduplicated.endsWith("/") ? deduplicated.slice(0, -1) : deduplicated;
|
|
211
|
+
}
|
|
212
|
+
function normalizeTimestamp(value) {
|
|
213
|
+
const timestamp = value ?? Math.floor(Date.now() / 1e3).toString();
|
|
214
|
+
if (!/^\d+$/.test(timestamp)) {
|
|
215
|
+
throw new Error("Request signing timestamp must be a Unix timestamp in seconds.");
|
|
216
|
+
}
|
|
217
|
+
return timestamp;
|
|
218
|
+
}
|
|
219
|
+
function normalizeNonce(value) {
|
|
220
|
+
const nonce = value ?? createNonce();
|
|
221
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(nonce)) {
|
|
222
|
+
throw new Error("Request signing nonce must be a 32-byte hex value.");
|
|
223
|
+
}
|
|
224
|
+
return nonce;
|
|
225
|
+
}
|
|
226
|
+
function normalizeChainId(chainId) {
|
|
227
|
+
const selected = chainId ?? BASE_MAINNET_CHAIN_ID;
|
|
228
|
+
if (selected !== BASE_MAINNET_CHAIN_ID && selected !== BASE_SEPOLIA_CHAIN_ID) {
|
|
229
|
+
throw new Error(`Unsupported chainId for request signing: ${selected}`);
|
|
230
|
+
}
|
|
231
|
+
return selected;
|
|
232
|
+
}
|
|
233
|
+
function createMnemosparkRequestDomain(chainId) {
|
|
234
|
+
return {
|
|
235
|
+
name: MNEMOSPARK_DOMAIN_NAME,
|
|
236
|
+
version: MNEMOSPARK_DOMAIN_VERSION,
|
|
237
|
+
chainId: normalizeChainId(chainId),
|
|
238
|
+
verifyingContract: MNEMOSPARK_VERIFYING_CONTRACT
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function createMnemosparkRequestPayload(method, path, walletAddress, options) {
|
|
242
|
+
return {
|
|
243
|
+
method: normalizeMethod(method),
|
|
244
|
+
path: normalizePath(path),
|
|
245
|
+
walletAddress: getAddress(walletAddress),
|
|
246
|
+
nonce: normalizeNonce(options?.nonce),
|
|
247
|
+
timestamp: normalizeTimestamp(options?.timestamp)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function createWalletSignatureHeaderValue(method, path, walletAddress, walletPrivateKey, options) {
|
|
251
|
+
const payload = createMnemosparkRequestPayload(method, path, walletAddress, {
|
|
252
|
+
nonce: options?.nonce,
|
|
253
|
+
timestamp: options?.timestamp
|
|
254
|
+
});
|
|
255
|
+
const signer = privateKeyToAccount(walletPrivateKey);
|
|
256
|
+
if (signer.address.toLowerCase() !== payload.walletAddress.toLowerCase()) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`Wallet address ${payload.walletAddress} does not match signer address ${signer.address}.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const signature = await signTypedData({
|
|
262
|
+
privateKey: walletPrivateKey,
|
|
263
|
+
domain: createMnemosparkRequestDomain(options?.chainId),
|
|
264
|
+
types: MNEMOSPARK_REQUEST_TYPES,
|
|
265
|
+
primaryType: "MnemosparkRequest",
|
|
266
|
+
message: payload
|
|
267
|
+
});
|
|
268
|
+
const headerEnvelope = {
|
|
269
|
+
payloadB64: encodeBase64Json(payload),
|
|
270
|
+
signature,
|
|
271
|
+
address: signer.address
|
|
272
|
+
};
|
|
273
|
+
return encodeBase64Json(headerEnvelope);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/cloud-utils.ts
|
|
277
|
+
function normalizeBaseUrl(baseUrl) {
|
|
278
|
+
return baseUrl.replace(/\/+$/, "");
|
|
279
|
+
}
|
|
280
|
+
function asRecord(value) {
|
|
281
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
function asNumber(value) {
|
|
287
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
291
|
+
const parsed = Number.parseFloat(value);
|
|
292
|
+
if (Number.isFinite(parsed)) {
|
|
293
|
+
return parsed;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
function asNonEmptyString(value) {
|
|
299
|
+
if (typeof value !== "string") {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const trimmed = value.trim();
|
|
303
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
304
|
+
}
|
|
305
|
+
function normalizePaymentRequired(headers) {
|
|
306
|
+
return headers.get("PAYMENT-REQUIRED") ?? headers.get("x-payment-required") ?? void 0;
|
|
307
|
+
}
|
|
308
|
+
function normalizePaymentResponse(headers) {
|
|
309
|
+
return headers.get("PAYMENT-RESPONSE") ?? headers.get("x-payment-response") ?? void 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/wallet-signature.ts
|
|
313
|
+
function normalizeWalletSignature(value) {
|
|
314
|
+
const trimmed = value?.trim();
|
|
315
|
+
return trimmed && trimmed.length > 0 ? trimmed : void 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/cloud-price-storage.ts
|
|
319
|
+
var PRICE_STORAGE_PROXY_PATH = "/mnemospark/price-storage";
|
|
320
|
+
var UPLOAD_PROXY_PATH = "/mnemospark/upload";
|
|
321
|
+
function parsePriceStorageQuoteRequest(payload) {
|
|
322
|
+
const record = asRecord(payload);
|
|
323
|
+
if (!record) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const walletAddress = asNonEmptyString(record.wallet_address);
|
|
327
|
+
const objectId = asNonEmptyString(record.object_id);
|
|
328
|
+
const objectIdHash = asNonEmptyString(record.object_id_hash);
|
|
329
|
+
const gb = asNumber(record.gb);
|
|
330
|
+
const provider = asNonEmptyString(record.provider);
|
|
331
|
+
const region = asNonEmptyString(record.region);
|
|
332
|
+
if (!walletAddress || !objectId || !objectIdHash || gb === null || !provider || !region) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
wallet_address: walletAddress,
|
|
337
|
+
object_id: objectId,
|
|
338
|
+
object_id_hash: objectIdHash,
|
|
339
|
+
gb,
|
|
340
|
+
provider,
|
|
341
|
+
region
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function parseStorageUploadRequest(payload) {
|
|
345
|
+
const record = asRecord(payload);
|
|
346
|
+
if (!record) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
const quoteId = asNonEmptyString(record.quote_id);
|
|
350
|
+
const walletAddress = asNonEmptyString(record.wallet_address);
|
|
351
|
+
const objectId = asNonEmptyString(record.object_id);
|
|
352
|
+
const objectIdHash = asNonEmptyString(record.object_id_hash);
|
|
353
|
+
const quotedStoragePrice = asNumber(record.quoted_storage_price);
|
|
354
|
+
const payloadRecord = asRecord(record.payload);
|
|
355
|
+
if (!payloadRecord) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const modeRaw = asNonEmptyString(payloadRecord.mode);
|
|
359
|
+
const mode = modeRaw === "inline" || modeRaw === "presigned" ? modeRaw : null;
|
|
360
|
+
const contentBase64 = payloadRecord.content_base64 === void 0 ? void 0 : asNonEmptyString(payloadRecord.content_base64);
|
|
361
|
+
const contentSha256 = asNonEmptyString(payloadRecord.content_sha256);
|
|
362
|
+
const contentLengthBytes = asNumber(payloadRecord.content_length_bytes);
|
|
363
|
+
const wrappedDek = asNonEmptyString(payloadRecord.wrapped_dek);
|
|
364
|
+
const encryptionAlgorithm = asNonEmptyString(payloadRecord.encryption_algorithm);
|
|
365
|
+
const bucketNameHint = asNonEmptyString(payloadRecord.bucket_name_hint);
|
|
366
|
+
const keyStorePathHint = asNonEmptyString(payloadRecord.key_store_path_hint);
|
|
367
|
+
if (!quoteId || !walletAddress || !objectId || !objectIdHash || quotedStoragePrice === null || !mode || !contentSha256 || contentLengthBytes === null || !wrappedDek || encryptionAlgorithm !== "AES-256-GCM" || !bucketNameHint || !keyStorePathHint) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
if (mode === "inline" && !contentBase64) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
quote_id: quoteId,
|
|
375
|
+
wallet_address: walletAddress,
|
|
376
|
+
object_id: objectId,
|
|
377
|
+
object_id_hash: objectIdHash,
|
|
378
|
+
quoted_storage_price: quotedStoragePrice,
|
|
379
|
+
payload: {
|
|
380
|
+
mode,
|
|
381
|
+
content_base64: contentBase64 ?? void 0,
|
|
382
|
+
content_sha256: contentSha256,
|
|
383
|
+
content_length_bytes: contentLengthBytes,
|
|
384
|
+
wrapped_dek: wrappedDek,
|
|
385
|
+
encryption_algorithm: "AES-256-GCM",
|
|
386
|
+
bucket_name_hint: bucketNameHint,
|
|
387
|
+
key_store_path_hint: keyStorePathHint
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async function forwardPriceStorageToBackend(request, options = {}) {
|
|
392
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
393
|
+
const backendBaseUrl = (options.backendBaseUrl ?? "").trim();
|
|
394
|
+
const walletSignature = normalizeWalletSignature(options.walletSignature);
|
|
395
|
+
if (!backendBaseUrl) {
|
|
396
|
+
throw new Error("MNEMOSPARK_BACKEND_API_BASE_URL is not configured");
|
|
397
|
+
}
|
|
398
|
+
const headers = {
|
|
399
|
+
"Content-Type": "application/json"
|
|
400
|
+
};
|
|
401
|
+
if (walletSignature) {
|
|
402
|
+
headers["X-Wallet-Signature"] = walletSignature;
|
|
403
|
+
}
|
|
404
|
+
const targetUrl = `${normalizeBaseUrl(backendBaseUrl)}/price-storage`;
|
|
405
|
+
const response = await fetchImpl(targetUrl, {
|
|
406
|
+
method: "POST",
|
|
407
|
+
headers,
|
|
408
|
+
body: JSON.stringify(request)
|
|
409
|
+
});
|
|
410
|
+
return {
|
|
411
|
+
status: response.status,
|
|
412
|
+
bodyText: await response.text(),
|
|
413
|
+
contentType: response.headers.get("content-type") ?? "application/json",
|
|
414
|
+
paymentRequired: normalizePaymentRequired(response.headers),
|
|
415
|
+
paymentResponse: normalizePaymentResponse(response.headers)
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function forwardStorageUploadToBackend(request, options = {}) {
|
|
419
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
420
|
+
const backendBaseUrl = (options.backendBaseUrl ?? "").trim();
|
|
421
|
+
const walletSignature = normalizeWalletSignature(options.walletSignature);
|
|
422
|
+
if (!backendBaseUrl) {
|
|
423
|
+
throw new Error("MNEMOSPARK_BACKEND_API_BASE_URL is not configured");
|
|
424
|
+
}
|
|
425
|
+
if (!walletSignature) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
"Wallet required for storage endpoints: wallet key must be present to sign requests."
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
const requestHeaders = {
|
|
431
|
+
"Content-Type": "application/json",
|
|
432
|
+
"X-Wallet-Signature": walletSignature
|
|
433
|
+
};
|
|
434
|
+
if (options.idempotencyKey && options.idempotencyKey.trim().length > 0) {
|
|
435
|
+
requestHeaders["Idempotency-Key"] = options.idempotencyKey.trim();
|
|
436
|
+
}
|
|
437
|
+
const paymentSignature = options.paymentSignature?.trim();
|
|
438
|
+
const legacyPayment = options.legacyPayment?.trim();
|
|
439
|
+
if (paymentSignature) {
|
|
440
|
+
requestHeaders["PAYMENT-SIGNATURE"] = paymentSignature;
|
|
441
|
+
requestHeaders["x-payment"] = paymentSignature;
|
|
442
|
+
}
|
|
443
|
+
if (legacyPayment) {
|
|
444
|
+
requestHeaders["x-payment"] = legacyPayment;
|
|
445
|
+
requestHeaders["PAYMENT-SIGNATURE"] = requestHeaders["PAYMENT-SIGNATURE"] ?? legacyPayment;
|
|
446
|
+
}
|
|
447
|
+
const targetUrl = `${normalizeBaseUrl(backendBaseUrl)}/storage/upload`;
|
|
448
|
+
const response = await fetchImpl(targetUrl, {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: requestHeaders,
|
|
451
|
+
body: JSON.stringify(request)
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
status: response.status,
|
|
455
|
+
bodyText: await response.text(),
|
|
456
|
+
contentType: response.headers.get("content-type") ?? "application/json",
|
|
457
|
+
paymentRequired: normalizePaymentRequired(response.headers),
|
|
458
|
+
paymentResponse: normalizePaymentResponse(response.headers)
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/cloud-storage.ts
|
|
463
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
464
|
+
import { dirname, join, resolve, sep } from "path";
|
|
465
|
+
var STORAGE_LS_PROXY_PATH = "/mnemospark/storage/ls";
|
|
466
|
+
var STORAGE_DOWNLOAD_PROXY_PATH = "/mnemospark/storage/download";
|
|
467
|
+
var STORAGE_DELETE_PROXY_PATH = "/mnemospark/storage/delete";
|
|
468
|
+
function parseJsonText(text, errorMessage) {
|
|
469
|
+
let parsed;
|
|
470
|
+
try {
|
|
471
|
+
parsed = JSON.parse(text);
|
|
472
|
+
} catch {
|
|
473
|
+
throw new Error(errorMessage);
|
|
474
|
+
}
|
|
475
|
+
const record = asRecord(parsed);
|
|
476
|
+
if (!record) {
|
|
477
|
+
throw new Error(errorMessage);
|
|
478
|
+
}
|
|
479
|
+
return record;
|
|
480
|
+
}
|
|
481
|
+
function sanitizeObjectKeyToRelativePath(objectKey) {
|
|
482
|
+
const normalized = objectKey.replace(/\\/g, "/").trim().replace(/^\/+/, "");
|
|
483
|
+
const segments = normalized.split("/").filter((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
484
|
+
if (segments.length === 0) {
|
|
485
|
+
return "downloaded-object";
|
|
486
|
+
}
|
|
487
|
+
return join(...segments);
|
|
488
|
+
}
|
|
489
|
+
function resolveDownloadPath(outputDir, objectKey) {
|
|
490
|
+
const resolvedOutputDir = resolve(outputDir);
|
|
491
|
+
const relativeObjectPath = sanitizeObjectKeyToRelativePath(objectKey);
|
|
492
|
+
const resolvedTargetPath = resolve(resolvedOutputDir, relativeObjectPath);
|
|
493
|
+
if (resolvedTargetPath !== resolvedOutputDir && !resolvedTargetPath.startsWith(`${resolvedOutputDir}${sep}`)) {
|
|
494
|
+
throw new Error("Resolved download target escapes output directory");
|
|
495
|
+
}
|
|
496
|
+
return resolvedTargetPath;
|
|
497
|
+
}
|
|
498
|
+
function parseFilenameFromContentDisposition(contentDisposition) {
|
|
499
|
+
if (!contentDisposition) {
|
|
500
|
+
return void 0;
|
|
501
|
+
}
|
|
502
|
+
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
|
503
|
+
if (utf8Match?.[1]) {
|
|
504
|
+
try {
|
|
505
|
+
return decodeURIComponent(utf8Match[1]);
|
|
506
|
+
} catch {
|
|
507
|
+
return utf8Match[1];
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const quotedMatch = contentDisposition.match(/filename="([^"]+)"/i);
|
|
511
|
+
if (quotedMatch?.[1]) {
|
|
512
|
+
return quotedMatch[1];
|
|
513
|
+
}
|
|
514
|
+
const plainMatch = contentDisposition.match(/filename=([^;]+)/i);
|
|
515
|
+
if (plainMatch?.[1]) {
|
|
516
|
+
return plainMatch[1].trim();
|
|
517
|
+
}
|
|
518
|
+
return void 0;
|
|
519
|
+
}
|
|
520
|
+
async function forwardStorageToBackend(path, method, request, options = {}) {
|
|
521
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
522
|
+
const backendBaseUrl = (options.backendBaseUrl ?? "").trim();
|
|
523
|
+
const walletSignature = normalizeWalletSignature(options.walletSignature);
|
|
524
|
+
if (!backendBaseUrl) {
|
|
525
|
+
throw new Error("MNEMOSPARK_BACKEND_API_BASE_URL is not configured");
|
|
526
|
+
}
|
|
527
|
+
if (!walletSignature) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
"Wallet required for storage endpoints: wallet key must be present to sign requests."
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
const targetUrl = `${normalizeBaseUrl(backendBaseUrl)}${path}`;
|
|
533
|
+
const response = await fetchImpl(targetUrl, {
|
|
534
|
+
method,
|
|
535
|
+
headers: {
|
|
536
|
+
"Content-Type": "application/json",
|
|
537
|
+
"X-Wallet-Signature": walletSignature
|
|
538
|
+
},
|
|
539
|
+
body: JSON.stringify(request)
|
|
540
|
+
});
|
|
541
|
+
const bodyBuffer = Buffer.from(await response.arrayBuffer());
|
|
542
|
+
return {
|
|
543
|
+
status: response.status,
|
|
544
|
+
bodyText: bodyBuffer.toString("utf-8"),
|
|
545
|
+
bodyBuffer,
|
|
546
|
+
contentType: response.headers.get("content-type") ?? "application/octet-stream",
|
|
547
|
+
contentDisposition: response.headers.get("content-disposition") ?? void 0,
|
|
548
|
+
paymentRequired: normalizePaymentRequired(response.headers),
|
|
549
|
+
paymentResponse: normalizePaymentResponse(response.headers)
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function parseStorageObjectRequest(payload) {
|
|
553
|
+
const record = asRecord(payload);
|
|
554
|
+
if (!record) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
const walletAddress = asNonEmptyString(record.wallet_address);
|
|
558
|
+
const objectKey = asNonEmptyString(record.object_key);
|
|
559
|
+
const location = asNonEmptyString(record.location) ?? void 0;
|
|
560
|
+
if (!walletAddress || !objectKey) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
564
|
+
wallet_address: walletAddress,
|
|
565
|
+
object_key: objectKey,
|
|
566
|
+
location
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
async function forwardStorageLsToBackend(request, options = {}) {
|
|
570
|
+
return forwardStorageToBackend("/storage/ls", "POST", request, options);
|
|
571
|
+
}
|
|
572
|
+
async function forwardStorageDownloadToBackend(request, options = {}) {
|
|
573
|
+
return forwardStorageToBackend("/storage/download", "POST", request, options);
|
|
574
|
+
}
|
|
575
|
+
async function forwardStorageDeleteToBackend(request, options = {}) {
|
|
576
|
+
return forwardStorageToBackend("/storage/delete", "POST", request, options);
|
|
577
|
+
}
|
|
578
|
+
async function downloadStorageToDisk(request, backendResponse, options = {}) {
|
|
579
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
580
|
+
const outputDir = options.outputDir ?? process.cwd();
|
|
581
|
+
let objectKey = request.object_key;
|
|
582
|
+
let bytes = backendResponse.bodyBuffer;
|
|
583
|
+
const contentType = backendResponse.contentType.toLowerCase();
|
|
584
|
+
if (contentType.includes("application/json")) {
|
|
585
|
+
const payload = parseJsonText(
|
|
586
|
+
backendResponse.bodyText,
|
|
587
|
+
"Download response was JSON but not parseable"
|
|
588
|
+
);
|
|
589
|
+
const payloadObjectKey = asNonEmptyString(payload.object_key) ?? asNonEmptyString(payload.key) ?? asNonEmptyString(payload.object_id);
|
|
590
|
+
const downloadUrl = asNonEmptyString(payload.download_url);
|
|
591
|
+
const inlineContent = asNonEmptyString(payload.content) ?? asNonEmptyString(payload.body_base64) ?? asNonEmptyString(payload.data);
|
|
592
|
+
if (payloadObjectKey) {
|
|
593
|
+
objectKey = payloadObjectKey;
|
|
594
|
+
}
|
|
595
|
+
if (downloadUrl) {
|
|
596
|
+
const fileResponse = await fetchImpl(downloadUrl, { method: "GET" });
|
|
597
|
+
if (!fileResponse.ok) {
|
|
598
|
+
throw new Error(`Presigned download failed with status ${fileResponse.status}`);
|
|
599
|
+
}
|
|
600
|
+
bytes = Buffer.from(await fileResponse.arrayBuffer());
|
|
601
|
+
} else if (inlineContent) {
|
|
602
|
+
bytes = Buffer.from(inlineContent, "base64");
|
|
603
|
+
} else {
|
|
604
|
+
throw new Error("Download response did not include download_url or inline content");
|
|
605
|
+
}
|
|
606
|
+
} else {
|
|
607
|
+
const filenameFromHeader = parseFilenameFromContentDisposition(
|
|
608
|
+
backendResponse.contentDisposition
|
|
609
|
+
);
|
|
610
|
+
if (filenameFromHeader) {
|
|
611
|
+
objectKey = filenameFromHeader;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const filePath = resolveDownloadPath(outputDir, objectKey);
|
|
615
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
616
|
+
await writeFile(filePath, bytes);
|
|
617
|
+
return {
|
|
618
|
+
key: objectKey,
|
|
619
|
+
filePath,
|
|
620
|
+
bytesWritten: bytes.length
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/proxy.ts
|
|
625
|
+
var HEALTH_CHECK_TIMEOUT_MS = 2e3;
|
|
626
|
+
var PORT_RETRY_ATTEMPTS = 5;
|
|
627
|
+
var PORT_RETRY_DELAY_MS = 1e3;
|
|
628
|
+
function matchesProxyPath(url, path) {
|
|
629
|
+
return url === path || url?.startsWith(`${path}?`) === true;
|
|
630
|
+
}
|
|
631
|
+
function readHeaderValue(value) {
|
|
632
|
+
if (typeof value === "string") {
|
|
633
|
+
const trimmed = value.trim();
|
|
634
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
635
|
+
}
|
|
636
|
+
if (Array.isArray(value)) {
|
|
637
|
+
for (const candidate of value) {
|
|
638
|
+
const trimmed = candidate.trim();
|
|
639
|
+
if (trimmed.length > 0) {
|
|
640
|
+
return trimmed;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return void 0;
|
|
645
|
+
}
|
|
646
|
+
async function readProxyJsonBody(req) {
|
|
647
|
+
const bodyChunks = [];
|
|
648
|
+
for await (const chunk of req) {
|
|
649
|
+
bodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
650
|
+
}
|
|
651
|
+
const bodyText = Buffer.concat(bodyChunks).toString("utf-8").trim();
|
|
652
|
+
if (bodyText.length === 0) {
|
|
653
|
+
return {};
|
|
654
|
+
}
|
|
655
|
+
return JSON.parse(bodyText);
|
|
656
|
+
}
|
|
657
|
+
function sendJson(res, status, body) {
|
|
658
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
659
|
+
res.end(JSON.stringify(body));
|
|
660
|
+
}
|
|
661
|
+
function createBackendForwardHeaders(response) {
|
|
662
|
+
const responseHeaders = {
|
|
663
|
+
"Content-Type": response.contentType
|
|
664
|
+
};
|
|
665
|
+
if (response.paymentRequired) {
|
|
666
|
+
responseHeaders["PAYMENT-REQUIRED"] = response.paymentRequired;
|
|
667
|
+
responseHeaders["x-payment-required"] = response.paymentRequired;
|
|
668
|
+
}
|
|
669
|
+
if (response.paymentResponse) {
|
|
670
|
+
responseHeaders["PAYMENT-RESPONSE"] = response.paymentResponse;
|
|
671
|
+
responseHeaders["x-payment-response"] = response.paymentResponse;
|
|
672
|
+
}
|
|
673
|
+
return responseHeaders;
|
|
674
|
+
}
|
|
675
|
+
function isLikelyWalletProofFailure(bodyText) {
|
|
676
|
+
return /(wallet|signature|proof|nonce|timestamp|expired|authoriz)/i.test(bodyText);
|
|
677
|
+
}
|
|
678
|
+
function normalizeBackendAuthFailure(status, bodyText) {
|
|
679
|
+
if (status !== 401 && status !== 403) {
|
|
680
|
+
return void 0;
|
|
681
|
+
}
|
|
682
|
+
const message = isLikelyWalletProofFailure(bodyText) ? "wallet proof invalid" : "unauthorized";
|
|
683
|
+
return {
|
|
684
|
+
status,
|
|
685
|
+
contentType: "application/json",
|
|
686
|
+
bodyText: createAuthErrorBody(message)
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function createAuthErrorBody(message) {
|
|
690
|
+
return JSON.stringify({
|
|
691
|
+
error: message.replace(/\s+/g, "_"),
|
|
692
|
+
message
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
function createWalletRequiredBody() {
|
|
696
|
+
return JSON.stringify({
|
|
697
|
+
error: "wallet_required",
|
|
698
|
+
message: "wallet required for storage endpoints"
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
function getProxyPort() {
|
|
702
|
+
return PROXY_PORT;
|
|
703
|
+
}
|
|
704
|
+
async function checkExistingProxy(port) {
|
|
705
|
+
const controller = new AbortController();
|
|
706
|
+
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT_MS);
|
|
707
|
+
try {
|
|
708
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
709
|
+
signal: controller.signal
|
|
710
|
+
});
|
|
711
|
+
clearTimeout(timeoutId);
|
|
712
|
+
if (response.ok) {
|
|
713
|
+
const data = await response.json();
|
|
714
|
+
if (data.status === "ok" && data.wallet) {
|
|
715
|
+
return data.wallet;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return void 0;
|
|
719
|
+
} catch {
|
|
720
|
+
clearTimeout(timeoutId);
|
|
721
|
+
return void 0;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function startProxy(options) {
|
|
725
|
+
const listenPort = options.port ?? getProxyPort();
|
|
726
|
+
const existingWallet = await checkExistingProxy(listenPort);
|
|
727
|
+
if (existingWallet) {
|
|
728
|
+
const account2 = privateKeyToAccount2(options.walletKey);
|
|
729
|
+
const balanceMonitor2 = new BalanceMonitor(account2.address);
|
|
730
|
+
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
731
|
+
if (existingWallet !== account2.address) {
|
|
732
|
+
console.warn(
|
|
733
|
+
`[mnemospark] Existing proxy on port ${listenPort} uses wallet ${existingWallet}, but current config uses ${account2.address}. Reusing existing proxy.`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
options.onReady?.(listenPort);
|
|
737
|
+
return {
|
|
738
|
+
port: listenPort,
|
|
739
|
+
baseUrl: baseUrl2,
|
|
740
|
+
walletAddress: existingWallet,
|
|
741
|
+
balanceMonitor: balanceMonitor2,
|
|
742
|
+
close: async () => {
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const walletPrivateKey = options.walletKey.trim();
|
|
747
|
+
const account = privateKeyToAccount2(walletPrivateKey);
|
|
748
|
+
const balanceMonitor = new BalanceMonitor(account.address);
|
|
749
|
+
const proxyWalletAddressLower = account.address.toLowerCase();
|
|
750
|
+
const connections = /* @__PURE__ */ new Set();
|
|
751
|
+
const createBackendWalletSignature = async (method, path, walletAddress) => {
|
|
752
|
+
if (walletAddress.toLowerCase() !== proxyWalletAddressLower) {
|
|
753
|
+
return void 0;
|
|
754
|
+
}
|
|
755
|
+
try {
|
|
756
|
+
return await createWalletSignatureHeaderValue(method, path, walletAddress, walletPrivateKey);
|
|
757
|
+
} catch (err) {
|
|
758
|
+
console.warn(
|
|
759
|
+
`[mnemospark] Failed to create wallet proof for ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
760
|
+
);
|
|
761
|
+
return void 0;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
const server = createServer(async (req, res) => {
|
|
765
|
+
req.on("error", (err) => {
|
|
766
|
+
console.error(`[mnemospark] Request stream error: ${err.message}`);
|
|
767
|
+
});
|
|
768
|
+
res.on("error", (err) => {
|
|
769
|
+
console.error(`[mnemospark] Response stream error: ${err.message}`);
|
|
770
|
+
});
|
|
771
|
+
if (req.method === "POST" && matchesProxyPath(req.url, PRICE_STORAGE_PROXY_PATH)) {
|
|
772
|
+
try {
|
|
773
|
+
let payload;
|
|
774
|
+
try {
|
|
775
|
+
payload = await readProxyJsonBody(req);
|
|
776
|
+
} catch {
|
|
777
|
+
sendJson(res, 400, {
|
|
778
|
+
error: "Bad request",
|
|
779
|
+
message: "Invalid JSON body for /cloud price-storage"
|
|
780
|
+
});
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const requestPayload = parsePriceStorageQuoteRequest(payload);
|
|
784
|
+
if (!requestPayload) {
|
|
785
|
+
sendJson(res, 400, {
|
|
786
|
+
error: "Bad request",
|
|
787
|
+
message: "Missing required fields: wallet_address, object_id, object_id_hash, gb, provider, region"
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const walletSignature = await createBackendWalletSignature(
|
|
792
|
+
"POST",
|
|
793
|
+
"/price-storage",
|
|
794
|
+
requestPayload.wallet_address
|
|
795
|
+
);
|
|
796
|
+
const backendResponse = await forwardPriceStorageToBackend(requestPayload, {
|
|
797
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
798
|
+
walletSignature
|
|
799
|
+
});
|
|
800
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
801
|
+
backendResponse.status,
|
|
802
|
+
backendResponse.bodyText
|
|
803
|
+
);
|
|
804
|
+
if (authFailure) {
|
|
805
|
+
const responseHeaders2 = createBackendForwardHeaders({
|
|
806
|
+
contentType: authFailure.contentType,
|
|
807
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
808
|
+
paymentResponse: backendResponse.paymentResponse
|
|
809
|
+
});
|
|
810
|
+
res.writeHead(authFailure.status, responseHeaders2);
|
|
811
|
+
res.end(authFailure.bodyText);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
815
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
816
|
+
res.end(backendResponse.bodyText);
|
|
817
|
+
} catch (err) {
|
|
818
|
+
sendJson(res, 502, {
|
|
819
|
+
error: "proxy_error",
|
|
820
|
+
message: `Failed to forward /cloud price-storage: ${err instanceof Error ? err.message : String(err)}`
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
if (req.method === "POST" && matchesProxyPath(req.url, UPLOAD_PROXY_PATH)) {
|
|
826
|
+
try {
|
|
827
|
+
let payload;
|
|
828
|
+
try {
|
|
829
|
+
payload = await readProxyJsonBody(req);
|
|
830
|
+
} catch {
|
|
831
|
+
sendJson(res, 400, {
|
|
832
|
+
error: "Bad request",
|
|
833
|
+
message: "Invalid JSON body for /cloud upload"
|
|
834
|
+
});
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const requestPayload = parseStorageUploadRequest(payload);
|
|
838
|
+
if (!requestPayload) {
|
|
839
|
+
sendJson(res, 400, {
|
|
840
|
+
error: "Bad request",
|
|
841
|
+
message: "Missing required fields: quote_id, wallet_address, object_id, object_id_hash, quoted_storage_price, payload"
|
|
842
|
+
});
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (requestPayload.wallet_address.toLowerCase() !== proxyWalletAddressLower) {
|
|
846
|
+
sendJson(res, 403, {
|
|
847
|
+
error: "wallet_proof_invalid",
|
|
848
|
+
message: "wallet proof invalid"
|
|
849
|
+
});
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const walletSignature = await createBackendWalletSignature(
|
|
853
|
+
"POST",
|
|
854
|
+
"/storage/upload",
|
|
855
|
+
requestPayload.wallet_address
|
|
856
|
+
);
|
|
857
|
+
if (!walletSignature) {
|
|
858
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
859
|
+
res.end(createWalletRequiredBody());
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const requiredMicros = BigInt(
|
|
863
|
+
Math.max(1, Math.ceil(requestPayload.quoted_storage_price * 1e6))
|
|
864
|
+
);
|
|
865
|
+
const uploadBalanceMonitor = requestPayload.wallet_address.toLowerCase() === account.address.toLowerCase() ? balanceMonitor : new BalanceMonitor(requestPayload.wallet_address);
|
|
866
|
+
const sufficiency = await uploadBalanceMonitor.checkSufficient(requiredMicros);
|
|
867
|
+
const requiredUSD = uploadBalanceMonitor.formatUSDC(requiredMicros);
|
|
868
|
+
if (!sufficiency.sufficient) {
|
|
869
|
+
options.onInsufficientFunds?.({
|
|
870
|
+
balanceUSD: sufficiency.info.balanceUSD,
|
|
871
|
+
requiredUSD,
|
|
872
|
+
walletAddress: requestPayload.wallet_address
|
|
873
|
+
});
|
|
874
|
+
sendJson(res, 400, {
|
|
875
|
+
error: "insufficient_balance",
|
|
876
|
+
message: `Insufficient USDC balance. Current: ${sufficiency.info.balanceUSD}, Required: ${requiredUSD}`,
|
|
877
|
+
wallet: requestPayload.wallet_address,
|
|
878
|
+
help: `Fund wallet ${requestPayload.wallet_address} on Base before running /cloud upload`
|
|
879
|
+
});
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (sufficiency.info.isLow) {
|
|
883
|
+
options.onLowBalance?.({
|
|
884
|
+
balanceUSD: sufficiency.info.balanceUSD,
|
|
885
|
+
walletAddress: requestPayload.wallet_address
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
const backendResponse = await forwardStorageUploadToBackend(requestPayload, {
|
|
889
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
890
|
+
walletSignature,
|
|
891
|
+
paymentSignature: readHeaderValue(req.headers["payment-signature"]),
|
|
892
|
+
legacyPayment: readHeaderValue(req.headers["x-payment"]),
|
|
893
|
+
idempotencyKey: readHeaderValue(req.headers["idempotency-key"])
|
|
894
|
+
});
|
|
895
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
896
|
+
backendResponse.status,
|
|
897
|
+
backendResponse.bodyText
|
|
898
|
+
);
|
|
899
|
+
if (authFailure) {
|
|
900
|
+
const responseHeaders2 = createBackendForwardHeaders({
|
|
901
|
+
contentType: authFailure.contentType,
|
|
902
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
903
|
+
paymentResponse: backendResponse.paymentResponse
|
|
904
|
+
});
|
|
905
|
+
res.writeHead(authFailure.status, responseHeaders2);
|
|
906
|
+
res.end(authFailure.bodyText);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
910
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
911
|
+
res.end(backendResponse.bodyText);
|
|
912
|
+
} catch (err) {
|
|
913
|
+
sendJson(res, 502, {
|
|
914
|
+
error: "proxy_error",
|
|
915
|
+
message: `Failed to forward /cloud upload: ${err instanceof Error ? err.message : String(err)}`
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (req.method === "POST" && matchesProxyPath(req.url, STORAGE_LS_PROXY_PATH)) {
|
|
921
|
+
try {
|
|
922
|
+
let payload;
|
|
923
|
+
try {
|
|
924
|
+
payload = await readProxyJsonBody(req);
|
|
925
|
+
} catch {
|
|
926
|
+
sendJson(res, 400, {
|
|
927
|
+
error: "Bad request",
|
|
928
|
+
message: "Invalid JSON body for /cloud ls"
|
|
929
|
+
});
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const requestPayload = parseStorageObjectRequest(payload);
|
|
933
|
+
if (!requestPayload) {
|
|
934
|
+
sendJson(res, 400, {
|
|
935
|
+
error: "Bad request",
|
|
936
|
+
message: "Missing required fields: wallet_address, object_key"
|
|
937
|
+
});
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (requestPayload.wallet_address.toLowerCase() !== proxyWalletAddressLower) {
|
|
941
|
+
sendJson(res, 403, {
|
|
942
|
+
error: "wallet_proof_invalid",
|
|
943
|
+
message: "wallet proof invalid"
|
|
944
|
+
});
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const walletSignature = await createBackendWalletSignature(
|
|
948
|
+
"POST",
|
|
949
|
+
"/storage/ls",
|
|
950
|
+
requestPayload.wallet_address
|
|
951
|
+
);
|
|
952
|
+
if (!walletSignature) {
|
|
953
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
954
|
+
res.end(createWalletRequiredBody());
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const backendResponse = await forwardStorageLsToBackend(requestPayload, {
|
|
958
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
959
|
+
walletSignature
|
|
960
|
+
});
|
|
961
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
962
|
+
backendResponse.status,
|
|
963
|
+
backendResponse.bodyText
|
|
964
|
+
);
|
|
965
|
+
if (authFailure) {
|
|
966
|
+
const responseHeaders2 = createBackendForwardHeaders({
|
|
967
|
+
contentType: authFailure.contentType,
|
|
968
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
969
|
+
paymentResponse: backendResponse.paymentResponse
|
|
970
|
+
});
|
|
971
|
+
res.writeHead(authFailure.status, responseHeaders2);
|
|
972
|
+
res.end(authFailure.bodyText);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
976
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
977
|
+
res.end(backendResponse.bodyText);
|
|
978
|
+
} catch (err) {
|
|
979
|
+
sendJson(res, 502, {
|
|
980
|
+
error: "proxy_error",
|
|
981
|
+
message: `Failed to forward /cloud ls: ${err instanceof Error ? err.message : String(err)}`
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (req.method === "POST" && matchesProxyPath(req.url, STORAGE_DOWNLOAD_PROXY_PATH)) {
|
|
987
|
+
try {
|
|
988
|
+
let payload;
|
|
989
|
+
try {
|
|
990
|
+
payload = await readProxyJsonBody(req);
|
|
991
|
+
} catch {
|
|
992
|
+
sendJson(res, 400, {
|
|
993
|
+
error: "Bad request",
|
|
994
|
+
message: "Invalid JSON body for /cloud download"
|
|
995
|
+
});
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const requestPayload = parseStorageObjectRequest(payload);
|
|
999
|
+
if (!requestPayload) {
|
|
1000
|
+
sendJson(res, 400, {
|
|
1001
|
+
error: "Bad request",
|
|
1002
|
+
message: "Missing required fields: wallet_address, object_key"
|
|
1003
|
+
});
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (requestPayload.wallet_address.toLowerCase() !== proxyWalletAddressLower) {
|
|
1007
|
+
sendJson(res, 403, {
|
|
1008
|
+
error: "wallet_proof_invalid",
|
|
1009
|
+
message: "wallet proof invalid"
|
|
1010
|
+
});
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const walletSignature = await createBackendWalletSignature(
|
|
1014
|
+
"POST",
|
|
1015
|
+
"/storage/download",
|
|
1016
|
+
requestPayload.wallet_address
|
|
1017
|
+
);
|
|
1018
|
+
if (!walletSignature) {
|
|
1019
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1020
|
+
res.end(createWalletRequiredBody());
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const backendResponse = await forwardStorageDownloadToBackend(requestPayload, {
|
|
1024
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
1025
|
+
walletSignature
|
|
1026
|
+
});
|
|
1027
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
1028
|
+
backendResponse.status,
|
|
1029
|
+
backendResponse.bodyText
|
|
1030
|
+
);
|
|
1031
|
+
if (authFailure) {
|
|
1032
|
+
const responseHeaders = createBackendForwardHeaders({
|
|
1033
|
+
contentType: authFailure.contentType,
|
|
1034
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
1035
|
+
paymentResponse: backendResponse.paymentResponse
|
|
1036
|
+
});
|
|
1037
|
+
res.writeHead(authFailure.status, responseHeaders);
|
|
1038
|
+
res.end(authFailure.bodyText);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (backendResponse.status < 200 || backendResponse.status >= 300) {
|
|
1042
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
1043
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
1044
|
+
res.end(backendResponse.bodyText);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
const downloadResult = await downloadStorageToDisk(requestPayload, backendResponse);
|
|
1048
|
+
sendJson(res, 200, {
|
|
1049
|
+
success: true,
|
|
1050
|
+
key: downloadResult.key,
|
|
1051
|
+
file_path: downloadResult.filePath,
|
|
1052
|
+
bytes_written: downloadResult.bytesWritten
|
|
1053
|
+
});
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
sendJson(res, 502, {
|
|
1056
|
+
error: "proxy_error",
|
|
1057
|
+
message: `Failed to forward /cloud download: ${err instanceof Error ? err.message : String(err)}`
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (req.method === "POST" && matchesProxyPath(req.url, STORAGE_DELETE_PROXY_PATH)) {
|
|
1063
|
+
try {
|
|
1064
|
+
let payload;
|
|
1065
|
+
try {
|
|
1066
|
+
payload = await readProxyJsonBody(req);
|
|
1067
|
+
} catch {
|
|
1068
|
+
sendJson(res, 400, {
|
|
1069
|
+
error: "Bad request",
|
|
1070
|
+
message: "Invalid JSON body for /cloud delete"
|
|
1071
|
+
});
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const requestPayload = parseStorageObjectRequest(payload);
|
|
1075
|
+
if (!requestPayload) {
|
|
1076
|
+
sendJson(res, 400, {
|
|
1077
|
+
error: "Bad request",
|
|
1078
|
+
message: "Missing required fields: wallet_address, object_key"
|
|
1079
|
+
});
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (requestPayload.wallet_address.toLowerCase() !== proxyWalletAddressLower) {
|
|
1083
|
+
sendJson(res, 403, {
|
|
1084
|
+
error: "wallet_proof_invalid",
|
|
1085
|
+
message: "wallet proof invalid"
|
|
1086
|
+
});
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const walletSignature = await createBackendWalletSignature(
|
|
1090
|
+
"POST",
|
|
1091
|
+
"/storage/delete",
|
|
1092
|
+
requestPayload.wallet_address
|
|
1093
|
+
);
|
|
1094
|
+
if (!walletSignature) {
|
|
1095
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1096
|
+
res.end(createWalletRequiredBody());
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
const backendResponse = await forwardStorageDeleteToBackend(requestPayload, {
|
|
1100
|
+
backendBaseUrl: MNEMOSPARK_BACKEND_API_BASE_URL,
|
|
1101
|
+
walletSignature
|
|
1102
|
+
});
|
|
1103
|
+
const authFailure = normalizeBackendAuthFailure(
|
|
1104
|
+
backendResponse.status,
|
|
1105
|
+
backendResponse.bodyText
|
|
1106
|
+
);
|
|
1107
|
+
if (authFailure) {
|
|
1108
|
+
const responseHeaders2 = createBackendForwardHeaders({
|
|
1109
|
+
contentType: authFailure.contentType,
|
|
1110
|
+
paymentRequired: backendResponse.paymentRequired,
|
|
1111
|
+
paymentResponse: backendResponse.paymentResponse
|
|
1112
|
+
});
|
|
1113
|
+
res.writeHead(authFailure.status, responseHeaders2);
|
|
1114
|
+
res.end(authFailure.bodyText);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const responseHeaders = createBackendForwardHeaders(backendResponse);
|
|
1118
|
+
res.writeHead(backendResponse.status, responseHeaders);
|
|
1119
|
+
res.end(backendResponse.bodyText);
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
sendJson(res, 502, {
|
|
1122
|
+
error: "proxy_error",
|
|
1123
|
+
message: `Failed to forward /cloud delete: ${err instanceof Error ? err.message : String(err)}`
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (req.url === "/health" || req.url?.startsWith("/health?")) {
|
|
1129
|
+
const url = new URL(req.url, "http://localhost");
|
|
1130
|
+
const full = url.searchParams.get("full") === "true";
|
|
1131
|
+
const response = {
|
|
1132
|
+
status: "ok",
|
|
1133
|
+
wallet: account.address
|
|
1134
|
+
};
|
|
1135
|
+
if (full) {
|
|
1136
|
+
try {
|
|
1137
|
+
const balanceInfo = await balanceMonitor.checkBalance();
|
|
1138
|
+
response.balance = balanceInfo.balanceUSD;
|
|
1139
|
+
response.isLow = balanceInfo.isLow;
|
|
1140
|
+
response.isEmpty = balanceInfo.isEmpty;
|
|
1141
|
+
} catch {
|
|
1142
|
+
response.balanceError = "Could not fetch balance";
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
sendJson(res, 200, response);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
sendJson(res, 404, {
|
|
1149
|
+
error: "Not found",
|
|
1150
|
+
message: "Supported paths: /health and /mnemospark/* storage endpoints"
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
const tryListen = (attempt) => {
|
|
1154
|
+
return new Promise((resolveAttempt, rejectAttempt) => {
|
|
1155
|
+
const onError = async (err) => {
|
|
1156
|
+
server.removeListener("error", onError);
|
|
1157
|
+
if (err.code === "EADDRINUSE") {
|
|
1158
|
+
const existingWallet2 = await checkExistingProxy(listenPort);
|
|
1159
|
+
if (existingWallet2) {
|
|
1160
|
+
console.log(`[mnemospark] Existing proxy detected on port ${listenPort}, reusing`);
|
|
1161
|
+
rejectAttempt({ code: "REUSE_EXISTING", wallet: existingWallet2 });
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (attempt < PORT_RETRY_ATTEMPTS) {
|
|
1165
|
+
console.log(
|
|
1166
|
+
`[mnemospark] Port ${listenPort} in TIME_WAIT, retrying in ${PORT_RETRY_DELAY_MS}ms (attempt ${attempt}/${PORT_RETRY_ATTEMPTS})`
|
|
1167
|
+
);
|
|
1168
|
+
rejectAttempt({ code: "RETRY", attempt });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
console.error(
|
|
1172
|
+
`[mnemospark] Port ${listenPort} still in use after ${PORT_RETRY_ATTEMPTS} attempts`
|
|
1173
|
+
);
|
|
1174
|
+
rejectAttempt(err);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
rejectAttempt(err);
|
|
1178
|
+
};
|
|
1179
|
+
server.once("error", onError);
|
|
1180
|
+
server.listen(listenPort, "127.0.0.1", () => {
|
|
1181
|
+
server.removeListener("error", onError);
|
|
1182
|
+
resolveAttempt();
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
};
|
|
1186
|
+
let lastError;
|
|
1187
|
+
for (let attempt = 1; attempt <= PORT_RETRY_ATTEMPTS; attempt++) {
|
|
1188
|
+
try {
|
|
1189
|
+
await tryListen(attempt);
|
|
1190
|
+
break;
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
const error = err;
|
|
1193
|
+
if (error.code === "REUSE_EXISTING" && error.wallet) {
|
|
1194
|
+
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
1195
|
+
options.onReady?.(listenPort);
|
|
1196
|
+
return {
|
|
1197
|
+
port: listenPort,
|
|
1198
|
+
baseUrl: baseUrl2,
|
|
1199
|
+
walletAddress: error.wallet,
|
|
1200
|
+
balanceMonitor,
|
|
1201
|
+
close: async () => {
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
if (error.code === "RETRY") {
|
|
1206
|
+
await new Promise((r) => setTimeout(r, PORT_RETRY_DELAY_MS));
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
lastError = err;
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
if (lastError) {
|
|
1214
|
+
throw lastError;
|
|
1215
|
+
}
|
|
1216
|
+
const addr = server.address();
|
|
1217
|
+
const port = addr.port;
|
|
1218
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
1219
|
+
options.onReady?.(port);
|
|
1220
|
+
server.on("error", (err) => {
|
|
1221
|
+
console.error(`[mnemospark] Server runtime error: ${err.message}`);
|
|
1222
|
+
options.onError?.(err);
|
|
1223
|
+
});
|
|
1224
|
+
server.on("clientError", (err, socket) => {
|
|
1225
|
+
console.error(`[mnemospark] Client error: ${err.message}`);
|
|
1226
|
+
if (socket.writable && !socket.destroyed) {
|
|
1227
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
server.on("connection", (socket) => {
|
|
1231
|
+
connections.add(socket);
|
|
1232
|
+
socket.setTimeout(3e5);
|
|
1233
|
+
socket.on("timeout", () => {
|
|
1234
|
+
console.error(`[mnemospark] Socket timeout, destroying connection`);
|
|
1235
|
+
socket.destroy();
|
|
1236
|
+
});
|
|
1237
|
+
socket.on("error", (err) => {
|
|
1238
|
+
console.error(`[mnemospark] Socket error: ${err.message}`);
|
|
1239
|
+
});
|
|
1240
|
+
socket.on("close", () => {
|
|
1241
|
+
connections.delete(socket);
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
return {
|
|
1245
|
+
port,
|
|
1246
|
+
baseUrl,
|
|
1247
|
+
walletAddress: account.address,
|
|
1248
|
+
balanceMonitor,
|
|
1249
|
+
close: () => new Promise((res, rej) => {
|
|
1250
|
+
const timeout = setTimeout(() => {
|
|
1251
|
+
rej(new Error("[mnemospark] Close timeout after 4s"));
|
|
1252
|
+
}, 4e3);
|
|
1253
|
+
for (const socket of connections) {
|
|
1254
|
+
socket.destroy();
|
|
1255
|
+
}
|
|
1256
|
+
connections.clear();
|
|
1257
|
+
server.close((err) => {
|
|
1258
|
+
clearTimeout(timeout);
|
|
1259
|
+
if (err) {
|
|
1260
|
+
rej(err);
|
|
1261
|
+
} else {
|
|
1262
|
+
res();
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
})
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/auth.ts
|
|
1270
|
+
import { writeFile as writeFile2, readFile, mkdir as mkdir2 } from "fs/promises";
|
|
1271
|
+
import { join as join2 } from "path";
|
|
1272
|
+
import { homedir } from "os";
|
|
1273
|
+
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
1274
|
+
var LEGACY_WALLET_DIR = join2(homedir(), ".openclaw", "blockrun");
|
|
1275
|
+
var LEGACY_WALLET_FILE = join2(LEGACY_WALLET_DIR, "wallet.key");
|
|
1276
|
+
var WALLET_DIR = join2(homedir(), ".openclaw", "mnemospark", "wallet");
|
|
1277
|
+
var WALLET_FILE = join2(WALLET_DIR, "wallet.key");
|
|
1278
|
+
async function loadSavedWallet() {
|
|
1279
|
+
for (const path of [WALLET_FILE, LEGACY_WALLET_FILE]) {
|
|
1280
|
+
try {
|
|
1281
|
+
const key = (await readFile(path, "utf-8")).trim();
|
|
1282
|
+
if (key.startsWith("0x") && key.length === 66) {
|
|
1283
|
+
console.log(`[mnemospark] \u2713 Loaded existing wallet from ${path}`);
|
|
1284
|
+
return key;
|
|
1285
|
+
}
|
|
1286
|
+
console.warn(`[mnemospark] \u26A0 Wallet file exists but is invalid (wrong format): ${path}`);
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
if (err.code !== "ENOENT") {
|
|
1289
|
+
console.error(
|
|
1290
|
+
`[mnemospark] \u2717 Failed to read wallet file ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
return void 0;
|
|
1296
|
+
}
|
|
1297
|
+
async function generateAndSaveWallet() {
|
|
1298
|
+
const key = generatePrivateKey();
|
|
1299
|
+
const account = privateKeyToAccount3(key);
|
|
1300
|
+
await mkdir2(WALLET_DIR, { recursive: true });
|
|
1301
|
+
await writeFile2(WALLET_FILE, key + "\n", { mode: 384 });
|
|
1302
|
+
try {
|
|
1303
|
+
const verification = (await readFile(WALLET_FILE, "utf-8")).trim();
|
|
1304
|
+
if (verification !== key) {
|
|
1305
|
+
throw new Error("Wallet file verification failed - content mismatch");
|
|
1306
|
+
}
|
|
1307
|
+
console.log(`[mnemospark] \u2713 Wallet saved and verified at ${WALLET_FILE}`);
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
`Failed to verify wallet file after creation: ${err instanceof Error ? err.message : String(err)}`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
return { key, address: account.address };
|
|
1314
|
+
}
|
|
1315
|
+
async function resolveOrGenerateWalletKey() {
|
|
1316
|
+
const saved = await loadSavedWallet();
|
|
1317
|
+
if (saved) {
|
|
1318
|
+
const account = privateKeyToAccount3(saved);
|
|
1319
|
+
return { key: saved, address: account.address, source: "saved" };
|
|
1320
|
+
}
|
|
1321
|
+
const envKey = process.env.BLOCKRUN_WALLET_KEY;
|
|
1322
|
+
if (typeof envKey === "string" && envKey.startsWith("0x") && envKey.length === 66) {
|
|
1323
|
+
const account = privateKeyToAccount3(envKey);
|
|
1324
|
+
return { key: envKey, address: account.address, source: "env" };
|
|
1325
|
+
}
|
|
1326
|
+
const { key, address } = await generateAndSaveWallet();
|
|
1327
|
+
return { key, address, source: "generated" };
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/version.ts
|
|
1331
|
+
import { createRequire } from "module";
|
|
1332
|
+
import { fileURLToPath } from "url";
|
|
1333
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1334
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
1335
|
+
var __dirname = dirname2(__filename);
|
|
1336
|
+
var require2 = createRequire(import.meta.url);
|
|
1337
|
+
var pkg = require2(join3(__dirname, "..", "package.json"));
|
|
1338
|
+
var VERSION = pkg.version;
|
|
1339
|
+
var USER_AGENT = `mnemospark/${VERSION}`;
|
|
1340
|
+
|
|
1341
|
+
// src/cli.ts
|
|
1342
|
+
import { dirname as dirname3 } from "path";
|
|
1343
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
1344
|
+
function isHexPrivateKey(value) {
|
|
1345
|
+
return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value.trim());
|
|
1346
|
+
}
|
|
1347
|
+
function printHelp() {
|
|
1348
|
+
console.log(`
|
|
1349
|
+
mnemospark v${VERSION} - Storage proxy and wallet tools
|
|
1350
|
+
|
|
1351
|
+
Usage:
|
|
1352
|
+
mnemospark [options]
|
|
1353
|
+
mnemospark install --default
|
|
1354
|
+
mnemospark install --standard
|
|
1355
|
+
mnemospark check-update Check if a new version is available
|
|
1356
|
+
mnemospark update Update to latest version
|
|
1357
|
+
|
|
1358
|
+
Options:
|
|
1359
|
+
--version, -v Show version number
|
|
1360
|
+
--help, -h Show this help message
|
|
1361
|
+
--port <number> Port to listen on (default: ${getProxyPort()})
|
|
1362
|
+
|
|
1363
|
+
Examples:
|
|
1364
|
+
# Start standalone proxy (survives gateway restarts)
|
|
1365
|
+
npx mnemospark
|
|
1366
|
+
|
|
1367
|
+
# Start on custom port
|
|
1368
|
+
npx mnemospark --port 9000
|
|
1369
|
+
|
|
1370
|
+
# Install mnemospark wallet with default behavior (create new wallet)
|
|
1371
|
+
npx mnemospark install --default
|
|
1372
|
+
|
|
1373
|
+
# Install mnemospark wallet with standard behavior (reuse Blockrun wallet if present)
|
|
1374
|
+
npx mnemospark install --standard
|
|
1375
|
+
|
|
1376
|
+
# Production deployment with PM2
|
|
1377
|
+
pm2 start "npx mnemospark" --name mnemospark
|
|
1378
|
+
|
|
1379
|
+
Environment Variables:
|
|
1380
|
+
BLOCKRUN_WALLET_KEY Private key for x402 storage payments (auto-generated if not set)
|
|
1381
|
+
MNEMOSPARK_PROXY_PORT Default proxy port (default: 7120)
|
|
1382
|
+
|
|
1383
|
+
For more info: https://github.com/pawlsclick/mnemospark
|
|
1384
|
+
`);
|
|
1385
|
+
}
|
|
1386
|
+
function parseArgs(args) {
|
|
1387
|
+
const result = {
|
|
1388
|
+
version: false,
|
|
1389
|
+
help: false,
|
|
1390
|
+
port: void 0,
|
|
1391
|
+
command: void 0,
|
|
1392
|
+
installMode: void 0
|
|
1393
|
+
};
|
|
1394
|
+
for (let i = 0; i < args.length; i++) {
|
|
1395
|
+
const arg = args[i];
|
|
1396
|
+
if (!result.command && !arg.startsWith("-")) {
|
|
1397
|
+
if (arg === "install") {
|
|
1398
|
+
result.command = "install";
|
|
1399
|
+
} else if (arg === "update") {
|
|
1400
|
+
result.command = "update";
|
|
1401
|
+
} else if (arg === "check-update") {
|
|
1402
|
+
result.command = "check-update";
|
|
1403
|
+
}
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
if (arg === "--version" || arg === "-v") {
|
|
1407
|
+
result.version = true;
|
|
1408
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
1409
|
+
result.help = true;
|
|
1410
|
+
} else if (result.command === "install" && arg === "--default") {
|
|
1411
|
+
result.installMode = "default";
|
|
1412
|
+
} else if (result.command === "install" && arg === "--standard") {
|
|
1413
|
+
result.installMode = "standard";
|
|
1414
|
+
} else if (arg === "--port" && args[i + 1]) {
|
|
1415
|
+
result.port = parseInt(args[i + 1], 10);
|
|
1416
|
+
i++;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
async function ensureDir(path) {
|
|
1422
|
+
await mkdir3(path, { recursive: true });
|
|
1423
|
+
}
|
|
1424
|
+
async function readLegacyWalletIfPresent() {
|
|
1425
|
+
try {
|
|
1426
|
+
const key = (await readFile2(LEGACY_WALLET_FILE, "utf-8")).trim();
|
|
1427
|
+
return isHexPrivateKey(key) ? key : null;
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
if (error.code === "ENOENT") {
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
throw error;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
async function writeMnemosparkWallet(key) {
|
|
1436
|
+
const dir = dirname3(WALLET_FILE);
|
|
1437
|
+
await ensureDir(dir);
|
|
1438
|
+
await writeFile3(WALLET_FILE, `${key}
|
|
1439
|
+
`, { mode: 384 });
|
|
1440
|
+
}
|
|
1441
|
+
async function promptReuseLegacyWallet() {
|
|
1442
|
+
process.stdout.write(
|
|
1443
|
+
`Found existing Blockrun wallet at ${LEGACY_WALLET_FILE}.
|
|
1444
|
+
Reuse this wallet for mnemospark? [Y/n]: `
|
|
1445
|
+
);
|
|
1446
|
+
return new Promise((resolve2) => {
|
|
1447
|
+
process.stdin.setEncoding("utf-8");
|
|
1448
|
+
process.stdin.once("data", (data) => {
|
|
1449
|
+
const input = typeof data === "string" ? data : data.toString("utf-8");
|
|
1450
|
+
const answer = input.trim().toLowerCase();
|
|
1451
|
+
if (!answer || answer === "y" || answer === "yes") {
|
|
1452
|
+
resolve2(true);
|
|
1453
|
+
} else {
|
|
1454
|
+
resolve2(false);
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
var NPM_REGISTRY_URL = "https://registry.npmjs.org/mnemospark/latest";
|
|
1460
|
+
function compareVersion(a, b) {
|
|
1461
|
+
const partsA = a.split("-")[0].split(".").map(Number);
|
|
1462
|
+
const partsB = b.split("-")[0].split(".").map(Number);
|
|
1463
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
1464
|
+
const na = partsA[i] ?? 0;
|
|
1465
|
+
const nb = partsB[i] ?? 0;
|
|
1466
|
+
if (na < nb) return -1;
|
|
1467
|
+
if (na > nb) return 1;
|
|
1468
|
+
}
|
|
1469
|
+
return 0;
|
|
1470
|
+
}
|
|
1471
|
+
async function fetchLatestVersion() {
|
|
1472
|
+
try {
|
|
1473
|
+
const res = await fetch(NPM_REGISTRY_URL, {
|
|
1474
|
+
headers: { Accept: "application/json" }
|
|
1475
|
+
});
|
|
1476
|
+
if (!res.ok) return null;
|
|
1477
|
+
const data = await res.json();
|
|
1478
|
+
return data.version ?? null;
|
|
1479
|
+
} catch {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
async function runCheckUpdate() {
|
|
1484
|
+
const latest = await fetchLatestVersion();
|
|
1485
|
+
if (!latest) {
|
|
1486
|
+
console.log("[mnemospark] Could not fetch latest version from registry.");
|
|
1487
|
+
process.exit(1);
|
|
1488
|
+
}
|
|
1489
|
+
const cmp = compareVersion(VERSION, latest);
|
|
1490
|
+
if (cmp < 0) {
|
|
1491
|
+
console.log(`[mnemospark] A new version is available: ${latest} (current: ${VERSION})`);
|
|
1492
|
+
console.log("Run: npx mnemospark update");
|
|
1493
|
+
} else if (cmp === 0) {
|
|
1494
|
+
console.log("You are on the latest version.");
|
|
1495
|
+
} else {
|
|
1496
|
+
console.log(`You are on the latest version. (current: ${VERSION}, registry: ${latest})`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
async function runUpdate() {
|
|
1500
|
+
const latest = await fetchLatestVersion();
|
|
1501
|
+
if (!latest) {
|
|
1502
|
+
console.log("[mnemospark] Could not fetch latest version from registry.");
|
|
1503
|
+
process.exit(1);
|
|
1504
|
+
}
|
|
1505
|
+
const cmp = compareVersion(VERSION, latest);
|
|
1506
|
+
if (cmp < 0) {
|
|
1507
|
+
console.log(`[mnemospark] Updating from ${VERSION} to ${latest}...`);
|
|
1508
|
+
const { execSync } = await import("child_process");
|
|
1509
|
+
try {
|
|
1510
|
+
execSync(`npm install mnemospark@${latest}`, { stdio: "inherit" });
|
|
1511
|
+
console.log(`[mnemospark] Updated to ${latest}.`);
|
|
1512
|
+
} catch {
|
|
1513
|
+
console.log(
|
|
1514
|
+
"[mnemospark] npm install failed. You can update manually: npm install mnemospark@latest"
|
|
1515
|
+
);
|
|
1516
|
+
process.exit(1);
|
|
1517
|
+
}
|
|
1518
|
+
} else {
|
|
1519
|
+
console.log("You are on the latest version.");
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
async function runInstall(mode) {
|
|
1523
|
+
if (mode === "standard") {
|
|
1524
|
+
const legacyWallet = await readLegacyWalletIfPresent();
|
|
1525
|
+
if (legacyWallet) {
|
|
1526
|
+
const reuse = await promptReuseLegacyWallet();
|
|
1527
|
+
if (reuse) {
|
|
1528
|
+
await writeMnemosparkWallet(legacyWallet);
|
|
1529
|
+
console.log("\n[mnemospark] Reused existing Blockrun wallet for mnemospark.");
|
|
1530
|
+
console.log(
|
|
1531
|
+
"[mnemospark] Wallet file: ~/.openclaw/mnemospark/wallet/wallet.key (chmod 600 expected)."
|
|
1532
|
+
);
|
|
1533
|
+
console.log(
|
|
1534
|
+
"[mnemospark] Your wallet will be used for mnemospark storage payments on Base."
|
|
1535
|
+
);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
const { address, source } = await resolveOrGenerateWalletKey();
|
|
1541
|
+
console.log("[mnemospark] Install complete.");
|
|
1542
|
+
console.log(`Your new Base blockchain wallet is: ${address}`);
|
|
1543
|
+
if (source === "env") {
|
|
1544
|
+
console.log(
|
|
1545
|
+
"Wallet is sourced from BLOCKRUN_WALLET_KEY. To persist it, save it under ~/.openclaw/mnemospark/wallet/wallet.key with chmod 600."
|
|
1546
|
+
);
|
|
1547
|
+
} else {
|
|
1548
|
+
console.log(
|
|
1549
|
+
"Wallet key stored under ~/.openclaw/mnemospark/wallet/wallet.key (permissions should be chmod 600)."
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
console.log("Add USDC on the Base network to start using mnemospark today.");
|
|
1553
|
+
console.log(
|
|
1554
|
+
"You can acquire USDC on Base from providers like Coinbase and Moonpay. Fund the wallet before running mnemospark."
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
async function main() {
|
|
1558
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1559
|
+
if (args.version) {
|
|
1560
|
+
console.log(VERSION);
|
|
1561
|
+
process.exit(0);
|
|
1562
|
+
}
|
|
1563
|
+
if (args.help) {
|
|
1564
|
+
printHelp();
|
|
1565
|
+
process.exit(0);
|
|
1566
|
+
}
|
|
1567
|
+
if (args.command === "install") {
|
|
1568
|
+
const mode = args.installMode ?? "standard";
|
|
1569
|
+
await runInstall(mode);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (args.command === "check-update") {
|
|
1573
|
+
await runCheckUpdate();
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (args.command === "update") {
|
|
1577
|
+
await runUpdate();
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const { key: walletKey, address, source } = await resolveOrGenerateWalletKey();
|
|
1581
|
+
if (source === "generated") {
|
|
1582
|
+
console.log(`[mnemospark] Generated new wallet: ${address}`);
|
|
1583
|
+
} else if (source === "saved") {
|
|
1584
|
+
console.log(`[mnemospark] Using saved wallet: ${address}`);
|
|
1585
|
+
} else {
|
|
1586
|
+
console.log(`[mnemospark] Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
|
|
1587
|
+
}
|
|
1588
|
+
const proxy = await startProxy({
|
|
1589
|
+
walletKey,
|
|
1590
|
+
port: args.port,
|
|
1591
|
+
onReady: (port) => {
|
|
1592
|
+
console.log(`[mnemospark] Proxy listening on http://127.0.0.1:${port}`);
|
|
1593
|
+
console.log(`[mnemospark] Health check: http://127.0.0.1:${port}/health`);
|
|
1594
|
+
},
|
|
1595
|
+
onError: (error) => {
|
|
1596
|
+
console.error(`[mnemospark] Error: ${error.message}`);
|
|
1597
|
+
},
|
|
1598
|
+
onLowBalance: (info) => {
|
|
1599
|
+
console.warn(`[mnemospark] Low balance: ${info.balanceUSD}. Fund: ${info.walletAddress}`);
|
|
1600
|
+
},
|
|
1601
|
+
onInsufficientFunds: (info) => {
|
|
1602
|
+
console.error(
|
|
1603
|
+
`[mnemospark] Insufficient funds. Balance: ${info.balanceUSD}, Need: ${info.requiredUSD}`
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
const monitor = new BalanceMonitor(address);
|
|
1608
|
+
try {
|
|
1609
|
+
const balance = await monitor.checkBalance();
|
|
1610
|
+
if (balance.isEmpty) {
|
|
1611
|
+
console.log(`[mnemospark] Wallet balance: $0.00 (using FREE model)`);
|
|
1612
|
+
console.log(`[mnemospark] Fund wallet for premium models: ${address}`);
|
|
1613
|
+
} else if (balance.isLow) {
|
|
1614
|
+
console.log(`[mnemospark] Wallet balance: ${balance.balanceUSD} (low)`);
|
|
1615
|
+
} else {
|
|
1616
|
+
console.log(`[mnemospark] Wallet balance: ${balance.balanceUSD}`);
|
|
1617
|
+
}
|
|
1618
|
+
} catch {
|
|
1619
|
+
console.log(`[mnemospark] Wallet: ${address} (balance check pending)`);
|
|
1620
|
+
}
|
|
1621
|
+
console.log(`[mnemospark] Ready - Ctrl+C to stop`);
|
|
1622
|
+
const shutdown = async (signal) => {
|
|
1623
|
+
console.log(`
|
|
1624
|
+
[mnemospark] Received ${signal}, shutting down...`);
|
|
1625
|
+
try {
|
|
1626
|
+
await proxy.close();
|
|
1627
|
+
console.log(`[mnemospark] Proxy closed`);
|
|
1628
|
+
process.exit(0);
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
console.error(`[mnemospark] Error during shutdown: ${err}`);
|
|
1631
|
+
process.exit(1);
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
1635
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
1636
|
+
await new Promise(() => {
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
main().catch((err) => {
|
|
1640
|
+
console.error(`[mnemospark] Fatal error: ${err.message}`);
|
|
1641
|
+
process.exit(1);
|
|
1642
|
+
});
|
|
1643
|
+
//# sourceMappingURL=cli.js.map
|