mrmainspring 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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/audit/service.d.ts +7 -0
  4. package/dist/audit/service.js +98 -0
  5. package/dist/audit/store.d.ts +7 -0
  6. package/dist/audit/store.js +37 -0
  7. package/dist/audit/supabase-store.d.ts +9 -0
  8. package/dist/audit/supabase-store.js +22 -0
  9. package/dist/audit/types.d.ts +31 -0
  10. package/dist/audit/types.js +1 -0
  11. package/dist/casper/anchorClient.d.ts +99 -0
  12. package/dist/casper/anchorClient.js +412 -0
  13. package/dist/config.d.ts +51 -0
  14. package/dist/config.js +215 -0
  15. package/dist/env-file.d.ts +1 -0
  16. package/dist/env-file.js +51 -0
  17. package/dist/grimoire/service.d.ts +13 -0
  18. package/dist/grimoire/service.js +199 -0
  19. package/dist/grimoire/store.d.ts +10 -0
  20. package/dist/grimoire/store.js +64 -0
  21. package/dist/grimoire/supabase-store.d.ts +13 -0
  22. package/dist/grimoire/supabase-store.js +50 -0
  23. package/dist/grimoire/types.d.ts +60 -0
  24. package/dist/grimoire/types.js +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +17 -0
  27. package/dist/mcp/auditTools.d.ts +3 -0
  28. package/dist/mcp/auditTools.js +13 -0
  29. package/dist/mcp/grimoireTools.d.ts +3 -0
  30. package/dist/mcp/grimoireTools.js +91 -0
  31. package/dist/mcp/jsonResult.d.ts +2 -0
  32. package/dist/mcp/jsonResult.js +10 -0
  33. package/dist/mcp/memoryTools.d.ts +3 -0
  34. package/dist/mcp/memoryTools.js +73 -0
  35. package/dist/mcp/paymentTools.d.ts +3 -0
  36. package/dist/mcp/paymentTools.js +33 -0
  37. package/dist/memory/canonical.d.ts +4 -0
  38. package/dist/memory/canonical.js +49 -0
  39. package/dist/memory/hash.d.ts +1 -0
  40. package/dist/memory/hash.js +4 -0
  41. package/dist/memory/service.d.ts +37 -0
  42. package/dist/memory/service.js +175 -0
  43. package/dist/memory/store.d.ts +8 -0
  44. package/dist/memory/store.js +49 -0
  45. package/dist/memory/supabase-store.d.ts +10 -0
  46. package/dist/memory/supabase-store.js +30 -0
  47. package/dist/memory/types.d.ts +56 -0
  48. package/dist/memory/types.js +7 -0
  49. package/dist/payments/service.d.ts +26 -0
  50. package/dist/payments/service.js +613 -0
  51. package/dist/payments/store.d.ts +10 -0
  52. package/dist/payments/store.js +64 -0
  53. package/dist/payments/supabase-store.d.ts +13 -0
  54. package/dist/payments/supabase-store.js +51 -0
  55. package/dist/payments/types.d.ts +101 -0
  56. package/dist/payments/types.js +1 -0
  57. package/dist/server.d.ts +5 -0
  58. package/dist/server.js +68 -0
  59. package/dist/storage/json-file-store.d.ts +17 -0
  60. package/dist/storage/json-file-store.js +87 -0
  61. package/dist/storage/store-factory.d.ts +12 -0
  62. package/dist/storage/store-factory.js +26 -0
  63. package/dist/storage/supabase-rest.d.ts +26 -0
  64. package/dist/storage/supabase-rest.js +85 -0
  65. package/dist/x402/client.d.ts +44 -0
  66. package/dist/x402/client.js +95 -0
  67. package/dist/x402/facilitator.d.ts +84 -0
  68. package/dist/x402/facilitator.js +800 -0
  69. package/dist/x402/readiness.d.ts +55 -0
  70. package/dist/x402/readiness.js +433 -0
  71. package/dist/x402/redaction.d.ts +1 -0
  72. package/dist/x402/redaction.js +30 -0
  73. package/dist/x402/resource.d.ts +69 -0
  74. package/dist/x402/resource.js +325 -0
  75. package/dist/x402/settlement.d.ts +176 -0
  76. package/dist/x402/settlement.js +1210 -0
  77. package/dist/x402/signer.d.ts +71 -0
  78. package/dist/x402/signer.js +616 -0
  79. package/package.json +61 -0
@@ -0,0 +1,800 @@
1
+ import { createPublicKey, verify as nodeVerify } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { canonicalizeJson, toJsonObject, toJsonValue } from "../memory/canonical.js";
4
+ import { sha256Hex } from "../memory/hash.js";
5
+ import { CasperCliX402SettlementProvider, createSignedPayloadHash } from "./settlement.js";
6
+ const MAX_BODY_BYTES = 64 * 1024;
7
+ const HASH_HEX_PATTERN = /^[a-f0-9]{64}$/i;
8
+ const UNSIGNED_INTEGER_PATTERN = /^(0|[1-9]\d*)$/;
9
+ const HTTP_METHOD_PATTERN = /^[A-Za-z]+$/;
10
+ const SECP256K1_P = BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f");
11
+ export class InMemoryX402FacilitatorReplayStore {
12
+ records = new Map();
13
+ reserveVerify(payment) {
14
+ const existing = this.records.get(payment.replayKey);
15
+ if (existing) {
16
+ return {
17
+ ok: false,
18
+ reason: existing.payloadHash === payment.payloadHash
19
+ ? "payment_payload_replayed"
20
+ : "nonce_replayed"
21
+ };
22
+ }
23
+ this.records.set(payment.replayKey, replayRecord(payment, "verified", null));
24
+ return { ok: true };
25
+ }
26
+ reserveSettle(payment) {
27
+ const existing = this.records.get(payment.replayKey);
28
+ if (!existing) {
29
+ this.records.set(payment.replayKey, replayRecord(payment, "settling", null));
30
+ return { ok: true };
31
+ }
32
+ if (existing.state === "verified" && existing.payloadHash === payment.payloadHash) {
33
+ this.records.set(payment.replayKey, replayRecord(payment, "settling", null));
34
+ return { ok: true };
35
+ }
36
+ return {
37
+ ok: false,
38
+ reason: existing.state === "settling"
39
+ ? "payment_settlement_in_progress"
40
+ : existing.payloadHash === payment.payloadHash
41
+ ? "payment_payload_replayed"
42
+ : "nonce_replayed"
43
+ };
44
+ }
45
+ completeSettle(payment, state, transactionHash) {
46
+ this.records.set(payment.replayKey, replayRecord(payment, state, transactionHash));
47
+ }
48
+ }
49
+ export function createX402FacilitatorHttpServer(config) {
50
+ const replayStore = config.replayStore ?? new InMemoryX402FacilitatorReplayStore();
51
+ return createServer(async (request, response) => {
52
+ try {
53
+ await handleX402FacilitatorHttpRequest(request, response, config, replayStore);
54
+ }
55
+ catch (error) {
56
+ config.logger?.error?.(`x402 facilitator request failed: ${errorMessage(error)}`);
57
+ sendJson(response, 500, {
58
+ success: false,
59
+ settled: false,
60
+ error: "facilitator_failed"
61
+ });
62
+ }
63
+ });
64
+ }
65
+ export function validateX402FacilitatorPayment(input, options = {}) {
66
+ const body = asRecord(input);
67
+ if (!body) {
68
+ return rejected("request_not_object");
69
+ }
70
+ let paymentPayload;
71
+ try {
72
+ paymentPayload = toJsonObject(body.paymentPayload ?? body.payment_payload, "paymentPayload");
73
+ }
74
+ catch {
75
+ return rejected("payment_payload_missing");
76
+ }
77
+ const selectedRequirementHash = firstString(paymentPayload, [
78
+ "selectedRequirementHash",
79
+ "selected_requirement_hash"
80
+ ]);
81
+ if (!selectedRequirementHash) {
82
+ return rejected("selected_requirement_hash_missing");
83
+ }
84
+ if (!HASH_HEX_PATTERN.test(selectedRequirementHash)) {
85
+ return rejected("selected_requirement_hash_invalid");
86
+ }
87
+ const paymentRequirements = extractSelectedPaymentRequirements(body.paymentRequirements ?? body.payment_requirements ?? body.selectedRequirement, selectedRequirementHash.toLowerCase());
88
+ if (!paymentRequirements.ok) {
89
+ return rejected(paymentRequirements.reason);
90
+ }
91
+ const actualRequirementHash = sha256Hex(canonicalizeJson(paymentRequirements.value));
92
+ if (actualRequirementHash !== selectedRequirementHash.toLowerCase()) {
93
+ return rejected("selected_requirement_hash_mismatch");
94
+ }
95
+ const accepted = asRecord(paymentPayload.accepted);
96
+ if (accepted) {
97
+ const acceptedHash = sha256Hex(canonicalizeJson(accepted));
98
+ if (acceptedHash !== selectedRequirementHash.toLowerCase()) {
99
+ return rejected("accepted_requirement_hash_mismatch");
100
+ }
101
+ }
102
+ const requirement = normalizeRequirement(paymentRequirements.value);
103
+ if (!requirement.ok) {
104
+ return rejected(requirement.reason);
105
+ }
106
+ if (options.expectedNetwork &&
107
+ !networkMatches(requirement.value.network, options.expectedNetwork)) {
108
+ return rejected("network_mismatch");
109
+ }
110
+ const authorization = asRecord(paymentPayload.authorization);
111
+ if (!authorization) {
112
+ return rejected("authorization_missing");
113
+ }
114
+ const payer = firstString(paymentPayload, ["payer", "payerAccount", "payer_account"]);
115
+ if (!payer) {
116
+ return rejected("payer_missing");
117
+ }
118
+ const paymentId = firstString(authorization, ["paymentId", "payment_id"]) ??
119
+ firstString(paymentPayload, ["paymentId", "payment_id"]);
120
+ if (!paymentId) {
121
+ return rejected("payment_id_missing");
122
+ }
123
+ if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(paymentId)) {
124
+ return rejected("payment_id_invalid");
125
+ }
126
+ const policyHash = firstString(authorization, ["policyHash", "policy_hash"]) ??
127
+ firstString(paymentPayload, ["policyHash", "policy_hash"]);
128
+ if (!policyHash) {
129
+ return rejected("policy_hash_missing");
130
+ }
131
+ if (!HASH_HEX_PATTERN.test(policyHash)) {
132
+ return rejected("policy_hash_invalid");
133
+ }
134
+ const publicKey = firstString(authorization, ["publicKey", "public_key"]);
135
+ if (!publicKey) {
136
+ return rejected("public_key_missing");
137
+ }
138
+ if (!validCasperPublicKey(publicKey)) {
139
+ return rejected("public_key_invalid");
140
+ }
141
+ const signature = firstString(authorization, ["signature"]);
142
+ if (!signature) {
143
+ return rejected("signature_missing");
144
+ }
145
+ const payloadNonce = firstString(paymentPayload, ["nonce"]);
146
+ const authorizationNonce = firstString(authorization, ["nonce"]);
147
+ if (!payloadNonce && !authorizationNonce) {
148
+ return rejected("nonce_missing");
149
+ }
150
+ if (payloadNonce && authorizationNonce && payloadNonce !== authorizationNonce) {
151
+ return rejected("nonce_mismatch");
152
+ }
153
+ const nonce = normalizeHex(payloadNonce ?? authorizationNonce, 64);
154
+ if (!nonce) {
155
+ return rejected("nonce_invalid");
156
+ }
157
+ const validAfter = firstString(paymentPayload, [
158
+ "validAfter",
159
+ "valid_after",
160
+ "validFrom",
161
+ "valid_from"
162
+ ]);
163
+ if (!validAfter) {
164
+ return rejected("valid_after_missing");
165
+ }
166
+ const validUntil = firstString(paymentPayload, [
167
+ "validUntil",
168
+ "valid_until",
169
+ "validBefore",
170
+ "valid_before",
171
+ "expiresAt",
172
+ "expires_at"
173
+ ]) ?? firstString(authorization, ["validBefore", "valid_before"]);
174
+ if (!validUntil) {
175
+ return rejected("valid_until_missing");
176
+ }
177
+ const validity = validateValidityWindow({
178
+ validAfter,
179
+ validUntil,
180
+ timeoutSeconds: requirement.value.timeoutSeconds,
181
+ now: options.now?.() ?? new Date()
182
+ });
183
+ if (!validity.ok) {
184
+ return rejected(validity.reason);
185
+ }
186
+ const fieldCheck = validatePayloadRequirementFields({
187
+ paymentPayload,
188
+ authorization,
189
+ requirement: requirement.value,
190
+ payer,
191
+ publicKey,
192
+ nonce
193
+ });
194
+ if (!fieldCheck.ok) {
195
+ return rejected(fieldCheck.reason);
196
+ }
197
+ const authorizationToSign = {
198
+ domain: "casper-x402-authorization",
199
+ version: 1,
200
+ paymentId,
201
+ policyHash: policyHash.toLowerCase(),
202
+ selectedRequirementHash: selectedRequirementHash.toLowerCase(),
203
+ method: requirement.value.method,
204
+ resource: requirement.value.resource,
205
+ scheme: requirement.value.scheme,
206
+ network: requirement.value.network,
207
+ asset: requirement.value.asset,
208
+ payTo: requirement.value.payTo,
209
+ amount: requirement.value.amount,
210
+ payer,
211
+ publicKey: publicKey.toLowerCase(),
212
+ validAfter,
213
+ validBefore: validUntil,
214
+ nonce
215
+ };
216
+ const canonicalAuthorization = canonicalizeJson(authorizationToSign);
217
+ const authorizationHash = firstString(authorization, [
218
+ "authorizationHash",
219
+ "authorization_hash"
220
+ ]);
221
+ if (authorizationHash && authorizationHash !== sha256Hex(canonicalAuthorization)) {
222
+ return rejected("authorization_hash_mismatch");
223
+ }
224
+ if (!verifyCasperPayloadSignature({
225
+ publicKey,
226
+ signature,
227
+ canonicalAuthorization
228
+ })) {
229
+ return rejected("signature_invalid");
230
+ }
231
+ const payloadHash = createSignedPayloadHash(paymentPayload);
232
+ const replayKey = sha256Hex([
233
+ requirement.value.network.toLowerCase(),
234
+ payer.toLowerCase(),
235
+ nonce
236
+ ].join(":"));
237
+ return {
238
+ ok: true,
239
+ payment: {
240
+ paymentPayload,
241
+ paymentRequirements: paymentRequirements.value,
242
+ selectedRequirementHash: selectedRequirementHash.toLowerCase(),
243
+ payloadHash,
244
+ replayKey,
245
+ nonce,
246
+ paymentId,
247
+ policyHash: policyHash.toLowerCase(),
248
+ payer,
249
+ publicKey: publicKey.toLowerCase(),
250
+ signature: signature.toLowerCase(),
251
+ requirement: requirement.value,
252
+ settlementInput: {
253
+ payment_id: paymentId,
254
+ facilitator_url: options.facilitatorUrl ?? null,
255
+ method: requirement.value.method,
256
+ url: requirement.value.resource,
257
+ selected_requirement: paymentRequirements.value,
258
+ selected_requirement_hash: selectedRequirementHash.toLowerCase(),
259
+ policy_hash: policyHash.toLowerCase()
260
+ }
261
+ }
262
+ };
263
+ }
264
+ async function handleX402FacilitatorHttpRequest(request, response, config, replayStore) {
265
+ const pathname = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
266
+ if (request.method !== "POST" || (pathname !== "/verify" && pathname !== "/settle")) {
267
+ sendJson(response, 404, { error: "not_found" });
268
+ return;
269
+ }
270
+ const body = await readJsonBody(request, config.maxBodyBytes ?? MAX_BODY_BYTES);
271
+ if (!body.ok) {
272
+ sendJson(response, 400, {
273
+ success: false,
274
+ settled: false,
275
+ valid: false,
276
+ error: "invalid_json",
277
+ reason: body.reason
278
+ });
279
+ return;
280
+ }
281
+ const validation = validateX402FacilitatorPayment(body.value, {
282
+ expectedNetwork: config.settlementConfig.caip2ChainId,
283
+ facilitatorUrl: config.facilitatorUrl,
284
+ now: config.now
285
+ });
286
+ if (!validation.ok) {
287
+ config.logger?.warn?.(`x402 facilitator rejected reason=${validation.reason}`);
288
+ sendJson(response, validation.statusCode, {
289
+ success: false,
290
+ settled: false,
291
+ valid: false,
292
+ error: "invalid_payment",
293
+ reason: validation.reason
294
+ });
295
+ return;
296
+ }
297
+ if (pathname === "/verify") {
298
+ const reservation = replayStore.reserveVerify(validation.payment);
299
+ if (!reservation.ok) {
300
+ sendReplayRejection(response, reservation.reason);
301
+ return;
302
+ }
303
+ config.logger?.info?.(`x402 facilitator verified payment_id=${validation.payment.paymentId} selected_requirement_hash=${validation.payment.selectedRequirementHash} payload_hash=${validation.payment.payloadHash}`);
304
+ sendJson(response, 200, {
305
+ valid: true,
306
+ selectedRequirementHash: validation.payment.selectedRequirementHash,
307
+ network: validation.payment.requirement.network,
308
+ asset: validation.payment.requirement.asset,
309
+ amount: validation.payment.requirement.amount,
310
+ payer: validation.payment.payer,
311
+ payTo: validation.payment.requirement.payTo
312
+ });
313
+ return;
314
+ }
315
+ const reservation = replayStore.reserveSettle(validation.payment);
316
+ if (!reservation.ok) {
317
+ sendReplayRejection(response, reservation.reason);
318
+ return;
319
+ }
320
+ const settlement = await settleValidatedPayment(validation.payment, config);
321
+ if (settlement.status === "settled") {
322
+ replayStore.completeSettle(validation.payment, "settled", settlement.casper_transaction_hash);
323
+ config.logger?.info?.(`x402 facilitator settled payment_id=${validation.payment.paymentId} transaction_hash=${settlement.casper_transaction_hash}`);
324
+ sendJson(response, 200, settlementSuccessResponse(validation.payment, settlement));
325
+ return;
326
+ }
327
+ replayStore.completeSettle(validation.payment, "failed", settlement.casper_transaction_hash);
328
+ config.logger?.warn?.(`x402 facilitator settlement failed payment_id=${validation.payment.paymentId} blocker=${settlement.blocker}`);
329
+ sendJson(response, settlement.status === "unavailable" ? 503 : 200, settlementFailedResponse(validation.payment, settlement));
330
+ }
331
+ async function settleValidatedPayment(payment, config) {
332
+ const signer = {
333
+ async sign() {
334
+ return {
335
+ signed: true,
336
+ signed_payload: payment.paymentPayload,
337
+ signed_payload_hash: payment.payloadHash
338
+ };
339
+ }
340
+ };
341
+ const provider = new CasperCliX402SettlementProvider(signer, {
342
+ ...config.settlementConfig,
343
+ now: config.settlementConfig.now ?? config.now
344
+ }, config.commandRunner);
345
+ return provider.settle(payment.settlementInput);
346
+ }
347
+ function settlementSuccessResponse(payment, settlement) {
348
+ return {
349
+ success: true,
350
+ settled: true,
351
+ transactionHash: settlement.casper_transaction_hash,
352
+ transaction: settlement.casper_transaction_hash,
353
+ network: payment.requirement.network,
354
+ asset: payment.requirement.asset,
355
+ amount: payment.requirement.amount,
356
+ payer: payment.payer,
357
+ payTo: payment.requirement.payTo,
358
+ receipt: receiptJsonValue(settlement.receipt_json)
359
+ };
360
+ }
361
+ function settlementFailedResponse(payment, settlement) {
362
+ return {
363
+ success: false,
364
+ settled: false,
365
+ error: settlement.status === "unavailable" ? "settlement_unavailable" : "settlement_failed",
366
+ reason: settlement.blocker,
367
+ transactionHash: settlement.casper_transaction_hash,
368
+ transaction: settlement.casper_transaction_hash,
369
+ network: payment.requirement.network,
370
+ asset: payment.requirement.asset,
371
+ amount: payment.requirement.amount,
372
+ payer: payment.payer,
373
+ payTo: payment.requirement.payTo,
374
+ receipt: receiptJsonValue(settlement.receipt_json)
375
+ };
376
+ }
377
+ function sendReplayRejection(response, reason) {
378
+ sendJson(response, 409, {
379
+ success: false,
380
+ settled: false,
381
+ valid: false,
382
+ error: "payment_replayed",
383
+ reason
384
+ });
385
+ }
386
+ function validatePayloadRequirementFields(input) {
387
+ const checks = [
388
+ {
389
+ value: firstString(input.paymentPayload, ["scheme"]) ?? firstString(input.authorization, ["scheme"]),
390
+ expected: input.requirement.scheme,
391
+ reason: "scheme_unsupported"
392
+ },
393
+ {
394
+ value: firstString(input.paymentPayload, ["network"]) ?? firstString(input.authorization, ["network"]),
395
+ expected: input.requirement.network,
396
+ reason: "network_mismatch"
397
+ },
398
+ {
399
+ value: firstString(input.paymentPayload, ["method"]) ?? firstString(input.authorization, ["method"]),
400
+ expected: input.requirement.method,
401
+ reason: "method_mismatch"
402
+ },
403
+ {
404
+ value: firstString(input.paymentPayload, ["resource"]) ?? firstString(input.authorization, ["resource"]),
405
+ expected: input.requirement.resource,
406
+ reason: "resource_mismatch"
407
+ },
408
+ {
409
+ value: firstString(input.paymentPayload, ["asset"]) ?? firstString(input.authorization, ["asset"]),
410
+ expected: input.requirement.asset,
411
+ reason: "asset_mismatch"
412
+ },
413
+ {
414
+ value: firstString(input.paymentPayload, ["payTo", "pay_to", "payee"]) ??
415
+ firstString(input.authorization, ["payTo", "pay_to", "to"]),
416
+ expected: input.requirement.payTo,
417
+ reason: "payee_mismatch"
418
+ },
419
+ {
420
+ value: firstString(input.paymentPayload, ["amount", "maxAmountRequired", "max_amount_required"]) ??
421
+ firstString(input.authorization, ["amount", "value"]),
422
+ expected: input.requirement.amount,
423
+ reason: "amount_mismatch"
424
+ },
425
+ {
426
+ value: firstString(input.authorization, ["payer", "from"]),
427
+ expected: input.payer,
428
+ reason: "payer_missing"
429
+ },
430
+ {
431
+ value: firstString(input.authorization, ["publicKey", "public_key"]),
432
+ expected: input.publicKey,
433
+ reason: "public_key_invalid"
434
+ },
435
+ {
436
+ value: firstString(input.authorization, ["nonce"]),
437
+ expected: input.nonce,
438
+ reason: "nonce_mismatch"
439
+ }
440
+ ];
441
+ for (const check of checks) {
442
+ if (check.value && !matchesNormalized(check.value, check.expected)) {
443
+ return { ok: false, reason: check.reason };
444
+ }
445
+ }
446
+ return { ok: true };
447
+ }
448
+ function normalizeRequirement(requirement) {
449
+ const scheme = firstString(requirement, ["scheme"]);
450
+ if (!scheme) {
451
+ return { ok: false, reason: "scheme_missing" };
452
+ }
453
+ if (scheme !== "exact") {
454
+ return { ok: false, reason: "scheme_unsupported" };
455
+ }
456
+ const network = firstString(requirement, [
457
+ "network",
458
+ "networkId",
459
+ "network_id",
460
+ "caip2_chain_id",
461
+ "caip2ChainId"
462
+ ]);
463
+ if (!network) {
464
+ return { ok: false, reason: "network_missing" };
465
+ }
466
+ const amount = firstString(requirement, [
467
+ "maxAmountRequired",
468
+ "amount",
469
+ "max_amount_required"
470
+ ]);
471
+ if (!amount) {
472
+ return { ok: false, reason: "amount_missing" };
473
+ }
474
+ if (!UNSIGNED_INTEGER_PATTERN.test(amount)) {
475
+ return { ok: false, reason: "amount_invalid" };
476
+ }
477
+ const resource = firstString(requirement, ["resource", "resourceUrl", "resource_url"]);
478
+ if (!resource) {
479
+ return { ok: false, reason: "resource_missing" };
480
+ }
481
+ const method = firstString(requirement, ["method", "httpMethod", "http_method"]);
482
+ if (!method) {
483
+ return { ok: false, reason: "method_missing" };
484
+ }
485
+ const normalizedMethod = method.toUpperCase();
486
+ if (!HTTP_METHOD_PATTERN.test(normalizedMethod)) {
487
+ return { ok: false, reason: "method_invalid" };
488
+ }
489
+ const asset = firstString(requirement, [
490
+ "asset",
491
+ "assetId",
492
+ "asset_id",
493
+ "assetPackage",
494
+ "asset_package",
495
+ "assetPackageHash",
496
+ "asset_package_hash"
497
+ ]);
498
+ if (!asset) {
499
+ return { ok: false, reason: "asset_missing" };
500
+ }
501
+ const payTo = firstString(requirement, [
502
+ "payTo",
503
+ "pay_to",
504
+ "payee",
505
+ "recipient",
506
+ "recipientAddress",
507
+ "recipient_address"
508
+ ]);
509
+ if (!payTo) {
510
+ return { ok: false, reason: "payee_missing" };
511
+ }
512
+ const timeout = firstValue(requirement, [
513
+ "timeout",
514
+ "timeoutSeconds",
515
+ "timeout_seconds",
516
+ "maxTimeoutSeconds",
517
+ "max_timeout_seconds"
518
+ ]);
519
+ if (timeout === null) {
520
+ return { ok: false, reason: "timeout_missing" };
521
+ }
522
+ const timeoutSeconds = timeoutValue(timeout);
523
+ if (timeoutSeconds === null) {
524
+ return { ok: false, reason: "timeout_invalid" };
525
+ }
526
+ return {
527
+ ok: true,
528
+ value: {
529
+ scheme: "exact",
530
+ network,
531
+ amount,
532
+ resource,
533
+ method: normalizedMethod,
534
+ asset,
535
+ payTo,
536
+ timeoutSeconds
537
+ }
538
+ };
539
+ }
540
+ function extractSelectedPaymentRequirements(value, selectedRequirementHash) {
541
+ const record = asRecord(value);
542
+ if (!record) {
543
+ return { ok: false, reason: "payment_requirements_missing" };
544
+ }
545
+ if (!Array.isArray(record.accepts)) {
546
+ return { ok: true, value: toJsonObject(record, "paymentRequirements") };
547
+ }
548
+ for (const item of record.accepts) {
549
+ const candidate = asRecord(item);
550
+ if (!candidate) {
551
+ continue;
552
+ }
553
+ const candidateObject = toJsonObject(candidate, "paymentRequirements.accepts[]");
554
+ if (sha256Hex(canonicalizeJson(candidateObject)) === selectedRequirementHash) {
555
+ return { ok: true, value: candidateObject };
556
+ }
557
+ }
558
+ return { ok: false, reason: "payment_requirements_no_matching_accept" };
559
+ }
560
+ function validateValidityWindow(input) {
561
+ const validAfterMs = Date.parse(input.validAfter);
562
+ const validUntilMs = Date.parse(input.validUntil);
563
+ if (!Number.isFinite(validAfterMs) ||
564
+ !Number.isFinite(validUntilMs) ||
565
+ validUntilMs <= validAfterMs) {
566
+ return { ok: false, reason: "validity_window_invalid" };
567
+ }
568
+ const nowMs = input.now.getTime();
569
+ if (validAfterMs > nowMs) {
570
+ return { ok: false, reason: "payment_not_yet_valid" };
571
+ }
572
+ if (validUntilMs <= nowMs) {
573
+ return { ok: false, reason: "payment_expired" };
574
+ }
575
+ if (validUntilMs - validAfterMs > input.timeoutSeconds * 1000) {
576
+ return { ok: false, reason: "timeout_exceeded" };
577
+ }
578
+ return { ok: true };
579
+ }
580
+ function verifyCasperPayloadSignature(input) {
581
+ const publicKey = normalizeHex(input.publicKey, null);
582
+ const signature = normalizeHex(input.signature, null);
583
+ if (!publicKey || !signature) {
584
+ return false;
585
+ }
586
+ const authorizationBytes = Buffer.from(input.canonicalAuthorization, "utf8");
587
+ try {
588
+ if (publicKey.startsWith("01") && signature.startsWith("01")) {
589
+ const publicKeyObject = createEd25519PublicKey(publicKey.slice(2));
590
+ return nodeVerify(null, authorizationBytes, publicKeyObject, Buffer.from(signature.slice(2), "hex"));
591
+ }
592
+ if (publicKey.startsWith("02") && signature.startsWith("02")) {
593
+ const publicKeyObject = createSecp256k1PublicKey(publicKey.slice(2));
594
+ if (!publicKeyObject) {
595
+ return false;
596
+ }
597
+ return nodeVerify("sha256", authorizationBytes, { key: publicKeyObject, dsaEncoding: "ieee-p1363" }, Buffer.from(signature.slice(2), "hex"));
598
+ }
599
+ }
600
+ catch {
601
+ return false;
602
+ }
603
+ return false;
604
+ }
605
+ function createEd25519PublicKey(rawHex) {
606
+ return createPublicKey({
607
+ key: {
608
+ kty: "OKP",
609
+ crv: "Ed25519",
610
+ x: Buffer.from(rawHex, "hex").toString("base64url")
611
+ },
612
+ format: "jwk"
613
+ });
614
+ }
615
+ function createSecp256k1PublicKey(compressedHex) {
616
+ const point = decompressSecp256k1PublicKey(compressedHex);
617
+ if (!point) {
618
+ return null;
619
+ }
620
+ return createPublicKey({
621
+ key: {
622
+ kty: "EC",
623
+ crv: "secp256k1",
624
+ x: bigIntToBuffer(point.x, 32).toString("base64url"),
625
+ y: bigIntToBuffer(point.y, 32).toString("base64url")
626
+ },
627
+ format: "jwk"
628
+ });
629
+ }
630
+ function decompressSecp256k1PublicKey(compressedHex) {
631
+ if (!/^(02|03)[a-f0-9]{64}$/i.test(compressedHex)) {
632
+ return null;
633
+ }
634
+ const prefix = compressedHex.slice(0, 2);
635
+ const x = BigInt(`0x${compressedHex.slice(2)}`);
636
+ const alpha = mod(x ** 3n + 7n, SECP256K1_P);
637
+ const beta = modPow(alpha, (SECP256K1_P + 1n) / 4n, SECP256K1_P);
638
+ const betaIsOdd = Boolean(beta & 1n);
639
+ const wantOdd = prefix === "03";
640
+ const y = betaIsOdd === wantOdd ? beta : SECP256K1_P - beta;
641
+ return { x, y };
642
+ }
643
+ function receiptJsonValue(receiptJson) {
644
+ try {
645
+ return toJsonValue(JSON.parse(receiptJson), "receipt");
646
+ }
647
+ catch {
648
+ return { raw: receiptJson };
649
+ }
650
+ }
651
+ function readJsonBody(request, maxBodyBytes) {
652
+ return new Promise((resolve) => {
653
+ const chunks = [];
654
+ let total = 0;
655
+ let resolved = false;
656
+ request.on("data", (chunk) => {
657
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
658
+ total += buffer.byteLength;
659
+ if (total > maxBodyBytes && !resolved) {
660
+ resolved = true;
661
+ request.destroy();
662
+ resolve({ ok: false, reason: "body_too_large" });
663
+ return;
664
+ }
665
+ chunks.push(buffer);
666
+ });
667
+ request.on("end", () => {
668
+ if (resolved) {
669
+ return;
670
+ }
671
+ const raw = Buffer.concat(chunks).toString("utf8");
672
+ try {
673
+ resolve({ ok: true, value: raw.trim() ? JSON.parse(raw) : {} });
674
+ }
675
+ catch {
676
+ resolve({ ok: false, reason: "body_invalid_json" });
677
+ }
678
+ });
679
+ request.on("error", () => {
680
+ if (!resolved) {
681
+ resolved = true;
682
+ resolve({ ok: false, reason: "body_invalid_json" });
683
+ }
684
+ });
685
+ });
686
+ }
687
+ function sendJson(response, statusCode, value) {
688
+ if (response.writableEnded) {
689
+ return;
690
+ }
691
+ response.statusCode = statusCode;
692
+ response.setHeader("content-type", "application/json");
693
+ response.end(`${JSON.stringify(value)}\n`);
694
+ }
695
+ function replayRecord(payment, state, transactionHash) {
696
+ return {
697
+ replayKey: payment.replayKey,
698
+ payloadHash: payment.payloadHash,
699
+ state,
700
+ transactionHash,
701
+ updatedAt: new Date().toISOString()
702
+ };
703
+ }
704
+ function rejected(reason) {
705
+ return {
706
+ ok: false,
707
+ reason,
708
+ statusCode: reason.includes("missing") ? 400 : 422
709
+ };
710
+ }
711
+ function asRecord(value) {
712
+ return value && typeof value === "object" && !Array.isArray(value)
713
+ ? value
714
+ : null;
715
+ }
716
+ function firstString(record, keys) {
717
+ if (!record) {
718
+ return null;
719
+ }
720
+ for (const key of keys) {
721
+ const value = record[key];
722
+ if (typeof value === "string" && value.trim()) {
723
+ return value.trim();
724
+ }
725
+ }
726
+ return null;
727
+ }
728
+ function firstValue(record, keys) {
729
+ for (const key of keys) {
730
+ if (record[key] !== undefined && record[key] !== null) {
731
+ return record[key];
732
+ }
733
+ }
734
+ return null;
735
+ }
736
+ function timeoutValue(value) {
737
+ if (typeof value === "number") {
738
+ return Number.isInteger(value) && value > 0 ? value : null;
739
+ }
740
+ if (typeof value === "string" && /^[1-9]\d*$/.test(value.trim())) {
741
+ const parsed = Number(value.trim());
742
+ return Number.isSafeInteger(parsed) ? parsed : null;
743
+ }
744
+ return null;
745
+ }
746
+ function normalizeHex(value, length) {
747
+ const normalized = value?.toLowerCase().replace(/^0x/, "");
748
+ if (!normalized) {
749
+ return null;
750
+ }
751
+ const pattern = length === null ? /^[a-f0-9]+$/ : new RegExp(`^[a-f0-9]{${length}}$`);
752
+ return pattern.test(normalized) ? normalized : null;
753
+ }
754
+ function validCasperPublicKey(value) {
755
+ const normalized = normalizeHex(value, null);
756
+ if (!normalized) {
757
+ return false;
758
+ }
759
+ return /^01[a-f0-9]{64}$/.test(normalized) || /^02(02|03)[a-f0-9]{64}$/.test(normalized);
760
+ }
761
+ function matchesNormalized(left, right) {
762
+ return left.toLowerCase() === right.toLowerCase();
763
+ }
764
+ function networkMatches(left, right) {
765
+ const normalizedLeft = left.toLowerCase();
766
+ const normalizedRight = right.toLowerCase();
767
+ if (normalizedLeft === normalizedRight) {
768
+ return true;
769
+ }
770
+ const leftSuffix = normalizedLeft.split(":").at(-1);
771
+ const rightSuffix = normalizedRight.split(":").at(-1);
772
+ return Boolean(leftSuffix && rightSuffix && leftSuffix === rightSuffix);
773
+ }
774
+ function mod(value, by) {
775
+ const result = value % by;
776
+ return result >= 0n ? result : result + by;
777
+ }
778
+ function modPow(value, exponent, by) {
779
+ let result = 1n;
780
+ let base = mod(value, by);
781
+ let remaining = exponent;
782
+ while (remaining > 0n) {
783
+ if (remaining & 1n) {
784
+ result = mod(result * base, by);
785
+ }
786
+ base = mod(base * base, by);
787
+ remaining >>= 1n;
788
+ }
789
+ return result;
790
+ }
791
+ function bigIntToBuffer(value, length) {
792
+ const hex = value.toString(16);
793
+ if (hex.length > length * 2) {
794
+ throw new Error("Integer does not fit fixed buffer");
795
+ }
796
+ return Buffer.from(hex.padStart(length * 2, "0"), "hex");
797
+ }
798
+ function errorMessage(error) {
799
+ return error instanceof Error ? error.message : String(error);
800
+ }