@reinconsole/gate 0.1.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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.cjs +864 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +561 -0
- package/dist/index.d.ts +561 -0
- package/dist/index.js +844 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { GateReceipt, ReinEvent } from '@reinconsole/core';
|
|
2
|
+
export { GateReceipt, ReinEvent } from '@reinconsole/core';
|
|
3
|
+
import { PaymentRequirement, PaymentRequired, FetchLike } from '@reinconsole/sdk';
|
|
4
|
+
export { PaymentRequired, PaymentRequirement } from '@reinconsole/sdk';
|
|
5
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Why the gate turned a payment away. Stable strings — clients switch on these.
|
|
9
|
+
*
|
|
10
|
+
* Two classes carry no payer fault and MUST NOT feed reputation evidence
|
|
11
|
+
* (@reinconsole/graph skips them): the throttle codes (`rate_limited`,
|
|
12
|
+
* `velocity_exceeded` — the vendor's cap, not payer misbehavior) and the rails
|
|
13
|
+
* codes (`rails_unavailable`, `settle_unknown` — the vendor's infrastructure
|
|
14
|
+
* failing; a settle_unknown payment may even have gone through).
|
|
15
|
+
*/
|
|
16
|
+
type GateRefusalCode = 'malformed_payment' | 'scheme_mismatch' | 'network_mismatch' | 'amount_mismatch' | 'recipient_mismatch' | 'payer_denied' | 'payer_not_allowed' | 'payment_replayed'
|
|
17
|
+
/** Too many presentations in the velocity window (429; Retry-After set). */
|
|
18
|
+
| 'rate_limited'
|
|
19
|
+
/** The settled-spend cap would be exceeded (429; Retry-After when a slot frees). */
|
|
20
|
+
| 'velocity_exceeded' | 'verify_failed' | 'settle_failed'
|
|
21
|
+
/** The rails could not be reached and the payment provably did NOT settle
|
|
22
|
+
* (503). The replay slot is released — the same header may be re-presented. */
|
|
23
|
+
| 'rails_unavailable'
|
|
24
|
+
/** Settlement was attempted but its fate is unknown — the request may have
|
|
25
|
+
* reached the rails (503). The slot stays burned; do NOT re-pay blindly:
|
|
26
|
+
* reconcile against transaction records (e.g. the on-chain indexer) first. */
|
|
27
|
+
| 'settle_unknown';
|
|
28
|
+
declare class GateError extends Error {
|
|
29
|
+
readonly code: GateRefusalCode;
|
|
30
|
+
/** Seconds until the refused payment could be admitted (throttle codes). */
|
|
31
|
+
readonly retryAfterSeconds?: number;
|
|
32
|
+
constructor(code: GateRefusalCode, message: string, options?: {
|
|
33
|
+
retryAfterSeconds?: number;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The gate's settlement seam. The gate decides WHETHER a payment is acceptable
|
|
39
|
+
* (routes, screening, replay, amount cross-checks); the rails decide whether it
|
|
40
|
+
* is VALID and move the money. Both Rein rails plug in via the structural
|
|
41
|
+
* adapters below — @reinconsole/gate deliberately imports neither, so vendors install
|
|
42
|
+
* only what they run.
|
|
43
|
+
*/
|
|
44
|
+
interface GateRails {
|
|
45
|
+
/** Throw GateError('verify_failed') if the payment does not check out. */
|
|
46
|
+
verify(paymentHeader: string, requirement: PaymentRequirement): Promise<void>;
|
|
47
|
+
/** Move the money. Throw GateError('settle_failed') if it cannot. */
|
|
48
|
+
settle(paymentHeader: string, requirement: PaymentRequirement): Promise<GateSettlement>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Error taxonomy at this seam — the gate's retry policy hangs off it:
|
|
52
|
+
*
|
|
53
|
+
* - `GateError` — a semantic verdict (bad signature, rejected authorization).
|
|
54
|
+
* Final; never retried.
|
|
55
|
+
* - `RailsUnreachableError` — the request PROVABLY never reached the rails
|
|
56
|
+
* (connection refused, DNS failure). Safe to retry, even for settle.
|
|
57
|
+
* - anything else — ambiguous transport failure (timeout, connection reset,
|
|
58
|
+
* gateway error): the request MAY have executed. verify is read-only so the
|
|
59
|
+
* gate retries it anyway; an ambiguous settle failure is never retried and
|
|
60
|
+
* refuses `settle_unknown`, because re-settling an authorization that
|
|
61
|
+
* actually landed would misreport a paid payment as failed.
|
|
62
|
+
*/
|
|
63
|
+
declare class RailsUnreachableError extends Error {
|
|
64
|
+
constructor(message: string, options?: {
|
|
65
|
+
cause?: unknown;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
interface GateSettlement {
|
|
69
|
+
/** Ready-made X-PAYMENT-RESPONSE header value. */
|
|
70
|
+
header: string;
|
|
71
|
+
/** Settlement transaction hash (mock ledger or on-chain). */
|
|
72
|
+
transaction: string;
|
|
73
|
+
network: string;
|
|
74
|
+
payer?: string;
|
|
75
|
+
}
|
|
76
|
+
/** What the gate needs of @reinconsole/mock-rails' MockFacilitator (structural). */
|
|
77
|
+
interface MockFacilitatorLike {
|
|
78
|
+
verify(paymentHeader: string, requirement: PaymentRequirement): unknown;
|
|
79
|
+
settle(paymentHeader: string, requirement: PaymentRequirement): {
|
|
80
|
+
header: string;
|
|
81
|
+
response: {
|
|
82
|
+
transaction: string;
|
|
83
|
+
network: string;
|
|
84
|
+
payer?: string;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Wire the gate to the mock rails (offline twin; throws map to refusals). */
|
|
89
|
+
declare function mockFacilitatorRails(facilitator: MockFacilitatorLike): GateRails;
|
|
90
|
+
/** What the gate needs of @reinconsole/x402-rails' FacilitatorClient (structural).
|
|
91
|
+
* `requirements` is untyped because the DIALECT varies per payment: v1
|
|
92
|
+
* payments relay Rein's internal (v1-shaped) requirement, v2 payments the
|
|
93
|
+
* converted v2 shape (see facilitatorClientRails). */
|
|
94
|
+
interface FacilitatorClientLike {
|
|
95
|
+
verify(payload: unknown, requirements: unknown): Promise<{
|
|
96
|
+
isValid: boolean;
|
|
97
|
+
invalidReason?: string;
|
|
98
|
+
}>;
|
|
99
|
+
settle(payload: unknown, requirements: unknown): Promise<{
|
|
100
|
+
success: boolean;
|
|
101
|
+
errorReason?: string;
|
|
102
|
+
transaction: string;
|
|
103
|
+
network: string;
|
|
104
|
+
payer?: string;
|
|
105
|
+
}>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Wire the gate to a real x402 facilitator client. The facilitator wants the
|
|
109
|
+
* DECODED payment envelope; the gate re-decodes the header it already
|
|
110
|
+
* inspected and relays the facilitator's settle response verbatim into the
|
|
111
|
+
* settlement header (tx hash included). Requirements travel in the same
|
|
112
|
+
* dialect the payment arrived in — a v2 envelope is verified against
|
|
113
|
+
* v2-shaped (amount/CAIP-2) requirements.
|
|
114
|
+
*/
|
|
115
|
+
declare function facilitatorClientRails(client: FacilitatorClientLike): GateRails;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One priced route. Anything a route doesn't override falls back to the
|
|
119
|
+
* gate-level payment defaults, so most vendors only write `path` + `price`.
|
|
120
|
+
*/
|
|
121
|
+
interface GateRoute {
|
|
122
|
+
/** Glob over the request path, e.g. "/api/reports/*". First match wins. */
|
|
123
|
+
path: string;
|
|
124
|
+
/** HTTP method (case-insensitive); omit to price every method. */
|
|
125
|
+
method?: string;
|
|
126
|
+
/** Human-unit decimal price, e.g. "0.05". */
|
|
127
|
+
price: string;
|
|
128
|
+
description?: string;
|
|
129
|
+
mimeType?: string;
|
|
130
|
+
payTo?: string;
|
|
131
|
+
network?: string;
|
|
132
|
+
asset?: string;
|
|
133
|
+
/** Atomic decimals of the asset (default 6, all Rein stablecoins). */
|
|
134
|
+
decimals?: number;
|
|
135
|
+
maxTimeoutSeconds?: number;
|
|
136
|
+
/** EIP-712 domain hints etc., merged over the gate-level extra. */
|
|
137
|
+
extra?: Record<string, unknown>;
|
|
138
|
+
}
|
|
139
|
+
/** Gate-level payment defaults every route inherits. */
|
|
140
|
+
interface PaymentDefaults {
|
|
141
|
+
payTo: string;
|
|
142
|
+
network: string;
|
|
143
|
+
asset: string;
|
|
144
|
+
decimals?: number;
|
|
145
|
+
maxTimeoutSeconds?: number;
|
|
146
|
+
extra?: Record<string, unknown>;
|
|
147
|
+
}
|
|
148
|
+
/** First route whose method and path glob accept the request, or undefined. */
|
|
149
|
+
declare function matchRoute(routes: readonly GateRoute[], method: string, pathname: string): GateRoute | undefined;
|
|
150
|
+
declare function routeDecimals(route: GateRoute, defaults: PaymentDefaults): number;
|
|
151
|
+
/**
|
|
152
|
+
* Build the v1 payment requirement this route quotes for a concrete resource
|
|
153
|
+
* URL. Fully populated (description/mimeType/maxTimeoutSeconds, absolute
|
|
154
|
+
* resource) because hosted facilitators validate requirements strictly.
|
|
155
|
+
*/
|
|
156
|
+
declare function requirementFor(route: GateRoute, defaults: PaymentDefaults, resourceUrl: string): PaymentRequirement;
|
|
157
|
+
|
|
158
|
+
/** Sync for in-memory stores; durable stores return a promise. */
|
|
159
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
160
|
+
/**
|
|
161
|
+
* The gate's storage seam. Two consistency classes live here, split the same
|
|
162
|
+
* way the engine and graph stores are:
|
|
163
|
+
*
|
|
164
|
+
* - **Replay slots are security state** — the gate AWAITS `burnReplay`, and a
|
|
165
|
+
* durable impl persists the burn BEFORE resolving. A payment settles only
|
|
166
|
+
* after its slot is durably burned, so a crash-and-restart cannot let the
|
|
167
|
+
* same header settle twice (the mock ledger, unlike the chain, would).
|
|
168
|
+
* - **Receipts and counters are telemetry** — writes may trail behind
|
|
169
|
+
* (cache-then-persist), with `flush()` as the error channel: it drains
|
|
170
|
+
* pending writes and throws the first failure since the last flush.
|
|
171
|
+
*
|
|
172
|
+
* Reads are synchronous from the working set (`stats()` stays sync).
|
|
173
|
+
*/
|
|
174
|
+
interface GateStorePort {
|
|
175
|
+
/**
|
|
176
|
+
* Check-and-burn a replay slot: returns false when this exact payment was
|
|
177
|
+
* already presented. The check-and-set MUST happen synchronously at call
|
|
178
|
+
* time so two concurrent copies of one header cannot both see it fresh;
|
|
179
|
+
* a durable impl then persists the burn before resolving. Slots are never
|
|
180
|
+
* released — a failed settle after the burn stays burned (deliberate:
|
|
181
|
+
* the payer must re-quote).
|
|
182
|
+
*/
|
|
183
|
+
burnReplay(key: string): MaybePromise<boolean>;
|
|
184
|
+
/**
|
|
185
|
+
* OPTIONAL: remove a burned slot (memory AND disk). The gate calls this in
|
|
186
|
+
* exactly one situation — the rails PROVABLY never saw the payment
|
|
187
|
+
* (`rails_unavailable`), so re-presenting the same header is safe once they
|
|
188
|
+
* return. Stores that omit it leave the slot burned: conservative, the
|
|
189
|
+
* payer re-signs instead. Never called after an ambiguous settle.
|
|
190
|
+
*/
|
|
191
|
+
releaseReplay?(key: string): MaybePromise<void>;
|
|
192
|
+
/** Record a settled payment's receipt (may trail; see flush). Note: the
|
|
193
|
+
* receipt is the ONLY durable record of a settlement — a hard crash with
|
|
194
|
+
* the tail unflushed permanently undercounts that payment's revenue. */
|
|
195
|
+
appendReceipt(receipt: GateReceipt): MaybePromise<void>;
|
|
196
|
+
/** Count a 402 quote served (may trail; see flush). */
|
|
197
|
+
recordQuote(): MaybePromise<void>;
|
|
198
|
+
/** Count a refused payment (may trail; see flush). */
|
|
199
|
+
recordRefusal(): MaybePromise<void>;
|
|
200
|
+
receipts(): readonly GateReceipt[];
|
|
201
|
+
quoted(): number;
|
|
202
|
+
refused(): number;
|
|
203
|
+
/** Durable impls: drain trailing writes, throw the first failure. */
|
|
204
|
+
flush?(): Promise<void>;
|
|
205
|
+
}
|
|
206
|
+
/** In-memory gate store — the default, and the working set durable stores hydrate into. */
|
|
207
|
+
declare class InMemoryGateStore implements GateStorePort {
|
|
208
|
+
private readonly seenPayments;
|
|
209
|
+
private readonly receiptLog;
|
|
210
|
+
private quotedCount;
|
|
211
|
+
private refusedCount;
|
|
212
|
+
burnReplay(key: string): boolean;
|
|
213
|
+
/**
|
|
214
|
+
* Release a burned slot — on the port (optional) since the rails-unreachable
|
|
215
|
+
* path, and still what a persist-then-cache impl uses to leave memory
|
|
216
|
+
* untouched when its own INSERT fails.
|
|
217
|
+
*/
|
|
218
|
+
releaseReplay(key: string): void;
|
|
219
|
+
appendReceipt(receipt: GateReceipt): void;
|
|
220
|
+
recordQuote(): void;
|
|
221
|
+
recordRefusal(): void;
|
|
222
|
+
/** Hydration primitive for durable stores: set the resumed counter totals. */
|
|
223
|
+
loadCounters(quoted: number, refused: number): void;
|
|
224
|
+
receipts(): readonly GateReceipt[];
|
|
225
|
+
quoted(): number;
|
|
226
|
+
refused(): number;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Per-payer velocity limits, all sharing one rolling window. Two evidence
|
|
231
|
+
* sources, deliberately different in durability:
|
|
232
|
+
*
|
|
233
|
+
* - `maxPayments` / `maxAmount` cap SETTLED spend and are derived from the
|
|
234
|
+
* gate's receipts at check time — on a durable store they survive restarts
|
|
235
|
+
* for free (receipts hydrate), and they never drift from revenue truth.
|
|
236
|
+
* - `maxAttempts` caps PRESENTATIONS (any outcome) and lives in memory only —
|
|
237
|
+
* refused attempts leave no durable row, so this counter resets on restart.
|
|
238
|
+
* That is acceptable for what it protects against (hammering), and it keeps
|
|
239
|
+
* the port unchanged.
|
|
240
|
+
*
|
|
241
|
+
* Limits are gate-wide per payer (no per-route caps in v0.1). A payment
|
|
242
|
+
* refused for velocity is NOT replay-burned — the same signed header may be
|
|
243
|
+
* re-presented once the window clears; the refusal carries Retry-After.
|
|
244
|
+
*/
|
|
245
|
+
interface GateVelocity {
|
|
246
|
+
/** Rolling window in milliseconds. */
|
|
247
|
+
windowMs: number;
|
|
248
|
+
/** Max settled payments per payer inside the window (any asset). */
|
|
249
|
+
maxPayments?: number;
|
|
250
|
+
/**
|
|
251
|
+
* Max settled decimal amount per payer inside the window, compared per
|
|
252
|
+
* asset (only receipts in the incoming payment's asset count toward it).
|
|
253
|
+
* Also acts as a per-payment ceiling: a single payment above it never
|
|
254
|
+
* clears, and its refusal carries no Retry-After.
|
|
255
|
+
*/
|
|
256
|
+
maxAmount?: string;
|
|
257
|
+
/** Max payment presentations per payer inside the window, any outcome. */
|
|
258
|
+
maxAttempts?: number;
|
|
259
|
+
}
|
|
260
|
+
/** Fail fast on misconfiguration — a silent zero-cap gate refuses everyone. */
|
|
261
|
+
declare function validateVelocity(velocity: GateVelocity): void;
|
|
262
|
+
|
|
263
|
+
/** Wallet-address screening. Addresses compare case-insensitively (EVM rule). */
|
|
264
|
+
interface GateScreen {
|
|
265
|
+
/** If set, ONLY these payers may pay. */
|
|
266
|
+
allowPayers?: readonly string[];
|
|
267
|
+
/** These payers are always refused, allowlist or not. */
|
|
268
|
+
denyPayers?: readonly string[];
|
|
269
|
+
/**
|
|
270
|
+
* Dynamic screening hook, consulted after the static lists with the payer
|
|
271
|
+
* address as presented. Return a refusal reason to turn the payer away
|
|
272
|
+
* (403, code `payer_denied`); return undefined to let the payment proceed.
|
|
273
|
+
* Reputation-driven screening (@reinconsole/graph's `payerCheck`) plugs in here.
|
|
274
|
+
*/
|
|
275
|
+
check?: (payer: string) => string | undefined;
|
|
276
|
+
}
|
|
277
|
+
interface GateOptions {
|
|
278
|
+
/** Priced routes; first match wins. Unmatched requests pass through free. */
|
|
279
|
+
routes: readonly GateRoute[];
|
|
280
|
+
/** Settlement rails: mockFacilitatorRails(...) or facilitatorClientRails(...). */
|
|
281
|
+
rails: GateRails;
|
|
282
|
+
/** Default recipient address (a route can override). */
|
|
283
|
+
payTo: string;
|
|
284
|
+
/** Default x402 network id, e.g. "base" (mock rails) or "base-sepolia". */
|
|
285
|
+
network: string;
|
|
286
|
+
/** Default asset: a symbol (mock rails) or token contract address (real). */
|
|
287
|
+
asset: string;
|
|
288
|
+
decimals?: number;
|
|
289
|
+
maxTimeoutSeconds?: number;
|
|
290
|
+
/** EIP-712 domain hints etc., quoted on every requirement. */
|
|
291
|
+
extra?: Record<string, unknown>;
|
|
292
|
+
screen?: GateScreen;
|
|
293
|
+
/**
|
|
294
|
+
* Also advertise quotes on the x402 v2 wire: 402 outcomes gain a
|
|
295
|
+
* `paymentRequiredHeader` (base64 PaymentRequired, CAIP-2 networks) that the
|
|
296
|
+
* adapters send as `PAYMENT-REQUIRED`, alongside the unchanged v1 body —
|
|
297
|
+
* dual-stack, each dialect reads its own channel. Off by default. Note the
|
|
298
|
+
* gate ACCEPTS v2 payments (PAYMENT-SIGNATURE envelopes) regardless of this
|
|
299
|
+
* flag; it only controls what quotes advertise.
|
|
300
|
+
*/
|
|
301
|
+
advertiseV2?: boolean;
|
|
302
|
+
/** Per-payer velocity limits (see GateVelocity). Off when omitted. */
|
|
303
|
+
velocity?: GateVelocity;
|
|
304
|
+
/**
|
|
305
|
+
* Rails transport-failure retry policy. `attempts` counts EXTRA tries after
|
|
306
|
+
* the first (default 2), spaced `backoffMs * attemptNumber` apart (default
|
|
307
|
+
* 250ms; pass 0 in tests). Applies only to transport failures — a GateError
|
|
308
|
+
* from the rails is a semantic verdict and is never retried. See GateRails
|
|
309
|
+
* for which settle failures are retry-safe.
|
|
310
|
+
*/
|
|
311
|
+
retry?: {
|
|
312
|
+
attempts?: number;
|
|
313
|
+
backoffMs?: number;
|
|
314
|
+
};
|
|
315
|
+
/** Injectable clock (tests). */
|
|
316
|
+
now?: () => Date;
|
|
317
|
+
/**
|
|
318
|
+
* Gate storage. Defaults in-memory; pass @reinconsole/store's gate store and
|
|
319
|
+
* receipts, revenue stats, and burned replay slots survive restarts.
|
|
320
|
+
*/
|
|
321
|
+
store?: GateStorePort;
|
|
322
|
+
}
|
|
323
|
+
/** The transport-agnostic request shape every adapter reduces to. */
|
|
324
|
+
interface GateRequest {
|
|
325
|
+
method: string;
|
|
326
|
+
/** Absolute request URL (quoted verbatim as the requirement's resource). */
|
|
327
|
+
url: string;
|
|
328
|
+
/** Raw X-PAYMENT header value, or null when absent. */
|
|
329
|
+
payment: string | null;
|
|
330
|
+
}
|
|
331
|
+
type GateOutcome =
|
|
332
|
+
/** No priced route matched — the vendor serves this request for free. */
|
|
333
|
+
{
|
|
334
|
+
kind: 'open';
|
|
335
|
+
}
|
|
336
|
+
/** No payment attached: here is what this resource costs. The optional
|
|
337
|
+
* `paymentRequiredHeader` (advertiseV2) is the base64 v2 PaymentRequired
|
|
338
|
+
* for the PAYMENT-REQUIRED response header. */
|
|
339
|
+
| {
|
|
340
|
+
kind: 'quote';
|
|
341
|
+
status: 402;
|
|
342
|
+
body: PaymentRequired;
|
|
343
|
+
paymentRequiredHeader?: string;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* A payment was attached and turned away. 403 = screening, 402 = re-quote,
|
|
347
|
+
* 429 = throttled (retryAfterSeconds set when a slot will free), 503 = the
|
|
348
|
+
* rails failed us (`rails_unavailable` = provably unsettled, slot released;
|
|
349
|
+
* `settle_unknown` = fate unknown, slot stays burned — do not re-pay blindly).
|
|
350
|
+
*/
|
|
351
|
+
| {
|
|
352
|
+
kind: 'refused';
|
|
353
|
+
status: 402 | 403 | 429 | 503;
|
|
354
|
+
code: GateRefusalCode;
|
|
355
|
+
reason: string;
|
|
356
|
+
body: unknown;
|
|
357
|
+
retryAfterSeconds?: number;
|
|
358
|
+
/** On 402 re-quotes with advertiseV2: the v2 PAYMENT-REQUIRED value. */
|
|
359
|
+
paymentRequiredHeader?: string;
|
|
360
|
+
}
|
|
361
|
+
/** Verified and settled — serve, attaching settlementHeader as X-PAYMENT-RESPONSE. */
|
|
362
|
+
| {
|
|
363
|
+
kind: 'paid';
|
|
364
|
+
receipt: GateReceipt;
|
|
365
|
+
settlementHeader: string;
|
|
366
|
+
};
|
|
367
|
+
interface GateLineStats {
|
|
368
|
+
settled: number;
|
|
369
|
+
revenue: string;
|
|
370
|
+
}
|
|
371
|
+
interface GateStats {
|
|
372
|
+
quoted: number;
|
|
373
|
+
settled: number;
|
|
374
|
+
refused: number;
|
|
375
|
+
/** Decimal revenue grouped by asset. */
|
|
376
|
+
revenue: Record<string, string>;
|
|
377
|
+
/** Per route pattern. */
|
|
378
|
+
routes: Record<string, GateLineStats>;
|
|
379
|
+
/** Per payer (lowercased address). */
|
|
380
|
+
payers: Record<string, GateLineStats>;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Rein Gate — the vendor side of the wire. One gate fronts one vendor API:
|
|
384
|
+
* it quotes x402 requirements for priced routes, cross-checks and screens
|
|
385
|
+
* incoming payments, settles them through the configured rails, and keeps
|
|
386
|
+
* vendor-side receipts so monetization is observable, not anecdotal.
|
|
387
|
+
*
|
|
388
|
+
* Check order for a presented payment: envelope decode -> rate limit ->
|
|
389
|
+
* quote consistency (scheme/network/amount/recipient) -> payer screening ->
|
|
390
|
+
* velocity caps -> replay burn -> rails verify -> rails settle. The replay
|
|
391
|
+
* slot is burned BEFORE the async legs so two concurrent copies of the same
|
|
392
|
+
* payment cannot both settle (the mock ledger, unlike the chain, would
|
|
393
|
+
* happily double-spend); throttle refusals fire before the burn so a
|
|
394
|
+
* velocity-refused header can be re-presented once its window clears.
|
|
395
|
+
*/
|
|
396
|
+
declare class Gate {
|
|
397
|
+
private readonly routes;
|
|
398
|
+
private readonly rails;
|
|
399
|
+
private readonly defaults;
|
|
400
|
+
private readonly allowPayers;
|
|
401
|
+
private readonly denyPayers;
|
|
402
|
+
private readonly screenCheck;
|
|
403
|
+
private readonly advertiseV2;
|
|
404
|
+
private readonly velocity;
|
|
405
|
+
private readonly attempts;
|
|
406
|
+
private readonly retryPolicy;
|
|
407
|
+
private readonly now;
|
|
408
|
+
private readonly bus;
|
|
409
|
+
private readonly store;
|
|
410
|
+
constructor(options: GateOptions);
|
|
411
|
+
onEvent(handler: (event: ReinEvent) => void): void;
|
|
412
|
+
get receipts(): readonly GateReceipt[];
|
|
413
|
+
/** Drain trailing telemetry writes; throws the first failure (durable stores). */
|
|
414
|
+
flush(): Promise<void>;
|
|
415
|
+
/** Telemetry writes may trail (cache-then-persist) — a store failure,
|
|
416
|
+
* rejected OR thrown synchronously, must surface through flush() and can
|
|
417
|
+
* never alter a payment outcome (a receipt write turning a SETTLED payment
|
|
418
|
+
* into a 500 would charge the payer and serve nothing). */
|
|
419
|
+
private fire;
|
|
420
|
+
/** The requirement a given request would be quoted (or undefined if free). */
|
|
421
|
+
quoteFor(method: string, url: string): PaymentRequirement | undefined;
|
|
422
|
+
handle(request: GateRequest): Promise<GateOutcome>;
|
|
423
|
+
stats(): GateStats;
|
|
424
|
+
private emit;
|
|
425
|
+
private checkConsistency;
|
|
426
|
+
/**
|
|
427
|
+
* Presentation rate limit — first check after decode, so a hammering payer
|
|
428
|
+
* is shed before any further work. Every presentation counts, including
|
|
429
|
+
* ones that would go on to fail consistency or screening.
|
|
430
|
+
*/
|
|
431
|
+
private checkRate;
|
|
432
|
+
/**
|
|
433
|
+
* Settled-spend velocity caps, derived from receipts at check time (restart
|
|
434
|
+
* -safe on durable stores). Runs BEFORE the replay burn on purpose: a
|
|
435
|
+
* velocity refusal is temporal, not a payment defect, so the same signed
|
|
436
|
+
* header may come back once the window clears.
|
|
437
|
+
*/
|
|
438
|
+
private checkVelocity;
|
|
439
|
+
/**
|
|
440
|
+
* Drive both rails legs under the retry policy (see GateRails for the error
|
|
441
|
+
* taxonomy). GateErrors are semantic verdicts and pass straight through.
|
|
442
|
+
* Transport failures:
|
|
443
|
+
*
|
|
444
|
+
* - verify (read-only): every transport failure is retried; exhausted ->
|
|
445
|
+
* `rails_unavailable`, and the slot is released (settle never ran).
|
|
446
|
+
* - settle: only RailsUnreachableError (provably never sent) is retried;
|
|
447
|
+
* exhausted -> `rails_unavailable` + release (still provably unsettled).
|
|
448
|
+
* Anything else is ambiguous — the money MAY have moved — so it refuses
|
|
449
|
+
* `settle_unknown` immediately and the slot STAYS burned: on real rails a
|
|
450
|
+
* re-present would misreport (nonce already used -> settle_failed), and on
|
|
451
|
+
* the mock ledger it would double-spend. Vendors reconcile ambiguous
|
|
452
|
+
* settles against transaction records (e.g. the on-chain indexer).
|
|
453
|
+
*/
|
|
454
|
+
private settleThroughRails;
|
|
455
|
+
private railsLeg;
|
|
456
|
+
private screenPayer;
|
|
457
|
+
/**
|
|
458
|
+
* Burn the replay slot. The store's check-and-set is sync at call time
|
|
459
|
+
* (concurrent copies cannot both pass); the await is the durability barrier
|
|
460
|
+
* — on a durable store the burn is on disk before verify/settle run, so a
|
|
461
|
+
* crash mid-settle cannot resurrect the slot on restart. A store WRITE
|
|
462
|
+
* failure is not a GateError: it escapes as a 500, because "we cannot
|
|
463
|
+
* guarantee replay protection" must never settle a payment.
|
|
464
|
+
*/
|
|
465
|
+
private burnReplay;
|
|
466
|
+
private refuse;
|
|
467
|
+
/** The v2 half of a dual-stack 402 (see advertiseV2), or nothing. */
|
|
468
|
+
private v2Quote;
|
|
469
|
+
}
|
|
470
|
+
declare function createGate(options: GateOptions): Gate;
|
|
471
|
+
|
|
472
|
+
/** The CAIP-2 id for a network name, or the lowercased name when unknown. */
|
|
473
|
+
declare function caip2Of(network: string): string;
|
|
474
|
+
/** Do two network ids name the same chain, across the v1/CAIP-2 divide? */
|
|
475
|
+
declare function sameNetwork(a: string, b: string): boolean;
|
|
476
|
+
/** x402 v2 PaymentRequirements (one entry of a 402's `accepts`). */
|
|
477
|
+
interface PaymentRequirementsV2 {
|
|
478
|
+
scheme: string;
|
|
479
|
+
/** CAIP-2, e.g. "eip155:84532". */
|
|
480
|
+
network: string;
|
|
481
|
+
/** Atomic units — v2's rename of v1's maxAmountRequired. */
|
|
482
|
+
amount: string;
|
|
483
|
+
asset: string;
|
|
484
|
+
payTo: string;
|
|
485
|
+
maxTimeoutSeconds?: number;
|
|
486
|
+
extra?: Record<string, unknown>;
|
|
487
|
+
}
|
|
488
|
+
/** v2 PaymentRequired — the decoded PAYMENT-REQUIRED header. */
|
|
489
|
+
interface PaymentRequiredV2 {
|
|
490
|
+
x402Version: 2;
|
|
491
|
+
error?: string;
|
|
492
|
+
resource?: {
|
|
493
|
+
url: string;
|
|
494
|
+
description?: string;
|
|
495
|
+
mimeType?: string;
|
|
496
|
+
};
|
|
497
|
+
accepts: PaymentRequirementsV2[];
|
|
498
|
+
extensions?: Record<string, unknown>;
|
|
499
|
+
}
|
|
500
|
+
/** Convert Rein's internal (v1-shaped) requirement to the v2 wire shape. */
|
|
501
|
+
declare function v2Requirements(requirement: PaymentRequirement): PaymentRequirementsV2;
|
|
502
|
+
/** The full v2 PaymentRequired for one quoted requirement. */
|
|
503
|
+
declare function buildPaymentRequiredV2(requirement: PaymentRequirement, error: string): PaymentRequiredV2;
|
|
504
|
+
/** The one codec every x402 header uses: base64 of compact JSON. */
|
|
505
|
+
declare function encodeBase64Json(value: unknown): string;
|
|
506
|
+
|
|
507
|
+
interface GateMiddlewareOptions {
|
|
508
|
+
/**
|
|
509
|
+
* Origin used to absolutize req.url for quotes, e.g. "https://api.vendor.com".
|
|
510
|
+
* Defaults to http://<Host header> — set this when serving behind TLS/proxies
|
|
511
|
+
* so quoted resources match what agents actually requested.
|
|
512
|
+
*/
|
|
513
|
+
origin?: string;
|
|
514
|
+
/** Called with every outcome — the vendor's observability hook. */
|
|
515
|
+
onOutcome?: (outcome: GateOutcome, req: IncomingMessage) => void;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Drop the gate in front of any Node HTTP handler. Express-compatible
|
|
519
|
+
* `(req, res, next)` signature; with a raw `node:http` server, pass your
|
|
520
|
+
* content handler as `next`:
|
|
521
|
+
*
|
|
522
|
+
* const paywall = gateMiddleware(gate);
|
|
523
|
+
* http.createServer((req, res) => paywall(req, res, () => serve(req, res)));
|
|
524
|
+
*
|
|
525
|
+
* Free and paid requests reach `next()` (paid ones with X-PAYMENT-RESPONSE
|
|
526
|
+
* already set on the response); quotes and refusals are answered here and
|
|
527
|
+
* never reach the vendor's handler.
|
|
528
|
+
*/
|
|
529
|
+
declare function gateMiddleware(gate: Gate, options?: GateMiddlewareOptions): (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
|
|
530
|
+
|
|
531
|
+
interface GatedFetchOptions {
|
|
532
|
+
/** Serves the actual content once the gate clears a request. */
|
|
533
|
+
serve?: (request: {
|
|
534
|
+
url: string;
|
|
535
|
+
method: string;
|
|
536
|
+
}) => Response | Promise<Response>;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* The gate as an in-process vendor fetch — a drop-in replacement for
|
|
540
|
+
* createMockVendor()/createRealVendor() anywhere a FetchLike vendor is
|
|
541
|
+
* composed (guard tests, demos, the console world), with the gate's pricing,
|
|
542
|
+
* screening, replay protection, and receipts in the loop.
|
|
543
|
+
*/
|
|
544
|
+
declare function createGatedFetch(gate: Gate, options?: GatedFetchOptions): FetchLike;
|
|
545
|
+
|
|
546
|
+
interface InspectedPayment {
|
|
547
|
+
/** Which wire dialect the payment arrived in. */
|
|
548
|
+
version: 1 | 2;
|
|
549
|
+
scheme: string;
|
|
550
|
+
network: string;
|
|
551
|
+
/** The paying wallet address (`from` in the transfer). */
|
|
552
|
+
payer: string;
|
|
553
|
+
to: string;
|
|
554
|
+
/** Atomic-unit amount. */
|
|
555
|
+
value: string;
|
|
556
|
+
/** The decoded envelope exactly as sent, for rails that re-verify it. */
|
|
557
|
+
envelope: unknown;
|
|
558
|
+
}
|
|
559
|
+
declare function inspectPaymentHeader(raw: string): InspectedPayment;
|
|
560
|
+
|
|
561
|
+
export { type FacilitatorClientLike, Gate, GateError, type GateLineStats, type GateMiddlewareOptions, type GateOptions, type GateOutcome, type GateRails, type GateRefusalCode, type GateRequest, type GateRoute, type GateScreen, type GateSettlement, type GateStats, type GateStorePort, type GateVelocity, type GatedFetchOptions, InMemoryGateStore, type InspectedPayment, type MaybePromise, type MockFacilitatorLike, type PaymentDefaults, type PaymentRequiredV2, type PaymentRequirementsV2, RailsUnreachableError, buildPaymentRequiredV2, caip2Of, createGate, createGatedFetch, encodeBase64Json, facilitatorClientRails, gateMiddleware, inspectPaymentHeader, matchRoute, mockFacilitatorRails, requirementFor, routeDecimals, sameNetwork, v2Requirements, validateVelocity };
|