wayleave 0.1.4 → 0.1.5
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 +55 -1
- package/index.d.ts +71 -1
- package/index.js +123 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ const gate = new Wayleave({
|
|
|
36
36
|
},
|
|
37
37
|
rateLimits: { declared_agent: 10 },
|
|
38
38
|
pricedPaths: { '/api/premium': 0.05 }, // agents pay 5¢/call, humans free
|
|
39
|
+
verifyPayment: (proof, { price, resource }) =>
|
|
40
|
+
facilitator.confirmSettled(proof, price, resource), // REQUIRED to sell
|
|
39
41
|
onEvent: e => queueForBilling(e), // metering hook; never blocks serving
|
|
40
42
|
});
|
|
41
43
|
|
|
@@ -53,7 +55,59 @@ app.use(gate.express());
|
|
|
53
55
|
|
|
54
56
|
## Status
|
|
55
57
|
|
|
56
|
-
v0.1 — verification, lanes, policy, pricing, and the metering hook, all under test (
|
|
58
|
+
v0.1.5 — verification, lanes, policy, pricing, and the metering hook, all under test (44 scenarios, including forgery, tampering, replay, guessed payment proofs, spoofed rate-limit identities, and cross-implementation wire formats). Payment settlement network integration is in progress; the 402 challenge is built in and settlement confirmation is a function you supply.
|
|
59
|
+
|
|
60
|
+
0.1.5 closes a hole worth naming: before it, a priced route accepted a payment proof that any agent could derive from the 402 challenge it had just been sent — free passage, recorded as revenue. Settlement confirmation is now yours to supply and denies by default. If you shipped 0.1.4 or earlier on a priced route, upgrade.
|
|
61
|
+
|
|
62
|
+
## How payment actually works
|
|
63
|
+
|
|
64
|
+
Pay-at-the-door, not IOUs. On a route you price:
|
|
65
|
+
|
|
66
|
+
1. An agent requests the route → Wayleave answers `402 Payment Required`
|
|
67
|
+
with an x402-shaped challenge: the price and *your* receiving address.
|
|
68
|
+
2. A wallet-carrying agent signs payment and retries with proof attached.
|
|
69
|
+
3. The proof is verified and settlement executes on a licensed rail
|
|
70
|
+
(x402 facilitator) — money moves to your account.
|
|
71
|
+
4. Only then: `200`. No settled payment, no passage. Data never moves
|
|
72
|
+
on a promise.
|
|
73
|
+
|
|
74
|
+
Agents without wallets (most crawlers today) are simply turned away on
|
|
75
|
+
priced routes — you aren't paid by them, but you also never serve them
|
|
76
|
+
free. The ledger shows exactly how much turned-away demand is standing
|
|
77
|
+
at your gate.
|
|
78
|
+
|
|
79
|
+
Wayleave never holds funds. Agent money flows agent → facilitator → you.
|
|
80
|
+
The hosted meter (not built yet — see the waitlist) is what wires this
|
|
81
|
+
end-to-end; today the 402 challenge is built in and step 3 is a function
|
|
82
|
+
you supply.
|
|
83
|
+
|
|
84
|
+
**Step 3 is yours, and it defaults to no.** `verifyPayment(proof, ctx)` is
|
|
85
|
+
the only thing that can turn a 402 into a 200. Configure nothing and every
|
|
86
|
+
priced route stays 402 forever — deliberately. A payment proof is a string
|
|
87
|
+
written by the party who owes you money, so there is nothing this library
|
|
88
|
+
can check about it on its own that the payer could not have fabricated.
|
|
89
|
+
Point it at a facilitator that confirms settlement, and never at a
|
|
90
|
+
comparison against the challenge you just issued. A verifier that returns
|
|
91
|
+
anything but `true`/`{ok:true}` — or that throws — denies passage, and
|
|
92
|
+
nothing but a confirmed settlement is ever written to the ledger as billed.
|
|
93
|
+
|
|
94
|
+
## Security posture
|
|
95
|
+
|
|
96
|
+
- **Rate limits key on the connection address, not `x-forwarded-for`.**
|
|
97
|
+
The header is client-written unless a proxy you own overwrites it; believing
|
|
98
|
+
it by default hands an attacker unlimited fresh buckets. Behind a real proxy,
|
|
99
|
+
set `trustProxy: true`. Pass `req.ip` (the Express adapter does).
|
|
100
|
+
- **In-memory state is bounded** by `maxTracked` (default 10,000) and cleared
|
|
101
|
+
each rate window, so a flood of distinct clients cannot exhaust memory.
|
|
102
|
+
- **Replay: single-use is enforced only for signers that send RFC 9421's
|
|
103
|
+
`nonce`.** Signature bytes are not a usable substitute — Ed25519 is
|
|
104
|
+
deterministic and the Web Bot Auth profile covers only `@authority`, so one
|
|
105
|
+
agent hitting two paths in the same second produces byte-identical
|
|
106
|
+
signatures. Rejecting those would break real traffic. Without a signer
|
|
107
|
+
nonce, replay is bounded by the expiry window and nothing tighter.
|
|
108
|
+
- **State is per-process.** Rate limits and replay memory do not survive a
|
|
109
|
+
restart and do not span instances — two instances mean two independent
|
|
110
|
+
limiters. Shared state is the next real gap, alongside key rotation.
|
|
57
111
|
|
|
58
112
|
## License
|
|
59
113
|
|
package/index.d.ts
CHANGED
|
@@ -53,6 +53,13 @@ export interface WayleaveRequest {
|
|
|
53
53
|
/** Host authority the signature was computed over, e.g. "example.com". */
|
|
54
54
|
authority: string;
|
|
55
55
|
headers: Record<string, string | string[] | undefined>;
|
|
56
|
+
/**
|
|
57
|
+
* The connection's source address, e.g. `req.socket.remoteAddress`. Used to
|
|
58
|
+
* key rate limits for unsigned traffic. Supply it: without it every
|
|
59
|
+
* anonymous client shares one bucket. Never taken from a header unless
|
|
60
|
+
* `trustProxy` is on.
|
|
61
|
+
*/
|
|
62
|
+
ip?: string;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
export interface VerifyResult {
|
|
@@ -61,6 +68,15 @@ export interface VerifyResult {
|
|
|
61
68
|
agentId: string | null;
|
|
62
69
|
/** Human-readable reason, safe to log. */
|
|
63
70
|
reason: string;
|
|
71
|
+
/**
|
|
72
|
+
* Single-use token, set only when the signer supplied RFC 9421's `nonce`
|
|
73
|
+
* parameter. Null otherwise — a signature hash is NOT a safe substitute,
|
|
74
|
+
* because deterministic Ed25519 over `@authority` alone makes two
|
|
75
|
+
* legitimate same-second requests byte-identical.
|
|
76
|
+
*/
|
|
77
|
+
nonce?: string | null;
|
|
78
|
+
/** Unix seconds the signature stops being valid. */
|
|
79
|
+
expires?: number;
|
|
64
80
|
}
|
|
65
81
|
|
|
66
82
|
export interface Classification {
|
|
@@ -68,8 +84,34 @@ export interface Classification {
|
|
|
68
84
|
agentId: string | null;
|
|
69
85
|
/** Why this lane was chosen. Loggable. */
|
|
70
86
|
evidence: string[];
|
|
87
|
+
/** Present only for verified agents whose signer sent a nonce. */
|
|
88
|
+
nonce?: string | null;
|
|
89
|
+
sigExpires?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** What `verifyPayment` is told about the crossing it is confirming. */
|
|
93
|
+
export interface PaymentContext {
|
|
94
|
+
/** Price in USD for the matched resource. */
|
|
95
|
+
price: number;
|
|
96
|
+
/** The priced path prefix that matched. */
|
|
97
|
+
resource: string;
|
|
98
|
+
/** Cryptographic agent identity when verified, else lane:address. */
|
|
99
|
+
identity: string;
|
|
100
|
+
path: string;
|
|
101
|
+
/** Unix seconds. */
|
|
102
|
+
now: number;
|
|
71
103
|
}
|
|
72
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Your answer to "did this actually settle?". Return `true`, or an object
|
|
107
|
+
* carrying the settlement reference so it reaches the ledger. Anything else
|
|
108
|
+
* — including a throw — denies passage.
|
|
109
|
+
*/
|
|
110
|
+
export type PaymentVerifier = (
|
|
111
|
+
proof: string,
|
|
112
|
+
ctx: PaymentContext
|
|
113
|
+
) => boolean | { ok: boolean; ref?: string; reason?: string };
|
|
114
|
+
|
|
73
115
|
/** x402-shaped payment challenge returned with a 402. */
|
|
74
116
|
export interface PaymentChallenge {
|
|
75
117
|
scheme: 'x402';
|
|
@@ -88,8 +130,10 @@ export interface Decision {
|
|
|
88
130
|
evidence: string[];
|
|
89
131
|
/** Present only on 402. */
|
|
90
132
|
challenge?: PaymentChallenge;
|
|
91
|
-
/** Present only when
|
|
133
|
+
/** Present only when `verifyPayment` confirmed settlement. */
|
|
92
134
|
billed?: number;
|
|
135
|
+
/** Settlement reference returned by your verifier, if any. */
|
|
136
|
+
paymentRef?: string | null;
|
|
93
137
|
}
|
|
94
138
|
|
|
95
139
|
/** What the metering hook receives. It may throw; serving continues. */
|
|
@@ -102,7 +146,10 @@ export interface MeterEvent {
|
|
|
102
146
|
status: number;
|
|
103
147
|
why: string;
|
|
104
148
|
evidence: string[];
|
|
149
|
+
/** Zero unless settlement was confirmed. Never records an unpaid crossing. */
|
|
105
150
|
billedUsd: number;
|
|
151
|
+
/** Settlement reference from your verifier, when it supplied one. */
|
|
152
|
+
paymentRef?: string;
|
|
106
153
|
}
|
|
107
154
|
|
|
108
155
|
/** `[pathPrefix, allow]` — first matching prefix wins. */
|
|
@@ -118,6 +165,29 @@ export interface WayleaveOptions {
|
|
|
118
165
|
rateWindow?: number;
|
|
119
166
|
/** Path prefix -> price in USD. Non-human lanes get 402 here. */
|
|
120
167
|
pricedPaths?: Record<string, number>;
|
|
168
|
+
/**
|
|
169
|
+
* Confirms a payment actually settled. **Without this, every priced route
|
|
170
|
+
* stays 402 forever** — deny is the only safe default, since the proof is a
|
|
171
|
+
* string the payer wrote. Wire it to your facilitator, not to a comparison
|
|
172
|
+
* against the challenge you just issued.
|
|
173
|
+
*/
|
|
174
|
+
verifyPayment?: PaymentVerifier;
|
|
175
|
+
/**
|
|
176
|
+
* Believe `x-forwarded-for` for the client address. Default false. Turn on
|
|
177
|
+
* ONLY behind a proxy you control that overwrites the header — otherwise
|
|
178
|
+
* clients mint themselves unlimited rate-limit buckets.
|
|
179
|
+
*/
|
|
180
|
+
trustProxy?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Reject a second use of a signature whose signer supplied a nonce.
|
|
183
|
+
* Default true. In-process only: it does not span instances or restarts.
|
|
184
|
+
*/
|
|
185
|
+
replayProtection?: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* Ceiling on in-memory identities and nonces. Default 10000. Bounds memory
|
|
188
|
+
* under a flood of distinct clients; oldest entries are evicted.
|
|
189
|
+
*/
|
|
190
|
+
maxTracked?: number;
|
|
121
191
|
/**
|
|
122
192
|
* Metering hook, called once per request. Exceptions are swallowed —
|
|
123
193
|
* billing must never break serving.
|
package/index.js
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* Zero dependencies. Node's native crypto only.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { createPublicKey, verify as edVerify, sign as edSign
|
|
20
|
+
import { createPublicKey, verify as edVerify, sign as edSign,
|
|
21
|
+
createHash } from 'node:crypto';
|
|
21
22
|
|
|
22
23
|
export const LANES = Object.freeze({
|
|
23
24
|
VERIFIED: 'verified_agent',
|
|
@@ -109,7 +110,15 @@ export function signRequest(headers, authority, privateKey, keyid, directoryUrl,
|
|
|
109
110
|
|
|
110
111
|
/**
|
|
111
112
|
* directories: { [directoryUrl]: { [keyid]: publicKey (KeyObject|PEM|raw b64) } }
|
|
112
|
-
* Returns { ok, agentId, reason }
|
|
113
|
+
* Returns { ok, agentId, reason, nonce, expires }
|
|
114
|
+
*
|
|
115
|
+
* `nonce` is set only when the signer supplied RFC 9421's `nonce` parameter,
|
|
116
|
+
* and it is what makes a signature single-use. Do NOT substitute a hash of
|
|
117
|
+
* the signature itself: Ed25519 is deterministic and the Web Bot Auth profile
|
|
118
|
+
* covers only `@authority`, so one agent hitting two different paths in the
|
|
119
|
+
* same second legitimately produces the SAME signature bytes. Treating that
|
|
120
|
+
* as a replay would reject real traffic. Without a signer-supplied nonce,
|
|
121
|
+
* replay is bounded by the expiry window and nothing tighter — see README.
|
|
113
122
|
*/
|
|
114
123
|
export function verifySignature(h, authority, directories,
|
|
115
124
|
now = Math.floor(Date.now() / 1000)) {
|
|
@@ -173,7 +182,12 @@ export function verifySignature(h, authority, directories,
|
|
|
173
182
|
if (!valid)
|
|
174
183
|
return { ok: false, agentId: null,
|
|
175
184
|
reason: 'signature verification FAILED (forged or tampered)' };
|
|
176
|
-
return { ok: true, agentId: `${directory}#${keyid}`, reason: 'valid signature'
|
|
185
|
+
return { ok: true, agentId: `${directory}#${keyid}`, reason: 'valid signature',
|
|
186
|
+
nonce: params.nonce
|
|
187
|
+
? createHash('sha256')
|
|
188
|
+
.update(`${directory}#${keyid}#${params.nonce}`).digest('base64')
|
|
189
|
+
: null,
|
|
190
|
+
expires };
|
|
177
191
|
}
|
|
178
192
|
|
|
179
193
|
// ── classification ──────────────────────────────────────────────────────
|
|
@@ -199,7 +213,8 @@ export function classify(req, directories, now) {
|
|
|
199
213
|
const h = req.headers, ua = (h['user-agent'] || '').toLowerCase();
|
|
200
214
|
const v = verifySignature(h, req.authority, directories, now);
|
|
201
215
|
if (v.ok) return { lane: LANES.VERIFIED, agentId: v.agentId,
|
|
202
|
-
evidence: [`crypto: ${v.reason}`]
|
|
216
|
+
evidence: [`crypto: ${v.reason}`],
|
|
217
|
+
nonce: v.nonce, sigExpires: v.expires };
|
|
203
218
|
if (h['signature'] || h['signature-input'])
|
|
204
219
|
return { lane: LANES.SUSPECT, agentId: null,
|
|
205
220
|
evidence: [`crypto: ${v.reason}`, 'presented invalid signature'] };
|
|
@@ -226,6 +241,10 @@ export class Wayleave {
|
|
|
226
241
|
* rules: { [lane]: [ [pathPrefix, allow], ... ] },
|
|
227
242
|
* rateLimits: { [lane]: perWindow }, rateWindow: 60,
|
|
228
243
|
* pricedPaths: { pathPrefix: priceUsd },
|
|
244
|
+
* verifyPayment: (proof, ctx) => boolean | { ok, ref },
|
|
245
|
+
* trustProxy: false,
|
|
246
|
+
* replayProtection: true,
|
|
247
|
+
* maxTracked: 10000,
|
|
229
248
|
* onEvent: (logEntry) => {} // metering/billing hook, async-safe
|
|
230
249
|
* }
|
|
231
250
|
*/
|
|
@@ -236,43 +255,110 @@ export class Wayleave {
|
|
|
236
255
|
this.rateWindow = opts.rateWindow || 60;
|
|
237
256
|
this.pricedPaths = opts.pricedPaths || {};
|
|
238
257
|
this.onEvent = opts.onEvent || (() => {});
|
|
239
|
-
|
|
258
|
+
|
|
259
|
+
// No payment verifier configured means no agent can ever buy passage.
|
|
260
|
+
// Deny is the only safe default: a proof is a string the PAYER wrote, and
|
|
261
|
+
// anything this library could check about it without a settlement network
|
|
262
|
+
// is something the payer could equally have made up.
|
|
263
|
+
this.verifyPayment = opts.verifyPayment || null;
|
|
264
|
+
|
|
265
|
+
// x-forwarded-for is written by the client unless a proxy you control
|
|
266
|
+
// overwrites it. Trusting it by default hands every attacker an unlimited
|
|
267
|
+
// supply of fresh rate-limit buckets. Opt in only behind a real proxy.
|
|
268
|
+
this.trustProxy = opts.trustProxy === true;
|
|
269
|
+
|
|
270
|
+
this.replayProtection = opts.replayProtection !== false;
|
|
271
|
+
this.maxTracked = opts.maxTracked || 10000;
|
|
272
|
+
|
|
273
|
+
this._hits = new Map(); // identity -> count, cleared each window
|
|
274
|
+
this._window = -1;
|
|
275
|
+
this._seen = new Map(); // signature nonce -> expiry, pruned on rollover
|
|
240
276
|
}
|
|
241
277
|
|
|
242
|
-
/** req: { method, path, authority, headers } → decision */
|
|
278
|
+
/** req: { method, path, authority, headers, ip } → decision */
|
|
243
279
|
handle(req, now = Math.floor(Date.now() / 1000), paymentProof = '') {
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
const
|
|
280
|
+
const c = classify(req, this.directories, now);
|
|
281
|
+
const { lane, agentId, evidence } = c;
|
|
282
|
+
const ident = agentId || `${lane}:${this._client(req)}`;
|
|
283
|
+
const proof = paymentProof || headerValue(req.headers, 'x-payment-proof');
|
|
284
|
+
const d = this._decide(req, lane, ident, now, proof, c);
|
|
248
285
|
const entry = { t: now, path: req.path, lane, identity: ident,
|
|
249
286
|
status: d.status, why: d.why, evidence,
|
|
250
287
|
billedUsd: d.billed || 0 };
|
|
288
|
+
if (d.paymentRef) entry.paymentRef = d.paymentRef;
|
|
251
289
|
try { this.onEvent(entry); } catch { /* metering must never break serving */ }
|
|
252
290
|
return { lane, identity: ident, ...d, evidence };
|
|
253
291
|
}
|
|
254
292
|
|
|
255
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Who to rate-limit. A cryptographic identity is authoritative; a network
|
|
295
|
+
* address is at least expensive to rotate. A header is neither.
|
|
296
|
+
*/
|
|
297
|
+
_client(req) {
|
|
298
|
+
if (this.trustProxy) {
|
|
299
|
+
const xff = headerValue(req.headers, 'x-forwarded-for');
|
|
300
|
+
if (xff) return xff.split(',')[0].trim();
|
|
301
|
+
}
|
|
302
|
+
return req.ip || 'unknown';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Fixed-window counting. On rollover the whole table is dropped — last
|
|
307
|
+
* window's counts are meaningless anyway, and it keeps memory bounded to
|
|
308
|
+
* one window's worth of identities instead of growing forever.
|
|
309
|
+
*/
|
|
310
|
+
_bump(ident, now) {
|
|
311
|
+
const w = Math.floor(now / this.rateWindow);
|
|
312
|
+
if (w !== this._window) {
|
|
313
|
+
this._window = w;
|
|
314
|
+
this._hits.clear();
|
|
315
|
+
for (const [n, exp] of this._seen) if (exp <= now) this._seen.delete(n);
|
|
316
|
+
}
|
|
317
|
+
// Hard ceiling so a flood of distinct identities inside a single window
|
|
318
|
+
// cannot exhaust memory. Evicting the oldest keeps the newest arrivals
|
|
319
|
+
// counted; the durable fix is shared state, not a bigger Map.
|
|
320
|
+
if (this._hits.size >= this.maxTracked && !this._hits.has(ident))
|
|
321
|
+
this._hits.delete(this._hits.keys().next().value);
|
|
322
|
+
const n = (this._hits.get(ident) || 0) + 1;
|
|
323
|
+
this._hits.set(ident, n);
|
|
324
|
+
return n;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
_decide(req, lane, ident, now, paymentProof, c) {
|
|
256
328
|
for (const [prefix, allow] of this.rules[lane] || []) {
|
|
257
329
|
if (req.path.startsWith(prefix)) {
|
|
258
330
|
if (!allow) return { status: 403, why: `${lane} denied on ${prefix}` };
|
|
259
331
|
break;
|
|
260
332
|
}
|
|
261
333
|
}
|
|
334
|
+
// A valid signature replayed is still a replay. Checked before rate
|
|
335
|
+
// limiting so a captured request cannot be farmed into N billed crossings.
|
|
336
|
+
// Only signers that send a nonce can be held to single use; see
|
|
337
|
+
// verifySignature for why a signature hash is not a safe substitute.
|
|
338
|
+
if (this.replayProtection && c && c.nonce) {
|
|
339
|
+
if (this._seen.has(c.nonce))
|
|
340
|
+
return { status: 403, why: 'signature already used (replay)' };
|
|
341
|
+
if (this._seen.size >= this.maxTracked)
|
|
342
|
+
this._seen.delete(this._seen.keys().next().value);
|
|
343
|
+
this._seen.set(c.nonce, c.sigExpires || now + this.rateWindow);
|
|
344
|
+
}
|
|
262
345
|
const limit = this.rateLimits[lane];
|
|
263
346
|
if (limit) {
|
|
264
|
-
const
|
|
265
|
-
const n = (this._hits.get(k) || 0) + 1;
|
|
266
|
-
this._hits.set(k, n);
|
|
347
|
+
const n = this._bump(ident, now);
|
|
267
348
|
if (n > limit) return { status: 429, why: `${n} req in window, limit ${limit}` };
|
|
268
349
|
}
|
|
269
350
|
if (lane !== LANES.HUMAN) {
|
|
270
351
|
for (const [prefix, price] of Object.entries(this.pricedPaths)) {
|
|
271
352
|
if (req.path.startsWith(prefix)) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
353
|
+
const challenge = { scheme: 'x402', price_usd: price, resource: prefix };
|
|
354
|
+
if (!paymentProof)
|
|
355
|
+
return { status: 402, why: 'payment required for agent access', challenge };
|
|
356
|
+
const v = this._checkPayment(paymentProof, {
|
|
357
|
+
price, resource: prefix, identity: ident, path: req.path, now });
|
|
358
|
+
if (v.ok)
|
|
359
|
+
return { status: 200, billed: price, paymentRef: v.ref,
|
|
360
|
+
why: `settled $${price}` };
|
|
361
|
+
return { status: 402, why: `payment proof rejected: ${v.reason}`, challenge };
|
|
276
362
|
}
|
|
277
363
|
}
|
|
278
364
|
}
|
|
@@ -281,6 +367,22 @@ export class Wayleave {
|
|
|
281
367
|
return { status: 200, why: `${lane} allowed` };
|
|
282
368
|
}
|
|
283
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Settlement is somebody else's job — a facilitator that actually moved the
|
|
372
|
+
* money. This asks yours and believes nothing else. A verifier that throws
|
|
373
|
+
* is a verifier that did not say yes, so the answer is no.
|
|
374
|
+
*/
|
|
375
|
+
_checkPayment(proof, ctx) {
|
|
376
|
+
if (!this.verifyPayment)
|
|
377
|
+
return { ok: false, reason: 'no verifyPayment configured' };
|
|
378
|
+
let r;
|
|
379
|
+
try { r = this.verifyPayment(proof, ctx); }
|
|
380
|
+
catch (e) { return { ok: false, reason: `verifier threw: ${e.message}` }; }
|
|
381
|
+
if (r === true) return { ok: true, ref: null };
|
|
382
|
+
if (r && r.ok === true) return { ok: true, ref: r.ref || null };
|
|
383
|
+
return { ok: false, reason: (r && r.reason) || 'not settled' };
|
|
384
|
+
}
|
|
385
|
+
|
|
284
386
|
/** Express/Connect adapter: app.use(wayleave.express()) */
|
|
285
387
|
express() {
|
|
286
388
|
return (req, res, next) => {
|
|
@@ -288,6 +390,9 @@ export class Wayleave {
|
|
|
288
390
|
method: req.method, path: req.path || req.url,
|
|
289
391
|
authority: req.headers.host || '',
|
|
290
392
|
headers: req.headers,
|
|
393
|
+
// The socket address, not a header. Express only populates req.ip
|
|
394
|
+
// from x-forwarded-for when ITS trust proxy setting says to.
|
|
395
|
+
ip: req.socket?.remoteAddress || req.connection?.remoteAddress,
|
|
291
396
|
}, undefined, req.headers['x-payment-proof'] || '');
|
|
292
397
|
req.wayleave = r;
|
|
293
398
|
if (r.status === 200) return next();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wayleave",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Charge AI agents for passage across your app. Verify agent signatures (Web Bot Auth / Ed25519), classify every request, and return 402 with a price on routes you choose. Humans browse free.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|