@reinconsole/x402-rails 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 +35 -0
- package/dist/index.d.ts +507 -0
- package/dist/index.js +808 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
// src/wire.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var RailsError = class extends Error {
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = "RailsError";
|
|
10
|
+
}
|
|
11
|
+
code;
|
|
12
|
+
};
|
|
13
|
+
var FacilitatorHttpError = class extends Error {
|
|
14
|
+
constructor(status, body) {
|
|
15
|
+
super(`facilitator responded ${status}: ${body.slice(0, 500)}`);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.body = body;
|
|
18
|
+
this.name = "FacilitatorHttpError";
|
|
19
|
+
}
|
|
20
|
+
status;
|
|
21
|
+
body;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// src/wire.ts
|
|
25
|
+
var Hex = z.string().regex(/^0x[0-9a-fA-F]*$/, "must be 0x-prefixed hex");
|
|
26
|
+
var EvmAddress = z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a 20-byte EVM address");
|
|
27
|
+
var UintString = z.string().regex(/^\d+$/, "must be a decimal integer string");
|
|
28
|
+
var ExactEvmAuthorization = z.object({
|
|
29
|
+
from: EvmAddress,
|
|
30
|
+
to: EvmAddress,
|
|
31
|
+
/** Atomic-unit amount, mirroring the requirement's maxAmountRequired. */
|
|
32
|
+
value: UintString,
|
|
33
|
+
validAfter: UintString,
|
|
34
|
+
validBefore: UintString,
|
|
35
|
+
/** bytes32 — Rein derives it from the intent id (see nonce.ts). */
|
|
36
|
+
nonce: z.string().regex(/^0x[0-9a-fA-F]{64}$/, "nonce must be bytes32 hex")
|
|
37
|
+
});
|
|
38
|
+
var ExactEvmPayload = z.object({
|
|
39
|
+
/** 65-byte EIP-712 signature over the authorization. */
|
|
40
|
+
signature: Hex,
|
|
41
|
+
authorization: ExactEvmAuthorization
|
|
42
|
+
});
|
|
43
|
+
var PaymentPayload = z.object({
|
|
44
|
+
x402Version: z.literal(1),
|
|
45
|
+
scheme: z.string(),
|
|
46
|
+
network: z.string(),
|
|
47
|
+
payload: ExactEvmPayload
|
|
48
|
+
});
|
|
49
|
+
var VerifyResponse = z.object({
|
|
50
|
+
isValid: z.boolean(),
|
|
51
|
+
invalidReason: z.string().optional(),
|
|
52
|
+
payer: z.string().optional()
|
|
53
|
+
});
|
|
54
|
+
var SettleResponse = z.object({
|
|
55
|
+
success: z.boolean(),
|
|
56
|
+
errorReason: z.string().optional(),
|
|
57
|
+
payer: z.string().optional(),
|
|
58
|
+
/** The on-chain tx hash (the guard surfaces this on the receipt). */
|
|
59
|
+
transaction: z.string(),
|
|
60
|
+
network: z.string()
|
|
61
|
+
});
|
|
62
|
+
function encodePaymentHeader(payload) {
|
|
63
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64");
|
|
64
|
+
}
|
|
65
|
+
function decodePaymentHeader(raw) {
|
|
66
|
+
let json;
|
|
67
|
+
try {
|
|
68
|
+
json = JSON.parse(Buffer.from(raw, "base64").toString("utf8"));
|
|
69
|
+
} catch {
|
|
70
|
+
throw new RailsError("malformed_payment", "X-PAYMENT is not base64-encoded JSON");
|
|
71
|
+
}
|
|
72
|
+
const parsed = PaymentPayload.safeParse(json);
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
throw new RailsError("malformed_payment", `invalid X-PAYMENT body: ${parsed.error.message}`);
|
|
75
|
+
}
|
|
76
|
+
return parsed.data;
|
|
77
|
+
}
|
|
78
|
+
function encodeSettlementHeader(response) {
|
|
79
|
+
return Buffer.from(JSON.stringify(response)).toString("base64");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/nonce.ts
|
|
83
|
+
import { keccak256, stringToBytes } from "viem";
|
|
84
|
+
function intentNonce(intentId) {
|
|
85
|
+
return keccak256(stringToBytes(intentId));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/networks.ts
|
|
89
|
+
var NETWORK_TO_CHAIN_ID = {
|
|
90
|
+
base: 8453,
|
|
91
|
+
"eip155:8453": 8453,
|
|
92
|
+
"base-sepolia": 84532,
|
|
93
|
+
"eip155:84532": 84532
|
|
94
|
+
};
|
|
95
|
+
function chainIdForNetwork(network) {
|
|
96
|
+
return NETWORK_TO_CHAIN_ID[network.toLowerCase()];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/payer.ts
|
|
100
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
101
|
+
var transferWithAuthorizationTypes = {
|
|
102
|
+
TransferWithAuthorization: [
|
|
103
|
+
{ name: "from", type: "address" },
|
|
104
|
+
{ name: "to", type: "address" },
|
|
105
|
+
{ name: "value", type: "uint256" },
|
|
106
|
+
{ name: "validAfter", type: "uint256" },
|
|
107
|
+
{ name: "validBefore", type: "uint256" },
|
|
108
|
+
{ name: "nonce", type: "bytes32" }
|
|
109
|
+
]
|
|
110
|
+
};
|
|
111
|
+
function createX402Payer(options) {
|
|
112
|
+
const account = privateKeyToAccount(options.privateKey);
|
|
113
|
+
const skew = options.validAfterSkewSeconds ?? 600;
|
|
114
|
+
const defaultTimeout = options.defaultTimeoutSeconds ?? 300;
|
|
115
|
+
const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
116
|
+
return async (requirement, intent) => {
|
|
117
|
+
const chainId = chainIdForNetwork(requirement.network);
|
|
118
|
+
if (chainId === void 0) {
|
|
119
|
+
throw new RailsError(
|
|
120
|
+
"unsupported_network",
|
|
121
|
+
`cannot sign for x402 network "${requirement.network}"`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const ts = now();
|
|
125
|
+
const authorization = {
|
|
126
|
+
from: account.address,
|
|
127
|
+
to: requirement.payTo,
|
|
128
|
+
value: requirement.maxAmountRequired,
|
|
129
|
+
validAfter: String(ts - skew),
|
|
130
|
+
validBefore: String(ts + (requirement.maxTimeoutSeconds ?? defaultTimeout)),
|
|
131
|
+
nonce: intentNonce(intent.id)
|
|
132
|
+
};
|
|
133
|
+
const signature = await account.signTypedData({
|
|
134
|
+
domain: {
|
|
135
|
+
name: extraString(requirement, "name") ?? "USDC",
|
|
136
|
+
version: extraString(requirement, "version") ?? "2",
|
|
137
|
+
chainId,
|
|
138
|
+
verifyingContract: requirement.asset
|
|
139
|
+
},
|
|
140
|
+
types: transferWithAuthorizationTypes,
|
|
141
|
+
primaryType: "TransferWithAuthorization",
|
|
142
|
+
message: {
|
|
143
|
+
from: authorization.from,
|
|
144
|
+
to: authorization.to,
|
|
145
|
+
value: BigInt(authorization.value),
|
|
146
|
+
validAfter: BigInt(authorization.validAfter),
|
|
147
|
+
validBefore: BigInt(authorization.validBefore),
|
|
148
|
+
nonce: authorization.nonce
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return encodePaymentHeader({
|
|
152
|
+
x402Version: 1,
|
|
153
|
+
scheme: requirement.scheme,
|
|
154
|
+
network: requirement.network,
|
|
155
|
+
payload: { signature, authorization }
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function extraString(requirement, key) {
|
|
160
|
+
const value = requirement.extra?.[key];
|
|
161
|
+
return typeof value === "string" ? value : void 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/facilitator.ts
|
|
165
|
+
var DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
|
|
166
|
+
var FacilitatorClient = class {
|
|
167
|
+
url;
|
|
168
|
+
fetch;
|
|
169
|
+
constructor(options = {}) {
|
|
170
|
+
this.url = (options.url ?? DEFAULT_FACILITATOR_URL).replace(/\/$/, "");
|
|
171
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
172
|
+
this.fetch = (input, init) => f(input, init);
|
|
173
|
+
}
|
|
174
|
+
async verify(payload, requirements) {
|
|
175
|
+
return VerifyResponse.parse(await this.post("/verify", payload, requirements));
|
|
176
|
+
}
|
|
177
|
+
async settle(payload, requirements) {
|
|
178
|
+
return SettleResponse.parse(await this.post("/settle", payload, requirements));
|
|
179
|
+
}
|
|
180
|
+
/** The facilitator's advertised (x402Version, scheme, network) kinds. */
|
|
181
|
+
async supported() {
|
|
182
|
+
const res = await this.fetch(`${this.url}/supported`);
|
|
183
|
+
if (!res.ok) throw new FacilitatorHttpError(res.status, await res.text());
|
|
184
|
+
return res.json();
|
|
185
|
+
}
|
|
186
|
+
async post(path, paymentPayload, paymentRequirements) {
|
|
187
|
+
const claimed = paymentPayload.x402Version;
|
|
188
|
+
const x402Version = typeof claimed === "number" ? claimed : 1;
|
|
189
|
+
const res = await this.fetch(`${this.url}${path}`, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: { "content-type": "application/json" },
|
|
192
|
+
body: JSON.stringify({ x402Version, paymentPayload, paymentRequirements })
|
|
193
|
+
});
|
|
194
|
+
if (!res.ok) throw new FacilitatorHttpError(res.status, await res.text());
|
|
195
|
+
return res.json();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ../../packages/core/dist/index.js
|
|
200
|
+
import { z as z2 } from "zod";
|
|
201
|
+
var Chain = z2.enum(["base", "solana", "polygon", "bnb"]);
|
|
202
|
+
var Asset = z2.enum(["USDC", "USDT", "EURC"]);
|
|
203
|
+
var DecimalString = z2.string().regex(/^\d+(\.\d+)?$/, 'must be a non-negative decimal string, e.g. "5.00"');
|
|
204
|
+
var ULID_BODY = "[0-9A-HJKMNP-TV-Z]{26}";
|
|
205
|
+
function prefixedId(prefix) {
|
|
206
|
+
return z2.string().regex(new RegExp(`^${prefix}_${ULID_BODY}$`), `expected a "${prefix}_" prefixed id`);
|
|
207
|
+
}
|
|
208
|
+
var OrgId = prefixedId("org");
|
|
209
|
+
var AgentId = prefixedId("agt");
|
|
210
|
+
var PolicyId = prefixedId("pol");
|
|
211
|
+
var IntentId = prefixedId("int");
|
|
212
|
+
var DecisionId = prefixedId("dec");
|
|
213
|
+
var ReceiptId = prefixedId("rcp");
|
|
214
|
+
var SessionId = prefixedId("ses");
|
|
215
|
+
var GateReceiptId = prefixedId("grc");
|
|
216
|
+
var EnforcementMode = z2.enum(["observed", "sdk", "session-key"]);
|
|
217
|
+
var AgentWallet = z2.object({
|
|
218
|
+
chain: Chain,
|
|
219
|
+
address: z2.string().min(1),
|
|
220
|
+
mode: EnforcementMode
|
|
221
|
+
});
|
|
222
|
+
var AgentStatus = z2.enum(["active", "frozen"]);
|
|
223
|
+
var Agent = z2.object({
|
|
224
|
+
id: AgentId,
|
|
225
|
+
orgId: OrgId,
|
|
226
|
+
name: z2.string().min(1).max(200),
|
|
227
|
+
/** On-chain ERC-8004 identity, if the agent is registered. */
|
|
228
|
+
erc8004Id: z2.string().optional(),
|
|
229
|
+
wallets: z2.array(AgentWallet).default([]),
|
|
230
|
+
status: AgentStatus.default("active"),
|
|
231
|
+
createdAt: z2.coerce.date()
|
|
232
|
+
});
|
|
233
|
+
var ID_PATTERN = /^eip155:(\d+):(0x[0-9a-fA-F]{40})\/(\d+)$/;
|
|
234
|
+
function parseErc8004Id(value) {
|
|
235
|
+
const match = ID_PATTERN.exec(value);
|
|
236
|
+
if (!match) return void 0;
|
|
237
|
+
const chainId = Number(match[1]);
|
|
238
|
+
if (!Number.isSafeInteger(chainId) || chainId < 1) return void 0;
|
|
239
|
+
return {
|
|
240
|
+
chainId,
|
|
241
|
+
registry: match[2].toLowerCase(),
|
|
242
|
+
tokenId: BigInt(match[3])
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
var Erc8004Id = z2.string().refine((value) => parseErc8004Id(value) !== void 0, {
|
|
246
|
+
message: 'expected "eip155:{chainId}:{registry}/{tokenId}"'
|
|
247
|
+
});
|
|
248
|
+
var Vendor = z2.object({
|
|
249
|
+
host: z2.string().min(1),
|
|
250
|
+
address: z2.string().min(1),
|
|
251
|
+
erc8004Id: z2.string().optional()
|
|
252
|
+
});
|
|
253
|
+
var TaskContext = z2.object({
|
|
254
|
+
taskId: z2.string().optional(),
|
|
255
|
+
parentRunId: z2.string().optional(),
|
|
256
|
+
purpose: z2.string().max(500).optional()
|
|
257
|
+
});
|
|
258
|
+
var PaymentIntent = z2.object({
|
|
259
|
+
id: IntentId,
|
|
260
|
+
agentId: AgentId,
|
|
261
|
+
vendor: Vendor,
|
|
262
|
+
resource: z2.string(),
|
|
263
|
+
amount: DecimalString,
|
|
264
|
+
asset: Asset,
|
|
265
|
+
chain: Chain,
|
|
266
|
+
taskContext: TaskContext.default({}),
|
|
267
|
+
nonce: z2.string().min(1),
|
|
268
|
+
createdAt: z2.coerce.date()
|
|
269
|
+
});
|
|
270
|
+
var DecisionOutcome = z2.enum(["allow", "deny", "escalate"]);
|
|
271
|
+
var Decision = z2.object({
|
|
272
|
+
id: DecisionId,
|
|
273
|
+
intentId: IntentId,
|
|
274
|
+
/**
|
|
275
|
+
* sha256 of the intent's canonical content (see canonical.ts). Binds the
|
|
276
|
+
* decision to the exact transfer it judged — amount, recipient, asset,
|
|
277
|
+
* chain — so a signer can verify an {intent, decision} pair offline as a
|
|
278
|
+
* self-contained spend voucher, not just a reference by id.
|
|
279
|
+
*/
|
|
280
|
+
intentHash: z2.string(),
|
|
281
|
+
outcome: DecisionOutcome,
|
|
282
|
+
/** Ids of the rules that fired, for explainability. */
|
|
283
|
+
matchedRules: z2.array(z2.string()).default([]),
|
|
284
|
+
/** Human-readable explanation surfaced in the dashboard. */
|
|
285
|
+
reason: z2.string().optional(),
|
|
286
|
+
policyId: z2.string(),
|
|
287
|
+
policyVersion: z2.string(),
|
|
288
|
+
/** Hash of the previous decision in the chain (tamper-evident log). */
|
|
289
|
+
prevHash: z2.string(),
|
|
290
|
+
/** Hash of this decision's canonical content. */
|
|
291
|
+
hash: z2.string(),
|
|
292
|
+
/** Service signing-key signature over `hash`. */
|
|
293
|
+
signature: z2.string(),
|
|
294
|
+
latencyMs: z2.number().nonnegative(),
|
|
295
|
+
decidedAt: z2.coerce.date()
|
|
296
|
+
});
|
|
297
|
+
var Session = z2.object({
|
|
298
|
+
id: SessionId,
|
|
299
|
+
agentId: AgentId,
|
|
300
|
+
/** sha256 hex of the bearer token. */
|
|
301
|
+
tokenHash: z2.string(),
|
|
302
|
+
/** Cumulative ceiling across the session's lifetime. Absent = uncapped. */
|
|
303
|
+
capAmount: DecimalString.optional(),
|
|
304
|
+
/** Ceiling per individual signature. Absent = uncapped. */
|
|
305
|
+
maxPerPayment: DecimalString.optional(),
|
|
306
|
+
expiresAt: z2.coerce.date(),
|
|
307
|
+
createdAt: z2.coerce.date(),
|
|
308
|
+
revokedAt: z2.coerce.date().optional()
|
|
309
|
+
});
|
|
310
|
+
var SettledPayment = z2.object({
|
|
311
|
+
intentId: IntentId,
|
|
312
|
+
txHash: z2.string(),
|
|
313
|
+
chain: Chain,
|
|
314
|
+
blockNumber: z2.coerce.bigint(),
|
|
315
|
+
facilitator: z2.string().optional(),
|
|
316
|
+
feePaid: DecimalString.optional(),
|
|
317
|
+
confirmedAt: z2.coerce.date()
|
|
318
|
+
});
|
|
319
|
+
var ReceiptSettlement = z2.object({
|
|
320
|
+
txHash: z2.string().optional(),
|
|
321
|
+
networkId: z2.string().optional(),
|
|
322
|
+
/** Raw header payload, kept for forensics until the indexer confirms. */
|
|
323
|
+
raw: z2.string().optional()
|
|
324
|
+
});
|
|
325
|
+
var Receipt = z2.object({
|
|
326
|
+
id: ReceiptId,
|
|
327
|
+
agentId: AgentId,
|
|
328
|
+
intentId: IntentId,
|
|
329
|
+
decisionId: DecisionId,
|
|
330
|
+
outcome: DecisionOutcome,
|
|
331
|
+
/** The URL the agent actually fetched (the paywalled resource). */
|
|
332
|
+
url: z2.string(),
|
|
333
|
+
method: z2.string().default("GET"),
|
|
334
|
+
vendorHost: z2.string(),
|
|
335
|
+
amount: DecimalString,
|
|
336
|
+
asset: Asset,
|
|
337
|
+
chain: Chain,
|
|
338
|
+
taskContext: TaskContext.default({}),
|
|
339
|
+
/** Human-readable explanation copied from the decision. */
|
|
340
|
+
reason: z2.string().optional(),
|
|
341
|
+
/** Present only once a payment was made and the vendor confirmed it. */
|
|
342
|
+
settlement: ReceiptSettlement.optional(),
|
|
343
|
+
createdAt: z2.coerce.date()
|
|
344
|
+
});
|
|
345
|
+
var GateReceipt = z2.object({
|
|
346
|
+
id: GateReceiptId,
|
|
347
|
+
at: z2.coerce.date(),
|
|
348
|
+
/** The route pattern that priced this request, e.g. "/api/reports/*". */
|
|
349
|
+
route: z2.string().min(1),
|
|
350
|
+
/** The concrete resource paid for (URL path actually requested). */
|
|
351
|
+
resource: z2.string().min(1),
|
|
352
|
+
method: z2.string().min(1),
|
|
353
|
+
/** The paying wallet address, as asserted by the settled payment. */
|
|
354
|
+
payer: z2.string().min(1),
|
|
355
|
+
payTo: z2.string().min(1),
|
|
356
|
+
/** Human-unit decimal amount, e.g. "0.05". */
|
|
357
|
+
amount: DecimalString,
|
|
358
|
+
/** The same amount in the asset's atomic units, e.g. "50000". */
|
|
359
|
+
amountAtomic: z2.string().regex(/^\d+$/, "atomic amount must be an integer string"),
|
|
360
|
+
/** Token symbol or contract address, exactly as quoted in the requirement. */
|
|
361
|
+
asset: z2.string().min(1),
|
|
362
|
+
network: z2.string().min(1),
|
|
363
|
+
/** Settlement transaction hash (mock ledger or on-chain). */
|
|
364
|
+
transaction: z2.string().min(1)
|
|
365
|
+
});
|
|
366
|
+
var Window = z2.string().regex(/^\d+[smhd]$/, 'window must be like "30s", "15m", "1h", or "7d"');
|
|
367
|
+
var Multiplier = z2.string().regex(/^\d+(\.\d+)?x$/, 'multiplier must be like "3x"');
|
|
368
|
+
var Condition = z2.object({
|
|
369
|
+
/** Per-transaction amount exceeds this value. */
|
|
370
|
+
amountGt: DecimalString.optional(),
|
|
371
|
+
/** Sum of spend in a rolling window exceeds `gt`. */
|
|
372
|
+
rollingSum: z2.object({ window: Window, gt: DecimalString }).optional(),
|
|
373
|
+
/** Transaction count in a rolling window exceeds `gt`. */
|
|
374
|
+
txCount: z2.object({ window: Window, gt: z2.number().int().nonnegative() }).optional(),
|
|
375
|
+
/** Vendor host matches one of these patterns (supports `*` globs). */
|
|
376
|
+
vendorHostIn: z2.array(z2.string()).optional(),
|
|
377
|
+
/** First time Rein has seen this vendor for the agent. */
|
|
378
|
+
vendorFirstSeen: z2.boolean().optional(),
|
|
379
|
+
/** Vendor reputation score is below this threshold (Phase 3 hook). */
|
|
380
|
+
vendorReputationLt: z2.number().min(0).max(100).optional(),
|
|
381
|
+
/** Amount is more than `gt`-times the observed median for this resource. */
|
|
382
|
+
amountVsResourceMedian: z2.object({ gt: Multiplier }).optional()
|
|
383
|
+
}).refine((c) => Object.values(c).some((v) => v !== void 0), {
|
|
384
|
+
message: "a condition must specify at least one predicate"
|
|
385
|
+
});
|
|
386
|
+
var Rule = z2.object({
|
|
387
|
+
id: z2.string().min(1),
|
|
388
|
+
allow: Condition.optional(),
|
|
389
|
+
deny: Condition.optional(),
|
|
390
|
+
escalate: Condition.optional()
|
|
391
|
+
}).refine((r) => [r.allow, r.deny, r.escalate].filter((v) => v !== void 0).length === 1, {
|
|
392
|
+
message: "a rule must specify exactly one of allow / deny / escalate"
|
|
393
|
+
});
|
|
394
|
+
var PolicyDefault = z2.enum(["allow", "deny"]);
|
|
395
|
+
var AppliesTo = z2.object({
|
|
396
|
+
/** Agent id patterns (supports `*` globs, e.g. "agt_research_*"). */
|
|
397
|
+
agents: z2.array(z2.string()).optional(),
|
|
398
|
+
chains: z2.array(Chain).optional()
|
|
399
|
+
});
|
|
400
|
+
var Escalation = z2.object({
|
|
401
|
+
approvers: z2.array(z2.string()).default([]),
|
|
402
|
+
timeoutAction: PolicyDefault.default("deny"),
|
|
403
|
+
timeoutMin: z2.number().int().positive().default(15)
|
|
404
|
+
});
|
|
405
|
+
var Policy = z2.object({
|
|
406
|
+
policyId: z2.string().min(1),
|
|
407
|
+
version: z2.string().default("1"),
|
|
408
|
+
appliesTo: AppliesTo.default({}),
|
|
409
|
+
rules: z2.array(Rule).default([]),
|
|
410
|
+
default: PolicyDefault.default("deny"),
|
|
411
|
+
/** Below this amount, fail-open is permitted during a policy-service outage. */
|
|
412
|
+
denyFloor: DecimalString.default("0.05"),
|
|
413
|
+
escalation: Escalation.optional()
|
|
414
|
+
});
|
|
415
|
+
var ReputationSubject = z2.object({
|
|
416
|
+
kind: z2.enum(["agent", "vendor"]),
|
|
417
|
+
id: z2.string()
|
|
418
|
+
});
|
|
419
|
+
var ReputationComponents = z2.object({
|
|
420
|
+
volume: z2.number(),
|
|
421
|
+
longevity: z2.number(),
|
|
422
|
+
disputeRate: z2.number(),
|
|
423
|
+
counterpartyQuality: z2.number(),
|
|
424
|
+
settlementReliability: z2.number()
|
|
425
|
+
});
|
|
426
|
+
var ReputationScore = z2.object({
|
|
427
|
+
subject: ReputationSubject,
|
|
428
|
+
score: z2.number().min(0).max(100),
|
|
429
|
+
components: ReputationComponents,
|
|
430
|
+
confidence: z2.number().min(0).max(1),
|
|
431
|
+
asOf: z2.coerce.date(),
|
|
432
|
+
/** Hash-anchored attestation, optionally published on-chain (ERC-8004). */
|
|
433
|
+
evidenceUri: z2.string().optional()
|
|
434
|
+
});
|
|
435
|
+
var ReinEvent = z2.discriminatedUnion("type", [
|
|
436
|
+
z2.object({ type: z2.literal("intent.created"), at: z2.coerce.date(), intent: PaymentIntent }),
|
|
437
|
+
z2.object({ type: z2.literal("decision.made"), at: z2.coerce.date(), decision: Decision }),
|
|
438
|
+
z2.object({ type: z2.literal("payment.settled"), at: z2.coerce.date(), payment: SettledPayment }),
|
|
439
|
+
z2.object({
|
|
440
|
+
type: z2.literal("shadow.spend"),
|
|
441
|
+
at: z2.coerce.date(),
|
|
442
|
+
agentId: AgentId,
|
|
443
|
+
txHash: z2.string(),
|
|
444
|
+
chain: Chain,
|
|
445
|
+
amount: DecimalString
|
|
446
|
+
}),
|
|
447
|
+
z2.object({
|
|
448
|
+
type: z2.literal("signature.released"),
|
|
449
|
+
at: z2.coerce.date(),
|
|
450
|
+
sessionId: SessionId,
|
|
451
|
+
agentId: AgentId,
|
|
452
|
+
intentId: IntentId,
|
|
453
|
+
decisionId: DecisionId,
|
|
454
|
+
amount: DecimalString
|
|
455
|
+
}),
|
|
456
|
+
z2.object({
|
|
457
|
+
type: z2.literal("signature.refused"),
|
|
458
|
+
at: z2.coerce.date(),
|
|
459
|
+
/** Refusal code, e.g. "decision_replayed" (see @reinconsole/signer). */
|
|
460
|
+
code: z2.string(),
|
|
461
|
+
reason: z2.string(),
|
|
462
|
+
sessionId: SessionId.optional(),
|
|
463
|
+
agentId: AgentId.optional(),
|
|
464
|
+
intentId: IntentId.optional()
|
|
465
|
+
}),
|
|
466
|
+
z2.object({
|
|
467
|
+
type: z2.literal("gate.quoted"),
|
|
468
|
+
at: z2.coerce.date(),
|
|
469
|
+
resource: z2.string(),
|
|
470
|
+
method: z2.string(),
|
|
471
|
+
amount: DecimalString,
|
|
472
|
+
asset: z2.string(),
|
|
473
|
+
network: z2.string()
|
|
474
|
+
}),
|
|
475
|
+
z2.object({ type: z2.literal("gate.settled"), at: z2.coerce.date(), receipt: GateReceipt }),
|
|
476
|
+
z2.object({
|
|
477
|
+
type: z2.literal("gate.refused"),
|
|
478
|
+
at: z2.coerce.date(),
|
|
479
|
+
/** Refusal code, e.g. "payment_replayed" (see @reinconsole/gate). */
|
|
480
|
+
code: z2.string(),
|
|
481
|
+
reason: z2.string(),
|
|
482
|
+
resource: z2.string(),
|
|
483
|
+
/** Known only when the payment header decoded far enough to name a payer. */
|
|
484
|
+
payer: z2.string().optional()
|
|
485
|
+
})
|
|
486
|
+
]);
|
|
487
|
+
|
|
488
|
+
// ../../packages/sdk/dist/index.js
|
|
489
|
+
import { z as z3 } from "zod";
|
|
490
|
+
var Health = z3.object({ status: z3.string(), publicKey: z3.string() });
|
|
491
|
+
var EvaluateResponse = z3.object({ intent: PaymentIntent, decision: Decision });
|
|
492
|
+
var PaymentRequirement = z3.object({
|
|
493
|
+
scheme: z3.string(),
|
|
494
|
+
network: z3.string(),
|
|
495
|
+
/** Amount in the asset's atomic units (e.g. "10000" = 0.01 USDC at 6 decimals). */
|
|
496
|
+
maxAmountRequired: z3.string().regex(/^\d+$/, "atomic amount must be an integer string"),
|
|
497
|
+
resource: z3.string().optional(),
|
|
498
|
+
description: z3.string().optional(),
|
|
499
|
+
mimeType: z3.string().optional(),
|
|
500
|
+
payTo: z3.string().min(1),
|
|
501
|
+
maxTimeoutSeconds: z3.number().optional(),
|
|
502
|
+
/** Token contract address — or, mock-first, a plain symbol like "USDC". */
|
|
503
|
+
asset: z3.string().min(1),
|
|
504
|
+
extra: z3.record(z3.unknown()).optional()
|
|
505
|
+
});
|
|
506
|
+
var PaymentRequired = z3.object({
|
|
507
|
+
x402Version: z3.number(),
|
|
508
|
+
accepts: z3.array(PaymentRequirement).min(1),
|
|
509
|
+
error: z3.string().optional()
|
|
510
|
+
});
|
|
511
|
+
function atomicToDecimal(atomic, decimals) {
|
|
512
|
+
if (!/^\d+$/.test(atomic)) throw new TypeError(`invalid atomic amount: ${atomic}`);
|
|
513
|
+
if (decimals === 0) return atomic.replace(/^0+(?=\d)/, "");
|
|
514
|
+
const digits = atomic.padStart(decimals + 1, "0");
|
|
515
|
+
const intPart = digits.slice(0, digits.length - decimals).replace(/^0+(?=\d)/, "");
|
|
516
|
+
const fracPart = digits.slice(digits.length - decimals).replace(/0+$/, "");
|
|
517
|
+
return fracPart.length > 0 ? `${intPart}.${fracPart}` : intPart;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/wallet.ts
|
|
521
|
+
import {
|
|
522
|
+
createPublicClient,
|
|
523
|
+
erc20Abi,
|
|
524
|
+
http
|
|
525
|
+
} from "viem";
|
|
526
|
+
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
527
|
+
import { baseSepolia } from "viem/chains";
|
|
528
|
+
var BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
529
|
+
var CIRCLE_FAUCET_URL = "https://faucet.circle.com";
|
|
530
|
+
var basescanTxUrl = (txHash) => `https://sepolia.basescan.org/tx/${txHash}`;
|
|
531
|
+
function generateWallet() {
|
|
532
|
+
const privateKey = generatePrivateKey();
|
|
533
|
+
return { privateKey, address: privateKeyToAccount2(privateKey).address };
|
|
534
|
+
}
|
|
535
|
+
function createBaseSepoliaClient(rpcUrl) {
|
|
536
|
+
return createPublicClient({ chain: baseSepolia, transport: http(rpcUrl) });
|
|
537
|
+
}
|
|
538
|
+
async function getUsdcBalance(client, address, usdc = BASE_SEPOLIA_USDC) {
|
|
539
|
+
return client.readContract({
|
|
540
|
+
address: usdc,
|
|
541
|
+
abi: erc20Abi,
|
|
542
|
+
functionName: "balanceOf",
|
|
543
|
+
args: [address]
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/vendor.ts
|
|
548
|
+
function createRealVendor(options) {
|
|
549
|
+
const calls = [];
|
|
550
|
+
const requirementFor = (url) => PaymentRequirement.parse({
|
|
551
|
+
scheme: "exact",
|
|
552
|
+
network: options.network ?? "base-sepolia",
|
|
553
|
+
maxAmountRequired: options.atomicPrice,
|
|
554
|
+
resource: url,
|
|
555
|
+
description: options.description ?? "",
|
|
556
|
+
mimeType: "application/json",
|
|
557
|
+
payTo: options.payTo,
|
|
558
|
+
maxTimeoutSeconds: options.maxTimeoutSeconds ?? 300,
|
|
559
|
+
asset: options.asset ?? BASE_SEPOLIA_USDC,
|
|
560
|
+
extra: options.extra ?? { name: "USDC", version: "2" }
|
|
561
|
+
});
|
|
562
|
+
const paymentRequired = (url, error) => new Response(JSON.stringify({ x402Version: 1, accepts: [requirementFor(url)], error }), {
|
|
563
|
+
status: 402,
|
|
564
|
+
headers: { "content-type": "application/json" }
|
|
565
|
+
});
|
|
566
|
+
const fetch = async (input, init) => {
|
|
567
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
568
|
+
const headers = new Headers(
|
|
569
|
+
init?.headers ?? (input instanceof Request ? input.headers : void 0)
|
|
570
|
+
);
|
|
571
|
+
const payment = headers.get("X-PAYMENT");
|
|
572
|
+
calls.push({ url, payment });
|
|
573
|
+
if (payment === null) return paymentRequired(url, "X-PAYMENT header is required");
|
|
574
|
+
const requirement = requirementFor(url);
|
|
575
|
+
let payload;
|
|
576
|
+
try {
|
|
577
|
+
payload = decodePaymentHeader(payment);
|
|
578
|
+
} catch (err) {
|
|
579
|
+
if (err instanceof RailsError) return paymentRequired(url, err.message);
|
|
580
|
+
throw err;
|
|
581
|
+
}
|
|
582
|
+
const verified = await options.facilitator.verify(payload, requirement);
|
|
583
|
+
if (!verified.isValid) {
|
|
584
|
+
return paymentRequired(url, verified.invalidReason ?? "payment verification failed");
|
|
585
|
+
}
|
|
586
|
+
const settled = await options.facilitator.settle(payload, requirement);
|
|
587
|
+
if (!settled.success) {
|
|
588
|
+
return paymentRequired(url, settled.errorReason ?? "payment settlement failed");
|
|
589
|
+
}
|
|
590
|
+
return new Response(JSON.stringify(options.body ?? { ok: true }), {
|
|
591
|
+
status: 200,
|
|
592
|
+
headers: {
|
|
593
|
+
"content-type": "application/json",
|
|
594
|
+
"X-PAYMENT-RESPONSE": encodeSettlementHeader(settled)
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
return { fetch, calls, requirementFor };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/indexer.ts
|
|
602
|
+
import { EventEmitter } from "events";
|
|
603
|
+
import { parseAbi } from "viem";
|
|
604
|
+
var railEventsAbi = parseAbi([
|
|
605
|
+
"event Transfer(address indexed from, address indexed to, uint256 value)",
|
|
606
|
+
"event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)"
|
|
607
|
+
]);
|
|
608
|
+
var OnchainIndexer = class {
|
|
609
|
+
options;
|
|
610
|
+
bus = new EventEmitter();
|
|
611
|
+
emitted = [];
|
|
612
|
+
/** Every intent the engine has seen, by id (from `intent.created`). */
|
|
613
|
+
intents = /* @__PURE__ */ new Map();
|
|
614
|
+
/** Intents with an ALLOW decision, by id. */
|
|
615
|
+
allowed = /* @__PURE__ */ new Map();
|
|
616
|
+
settledIntents = /* @__PURE__ */ new Set();
|
|
617
|
+
/** Expected on-chain nonce -> intent id, for every allowed intent. */
|
|
618
|
+
nonceToIntent = /* @__PURE__ */ new Map();
|
|
619
|
+
timer;
|
|
620
|
+
nextBlock;
|
|
621
|
+
scanning = false;
|
|
622
|
+
constructor(options) {
|
|
623
|
+
this.options = options;
|
|
624
|
+
}
|
|
625
|
+
/** Subscribe to a policy engine's event stream to learn which intents were allowed. */
|
|
626
|
+
connectEngine(engine) {
|
|
627
|
+
engine.onEvent((event) => {
|
|
628
|
+
if (event.type === "intent.created") {
|
|
629
|
+
this.intents.set(event.intent.id, event.intent);
|
|
630
|
+
} else if (event.type === "decision.made" && event.decision.outcome === "allow") {
|
|
631
|
+
const intent = this.intents.get(event.decision.intentId);
|
|
632
|
+
if (intent) {
|
|
633
|
+
this.allowed.set(intent.id, intent);
|
|
634
|
+
this.nonceToIntent.set(intentNonce(intent.id).toLowerCase(), intent.id);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
/** Begin polling. Scans from `fromBlock` (default: the current head). */
|
|
640
|
+
async start() {
|
|
641
|
+
if (this.timer !== void 0) return;
|
|
642
|
+
this.nextBlock = this.options.fromBlock ?? await this.options.client.getBlockNumber() + 1n;
|
|
643
|
+
const interval = this.options.pollIntervalMs ?? 3e3;
|
|
644
|
+
this.timer = setInterval(() => void this.scan(), interval);
|
|
645
|
+
this.timer.unref?.();
|
|
646
|
+
}
|
|
647
|
+
stop() {
|
|
648
|
+
if (this.timer !== void 0) clearInterval(this.timer);
|
|
649
|
+
this.timer = void 0;
|
|
650
|
+
}
|
|
651
|
+
onEvent(handler) {
|
|
652
|
+
this.bus.on("event", handler);
|
|
653
|
+
}
|
|
654
|
+
/** Everything the indexer has emitted, oldest first. */
|
|
655
|
+
events() {
|
|
656
|
+
return this.emitted;
|
|
657
|
+
}
|
|
658
|
+
settledPayments() {
|
|
659
|
+
return this.emitted.flatMap((e) => e.type === "payment.settled" ? [e.payment] : []);
|
|
660
|
+
}
|
|
661
|
+
shadowSpends() {
|
|
662
|
+
return this.emitted.filter((e) => e.type === "shadow.spend");
|
|
663
|
+
}
|
|
664
|
+
/** Resolve when an event (past or future) matches; reject on timeout. */
|
|
665
|
+
waitFor(predicate, timeoutMs = 9e4) {
|
|
666
|
+
const existing = this.emitted.find(predicate);
|
|
667
|
+
if (existing) return Promise.resolve(existing);
|
|
668
|
+
return new Promise((resolve, reject) => {
|
|
669
|
+
const timeout = setTimeout(() => {
|
|
670
|
+
this.bus.off("event", handler);
|
|
671
|
+
reject(new Error(`indexer: no matching event within ${timeoutMs}ms`));
|
|
672
|
+
}, timeoutMs);
|
|
673
|
+
const handler = (event) => {
|
|
674
|
+
if (!predicate(event)) return;
|
|
675
|
+
clearTimeout(timeout);
|
|
676
|
+
this.bus.off("event", handler);
|
|
677
|
+
resolve(event);
|
|
678
|
+
};
|
|
679
|
+
this.bus.on("event", handler);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
/** One poll: fetch logs since the last scanned block and classify them. */
|
|
683
|
+
async scan() {
|
|
684
|
+
if (this.scanning || this.nextBlock === void 0) return;
|
|
685
|
+
this.scanning = true;
|
|
686
|
+
try {
|
|
687
|
+
const head = await this.options.client.getBlockNumber();
|
|
688
|
+
if (head < this.nextBlock) return;
|
|
689
|
+
const logs = await this.options.client.getLogs({
|
|
690
|
+
address: this.options.usdcAddress ?? BASE_SEPOLIA_USDC,
|
|
691
|
+
events: railEventsAbi,
|
|
692
|
+
fromBlock: this.nextBlock,
|
|
693
|
+
toBlock: head
|
|
694
|
+
});
|
|
695
|
+
for (const tx of groupByTx(logs)) this.observe(tx);
|
|
696
|
+
this.nextBlock = head + 1n;
|
|
697
|
+
} catch (err) {
|
|
698
|
+
this.options.onError?.(err);
|
|
699
|
+
} finally {
|
|
700
|
+
this.scanning = false;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
emit(event) {
|
|
704
|
+
const parsed = ReinEvent.parse(event);
|
|
705
|
+
this.emitted.push(parsed);
|
|
706
|
+
this.bus.emit("event", parsed);
|
|
707
|
+
}
|
|
708
|
+
agentFor(from) {
|
|
709
|
+
const chain = this.options.chain ?? "base";
|
|
710
|
+
for (const agent of this.options.agents()) {
|
|
711
|
+
const owns = agent.wallets.some((w) => w.chain === chain && sameAddress(w.address, from));
|
|
712
|
+
if (owns) return agent.id;
|
|
713
|
+
}
|
|
714
|
+
return void 0;
|
|
715
|
+
}
|
|
716
|
+
observe(tx) {
|
|
717
|
+
for (const transfer of tx.transfers) {
|
|
718
|
+
const from = transfer.args.from;
|
|
719
|
+
if (from === void 0 || transfer.args.value === void 0) continue;
|
|
720
|
+
const agentId = this.agentFor(from);
|
|
721
|
+
if (agentId === void 0) continue;
|
|
722
|
+
const intent = this.reconcile(tx, from, agentId);
|
|
723
|
+
const at = /* @__PURE__ */ new Date();
|
|
724
|
+
if (intent) {
|
|
725
|
+
this.settledIntents.add(intent.id);
|
|
726
|
+
this.emit({
|
|
727
|
+
type: "payment.settled",
|
|
728
|
+
at,
|
|
729
|
+
payment: {
|
|
730
|
+
intentId: intent.id,
|
|
731
|
+
txHash: tx.txHash,
|
|
732
|
+
chain: this.options.chain ?? "base",
|
|
733
|
+
blockNumber: transfer.blockNumber ?? 0n,
|
|
734
|
+
facilitator: this.options.facilitator,
|
|
735
|
+
confirmedAt: at
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
this.emit({
|
|
741
|
+
type: "shadow.spend",
|
|
742
|
+
at,
|
|
743
|
+
agentId,
|
|
744
|
+
txHash: tx.txHash,
|
|
745
|
+
chain: this.options.chain ?? "base",
|
|
746
|
+
amount: atomicToDecimal(transfer.args.value.toString(), this.options.decimals ?? 6)
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
reconcile(tx, from, agentId) {
|
|
751
|
+
for (const auth of tx.authorizations) {
|
|
752
|
+
const { authorizer, nonce } = auth.args;
|
|
753
|
+
if (authorizer === void 0 || nonce === void 0) continue;
|
|
754
|
+
if (!sameAddress(authorizer, from)) continue;
|
|
755
|
+
const intentId = this.nonceToIntent.get(nonce.toLowerCase());
|
|
756
|
+
if (intentId === void 0) continue;
|
|
757
|
+
const intent = this.allowed.get(intentId);
|
|
758
|
+
if (intent && intent.agentId === agentId && !this.settledIntents.has(intent.id)) {
|
|
759
|
+
return intent;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return void 0;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
function groupByTx(logs) {
|
|
766
|
+
const byTx = /* @__PURE__ */ new Map();
|
|
767
|
+
for (const log of logs) {
|
|
768
|
+
if (log.transactionHash === null) continue;
|
|
769
|
+
let tx = byTx.get(log.transactionHash);
|
|
770
|
+
if (!tx) {
|
|
771
|
+
tx = { txHash: log.transactionHash, transfers: [], authorizations: [] };
|
|
772
|
+
byTx.set(log.transactionHash, tx);
|
|
773
|
+
}
|
|
774
|
+
(log.eventName === "Transfer" ? tx.transfers : tx.authorizations).push(log);
|
|
775
|
+
}
|
|
776
|
+
return [...byTx.values()];
|
|
777
|
+
}
|
|
778
|
+
function sameAddress(a, b) {
|
|
779
|
+
return a === b || a.toLowerCase() === b.toLowerCase();
|
|
780
|
+
}
|
|
781
|
+
export {
|
|
782
|
+
BASE_SEPOLIA_USDC,
|
|
783
|
+
CIRCLE_FAUCET_URL,
|
|
784
|
+
DEFAULT_FACILITATOR_URL,
|
|
785
|
+
ExactEvmAuthorization,
|
|
786
|
+
ExactEvmPayload,
|
|
787
|
+
FacilitatorClient,
|
|
788
|
+
FacilitatorHttpError,
|
|
789
|
+
OnchainIndexer,
|
|
790
|
+
PaymentPayload,
|
|
791
|
+
RailsError,
|
|
792
|
+
SettleResponse,
|
|
793
|
+
VerifyResponse,
|
|
794
|
+
basescanTxUrl,
|
|
795
|
+
chainIdForNetwork,
|
|
796
|
+
createBaseSepoliaClient,
|
|
797
|
+
createRealVendor,
|
|
798
|
+
createX402Payer,
|
|
799
|
+
decodePaymentHeader,
|
|
800
|
+
encodePaymentHeader,
|
|
801
|
+
encodeSettlementHeader,
|
|
802
|
+
generateWallet,
|
|
803
|
+
getUsdcBalance,
|
|
804
|
+
intentNonce,
|
|
805
|
+
railEventsAbi,
|
|
806
|
+
transferWithAuthorizationTypes
|
|
807
|
+
};
|
|
808
|
+
//# sourceMappingURL=index.js.map
|