@useagentpay/sdk 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/dist/index.cjs +2590 -162
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -12
- package/dist/index.d.ts +163 -12
- package/dist/index.js +2561 -153
- package/dist/index.js.map +1 -1
- package/package.json +37 -18
- package/LICENSE +0 -21
- package/dist/cli.cjs +0 -2758
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -2728
- package/dist/cli.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -111,6 +111,139 @@ var init_errors = __esm({
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
// src/utils/paths.ts
|
|
115
|
+
function getHomePath() {
|
|
116
|
+
if (process.env.AGENTPAY_HOME) return process.env.AGENTPAY_HOME;
|
|
117
|
+
const local = (0, import_node_path.join)(process.cwd(), "agentpay");
|
|
118
|
+
if ((0, import_node_fs.existsSync)(local)) return local;
|
|
119
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentpay");
|
|
120
|
+
}
|
|
121
|
+
function getCredentialsPath() {
|
|
122
|
+
return (0, import_node_path.join)(getHomePath(), "credentials.enc");
|
|
123
|
+
}
|
|
124
|
+
function getKeysPath() {
|
|
125
|
+
return (0, import_node_path.join)(getHomePath(), "keys");
|
|
126
|
+
}
|
|
127
|
+
function getPublicKeyPath() {
|
|
128
|
+
return (0, import_node_path.join)(getKeysPath(), "public.pem");
|
|
129
|
+
}
|
|
130
|
+
function getPrivateKeyPath() {
|
|
131
|
+
return (0, import_node_path.join)(getKeysPath(), "private.pem");
|
|
132
|
+
}
|
|
133
|
+
function getWalletPath() {
|
|
134
|
+
return (0, import_node_path.join)(getHomePath(), "wallet.json");
|
|
135
|
+
}
|
|
136
|
+
function getTransactionsPath() {
|
|
137
|
+
return (0, import_node_path.join)(getHomePath(), "transactions.json");
|
|
138
|
+
}
|
|
139
|
+
function getAuditPath() {
|
|
140
|
+
return (0, import_node_path.join)(getHomePath(), "audit.log");
|
|
141
|
+
}
|
|
142
|
+
var import_node_os, import_node_path, import_node_fs;
|
|
143
|
+
var init_paths = __esm({
|
|
144
|
+
"src/utils/paths.ts"() {
|
|
145
|
+
"use strict";
|
|
146
|
+
init_cjs_shims();
|
|
147
|
+
import_node_os = require("os");
|
|
148
|
+
import_node_path = require("path");
|
|
149
|
+
import_node_fs = require("fs");
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// src/auth/keypair.ts
|
|
154
|
+
function generateKeyPair(passphrase) {
|
|
155
|
+
const { publicKey, privateKey } = (0, import_node_crypto3.generateKeyPairSync)("ed25519", {
|
|
156
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
157
|
+
privateKeyEncoding: {
|
|
158
|
+
type: "pkcs8",
|
|
159
|
+
format: "pem",
|
|
160
|
+
cipher: "aes-256-cbc",
|
|
161
|
+
passphrase
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return { publicKey, privateKey };
|
|
165
|
+
}
|
|
166
|
+
function saveKeyPair(keys, publicPath, privatePath) {
|
|
167
|
+
const pubPath = publicPath ?? getPublicKeyPath();
|
|
168
|
+
const privPath = privatePath ?? getPrivateKeyPath();
|
|
169
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.dirname)(pubPath), { recursive: true });
|
|
170
|
+
(0, import_node_fs3.writeFileSync)(pubPath, keys.publicKey, { mode: 420 });
|
|
171
|
+
(0, import_node_fs3.writeFileSync)(privPath, keys.privateKey, { mode: 384 });
|
|
172
|
+
}
|
|
173
|
+
function loadPublicKey(path) {
|
|
174
|
+
return (0, import_node_fs3.readFileSync)(path ?? getPublicKeyPath(), "utf8");
|
|
175
|
+
}
|
|
176
|
+
function loadPrivateKey(path) {
|
|
177
|
+
return (0, import_node_fs3.readFileSync)(path ?? getPrivateKeyPath(), "utf8");
|
|
178
|
+
}
|
|
179
|
+
var import_node_crypto3, import_node_fs3, import_node_path3;
|
|
180
|
+
var init_keypair = __esm({
|
|
181
|
+
"src/auth/keypair.ts"() {
|
|
182
|
+
"use strict";
|
|
183
|
+
init_cjs_shims();
|
|
184
|
+
import_node_crypto3 = require("crypto");
|
|
185
|
+
import_node_fs3 = require("fs");
|
|
186
|
+
import_node_path3 = require("path");
|
|
187
|
+
init_paths();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// src/auth/mandate.ts
|
|
192
|
+
function hashTransactionDetails(details) {
|
|
193
|
+
const canonical = JSON.stringify({
|
|
194
|
+
txId: details.txId,
|
|
195
|
+
merchant: details.merchant,
|
|
196
|
+
amount: details.amount,
|
|
197
|
+
description: details.description,
|
|
198
|
+
timestamp: details.timestamp
|
|
199
|
+
});
|
|
200
|
+
return (0, import_node_crypto4.createHash)("sha256").update(canonical).digest("hex");
|
|
201
|
+
}
|
|
202
|
+
function createMandate(txDetails, privateKeyPem, passphrase) {
|
|
203
|
+
const txHash = hashTransactionDetails(txDetails);
|
|
204
|
+
const data = Buffer.from(txHash);
|
|
205
|
+
const privateKey = (0, import_node_crypto4.createPrivateKey)({
|
|
206
|
+
key: privateKeyPem,
|
|
207
|
+
format: "pem",
|
|
208
|
+
type: "pkcs8",
|
|
209
|
+
passphrase
|
|
210
|
+
});
|
|
211
|
+
const signature = (0, import_node_crypto4.sign)(null, data, privateKey);
|
|
212
|
+
const publicKey = (0, import_node_crypto4.createPublicKey)(privateKey);
|
|
213
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
214
|
+
return {
|
|
215
|
+
txId: txDetails.txId,
|
|
216
|
+
txHash,
|
|
217
|
+
signature: signature.toString("base64"),
|
|
218
|
+
publicKey: publicKeyPem,
|
|
219
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function verifyMandate(mandate, txDetails) {
|
|
223
|
+
try {
|
|
224
|
+
const txHash = hashTransactionDetails(txDetails);
|
|
225
|
+
if (txHash !== mandate.txHash) return false;
|
|
226
|
+
const data = Buffer.from(txHash);
|
|
227
|
+
const signature = Buffer.from(mandate.signature, "base64");
|
|
228
|
+
const publicKey = (0, import_node_crypto4.createPublicKey)({
|
|
229
|
+
key: mandate.publicKey,
|
|
230
|
+
format: "pem",
|
|
231
|
+
type: "spki"
|
|
232
|
+
});
|
|
233
|
+
return (0, import_node_crypto4.verify)(null, data, publicKey, signature);
|
|
234
|
+
} catch {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
var import_node_crypto4;
|
|
239
|
+
var init_mandate = __esm({
|
|
240
|
+
"src/auth/mandate.ts"() {
|
|
241
|
+
"use strict";
|
|
242
|
+
init_cjs_shims();
|
|
243
|
+
import_node_crypto4 = require("crypto");
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
114
247
|
// src/transactions/poller.ts
|
|
115
248
|
var poller_exports = {};
|
|
116
249
|
__export(poller_exports, {
|
|
@@ -137,6 +270,1024 @@ var init_poller = __esm({
|
|
|
137
270
|
}
|
|
138
271
|
});
|
|
139
272
|
|
|
273
|
+
// src/server/approval-html.ts
|
|
274
|
+
function esc(s) {
|
|
275
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
276
|
+
}
|
|
277
|
+
function formatCurrency2(n) {
|
|
278
|
+
return "$" + n.toFixed(2);
|
|
279
|
+
}
|
|
280
|
+
function getApprovalHtml(token, tx) {
|
|
281
|
+
const lines = [];
|
|
282
|
+
lines.push(`<div class="detail"><span class="detail-label">Merchant</span><span class="detail-value">${esc(tx.merchant)}</span></div>`);
|
|
283
|
+
lines.push(`<div class="detail"><span class="detail-label">Amount</span><span class="detail-value">${formatCurrency2(tx.amount)}</span></div>`);
|
|
284
|
+
lines.push(`<div class="detail"><span class="detail-label">Description</span><span class="detail-value">${esc(tx.description)}</span></div>`);
|
|
285
|
+
if (tx.url) {
|
|
286
|
+
lines.push(`<div class="detail"><span class="detail-label">URL</span><span class="detail-value"><a href="${esc(tx.url)}" target="_blank" rel="noopener" style="color:#111;">${esc(tx.url)}</a></span></div>`);
|
|
287
|
+
}
|
|
288
|
+
lines.push(`<div class="detail"><span class="detail-label">Transaction</span><span class="detail-value" style="font-family:monospace;font-size:12px;">${esc(tx.id)}</span></div>`);
|
|
289
|
+
const contextHtml = `<div class="card context-card">${lines.join("")}</div>`;
|
|
290
|
+
return `<!DOCTYPE html>
|
|
291
|
+
<html lang="en">
|
|
292
|
+
<head>
|
|
293
|
+
<meta charset="utf-8">
|
|
294
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
295
|
+
<title>AgentPay \u2014 Approve Purchase</title>
|
|
296
|
+
<style>
|
|
297
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
298
|
+
body {
|
|
299
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
300
|
+
background: #f5f5f5;
|
|
301
|
+
color: #111;
|
|
302
|
+
min-height: 100vh;
|
|
303
|
+
display: flex;
|
|
304
|
+
justify-content: center;
|
|
305
|
+
padding: 40px 16px;
|
|
306
|
+
}
|
|
307
|
+
.container { width: 100%; max-width: 420px; }
|
|
308
|
+
h1 { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
|
|
309
|
+
.subtitle { color: #666; font-size: 14px; margin-bottom: 24px; }
|
|
310
|
+
.card {
|
|
311
|
+
background: #fff;
|
|
312
|
+
border-radius: 8px;
|
|
313
|
+
padding: 24px;
|
|
314
|
+
margin-bottom: 16px;
|
|
315
|
+
border: 1px solid #e0e0e0;
|
|
316
|
+
}
|
|
317
|
+
.context-card { padding: 16px 20px; }
|
|
318
|
+
.detail {
|
|
319
|
+
display: flex;
|
|
320
|
+
justify-content: space-between;
|
|
321
|
+
align-items: center;
|
|
322
|
+
padding: 8px 0;
|
|
323
|
+
border-bottom: 1px solid #f0f0f0;
|
|
324
|
+
}
|
|
325
|
+
.detail:last-child { border-bottom: none; }
|
|
326
|
+
.detail-label { font-size: 13px; color: #666; }
|
|
327
|
+
.detail-value { font-size: 14px; font-weight: 600; }
|
|
328
|
+
label { display: block; font-size: 13px; font-weight: 500; color: #333; margin-bottom: 6px; }
|
|
329
|
+
input[type="password"], textarea {
|
|
330
|
+
width: 100%;
|
|
331
|
+
padding: 12px 14px;
|
|
332
|
+
border: 1px solid #d0d0d0;
|
|
333
|
+
border-radius: 8px;
|
|
334
|
+
font-size: 15px;
|
|
335
|
+
font-family: inherit;
|
|
336
|
+
outline: none;
|
|
337
|
+
transition: border-color 0.15s;
|
|
338
|
+
}
|
|
339
|
+
input[type="password"]:focus, textarea:focus { border-color: #111; }
|
|
340
|
+
textarea { resize: vertical; min-height: 60px; margin-top: 8px; }
|
|
341
|
+
.btn-row { display: flex; gap: 12px; margin-top: 16px; }
|
|
342
|
+
.btn-approve, .btn-deny {
|
|
343
|
+
flex: 1;
|
|
344
|
+
padding: 12px;
|
|
345
|
+
border: none;
|
|
346
|
+
border-radius: 8px;
|
|
347
|
+
font-size: 14px;
|
|
348
|
+
font-weight: 600;
|
|
349
|
+
cursor: pointer;
|
|
350
|
+
transition: opacity 0.15s;
|
|
351
|
+
}
|
|
352
|
+
.btn-approve { background: #111; color: #fff; }
|
|
353
|
+
.btn-approve:hover { opacity: 0.85; }
|
|
354
|
+
.btn-approve:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
355
|
+
.btn-deny { background: #fff; color: #c62828; border: 2px solid #c62828; }
|
|
356
|
+
.btn-deny:hover { opacity: 0.85; }
|
|
357
|
+
.btn-deny:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
358
|
+
.error { color: #c62828; font-size: 13px; margin-top: 10px; }
|
|
359
|
+
.success-screen {
|
|
360
|
+
text-align: center;
|
|
361
|
+
padding: 40px 0;
|
|
362
|
+
}
|
|
363
|
+
.checkmark { font-size: 48px; margin-bottom: 16px; }
|
|
364
|
+
.success-msg { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
|
365
|
+
.success-hint { font-size: 13px; color: #666; }
|
|
366
|
+
.hidden { display: none; }
|
|
367
|
+
.reason-section { margin-top: 12px; }
|
|
368
|
+
</style>
|
|
369
|
+
</head>
|
|
370
|
+
<body>
|
|
371
|
+
<div class="container">
|
|
372
|
+
<div id="form-view">
|
|
373
|
+
<h1>AgentPay</h1>
|
|
374
|
+
<p class="subtitle">Approve Purchase</p>
|
|
375
|
+
${contextHtml}
|
|
376
|
+
<div class="card">
|
|
377
|
+
<label for="passphrase">Passphrase</label>
|
|
378
|
+
<input type="password" id="passphrase" placeholder="Enter your passphrase" autofocus>
|
|
379
|
+
<div id="reason-section" class="reason-section hidden">
|
|
380
|
+
<label for="reason">Reason (optional)</label>
|
|
381
|
+
<textarea id="reason" placeholder="Why are you denying this purchase?"></textarea>
|
|
382
|
+
</div>
|
|
383
|
+
<div id="error" class="error hidden"></div>
|
|
384
|
+
<div class="btn-row">
|
|
385
|
+
<button class="btn-approve" id="btn-approve">Approve</button>
|
|
386
|
+
<button class="btn-deny" id="btn-deny">Deny</button>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
<div id="success-view" class="hidden">
|
|
391
|
+
<h1>AgentPay</h1>
|
|
392
|
+
<p class="subtitle" id="success-subtitle"></p>
|
|
393
|
+
<div class="card">
|
|
394
|
+
<div class="success-screen">
|
|
395
|
+
<div class="checkmark" id="success-icon"></div>
|
|
396
|
+
<div class="success-msg" id="success-msg"></div>
|
|
397
|
+
<div class="success-hint">This tab will close automatically.</div>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
<script>
|
|
403
|
+
(function() {
|
|
404
|
+
var token = ${JSON.stringify(token)};
|
|
405
|
+
var form = document.getElementById('form-view');
|
|
406
|
+
var successView = document.getElementById('success-view');
|
|
407
|
+
var input = document.getElementById('passphrase');
|
|
408
|
+
var btnApprove = document.getElementById('btn-approve');
|
|
409
|
+
var btnDeny = document.getElementById('btn-deny');
|
|
410
|
+
var errDiv = document.getElementById('error');
|
|
411
|
+
var reasonSection = document.getElementById('reason-section');
|
|
412
|
+
var reasonInput = document.getElementById('reason');
|
|
413
|
+
var successSubtitle = document.getElementById('success-subtitle');
|
|
414
|
+
var successIcon = document.getElementById('success-icon');
|
|
415
|
+
var successMsg = document.getElementById('success-msg');
|
|
416
|
+
var denyMode = false;
|
|
417
|
+
|
|
418
|
+
function showError(msg) {
|
|
419
|
+
errDiv.textContent = msg;
|
|
420
|
+
errDiv.classList.remove('hidden');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function clearError() {
|
|
424
|
+
errDiv.classList.add('hidden');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function disableButtons() {
|
|
428
|
+
btnApprove.disabled = true;
|
|
429
|
+
btnDeny.disabled = true;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function enableButtons() {
|
|
433
|
+
btnApprove.disabled = false;
|
|
434
|
+
btnDeny.disabled = false;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function showSuccess(approved) {
|
|
438
|
+
form.classList.add('hidden');
|
|
439
|
+
successView.classList.remove('hidden');
|
|
440
|
+
if (approved) {
|
|
441
|
+
successSubtitle.textContent = 'Purchase Approved';
|
|
442
|
+
successIcon.innerHTML = '✓';
|
|
443
|
+
successIcon.style.color = '#2e7d32';
|
|
444
|
+
successMsg.textContent = 'Transaction approved and signed.';
|
|
445
|
+
} else {
|
|
446
|
+
successSubtitle.textContent = 'Purchase Denied';
|
|
447
|
+
successIcon.innerHTML = '✗';
|
|
448
|
+
successIcon.style.color = '#c62828';
|
|
449
|
+
successMsg.textContent = 'Transaction has been denied.';
|
|
450
|
+
}
|
|
451
|
+
setTimeout(function() { window.close(); }, 3000);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function doApprove() {
|
|
455
|
+
var passphrase = input.value;
|
|
456
|
+
if (!passphrase) {
|
|
457
|
+
showError('Passphrase is required to approve.');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
clearError();
|
|
461
|
+
disableButtons();
|
|
462
|
+
btnApprove.textContent = 'Approving...';
|
|
463
|
+
|
|
464
|
+
fetch('/api/approve', {
|
|
465
|
+
method: 'POST',
|
|
466
|
+
headers: { 'Content-Type': 'application/json' },
|
|
467
|
+
body: JSON.stringify({ token: token, passphrase: passphrase })
|
|
468
|
+
})
|
|
469
|
+
.then(function(res) { return res.json(); })
|
|
470
|
+
.then(function(data) {
|
|
471
|
+
if (data.error) {
|
|
472
|
+
showError(data.error);
|
|
473
|
+
enableButtons();
|
|
474
|
+
btnApprove.textContent = 'Approve';
|
|
475
|
+
} else {
|
|
476
|
+
showSuccess(true);
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
.catch(function() {
|
|
480
|
+
showError('Failed to submit. Is the CLI still running?');
|
|
481
|
+
enableButtons();
|
|
482
|
+
btnApprove.textContent = 'Approve';
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function doReject() {
|
|
487
|
+
if (!denyMode) {
|
|
488
|
+
denyMode = true;
|
|
489
|
+
reasonSection.classList.remove('hidden');
|
|
490
|
+
btnDeny.textContent = 'Confirm Deny';
|
|
491
|
+
reasonInput.focus();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
clearError();
|
|
495
|
+
disableButtons();
|
|
496
|
+
btnDeny.textContent = 'Denying...';
|
|
497
|
+
|
|
498
|
+
fetch('/api/reject', {
|
|
499
|
+
method: 'POST',
|
|
500
|
+
headers: { 'Content-Type': 'application/json' },
|
|
501
|
+
body: JSON.stringify({ token: token, reason: reasonInput.value || undefined })
|
|
502
|
+
})
|
|
503
|
+
.then(function(res) { return res.json(); })
|
|
504
|
+
.then(function(data) {
|
|
505
|
+
if (data.error) {
|
|
506
|
+
showError(data.error);
|
|
507
|
+
enableButtons();
|
|
508
|
+
btnDeny.textContent = 'Confirm Deny';
|
|
509
|
+
} else {
|
|
510
|
+
showSuccess(false);
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
.catch(function() {
|
|
514
|
+
showError('Failed to submit. Is the CLI still running?');
|
|
515
|
+
enableButtons();
|
|
516
|
+
btnDeny.textContent = 'Confirm Deny';
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
btnApprove.addEventListener('click', doApprove);
|
|
521
|
+
btnDeny.addEventListener('click', doReject);
|
|
522
|
+
input.addEventListener('keydown', function(e) {
|
|
523
|
+
if (e.key === 'Enter') doApprove();
|
|
524
|
+
});
|
|
525
|
+
})();
|
|
526
|
+
</script>
|
|
527
|
+
</body>
|
|
528
|
+
</html>`;
|
|
529
|
+
}
|
|
530
|
+
var init_approval_html = __esm({
|
|
531
|
+
"src/server/approval-html.ts"() {
|
|
532
|
+
"use strict";
|
|
533
|
+
init_cjs_shims();
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// src/utils/open-browser.ts
|
|
538
|
+
function openBrowser(url) {
|
|
539
|
+
const plat = (0, import_node_os2.platform)();
|
|
540
|
+
let cmd;
|
|
541
|
+
if (plat === "darwin") cmd = `open "${url}"`;
|
|
542
|
+
else if (plat === "win32") cmd = `start "" "${url}"`;
|
|
543
|
+
else cmd = `xdg-open "${url}"`;
|
|
544
|
+
(0, import_node_child_process.exec)(cmd, (err) => {
|
|
545
|
+
if (err) {
|
|
546
|
+
console.log(`Open ${url} in your browser.`);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
var import_node_child_process, import_node_os2;
|
|
551
|
+
var init_open_browser = __esm({
|
|
552
|
+
"src/utils/open-browser.ts"() {
|
|
553
|
+
"use strict";
|
|
554
|
+
init_cjs_shims();
|
|
555
|
+
import_node_child_process = require("child_process");
|
|
556
|
+
import_node_os2 = require("os");
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// src/server/approval-server.ts
|
|
561
|
+
var approval_server_exports = {};
|
|
562
|
+
__export(approval_server_exports, {
|
|
563
|
+
createApprovalServer: () => createApprovalServer,
|
|
564
|
+
requestBrowserApproval: () => requestBrowserApproval
|
|
565
|
+
});
|
|
566
|
+
function parseBody(req) {
|
|
567
|
+
return new Promise((resolve, reject) => {
|
|
568
|
+
const chunks = [];
|
|
569
|
+
let size = 0;
|
|
570
|
+
req.on("data", (chunk) => {
|
|
571
|
+
size += chunk.length;
|
|
572
|
+
if (size > MAX_BODY) {
|
|
573
|
+
req.destroy();
|
|
574
|
+
reject(new Error("Request body too large"));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
chunks.push(chunk);
|
|
578
|
+
});
|
|
579
|
+
req.on("end", () => {
|
|
580
|
+
try {
|
|
581
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
582
|
+
resolve(text ? JSON.parse(text) : {});
|
|
583
|
+
} catch {
|
|
584
|
+
reject(new Error("Invalid JSON"));
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
req.on("error", reject);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
function sendJson(res, status, body) {
|
|
591
|
+
const json = JSON.stringify(body);
|
|
592
|
+
res.writeHead(status, {
|
|
593
|
+
"Content-Type": "application/json",
|
|
594
|
+
"Content-Length": Buffer.byteLength(json)
|
|
595
|
+
});
|
|
596
|
+
res.end(json);
|
|
597
|
+
}
|
|
598
|
+
function sendHtml(res, html) {
|
|
599
|
+
res.writeHead(200, {
|
|
600
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
601
|
+
"Content-Length": Buffer.byteLength(html)
|
|
602
|
+
});
|
|
603
|
+
res.end(html);
|
|
604
|
+
}
|
|
605
|
+
function createApprovalServer(tx, tm, audit, options) {
|
|
606
|
+
const nonce = (0, import_node_crypto5.randomBytes)(32).toString("hex");
|
|
607
|
+
let settled = false;
|
|
608
|
+
let tokenUsed = false;
|
|
609
|
+
let resolvePromise;
|
|
610
|
+
let rejectPromise;
|
|
611
|
+
let timer;
|
|
612
|
+
let serverInstance;
|
|
613
|
+
const promise = new Promise((resolve, reject) => {
|
|
614
|
+
resolvePromise = resolve;
|
|
615
|
+
rejectPromise = reject;
|
|
616
|
+
});
|
|
617
|
+
function cleanup() {
|
|
618
|
+
clearTimeout(timer);
|
|
619
|
+
serverInstance.close();
|
|
620
|
+
}
|
|
621
|
+
serverInstance = (0, import_node_http.createServer)(async (req, res) => {
|
|
622
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
623
|
+
const method = req.method ?? "GET";
|
|
624
|
+
try {
|
|
625
|
+
if (method === "GET" && url.pathname === `/approve/${tx.id}`) {
|
|
626
|
+
const token = url.searchParams.get("token");
|
|
627
|
+
if (token !== nonce || tokenUsed) {
|
|
628
|
+
sendHtml(res, "<h1>Invalid or expired link.</h1>");
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
sendHtml(res, getApprovalHtml(nonce, tx));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (method === "POST" && url.pathname === "/api/approve") {
|
|
635
|
+
const body = await parseBody(req);
|
|
636
|
+
if (body.token !== nonce || tokenUsed) {
|
|
637
|
+
sendJson(res, 403, { error: "Invalid or expired token." });
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const passphrase = body.passphrase;
|
|
641
|
+
if (typeof passphrase !== "string" || !passphrase) {
|
|
642
|
+
sendJson(res, 400, { error: "Passphrase is required." });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const currentTx = tm.get(tx.id);
|
|
646
|
+
if (!currentTx || currentTx.status !== "pending") {
|
|
647
|
+
sendJson(res, 400, { error: "Transaction is no longer pending." });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const keyPath = options?.home ? (0, import_node_path8.join)(options.home, "keys", "private.pem") : void 0;
|
|
652
|
+
const privateKeyPem = loadPrivateKey(keyPath);
|
|
653
|
+
const txDetails = {
|
|
654
|
+
txId: tx.id,
|
|
655
|
+
merchant: tx.merchant,
|
|
656
|
+
amount: tx.amount,
|
|
657
|
+
description: tx.description,
|
|
658
|
+
timestamp: tx.createdAt
|
|
659
|
+
};
|
|
660
|
+
const mandate = createMandate(txDetails, privateKeyPem, passphrase);
|
|
661
|
+
tm.approve(tx.id, mandate);
|
|
662
|
+
audit.log("APPROVE", { txId: tx.id, source: "browser-approval", mandateSigned: true });
|
|
663
|
+
} catch (err) {
|
|
664
|
+
const msg = err instanceof Error ? err.message : "Signing failed";
|
|
665
|
+
sendJson(res, 400, { error: msg });
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
tokenUsed = true;
|
|
669
|
+
sendJson(res, 200, { ok: true });
|
|
670
|
+
if (!settled) {
|
|
671
|
+
settled = true;
|
|
672
|
+
cleanup();
|
|
673
|
+
resolvePromise({ action: "approved", passphrase });
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (method === "POST" && url.pathname === "/api/reject") {
|
|
678
|
+
const body = await parseBody(req);
|
|
679
|
+
if (body.token !== nonce || tokenUsed) {
|
|
680
|
+
sendJson(res, 403, { error: "Invalid or expired token." });
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const currentTx = tm.get(tx.id);
|
|
684
|
+
if (!currentTx || currentTx.status !== "pending") {
|
|
685
|
+
sendJson(res, 400, { error: "Transaction is no longer pending." });
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const reason = typeof body.reason === "string" ? body.reason : void 0;
|
|
689
|
+
tm.reject(tx.id, reason);
|
|
690
|
+
audit.log("REJECT", { txId: tx.id, source: "browser-approval", reason });
|
|
691
|
+
tokenUsed = true;
|
|
692
|
+
sendJson(res, 200, { ok: true });
|
|
693
|
+
if (!settled) {
|
|
694
|
+
settled = true;
|
|
695
|
+
cleanup();
|
|
696
|
+
resolvePromise({ action: "rejected", reason });
|
|
697
|
+
}
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
sendJson(res, 404, { error: "Not found" });
|
|
701
|
+
} catch (err) {
|
|
702
|
+
const message = err instanceof Error ? err.message : "Internal error";
|
|
703
|
+
sendJson(res, 500, { error: message });
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
timer = setTimeout(() => {
|
|
707
|
+
if (!settled) {
|
|
708
|
+
settled = true;
|
|
709
|
+
cleanup();
|
|
710
|
+
rejectPromise(new TimeoutError("Approval timed out after 5 minutes."));
|
|
711
|
+
}
|
|
712
|
+
}, TIMEOUT_MS);
|
|
713
|
+
serverInstance.on("error", (err) => {
|
|
714
|
+
if (!settled) {
|
|
715
|
+
settled = true;
|
|
716
|
+
cleanup();
|
|
717
|
+
rejectPromise(err);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
let portValue = 0;
|
|
721
|
+
const handle = {
|
|
722
|
+
server: serverInstance,
|
|
723
|
+
token: nonce,
|
|
724
|
+
get port() {
|
|
725
|
+
return portValue;
|
|
726
|
+
},
|
|
727
|
+
promise
|
|
728
|
+
};
|
|
729
|
+
serverInstance.listen(0, "127.0.0.1", () => {
|
|
730
|
+
const addr = serverInstance.address();
|
|
731
|
+
if (!addr || typeof addr === "string") {
|
|
732
|
+
if (!settled) {
|
|
733
|
+
settled = true;
|
|
734
|
+
cleanup();
|
|
735
|
+
rejectPromise(new Error("Failed to bind server"));
|
|
736
|
+
}
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
portValue = addr.port;
|
|
740
|
+
const pageUrl = `http://127.0.0.1:${addr.port}/approve/${tx.id}?token=${nonce}`;
|
|
741
|
+
if (options?.openBrowser !== false) {
|
|
742
|
+
console.log("Opening approval page in browser...");
|
|
743
|
+
openBrowser(pageUrl);
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
return handle;
|
|
747
|
+
}
|
|
748
|
+
function requestBrowserApproval(tx, tm, audit, home) {
|
|
749
|
+
const handle = createApprovalServer(tx, tm, audit, { openBrowser: true, home });
|
|
750
|
+
return handle.promise;
|
|
751
|
+
}
|
|
752
|
+
var import_node_http, import_node_crypto5, import_node_path8, TIMEOUT_MS, MAX_BODY;
|
|
753
|
+
var init_approval_server = __esm({
|
|
754
|
+
"src/server/approval-server.ts"() {
|
|
755
|
+
"use strict";
|
|
756
|
+
init_cjs_shims();
|
|
757
|
+
import_node_http = require("http");
|
|
758
|
+
import_node_crypto5 = require("crypto");
|
|
759
|
+
import_node_path8 = require("path");
|
|
760
|
+
init_approval_html();
|
|
761
|
+
init_open_browser();
|
|
762
|
+
init_keypair();
|
|
763
|
+
init_mandate();
|
|
764
|
+
init_errors();
|
|
765
|
+
TIMEOUT_MS = 5 * 60 * 1e3;
|
|
766
|
+
MAX_BODY = 4096;
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// src/tunnel/tunnel.ts
|
|
771
|
+
async function openTunnel(port) {
|
|
772
|
+
return new Promise((resolve, reject) => {
|
|
773
|
+
let child;
|
|
774
|
+
try {
|
|
775
|
+
child = (0, import_node_child_process2.spawn)("cloudflared", ["tunnel", "--url", `http://127.0.0.1:${port}`], {
|
|
776
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
777
|
+
});
|
|
778
|
+
} catch {
|
|
779
|
+
reject(new Error(
|
|
780
|
+
"cloudflared is required for mobile approval. Install it: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
|
|
781
|
+
));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
let resolved = false;
|
|
785
|
+
const timeout = setTimeout(() => {
|
|
786
|
+
if (!resolved) {
|
|
787
|
+
resolved = true;
|
|
788
|
+
child.kill();
|
|
789
|
+
reject(new Error("cloudflared tunnel timed out waiting for URL (15s). Is cloudflared installed?"));
|
|
790
|
+
}
|
|
791
|
+
}, 15e3);
|
|
792
|
+
const close = () => {
|
|
793
|
+
child.kill();
|
|
794
|
+
};
|
|
795
|
+
const chunks = [];
|
|
796
|
+
child.stderr?.on("data", (data) => {
|
|
797
|
+
const text = data.toString();
|
|
798
|
+
chunks.push(text);
|
|
799
|
+
const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
800
|
+
if (match && !resolved) {
|
|
801
|
+
resolved = true;
|
|
802
|
+
clearTimeout(timeout);
|
|
803
|
+
resolve({ url: match[0], close });
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
child.on("error", (err) => {
|
|
807
|
+
if (!resolved) {
|
|
808
|
+
resolved = true;
|
|
809
|
+
clearTimeout(timeout);
|
|
810
|
+
reject(new Error(
|
|
811
|
+
`cloudflared failed to start: ${err.message}. Install it: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/`
|
|
812
|
+
));
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
child.on("exit", (code) => {
|
|
816
|
+
if (!resolved) {
|
|
817
|
+
resolved = true;
|
|
818
|
+
clearTimeout(timeout);
|
|
819
|
+
const output = chunks.join("");
|
|
820
|
+
reject(new Error(
|
|
821
|
+
`cloudflared exited with code ${code}. Output: ${output.slice(0, 500)}`
|
|
822
|
+
));
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
var import_node_child_process2;
|
|
828
|
+
var init_tunnel = __esm({
|
|
829
|
+
"src/tunnel/tunnel.ts"() {
|
|
830
|
+
"use strict";
|
|
831
|
+
init_cjs_shims();
|
|
832
|
+
import_node_child_process2 = require("child_process");
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// src/notify/notify.ts
|
|
837
|
+
async function sendNotification(payload, options) {
|
|
838
|
+
const results = [];
|
|
839
|
+
if (options.command) {
|
|
840
|
+
const cmd = options.command.replace(/\{\{url\}\}/g, payload.url);
|
|
841
|
+
try {
|
|
842
|
+
await runCommand(cmd);
|
|
843
|
+
results.push({ method: "command", success: true });
|
|
844
|
+
} catch (err) {
|
|
845
|
+
results.push({
|
|
846
|
+
method: "command",
|
|
847
|
+
success: false,
|
|
848
|
+
error: err instanceof Error ? err.message : "Command failed"
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (options.webhookUrl) {
|
|
853
|
+
try {
|
|
854
|
+
const res = await fetch(options.webhookUrl, {
|
|
855
|
+
method: "POST",
|
|
856
|
+
headers: { "Content-Type": "application/json" },
|
|
857
|
+
body: JSON.stringify(payload)
|
|
858
|
+
});
|
|
859
|
+
if (!res.ok) {
|
|
860
|
+
results.push({
|
|
861
|
+
method: "webhook",
|
|
862
|
+
success: false,
|
|
863
|
+
error: `Webhook returned ${res.status}`
|
|
864
|
+
});
|
|
865
|
+
} else {
|
|
866
|
+
results.push({ method: "webhook", success: true });
|
|
867
|
+
}
|
|
868
|
+
} catch (err) {
|
|
869
|
+
results.push({
|
|
870
|
+
method: "webhook",
|
|
871
|
+
success: false,
|
|
872
|
+
error: err instanceof Error ? err.message : "Webhook failed"
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (results.length === 0) {
|
|
877
|
+
results.push({
|
|
878
|
+
method: "none",
|
|
879
|
+
success: false,
|
|
880
|
+
error: "No notification method configured. Set AGENTPAY_NOTIFY_COMMAND or AGENTPAY_NOTIFY_WEBHOOK."
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
return results;
|
|
884
|
+
}
|
|
885
|
+
function runCommand(cmd) {
|
|
886
|
+
return new Promise((resolve, reject) => {
|
|
887
|
+
(0, import_node_child_process3.execFile)("sh", ["-c", cmd], { timeout: 1e4 }, (err) => {
|
|
888
|
+
if (err) reject(err);
|
|
889
|
+
else resolve();
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
var import_node_child_process3;
|
|
894
|
+
var init_notify = __esm({
|
|
895
|
+
"src/notify/notify.ts"() {
|
|
896
|
+
"use strict";
|
|
897
|
+
init_cjs_shims();
|
|
898
|
+
import_node_child_process3 = require("child_process");
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
// src/server/mobile-approval-server.ts
|
|
903
|
+
var mobile_approval_server_exports = {};
|
|
904
|
+
__export(mobile_approval_server_exports, {
|
|
905
|
+
requestMobileApproval: () => requestMobileApproval
|
|
906
|
+
});
|
|
907
|
+
async function requestMobileApproval(tx, tm, audit, options) {
|
|
908
|
+
const handle = createApprovalServer(tx, tm, audit, {
|
|
909
|
+
openBrowser: false,
|
|
910
|
+
home: options.home
|
|
911
|
+
});
|
|
912
|
+
await waitForPort(handle);
|
|
913
|
+
let tunnel;
|
|
914
|
+
try {
|
|
915
|
+
tunnel = await openTunnel(handle.port);
|
|
916
|
+
const approvalUrl = `${tunnel.url}/approve/${tx.id}?token=${handle.token}`;
|
|
917
|
+
const payload = {
|
|
918
|
+
url: approvalUrl,
|
|
919
|
+
txId: tx.id,
|
|
920
|
+
merchant: tx.merchant,
|
|
921
|
+
amount: tx.amount
|
|
922
|
+
};
|
|
923
|
+
const notifyResults = await sendNotification(payload, options.notify);
|
|
924
|
+
audit.log("MOBILE_APPROVAL_REQUESTED", {
|
|
925
|
+
txId: tx.id,
|
|
926
|
+
tunnelUrl: tunnel.url,
|
|
927
|
+
notifyResults
|
|
928
|
+
});
|
|
929
|
+
const result = await handle.promise;
|
|
930
|
+
return {
|
|
931
|
+
...result,
|
|
932
|
+
approvalUrl,
|
|
933
|
+
notifyResults
|
|
934
|
+
};
|
|
935
|
+
} finally {
|
|
936
|
+
tunnel?.close();
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function waitForPort(handle, timeoutMs = 5e3) {
|
|
940
|
+
return new Promise((resolve, reject) => {
|
|
941
|
+
const start = Date.now();
|
|
942
|
+
const check = () => {
|
|
943
|
+
if (handle.port > 0) {
|
|
944
|
+
resolve();
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (Date.now() - start > timeoutMs) {
|
|
948
|
+
reject(new Error("Approval server failed to bind to a port"));
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
setTimeout(check, 50);
|
|
952
|
+
};
|
|
953
|
+
check();
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
var init_mobile_approval_server = __esm({
|
|
957
|
+
"src/server/mobile-approval-server.ts"() {
|
|
958
|
+
"use strict";
|
|
959
|
+
init_cjs_shims();
|
|
960
|
+
init_approval_server();
|
|
961
|
+
init_tunnel();
|
|
962
|
+
init_notify();
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// src/server/passphrase-html.ts
|
|
967
|
+
function esc2(s) {
|
|
968
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
969
|
+
}
|
|
970
|
+
function formatCurrency3(n) {
|
|
971
|
+
return "$" + n.toFixed(2);
|
|
972
|
+
}
|
|
973
|
+
function getPassphraseHtml(token, context) {
|
|
974
|
+
const actionLabel = context?.action === "approve" ? "Approve Transaction" : "Approve Purchase";
|
|
975
|
+
const buttonLabel = context?.action === "approve" ? "Unlock & Approve" : "Unlock & Approve";
|
|
976
|
+
let contextHtml = "";
|
|
977
|
+
if (context) {
|
|
978
|
+
const lines = [];
|
|
979
|
+
if (context.merchant) lines.push(`<div class="detail"><span class="detail-label">Merchant</span><span class="detail-value">${esc2(context.merchant)}</span></div>`);
|
|
980
|
+
if (context.amount !== void 0) lines.push(`<div class="detail"><span class="detail-label">Amount</span><span class="detail-value">${formatCurrency3(context.amount)}</span></div>`);
|
|
981
|
+
if (context.description) lines.push(`<div class="detail"><span class="detail-label">Description</span><span class="detail-value">${esc2(context.description)}</span></div>`);
|
|
982
|
+
if (context.txId) lines.push(`<div class="detail"><span class="detail-label">Transaction</span><span class="detail-value" style="font-family:monospace;font-size:12px;">${esc2(context.txId)}</span></div>`);
|
|
983
|
+
if (lines.length > 0) {
|
|
984
|
+
contextHtml = `<div class="card context-card">${lines.join("")}</div>`;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return `<!DOCTYPE html>
|
|
988
|
+
<html lang="en">
|
|
989
|
+
<head>
|
|
990
|
+
<meta charset="utf-8">
|
|
991
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
992
|
+
<title>AgentPay \u2014 Passphrase Required</title>
|
|
993
|
+
<style>
|
|
994
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
995
|
+
body {
|
|
996
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
997
|
+
background: #f5f5f5;
|
|
998
|
+
color: #111;
|
|
999
|
+
min-height: 100vh;
|
|
1000
|
+
display: flex;
|
|
1001
|
+
justify-content: center;
|
|
1002
|
+
padding: 40px 16px;
|
|
1003
|
+
}
|
|
1004
|
+
.container { width: 100%; max-width: 420px; }
|
|
1005
|
+
h1 { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
|
|
1006
|
+
.subtitle { color: #666; font-size: 14px; margin-bottom: 24px; }
|
|
1007
|
+
.card {
|
|
1008
|
+
background: #fff;
|
|
1009
|
+
border-radius: 8px;
|
|
1010
|
+
padding: 24px;
|
|
1011
|
+
margin-bottom: 16px;
|
|
1012
|
+
border: 1px solid #e0e0e0;
|
|
1013
|
+
}
|
|
1014
|
+
.context-card { padding: 16px 20px; }
|
|
1015
|
+
.detail {
|
|
1016
|
+
display: flex;
|
|
1017
|
+
justify-content: space-between;
|
|
1018
|
+
align-items: center;
|
|
1019
|
+
padding: 8px 0;
|
|
1020
|
+
border-bottom: 1px solid #f0f0f0;
|
|
1021
|
+
}
|
|
1022
|
+
.detail:last-child { border-bottom: none; }
|
|
1023
|
+
.detail-label { font-size: 13px; color: #666; }
|
|
1024
|
+
.detail-value { font-size: 14px; font-weight: 600; }
|
|
1025
|
+
label { display: block; font-size: 13px; font-weight: 500; color: #333; margin-bottom: 6px; }
|
|
1026
|
+
input[type="password"] {
|
|
1027
|
+
width: 100%;
|
|
1028
|
+
padding: 12px 14px;
|
|
1029
|
+
border: 1px solid #d0d0d0;
|
|
1030
|
+
border-radius: 8px;
|
|
1031
|
+
font-size: 15px;
|
|
1032
|
+
outline: none;
|
|
1033
|
+
transition: border-color 0.15s;
|
|
1034
|
+
}
|
|
1035
|
+
input[type="password"]:focus { border-color: #111; }
|
|
1036
|
+
button {
|
|
1037
|
+
width: 100%;
|
|
1038
|
+
padding: 12px;
|
|
1039
|
+
border: none;
|
|
1040
|
+
border-radius: 8px;
|
|
1041
|
+
font-size: 14px;
|
|
1042
|
+
font-weight: 600;
|
|
1043
|
+
cursor: pointer;
|
|
1044
|
+
transition: opacity 0.15s;
|
|
1045
|
+
margin-top: 16px;
|
|
1046
|
+
background: #111;
|
|
1047
|
+
color: #fff;
|
|
1048
|
+
}
|
|
1049
|
+
button:hover { opacity: 0.85; }
|
|
1050
|
+
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1051
|
+
.error { color: #c62828; font-size: 13px; margin-top: 10px; }
|
|
1052
|
+
.success-screen {
|
|
1053
|
+
text-align: center;
|
|
1054
|
+
padding: 40px 0;
|
|
1055
|
+
}
|
|
1056
|
+
.checkmark {
|
|
1057
|
+
font-size: 48px;
|
|
1058
|
+
margin-bottom: 16px;
|
|
1059
|
+
}
|
|
1060
|
+
.success-msg {
|
|
1061
|
+
font-size: 16px;
|
|
1062
|
+
font-weight: 600;
|
|
1063
|
+
margin-bottom: 8px;
|
|
1064
|
+
}
|
|
1065
|
+
.success-hint {
|
|
1066
|
+
font-size: 13px;
|
|
1067
|
+
color: #666;
|
|
1068
|
+
}
|
|
1069
|
+
.hidden { display: none; }
|
|
1070
|
+
</style>
|
|
1071
|
+
</head>
|
|
1072
|
+
<body>
|
|
1073
|
+
<div class="container">
|
|
1074
|
+
<div id="form-view">
|
|
1075
|
+
<h1>AgentPay</h1>
|
|
1076
|
+
<p class="subtitle">${esc2(actionLabel)}</p>
|
|
1077
|
+
${contextHtml}
|
|
1078
|
+
<div class="card">
|
|
1079
|
+
<label for="passphrase">Passphrase</label>
|
|
1080
|
+
<input type="password" id="passphrase" placeholder="Enter your passphrase" autofocus>
|
|
1081
|
+
<div id="error" class="error hidden"></div>
|
|
1082
|
+
<button id="submit">${buttonLabel}</button>
|
|
1083
|
+
</div>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div id="success-view" class="hidden">
|
|
1086
|
+
<h1>AgentPay</h1>
|
|
1087
|
+
<p class="subtitle">${esc2(actionLabel)}</p>
|
|
1088
|
+
<div class="card">
|
|
1089
|
+
<div class="success-screen">
|
|
1090
|
+
<div class="checkmark">✓</div>
|
|
1091
|
+
<div class="success-msg">Passphrase received</div>
|
|
1092
|
+
<div class="success-hint">You can close this tab.</div>
|
|
1093
|
+
</div>
|
|
1094
|
+
</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
<script>
|
|
1098
|
+
(function() {
|
|
1099
|
+
var token = ${JSON.stringify(token)};
|
|
1100
|
+
var form = document.getElementById('form-view');
|
|
1101
|
+
var success = document.getElementById('success-view');
|
|
1102
|
+
var input = document.getElementById('passphrase');
|
|
1103
|
+
var btn = document.getElementById('submit');
|
|
1104
|
+
var errDiv = document.getElementById('error');
|
|
1105
|
+
|
|
1106
|
+
function submit() {
|
|
1107
|
+
var passphrase = input.value;
|
|
1108
|
+
if (!passphrase) {
|
|
1109
|
+
errDiv.textContent = 'Passphrase is required.';
|
|
1110
|
+
errDiv.classList.remove('hidden');
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
btn.disabled = true;
|
|
1114
|
+
btn.textContent = 'Submitting...';
|
|
1115
|
+
errDiv.classList.add('hidden');
|
|
1116
|
+
|
|
1117
|
+
fetch('/passphrase', {
|
|
1118
|
+
method: 'POST',
|
|
1119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1120
|
+
body: JSON.stringify({ token: token, passphrase: passphrase })
|
|
1121
|
+
})
|
|
1122
|
+
.then(function(res) { return res.json(); })
|
|
1123
|
+
.then(function(data) {
|
|
1124
|
+
if (data.error) {
|
|
1125
|
+
errDiv.textContent = data.error;
|
|
1126
|
+
errDiv.classList.remove('hidden');
|
|
1127
|
+
btn.disabled = false;
|
|
1128
|
+
btn.textContent = '${buttonLabel}';
|
|
1129
|
+
} else {
|
|
1130
|
+
form.classList.add('hidden');
|
|
1131
|
+
success.classList.remove('hidden');
|
|
1132
|
+
}
|
|
1133
|
+
})
|
|
1134
|
+
.catch(function() {
|
|
1135
|
+
errDiv.textContent = 'Failed to submit. Is the CLI still running?';
|
|
1136
|
+
errDiv.classList.remove('hidden');
|
|
1137
|
+
btn.disabled = false;
|
|
1138
|
+
btn.textContent = '${buttonLabel}';
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
btn.addEventListener('click', submit);
|
|
1143
|
+
input.addEventListener('keydown', function(e) {
|
|
1144
|
+
if (e.key === 'Enter') submit();
|
|
1145
|
+
});
|
|
1146
|
+
})();
|
|
1147
|
+
</script>
|
|
1148
|
+
</body>
|
|
1149
|
+
</html>`;
|
|
1150
|
+
}
|
|
1151
|
+
var init_passphrase_html = __esm({
|
|
1152
|
+
"src/server/passphrase-html.ts"() {
|
|
1153
|
+
"use strict";
|
|
1154
|
+
init_cjs_shims();
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// src/server/passphrase-server.ts
|
|
1159
|
+
var passphrase_server_exports = {};
|
|
1160
|
+
__export(passphrase_server_exports, {
|
|
1161
|
+
collectPassphrase: () => collectPassphrase
|
|
1162
|
+
});
|
|
1163
|
+
function parseBody4(req) {
|
|
1164
|
+
return new Promise((resolve, reject) => {
|
|
1165
|
+
const chunks = [];
|
|
1166
|
+
let size = 0;
|
|
1167
|
+
req.on("data", (chunk) => {
|
|
1168
|
+
size += chunk.length;
|
|
1169
|
+
if (size > MAX_BODY4) {
|
|
1170
|
+
req.destroy();
|
|
1171
|
+
reject(new Error("Request body too large"));
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
chunks.push(chunk);
|
|
1175
|
+
});
|
|
1176
|
+
req.on("end", () => {
|
|
1177
|
+
try {
|
|
1178
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
1179
|
+
resolve(text ? JSON.parse(text) : {});
|
|
1180
|
+
} catch {
|
|
1181
|
+
reject(new Error("Invalid JSON"));
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
req.on("error", reject);
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
function sendJson4(res, status, body) {
|
|
1188
|
+
const json = JSON.stringify(body);
|
|
1189
|
+
res.writeHead(status, {
|
|
1190
|
+
"Content-Type": "application/json",
|
|
1191
|
+
"Content-Length": Buffer.byteLength(json)
|
|
1192
|
+
});
|
|
1193
|
+
res.end(json);
|
|
1194
|
+
}
|
|
1195
|
+
function sendHtml4(res, html) {
|
|
1196
|
+
res.writeHead(200, {
|
|
1197
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1198
|
+
"Content-Length": Buffer.byteLength(html)
|
|
1199
|
+
});
|
|
1200
|
+
res.end(html);
|
|
1201
|
+
}
|
|
1202
|
+
function collectPassphrase(context) {
|
|
1203
|
+
return new Promise((resolve, reject) => {
|
|
1204
|
+
const nonce = (0, import_node_crypto7.randomBytes)(32).toString("hex");
|
|
1205
|
+
let settled = false;
|
|
1206
|
+
const server = (0, import_node_http4.createServer)(async (req, res) => {
|
|
1207
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
1208
|
+
const method = req.method ?? "GET";
|
|
1209
|
+
try {
|
|
1210
|
+
if (method === "GET" && url.pathname === "/passphrase") {
|
|
1211
|
+
const token = url.searchParams.get("token");
|
|
1212
|
+
if (token !== nonce) {
|
|
1213
|
+
sendHtml4(res, "<h1>Invalid or expired link.</h1>");
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
sendHtml4(res, getPassphraseHtml(nonce, context));
|
|
1217
|
+
} else if (method === "POST" && url.pathname === "/passphrase") {
|
|
1218
|
+
const body = await parseBody4(req);
|
|
1219
|
+
if (body.token !== nonce) {
|
|
1220
|
+
sendJson4(res, 403, { error: "Invalid token." });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const passphrase = body.passphrase;
|
|
1224
|
+
if (typeof passphrase !== "string" || !passphrase) {
|
|
1225
|
+
sendJson4(res, 400, { error: "Passphrase is required." });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
sendJson4(res, 200, { ok: true });
|
|
1229
|
+
if (!settled) {
|
|
1230
|
+
settled = true;
|
|
1231
|
+
cleanup();
|
|
1232
|
+
resolve(passphrase);
|
|
1233
|
+
}
|
|
1234
|
+
} else {
|
|
1235
|
+
sendJson4(res, 404, { error: "Not found" });
|
|
1236
|
+
}
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
const message = err instanceof Error ? err.message : "Internal error";
|
|
1239
|
+
sendJson4(res, 500, { error: message });
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
const timer = setTimeout(() => {
|
|
1243
|
+
if (!settled) {
|
|
1244
|
+
settled = true;
|
|
1245
|
+
cleanup();
|
|
1246
|
+
reject(new TimeoutError("Passphrase entry timed out after 5 minutes."));
|
|
1247
|
+
}
|
|
1248
|
+
}, TIMEOUT_MS3);
|
|
1249
|
+
function cleanup() {
|
|
1250
|
+
clearTimeout(timer);
|
|
1251
|
+
server.close();
|
|
1252
|
+
}
|
|
1253
|
+
server.on("error", (err) => {
|
|
1254
|
+
if (!settled) {
|
|
1255
|
+
settled = true;
|
|
1256
|
+
cleanup();
|
|
1257
|
+
reject(err);
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1261
|
+
const addr = server.address();
|
|
1262
|
+
if (!addr || typeof addr === "string") {
|
|
1263
|
+
if (!settled) {
|
|
1264
|
+
settled = true;
|
|
1265
|
+
cleanup();
|
|
1266
|
+
reject(new Error("Failed to bind server"));
|
|
1267
|
+
}
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const url = `http://127.0.0.1:${addr.port}/passphrase?token=${nonce}`;
|
|
1271
|
+
console.log("Waiting for passphrase entry in browser...");
|
|
1272
|
+
openBrowser(url);
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
var import_node_http4, import_node_crypto7, TIMEOUT_MS3, MAX_BODY4;
|
|
1277
|
+
var init_passphrase_server = __esm({
|
|
1278
|
+
"src/server/passphrase-server.ts"() {
|
|
1279
|
+
"use strict";
|
|
1280
|
+
init_cjs_shims();
|
|
1281
|
+
import_node_http4 = require("http");
|
|
1282
|
+
import_node_crypto7 = require("crypto");
|
|
1283
|
+
init_passphrase_html();
|
|
1284
|
+
init_open_browser();
|
|
1285
|
+
init_errors();
|
|
1286
|
+
TIMEOUT_MS3 = 5 * 60 * 1e3;
|
|
1287
|
+
MAX_BODY4 = 4096;
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
|
|
140
1291
|
// src/index.ts
|
|
141
1292
|
var src_exports = {};
|
|
142
1293
|
__export(src_exports, {
|
|
@@ -173,11 +1324,25 @@ __export(src_exports, {
|
|
|
173
1324
|
getPlaceholderVariables: () => getPlaceholderVariables,
|
|
174
1325
|
getTransactionsPath: () => getTransactionsPath,
|
|
175
1326
|
getWalletPath: () => getWalletPath,
|
|
1327
|
+
loadConfig: () => loadConfig,
|
|
176
1328
|
loadPrivateKey: () => loadPrivateKey,
|
|
177
1329
|
loadPublicKey: () => loadPublicKey,
|
|
178
1330
|
loadVault: () => loadVault,
|
|
1331
|
+
openBrowser: () => openBrowser,
|
|
1332
|
+
openTunnel: () => openTunnel,
|
|
1333
|
+
promptConfirm: () => promptConfirm,
|
|
1334
|
+
promptInput: () => promptInput,
|
|
1335
|
+
promptPassphrase: () => promptPassphrase,
|
|
1336
|
+
promptPassphraseSafe: () => promptPassphraseSafe,
|
|
1337
|
+
requestBrowserApproval: () => requestBrowserApproval,
|
|
1338
|
+
requestBrowserSetup: () => requestBrowserSetup,
|
|
1339
|
+
requestMobileApproval: () => requestMobileApproval,
|
|
1340
|
+
saveConfig: () => saveConfig,
|
|
179
1341
|
saveKeyPair: () => saveKeyPair,
|
|
180
1342
|
saveVault: () => saveVault,
|
|
1343
|
+
sendNotification: () => sendNotification,
|
|
1344
|
+
setMobileMode: () => setMobileMode,
|
|
1345
|
+
startDashboardServer: () => startServer,
|
|
181
1346
|
verifyMandate: () => verifyMandate,
|
|
182
1347
|
waitForApproval: () => waitForApproval
|
|
183
1348
|
});
|
|
@@ -236,41 +1401,16 @@ function formatStatus(data) {
|
|
|
236
1401
|
return lines.join("\n");
|
|
237
1402
|
}
|
|
238
1403
|
|
|
239
|
-
// src/
|
|
240
|
-
|
|
241
|
-
var import_node_os = require("os");
|
|
242
|
-
var import_node_path = require("path");
|
|
243
|
-
function getHomePath() {
|
|
244
|
-
return process.env.AGENTPAY_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".agentpay");
|
|
245
|
-
}
|
|
246
|
-
function getCredentialsPath() {
|
|
247
|
-
return (0, import_node_path.join)(getHomePath(), "credentials.enc");
|
|
248
|
-
}
|
|
249
|
-
function getKeysPath() {
|
|
250
|
-
return (0, import_node_path.join)(getHomePath(), "keys");
|
|
251
|
-
}
|
|
252
|
-
function getPublicKeyPath() {
|
|
253
|
-
return (0, import_node_path.join)(getKeysPath(), "public.pem");
|
|
254
|
-
}
|
|
255
|
-
function getPrivateKeyPath() {
|
|
256
|
-
return (0, import_node_path.join)(getKeysPath(), "private.pem");
|
|
257
|
-
}
|
|
258
|
-
function getWalletPath() {
|
|
259
|
-
return (0, import_node_path.join)(getHomePath(), "wallet.json");
|
|
260
|
-
}
|
|
261
|
-
function getTransactionsPath() {
|
|
262
|
-
return (0, import_node_path.join)(getHomePath(), "transactions.json");
|
|
263
|
-
}
|
|
264
|
-
function getAuditPath() {
|
|
265
|
-
return (0, import_node_path.join)(getHomePath(), "audit.log");
|
|
266
|
-
}
|
|
1404
|
+
// src/index.ts
|
|
1405
|
+
init_paths();
|
|
267
1406
|
|
|
268
1407
|
// src/vault/vault.ts
|
|
269
1408
|
init_cjs_shims();
|
|
270
1409
|
var import_node_crypto2 = require("crypto");
|
|
271
|
-
var
|
|
1410
|
+
var import_node_fs2 = require("fs");
|
|
272
1411
|
var import_node_path2 = require("path");
|
|
273
1412
|
init_errors();
|
|
1413
|
+
init_paths();
|
|
274
1414
|
var ALGORITHM = "aes-256-gcm";
|
|
275
1415
|
var PBKDF2_ITERATIONS = 1e5;
|
|
276
1416
|
var PBKDF2_DIGEST = "sha512";
|
|
@@ -312,105 +1452,29 @@ function decrypt(vault, passphrase) {
|
|
|
312
1452
|
}
|
|
313
1453
|
function saveVault(vault, path) {
|
|
314
1454
|
const filePath = path ?? getCredentialsPath();
|
|
315
|
-
(0,
|
|
316
|
-
(0,
|
|
1455
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(filePath), { recursive: true });
|
|
1456
|
+
(0, import_node_fs2.writeFileSync)(filePath, JSON.stringify(vault, null, 2), { mode: 384 });
|
|
317
1457
|
}
|
|
318
1458
|
function loadVault(path) {
|
|
319
1459
|
const filePath = path ?? getCredentialsPath();
|
|
320
1460
|
try {
|
|
321
|
-
const data = (0,
|
|
1461
|
+
const data = (0, import_node_fs2.readFileSync)(filePath, "utf8");
|
|
322
1462
|
return JSON.parse(data);
|
|
323
1463
|
} catch {
|
|
324
1464
|
throw new NotSetupError();
|
|
325
1465
|
}
|
|
326
1466
|
}
|
|
327
1467
|
|
|
328
|
-
// src/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
var import_node_fs2 = require("fs");
|
|
332
|
-
var import_node_path3 = require("path");
|
|
333
|
-
function generateKeyPair(passphrase) {
|
|
334
|
-
const { publicKey, privateKey } = (0, import_node_crypto3.generateKeyPairSync)("ed25519", {
|
|
335
|
-
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
336
|
-
privateKeyEncoding: {
|
|
337
|
-
type: "pkcs8",
|
|
338
|
-
format: "pem",
|
|
339
|
-
cipher: "aes-256-cbc",
|
|
340
|
-
passphrase
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
return { publicKey, privateKey };
|
|
344
|
-
}
|
|
345
|
-
function saveKeyPair(keys, publicPath, privatePath) {
|
|
346
|
-
const pubPath = publicPath ?? getPublicKeyPath();
|
|
347
|
-
const privPath = privatePath ?? getPrivateKeyPath();
|
|
348
|
-
(0, import_node_fs2.mkdirSync)((0, import_node_path3.dirname)(pubPath), { recursive: true });
|
|
349
|
-
(0, import_node_fs2.writeFileSync)(pubPath, keys.publicKey, { mode: 420 });
|
|
350
|
-
(0, import_node_fs2.writeFileSync)(privPath, keys.privateKey, { mode: 384 });
|
|
351
|
-
}
|
|
352
|
-
function loadPublicKey(path) {
|
|
353
|
-
return (0, import_node_fs2.readFileSync)(path ?? getPublicKeyPath(), "utf8");
|
|
354
|
-
}
|
|
355
|
-
function loadPrivateKey(path) {
|
|
356
|
-
return (0, import_node_fs2.readFileSync)(path ?? getPrivateKeyPath(), "utf8");
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// src/auth/mandate.ts
|
|
360
|
-
init_cjs_shims();
|
|
361
|
-
var import_node_crypto4 = require("crypto");
|
|
362
|
-
function hashTransactionDetails(details) {
|
|
363
|
-
const canonical = JSON.stringify({
|
|
364
|
-
txId: details.txId,
|
|
365
|
-
merchant: details.merchant,
|
|
366
|
-
amount: details.amount,
|
|
367
|
-
description: details.description,
|
|
368
|
-
timestamp: details.timestamp
|
|
369
|
-
});
|
|
370
|
-
return (0, import_node_crypto4.createHash)("sha256").update(canonical).digest("hex");
|
|
371
|
-
}
|
|
372
|
-
function createMandate(txDetails, privateKeyPem, passphrase) {
|
|
373
|
-
const txHash = hashTransactionDetails(txDetails);
|
|
374
|
-
const data = Buffer.from(txHash);
|
|
375
|
-
const privateKey = (0, import_node_crypto4.createPrivateKey)({
|
|
376
|
-
key: privateKeyPem,
|
|
377
|
-
format: "pem",
|
|
378
|
-
type: "pkcs8",
|
|
379
|
-
passphrase
|
|
380
|
-
});
|
|
381
|
-
const signature = (0, import_node_crypto4.sign)(null, data, privateKey);
|
|
382
|
-
const publicKey = (0, import_node_crypto4.createPublicKey)(privateKey);
|
|
383
|
-
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
384
|
-
return {
|
|
385
|
-
txId: txDetails.txId,
|
|
386
|
-
txHash,
|
|
387
|
-
signature: signature.toString("base64"),
|
|
388
|
-
publicKey: publicKeyPem,
|
|
389
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
function verifyMandate(mandate, txDetails) {
|
|
393
|
-
try {
|
|
394
|
-
const txHash = hashTransactionDetails(txDetails);
|
|
395
|
-
if (txHash !== mandate.txHash) return false;
|
|
396
|
-
const data = Buffer.from(txHash);
|
|
397
|
-
const signature = Buffer.from(mandate.signature, "base64");
|
|
398
|
-
const publicKey = (0, import_node_crypto4.createPublicKey)({
|
|
399
|
-
key: mandate.publicKey,
|
|
400
|
-
format: "pem",
|
|
401
|
-
type: "spki"
|
|
402
|
-
});
|
|
403
|
-
return (0, import_node_crypto4.verify)(null, data, publicKey, signature);
|
|
404
|
-
} catch {
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
1468
|
+
// src/index.ts
|
|
1469
|
+
init_keypair();
|
|
1470
|
+
init_mandate();
|
|
408
1471
|
|
|
409
1472
|
// src/budget/budget.ts
|
|
410
1473
|
init_cjs_shims();
|
|
411
|
-
var
|
|
1474
|
+
var import_node_fs4 = require("fs");
|
|
412
1475
|
var import_node_path4 = require("path");
|
|
413
1476
|
init_errors();
|
|
1477
|
+
init_paths();
|
|
414
1478
|
var BudgetManager = class {
|
|
415
1479
|
walletPath;
|
|
416
1480
|
constructor(walletPath) {
|
|
@@ -418,15 +1482,15 @@ var BudgetManager = class {
|
|
|
418
1482
|
}
|
|
419
1483
|
getWallet() {
|
|
420
1484
|
try {
|
|
421
|
-
const data = (0,
|
|
1485
|
+
const data = (0, import_node_fs4.readFileSync)(this.walletPath, "utf8");
|
|
422
1486
|
return JSON.parse(data);
|
|
423
1487
|
} catch {
|
|
424
1488
|
throw new NotSetupError();
|
|
425
1489
|
}
|
|
426
1490
|
}
|
|
427
1491
|
saveWallet(wallet) {
|
|
428
|
-
(0,
|
|
429
|
-
(0,
|
|
1492
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(this.walletPath), { recursive: true });
|
|
1493
|
+
(0, import_node_fs4.writeFileSync)(this.walletPath, JSON.stringify(wallet, null, 2), { mode: 384 });
|
|
430
1494
|
}
|
|
431
1495
|
setBudget(amount) {
|
|
432
1496
|
let wallet;
|
|
@@ -484,10 +1548,47 @@ var BudgetManager = class {
|
|
|
484
1548
|
}
|
|
485
1549
|
};
|
|
486
1550
|
|
|
1551
|
+
// src/config/config.ts
|
|
1552
|
+
init_cjs_shims();
|
|
1553
|
+
var import_node_fs5 = require("fs");
|
|
1554
|
+
var import_node_path5 = require("path");
|
|
1555
|
+
init_paths();
|
|
1556
|
+
|
|
1557
|
+
// src/config/types.ts
|
|
1558
|
+
init_cjs_shims();
|
|
1559
|
+
var DEFAULT_CONFIG = {
|
|
1560
|
+
mobileMode: false
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
// src/config/config.ts
|
|
1564
|
+
function getConfigPath(home) {
|
|
1565
|
+
return (0, import_node_path5.join)(home ?? getHomePath(), "config.json");
|
|
1566
|
+
}
|
|
1567
|
+
function loadConfig(home) {
|
|
1568
|
+
try {
|
|
1569
|
+
const data = (0, import_node_fs5.readFileSync)(getConfigPath(home), "utf8");
|
|
1570
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(data) };
|
|
1571
|
+
} catch {
|
|
1572
|
+
return { ...DEFAULT_CONFIG };
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function saveConfig(config, home) {
|
|
1576
|
+
const path = getConfigPath(home);
|
|
1577
|
+
(0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
1578
|
+
(0, import_node_fs5.writeFileSync)(path, JSON.stringify(config, null, 2), { mode: 384 });
|
|
1579
|
+
}
|
|
1580
|
+
function setMobileMode(enabled, home) {
|
|
1581
|
+
const config = loadConfig(home);
|
|
1582
|
+
config.mobileMode = enabled;
|
|
1583
|
+
saveConfig(config, home);
|
|
1584
|
+
return config;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
487
1587
|
// src/transactions/manager.ts
|
|
488
1588
|
init_cjs_shims();
|
|
489
|
-
var
|
|
490
|
-
var
|
|
1589
|
+
var import_node_fs6 = require("fs");
|
|
1590
|
+
var import_node_path6 = require("path");
|
|
1591
|
+
init_paths();
|
|
491
1592
|
var TransactionManager = class {
|
|
492
1593
|
txPath;
|
|
493
1594
|
constructor(txPath) {
|
|
@@ -495,15 +1596,15 @@ var TransactionManager = class {
|
|
|
495
1596
|
}
|
|
496
1597
|
loadAll() {
|
|
497
1598
|
try {
|
|
498
|
-
const data = (0,
|
|
1599
|
+
const data = (0, import_node_fs6.readFileSync)(this.txPath, "utf8");
|
|
499
1600
|
return JSON.parse(data);
|
|
500
1601
|
} catch {
|
|
501
1602
|
return [];
|
|
502
1603
|
}
|
|
503
1604
|
}
|
|
504
1605
|
saveAll(transactions) {
|
|
505
|
-
(0,
|
|
506
|
-
(0,
|
|
1606
|
+
(0, import_node_fs6.mkdirSync)((0, import_node_path6.dirname)(this.txPath), { recursive: true });
|
|
1607
|
+
(0, import_node_fs6.writeFileSync)(this.txPath, JSON.stringify(transactions, null, 2), { mode: 384 });
|
|
507
1608
|
}
|
|
508
1609
|
propose(options) {
|
|
509
1610
|
const transactions = this.loadAll();
|
|
@@ -583,8 +1684,9 @@ init_poller();
|
|
|
583
1684
|
|
|
584
1685
|
// src/audit/logger.ts
|
|
585
1686
|
init_cjs_shims();
|
|
586
|
-
var
|
|
587
|
-
var
|
|
1687
|
+
var import_node_fs7 = require("fs");
|
|
1688
|
+
var import_node_path7 = require("path");
|
|
1689
|
+
init_paths();
|
|
588
1690
|
var AuditLogger = class {
|
|
589
1691
|
logPath;
|
|
590
1692
|
constructor(logPath) {
|
|
@@ -595,12 +1697,12 @@ var AuditLogger = class {
|
|
|
595
1697
|
const detailsStr = JSON.stringify(details);
|
|
596
1698
|
const entry = `${timestamp} ${action} ${detailsStr}
|
|
597
1699
|
`;
|
|
598
|
-
(0,
|
|
599
|
-
(0,
|
|
1700
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path7.dirname)(this.logPath), { recursive: true });
|
|
1701
|
+
(0, import_node_fs7.appendFileSync)(this.logPath, entry, { mode: 384 });
|
|
600
1702
|
}
|
|
601
1703
|
getLog() {
|
|
602
1704
|
try {
|
|
603
|
-
const data = (0,
|
|
1705
|
+
const data = (0, import_node_fs7.readFileSync)(this.logPath, "utf8");
|
|
604
1706
|
return data.trim().split("\n").filter(Boolean);
|
|
605
1707
|
} catch {
|
|
606
1708
|
return [];
|
|
@@ -610,7 +1712,6 @@ var AuditLogger = class {
|
|
|
610
1712
|
|
|
611
1713
|
// src/executor/executor.ts
|
|
612
1714
|
init_cjs_shims();
|
|
613
|
-
var import_stagehand = require("@browserbasehq/stagehand");
|
|
614
1715
|
init_errors();
|
|
615
1716
|
|
|
616
1717
|
// src/executor/placeholder.ts
|
|
@@ -674,29 +1775,33 @@ function credentialsToSwapMap(creds) {
|
|
|
674
1775
|
};
|
|
675
1776
|
}
|
|
676
1777
|
|
|
1778
|
+
// src/executor/providers/local-provider.ts
|
|
1779
|
+
init_cjs_shims();
|
|
1780
|
+
var import_stagehand = require("@browserbasehq/stagehand");
|
|
1781
|
+
var LocalBrowserProvider = class {
|
|
1782
|
+
createStagehand(modelApiKey) {
|
|
1783
|
+
return new import_stagehand.Stagehand({
|
|
1784
|
+
env: "LOCAL",
|
|
1785
|
+
model: modelApiKey ? { modelName: "claude-3-7-sonnet-latest", apiKey: modelApiKey } : void 0
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
async close() {
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
|
|
677
1792
|
// src/executor/executor.ts
|
|
678
1793
|
var PurchaseExecutor = class {
|
|
679
|
-
|
|
1794
|
+
provider;
|
|
1795
|
+
modelApiKey;
|
|
680
1796
|
stagehand = null;
|
|
1797
|
+
proxyUrl;
|
|
1798
|
+
originalBaseUrl;
|
|
681
1799
|
constructor(config) {
|
|
682
|
-
this.
|
|
683
|
-
|
|
684
|
-
browserbaseProjectId: config?.browserbaseProjectId ?? process.env.BROWSERBASE_PROJECT_ID,
|
|
685
|
-
modelApiKey: config?.modelApiKey ?? process.env.ANTHROPIC_API_KEY
|
|
686
|
-
};
|
|
1800
|
+
this.provider = config?.provider ?? new LocalBrowserProvider();
|
|
1801
|
+
this.modelApiKey = config?.modelApiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
687
1802
|
}
|
|
688
1803
|
createStagehand() {
|
|
689
|
-
return
|
|
690
|
-
env: "BROWSERBASE",
|
|
691
|
-
apiKey: this.config.browserbaseApiKey,
|
|
692
|
-
projectId: this.config.browserbaseProjectId,
|
|
693
|
-
model: this.config.modelApiKey ? { modelName: "claude-3-7-sonnet-latest", apiKey: this.config.modelApiKey } : void 0,
|
|
694
|
-
browserbaseSessionCreateParams: {
|
|
695
|
-
browserSettings: {
|
|
696
|
-
recordSession: false
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
});
|
|
1804
|
+
return this.provider.createStagehand(this.modelApiKey);
|
|
700
1805
|
}
|
|
701
1806
|
/**
|
|
702
1807
|
* Phase 1: Open browser, navigate to URL, extract price and product info.
|
|
@@ -856,13 +1961,1295 @@ var PurchaseExecutor = class {
|
|
|
856
1961
|
this.stagehand = null;
|
|
857
1962
|
}
|
|
858
1963
|
} catch {
|
|
1964
|
+
} finally {
|
|
1965
|
+
await this.provider.close();
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
// src/index.ts
|
|
1971
|
+
init_approval_server();
|
|
1972
|
+
init_mobile_approval_server();
|
|
1973
|
+
init_tunnel();
|
|
1974
|
+
init_notify();
|
|
1975
|
+
|
|
1976
|
+
// src/server/setup-server.ts
|
|
1977
|
+
init_cjs_shims();
|
|
1978
|
+
var import_node_http2 = require("http");
|
|
1979
|
+
var import_node_crypto6 = require("crypto");
|
|
1980
|
+
var import_node_fs8 = require("fs");
|
|
1981
|
+
var import_node_path9 = require("path");
|
|
1982
|
+
|
|
1983
|
+
// src/server/setup-html.ts
|
|
1984
|
+
init_cjs_shims();
|
|
1985
|
+
function getSetupHtml(token) {
|
|
1986
|
+
return `<!DOCTYPE html>
|
|
1987
|
+
<html lang="en">
|
|
1988
|
+
<head>
|
|
1989
|
+
<meta charset="utf-8">
|
|
1990
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1991
|
+
<title>AgentPay \u2014 Setup</title>
|
|
1992
|
+
<style>
|
|
1993
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1994
|
+
body {
|
|
1995
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
1996
|
+
background: #f5f5f5;
|
|
1997
|
+
color: #111;
|
|
1998
|
+
min-height: 100vh;
|
|
1999
|
+
display: flex;
|
|
2000
|
+
justify-content: center;
|
|
2001
|
+
padding: 40px 16px;
|
|
2002
|
+
}
|
|
2003
|
+
.container { width: 100%; max-width: 480px; }
|
|
2004
|
+
h1 { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
|
|
2005
|
+
.subtitle { color: #666; font-size: 14px; margin-bottom: 24px; }
|
|
2006
|
+
.card {
|
|
2007
|
+
background: #fff;
|
|
2008
|
+
border-radius: 8px;
|
|
2009
|
+
padding: 24px;
|
|
2010
|
+
margin-bottom: 16px;
|
|
2011
|
+
border: 1px solid #e0e0e0;
|
|
2012
|
+
}
|
|
2013
|
+
.card-title {
|
|
2014
|
+
font-size: 15px;
|
|
2015
|
+
font-weight: 600;
|
|
2016
|
+
margin-bottom: 16px;
|
|
2017
|
+
padding-bottom: 8px;
|
|
2018
|
+
border-bottom: 1px solid #f0f0f0;
|
|
2019
|
+
}
|
|
2020
|
+
label { display: block; font-size: 13px; font-weight: 500; color: #333; margin-bottom: 6px; }
|
|
2021
|
+
input[type="text"], input[type="password"], input[type="email"], input[type="tel"], input[type="number"] {
|
|
2022
|
+
width: 100%;
|
|
2023
|
+
padding: 12px 14px;
|
|
2024
|
+
border: 1px solid #d0d0d0;
|
|
2025
|
+
border-radius: 8px;
|
|
2026
|
+
font-size: 15px;
|
|
2027
|
+
font-family: inherit;
|
|
2028
|
+
outline: none;
|
|
2029
|
+
transition: border-color 0.15s;
|
|
2030
|
+
}
|
|
2031
|
+
input:focus { border-color: #111; }
|
|
2032
|
+
.field { margin-bottom: 14px; }
|
|
2033
|
+
.field:last-child { margin-bottom: 0; }
|
|
2034
|
+
.row { display: flex; gap: 12px; }
|
|
2035
|
+
.row > .field { flex: 1; }
|
|
2036
|
+
.checkbox-row {
|
|
2037
|
+
display: flex;
|
|
2038
|
+
align-items: center;
|
|
2039
|
+
gap: 8px;
|
|
2040
|
+
margin-bottom: 14px;
|
|
2041
|
+
}
|
|
2042
|
+
.checkbox-row input[type="checkbox"] {
|
|
2043
|
+
width: 16px;
|
|
2044
|
+
height: 16px;
|
|
2045
|
+
cursor: pointer;
|
|
2046
|
+
}
|
|
2047
|
+
.checkbox-row label {
|
|
2048
|
+
margin-bottom: 0;
|
|
2049
|
+
cursor: pointer;
|
|
2050
|
+
font-size: 14px;
|
|
2051
|
+
}
|
|
2052
|
+
.btn-submit {
|
|
2053
|
+
width: 100%;
|
|
2054
|
+
padding: 14px;
|
|
2055
|
+
border: none;
|
|
2056
|
+
border-radius: 8px;
|
|
2057
|
+
font-size: 15px;
|
|
2058
|
+
font-weight: 600;
|
|
2059
|
+
cursor: pointer;
|
|
2060
|
+
background: #111;
|
|
2061
|
+
color: #fff;
|
|
2062
|
+
transition: opacity 0.15s;
|
|
2063
|
+
}
|
|
2064
|
+
.btn-submit:hover { opacity: 0.85; }
|
|
2065
|
+
.btn-submit:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
2066
|
+
.error { color: #c62828; font-size: 13px; margin-top: 10px; }
|
|
2067
|
+
.success-screen {
|
|
2068
|
+
text-align: center;
|
|
2069
|
+
padding: 40px 0;
|
|
2070
|
+
}
|
|
2071
|
+
.checkmark { font-size: 48px; margin-bottom: 16px; color: #2e7d32; }
|
|
2072
|
+
.success-msg { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
|
|
2073
|
+
.success-hint { font-size: 13px; color: #666; }
|
|
2074
|
+
.hidden { display: none; }
|
|
2075
|
+
.hint { font-size: 12px; color: #999; margin-top: 4px; }
|
|
2076
|
+
</style>
|
|
2077
|
+
</head>
|
|
2078
|
+
<body>
|
|
2079
|
+
<div class="container">
|
|
2080
|
+
<div id="form-view">
|
|
2081
|
+
<h1>AgentPay</h1>
|
|
2082
|
+
<p class="subtitle">Initial Setup</p>
|
|
2083
|
+
|
|
2084
|
+
<div class="card">
|
|
2085
|
+
<div class="card-title">Passphrase</div>
|
|
2086
|
+
<div class="field">
|
|
2087
|
+
<label for="passphrase">Choose a passphrase</label>
|
|
2088
|
+
<input type="password" id="passphrase" placeholder="Enter passphrase" autofocus>
|
|
2089
|
+
</div>
|
|
2090
|
+
<div class="field">
|
|
2091
|
+
<label for="passphrase-confirm">Confirm passphrase</label>
|
|
2092
|
+
<input type="password" id="passphrase-confirm" placeholder="Re-enter passphrase">
|
|
2093
|
+
</div>
|
|
2094
|
+
</div>
|
|
2095
|
+
|
|
2096
|
+
<div class="card">
|
|
2097
|
+
<div class="card-title">Budget</div>
|
|
2098
|
+
<div class="row">
|
|
2099
|
+
<div class="field">
|
|
2100
|
+
<label for="budget">Total budget ($)</label>
|
|
2101
|
+
<input type="number" id="budget" placeholder="0" min="0" step="0.01">
|
|
2102
|
+
</div>
|
|
2103
|
+
<div class="field">
|
|
2104
|
+
<label for="limit-per-tx">Per-transaction limit ($)</label>
|
|
2105
|
+
<input type="number" id="limit-per-tx" placeholder="0" min="0" step="0.01">
|
|
2106
|
+
</div>
|
|
2107
|
+
</div>
|
|
2108
|
+
<div class="hint">Leave at 0 to set later via <code>agentpay budget</code>.</div>
|
|
2109
|
+
</div>
|
|
2110
|
+
|
|
2111
|
+
<div class="card">
|
|
2112
|
+
<div class="card-title">Card</div>
|
|
2113
|
+
<div class="field">
|
|
2114
|
+
<label for="card-number">Card number</label>
|
|
2115
|
+
<input type="text" id="card-number" placeholder="4111 1111 1111 1111" autocomplete="cc-number">
|
|
2116
|
+
</div>
|
|
2117
|
+
<div class="row">
|
|
2118
|
+
<div class="field">
|
|
2119
|
+
<label for="card-expiry">Expiry (MM/YY)</label>
|
|
2120
|
+
<input type="text" id="card-expiry" placeholder="MM/YY" autocomplete="cc-exp" maxlength="5">
|
|
2121
|
+
</div>
|
|
2122
|
+
<div class="field">
|
|
2123
|
+
<label for="card-cvv">CVV</label>
|
|
2124
|
+
<input type="text" id="card-cvv" placeholder="123" autocomplete="cc-csc" maxlength="4">
|
|
2125
|
+
</div>
|
|
2126
|
+
</div>
|
|
2127
|
+
</div>
|
|
2128
|
+
|
|
2129
|
+
<div class="card">
|
|
2130
|
+
<div class="card-title">Personal</div>
|
|
2131
|
+
<div class="field">
|
|
2132
|
+
<label for="name">Full name</label>
|
|
2133
|
+
<input type="text" id="name" placeholder="Jane Doe" autocomplete="name">
|
|
2134
|
+
</div>
|
|
2135
|
+
<div class="row">
|
|
2136
|
+
<div class="field">
|
|
2137
|
+
<label for="email">Email</label>
|
|
2138
|
+
<input type="email" id="email" placeholder="jane@example.com" autocomplete="email">
|
|
2139
|
+
</div>
|
|
2140
|
+
<div class="field">
|
|
2141
|
+
<label for="phone">Phone</label>
|
|
2142
|
+
<input type="tel" id="phone" placeholder="+1 555-0100" autocomplete="tel">
|
|
2143
|
+
</div>
|
|
2144
|
+
</div>
|
|
2145
|
+
</div>
|
|
2146
|
+
|
|
2147
|
+
<div class="card">
|
|
2148
|
+
<div class="card-title">Billing Address</div>
|
|
2149
|
+
<div class="field">
|
|
2150
|
+
<label for="billing-street">Street</label>
|
|
2151
|
+
<input type="text" id="billing-street" autocomplete="billing street-address">
|
|
2152
|
+
</div>
|
|
2153
|
+
<div class="row">
|
|
2154
|
+
<div class="field">
|
|
2155
|
+
<label for="billing-city">City</label>
|
|
2156
|
+
<input type="text" id="billing-city" autocomplete="billing address-level2">
|
|
2157
|
+
</div>
|
|
2158
|
+
<div class="field">
|
|
2159
|
+
<label for="billing-state">State</label>
|
|
2160
|
+
<input type="text" id="billing-state" autocomplete="billing address-level1">
|
|
2161
|
+
</div>
|
|
2162
|
+
</div>
|
|
2163
|
+
<div class="row">
|
|
2164
|
+
<div class="field">
|
|
2165
|
+
<label for="billing-zip">ZIP</label>
|
|
2166
|
+
<input type="text" id="billing-zip" autocomplete="billing postal-code">
|
|
2167
|
+
</div>
|
|
2168
|
+
<div class="field">
|
|
2169
|
+
<label for="billing-country">Country</label>
|
|
2170
|
+
<input type="text" id="billing-country" placeholder="US" autocomplete="billing country">
|
|
2171
|
+
</div>
|
|
2172
|
+
</div>
|
|
2173
|
+
</div>
|
|
2174
|
+
|
|
2175
|
+
<div class="card">
|
|
2176
|
+
<div class="card-title">Shipping Address</div>
|
|
2177
|
+
<div class="checkbox-row">
|
|
2178
|
+
<input type="checkbox" id="same-as-billing" checked>
|
|
2179
|
+
<label for="same-as-billing">Same as billing</label>
|
|
2180
|
+
</div>
|
|
2181
|
+
<div id="shipping-fields" class="hidden">
|
|
2182
|
+
<div class="field">
|
|
2183
|
+
<label for="shipping-street">Street</label>
|
|
2184
|
+
<input type="text" id="shipping-street" autocomplete="shipping street-address">
|
|
2185
|
+
</div>
|
|
2186
|
+
<div class="row">
|
|
2187
|
+
<div class="field">
|
|
2188
|
+
<label for="shipping-city">City</label>
|
|
2189
|
+
<input type="text" id="shipping-city" autocomplete="shipping address-level2">
|
|
2190
|
+
</div>
|
|
2191
|
+
<div class="field">
|
|
2192
|
+
<label for="shipping-state">State</label>
|
|
2193
|
+
<input type="text" id="shipping-state" autocomplete="shipping address-level1">
|
|
2194
|
+
</div>
|
|
2195
|
+
</div>
|
|
2196
|
+
<div class="row">
|
|
2197
|
+
<div class="field">
|
|
2198
|
+
<label for="shipping-zip">ZIP</label>
|
|
2199
|
+
<input type="text" id="shipping-zip" autocomplete="shipping postal-code">
|
|
2200
|
+
</div>
|
|
2201
|
+
<div class="field">
|
|
2202
|
+
<label for="shipping-country">Country</label>
|
|
2203
|
+
<input type="text" id="shipping-country" placeholder="US" autocomplete="shipping country">
|
|
2204
|
+
</div>
|
|
2205
|
+
</div>
|
|
2206
|
+
</div>
|
|
2207
|
+
</div>
|
|
2208
|
+
|
|
2209
|
+
<div id="error" class="error hidden"></div>
|
|
2210
|
+
<button class="btn-submit" id="btn-submit">Complete Setup</button>
|
|
2211
|
+
</div>
|
|
2212
|
+
|
|
2213
|
+
<div id="success-view" class="hidden">
|
|
2214
|
+
<h1>AgentPay</h1>
|
|
2215
|
+
<p class="subtitle">Setup Complete</p>
|
|
2216
|
+
<div class="card">
|
|
2217
|
+
<div class="success-screen">
|
|
2218
|
+
<div class="checkmark">✓</div>
|
|
2219
|
+
<div class="success-msg">Your wallet is ready.</div>
|
|
2220
|
+
<div class="success-hint">You can close this tab and return to the terminal.</div>
|
|
2221
|
+
</div>
|
|
2222
|
+
</div>
|
|
2223
|
+
</div>
|
|
2224
|
+
</div>
|
|
2225
|
+
<script>
|
|
2226
|
+
(function() {
|
|
2227
|
+
var token = ${JSON.stringify(token)};
|
|
2228
|
+
var formView = document.getElementById('form-view');
|
|
2229
|
+
var successView = document.getElementById('success-view');
|
|
2230
|
+
var btnSubmit = document.getElementById('btn-submit');
|
|
2231
|
+
var errDiv = document.getElementById('error');
|
|
2232
|
+
var sameAsBilling = document.getElementById('same-as-billing');
|
|
2233
|
+
var shippingFields = document.getElementById('shipping-fields');
|
|
2234
|
+
|
|
2235
|
+
sameAsBilling.addEventListener('change', function() {
|
|
2236
|
+
shippingFields.classList.toggle('hidden', sameAsBilling.checked);
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
function val(id) { return document.getElementById(id).value.trim(); }
|
|
2240
|
+
|
|
2241
|
+
function showError(msg) {
|
|
2242
|
+
errDiv.textContent = msg;
|
|
2243
|
+
errDiv.classList.remove('hidden');
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function clearError() {
|
|
2247
|
+
errDiv.classList.add('hidden');
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
btnSubmit.addEventListener('click', function() {
|
|
2251
|
+
clearError();
|
|
2252
|
+
|
|
2253
|
+
var passphrase = val('passphrase');
|
|
2254
|
+
var passphraseConfirm = val('passphrase-confirm');
|
|
2255
|
+
|
|
2256
|
+
if (!passphrase) { showError('Passphrase is required.'); return; }
|
|
2257
|
+
if (passphrase !== passphraseConfirm) { showError('Passphrases do not match.'); return; }
|
|
2258
|
+
|
|
2259
|
+
var cardNumber = val('card-number');
|
|
2260
|
+
var cardExpiry = val('card-expiry');
|
|
2261
|
+
var cardCvv = val('card-cvv');
|
|
2262
|
+
if (!cardNumber || !cardExpiry || !cardCvv) { showError('Card number, expiry, and CVV are required.'); return; }
|
|
2263
|
+
|
|
2264
|
+
var name = val('name');
|
|
2265
|
+
var email = val('email');
|
|
2266
|
+
var phone = val('phone');
|
|
2267
|
+
if (!name || !email || !phone) { showError('Name, email, and phone are required.'); return; }
|
|
2268
|
+
|
|
2269
|
+
var billingStreet = val('billing-street');
|
|
2270
|
+
var billingCity = val('billing-city');
|
|
2271
|
+
var billingState = val('billing-state');
|
|
2272
|
+
var billingZip = val('billing-zip');
|
|
2273
|
+
var billingCountry = val('billing-country');
|
|
2274
|
+
if (!billingStreet || !billingCity || !billingState || !billingZip || !billingCountry) {
|
|
2275
|
+
showError('All billing address fields are required.'); return;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
var shippingStreet, shippingCity, shippingState, shippingZip, shippingCountry;
|
|
2279
|
+
if (sameAsBilling.checked) {
|
|
2280
|
+
shippingStreet = billingStreet;
|
|
2281
|
+
shippingCity = billingCity;
|
|
2282
|
+
shippingState = billingState;
|
|
2283
|
+
shippingZip = billingZip;
|
|
2284
|
+
shippingCountry = billingCountry;
|
|
2285
|
+
} else {
|
|
2286
|
+
shippingStreet = val('shipping-street');
|
|
2287
|
+
shippingCity = val('shipping-city');
|
|
2288
|
+
shippingState = val('shipping-state');
|
|
2289
|
+
shippingZip = val('shipping-zip');
|
|
2290
|
+
shippingCountry = val('shipping-country');
|
|
2291
|
+
if (!shippingStreet || !shippingCity || !shippingState || !shippingZip || !shippingCountry) {
|
|
2292
|
+
showError('All shipping address fields are required.'); return;
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
var budget = parseFloat(document.getElementById('budget').value) || 0;
|
|
2297
|
+
var limitPerTx = parseFloat(document.getElementById('limit-per-tx').value) || 0;
|
|
2298
|
+
|
|
2299
|
+
btnSubmit.disabled = true;
|
|
2300
|
+
btnSubmit.textContent = 'Setting up...';
|
|
2301
|
+
|
|
2302
|
+
fetch('/api/setup', {
|
|
2303
|
+
method: 'POST',
|
|
2304
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2305
|
+
body: JSON.stringify({
|
|
2306
|
+
token: token,
|
|
2307
|
+
passphrase: passphrase,
|
|
2308
|
+
budget: budget,
|
|
2309
|
+
limitPerTx: limitPerTx,
|
|
2310
|
+
card: { number: cardNumber, expiry: cardExpiry, cvv: cardCvv },
|
|
2311
|
+
name: name,
|
|
2312
|
+
email: email,
|
|
2313
|
+
phone: phone,
|
|
2314
|
+
billingAddress: { street: billingStreet, city: billingCity, state: billingState, zip: billingZip, country: billingCountry },
|
|
2315
|
+
shippingAddress: { street: shippingStreet, city: shippingCity, state: shippingState, zip: shippingZip, country: shippingCountry }
|
|
2316
|
+
})
|
|
2317
|
+
})
|
|
2318
|
+
.then(function(res) { return res.json(); })
|
|
2319
|
+
.then(function(data) {
|
|
2320
|
+
if (data.error) {
|
|
2321
|
+
showError(data.error);
|
|
2322
|
+
btnSubmit.disabled = false;
|
|
2323
|
+
btnSubmit.textContent = 'Complete Setup';
|
|
2324
|
+
} else {
|
|
2325
|
+
formView.classList.add('hidden');
|
|
2326
|
+
successView.classList.remove('hidden');
|
|
2327
|
+
setTimeout(function() { window.close(); }, 3000);
|
|
2328
|
+
}
|
|
2329
|
+
})
|
|
2330
|
+
.catch(function() {
|
|
2331
|
+
showError('Failed to submit. Is the CLI still running?');
|
|
2332
|
+
btnSubmit.disabled = false;
|
|
2333
|
+
btnSubmit.textContent = 'Complete Setup';
|
|
2334
|
+
});
|
|
2335
|
+
});
|
|
2336
|
+
})();
|
|
2337
|
+
</script>
|
|
2338
|
+
</body>
|
|
2339
|
+
</html>`;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// src/server/setup-server.ts
|
|
2343
|
+
init_open_browser();
|
|
2344
|
+
init_keypair();
|
|
2345
|
+
init_paths();
|
|
2346
|
+
init_errors();
|
|
2347
|
+
var TIMEOUT_MS2 = 10 * 60 * 1e3;
|
|
2348
|
+
var MAX_BODY2 = 8192;
|
|
2349
|
+
function parseBody2(req) {
|
|
2350
|
+
return new Promise((resolve, reject) => {
|
|
2351
|
+
const chunks = [];
|
|
2352
|
+
let size = 0;
|
|
2353
|
+
req.on("data", (chunk) => {
|
|
2354
|
+
size += chunk.length;
|
|
2355
|
+
if (size > MAX_BODY2) {
|
|
2356
|
+
req.destroy();
|
|
2357
|
+
reject(new Error("Request body too large"));
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
chunks.push(chunk);
|
|
2361
|
+
});
|
|
2362
|
+
req.on("end", () => {
|
|
2363
|
+
try {
|
|
2364
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
2365
|
+
resolve(text ? JSON.parse(text) : {});
|
|
2366
|
+
} catch {
|
|
2367
|
+
reject(new Error("Invalid JSON"));
|
|
2368
|
+
}
|
|
2369
|
+
});
|
|
2370
|
+
req.on("error", reject);
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
function sendJson2(res, status, body) {
|
|
2374
|
+
const json = JSON.stringify(body);
|
|
2375
|
+
res.writeHead(status, {
|
|
2376
|
+
"Content-Type": "application/json",
|
|
2377
|
+
"Content-Length": Buffer.byteLength(json)
|
|
2378
|
+
});
|
|
2379
|
+
res.end(json);
|
|
2380
|
+
}
|
|
2381
|
+
function sendHtml2(res, html) {
|
|
2382
|
+
res.writeHead(200, {
|
|
2383
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2384
|
+
"Content-Length": Buffer.byteLength(html)
|
|
2385
|
+
});
|
|
2386
|
+
res.end(html);
|
|
2387
|
+
}
|
|
2388
|
+
function isNonEmptyString(val) {
|
|
2389
|
+
return typeof val === "string" && val.length > 0;
|
|
2390
|
+
}
|
|
2391
|
+
function isAddress(val) {
|
|
2392
|
+
if (!val || typeof val !== "object") return false;
|
|
2393
|
+
const a = val;
|
|
2394
|
+
return isNonEmptyString(a.street) && isNonEmptyString(a.city) && isNonEmptyString(a.state) && isNonEmptyString(a.zip) && isNonEmptyString(a.country);
|
|
2395
|
+
}
|
|
2396
|
+
function isCard(val) {
|
|
2397
|
+
if (!val || typeof val !== "object") return false;
|
|
2398
|
+
const c = val;
|
|
2399
|
+
return isNonEmptyString(c.number) && isNonEmptyString(c.expiry) && isNonEmptyString(c.cvv);
|
|
2400
|
+
}
|
|
2401
|
+
function createSetupServer(options) {
|
|
2402
|
+
const nonce = (0, import_node_crypto6.randomBytes)(32).toString("hex");
|
|
2403
|
+
let settled = false;
|
|
2404
|
+
let tokenUsed = false;
|
|
2405
|
+
let resolvePromise;
|
|
2406
|
+
let rejectPromise;
|
|
2407
|
+
let timer;
|
|
2408
|
+
let serverInstance;
|
|
2409
|
+
const promise = new Promise((resolve, reject) => {
|
|
2410
|
+
resolvePromise = resolve;
|
|
2411
|
+
rejectPromise = reject;
|
|
2412
|
+
});
|
|
2413
|
+
function cleanup() {
|
|
2414
|
+
clearTimeout(timer);
|
|
2415
|
+
serverInstance.close();
|
|
2416
|
+
}
|
|
2417
|
+
const home = options?.home ?? getHomePath();
|
|
2418
|
+
serverInstance = (0, import_node_http2.createServer)(async (req, res) => {
|
|
2419
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
2420
|
+
const method = req.method ?? "GET";
|
|
2421
|
+
try {
|
|
2422
|
+
if (method === "GET" && url.pathname === "/setup") {
|
|
2423
|
+
const token = url.searchParams.get("token");
|
|
2424
|
+
if (token !== nonce || tokenUsed) {
|
|
2425
|
+
sendHtml2(res, "<h1>Invalid or expired link.</h1>");
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
sendHtml2(res, getSetupHtml(nonce));
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
if (method === "POST" && url.pathname === "/api/setup") {
|
|
2432
|
+
const body = await parseBody2(req);
|
|
2433
|
+
if (body.token !== nonce || tokenUsed) {
|
|
2434
|
+
sendJson2(res, 403, { error: "Invalid or expired token." });
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
const passphrase = body.passphrase;
|
|
2438
|
+
if (!isNonEmptyString(passphrase)) {
|
|
2439
|
+
sendJson2(res, 400, { error: "Passphrase is required." });
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
if (!isCard(body.card)) {
|
|
2443
|
+
sendJson2(res, 400, { error: "Card number, expiry, and CVV are required." });
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (!isNonEmptyString(body.name) || !isNonEmptyString(body.email) || !isNonEmptyString(body.phone)) {
|
|
2447
|
+
sendJson2(res, 400, { error: "Name, email, and phone are required." });
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
if (!isAddress(body.billingAddress)) {
|
|
2451
|
+
sendJson2(res, 400, { error: "Complete billing address is required." });
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
if (!isAddress(body.shippingAddress)) {
|
|
2455
|
+
sendJson2(res, 400, { error: "Complete shipping address is required." });
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
try {
|
|
2459
|
+
(0, import_node_fs8.mkdirSync)(home, { recursive: true });
|
|
2460
|
+
const credentials = {
|
|
2461
|
+
card: { number: body.card.number, expiry: body.card.expiry, cvv: body.card.cvv },
|
|
2462
|
+
name: body.name,
|
|
2463
|
+
billingAddress: {
|
|
2464
|
+
street: body.billingAddress.street,
|
|
2465
|
+
city: body.billingAddress.city,
|
|
2466
|
+
state: body.billingAddress.state,
|
|
2467
|
+
zip: body.billingAddress.zip,
|
|
2468
|
+
country: body.billingAddress.country
|
|
2469
|
+
},
|
|
2470
|
+
shippingAddress: {
|
|
2471
|
+
street: body.shippingAddress.street,
|
|
2472
|
+
city: body.shippingAddress.city,
|
|
2473
|
+
state: body.shippingAddress.state,
|
|
2474
|
+
zip: body.shippingAddress.zip,
|
|
2475
|
+
country: body.shippingAddress.country
|
|
2476
|
+
},
|
|
2477
|
+
email: body.email,
|
|
2478
|
+
phone: body.phone
|
|
2479
|
+
};
|
|
2480
|
+
const credPath = (0, import_node_path9.join)(home, "credentials.enc");
|
|
2481
|
+
const vault = encrypt(credentials, passphrase);
|
|
2482
|
+
saveVault(vault, credPath);
|
|
2483
|
+
const keysDir = (0, import_node_path9.join)(home, "keys");
|
|
2484
|
+
(0, import_node_fs8.mkdirSync)(keysDir, { recursive: true });
|
|
2485
|
+
const keys = generateKeyPair(passphrase);
|
|
2486
|
+
saveKeyPair(keys, (0, import_node_path9.join)(keysDir, "public.pem"), (0, import_node_path9.join)(keysDir, "private.pem"));
|
|
2487
|
+
const budget = typeof body.budget === "number" ? body.budget : 0;
|
|
2488
|
+
const limitPerTx = typeof body.limitPerTx === "number" ? body.limitPerTx : 0;
|
|
2489
|
+
const bm = new BudgetManager((0, import_node_path9.join)(home, "wallet.json"));
|
|
2490
|
+
bm.initWallet(budget, limitPerTx);
|
|
2491
|
+
const audit = new AuditLogger((0, import_node_path9.join)(home, "audit.log"));
|
|
2492
|
+
audit.log("SETUP", { message: "credentials encrypted, keypair generated, wallet initialized", source: "browser-setup" });
|
|
2493
|
+
} catch (err) {
|
|
2494
|
+
const msg = err instanceof Error ? err.message : "Setup failed";
|
|
2495
|
+
sendJson2(res, 500, { error: msg });
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
tokenUsed = true;
|
|
2499
|
+
sendJson2(res, 200, { ok: true });
|
|
2500
|
+
if (!settled) {
|
|
2501
|
+
settled = true;
|
|
2502
|
+
cleanup();
|
|
2503
|
+
resolvePromise({ completed: true });
|
|
2504
|
+
}
|
|
2505
|
+
return;
|
|
2506
|
+
}
|
|
2507
|
+
sendJson2(res, 404, { error: "Not found" });
|
|
2508
|
+
} catch (err) {
|
|
2509
|
+
const message = err instanceof Error ? err.message : "Internal error";
|
|
2510
|
+
sendJson2(res, 500, { error: message });
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
timer = setTimeout(() => {
|
|
2514
|
+
if (!settled) {
|
|
2515
|
+
settled = true;
|
|
2516
|
+
cleanup();
|
|
2517
|
+
rejectPromise(new TimeoutError("Setup timed out after 10 minutes."));
|
|
2518
|
+
}
|
|
2519
|
+
}, TIMEOUT_MS2);
|
|
2520
|
+
serverInstance.on("error", (err) => {
|
|
2521
|
+
if (!settled) {
|
|
2522
|
+
settled = true;
|
|
2523
|
+
cleanup();
|
|
2524
|
+
rejectPromise(err);
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
let portValue = 0;
|
|
2528
|
+
const handle = {
|
|
2529
|
+
server: serverInstance,
|
|
2530
|
+
token: nonce,
|
|
2531
|
+
get port() {
|
|
2532
|
+
return portValue;
|
|
2533
|
+
},
|
|
2534
|
+
promise
|
|
2535
|
+
};
|
|
2536
|
+
serverInstance.listen(0, "127.0.0.1", () => {
|
|
2537
|
+
const addr = serverInstance.address();
|
|
2538
|
+
if (!addr || typeof addr === "string") {
|
|
2539
|
+
if (!settled) {
|
|
2540
|
+
settled = true;
|
|
2541
|
+
cleanup();
|
|
2542
|
+
rejectPromise(new Error("Failed to bind server"));
|
|
2543
|
+
}
|
|
2544
|
+
return;
|
|
859
2545
|
}
|
|
2546
|
+
portValue = addr.port;
|
|
2547
|
+
const pageUrl = `http://127.0.0.1:${addr.port}/setup?token=${nonce}`;
|
|
2548
|
+
if (options?.openBrowser !== false) {
|
|
2549
|
+
console.log("Opening setup form in browser...");
|
|
2550
|
+
openBrowser(pageUrl);
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
return handle;
|
|
2554
|
+
}
|
|
2555
|
+
function requestBrowserSetup(home) {
|
|
2556
|
+
const handle = createSetupServer({ openBrowser: true, home });
|
|
2557
|
+
return handle.promise;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/server/index.ts
|
|
2561
|
+
init_cjs_shims();
|
|
2562
|
+
var import_node_http3 = require("http");
|
|
2563
|
+
|
|
2564
|
+
// src/server/html.ts
|
|
2565
|
+
init_cjs_shims();
|
|
2566
|
+
function getDashboardHtml() {
|
|
2567
|
+
return `<!DOCTYPE html>
|
|
2568
|
+
<html lang="en">
|
|
2569
|
+
<head>
|
|
2570
|
+
<meta charset="utf-8">
|
|
2571
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2572
|
+
<title>AgentPay Dashboard</title>
|
|
2573
|
+
<style>
|
|
2574
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2575
|
+
body {
|
|
2576
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
2577
|
+
background: #f5f5f5;
|
|
2578
|
+
color: #111;
|
|
2579
|
+
min-height: 100vh;
|
|
2580
|
+
display: flex;
|
|
2581
|
+
justify-content: center;
|
|
2582
|
+
padding: 40px 16px;
|
|
860
2583
|
}
|
|
2584
|
+
.container { width: 100%; max-width: 480px; }
|
|
2585
|
+
h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
|
|
2586
|
+
h2 { font-size: 18px; font-weight: 600; margin-bottom: 12px; }
|
|
2587
|
+
.subtitle { color: #666; font-size: 14px; margin-bottom: 32px; }
|
|
2588
|
+
.card {
|
|
2589
|
+
background: #fff;
|
|
2590
|
+
border-radius: 8px;
|
|
2591
|
+
padding: 24px;
|
|
2592
|
+
margin-bottom: 16px;
|
|
2593
|
+
border: 1px solid #e0e0e0;
|
|
2594
|
+
}
|
|
2595
|
+
label { display: block; font-size: 13px; font-weight: 500; color: #333; margin-bottom: 4px; }
|
|
2596
|
+
input, select {
|
|
2597
|
+
width: 100%;
|
|
2598
|
+
padding: 10px 12px;
|
|
2599
|
+
border: 1px solid #d0d0d0;
|
|
2600
|
+
border-radius: 8px;
|
|
2601
|
+
font-size: 14px;
|
|
2602
|
+
margin-bottom: 16px;
|
|
2603
|
+
outline: none;
|
|
2604
|
+
transition: border-color 0.15s;
|
|
2605
|
+
}
|
|
2606
|
+
input:focus { border-color: #111; }
|
|
2607
|
+
.row { display: flex; gap: 12px; }
|
|
2608
|
+
.row > div { flex: 1; }
|
|
2609
|
+
button {
|
|
2610
|
+
width: 100%;
|
|
2611
|
+
padding: 12px;
|
|
2612
|
+
border: none;
|
|
2613
|
+
border-radius: 8px;
|
|
2614
|
+
font-size: 14px;
|
|
2615
|
+
font-weight: 600;
|
|
2616
|
+
cursor: pointer;
|
|
2617
|
+
transition: opacity 0.15s;
|
|
2618
|
+
}
|
|
2619
|
+
button:hover { opacity: 0.85; }
|
|
2620
|
+
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
2621
|
+
.btn-primary { background: #111; color: #fff; }
|
|
2622
|
+
.btn-secondary { background: #e0e0e0; color: #111; }
|
|
2623
|
+
|
|
2624
|
+
/* Progress bar for wizard */
|
|
2625
|
+
.progress { display: flex; gap: 6px; margin-bottom: 24px; }
|
|
2626
|
+
.progress .step {
|
|
2627
|
+
flex: 1; height: 4px; border-radius: 2px; background: #e0e0e0;
|
|
2628
|
+
transition: background 0.3s;
|
|
2629
|
+
}
|
|
2630
|
+
.progress .step.active { background: #111; }
|
|
2631
|
+
|
|
2632
|
+
/* Balance display */
|
|
2633
|
+
.balance-display {
|
|
2634
|
+
text-align: center;
|
|
2635
|
+
padding: 32px 0;
|
|
2636
|
+
}
|
|
2637
|
+
.balance-amount {
|
|
2638
|
+
font-size: 48px;
|
|
2639
|
+
font-weight: 700;
|
|
2640
|
+
letter-spacing: -1px;
|
|
2641
|
+
}
|
|
2642
|
+
.balance-label { font-size: 13px; color: #666; margin-top: 4px; }
|
|
2643
|
+
|
|
2644
|
+
/* Budget bar */
|
|
2645
|
+
.budget-bar-container { margin: 16px 0; }
|
|
2646
|
+
.budget-bar {
|
|
2647
|
+
height: 8px;
|
|
2648
|
+
background: #e0e0e0;
|
|
2649
|
+
border-radius: 4px;
|
|
2650
|
+
overflow: hidden;
|
|
2651
|
+
}
|
|
2652
|
+
.budget-bar-fill {
|
|
2653
|
+
height: 100%;
|
|
2654
|
+
background: #111;
|
|
2655
|
+
border-radius: 4px;
|
|
2656
|
+
transition: width 0.3s;
|
|
2657
|
+
}
|
|
2658
|
+
.budget-bar-labels {
|
|
2659
|
+
display: flex;
|
|
2660
|
+
justify-content: space-between;
|
|
2661
|
+
font-size: 12px;
|
|
2662
|
+
color: #666;
|
|
2663
|
+
margin-top: 4px;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
/* Stats grid */
|
|
2667
|
+
.stats { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 16px; }
|
|
2668
|
+
.stat { text-align: center; padding: 12px; background: #f9f9f9; border-radius: 8px; }
|
|
2669
|
+
.stat-value { font-size: 18px; font-weight: 700; }
|
|
2670
|
+
.stat-label { font-size: 11px; color: #666; margin-top: 2px; }
|
|
2671
|
+
|
|
2672
|
+
/* Add funds inline */
|
|
2673
|
+
.add-funds-row { display: flex; gap: 8px; }
|
|
2674
|
+
.add-funds-row input { margin-bottom: 0; flex: 1; }
|
|
2675
|
+
.add-funds-row button { width: auto; padding: 10px 20px; }
|
|
2676
|
+
|
|
2677
|
+
/* Transactions */
|
|
2678
|
+
.tx-list { margin-top: 12px; }
|
|
2679
|
+
.tx-item {
|
|
2680
|
+
display: flex;
|
|
2681
|
+
justify-content: space-between;
|
|
2682
|
+
align-items: center;
|
|
2683
|
+
padding: 10px 0;
|
|
2684
|
+
border-bottom: 1px solid #f0f0f0;
|
|
2685
|
+
font-size: 13px;
|
|
2686
|
+
}
|
|
2687
|
+
.tx-item:last-child { border-bottom: none; }
|
|
2688
|
+
.tx-merchant { font-weight: 500; }
|
|
2689
|
+
.tx-amount { font-weight: 600; }
|
|
2690
|
+
.tx-status {
|
|
2691
|
+
font-size: 11px;
|
|
2692
|
+
padding: 2px 6px;
|
|
2693
|
+
border-radius: 4px;
|
|
2694
|
+
font-weight: 500;
|
|
2695
|
+
}
|
|
2696
|
+
.tx-status.completed { background: #e6f4ea; color: #1e7e34; }
|
|
2697
|
+
.tx-status.pending { background: #fff8e1; color: #f57f17; }
|
|
2698
|
+
.tx-status.failed { background: #fde8e8; color: #c62828; }
|
|
2699
|
+
.tx-status.rejected { background: #fde8e8; color: #c62828; }
|
|
2700
|
+
.tx-status.approved { background: #e3f2fd; color: #1565c0; }
|
|
2701
|
+
.tx-status.executing { background: #e3f2fd; color: #1565c0; }
|
|
2702
|
+
|
|
2703
|
+
.error { color: #c62828; font-size: 13px; margin-top: 8px; }
|
|
2704
|
+
.success { color: #1e7e34; font-size: 13px; margin-top: 8px; }
|
|
2705
|
+
.hidden { display: none; }
|
|
2706
|
+
|
|
2707
|
+
/* Checkbox row for same-as-billing */
|
|
2708
|
+
.checkbox-row {
|
|
2709
|
+
display: flex;
|
|
2710
|
+
align-items: center;
|
|
2711
|
+
gap: 8px;
|
|
2712
|
+
margin-bottom: 16px;
|
|
2713
|
+
}
|
|
2714
|
+
.checkbox-row input { width: auto; margin: 0; }
|
|
2715
|
+
.checkbox-row label { margin: 0; }
|
|
2716
|
+
</style>
|
|
2717
|
+
</head>
|
|
2718
|
+
<body>
|
|
2719
|
+
<div class="container" id="app">
|
|
2720
|
+
<div id="loading">Loading...</div>
|
|
2721
|
+
</div>
|
|
2722
|
+
|
|
2723
|
+
<script>
|
|
2724
|
+
const App = {
|
|
2725
|
+
state: { isSetup: false, wallet: null, recentTransactions: [], wizardStep: 1 },
|
|
2726
|
+
|
|
2727
|
+
async init() {
|
|
2728
|
+
try {
|
|
2729
|
+
const res = await fetch('/api/status');
|
|
2730
|
+
const data = await res.json();
|
|
2731
|
+
this.state.isSetup = data.isSetup;
|
|
2732
|
+
this.state.wallet = data.wallet;
|
|
2733
|
+
this.state.recentTransactions = data.recentTransactions || [];
|
|
2734
|
+
} catch (e) {
|
|
2735
|
+
console.error('Failed to load status', e);
|
|
2736
|
+
}
|
|
2737
|
+
this.render();
|
|
2738
|
+
},
|
|
2739
|
+
|
|
2740
|
+
render() {
|
|
2741
|
+
const app = document.getElementById('app');
|
|
2742
|
+
if (this.state.isSetup && this.state.wallet) {
|
|
2743
|
+
app.innerHTML = this.renderDashboard();
|
|
2744
|
+
this.bindDashboard();
|
|
2745
|
+
} else if (this.state.isSetup) {
|
|
2746
|
+
app.innerHTML = '<div class="card"><h2>Setup detected</h2><p>Wallet data could not be loaded. Try running <code>agentpay budget --set 100</code> from the CLI.</p></div>';
|
|
2747
|
+
} else {
|
|
2748
|
+
app.innerHTML = this.renderWizard();
|
|
2749
|
+
this.bindWizard();
|
|
2750
|
+
}
|
|
2751
|
+
},
|
|
2752
|
+
|
|
2753
|
+
fmt(n) {
|
|
2754
|
+
return '$' + Number(n).toFixed(2);
|
|
2755
|
+
},
|
|
2756
|
+
|
|
2757
|
+
renderDashboard() {
|
|
2758
|
+
const w = this.state.wallet;
|
|
2759
|
+
const pct = w.budget > 0 ? Math.min(100, (w.spent / w.budget) * 100) : 0;
|
|
2760
|
+
const txHtml = this.state.recentTransactions.length === 0
|
|
2761
|
+
? '<p style="color:#666;font-size:13px;">No transactions yet.</p>'
|
|
2762
|
+
: this.state.recentTransactions.map(tx => \`
|
|
2763
|
+
<div class="tx-item">
|
|
2764
|
+
<div>
|
|
2765
|
+
<div class="tx-merchant">\${this.esc(tx.merchant)}</div>
|
|
2766
|
+
<div style="color:#999;font-size:11px;">\${new Date(tx.createdAt).toLocaleDateString()}</div>
|
|
2767
|
+
</div>
|
|
2768
|
+
<div style="text-align:right">
|
|
2769
|
+
<div class="tx-amount">\${this.fmt(tx.amount)}</div>
|
|
2770
|
+
<span class="tx-status \${tx.status}">\${tx.status}</span>
|
|
2771
|
+
</div>
|
|
2772
|
+
</div>\`).join('');
|
|
2773
|
+
|
|
2774
|
+
return \`
|
|
2775
|
+
<h1>AgentPay</h1>
|
|
2776
|
+
<p class="subtitle">Wallet Dashboard</p>
|
|
2777
|
+
|
|
2778
|
+
<div class="card">
|
|
2779
|
+
<div class="balance-display">
|
|
2780
|
+
<div class="balance-amount">\${this.fmt(w.balance)}</div>
|
|
2781
|
+
<div class="balance-label">Available Balance</div>
|
|
2782
|
+
</div>
|
|
2783
|
+
<div class="budget-bar-container">
|
|
2784
|
+
<div class="budget-bar"><div class="budget-bar-fill" style="width:\${pct.toFixed(1)}%"></div></div>
|
|
2785
|
+
<div class="budget-bar-labels">
|
|
2786
|
+
<span>\${this.fmt(w.spent)} spent</span>
|
|
2787
|
+
<span>\${this.fmt(w.budget)} budget</span>
|
|
2788
|
+
</div>
|
|
2789
|
+
</div>
|
|
2790
|
+
</div>
|
|
2791
|
+
|
|
2792
|
+
<div class="card">
|
|
2793
|
+
<div class="stats">
|
|
2794
|
+
<div class="stat"><div class="stat-value">\${this.fmt(w.budget)}</div><div class="stat-label">Total Budget</div></div>
|
|
2795
|
+
<div class="stat"><div class="stat-value">\${this.fmt(w.spent)}</div><div class="stat-label">Total Spent</div></div>
|
|
2796
|
+
<div class="stat"><div class="stat-value">\${this.fmt(w.balance)}</div><div class="stat-label">Remaining</div></div>
|
|
2797
|
+
<div class="stat"><div class="stat-value">\${w.limitPerTx > 0 ? this.fmt(w.limitPerTx) : 'None'}</div><div class="stat-label">Per-Tx Limit</div></div>
|
|
2798
|
+
</div>
|
|
2799
|
+
</div>
|
|
2800
|
+
|
|
2801
|
+
<div class="card">
|
|
2802
|
+
<h2>Add Funds</h2>
|
|
2803
|
+
<div class="add-funds-row">
|
|
2804
|
+
<input type="number" id="fundAmount" placeholder="Amount" min="0.01" step="0.01">
|
|
2805
|
+
<button class="btn-primary" id="fundBtn">Add</button>
|
|
2806
|
+
</div>
|
|
2807
|
+
<div id="fundMsg"></div>
|
|
2808
|
+
</div>
|
|
2809
|
+
|
|
2810
|
+
<div class="card">
|
|
2811
|
+
<h2>Recent Transactions</h2>
|
|
2812
|
+
<div class="tx-list">\${txHtml}</div>
|
|
2813
|
+
</div>
|
|
2814
|
+
\`;
|
|
2815
|
+
},
|
|
2816
|
+
|
|
2817
|
+
bindDashboard() {
|
|
2818
|
+
const btn = document.getElementById('fundBtn');
|
|
2819
|
+
const input = document.getElementById('fundAmount');
|
|
2820
|
+
const msg = document.getElementById('fundMsg');
|
|
2821
|
+
|
|
2822
|
+
btn.addEventListener('click', async () => {
|
|
2823
|
+
const amount = parseFloat(input.value);
|
|
2824
|
+
if (!amount || amount <= 0) {
|
|
2825
|
+
msg.innerHTML = '<p class="error">Enter a valid amount.</p>';
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
btn.disabled = true;
|
|
2829
|
+
btn.textContent = '...';
|
|
2830
|
+
try {
|
|
2831
|
+
const res = await fetch('/api/fund', {
|
|
2832
|
+
method: 'POST',
|
|
2833
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2834
|
+
body: JSON.stringify({ amount }),
|
|
2835
|
+
});
|
|
2836
|
+
const data = await res.json();
|
|
2837
|
+
if (data.error) {
|
|
2838
|
+
msg.innerHTML = '<p class="error">' + this.esc(data.error) + '</p>';
|
|
2839
|
+
} else {
|
|
2840
|
+
this.state.wallet = data.wallet;
|
|
2841
|
+
input.value = '';
|
|
2842
|
+
this.render();
|
|
2843
|
+
}
|
|
2844
|
+
} catch (e) {
|
|
2845
|
+
msg.innerHTML = '<p class="error">Request failed.</p>';
|
|
2846
|
+
}
|
|
2847
|
+
btn.disabled = false;
|
|
2848
|
+
btn.textContent = 'Add';
|
|
2849
|
+
});
|
|
2850
|
+
},
|
|
2851
|
+
|
|
2852
|
+
renderWizard() {
|
|
2853
|
+
const step = this.state.wizardStep;
|
|
2854
|
+
const steps = [1, 2, 3, 4];
|
|
2855
|
+
const progressHtml = '<div class="progress">' + steps.map(s =>
|
|
2856
|
+
'<div class="step' + (s <= step ? ' active' : '') + '"></div>'
|
|
2857
|
+
).join('') + '</div>';
|
|
2858
|
+
|
|
2859
|
+
const titles = ['Create Passphrase', 'Card Information', 'Personal Details', 'Budget & Limits'];
|
|
2860
|
+
|
|
2861
|
+
let fields = '';
|
|
2862
|
+
if (step === 1) {
|
|
2863
|
+
fields = \`
|
|
2864
|
+
<label for="w_pass">Passphrase</label>
|
|
2865
|
+
<input type="password" id="w_pass" placeholder="Choose a strong passphrase">
|
|
2866
|
+
<label for="w_pass2">Confirm Passphrase</label>
|
|
2867
|
+
<input type="password" id="w_pass2" placeholder="Confirm your passphrase">
|
|
2868
|
+
\`;
|
|
2869
|
+
} else if (step === 2) {
|
|
2870
|
+
fields = \`
|
|
2871
|
+
<label for="w_cardNum">Card Number</label>
|
|
2872
|
+
<input type="text" id="w_cardNum" placeholder="4242 4242 4242 4242">
|
|
2873
|
+
<div class="row">
|
|
2874
|
+
<div><label for="w_expiry">Expiry</label><input type="text" id="w_expiry" placeholder="MM/YY"></div>
|
|
2875
|
+
<div><label for="w_cvv">CVV</label><input type="text" id="w_cvv" placeholder="123"></div>
|
|
2876
|
+
</div>
|
|
2877
|
+
\`;
|
|
2878
|
+
} else if (step === 3) {
|
|
2879
|
+
fields = \`
|
|
2880
|
+
<label for="w_name">Full Name</label>
|
|
2881
|
+
<input type="text" id="w_name" placeholder="Jane Doe">
|
|
2882
|
+
<div class="row">
|
|
2883
|
+
<div><label for="w_email">Email</label><input type="email" id="w_email" placeholder="jane@example.com"></div>
|
|
2884
|
+
<div><label for="w_phone">Phone</label><input type="tel" id="w_phone" placeholder="+1 555 0123"></div>
|
|
2885
|
+
</div>
|
|
2886
|
+
<label for="w_street">Street Address</label>
|
|
2887
|
+
<input type="text" id="w_street" placeholder="123 Main St">
|
|
2888
|
+
<div class="row">
|
|
2889
|
+
<div><label for="w_city">City</label><input type="text" id="w_city" placeholder="San Francisco"></div>
|
|
2890
|
+
<div><label for="w_state">State</label><input type="text" id="w_state" placeholder="CA"></div>
|
|
2891
|
+
</div>
|
|
2892
|
+
<div class="row">
|
|
2893
|
+
<div><label for="w_zip">ZIP</label><input type="text" id="w_zip" placeholder="94102"></div>
|
|
2894
|
+
<div><label for="w_country">Country</label><input type="text" id="w_country" placeholder="US" value="US"></div>
|
|
2895
|
+
</div>
|
|
2896
|
+
\`;
|
|
2897
|
+
} else if (step === 4) {
|
|
2898
|
+
fields = \`
|
|
2899
|
+
<label for="w_budget">Initial Budget ($)</label>
|
|
2900
|
+
<input type="number" id="w_budget" placeholder="200" min="0" step="0.01">
|
|
2901
|
+
<label for="w_limit">Per-Transaction Limit ($)</label>
|
|
2902
|
+
<input type="number" id="w_limit" placeholder="50 (0 = no limit)" min="0" step="0.01">
|
|
2903
|
+
\`;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
return \`
|
|
2907
|
+
<h1>AgentPay</h1>
|
|
2908
|
+
<p class="subtitle">Step \${step} of 4 \u2014 \${titles[step - 1]}</p>
|
|
2909
|
+
\${progressHtml}
|
|
2910
|
+
<div class="card">
|
|
2911
|
+
\${fields}
|
|
2912
|
+
<div id="wizardError"></div>
|
|
2913
|
+
<div style="display:flex;gap:8px;margin-top:8px;">
|
|
2914
|
+
\${step > 1 ? '<button class="btn-secondary" id="wizBack">Back</button>' : ''}
|
|
2915
|
+
<button class="btn-primary" id="wizNext">\${step === 4 ? 'Complete Setup' : 'Continue'}</button>
|
|
2916
|
+
</div>
|
|
2917
|
+
</div>
|
|
2918
|
+
\`;
|
|
2919
|
+
},
|
|
2920
|
+
|
|
2921
|
+
// Wizard form data persisted across steps
|
|
2922
|
+
wizardData: {},
|
|
2923
|
+
|
|
2924
|
+
bindWizard() {
|
|
2925
|
+
const step = this.state.wizardStep;
|
|
2926
|
+
const errDiv = document.getElementById('wizardError');
|
|
2927
|
+
const nextBtn = document.getElementById('wizNext');
|
|
2928
|
+
const backBtn = document.getElementById('wizBack');
|
|
2929
|
+
|
|
2930
|
+
// Restore saved data into fields
|
|
2931
|
+
this.restoreWizardFields(step);
|
|
2932
|
+
|
|
2933
|
+
if (backBtn) {
|
|
2934
|
+
backBtn.addEventListener('click', () => {
|
|
2935
|
+
this.saveWizardFields(step);
|
|
2936
|
+
this.state.wizardStep--;
|
|
2937
|
+
this.render();
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
nextBtn.addEventListener('click', async () => {
|
|
2942
|
+
errDiv.innerHTML = '';
|
|
2943
|
+
|
|
2944
|
+
if (step === 1) {
|
|
2945
|
+
const pass = document.getElementById('w_pass').value;
|
|
2946
|
+
const pass2 = document.getElementById('w_pass2').value;
|
|
2947
|
+
if (!pass) { errDiv.innerHTML = '<p class="error">Passphrase is required.</p>'; return; }
|
|
2948
|
+
if (pass !== pass2) { errDiv.innerHTML = '<p class="error">Passphrases do not match.</p>'; return; }
|
|
2949
|
+
this.wizardData.passphrase = pass;
|
|
2950
|
+
this.state.wizardStep = 2;
|
|
2951
|
+
this.render();
|
|
2952
|
+
} else if (step === 2) {
|
|
2953
|
+
this.saveWizardFields(step);
|
|
2954
|
+
const d = this.wizardData;
|
|
2955
|
+
if (!d.cardNumber) { errDiv.innerHTML = '<p class="error">Card number is required.</p>'; return; }
|
|
2956
|
+
if (!d.expiry) { errDiv.innerHTML = '<p class="error">Expiry is required.</p>'; return; }
|
|
2957
|
+
if (!d.cvv) { errDiv.innerHTML = '<p class="error">CVV is required.</p>'; return; }
|
|
2958
|
+
this.state.wizardStep = 3;
|
|
2959
|
+
this.render();
|
|
2960
|
+
} else if (step === 3) {
|
|
2961
|
+
this.saveWizardFields(step);
|
|
2962
|
+
const d = this.wizardData;
|
|
2963
|
+
if (!d.name) { errDiv.innerHTML = '<p class="error">Full name is required.</p>'; return; }
|
|
2964
|
+
this.state.wizardStep = 4;
|
|
2965
|
+
this.render();
|
|
2966
|
+
} else if (step === 4) {
|
|
2967
|
+
this.saveWizardFields(step);
|
|
2968
|
+
const d = this.wizardData;
|
|
2969
|
+
|
|
2970
|
+
nextBtn.disabled = true;
|
|
2971
|
+
nextBtn.textContent = 'Setting up...';
|
|
2972
|
+
|
|
2973
|
+
try {
|
|
2974
|
+
const res = await fetch('/api/setup', {
|
|
2975
|
+
method: 'POST',
|
|
2976
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2977
|
+
body: JSON.stringify({
|
|
2978
|
+
passphrase: d.passphrase,
|
|
2979
|
+
credentials: {
|
|
2980
|
+
card: { number: d.cardNumber, expiry: d.expiry, cvv: d.cvv },
|
|
2981
|
+
name: d.name,
|
|
2982
|
+
billingAddress: { street: d.street || '', city: d.city || '', state: d.state || '', zip: d.zip || '', country: d.country || 'US' },
|
|
2983
|
+
shippingAddress: { street: d.street || '', city: d.city || '', state: d.state || '', zip: d.zip || '', country: d.country || 'US' },
|
|
2984
|
+
email: d.email || '',
|
|
2985
|
+
phone: d.phone || '',
|
|
2986
|
+
},
|
|
2987
|
+
budget: parseFloat(d.budget) || 0,
|
|
2988
|
+
limitPerTx: parseFloat(d.limit) || 0,
|
|
2989
|
+
}),
|
|
2990
|
+
});
|
|
2991
|
+
const result = await res.json();
|
|
2992
|
+
if (result.error) {
|
|
2993
|
+
errDiv.innerHTML = '<p class="error">' + this.esc(result.error) + '</p>';
|
|
2994
|
+
nextBtn.disabled = false;
|
|
2995
|
+
nextBtn.textContent = 'Complete Setup';
|
|
2996
|
+
} else {
|
|
2997
|
+
this.state.isSetup = true;
|
|
2998
|
+
this.state.wallet = result.wallet;
|
|
2999
|
+
this.state.recentTransactions = [];
|
|
3000
|
+
this.wizardData = {};
|
|
3001
|
+
this.render();
|
|
3002
|
+
}
|
|
3003
|
+
} catch (e) {
|
|
3004
|
+
errDiv.innerHTML = '<p class="error">Setup request failed.</p>';
|
|
3005
|
+
nextBtn.disabled = false;
|
|
3006
|
+
nextBtn.textContent = 'Complete Setup';
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
},
|
|
3011
|
+
|
|
3012
|
+
saveWizardFields(step) {
|
|
3013
|
+
const val = (id) => { const el = document.getElementById(id); return el ? el.value : ''; };
|
|
3014
|
+
if (step === 2) {
|
|
3015
|
+
this.wizardData.cardNumber = val('w_cardNum');
|
|
3016
|
+
this.wizardData.expiry = val('w_expiry');
|
|
3017
|
+
this.wizardData.cvv = val('w_cvv');
|
|
3018
|
+
} else if (step === 3) {
|
|
3019
|
+
this.wizardData.name = val('w_name');
|
|
3020
|
+
this.wizardData.email = val('w_email');
|
|
3021
|
+
this.wizardData.phone = val('w_phone');
|
|
3022
|
+
this.wizardData.street = val('w_street');
|
|
3023
|
+
this.wizardData.city = val('w_city');
|
|
3024
|
+
this.wizardData.state = val('w_state');
|
|
3025
|
+
this.wizardData.zip = val('w_zip');
|
|
3026
|
+
this.wizardData.country = val('w_country');
|
|
3027
|
+
} else if (step === 4) {
|
|
3028
|
+
this.wizardData.budget = val('w_budget');
|
|
3029
|
+
this.wizardData.limit = val('w_limit');
|
|
3030
|
+
}
|
|
3031
|
+
},
|
|
3032
|
+
|
|
3033
|
+
restoreWizardFields(step) {
|
|
3034
|
+
const set = (id, val) => { const el = document.getElementById(id); if (el && val) el.value = val; };
|
|
3035
|
+
if (step === 2) {
|
|
3036
|
+
set('w_cardNum', this.wizardData.cardNumber);
|
|
3037
|
+
set('w_expiry', this.wizardData.expiry);
|
|
3038
|
+
set('w_cvv', this.wizardData.cvv);
|
|
3039
|
+
} else if (step === 3) {
|
|
3040
|
+
set('w_name', this.wizardData.name);
|
|
3041
|
+
set('w_email', this.wizardData.email);
|
|
3042
|
+
set('w_phone', this.wizardData.phone);
|
|
3043
|
+
set('w_street', this.wizardData.street);
|
|
3044
|
+
set('w_city', this.wizardData.city);
|
|
3045
|
+
set('w_state', this.wizardData.state);
|
|
3046
|
+
set('w_zip', this.wizardData.zip);
|
|
3047
|
+
set('w_country', this.wizardData.country);
|
|
3048
|
+
} else if (step === 4) {
|
|
3049
|
+
set('w_budget', this.wizardData.budget);
|
|
3050
|
+
set('w_limit', this.wizardData.limit);
|
|
3051
|
+
}
|
|
3052
|
+
},
|
|
3053
|
+
|
|
3054
|
+
esc(s) {
|
|
3055
|
+
const d = document.createElement('div');
|
|
3056
|
+
d.textContent = s;
|
|
3057
|
+
return d.innerHTML;
|
|
3058
|
+
},
|
|
861
3059
|
};
|
|
862
3060
|
|
|
3061
|
+
App.init();
|
|
3062
|
+
</script>
|
|
3063
|
+
</body>
|
|
3064
|
+
</html>`;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// src/server/routes.ts
|
|
3068
|
+
init_cjs_shims();
|
|
3069
|
+
var import_node_fs9 = require("fs");
|
|
3070
|
+
init_keypair();
|
|
3071
|
+
init_paths();
|
|
3072
|
+
function handleGetStatus() {
|
|
3073
|
+
const isSetup = (0, import_node_fs9.existsSync)(getCredentialsPath());
|
|
3074
|
+
if (!isSetup) {
|
|
3075
|
+
return { status: 200, body: { isSetup: false } };
|
|
3076
|
+
}
|
|
3077
|
+
try {
|
|
3078
|
+
const bm = new BudgetManager();
|
|
3079
|
+
const wallet = bm.getBalance();
|
|
3080
|
+
const tm = new TransactionManager();
|
|
3081
|
+
const recent = tm.list().slice(-10).reverse();
|
|
3082
|
+
return {
|
|
3083
|
+
status: 200,
|
|
3084
|
+
body: { isSetup: true, wallet, recentTransactions: recent }
|
|
3085
|
+
};
|
|
3086
|
+
} catch {
|
|
3087
|
+
return { status: 200, body: { isSetup: true, wallet: null, recentTransactions: [] } };
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
function handlePostSetup(body) {
|
|
3091
|
+
if (!body.passphrase || !body.credentials) {
|
|
3092
|
+
return { status: 400, body: { error: "Missing passphrase or credentials" } };
|
|
3093
|
+
}
|
|
3094
|
+
try {
|
|
3095
|
+
const home = getHomePath();
|
|
3096
|
+
(0, import_node_fs9.mkdirSync)(home, { recursive: true });
|
|
3097
|
+
const vault = encrypt(body.credentials, body.passphrase);
|
|
3098
|
+
saveVault(vault, getCredentialsPath());
|
|
3099
|
+
const keys = generateKeyPair(body.passphrase);
|
|
3100
|
+
(0, import_node_fs9.mkdirSync)(getKeysPath(), { recursive: true });
|
|
3101
|
+
saveKeyPair(keys);
|
|
3102
|
+
const bm = new BudgetManager();
|
|
3103
|
+
bm.initWallet(body.budget || 0, body.limitPerTx || 0);
|
|
3104
|
+
const audit = new AuditLogger();
|
|
3105
|
+
audit.log("SETUP", { source: "dashboard", message: "credentials encrypted, keypair generated, wallet initialized" });
|
|
3106
|
+
const wallet = bm.getBalance();
|
|
3107
|
+
return { status: 200, body: { success: true, wallet } };
|
|
3108
|
+
} catch (err) {
|
|
3109
|
+
const message = err instanceof Error ? err.message : "Setup failed";
|
|
3110
|
+
return { status: 500, body: { error: message } };
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
function handlePostFund(body) {
|
|
3114
|
+
if (!body.amount || body.amount <= 0) {
|
|
3115
|
+
return { status: 400, body: { error: "Amount must be positive" } };
|
|
3116
|
+
}
|
|
3117
|
+
try {
|
|
3118
|
+
const bm = new BudgetManager();
|
|
3119
|
+
const wallet = bm.addFunds(body.amount);
|
|
3120
|
+
const audit = new AuditLogger();
|
|
3121
|
+
audit.log("ADD_FUNDS", { source: "dashboard", amount: body.amount });
|
|
3122
|
+
return { status: 200, body: { success: true, wallet } };
|
|
3123
|
+
} catch (err) {
|
|
3124
|
+
const message = err instanceof Error ? err.message : "Failed to add funds";
|
|
3125
|
+
return { status: 500, body: { error: message } };
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
// src/server/index.ts
|
|
3130
|
+
var MAX_BODY3 = 1048576;
|
|
3131
|
+
function parseBody3(req) {
|
|
3132
|
+
return new Promise((resolve, reject) => {
|
|
3133
|
+
const chunks = [];
|
|
3134
|
+
let size = 0;
|
|
3135
|
+
req.on("data", (chunk) => {
|
|
3136
|
+
size += chunk.length;
|
|
3137
|
+
if (size > MAX_BODY3) {
|
|
3138
|
+
req.destroy();
|
|
3139
|
+
reject(new Error("Request body too large"));
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
chunks.push(chunk);
|
|
3143
|
+
});
|
|
3144
|
+
req.on("end", () => {
|
|
3145
|
+
try {
|
|
3146
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
3147
|
+
resolve(text ? JSON.parse(text) : {});
|
|
3148
|
+
} catch {
|
|
3149
|
+
reject(new Error("Invalid JSON"));
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
req.on("error", reject);
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
function sendJson3(res, status, body) {
|
|
3156
|
+
const json = JSON.stringify(body);
|
|
3157
|
+
res.writeHead(status, {
|
|
3158
|
+
"Content-Type": "application/json",
|
|
3159
|
+
"Content-Length": Buffer.byteLength(json)
|
|
3160
|
+
});
|
|
3161
|
+
res.end(json);
|
|
3162
|
+
}
|
|
3163
|
+
function sendHtml3(res, html) {
|
|
3164
|
+
res.writeHead(200, {
|
|
3165
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
3166
|
+
"Content-Length": Buffer.byteLength(html)
|
|
3167
|
+
});
|
|
3168
|
+
res.end(html);
|
|
3169
|
+
}
|
|
3170
|
+
function startServer(port) {
|
|
3171
|
+
return new Promise((resolve, reject) => {
|
|
3172
|
+
const server = (0, import_node_http3.createServer)(async (req, res) => {
|
|
3173
|
+
const url = req.url ?? "/";
|
|
3174
|
+
const method = req.method ?? "GET";
|
|
3175
|
+
try {
|
|
3176
|
+
if (method === "GET" && url === "/api/status") {
|
|
3177
|
+
const result = handleGetStatus();
|
|
3178
|
+
sendJson3(res, result.status, result.body);
|
|
3179
|
+
} else if (method === "POST" && url === "/api/setup") {
|
|
3180
|
+
const body = await parseBody3(req);
|
|
3181
|
+
const result = handlePostSetup(body);
|
|
3182
|
+
sendJson3(res, result.status, result.body);
|
|
3183
|
+
} else if (method === "POST" && url === "/api/fund") {
|
|
3184
|
+
const body = await parseBody3(req);
|
|
3185
|
+
const result = handlePostFund(body);
|
|
3186
|
+
sendJson3(res, result.status, result.body);
|
|
3187
|
+
} else if (method === "GET" && (url === "/" || url === "/index.html")) {
|
|
3188
|
+
sendHtml3(res, getDashboardHtml());
|
|
3189
|
+
} else {
|
|
3190
|
+
sendJson3(res, 404, { error: "Not found" });
|
|
3191
|
+
}
|
|
3192
|
+
} catch (err) {
|
|
3193
|
+
const message = err instanceof Error ? err.message : "Internal error";
|
|
3194
|
+
sendJson3(res, 500, { error: message });
|
|
3195
|
+
}
|
|
3196
|
+
});
|
|
3197
|
+
server.on("error", (err) => {
|
|
3198
|
+
if (err.code === "EADDRINUSE") {
|
|
3199
|
+
reject(new Error(`Port ${port} is already in use. Try a different port with --port <number>.`));
|
|
3200
|
+
} else {
|
|
3201
|
+
reject(err);
|
|
3202
|
+
}
|
|
3203
|
+
});
|
|
3204
|
+
server.listen(port, "127.0.0.1", () => {
|
|
3205
|
+
resolve(server);
|
|
3206
|
+
});
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
// src/index.ts
|
|
3211
|
+
init_open_browser();
|
|
3212
|
+
|
|
3213
|
+
// src/utils/prompt.ts
|
|
3214
|
+
init_cjs_shims();
|
|
3215
|
+
var import_node_readline = require("readline");
|
|
3216
|
+
function createRl() {
|
|
3217
|
+
return (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
3218
|
+
}
|
|
3219
|
+
function promptInput(question) {
|
|
3220
|
+
return new Promise((resolve) => {
|
|
3221
|
+
const rl = createRl();
|
|
3222
|
+
rl.question(question, (answer) => {
|
|
3223
|
+
rl.close();
|
|
3224
|
+
resolve(answer.trim());
|
|
3225
|
+
});
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
async function promptPassphrase(prompt = "Passphrase: ") {
|
|
3229
|
+
return promptInput(prompt);
|
|
3230
|
+
}
|
|
3231
|
+
function promptConfirm(question) {
|
|
3232
|
+
return new Promise((resolve) => {
|
|
3233
|
+
const rl = createRl();
|
|
3234
|
+
rl.question(`${question} (y/N): `, (answer) => {
|
|
3235
|
+
rl.close();
|
|
3236
|
+
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
3237
|
+
});
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
async function promptPassphraseSafe(context) {
|
|
3241
|
+
if (process.stdin.isTTY) {
|
|
3242
|
+
return promptPassphrase("Enter passphrase: ");
|
|
3243
|
+
}
|
|
3244
|
+
const { collectPassphrase: collectPassphrase2 } = await Promise.resolve().then(() => (init_passphrase_server(), passphrase_server_exports));
|
|
3245
|
+
return collectPassphrase2(context);
|
|
3246
|
+
}
|
|
3247
|
+
|
|
863
3248
|
// src/agentpay.ts
|
|
864
3249
|
init_cjs_shims();
|
|
865
|
-
var
|
|
3250
|
+
var import_node_path10 = require("path");
|
|
3251
|
+
init_mandate();
|
|
3252
|
+
init_paths();
|
|
866
3253
|
init_errors();
|
|
867
3254
|
var AgentPay = class {
|
|
868
3255
|
home;
|
|
@@ -874,9 +3261,9 @@ var AgentPay = class {
|
|
|
874
3261
|
constructor(options) {
|
|
875
3262
|
this.home = options?.home ?? getHomePath();
|
|
876
3263
|
this.passphrase = options?.passphrase;
|
|
877
|
-
this.budgetManager = new BudgetManager((0,
|
|
878
|
-
this.txManager = new TransactionManager((0,
|
|
879
|
-
this.auditLogger = new AuditLogger((0,
|
|
3264
|
+
this.budgetManager = new BudgetManager((0, import_node_path10.join)(this.home, "wallet.json"));
|
|
3265
|
+
this.txManager = new TransactionManager((0, import_node_path10.join)(this.home, "transactions.json"));
|
|
3266
|
+
this.auditLogger = new AuditLogger((0, import_node_path10.join)(this.home, "audit.log"));
|
|
880
3267
|
this.executor = new PurchaseExecutor(options?.executor);
|
|
881
3268
|
}
|
|
882
3269
|
get wallet() {
|
|
@@ -887,19 +3274,16 @@ var AgentPay = class {
|
|
|
887
3274
|
getLimits: () => {
|
|
888
3275
|
const w = bm.getBalance();
|
|
889
3276
|
return { budget: w.budget, limitPerTx: w.limitPerTx, remaining: w.balance };
|
|
890
|
-
},
|
|
891
|
-
generateFundingQR: async (options) => {
|
|
892
|
-
const QRCode = await import("qrcode");
|
|
893
|
-
const params = new URLSearchParams();
|
|
894
|
-
if (options?.suggestedBudget) params.set("budget", String(options.suggestedBudget));
|
|
895
|
-
if (options?.message) params.set("msg", options.message);
|
|
896
|
-
const baseUrl = process.env.AGENTPAY_WEB_URL ?? "http://localhost:3000";
|
|
897
|
-
const url = `${baseUrl}/setup${params.toString() ? `?${params.toString()}` : ""}`;
|
|
898
|
-
const qrDataUrl = await QRCode.toDataURL(url);
|
|
899
|
-
return { url, qrDataUrl };
|
|
900
3277
|
}
|
|
901
3278
|
};
|
|
902
3279
|
}
|
|
3280
|
+
get config() {
|
|
3281
|
+
return {
|
|
3282
|
+
get: () => loadConfig(this.home),
|
|
3283
|
+
setMobileMode: (enabled) => setMobileMode(enabled, this.home),
|
|
3284
|
+
save: (config) => saveConfig(config, this.home)
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
903
3287
|
get transactions() {
|
|
904
3288
|
return {
|
|
905
3289
|
propose: (options) => {
|
|
@@ -913,6 +3297,33 @@ var AgentPay = class {
|
|
|
913
3297
|
const { waitForApproval: waitForApproval2 } = await Promise.resolve().then(() => (init_poller(), poller_exports));
|
|
914
3298
|
return waitForApproval2(txId, this.txManager, options);
|
|
915
3299
|
},
|
|
3300
|
+
requestApproval: async (txId) => {
|
|
3301
|
+
const tx = this.txManager.get(txId);
|
|
3302
|
+
if (!tx) throw new Error(`Transaction ${txId} not found.`);
|
|
3303
|
+
if (tx.status !== "pending") throw new Error(`Transaction ${txId} is not pending.`);
|
|
3304
|
+
const { existsSync: existsSync3 } = await import("fs");
|
|
3305
|
+
const keyPath = (0, import_node_path10.join)(this.home, "keys", "private.pem");
|
|
3306
|
+
if (!existsSync3(keyPath)) {
|
|
3307
|
+
throw new Error('Private key not found. Run "agentpay setup" first.');
|
|
3308
|
+
}
|
|
3309
|
+
const { requestBrowserApproval: requestBrowserApproval2 } = await Promise.resolve().then(() => (init_approval_server(), approval_server_exports));
|
|
3310
|
+
return requestBrowserApproval2(tx, this.txManager, this.auditLogger, this.home);
|
|
3311
|
+
},
|
|
3312
|
+
requestMobileApproval: async (txId, notify) => {
|
|
3313
|
+
const tx = this.txManager.get(txId);
|
|
3314
|
+
if (!tx) throw new Error(`Transaction ${txId} not found.`);
|
|
3315
|
+
if (tx.status !== "pending") throw new Error(`Transaction ${txId} is not pending.`);
|
|
3316
|
+
const { existsSync: existsSync3 } = await import("fs");
|
|
3317
|
+
const keyPath = (0, import_node_path10.join)(this.home, "keys", "private.pem");
|
|
3318
|
+
if (!existsSync3(keyPath)) {
|
|
3319
|
+
throw new Error('Private key not found. Run "agentpay setup" first.');
|
|
3320
|
+
}
|
|
3321
|
+
const { requestMobileApproval: requestMobileApproval2 } = await Promise.resolve().then(() => (init_mobile_approval_server(), mobile_approval_server_exports));
|
|
3322
|
+
return requestMobileApproval2(tx, this.txManager, this.auditLogger, {
|
|
3323
|
+
notify,
|
|
3324
|
+
home: this.home
|
|
3325
|
+
});
|
|
3326
|
+
},
|
|
916
3327
|
execute: async (txId) => {
|
|
917
3328
|
const tx = this.txManager.get(txId);
|
|
918
3329
|
if (!tx) throw new Error(`Transaction ${txId} not found.`);
|
|
@@ -942,7 +3353,7 @@ var AgentPay = class {
|
|
|
942
3353
|
if (!this.passphrase) {
|
|
943
3354
|
throw new Error("Passphrase required for execution. Pass it to AgentPay constructor.");
|
|
944
3355
|
}
|
|
945
|
-
const vaultPath = (0,
|
|
3356
|
+
const vaultPath = (0, import_node_path10.join)(this.home, "credentials.enc");
|
|
946
3357
|
const vault = loadVault(vaultPath);
|
|
947
3358
|
const credentials = decrypt(vault, this.passphrase);
|
|
948
3359
|
const result = await this.executor.execute(tx, credentials);
|
|
@@ -973,6 +3384,7 @@ var AgentPay = class {
|
|
|
973
3384
|
return { getLog: () => this.auditLogger.getLog() };
|
|
974
3385
|
}
|
|
975
3386
|
status() {
|
|
3387
|
+
const cfg = loadConfig(this.home);
|
|
976
3388
|
try {
|
|
977
3389
|
const wallet = this.budgetManager.getBalance();
|
|
978
3390
|
const pending = this.txManager.getPending();
|
|
@@ -983,7 +3395,8 @@ var AgentPay = class {
|
|
|
983
3395
|
limitPerTx: wallet.limitPerTx,
|
|
984
3396
|
pending,
|
|
985
3397
|
recent,
|
|
986
|
-
isSetup: true
|
|
3398
|
+
isSetup: true,
|
|
3399
|
+
mobileMode: cfg.mobileMode
|
|
987
3400
|
};
|
|
988
3401
|
} catch {
|
|
989
3402
|
return {
|
|
@@ -992,14 +3405,15 @@ var AgentPay = class {
|
|
|
992
3405
|
limitPerTx: 0,
|
|
993
3406
|
pending: [],
|
|
994
3407
|
recent: [],
|
|
995
|
-
isSetup: false
|
|
3408
|
+
isSetup: false,
|
|
3409
|
+
mobileMode: cfg.mobileMode
|
|
996
3410
|
};
|
|
997
3411
|
}
|
|
998
3412
|
}
|
|
999
3413
|
};
|
|
1000
3414
|
|
|
1001
3415
|
// src/index.ts
|
|
1002
|
-
var VERSION = "0.1.0";
|
|
3416
|
+
var VERSION = true ? "0.1.3" : "0.0.0";
|
|
1003
3417
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1004
3418
|
0 && (module.exports = {
|
|
1005
3419
|
AgentPay,
|
|
@@ -1035,11 +3449,25 @@ var VERSION = "0.1.0";
|
|
|
1035
3449
|
getPlaceholderVariables,
|
|
1036
3450
|
getTransactionsPath,
|
|
1037
3451
|
getWalletPath,
|
|
3452
|
+
loadConfig,
|
|
1038
3453
|
loadPrivateKey,
|
|
1039
3454
|
loadPublicKey,
|
|
1040
3455
|
loadVault,
|
|
3456
|
+
openBrowser,
|
|
3457
|
+
openTunnel,
|
|
3458
|
+
promptConfirm,
|
|
3459
|
+
promptInput,
|
|
3460
|
+
promptPassphrase,
|
|
3461
|
+
promptPassphraseSafe,
|
|
3462
|
+
requestBrowserApproval,
|
|
3463
|
+
requestBrowserSetup,
|
|
3464
|
+
requestMobileApproval,
|
|
3465
|
+
saveConfig,
|
|
1041
3466
|
saveKeyPair,
|
|
1042
3467
|
saveVault,
|
|
3468
|
+
sendNotification,
|
|
3469
|
+
setMobileMode,
|
|
3470
|
+
startDashboardServer,
|
|
1043
3471
|
verifyMandate,
|
|
1044
3472
|
waitForApproval
|
|
1045
3473
|
});
|