@naulon/wayfarer 0.1.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/README.md +39 -0
- package/dist/agent.d.ts +77 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +235 -0
- package/dist/agent.js.map +1 -0
- package/dist/allocation.d.ts +36 -0
- package/dist/allocation.d.ts.map +1 -0
- package/dist/allocation.js +45 -0
- package/dist/allocation.js.map +1 -0
- package/dist/appraise.d.ts +3 -0
- package/dist/appraise.d.ts.map +1 -0
- package/dist/appraise.js +60 -0
- package/dist/appraise.js.map +1 -0
- package/dist/buyer.d.ts +195 -0
- package/dist/buyer.d.ts.map +1 -0
- package/dist/buyer.js +254 -0
- package/dist/buyer.js.map +1 -0
- package/dist/decide.d.ts +115 -0
- package/dist/decide.d.ts.map +1 -0
- package/dist/decide.js +206 -0
- package/dist/decide.js.map +1 -0
- package/dist/discover.d.ts +10 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +5 -0
- package/dist/discover.js.map +1 -0
- package/dist/discovery.d.ts +26 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +93 -0
- package/dist/discovery.js.map +1 -0
- package/dist/gateway.d.ts +117 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +187 -0
- package/dist/gateway.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +40 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +44 -0
- package/dist/lib.js.map +1 -0
- package/dist/licenseStore.d.ts +56 -0
- package/dist/licenseStore.d.ts.map +1 -0
- package/dist/licenseStore.js +79 -0
- package/dist/licenseStore.js.map +1 -0
- package/dist/memo.d.ts +43 -0
- package/dist/memo.d.ts.map +1 -0
- package/dist/memo.js +102 -0
- package/dist/memo.js.map +1 -0
- package/dist/origin-policy.d.ts +74 -0
- package/dist/origin-policy.d.ts.map +1 -0
- package/dist/origin-policy.js +88 -0
- package/dist/origin-policy.js.map +1 -0
- package/dist/paidFetch.d.ts +14 -0
- package/dist/paidFetch.d.ts.map +1 -0
- package/dist/paidFetch.js +93 -0
- package/dist/paidFetch.js.map +1 -0
- package/dist/pay.d.ts +3 -0
- package/dist/pay.d.ts.map +1 -0
- package/dist/pay.js +82 -0
- package/dist/pay.js.map +1 -0
- package/dist/pop.d.ts +8 -0
- package/dist/pop.d.ts.map +1 -0
- package/dist/pop.js +25 -0
- package/dist/pop.js.map +1 -0
- package/dist/rail.d.ts +18 -0
- package/dist/rail.d.ts.map +1 -0
- package/dist/rail.js +73 -0
- package/dist/rail.js.map +1 -0
- package/dist/rss.d.ts +38 -0
- package/dist/rss.d.ts.map +1 -0
- package/dist/rss.js +110 -0
- package/dist/rss.js.map +1 -0
- package/dist/sign.d.ts +13 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +52 -0
- package/dist/sign.js.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wallet.d.ts +12 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +46 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +42 -0
package/dist/buyer.d.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
declare const AGENT_UA = "naulon-wayfarer/0.1";
|
|
2
|
+
export interface Quoted {
|
|
3
|
+
priceUsdc: number;
|
|
4
|
+
amountAtomic: string;
|
|
5
|
+
/** Nonce the gate issued on the 402; echo it back in the payment (replay guard). */
|
|
6
|
+
nonce?: string;
|
|
7
|
+
requirements: {
|
|
8
|
+
network: string;
|
|
9
|
+
asset: string;
|
|
10
|
+
payTo: string;
|
|
11
|
+
amount: string;
|
|
12
|
+
maxTimeoutSeconds: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* When the publisher declares extra settlement legs (e.g. a control-plane operator
|
|
16
|
+
* fee), the gate advertises the FULL per-leg list (author first) via the
|
|
17
|
+
* `extensions.naulonLegs` block, and rejects any payment that doesn't sign every leg.
|
|
18
|
+
* Present only for such an N-leg quote; absent for the stock single-author toll —
|
|
19
|
+
* then the buyer pays the bare single payload, byte-identical to before.
|
|
20
|
+
*/
|
|
21
|
+
legs?: {
|
|
22
|
+
role: string;
|
|
23
|
+
payTo: string;
|
|
24
|
+
amount: string;
|
|
25
|
+
nonce?: string;
|
|
26
|
+
}[];
|
|
27
|
+
/** The 402's `resource` object (`{url, description, mimeType}`), verbatim. The gateway
|
|
28
|
+
* rail's payment envelope MUST echo it back — the facilitator's `verify` rejects a
|
|
29
|
+
* payload missing `resource` (400 `resource: Required`). Absent on the memo rail, which
|
|
30
|
+
* relays the raw authorization and never sends the x402 envelope. */
|
|
31
|
+
resource?: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** A per-leg requirement the buyer signs — the author requirement with this leg's
|
|
34
|
+
* payTo + amount substituted (same network/asset/timeout). */
|
|
35
|
+
export type LegRequirements = Quoted["requirements"];
|
|
36
|
+
/**
|
|
37
|
+
* Why a paid fetch failed, classified so the host can decide what to do (BUY-1.4).
|
|
38
|
+
* The point is the `retryable` split: `insufficient_funds` is a HARD stop (fund the
|
|
39
|
+
* wallet, don't re-call), while `toll_moved` / `expired` / `rejected` are transient
|
|
40
|
+
* (re-quote and try again may succeed). `not_gated` means there was nothing to pay.
|
|
41
|
+
*/
|
|
42
|
+
export type FetchErrorCode = "not_gated" | "not_found" | "toll_moved" | "insufficient_funds" | "expired" | "rejected" | "origin_error" | "needs_topup" | "grant_expired" | "settlement_ambiguous";
|
|
43
|
+
export interface Fetched {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
content?: string;
|
|
46
|
+
settlementRef?: string;
|
|
47
|
+
/** The author-leg amount paid, in USDC (back-compat — only the primary leg). */
|
|
48
|
+
paidUsdc?: number;
|
|
49
|
+
/** The buyer's TRUE outflow authorized for this read, in USDC — the sum across every
|
|
50
|
+
* settlement leg (== quotedTotalAtomic of the quote actually signed). Callers debit
|
|
51
|
+
* budgets on THIS, not paidUsdc, so a fee'd toll is never under-counted. */
|
|
52
|
+
costUsdc?: number;
|
|
53
|
+
/** Citation License (compact JWS) the gate handed back on a paid read. */
|
|
54
|
+
license?: string;
|
|
55
|
+
error?: string;
|
|
56
|
+
/** Typed failure classification (BUY-1.4); absent on success. */
|
|
57
|
+
errorCode?: FetchErrorCode;
|
|
58
|
+
/** True when re-quoting/retrying may succeed (toll moved, validity expired, a
|
|
59
|
+
* generic rejection); false for a hard stop (insufficient funds — fund first).
|
|
60
|
+
* Absent on success. */
|
|
61
|
+
retryable?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A pay-time spend ceiling the buyer must not exceed (BUY-1.4). The buyer re-quotes
|
|
65
|
+
* at pay time (its own pre-pay probe IS the re-quote) and ABORTS — paying nothing —
|
|
66
|
+
* if the live toll total tops this. The caller (the MCP) sets it to the quote it
|
|
67
|
+
* already gated the budget on, plus a configured tolerance, so a toll that moved up
|
|
68
|
+
* between the budget check and the pay can never silently overspend.
|
|
69
|
+
*/
|
|
70
|
+
export interface PayGuard {
|
|
71
|
+
/** Max atomic (micro-USDC) total across all legs the buyer may pay. */
|
|
72
|
+
maxTotalAtomic: string;
|
|
73
|
+
}
|
|
74
|
+
export interface Buyer {
|
|
75
|
+
readonly address: string;
|
|
76
|
+
/** One-time setup (gateway mode deposits USDC into the Gateway Wallet). */
|
|
77
|
+
init(): Promise<void>;
|
|
78
|
+
/** Probe price without paying. null if the article isn't gated. */
|
|
79
|
+
price(url: string, kind: "read" | "citation"): Promise<Quoted | null>;
|
|
80
|
+
/** Pay and fetch the content. `guard` (optional) caps the pay-time total — the
|
|
81
|
+
* buyer aborts beyond it (toll-moved protection), paying nothing. */
|
|
82
|
+
fetch(url: string, kind: "read" | "citation", guard?: PayGuard): Promise<Fetched>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The buyer's true outflow for a quote, in atomic micro-USDC (integer) — the sum of
|
|
86
|
+
* every advertised settlement leg, or the single author amount when there are none.
|
|
87
|
+
* Integer math only (AGENTS.md: money is integer micro-USDC, never floats).
|
|
88
|
+
*/
|
|
89
|
+
export declare function quotedTotalAtomic(quoted: Quoted): bigint;
|
|
90
|
+
/**
|
|
91
|
+
* The toll-moved guard. If `guard` is set and the LIVE quote's true total tops the
|
|
92
|
+
* guard ceiling, returns a typed `toll_moved` failure (the buyer pays NOTHING);
|
|
93
|
+
* otherwise null (clear to pay). This is "re-quote at pay time, abort beyond
|
|
94
|
+
* tolerance": the buyer's own pre-pay probe is the re-quote, compared here against
|
|
95
|
+
* the ceiling the caller authorized.
|
|
96
|
+
*/
|
|
97
|
+
export declare function tollMovedOrNull(quoted: Quoted, guard?: PayGuard): Fetched | null;
|
|
98
|
+
/**
|
|
99
|
+
* Classify a gate's payment-rejection message (BUY-1.4). The gate surfaces the real
|
|
100
|
+
* reason in its 402 body; this maps the known signals onto a typed code + a retry
|
|
101
|
+
* verdict. Insufficient funds is the one hard stop — every other rejection is worth a
|
|
102
|
+
* re-quote. Conservative by design: an unrecognized reason is `rejected` (retryable),
|
|
103
|
+
* never silently treated as a fundable hard stop.
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* Classify a THROWN session-signer refusal (BUY-4 hosted path). The cloud injects a
|
|
107
|
+
* grant-guarded session signer whose `signTypedData` throws an Error whose message is the
|
|
108
|
+
* sign guard's code, optionally suffixed " (remaining <micro>)". Unlike `classifyPaymentError`
|
|
109
|
+
* (which reads a gate 402 body), this refusal never reached the gate — nothing was paid — and
|
|
110
|
+
* must NOT be run through the 402 classifier, where `grant_exceeded`/`no_session` would fall
|
|
111
|
+
* through to a retryable `rejected`, telling the agent to retry a pay that can only fail again.
|
|
112
|
+
*
|
|
113
|
+
* grant_exceeded · leg_too_large · no_session → needs_topup (fund the session)
|
|
114
|
+
* grant_expired → grant_expired (renew — funds intact)
|
|
115
|
+
* bad_from · chain_mismatch · payee_not_allowed → rejected (a config error a top-up can't fix)
|
|
116
|
+
*
|
|
117
|
+
* Returns null when the message is NOT a known signer code (a real socket / unknown throw the
|
|
118
|
+
* caller should surface as `origin_error`). Never retryable: every signer refusal needs a
|
|
119
|
+
* host-side action (fund / renew / fix config), never a blind re-call.
|
|
120
|
+
*/
|
|
121
|
+
export declare function classifySignerRefusal(errorText: string): {
|
|
122
|
+
errorCode: FetchErrorCode;
|
|
123
|
+
retryable: boolean;
|
|
124
|
+
} | null;
|
|
125
|
+
export declare function classifyPaymentError(errorText: string): {
|
|
126
|
+
errorCode: FetchErrorCode;
|
|
127
|
+
retryable: boolean;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* The classified outcome of a price probe. A probe is NOT just "gated or not" — the
|
|
131
|
+
* reason it isn't gated matters, and collapsing everything non-402 into one bucket is a
|
|
132
|
+
* money-correctness bug: a wrong URL (404) or a down origin (5xx) is NOT a free read, and
|
|
133
|
+
* an agent that treats it as one silently skips paying and reads nothing (or an error
|
|
134
|
+
* page). This union keeps the four cases apart so every caller can respond correctly:
|
|
135
|
+
* - `gated` — a real 402 with a decodable toll; pay it.
|
|
136
|
+
* - `free` — a genuine 2xx; there is nothing to pay (the one true "not gated").
|
|
137
|
+
* - `not_found` — a 404; the path is wrong/unknown, NOT free. On a slug-only pay this
|
|
138
|
+
* is usually the `/essays/<slug>` fallback missing a publisher that
|
|
139
|
+
* serves `/articles/<slug>` — the fix is to pass the canonical url.
|
|
140
|
+
* - `unreachable` — any other non-2xx (5xx, 403, a network throw); transient, retryable.
|
|
141
|
+
* - `malformed` — a 402 whose PAYMENT-REQUIRED header is missing/undecodable/empty;
|
|
142
|
+
* a broken gate, never silently a free read.
|
|
143
|
+
*/
|
|
144
|
+
export type ProbeOutcome = {
|
|
145
|
+
status: "gated";
|
|
146
|
+
quoted: Quoted;
|
|
147
|
+
} | {
|
|
148
|
+
status: "free";
|
|
149
|
+
} | {
|
|
150
|
+
status: "not_found";
|
|
151
|
+
httpStatus: number;
|
|
152
|
+
} | {
|
|
153
|
+
status: "unreachable";
|
|
154
|
+
httpStatus: number;
|
|
155
|
+
} | {
|
|
156
|
+
status: "malformed";
|
|
157
|
+
reason: string;
|
|
158
|
+
};
|
|
159
|
+
/** Shared price probe — classify the gate's response by HTTP status, decoding the 402
|
|
160
|
+
* PAYMENT-REQUIRED header only for a real toll. Never throws: a broken 402 body or a
|
|
161
|
+
* network failure is returned as a typed outcome, not an exception. */
|
|
162
|
+
export declare function probe(url: string, kind: "read" | "citation", agentId: string): Promise<ProbeOutcome>;
|
|
163
|
+
/** Back-compat thin wrapper: the decoded quote for a gated 402, else null. Callers that
|
|
164
|
+
* must distinguish free / not_found / unreachable use `probe()` directly. */
|
|
165
|
+
export declare function probePrice(url: string, kind: "read" | "citation", agentId: string): Promise<Quoted | null>;
|
|
166
|
+
/**
|
|
167
|
+
* Map a NON-gated probe outcome to the typed `Fetched` failure every buyer returns, so a
|
|
168
|
+
* 404/5xx/malformed response never masquerades as a paid or free success. `not_gated` is
|
|
169
|
+
* reserved for the one true free (2xx) read; a 404 is `not_found` with a message that
|
|
170
|
+
* points the agent at the canonical url (the usual cause is the `/essays/<slug>` fallback
|
|
171
|
+
* not matching a `/articles/<slug>` publisher).
|
|
172
|
+
*/
|
|
173
|
+
export declare function probeFailure(outcome: Exclude<ProbeOutcome, {
|
|
174
|
+
status: "gated";
|
|
175
|
+
}>, url: string): Fetched;
|
|
176
|
+
/**
|
|
177
|
+
* Assemble the `payment-signature` header. For an N-leg quote (a publisher with extra
|
|
178
|
+
* settlement legs, e.g. an operator fee), sign one payload per advertised leg and emit
|
|
179
|
+
* them as the ARRAY the gate's `verifyAndSettle` parses (leg order, author first). For
|
|
180
|
+
* a stock single-author quote, emit today's BARE single payload — byte-identical, so a
|
|
181
|
+
* non-fee toll is untouched. `signLeg` is the payment mode's per-leg signer (mock /
|
|
182
|
+
* memo / gateway); it receives the leg's substituted requirements + the leg's nonce and
|
|
183
|
+
* returns the raw payload object (this helper does the single-vs-array framing + base64).
|
|
184
|
+
*/
|
|
185
|
+
export declare function assemblePayment(quoted: Quoted, signLeg: (req: LegRequirements, nonce?: string) => unknown | Promise<unknown>): Promise<string>;
|
|
186
|
+
/**
|
|
187
|
+
* Re-read an essay using a held Citation License instead of paying. Mode-agnostic
|
|
188
|
+
* — it's just an authenticated GET; the gate honors the license and serves free.
|
|
189
|
+
*/
|
|
190
|
+
export declare function rereadWithLicense(url: string, kind: "read" | "citation", license: string, agentId: string,
|
|
191
|
+
/** Holder-of-key proof (`<ts>.<nonce>.<sig>`); required for a cnf-bound license. */
|
|
192
|
+
proof?: string): Promise<Fetched>;
|
|
193
|
+
export declare function selectBuyer(): Promise<Buyer>;
|
|
194
|
+
export { AGENT_UA };
|
|
195
|
+
//# sourceMappingURL=buyer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buyer.d.ts","sourceRoot":"","sources":["../src/buyer.ts"],"names":[],"mappings":"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"}
|
package/dist/buyer.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buyer abstraction — how the Wayfarer prices and pays for an article.
|
|
3
|
+
*
|
|
4
|
+
* Pricing is the same in both modes: probe the tollgate, read the x402
|
|
5
|
+
* `PAYMENT-REQUIRED` header (free — no payment yet). Paying differs:
|
|
6
|
+
* - mock: sign a simple offline payment-signature the mock gate accepts.
|
|
7
|
+
* - gateway: Circle's GatewayClient does the full deposit-backed 402 flow.
|
|
8
|
+
*/
|
|
9
|
+
import { activeNetwork, getConfig, supportsMemo } from "@naulon/shared";
|
|
10
|
+
import { agentFetch } from "./sign.js";
|
|
11
|
+
const AGENT_UA = "naulon-wayfarer/0.1";
|
|
12
|
+
/**
|
|
13
|
+
* The buyer's true outflow for a quote, in atomic micro-USDC (integer) — the sum of
|
|
14
|
+
* every advertised settlement leg, or the single author amount when there are none.
|
|
15
|
+
* Integer math only (AGENTS.md: money is integer micro-USDC, never floats).
|
|
16
|
+
*/
|
|
17
|
+
export function quotedTotalAtomic(quoted) {
|
|
18
|
+
// Threshold MUST match assemblePayment (> 1): the signer only pays the `legs` array
|
|
19
|
+
// when there are 2+ legs, otherwise it signs `requirements.amount`. Summing a lone leg
|
|
20
|
+
// here would check a different number than gets signed — a gate could advertise a real
|
|
21
|
+
// price in requirements and one fake-cheap leg to slip past the ceiling. Check == sign.
|
|
22
|
+
if (quoted.legs && quoted.legs.length > 1) {
|
|
23
|
+
return quoted.legs.reduce((sum, leg) => sum + BigInt(leg.amount), 0n);
|
|
24
|
+
}
|
|
25
|
+
return BigInt(quoted.amountAtomic);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The toll-moved guard. If `guard` is set and the LIVE quote's true total tops the
|
|
29
|
+
* guard ceiling, returns a typed `toll_moved` failure (the buyer pays NOTHING);
|
|
30
|
+
* otherwise null (clear to pay). This is "re-quote at pay time, abort beyond
|
|
31
|
+
* tolerance": the buyer's own pre-pay probe is the re-quote, compared here against
|
|
32
|
+
* the ceiling the caller authorized.
|
|
33
|
+
*/
|
|
34
|
+
export function tollMovedOrNull(quoted, guard) {
|
|
35
|
+
if (!guard)
|
|
36
|
+
return null;
|
|
37
|
+
const live = quotedTotalAtomic(quoted);
|
|
38
|
+
const ceiling = BigInt(guard.maxTotalAtomic);
|
|
39
|
+
if (live <= ceiling)
|
|
40
|
+
return null;
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
errorCode: "toll_moved",
|
|
44
|
+
retryable: true,
|
|
45
|
+
error: `Toll moved at pay time: the live total is ${live} atomic but only ${ceiling} was authorized ` +
|
|
46
|
+
`(the quoted total plus tolerance). Re-quote and decide again — nothing was paid.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Classify a gate's payment-rejection message (BUY-1.4). The gate surfaces the real
|
|
51
|
+
* reason in its 402 body; this maps the known signals onto a typed code + a retry
|
|
52
|
+
* verdict. Insufficient funds is the one hard stop — every other rejection is worth a
|
|
53
|
+
* re-quote. Conservative by design: an unrecognized reason is `rejected` (retryable),
|
|
54
|
+
* never silently treated as a fundable hard stop.
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* Classify a THROWN session-signer refusal (BUY-4 hosted path). The cloud injects a
|
|
58
|
+
* grant-guarded session signer whose `signTypedData` throws an Error whose message is the
|
|
59
|
+
* sign guard's code, optionally suffixed " (remaining <micro>)". Unlike `classifyPaymentError`
|
|
60
|
+
* (which reads a gate 402 body), this refusal never reached the gate — nothing was paid — and
|
|
61
|
+
* must NOT be run through the 402 classifier, where `grant_exceeded`/`no_session` would fall
|
|
62
|
+
* through to a retryable `rejected`, telling the agent to retry a pay that can only fail again.
|
|
63
|
+
*
|
|
64
|
+
* grant_exceeded · leg_too_large · no_session → needs_topup (fund the session)
|
|
65
|
+
* grant_expired → grant_expired (renew — funds intact)
|
|
66
|
+
* bad_from · chain_mismatch · payee_not_allowed → rejected (a config error a top-up can't fix)
|
|
67
|
+
*
|
|
68
|
+
* Returns null when the message is NOT a known signer code (a real socket / unknown throw the
|
|
69
|
+
* caller should surface as `origin_error`). Never retryable: every signer refusal needs a
|
|
70
|
+
* host-side action (fund / renew / fix config), never a blind re-call.
|
|
71
|
+
*/
|
|
72
|
+
export function classifySignerRefusal(errorText) {
|
|
73
|
+
const code = errorText.toLowerCase().match(/^([a-z_]+)/)?.[1];
|
|
74
|
+
switch (code) {
|
|
75
|
+
case "grant_exceeded":
|
|
76
|
+
case "leg_too_large":
|
|
77
|
+
case "no_session":
|
|
78
|
+
return { errorCode: "needs_topup", retryable: false };
|
|
79
|
+
case "grant_expired":
|
|
80
|
+
return { errorCode: "grant_expired", retryable: false };
|
|
81
|
+
case "bad_from":
|
|
82
|
+
case "chain_mismatch":
|
|
83
|
+
case "payee_not_allowed":
|
|
84
|
+
return { errorCode: "rejected", retryable: false };
|
|
85
|
+
default:
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function classifyPaymentError(errorText) {
|
|
90
|
+
const t = errorText.toLowerCase();
|
|
91
|
+
if (/insufficient|exceeds balance|transfer amount exceeds|not enough|balance too low/.test(t)) {
|
|
92
|
+
return { errorCode: "insufficient_funds", retryable: false };
|
|
93
|
+
}
|
|
94
|
+
if (/validity_too_short|validity too short|expired|valid ?before|too short|window/.test(t)) {
|
|
95
|
+
return { errorCode: "expired", retryable: true };
|
|
96
|
+
}
|
|
97
|
+
return { errorCode: "rejected", retryable: true };
|
|
98
|
+
}
|
|
99
|
+
/** Shared price probe — classify the gate's response by HTTP status, decoding the 402
|
|
100
|
+
* PAYMENT-REQUIRED header only for a real toll. Never throws: a broken 402 body or a
|
|
101
|
+
* network failure is returned as a typed outcome, not an exception. */
|
|
102
|
+
export async function probe(url, kind, agentId) {
|
|
103
|
+
let res;
|
|
104
|
+
try {
|
|
105
|
+
res = await agentFetch(url, {
|
|
106
|
+
headers: { "user-agent": AGENT_UA, "x-naulon-agent": agentId, "x-naulon-kind": kind },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// A DNS/connection failure is unreachable, not "free" — httpStatus 0 = no response.
|
|
111
|
+
return { status: "unreachable", httpStatus: 0 };
|
|
112
|
+
}
|
|
113
|
+
if (res.status === 402) {
|
|
114
|
+
const header = res.headers.get("payment-required");
|
|
115
|
+
if (!header)
|
|
116
|
+
return { status: "malformed", reason: "missing the PAYMENT-REQUIRED header" };
|
|
117
|
+
let decoded;
|
|
118
|
+
try {
|
|
119
|
+
decoded = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { status: "malformed", reason: "an undecodable PAYMENT-REQUIRED header" };
|
|
123
|
+
}
|
|
124
|
+
const req = decoded.accepts?.[0];
|
|
125
|
+
if (!req)
|
|
126
|
+
return { status: "malformed", reason: "a 402 with no payment options (empty accepts)" };
|
|
127
|
+
// The author leg is what the agent appraises (the content's price), so `priceUsdc`
|
|
128
|
+
// stays the author amount even when an additive fee leg makes the buyer's TOTAL
|
|
129
|
+
// higher. `legs` (when present) is the full set the buyer must sign — see assemblePayment.
|
|
130
|
+
const legs = decoded.extensions?.naulonLegs?.legs;
|
|
131
|
+
return {
|
|
132
|
+
status: "gated",
|
|
133
|
+
quoted: {
|
|
134
|
+
priceUsdc: Number(req.amount) / 1_000_000,
|
|
135
|
+
amountAtomic: req.amount,
|
|
136
|
+
nonce: req.extra?.nonce,
|
|
137
|
+
requirements: req,
|
|
138
|
+
...(decoded.resource !== undefined ? { resource: decoded.resource } : {}),
|
|
139
|
+
// Only a real multi-leg (>1) quote carries `legs`; an honest gate emits naulonLegs
|
|
140
|
+
// solely for 2+ legs (build402). A lone-leg array is anomalous — dropping it keeps
|
|
141
|
+
// the invariant "legs present ⟺ signed as an array" that the ceiling check relies on.
|
|
142
|
+
...(legs && legs.length > 1 ? { legs } : {}),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (res.status === 404)
|
|
147
|
+
return { status: "not_found", httpStatus: 404 };
|
|
148
|
+
if (res.ok)
|
|
149
|
+
return { status: "free" };
|
|
150
|
+
return { status: "unreachable", httpStatus: res.status };
|
|
151
|
+
}
|
|
152
|
+
/** Back-compat thin wrapper: the decoded quote for a gated 402, else null. Callers that
|
|
153
|
+
* must distinguish free / not_found / unreachable use `probe()` directly. */
|
|
154
|
+
export async function probePrice(url, kind, agentId) {
|
|
155
|
+
const outcome = await probe(url, kind, agentId);
|
|
156
|
+
return outcome.status === "gated" ? outcome.quoted : null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Map a NON-gated probe outcome to the typed `Fetched` failure every buyer returns, so a
|
|
160
|
+
* 404/5xx/malformed response never masquerades as a paid or free success. `not_gated` is
|
|
161
|
+
* reserved for the one true free (2xx) read; a 404 is `not_found` with a message that
|
|
162
|
+
* points the agent at the canonical url (the usual cause is the `/essays/<slug>` fallback
|
|
163
|
+
* not matching a `/articles/<slug>` publisher).
|
|
164
|
+
*/
|
|
165
|
+
export function probeFailure(outcome, url) {
|
|
166
|
+
switch (outcome.status) {
|
|
167
|
+
case "free":
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
errorCode: "not_gated",
|
|
171
|
+
retryable: false,
|
|
172
|
+
error: "not gated — the source returned a free (2xx) read; no payment is required.",
|
|
173
|
+
};
|
|
174
|
+
case "not_found":
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
errorCode: "not_found",
|
|
178
|
+
retryable: false,
|
|
179
|
+
error: `probed ${url} — HTTP 404. This is NOT a free read: the path was not found. Pass the canonical ` +
|
|
180
|
+
`url from naulon_discover — the /essays/<slug> fallback does not match every publisher (many serve ` +
|
|
181
|
+
`/articles/<slug> or a custom path).`,
|
|
182
|
+
};
|
|
183
|
+
case "unreachable":
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
errorCode: "origin_error",
|
|
187
|
+
retryable: true,
|
|
188
|
+
error: `probed ${url} — HTTP ${outcome.httpStatus || "no response"}. The origin/gate is unreachable or erroring; retry.`,
|
|
189
|
+
};
|
|
190
|
+
case "malformed":
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
errorCode: "rejected",
|
|
194
|
+
retryable: true,
|
|
195
|
+
error: `the gate returned a 402 but ${outcome.reason}; cannot quote the toll. Retry or check the gate.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Assemble the `payment-signature` header. For an N-leg quote (a publisher with extra
|
|
201
|
+
* settlement legs, e.g. an operator fee), sign one payload per advertised leg and emit
|
|
202
|
+
* them as the ARRAY the gate's `verifyAndSettle` parses (leg order, author first). For
|
|
203
|
+
* a stock single-author quote, emit today's BARE single payload — byte-identical, so a
|
|
204
|
+
* non-fee toll is untouched. `signLeg` is the payment mode's per-leg signer (mock /
|
|
205
|
+
* memo / gateway); it receives the leg's substituted requirements + the leg's nonce and
|
|
206
|
+
* returns the raw payload object (this helper does the single-vs-array framing + base64).
|
|
207
|
+
*/
|
|
208
|
+
export async function assemblePayment(quoted, signLeg) {
|
|
209
|
+
if (quoted.legs && quoted.legs.length > 1) {
|
|
210
|
+
const payloads = await Promise.all(quoted.legs.map((leg) => signLeg({ ...quoted.requirements, payTo: leg.payTo, amount: leg.amount }, leg.nonce)));
|
|
211
|
+
return Buffer.from(JSON.stringify(payloads)).toString("base64");
|
|
212
|
+
}
|
|
213
|
+
const payload = await signLeg(quoted.requirements, quoted.nonce);
|
|
214
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Re-read an essay using a held Citation License instead of paying. Mode-agnostic
|
|
218
|
+
* — it's just an authenticated GET; the gate honors the license and serves free.
|
|
219
|
+
*/
|
|
220
|
+
export async function rereadWithLicense(url, kind, license, agentId,
|
|
221
|
+
/** Holder-of-key proof (`<ts>.<nonce>.<sig>`); required for a cnf-bound license. */
|
|
222
|
+
proof) {
|
|
223
|
+
const headers = {
|
|
224
|
+
"user-agent": AGENT_UA,
|
|
225
|
+
"x-naulon-agent": agentId,
|
|
226
|
+
"x-naulon-kind": kind,
|
|
227
|
+
"x-naulon-license": license,
|
|
228
|
+
};
|
|
229
|
+
if (proof)
|
|
230
|
+
headers["x-naulon-proof"] = proof;
|
|
231
|
+
const res = await agentFetch(url, { headers });
|
|
232
|
+
if (!res.ok)
|
|
233
|
+
return { ok: false, error: `re-read returned ${res.status}` };
|
|
234
|
+
return { ok: true, content: await res.text(), paidUsdc: 0, license };
|
|
235
|
+
}
|
|
236
|
+
export async function selectBuyer() {
|
|
237
|
+
const cfg = getConfig();
|
|
238
|
+
if (cfg.PAYMENT_MODE === "gateway") {
|
|
239
|
+
// On a memo-capable network (Arc) the gate settles via the self-relay rail, which
|
|
240
|
+
// expects a RAW USDC EIP-3009 authorization (USDC domain), not Circle's Gateway
|
|
241
|
+
// payload (GatewayWallet domain) — so the buyer signs differently. Field-presence
|
|
242
|
+
// gate, mirroring the gate's settle routing: a swap to Base falls back to the SDK.
|
|
243
|
+
if (supportsMemo(activeNetwork())) {
|
|
244
|
+
const { memoBuyer } = await import("./memo.js");
|
|
245
|
+
return memoBuyer();
|
|
246
|
+
}
|
|
247
|
+
const { gatewayBuyer } = await import("./gateway.js");
|
|
248
|
+
return gatewayBuyer();
|
|
249
|
+
}
|
|
250
|
+
const { mockBuyer } = await import("./pay.js");
|
|
251
|
+
return mockBuyer();
|
|
252
|
+
}
|
|
253
|
+
export { AGENT_UA };
|
|
254
|
+
//# sourceMappingURL=buyer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buyer.js","sourceRoot":"","sources":["../src/buyer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,MAAM,QAAQ,GAAG,qBAAqB,CAAC;AA2FvC;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,oFAAoF;IACpF,uFAAuF;IACvF,uFAAuF;IACvF,wFAAwF;IACxF,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,KAAgB;IAC9D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAC7C,IAAI,IAAI,IAAI,OAAO;QAAE,OAAO,IAAI,CAAC;IACjC,OAAO;QACL,EAAE,EAAE,KAAK;QACT,SAAS,EAAE,YAAY;QACvB,SAAS,EAAE,IAAI;QACf,KAAK,EACH,6CAA6C,IAAI,oBAAoB,OAAO,kBAAkB;YAC9F,kFAAkF;KACrF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,qBAAqB,CAAC,SAAiB;IACrD,MAAM,IAAI,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,gBAAgB,CAAC;QACtB,KAAK,eAAe,CAAC;QACrB,KAAK,YAAY;YACf,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QACxD,KAAK,eAAe;YAClB,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QAC1D,KAAK,UAAU,CAAC;QAChB,KAAK,gBAAgB,CAAC;QACtB,KAAK,mBAAmB;YACtB,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;QACrD;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,MAAM,CAAC,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;IAClC,IAAI,iFAAiF,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9F,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAC/D,CAAC;IACD,IAAI,8EAA8E,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3F,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACnD,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AACpD,CAAC;AAwBD;;wEAEwE;AACxE,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,GAAW,EACX,IAAyB,EACzB,OAAe;IAEf,IAAI,GAAa,CAAC;IAClB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE;YAC1B,OAAO,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE;SACtF,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,oFAAoF;QACpF,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACnD,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,qCAAqC,EAAE,CAAC;QAC3F,IAAI,OAWH,CAAC;QACF,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACvE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,wCAAwC,EAAE,CAAC;QACnF,CAAC;QACD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG;YAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,+CAA+C,EAAE,CAAC;QAClG,mFAAmF;QACnF,gFAAgF;QAChF,2FAA2F;QAC3F,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,CAAC;QAClD,OAAO;YACL,MAAM,EAAE,OAAO;YACf,MAAM,EAAE;gBACN,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS;gBACzC,YAAY,EAAE,GAAG,CAAC,MAAM;gBACxB,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,KAAK;gBACvB,YAAY,EAAE,GAAG;gBACjB,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzE,mFAAmF;gBACnF,mFAAmF;gBACnF,sFAAsF;gBACtF,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC7C;SACF,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;IACxE,IAAI,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACtC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;AAC3D,CAAC;AAED;8EAC8E;AAC9E,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,GAAW,EACX,IAAyB,EACzB,OAAe;IAEf,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;IAChD,OAAO,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,OAAmD,EAAE,GAAW;IAC3F,QAAQ,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,KAAK,MAAM;YACT,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,SAAS,EAAE,WAAW;gBACtB,SAAS,EAAE,KAAK;gBAChB,KAAK,EAAE,4EAA4E;aACpF,CAAC;QACJ,KAAK,WAAW;YACd,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,SAAS,EAAE,WAAW;gBACtB,SAAS,EAAE,KAAK;gBAChB,KAAK,EACH,UAAU,GAAG,mFAAmF;oBAChG,oGAAoG;oBACpG,qCAAqC;aACxC,CAAC;QACJ,KAAK,aAAa;YAChB,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,SAAS,EAAE,cAAc;gBACzB,SAAS,EAAE,IAAI;gBACf,KAAK,EAAE,UAAU,GAAG,WAAW,OAAO,CAAC,UAAU,IAAI,aAAa,sDAAsD;aACzH,CAAC;QACJ,KAAK,WAAW;YACd,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,SAAS,EAAE,UAAU;gBACrB,SAAS,EAAE,IAAI;gBACf,KAAK,EAAE,+BAA+B,OAAO,CAAC,MAAM,mDAAmD;aACxG,CAAC;IACN,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,OAA6E;IAE7E,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CACtB,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,YAAY,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CACrF,CACF,CAAC;QACF,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClE,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IACjE,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACjE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAW,EACX,IAAyB,EACzB,OAAe,EACf,OAAe;AACf,oFAAoF;AACpF,KAAc;IAEd,MAAM,OAAO,GAA2B;QACtC,YAAY,EAAE,QAAQ;QACtB,gBAAgB,EAAE,OAAO;QACzB,eAAe,EAAE,IAAI;QACrB,kBAAkB,EAAE,OAAO;KAC5B,CAAC;IACF,IAAI,KAAK;QAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,KAAK,CAAC;IAC7C,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC/C,IAAI,CAAC,GAAG,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,oBAAoB,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;IAC3E,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC;AACvE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;IACxB,IAAI,GAAG,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACnC,kFAAkF;QAClF,gFAAgF;QAChF,kFAAkF;QAClF,mFAAmF;QACnF,IAAI,YAAY,CAAC,aAAa,EAAE,CAAC,EAAE,CAAC;YAClC,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;YAChD,OAAO,SAAS,EAAE,CAAC;QACrB,CAAC;QACD,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACtD,OAAO,YAAY,EAAE,CAAC;IACxB,CAAC;IACD,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;IAC/C,OAAO,SAAS,EAAE,CAAC;AACrB,CAAC;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC"}
|
package/dist/decide.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AppraisedCandidate, Decision } from "./types.ts";
|
|
2
|
+
export interface DecisionPolicy {
|
|
3
|
+
/**
|
|
4
|
+
* Don't pay for anything below this relevance, however cheap. Protects the
|
|
5
|
+
* budget from spending on near-misses just because they're affordable.
|
|
6
|
+
*/
|
|
7
|
+
relevanceFloor: number;
|
|
8
|
+
/** Hard ceiling on how many essays to pay for in one run. */
|
|
9
|
+
maxPaid: number;
|
|
10
|
+
/**
|
|
11
|
+
* Agent identity this run is attributed to. It does NOT alter spend math here —
|
|
12
|
+
* a per-agent cap is applied by the caller resolving that agent's remaining
|
|
13
|
+
* allowance and passing it as `budgetUsdc`. It rides on the policy so the
|
|
14
|
+
* decision log can be tagged per agent in the audit plane (BUY-3.3).
|
|
15
|
+
*/
|
|
16
|
+
agentId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Allowlist of publisher hosts. When set, ONLY these hosts are payable; every
|
|
19
|
+
* other host — and any candidate whose host is unknown — is skipped
|
|
20
|
+
* (deny-by-default). Host match is exact, case-insensitive.
|
|
21
|
+
*/
|
|
22
|
+
allowDomains?: string[];
|
|
23
|
+
/** Hosts that are never paid, even when allowed and affordable. Deny wins over allow. */
|
|
24
|
+
denyDomains?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Max essays to pay for from any single host. Counts pays made this run plus
|
|
27
|
+
* any prior pays for the host in the current window (`context.priorDomainCounts`).
|
|
28
|
+
*/
|
|
29
|
+
perDomainCap?: number;
|
|
30
|
+
/**
|
|
31
|
+
* A toll at or above this price is not auto-paid — it becomes an `approve`
|
|
32
|
+
* decision (human gate) instead. Cheaper tolls pay automatically.
|
|
33
|
+
*/
|
|
34
|
+
approvalThresholdUsdc?: number;
|
|
35
|
+
/** Kill-switch: when true, halt all new spend this run (free re-reads still allowed). */
|
|
36
|
+
killSwitch?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Runtime state injected into `decide()` that can't be known from the candidates alone. */
|
|
39
|
+
export interface DecideContext {
|
|
40
|
+
/**
|
|
41
|
+
* Pays already made to each host in the current rate-cap window (across earlier
|
|
42
|
+
* runs), added to this run's per-host count when enforcing `perDomainCap`.
|
|
43
|
+
*/
|
|
44
|
+
priorDomainCounts?: Record<string, number>;
|
|
45
|
+
/**
|
|
46
|
+
* The configured gate base URL. Supplied so `decide()` can resolve a slug-only candidate to the
|
|
47
|
+
* SAME url the pay step will use (`c.url ?? articleUrl(gateBase, c.slug)`), and evaluate domain
|
|
48
|
+
* policy against that real target. Without it a slug-only candidate has no derivable host and is
|
|
49
|
+
* treated as unknown — which an allowlist correctly denies by default.
|
|
50
|
+
*/
|
|
51
|
+
gateBase?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The hostname money will actually go to, mirroring the pay step's own resolution
|
|
55
|
+
* (`c.url ?? articleUrl(gateBase, c.slug)`). This — not the discovery source's `Candidate.host`
|
|
56
|
+
* field — is what domain policy is evaluated against, so an allow/deny decision can never be made
|
|
57
|
+
* about a different host than the one that gets paid. Returns undefined when no url is derivable
|
|
58
|
+
* (an allowlist then denies by default).
|
|
59
|
+
*/
|
|
60
|
+
export declare function payUrlOf(url: string | undefined, gateBase: string | undefined, slug: string): string | undefined;
|
|
61
|
+
export declare function payHostOf(url: string | undefined, gateBase: string | undefined, slug: string): string | undefined;
|
|
62
|
+
/** The shared spend gate's verdict. `approve` means "real, but needs a human" — distinct from
|
|
63
|
+
* `skip` so callers can surface a human-approval affordance rather than a flat refusal. */
|
|
64
|
+
export type SpendVerdict = {
|
|
65
|
+
ok: true;
|
|
66
|
+
} | {
|
|
67
|
+
ok: false;
|
|
68
|
+
action: "skip" | "approve";
|
|
69
|
+
reason: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* THE operator-policy gate — the single source of truth for "may I pay this host, at this price,
|
|
73
|
+
* right now". Every spending path calls this: `decide()` for the composite research run, and the
|
|
74
|
+
* MCP's granular `naulon_pay_and_read`.
|
|
75
|
+
*
|
|
76
|
+
* It exists because the checks previously lived ONLY inside `decide()`, so the granular pay tool
|
|
77
|
+
* — the path the tool descriptions tell agents to prefer — silently ignored the kill-switch,
|
|
78
|
+
* deny/allow lists, per-domain cap, and approval threshold an operator had configured. Two
|
|
79
|
+
* implementations of one rule is how that bug happens; there is now one implementation.
|
|
80
|
+
*
|
|
81
|
+
* Order is load-bearing and matches the original `decide()` sequence, so the reason a caller
|
|
82
|
+
* surfaces when several gates apply is unchanged: kill → deny → allow → maxPaid → perDomainCap →
|
|
83
|
+
* budget → approval. `paidCount` / `remainingUsdc` are optional; omit them when the caller
|
|
84
|
+
* enforces those with its own accounting and messaging (the MCP session envelope does).
|
|
85
|
+
*/
|
|
86
|
+
export declare function spendGate(input: {
|
|
87
|
+
/** Publisher host, already normalized-ish; undefined when unknown (deny-by-default under an allowlist). */
|
|
88
|
+
host: string | undefined;
|
|
89
|
+
/** The buyer's TRUE total for this read, in USDC. */
|
|
90
|
+
priceUsdc: number;
|
|
91
|
+
policy: DecisionPolicy;
|
|
92
|
+
/** Pays already made for this host (this run/session + any prior window). */
|
|
93
|
+
paidForHost?: number;
|
|
94
|
+
/** Pays already made overall — enables the `maxPaid` gate when provided. */
|
|
95
|
+
paidCount?: number;
|
|
96
|
+
/** Budget left in USDC — enables the budget gate when provided. */
|
|
97
|
+
remainingUsdc?: number;
|
|
98
|
+
}): SpendVerdict;
|
|
99
|
+
/**
|
|
100
|
+
* TODO(you): this policy is the lever that defines the agent's "taste". The
|
|
101
|
+
* defaults are sensible, but the interesting choices are yours to make:
|
|
102
|
+
*
|
|
103
|
+
* - relevanceFloor: how picky? Too low → wastes budget on tangential essays.
|
|
104
|
+
* Too high → misses useful context. 0.35 is a starting guess.
|
|
105
|
+
* - Ranking key: density (relevance/price) favors cheap-and-relevant. Would
|
|
106
|
+
* you instead rank by raw relevance (quality at any price), or blend them?
|
|
107
|
+
* See `rank()` below — that sort is the whole strategy in one line.
|
|
108
|
+
* - maxPaid: a stop so a big budget doesn't over-cite a thin topic.
|
|
109
|
+
*
|
|
110
|
+
* Tune these against real runs and watch the decision log; that visible
|
|
111
|
+
* reasoning is what the judges reward.
|
|
112
|
+
*/
|
|
113
|
+
export declare const DEFAULT_POLICY: DecisionPolicy;
|
|
114
|
+
export declare function decide(candidates: AppraisedCandidate[], budgetUsdc: number, cached?: ReadonlySet<string>, policy?: DecisionPolicy, context?: DecideContext): Decision[];
|
|
115
|
+
//# sourceMappingURL=decide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decide.d.ts","sourceRoot":"","sources":["../src/decide.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE/D,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAIhB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,yFAAyF;IACzF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,yFAAyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAOD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAYhH;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjH;AAED;4FAC4F;AAC5F,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpG;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE;IAC/B,2GAA2G;IAC3G,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,cAAc,CAAC;IACvB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,YAAY,CAwCf;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,EAAE,cAG5B,CAAC;AASF,wBAAgB,MAAM,CACpB,UAAU,EAAE,kBAAkB,EAAE,EAChC,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,WAAW,CAAC,MAAM,CAAa,EACvC,MAAM,GAAE,cAA+B,EACvC,OAAO,GAAE,aAAkB,GAC1B,QAAQ,EAAE,CA2FZ"}
|