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 +43 -0
- package/index.js +31 -3
- package/manifest.js +61 -2
- package/package.json +1 -1
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
|
-
|
|
811
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|