wayleave 0.5.7 → 0.6.0

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 CHANGED
@@ -240,6 +240,49 @@ confirmation is yours to supply and denies by default. If you shipped 0.1.4 or e
240
240
  route, upgrade.
241
241
 
242
242
  **0.1.6** addressed the other half: payment could be faked, and so could being human.
243
+ ### Charge by lane
244
+
245
+ A route can cost different amounts depending on who is asking.
246
+
247
+ ```js
248
+ pricedPaths: {
249
+ '/api/v1/properties/search': 0.001, // flat: every lane pays this
250
+
251
+ '/api/v1/properties/valuation-history': { // or per lane
252
+ verified_agent: 0.01, // proved who it is
253
+ declared_agent: 0.05, // said who it is
254
+ suspected_bot: 0.15, // neither
255
+ method: 'GET',
256
+ description: 'Historical comps, tax and owner records',
257
+ },
258
+ }
259
+ ```
260
+
261
+ The reason to bother is not price discrimination. Signing a request under Web
262
+ Bot Auth currently earns an operator nothing — it is pure cost, which is the
263
+ honest explanation for how few agents do it. A lower price for a proved
264
+ identity turns adoption into a bill that goes down, and it is the only lever
265
+ on that an origin actually controls.
266
+
267
+ Three rules, each of which exists because the alternative is worse:
268
+
269
+ - **The manifest publishes the dearest lane.** A discovery document is read
270
+ before anyone knows who is asking, so the only safe direction for the real
271
+ quote to move is downward. Advertise $0.01 and bill a suspected bot $0.15
272
+ and you have surprised them upward; advertise $0.15 and every other lane is
273
+ a discount.
274
+ - **An unlisted lane pays the list price, never free.** Set `default` to
275
+ choose otherwise. A lane nobody configured is a case nobody considered, and
276
+ giving the route away is the one outcome they certainly did not intend.
277
+ - **The `human` lane cannot be priced.** It is a fall-through, so that is
278
+ where an ordinary visitor lands. A `human:` key warns at startup and is
279
+ unreachable in the pricing path — including under `strictPricedPaths`,
280
+ where an unconfirmed human *does* reach it. To charge everything that is
281
+ not a proven person, use `strictPricedPaths: true`, not a human price.
282
+
283
+ The 402 names the lane it priced, so a caller asking why it was charged more
284
+ than someone else has an answer.
285
+
243
286
  `strictPricedPaths` lets a priced route demand positive evidence rather than accepting the absence of
244
287
  a bot signal as one.
245
288
 
package/index.js CHANGED
@@ -507,6 +507,8 @@ export class Wayleave {
507
507
  * rateLimits: { [lane]: perWindow }, rateWindow: 60,
508
508
  * pricedPaths: { pathPrefix: priceUsd },
509
509
  * pricedPaths: { pathPrefix: { price, method, description } },
510
+ * pricedPaths: { pathPrefix: { verified_agent, declared_agent,
511
+ * suspected_bot, default } },
510
512
  * verifyPayment: (proof, ctx) => boolean | { ok, ref },
511
513
  * strictPricedPaths: false,
512
514
  * confirmHuman: (req) => boolean,
@@ -573,6 +575,27 @@ export class Wayleave {
573
575
  : opts.strictPricedPaths === true;
574
576
  const priced = Object.keys(this.pricedPaths).length > 0;
575
577
  const warn = this._onWarn = opts.onWarn || console.warn;
578
+ /* A price on the human lane would charge people.
579
+ *
580
+ * The lane is a fall-through: a request reaches it by tripping none of
581
+ * the automation tells, which is how an ordinary visitor arrives. Every
582
+ * other guarantee here is hedged somewhere in the README; "humans are
583
+ * never charged" is not, and a per-lane table is the first place someone
584
+ * could break it by typing one extra key.
585
+ *
586
+ * Ignored rather than thrown -- an app that boots is easier to fix than
587
+ * one that does not -- but said once, loudly, naming the route. */
588
+ for (const [prefix, cfg] of Object.entries(this.pricedPaths)) {
589
+ if (cfg === null || typeof cfg !== 'object') continue;
590
+ for (const k of ['human', 'unclassified', 'unknown']) {
591
+ if (typeof cfg[k] === 'number')
592
+ warn(`[wayleave] pricedPaths["${prefix}"] sets a price for the ` +
593
+ `"${k}" lane. That lane is where people land, and it is not ` +
594
+ `charged. The price is ignored. To charge anything that is ` +
595
+ `not a proven person, set strictPricedPaths: true instead.`);
596
+ }
597
+ }
598
+
576
599
  if (opts.strictPricedPaths === false && priced)
577
600
  warn('[wayleave] strictPricedPaths is off: priced routes are reachable free ' +
578
601
  'by any client that looks like a browser. Pass confirmHuman() to charge ' +
@@ -806,9 +829,14 @@ export class Wayleave {
806
829
  /* Through routeSpec, so a route configured with a method and a
807
830
  description still charges its price and not the object holding
808
831
  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);
811
- const challenge = { scheme: 'x402', price_usd: price, resource: prefix };
832
+ this challenge cannot disagree about what a route costs.
833
+
834
+ The lane is passed in because a route may be priced per lane.
835
+ Here we know who is asking, so the quote is exact; the manifest
836
+ has no lane and publishes the dearest, which is why the two can
837
+ differ without either being wrong. */
838
+ const { price } = routeSpec(configured, lane);
839
+ const challenge = { scheme: 'x402', price_usd: price, resource: prefix, lane };
812
840
  if (!paymentProof) {
813
841
  const why = this.strictPricedPaths && lane === LANES.HUMAN
814
842
  ? 'payment required: no signature and no confirmed human'
package/manifest.js CHANGED
@@ -59,11 +59,70 @@ const DEFAULT_TIMEOUT_SECONDS = 60;
59
59
  * pricedPaths: { '/api/x': { price: 0.05, method: 'POST',
60
60
  * description: 'Score one transaction' } }
61
61
  */
62
- export function routeSpec(value) {
62
+ /**
63
+ * The agent lanes a route may be priced for, dearest first.
64
+ *
65
+ * `human` is deliberately absent and cannot be priced. It is a fall-through
66
+ * lane -- reached by tripping none of the automation tells -- so a real
67
+ * person lands there, and charging it would charge people. That is the one
68
+ * promise this library makes without qualification.
69
+ */
70
+ export const PRICED_LANES = Object.freeze(
71
+ ['suspected_bot', 'declared_agent', 'verified_agent']);
72
+
73
+ /** Is this object a per-lane price table rather than a rich single price? */
74
+ function isLaneTable(v) {
75
+ return v !== null && typeof v === 'object' &&
76
+ PRICED_LANES.some((l) => typeof v[l] === 'number');
77
+ }
78
+
79
+ /**
80
+ * One priced route, normalised.
81
+ *
82
+ * Called with a lane it answers what that lane pays. Called without one -- as
83
+ * the manifest does, because a document has no lane -- it answers the LIST
84
+ * price, which is the dearest of the configured lanes.
85
+ *
86
+ * List price is the maximum on purpose. The manifest is an advertisement read
87
+ * before anyone knows who is asking, so the only safe direction for the real
88
+ * quote to differ is downward. Publish the cheapest and a suspected bot that
89
+ * discovered the route at $0.01 gets billed $0.15, which is a surprise in the
90
+ * worst direction. Publish the dearest and every other lane is a discount.
91
+ *
92
+ * A lane with no entry falls back to `default`, and failing that to the list
93
+ * price. Not to free: an unlisted lane is an operator who did not think about
94
+ * that case, and money fails closed.
95
+ */
96
+ export function routeSpec(value, lane) {
63
97
  const rich = value !== null && typeof value === 'object';
64
- const price = rich ? value.price : value;
98
+ const lanes = isLaneTable(value) ? value : null;
99
+
100
+ let price;
101
+ if (!lanes) {
102
+ price = rich ? value.price : value;
103
+ } else if (lane && PRICED_LANES.includes(lane) && typeof lanes[lane] === 'number') {
104
+ /* PRICED_LANES.includes() is load-bearing, not a tidy-up.
105
+ *
106
+ * Without it a `human: 0.99` key is reachable: under strictPricedPaths an
107
+ * unconfirmed human DOES reach the pricing loop -- that is the whole
108
+ * point of strict mode -- and would be quoted its own entry. The
109
+ * constructor warns about such a key, but a warning is advice and this is
110
+ * the code path that takes money. Reading the lane through the allowed
111
+ * list means the price cannot be quoted even if the warning is ignored. */
112
+ price = lanes[lane];
113
+ } else if (typeof lanes.default === 'number') {
114
+ price = lanes.default;
115
+ } else {
116
+ price = Math.max(...PRICED_LANES
117
+ .map((l) => lanes[l]).filter((n) => typeof n === 'number'));
118
+ }
119
+
65
120
  return {
66
121
  price,
122
+ lanes: lanes
123
+ ? Object.fromEntries(PRICED_LANES
124
+ .filter((l) => typeof lanes[l] === 'number').map((l) => [l, lanes[l]]))
125
+ : null,
67
126
  method: String((rich && value.method) || 'GET').toUpperCase(),
68
127
  description: (rich && value.description) || 'x402-priced route',
69
128
  mimeType: (rich && value.mimeType) || 'application/json',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayleave",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
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",