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