wayleave 0.5.5 → 0.5.7

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/index.d.ts CHANGED
@@ -264,7 +264,16 @@ export interface WayleaveOptions {
264
264
  /** Window length in seconds. Default 60. */
265
265
  rateWindow?: number;
266
266
  /** Path prefix -> price in USD. Non-human lanes get 402 here. */
267
- pricedPaths?: Record<string, number>;
267
+ /** A price, or a price with the method and description to publish.
268
+ * A bare number keeps the old behaviour: GET, generic description. */
269
+ pricedPaths?: Record<string, number | {
270
+ price: number | string;
271
+ /** The HTTP method this route actually serves. Default GET.
272
+ * Publishing GET for a POST API advertises a call that cannot work. */
273
+ method?: string;
274
+ description?: string;
275
+ mimeType?: string;
276
+ }>;
268
277
  /**
269
278
  * Confirms a payment actually settled. **Without this, every priced route
270
279
  * stays 402 forever** — deny is the only safe default, since the proof is a
package/index.js CHANGED
@@ -25,7 +25,7 @@ import { MeterSink } from './meter.js';
25
25
  import { RemotePolicy } from './policy.js';
26
26
  import { evaluatePolicy, evaluatePolicySync } from './policy-engine.js';
27
27
  import { Registry } from './registry.js';
28
- import { ManifestServer, MANIFEST_PATHS, originFor, paymentRequirementsFor } from './manifest.js';
28
+ import { ManifestServer, MANIFEST_PATHS, originFor, paymentRequirementsFor, routeSpec } from './manifest.js';
29
29
 
30
30
  export const LANES = Object.freeze({
31
31
  VERIFIED: 'verified_agent',
@@ -81,6 +81,30 @@ function headerValue(headers, name) {
81
81
  return Array.isArray(v) ? v.join(', ') : (v == null ? '' : String(v));
82
82
  }
83
83
 
84
+ /**
85
+ * The payment header, read under both names.
86
+ *
87
+ * x402 calls this `X-PAYMENT`. This gate shipped reading only
88
+ * `x-payment-proof`, a name of its own invention, and nothing read the
89
+ * standard one. So an agent that did everything right — fetched
90
+ * /.well-known/x402, read the `accepts` block this gate itself publishes,
91
+ * and paid exactly as the protocol specifies — had its payment ignored and
92
+ * received a second 402. The gate advertised x402 and could not be paid by
93
+ * x402, which is the one thing it exists to do.
94
+ *
95
+ * `X-PAYMENT` wins because it is what the protocol mandates and what a
96
+ * stranger's agent sends without being told. `x-payment-proof` is still read
97
+ * so anything already deployed against it keeps working; this is strictly a
98
+ * superset.
99
+ *
100
+ * The value is passed through untouched. This gate does not parse payments —
101
+ * `verifyPayment` does, and it is yours — so this is a transport fix, not a
102
+ * protocol implementation.
103
+ */
104
+ function paymentHeader(headers) {
105
+ return headerValue(headers, 'x-payment') || headerValue(headers, 'x-payment-proof');
106
+ }
107
+
84
108
  /**
85
109
  * Build the signature base from the components the signer actually declared,
86
110
  * not from a fixed list. An agent covering only ("@authority") is valid.
@@ -482,6 +506,7 @@ export class Wayleave {
482
506
  * rules: { [lane]: [ [pathPrefix, allow], ... ] },
483
507
  * rateLimits: { [lane]: perWindow }, rateWindow: 60,
484
508
  * pricedPaths: { pathPrefix: priceUsd },
509
+ * pricedPaths: { pathPrefix: { price, method, description } },
485
510
  * verifyPayment: (proof, ctx) => boolean | { ok, ref },
486
511
  * strictPricedPaths: false,
487
512
  * confirmHuman: (req) => boolean,
@@ -628,7 +653,7 @@ export class Wayleave {
628
653
  { verifyAgentIP: this.verifyAgentIP, ip: this._client(r) });
629
654
  const { lane, agentId, evidence } = c;
630
655
  const ident = agentId || `${lane}:${this._client(r)}`;
631
- const proof = paymentProof || headerValue(req.headers, 'x-payment-proof');
656
+ const proof = paymentProof || paymentHeader(req.headers);
632
657
  /* The same policy the async path enforces, evaluated here for every rule
633
658
  that needs no I/O. Before this, `handle()` skipped policy entirely and
634
659
  one document behaved as two: enforced through express(), ignored here. */
@@ -687,7 +712,7 @@ export class Wayleave {
687
712
  { verifyAgentIP: this.verifyAgentIP, ip: this._client(r) });
688
713
  const { lane, agentId, evidence } = c;
689
714
  const ident = agentId || `${lane}:${this._client(r)}`;
690
- const proof = paymentProof || headerValue(req.headers, 'x-payment-proof');
715
+ const proof = paymentProof || paymentHeader(req.headers);
691
716
  const fromPolicy = await this._policyDecision(r, lane, ident, c);
692
717
  const d = fromPolicy || await this._decideAsync(r, lane, ident, now, proof, c);
693
718
  const entry = { v: 1, idempotencyKey: `${this._instance}:${this._seq++}`,
@@ -776,8 +801,13 @@ export class Wayleave {
776
801
  ? this._confirmedHuman(req)
777
802
  : lane === LANES.HUMAN;
778
803
  if (!freeOnPriced) {
779
- for (const [prefix, price] of Object.entries(this.pricedPaths)) {
804
+ for (const [prefix, configured] of Object.entries(this.pricedPaths)) {
780
805
  if (pathUnder(req.path, prefix)) {
806
+ /* Through routeSpec, so a route configured with a method and a
807
+ description still charges its price and not the object holding
808
+ it. The manifest reads the same function, so the document and
809
+ this challenge cannot disagree about what a route costs. */
810
+ const { price } = routeSpec(configured);
781
811
  const challenge = { scheme: 'x402', price_usd: price, resource: prefix };
782
812
  if (!paymentProof) {
783
813
  const why = this.strictPricedPaths && lane === LANES.HUMAN
@@ -1010,7 +1040,7 @@ export class Wayleave {
1010
1040
  // none of which survive the projection above. Classification still
1011
1041
  // reads only the fields it declares; this is for your callback.
1012
1042
  raw: req,
1013
- }, undefined, req.headers['x-payment-proof'] || '');
1043
+ }, undefined, paymentHeader(req.headers));
1014
1044
  req.wayleave = r;
1015
1045
  if (r.status === 200) return next();
1016
1046
  if (r.status === 402) {
package/manifest.js CHANGED
@@ -41,6 +41,35 @@ const KNOWN_ASSETS = {
41
41
 
42
42
  const DEFAULT_TIMEOUT_SECONDS = 60;
43
43
 
44
+ /**
45
+ * One priced route, however it was configured.
46
+ *
47
+ * `pricedPaths` was `{ path: priceUsd }`, and the manifest hardcoded
48
+ * `method: 'GET'` and the description "x402-priced route" for every entry.
49
+ * For a POST API — which is most paid APIs — that published a call that
50
+ * cannot work, and by this module's own standard an entry an index cannot
51
+ * act on is "indistinguishable from not publishing at all".
52
+ *
53
+ * A value may now be the price as before, or an object carrying the method
54
+ * and a real description. Both consumers of a priced path go through here —
55
+ * the manifest and the gate's own 402 — so the document and the challenge
56
+ * cannot describe the route differently.
57
+ *
58
+ * pricedPaths: { '/api/x': 0.05 }
59
+ * pricedPaths: { '/api/x': { price: 0.05, method: 'POST',
60
+ * description: 'Score one transaction' } }
61
+ */
62
+ export function routeSpec(value) {
63
+ const rich = value !== null && typeof value === 'object';
64
+ const price = rich ? value.price : value;
65
+ return {
66
+ price,
67
+ method: String((rich && value.method) || 'GET').toUpperCase(),
68
+ description: (rich && value.description) || 'x402-priced route',
69
+ mimeType: (rich && value.mimeType) || 'application/json',
70
+ };
71
+ }
72
+
44
73
  /**
45
74
  * Build the manifest from configuration. A pure function of `pricedPaths` and
46
75
  * `payment`: there is no separate document to author, and therefore none to
@@ -68,19 +97,20 @@ export function buildManifest({ pricedPaths = {}, payment = {}, origin,
68
97
  // a wrong address is worse than a missing one.
69
98
  const priceable = Boolean(asset && payTo && Number.isInteger(decimals));
70
99
 
71
- const resources = routes.map(([path, priceUsd]) => {
100
+ const resources = routes.map(([path, value]) => {
101
+ const spec = routeSpec(value);
72
102
  const url = origin.replace(/\/+$/, '') + path;
73
- const entry = { url, method: 'GET', description: 'x402-priced route' };
103
+ const entry = { url, method: spec.method, description: spec.description };
74
104
  if (priceable) {
75
105
  entry.accepts = [{
76
106
  scheme: 'exact',
77
107
  network,
78
- maxAmountRequired: toAtomicUnits(String(priceUsd), decimals),
108
+ maxAmountRequired: toAtomicUnits(String(spec.price), decimals),
79
109
  asset,
80
110
  payTo,
81
111
  resource: url,
82
- description: 'x402-priced route',
83
- mimeType: 'application/json',
112
+ description: spec.description,
113
+ mimeType: spec.mimeType,
84
114
  maxTimeoutSeconds: payment.maxTimeoutSeconds || DEFAULT_TIMEOUT_SECONDS,
85
115
  }];
86
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayleave",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Know which AI agents cross your API \u2014 verified by signature, not guessed from an IP. Sorts every request into four lanes, prices the non-human ones with HTTP 402, and never charges a human. Zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/x402.js CHANGED
@@ -87,6 +87,19 @@ const KNOWN_ASSETS = {
87
87
  },
88
88
  };
89
89
 
90
+ /** JSON, or base64 of JSON. Returns null when it is neither. */
91
+ function parsePayload(raw) {
92
+ const text = String(raw).trim();
93
+ if (!text) return null;
94
+ try { const v = JSON.parse(text); if (v && typeof v === 'object') return v; } catch {}
95
+ try {
96
+ const decoded = Buffer.from(text, 'base64').toString('utf8');
97
+ const v = JSON.parse(decoded);
98
+ if (v && typeof v === 'object') return v;
99
+ } catch {}
100
+ return null;
101
+ }
102
+
90
103
  /**
91
104
  * Build a `verifyPayment(proof, ctx)` for the Coinbase x402 facilitator.
92
105
  *
@@ -138,8 +151,21 @@ export function coinbaseFacilitator({
138
151
  // payload as JSON. Accept either so a customer does not have to care.
139
152
  let paymentPayload = proof;
140
153
  if (typeof proof === 'string') {
141
- try { paymentPayload = JSON.parse(proof); }
142
- catch { return deny('payment proof is not a valid x402 payload'); }
154
+ /* Raw JSON or base64 JSON.
155
+ *
156
+ * x402 defines the X-PAYMENT header as the base64 encoding of the
157
+ * payload, so that is what a spec-compliant agent sends. This accepted
158
+ * only raw JSON, which meant the agent cleared the header fix and then
159
+ * failed here with "payment proof is not a valid x402 payload" — an
160
+ * error that reads like the agent sent something malformed when what it
161
+ * sent was exactly what the protocol prescribes.
162
+ *
163
+ * Both are taken, which is the stated intent of this block anyway.
164
+ * JSON first: a payload is unambiguous JSON and never also valid
165
+ * base64 of different JSON, so trying it first cannot mis-read one as
166
+ * the other. */
167
+ paymentPayload = parsePayload(proof);
168
+ if (!paymentPayload) return deny('payment proof is not a valid x402 payload');
143
169
  }
144
170
  if (!paymentPayload || typeof paymentPayload !== 'object')
145
171
  return deny('no payment payload supplied');