web-agent-bridge 3.9.0 → 3.9.1
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/package.json +1 -1
- package/public/atp.html +3 -0
- package/public/transparency.html +285 -0
- package/server/routes/transactions.js +15 -0
- package/server/services/stripe.js +19 -0
- package/server/services/transactions.js +150 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "web-agent-bridge",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.1",
|
|
4
4
|
"description": "Agent Transaction Bridge — the trust + transaction layer for agentic commerce. Signed intent contracts, idempotent transactions, Ed25519-verifiable receipts, explicit compensation. Plus the original WAB stack: sovereign browser, ShieldQR, SSL health, DNS discovery, agent mesh, and unified gateway for safe AI–website interaction.",
|
|
5
5
|
"author": "Web Agent Bridge <dev@webagentbridge.com>",
|
|
6
6
|
"main": "server/index.js",
|
package/public/atp.html
CHANGED
|
@@ -166,6 +166,9 @@ console.log(verification.verification.ok); // true</code></pre>
|
|
|
166
166
|
|
|
167
167
|
<h2>Reference</h2>
|
|
168
168
|
<p>Full API in <a href="/docs.html">/docs.html</a> and machine-readable spec in <code>docs/SPEC.md</code>. Mount path: <code>/api/atp</code>. Source: <a href="https://github.com/abokenan444/web-agent-bridge">github.com/abokenan444/web-agent-bridge</a>.</p>
|
|
169
|
+
|
|
170
|
+
<h2>We run our own business on it</h2>
|
|
171
|
+
<p>WAB doesn't just publish this protocol — it bills its own customers with it. Every subscription processed through webagentbridge.com produces a publicly-verifiable Ed25519 receipt. Audit the books at <a href="/transparency.html"><b>/transparency.html</b></a>.</p>
|
|
169
172
|
</main>
|
|
170
173
|
</body>
|
|
171
174
|
</html>
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>WAB Transparency — Live ATP Receipts for Every Subscription</title>
|
|
7
|
+
<meta name="description" content="WAB doesn't just build the trust layer for agentic commerce — it runs its own business on it. Every subscription processed through webagentbridge.com is an ATP transaction with a public Ed25519 receipt.">
|
|
8
|
+
<meta property="og:title" content="WAB Transparency — Live ATP Receipts">
|
|
9
|
+
<meta property="og:description" content="Every WAB subscription produces a publicly-verifiable Ed25519 receipt. Audit the books, byte by byte.">
|
|
10
|
+
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
|
|
11
|
+
<style>
|
|
12
|
+
:root {
|
|
13
|
+
--bg:#0b0d12; --bg2:#11141b; --fg:#e6e8ec; --muted:#8a93a6;
|
|
14
|
+
--accent:#7c5cff; --accent2:#22d3ee; --ok:#10b981; --bad:#ef4444;
|
|
15
|
+
--border:#1f2430;
|
|
16
|
+
}
|
|
17
|
+
* { box-sizing:border-box; }
|
|
18
|
+
body {
|
|
19
|
+
margin:0; font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
|
|
20
|
+
background: radial-gradient(1200px 600px at 20% -10%, rgba(124,92,255,.18), transparent 60%),
|
|
21
|
+
radial-gradient(900px 500px at 90% 10%, rgba(34,211,238,.12), transparent 55%),
|
|
22
|
+
var(--bg);
|
|
23
|
+
color:var(--fg); line-height:1.55; min-height:100vh;
|
|
24
|
+
}
|
|
25
|
+
a { color:var(--accent2); text-decoration:none; }
|
|
26
|
+
a:hover { text-decoration:underline; }
|
|
27
|
+
.container { max-width: 1100px; margin: 0 auto; padding: 0 24px; }
|
|
28
|
+
header.top {
|
|
29
|
+
padding: 22px 0; border-bottom:1px solid var(--border);
|
|
30
|
+
display:flex; align-items:center; justify-content:space-between;
|
|
31
|
+
}
|
|
32
|
+
.brand { font-weight:700; letter-spacing:.2px; }
|
|
33
|
+
.brand span { color: var(--accent); }
|
|
34
|
+
nav a { margin-left: 18px; color: var(--muted); font-size:14px; }
|
|
35
|
+
nav a:hover { color: var(--fg); }
|
|
36
|
+
|
|
37
|
+
.hero { padding: 70px 0 50px; text-align:center; }
|
|
38
|
+
.hero .eyebrow {
|
|
39
|
+
display:inline-block; padding:6px 14px; border-radius:999px;
|
|
40
|
+
background: rgba(124,92,255,.12); color:#c8bdff; font-size:12px;
|
|
41
|
+
letter-spacing:.18em; text-transform:uppercase; border:1px solid rgba(124,92,255,.35);
|
|
42
|
+
}
|
|
43
|
+
.hero h1 {
|
|
44
|
+
font-size: clamp(34px, 5vw, 56px); margin: 18px 0 14px; line-height:1.08;
|
|
45
|
+
background: linear-gradient(180deg, #fff 30%, #b8c0d4 100%);
|
|
46
|
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
47
|
+
font-weight: 800; letter-spacing:-.5px;
|
|
48
|
+
}
|
|
49
|
+
.hero p.sub { color: var(--muted); max-width: 780px; margin: 0 auto; font-size:18px; }
|
|
50
|
+
.hero p.tag { margin-top: 22px; font-size: 15px; color:#cdd5e3; }
|
|
51
|
+
.hero p.tag strong { color: var(--fg); }
|
|
52
|
+
|
|
53
|
+
.stats {
|
|
54
|
+
display:grid; grid-template-columns: repeat(4, 1fr); gap: 14px;
|
|
55
|
+
margin: 36px 0 12px;
|
|
56
|
+
}
|
|
57
|
+
.stat {
|
|
58
|
+
background: linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.01));
|
|
59
|
+
border:1px solid var(--border); border-radius:14px; padding:18px;
|
|
60
|
+
}
|
|
61
|
+
.stat .k { color: var(--muted); font-size:12px; letter-spacing:.08em; text-transform:uppercase; }
|
|
62
|
+
.stat .v { font-size: 26px; font-weight: 700; margin-top:6px; }
|
|
63
|
+
.stat .v small { color: var(--muted); font-size:13px; font-weight:500; }
|
|
64
|
+
@media (max-width: 720px) { .stats { grid-template-columns: repeat(2, 1fr); } }
|
|
65
|
+
|
|
66
|
+
section { padding: 30px 0; }
|
|
67
|
+
h2 { font-size: 24px; margin: 28px 0 14px; letter-spacing:-.3px; }
|
|
68
|
+
|
|
69
|
+
.feed {
|
|
70
|
+
background: var(--bg2); border: 1px solid var(--border); border-radius: 14px; overflow:hidden;
|
|
71
|
+
}
|
|
72
|
+
table { width:100%; border-collapse: collapse; font-size: 14px; }
|
|
73
|
+
th, td { padding: 12px 14px; text-align: left; border-bottom: 1px solid var(--border); }
|
|
74
|
+
th { background: rgba(255,255,255,.02); color: var(--muted); font-weight: 600;
|
|
75
|
+
font-size: 12px; letter-spacing:.08em; text-transform:uppercase; }
|
|
76
|
+
tr:last-child td { border-bottom: 0; }
|
|
77
|
+
tr:hover td { background: rgba(124,92,255,.04); }
|
|
78
|
+
.mono { font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12.5px; }
|
|
79
|
+
.receipt-id { color: var(--accent2); }
|
|
80
|
+
.tier { display:inline-block; padding:3px 8px; border-radius:6px; font-size:11px;
|
|
81
|
+
font-weight:700; text-transform:uppercase; letter-spacing:.06em; }
|
|
82
|
+
.tier.starter { background: rgba(34,211,238,.15); color:#7ee9ff; }
|
|
83
|
+
.tier.pro { background: rgba(124,92,255,.18); color:#c8bdff; }
|
|
84
|
+
.tier.business { background: rgba(16,185,129,.18); color:#7feac3; }
|
|
85
|
+
.tier.enterprise{ background: rgba(245,158,11,.18); color:#ffd58a; }
|
|
86
|
+
.badge-verify {
|
|
87
|
+
display:inline-flex; align-items:center; gap:6px;
|
|
88
|
+
background: rgba(16,185,129,.12); color: var(--ok); padding: 4px 10px;
|
|
89
|
+
border-radius:999px; font-size:12px; font-weight:600; border:1px solid rgba(16,185,129,.3);
|
|
90
|
+
cursor:pointer; transition: all .15s;
|
|
91
|
+
}
|
|
92
|
+
.badge-verify:hover { background: rgba(16,185,129,.22); }
|
|
93
|
+
.badge-verify.bad { background: rgba(239,68,68,.12); color:var(--bad); border-color: rgba(239,68,68,.3); }
|
|
94
|
+
.badge-verify.loading { opacity:.6; }
|
|
95
|
+
|
|
96
|
+
.empty { padding:38px 18px; text-align:center; color: var(--muted); }
|
|
97
|
+
.empty code { background: var(--bg); padding:2px 6px; border-radius:4px; color: var(--accent2); }
|
|
98
|
+
|
|
99
|
+
.how {
|
|
100
|
+
background: var(--bg2); border:1px solid var(--border); border-radius:14px;
|
|
101
|
+
padding: 22px 24px; margin: 24px 0;
|
|
102
|
+
}
|
|
103
|
+
.how ol { padding-left: 20px; margin: 8px 0 0; }
|
|
104
|
+
.how li { margin: 8px 0; color:#cdd5e3; }
|
|
105
|
+
.how code { background: var(--bg); padding:2px 6px; border-radius:4px;
|
|
106
|
+
color: var(--accent2); font-size: 12.5px; }
|
|
107
|
+
|
|
108
|
+
footer {
|
|
109
|
+
border-top:1px solid var(--border); margin-top: 60px; padding: 30px 0;
|
|
110
|
+
color: var(--muted); font-size: 13px; text-align:center;
|
|
111
|
+
}
|
|
112
|
+
footer a { color: var(--muted); margin: 0 10px; }
|
|
113
|
+
</style>
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<header class="top">
|
|
117
|
+
<div class="container" style="display:flex;align-items:center;justify-content:space-between;width:100%;">
|
|
118
|
+
<div class="brand">Web Agent Bridge <span>·</span> Transparency</div>
|
|
119
|
+
<nav>
|
|
120
|
+
<a href="/">Home</a>
|
|
121
|
+
<a href="/atp.html">ATP Spec</a>
|
|
122
|
+
<a href="/docs.html">Docs</a>
|
|
123
|
+
<a href="/premium.html">Pricing</a>
|
|
124
|
+
</nav>
|
|
125
|
+
</div>
|
|
126
|
+
</header>
|
|
127
|
+
|
|
128
|
+
<main>
|
|
129
|
+
<section class="hero">
|
|
130
|
+
<div class="container">
|
|
131
|
+
<div class="eyebrow">Eat your own cooking</div>
|
|
132
|
+
<h1>Every subscription is a public, signed receipt.</h1>
|
|
133
|
+
<p class="sub">
|
|
134
|
+
WAB doesn't just build the trust layer for agentic commerce — it runs its own
|
|
135
|
+
business on it. Every dollar that flows into webagentbridge.com is itself an
|
|
136
|
+
ATP transaction with a publicly-verifiable Ed25519 receipt.
|
|
137
|
+
</p>
|
|
138
|
+
<p class="tag">
|
|
139
|
+
<strong>This page reads directly from the production ATP ledger.</strong>
|
|
140
|
+
Click <em>Verify</em> on any row to re-check the signature in your browser.
|
|
141
|
+
</p>
|
|
142
|
+
|
|
143
|
+
<div class="stats" id="stats">
|
|
144
|
+
<div class="stat"><div class="k">Receipts issued</div><div class="v" id="s-count">—</div></div>
|
|
145
|
+
<div class="stat"><div class="k">Total settled</div><div class="v" id="s-total">—</div></div>
|
|
146
|
+
<div class="stat"><div class="k">First receipt</div><div class="v" id="s-first">—</div></div>
|
|
147
|
+
<div class="stat"><div class="k">Latest receipt</div><div class="v" id="s-last">—</div></div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</section>
|
|
151
|
+
|
|
152
|
+
<section>
|
|
153
|
+
<div class="container">
|
|
154
|
+
<h2>Live receipt feed</h2>
|
|
155
|
+
<div class="feed">
|
|
156
|
+
<table>
|
|
157
|
+
<thead>
|
|
158
|
+
<tr>
|
|
159
|
+
<th>Issued</th>
|
|
160
|
+
<th>Tier</th>
|
|
161
|
+
<th>Amount</th>
|
|
162
|
+
<th>Receipt ID</th>
|
|
163
|
+
<th>Key</th>
|
|
164
|
+
<th style="text-align:right;">Signature</th>
|
|
165
|
+
</tr>
|
|
166
|
+
</thead>
|
|
167
|
+
<tbody id="rows">
|
|
168
|
+
<tr><td colspan="6" class="empty">Loading public ATP ledger…</td></tr>
|
|
169
|
+
</tbody>
|
|
170
|
+
</table>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="how">
|
|
174
|
+
<h2 style="margin-top:0;">How to audit this yourself</h2>
|
|
175
|
+
<ol>
|
|
176
|
+
<li>List receipts: <code>GET /api/atp/platform/receipts?limit=50</code></li>
|
|
177
|
+
<li>Fetch full signed body: <code>GET /api/atp/receipts/<receipt_id></code></li>
|
|
178
|
+
<li>Verify Ed25519 signature: <code>POST /api/atp/receipts/verify</code> with <code>{"receipt_id":"…"}</code></li>
|
|
179
|
+
<li>Or verify offline: every receipt embeds its own public key under <code>signature.public_key</code>. The canonical body is the receipt JSON with the <code>signature</code> field removed.</li>
|
|
180
|
+
</ol>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</section>
|
|
184
|
+
</main>
|
|
185
|
+
|
|
186
|
+
<footer>
|
|
187
|
+
<div class="container">
|
|
188
|
+
<div>WAB · <a href="/atp.html">ATP Protocol</a> · <a href="/docs.html">Docs</a> · <a href="/privacy.html">Privacy</a> · <a href="/terms.html">Terms</a></div>
|
|
189
|
+
<div style="margin-top:8px;">Receipts shown on this page are real, generated automatically when payments settle on Stripe.</div>
|
|
190
|
+
</div>
|
|
191
|
+
</footer>
|
|
192
|
+
|
|
193
|
+
<script>
|
|
194
|
+
const $ = (id) => document.getElementById(id);
|
|
195
|
+
|
|
196
|
+
function fmtMoney(cents, currency) {
|
|
197
|
+
const v = (cents || 0) / 100;
|
|
198
|
+
try { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency || 'USD' }).format(v); }
|
|
199
|
+
catch { return `${v.toFixed(2)} ${currency || ''}`; }
|
|
200
|
+
}
|
|
201
|
+
function fmtDate(s) {
|
|
202
|
+
if (!s) return '—';
|
|
203
|
+
const d = new Date(s.replace(' ', 'T') + (s.includes('Z') ? '' : 'Z'));
|
|
204
|
+
if (isNaN(+d)) return s;
|
|
205
|
+
return d.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
|
206
|
+
}
|
|
207
|
+
function shortId(id, n = 12) { return id ? id.slice(0, n) + '…' : '—'; }
|
|
208
|
+
|
|
209
|
+
async function loadStats() {
|
|
210
|
+
try {
|
|
211
|
+
const r = await fetch('/api/atp/platform/stats').then(r => r.json());
|
|
212
|
+
const s = r.data || {};
|
|
213
|
+
$('s-count').innerHTML = (s.receipts || 0).toLocaleString();
|
|
214
|
+
// total_cents is mixed currency; show approximate USD-equivalent string with note
|
|
215
|
+
$('s-total').innerHTML = `${fmtMoney(s.total_cents || 0, 'USD')} <small>gross</small>`;
|
|
216
|
+
$('s-first').innerHTML = s.first_at ? fmtDate(s.first_at) : '—';
|
|
217
|
+
$('s-last').innerHTML = s.last_at ? fmtDate(s.last_at) : '—';
|
|
218
|
+
} catch (e) {
|
|
219
|
+
$('s-count').textContent = 'n/a';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function verifyReceipt(receiptId, btn) {
|
|
224
|
+
btn.classList.add('loading');
|
|
225
|
+
btn.textContent = 'verifying…';
|
|
226
|
+
try {
|
|
227
|
+
const r = await fetch('/api/atp/receipts/verify', {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
headers: { 'Content-Type': 'application/json' },
|
|
230
|
+
body: JSON.stringify({ receipt_id: receiptId })
|
|
231
|
+
}).then(r => r.json());
|
|
232
|
+
btn.classList.remove('loading');
|
|
233
|
+
if (r.ok && r.verification && r.verification.ok) {
|
|
234
|
+
btn.classList.remove('bad');
|
|
235
|
+
btn.innerHTML = '✓ signature valid';
|
|
236
|
+
} else {
|
|
237
|
+
btn.classList.add('bad');
|
|
238
|
+
btn.textContent = '✗ ' + ((r.verification && r.verification.reason) || 'invalid');
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
btn.classList.remove('loading');
|
|
242
|
+
btn.classList.add('bad');
|
|
243
|
+
btn.textContent = '✗ network';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function loadReceipts() {
|
|
248
|
+
try {
|
|
249
|
+
const r = await fetch('/api/atp/platform/receipts?limit=50').then(r => r.json());
|
|
250
|
+
const rows = (r.data || []);
|
|
251
|
+
const tb = $('rows');
|
|
252
|
+
if (!rows.length) {
|
|
253
|
+
tb.innerHTML = `<tr><td colspan="6" class="empty">
|
|
254
|
+
No platform receipts yet. Receipts appear automatically when Stripe settles a subscription.<br>
|
|
255
|
+
Try the protocol with your own intent at <code>/atp.html</code>.
|
|
256
|
+
</td></tr>`;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
tb.innerHTML = rows.map(row => `
|
|
260
|
+
<tr>
|
|
261
|
+
<td>${fmtDate(row.issued_at)}</td>
|
|
262
|
+
<td><span class="tier ${(row.tier || '').toLowerCase()}">${row.tier || '—'}</span></td>
|
|
263
|
+
<td class="mono">${fmtMoney(row.amount_cents, row.currency)}</td>
|
|
264
|
+
<td class="mono receipt-id" title="${row.receipt_id}">
|
|
265
|
+
<a href="/api/atp/receipts/${row.receipt_id}" target="_blank" rel="noopener">${shortId(row.receipt_id, 22)}</a>
|
|
266
|
+
</td>
|
|
267
|
+
<td class="mono">${shortId(row.key_id, 10)}</td>
|
|
268
|
+
<td style="text-align:right;">
|
|
269
|
+
<button class="badge-verify" data-rid="${row.receipt_id}">Verify</button>
|
|
270
|
+
</td>
|
|
271
|
+
</tr>
|
|
272
|
+
`).join('');
|
|
273
|
+
tb.querySelectorAll('.badge-verify').forEach(btn => {
|
|
274
|
+
btn.addEventListener('click', () => verifyReceipt(btn.dataset.rid, btn));
|
|
275
|
+
});
|
|
276
|
+
} catch (e) {
|
|
277
|
+
$('rows').innerHTML = `<tr><td colspan="6" class="empty">Failed to load: ${e.message}</td></tr>`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
loadStats();
|
|
282
|
+
loadReceipts();
|
|
283
|
+
</script>
|
|
284
|
+
</body>
|
|
285
|
+
</html>
|
|
@@ -230,4 +230,19 @@ router.post('/receipts/verify', publicReceiptLimiter, express.json({ limit: '256
|
|
|
230
230
|
|
|
231
231
|
router.get('/health', (req, res) => res.json({ ok: true, service: 'atp', version: '1.0.0' }));
|
|
232
232
|
|
|
233
|
+
// ─── Public transparency feed ─────────────────────────────────────────────
|
|
234
|
+
// WAB dogfoods ATP: every subscription payment processed via webagentbridge.com
|
|
235
|
+
// produces a publicly-verifiable Ed25519 receipt. This feed lists them.
|
|
236
|
+
router.get('/platform/receipts', publicReceiptLimiter, (req, res) => {
|
|
237
|
+
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit, 10) || 20));
|
|
238
|
+
const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
|
|
239
|
+
const items = transactions.listPlatformReceipts({ limit, offset });
|
|
240
|
+
res.json({ ok: true, data: items, limit, offset });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
router.get('/platform/stats', publicReceiptLimiter, (req, res) => {
|
|
244
|
+
const stats = transactions.getPlatformStats();
|
|
245
|
+
res.json({ ok: true, data: stats });
|
|
246
|
+
});
|
|
247
|
+
|
|
233
248
|
module.exports = router;
|
|
@@ -138,6 +138,25 @@ function handleWebhookEvent(event) {
|
|
|
138
138
|
periodStart: new Date(invoice.period_start * 1000).toISOString(),
|
|
139
139
|
periodEnd: new Date(invoice.period_end * 1000).toISOString()
|
|
140
140
|
});
|
|
141
|
+
// ── WAB dogfooding: record this real money event as an ATP receipt ──
|
|
142
|
+
// Every dollar that flows into WAB is itself a publicly-verifiable
|
|
143
|
+
// Ed25519 receipt. Failure here MUST NOT block payment confirmation.
|
|
144
|
+
try {
|
|
145
|
+
const transactions = require('./transactions');
|
|
146
|
+
transactions.recordPlatformPayment({
|
|
147
|
+
userId: sub.user_id,
|
|
148
|
+
amountCents: invoice.amount_paid,
|
|
149
|
+
currency: (invoice.currency || 'USD').toUpperCase(),
|
|
150
|
+
tier: sub.tier,
|
|
151
|
+
externalRef: invoice.id || invoice.payment_intent,
|
|
152
|
+
description: `WAB ${sub.tier} subscription`,
|
|
153
|
+
periodStart: invoice.period_start ? new Date(invoice.period_start * 1000).toISOString() : null,
|
|
154
|
+
periodEnd: invoice.period_end ? new Date(invoice.period_end * 1000).toISOString() : null,
|
|
155
|
+
provider: 'stripe',
|
|
156
|
+
});
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.error('[atp] recordPlatformPayment failed (non-fatal):', e.message);
|
|
159
|
+
}
|
|
141
160
|
}
|
|
142
161
|
}
|
|
143
162
|
break;
|
|
@@ -507,6 +507,154 @@ function expireOverdueIntents() {
|
|
|
507
507
|
return r.changes;
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
// ── Platform self-dogfooding ─────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Record a real WAB platform money event (e.g. Stripe subscription payment)
|
|
514
|
+
* as a complete ATP cycle: intent → authorize → tx → step → settle → receipt.
|
|
515
|
+
*
|
|
516
|
+
* This is "eat-your-own-cooking": every dollar that flows into WAB is itself
|
|
517
|
+
* a publicly-verifiable Ed25519 receipt that any auditor can re-verify.
|
|
518
|
+
*
|
|
519
|
+
* Idempotent on (externalRef): replaying the same Stripe invoice id is a
|
|
520
|
+
* no-op and returns the existing receipt.
|
|
521
|
+
*/
|
|
522
|
+
function recordPlatformPayment(params) {
|
|
523
|
+
const {
|
|
524
|
+
userId, amountCents, currency = 'USD', tier,
|
|
525
|
+
externalRef, description = null,
|
|
526
|
+
periodStart = null, periodEnd = null,
|
|
527
|
+
provider = 'stripe',
|
|
528
|
+
} = params;
|
|
529
|
+
|
|
530
|
+
if (!userId) throw badRequest('userId required');
|
|
531
|
+
if (!Number.isInteger(amountCents) || amountCents < 0) throw badRequest('amountCents must be non-negative integer');
|
|
532
|
+
if (!externalRef || typeof externalRef !== 'string') throw badRequest('externalRef required');
|
|
533
|
+
if (!tier || typeof tier !== 'string') throw badRequest('tier required');
|
|
534
|
+
|
|
535
|
+
// Idempotency: if a receipt already exists for this external ref, return it.
|
|
536
|
+
const prior = db.prepare(`
|
|
537
|
+
SELECT r.id AS rid
|
|
538
|
+
FROM atp_receipts r
|
|
539
|
+
JOIN atp_transactions t ON t.id = r.transaction_id
|
|
540
|
+
JOIN atp_intents i ON i.id = t.intent_id
|
|
541
|
+
WHERE json_extract(i.metadata, '$.platform') = 1
|
|
542
|
+
AND json_extract(i.metadata, '$.external_ref') = ?
|
|
543
|
+
LIMIT 1
|
|
544
|
+
`).get(externalRef);
|
|
545
|
+
if (prior) return getReceipt(prior.rid);
|
|
546
|
+
|
|
547
|
+
const cur = String(currency || 'USD').toUpperCase();
|
|
548
|
+
const meta = {
|
|
549
|
+
platform: true,
|
|
550
|
+
kind: 'wab_subscription',
|
|
551
|
+
tier,
|
|
552
|
+
provider,
|
|
553
|
+
external_ref: externalRef,
|
|
554
|
+
period_start: periodStart,
|
|
555
|
+
period_end: periodEnd,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// 1. Intent
|
|
559
|
+
const intent = createIntent({
|
|
560
|
+
userId,
|
|
561
|
+
purpose: `WAB platform subscription — ${tier}`,
|
|
562
|
+
scope: { actions: ['pay'] },
|
|
563
|
+
spendCapCents: amountCents,
|
|
564
|
+
spendCurrency: cur,
|
|
565
|
+
maxExecutions: 1,
|
|
566
|
+
ttlSeconds: 3600,
|
|
567
|
+
metadata: meta,
|
|
568
|
+
});
|
|
569
|
+
// 2. Authorize
|
|
570
|
+
authorizeIntent(intent.id, { userId });
|
|
571
|
+
// 3. Begin tx (idempotent on externalRef)
|
|
572
|
+
const tx = beginTransaction({
|
|
573
|
+
intentId: intent.id,
|
|
574
|
+
idempotencyKey: externalRef,
|
|
575
|
+
amountCents,
|
|
576
|
+
currency: cur,
|
|
577
|
+
summary: description || `Subscription payment for ${tier}`,
|
|
578
|
+
metadata: { external_ref: externalRef, provider },
|
|
579
|
+
});
|
|
580
|
+
// 4. Step (the actual payment evidence)
|
|
581
|
+
appendStep(tx.id, {
|
|
582
|
+
action: 'pay',
|
|
583
|
+
evidence: {
|
|
584
|
+
provider,
|
|
585
|
+
external_ref: externalRef,
|
|
586
|
+
amount_cents: amountCents,
|
|
587
|
+
currency: cur,
|
|
588
|
+
tier,
|
|
589
|
+
period_start: periodStart,
|
|
590
|
+
period_end: periodEnd,
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
// 5. Drive lifecycle to settled
|
|
594
|
+
transitionTransaction(tx.id, 'executing');
|
|
595
|
+
transitionTransaction(tx.id, 'executed');
|
|
596
|
+
transitionTransaction(tx.id, 'settled');
|
|
597
|
+
// 6. Issue receipt
|
|
598
|
+
return issueReceipt(tx.id);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* List platform-issued receipts (the "transparency feed").
|
|
603
|
+
* Public-safe: filters to intents with metadata.platform=1 only.
|
|
604
|
+
*/
|
|
605
|
+
function listPlatformReceipts({ limit = 20, offset = 0 } = {}) {
|
|
606
|
+
const lim = Math.max(1, Math.min(100, Number(limit) || 20));
|
|
607
|
+
const off = Math.max(0, Number(offset) || 0);
|
|
608
|
+
const rows = db.prepare(`
|
|
609
|
+
SELECT r.id AS receipt_id,
|
|
610
|
+
r.issued_at AS issued_at,
|
|
611
|
+
r.algorithm AS algorithm,
|
|
612
|
+
r.key_id AS key_id,
|
|
613
|
+
t.amount_cents AS amount_cents,
|
|
614
|
+
t.currency AS currency,
|
|
615
|
+
t.status AS tx_status,
|
|
616
|
+
json_extract(i.metadata, '$.tier') AS tier,
|
|
617
|
+
json_extract(i.metadata, '$.provider') AS provider,
|
|
618
|
+
json_extract(i.metadata, '$.period_start') AS period_start,
|
|
619
|
+
json_extract(i.metadata, '$.period_end') AS period_end
|
|
620
|
+
FROM atp_receipts r
|
|
621
|
+
JOIN atp_transactions t ON t.id = r.transaction_id
|
|
622
|
+
JOIN atp_intents i ON i.id = t.intent_id
|
|
623
|
+
WHERE json_extract(i.metadata, '$.platform') = 1
|
|
624
|
+
ORDER BY r.issued_at DESC
|
|
625
|
+
LIMIT ? OFFSET ?
|
|
626
|
+
`).all(lim, off);
|
|
627
|
+
return rows;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Aggregate stats for the public transparency page.
|
|
632
|
+
*/
|
|
633
|
+
function getPlatformStats() {
|
|
634
|
+
const row = db.prepare(`
|
|
635
|
+
SELECT COUNT(*) AS receipts,
|
|
636
|
+
COALESCE(SUM(t.amount_cents), 0) AS total_cents,
|
|
637
|
+
MIN(r.issued_at) AS first_at,
|
|
638
|
+
MAX(r.issued_at) AS last_at
|
|
639
|
+
FROM atp_receipts r
|
|
640
|
+
JOIN atp_transactions t ON t.id = r.transaction_id
|
|
641
|
+
JOIN atp_intents i ON i.id = t.intent_id
|
|
642
|
+
WHERE json_extract(i.metadata, '$.platform') = 1
|
|
643
|
+
`).get();
|
|
644
|
+
const byTier = db.prepare(`
|
|
645
|
+
SELECT json_extract(i.metadata, '$.tier') AS tier,
|
|
646
|
+
COUNT(*) AS receipts,
|
|
647
|
+
COALESCE(SUM(t.amount_cents), 0) AS total_cents
|
|
648
|
+
FROM atp_receipts r
|
|
649
|
+
JOIN atp_transactions t ON t.id = r.transaction_id
|
|
650
|
+
JOIN atp_intents i ON i.id = t.intent_id
|
|
651
|
+
WHERE json_extract(i.metadata, '$.platform') = 1
|
|
652
|
+
GROUP BY tier
|
|
653
|
+
ORDER BY total_cents DESC
|
|
654
|
+
`).all();
|
|
655
|
+
return { ...row, by_tier: byTier };
|
|
656
|
+
}
|
|
657
|
+
|
|
510
658
|
module.exports = {
|
|
511
659
|
// intents
|
|
512
660
|
createIntent, getIntent, listIntentsForUser, authorizeIntent, revokeIntent,
|
|
@@ -520,6 +668,8 @@ module.exports = {
|
|
|
520
668
|
compensateTransaction,
|
|
521
669
|
// maintenance
|
|
522
670
|
expireOverdueIntents,
|
|
671
|
+
// platform dogfooding
|
|
672
|
+
recordPlatformPayment, listPlatformReceipts, getPlatformStats,
|
|
523
673
|
// re-exports for tests
|
|
524
674
|
_validateScope: validateScope,
|
|
525
675
|
};
|