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,613 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { approveX402Requirements } from "../x402/readiness.js";
3
+ import { redactX402Value } from "../x402/redaction.js";
4
+ export class PaymentService {
5
+ grimoireService;
6
+ store;
7
+ audit;
8
+ challengeClient;
9
+ settlementProvider;
10
+ constructor(grimoireService, store, audit, challengeClient, settlementProvider) {
11
+ this.grimoireService = grimoireService;
12
+ this.store = store;
13
+ this.audit = audit;
14
+ this.challengeClient = challengeClient;
15
+ this.settlementProvider = settlementProvider;
16
+ }
17
+ async preflightFetch(input) {
18
+ return this.fetch({ ...input, request_challenge: false });
19
+ }
20
+ async fetch(input) {
21
+ const method = input.method.toUpperCase();
22
+ const idempotencyKey = input.idempotency_key ?? null;
23
+ if (idempotencyKey) {
24
+ const existing = await this.store.findIntentByIdempotencyKey(input.agent_id, idempotencyKey);
25
+ if (existing) {
26
+ return this.resultFromIntent(existing);
27
+ }
28
+ }
29
+ const now = new Date().toISOString();
30
+ const paymentId = createPaymentId();
31
+ const policy = await this.grimoireService.getPolicy(input.agent_id, input.policy_id);
32
+ let intent = {
33
+ id: paymentId,
34
+ agent_id: input.agent_id,
35
+ policy_id: input.policy_id,
36
+ method,
37
+ url: input.url,
38
+ amount: input.expected_amount ?? null,
39
+ status: "created",
40
+ idempotency_key: idempotencyKey,
41
+ policy_hash: null,
42
+ denial_reason: null,
43
+ requirements_json: null,
44
+ signed_payload_hash: null,
45
+ settlement_blocker: null,
46
+ created_at: now,
47
+ updated_at: now
48
+ };
49
+ const denialReason = policy
50
+ ? validatePolicy(input, method, policy)
51
+ : "policy_not_found";
52
+ if (denialReason) {
53
+ intent = {
54
+ ...intent,
55
+ status: "policy_denied",
56
+ denial_reason: denialReason,
57
+ policy_hash: policy?.policy_hash ?? null,
58
+ updated_at: new Date().toISOString()
59
+ };
60
+ await this.store.saveIntent(intent);
61
+ await this.audit?.record({
62
+ agent_id: intent.agent_id,
63
+ event_type: "payment.policy_denied",
64
+ subject_type: "payment",
65
+ subject_id: intent.id,
66
+ severity: "warn",
67
+ metadata: {
68
+ policy_id: intent.policy_id,
69
+ method: intent.method,
70
+ url: intent.url,
71
+ amount: intent.amount,
72
+ reason: denialReason,
73
+ idempotency_key: idempotencyKey
74
+ }
75
+ });
76
+ return this.resultFromIntent(intent);
77
+ }
78
+ intent = {
79
+ ...intent,
80
+ status: "policy_checked",
81
+ policy_hash: policy.policy_hash,
82
+ updated_at: new Date().toISOString()
83
+ };
84
+ await this.store.saveIntent(intent);
85
+ await this.audit?.record({
86
+ agent_id: intent.agent_id,
87
+ event_type: "payment.policy_approved",
88
+ subject_type: "payment",
89
+ subject_id: intent.id,
90
+ metadata: {
91
+ policy_id: intent.policy_id,
92
+ method: intent.method,
93
+ url: intent.url,
94
+ amount: intent.amount,
95
+ policy_hash: intent.policy_hash,
96
+ idempotency_key: idempotencyKey,
97
+ next_state: "challenge_received",
98
+ request_challenge: input.request_challenge ?? false
99
+ }
100
+ });
101
+ if (input.request_challenge) {
102
+ return this.fetchChallenge(intent, policy, input.expected_amount ?? null);
103
+ }
104
+ return this.resultFromIntent(intent);
105
+ }
106
+ async receipt(paymentId) {
107
+ const intent = await this.store.getIntent(paymentId);
108
+ if (!intent) {
109
+ return { found: false, payment_id: paymentId };
110
+ }
111
+ return {
112
+ found: true,
113
+ payment_id: paymentId,
114
+ intent,
115
+ receipt: await this.store.getReceipt(paymentId)
116
+ };
117
+ }
118
+ async fetchChallenge(intent, policy, expectedAmount) {
119
+ if (!this.challengeClient) {
120
+ const unavailable = await this.markSettlementUnavailable(intent, "x402_challenge_client_unavailable");
121
+ return this.resultFromIntent(unavailable, undefined, "x402_challenge_client_unavailable");
122
+ }
123
+ try {
124
+ const challenge = await this.challengeClient.requestChallenge({
125
+ method: intent.method,
126
+ url: intent.url
127
+ });
128
+ if (challenge.status === "payment_required") {
129
+ const safeRequirements = redactX402Value(challenge.requirements);
130
+ const safeRequirementsJson = JSON.stringify(safeRequirements);
131
+ const safeChallenge = {
132
+ ...challenge,
133
+ requirements: safeRequirements,
134
+ requirements_json: safeRequirementsJson
135
+ };
136
+ const updated = {
137
+ ...intent,
138
+ status: "challenge_received",
139
+ requirements_json: safeRequirementsJson,
140
+ updated_at: new Date().toISOString()
141
+ };
142
+ await this.store.saveIntent(updated);
143
+ const approval = approveX402Requirements({
144
+ requirements: challenge.requirements,
145
+ policy,
146
+ method: updated.method,
147
+ url: updated.url,
148
+ expectedAmount
149
+ });
150
+ await this.audit?.record({
151
+ agent_id: updated.agent_id,
152
+ event_type: "payment.challenge_received",
153
+ subject_type: "payment",
154
+ subject_id: updated.id,
155
+ metadata: {
156
+ policy_id: updated.policy_id,
157
+ method: updated.method,
158
+ url: updated.url,
159
+ status_code: challenge.status_code,
160
+ requirements_source: challenge.requirements_source,
161
+ facilitator_url: challenge.facilitator_url,
162
+ resource_url: challenge.resource_url,
163
+ settlement: "not_started",
164
+ requirements_approved: approval.approved,
165
+ selected_requirement_hash: approval.approved
166
+ ? approval.selected_requirement_hash
167
+ : null
168
+ }
169
+ });
170
+ if (!approval.approved) {
171
+ const unavailable = await this.markSettlementUnavailable(updated, "x402_requirements_not_allowed");
172
+ await this.audit?.record({
173
+ agent_id: unavailable.agent_id,
174
+ event_type: "payment.requirements_rejected",
175
+ subject_type: "payment",
176
+ subject_id: unavailable.id,
177
+ severity: "warn",
178
+ metadata: {
179
+ policy_id: unavailable.policy_id,
180
+ method: unavailable.method,
181
+ url: unavailable.url,
182
+ reason: approval.reason,
183
+ rejected_candidates: approval.rejected_candidates
184
+ }
185
+ });
186
+ return this.resultFromIntent(unavailable, safeChallenge, "x402_requirements_not_allowed");
187
+ }
188
+ return this.settleApprovedRequirement(updated, safeChallenge, approval.selected_requirement, approval.selected_requirement_hash);
189
+ }
190
+ if (challenge.status === "free_response") {
191
+ await this.audit?.record({
192
+ agent_id: intent.agent_id,
193
+ event_type: "payment.challenge_not_required",
194
+ subject_type: "payment",
195
+ subject_id: intent.id,
196
+ metadata: {
197
+ policy_id: intent.policy_id,
198
+ method: intent.method,
199
+ url: intent.url,
200
+ status_code: challenge.status_code,
201
+ response_hash: challenge.response_hash,
202
+ settlement: "not_required"
203
+ }
204
+ });
205
+ return this.resultFromIntent(intent, challenge);
206
+ }
207
+ const unavailable = await this.markSettlementUnavailable(intent, "x402_unexpected_resource_response");
208
+ await this.audit?.record({
209
+ agent_id: unavailable.agent_id,
210
+ event_type: "payment.settlement_unavailable",
211
+ subject_type: "payment",
212
+ subject_id: unavailable.id,
213
+ severity: "warn",
214
+ metadata: {
215
+ policy_id: unavailable.policy_id,
216
+ method: unavailable.method,
217
+ url: unavailable.url,
218
+ status_code: challenge.status_code,
219
+ response_hash: challenge.response_hash,
220
+ reason: "x402_unexpected_resource_response"
221
+ }
222
+ });
223
+ return this.resultFromIntent(unavailable, challenge, "x402_unexpected_resource_response");
224
+ }
225
+ catch (error) {
226
+ const unavailable = await this.markSettlementUnavailable(intent, "x402_challenge_request_failed");
227
+ await this.audit?.record({
228
+ agent_id: unavailable.agent_id,
229
+ event_type: "payment.settlement_unavailable",
230
+ subject_type: "payment",
231
+ subject_id: unavailable.id,
232
+ severity: "error",
233
+ metadata: {
234
+ policy_id: unavailable.policy_id,
235
+ method: unavailable.method,
236
+ url: unavailable.url,
237
+ reason: "x402_challenge_request_failed",
238
+ error: errorMessage(error)
239
+ }
240
+ });
241
+ return this.resultFromIntent(unavailable, undefined, "x402_challenge_request_failed");
242
+ }
243
+ }
244
+ async markSettlementUnavailable(intent, reason) {
245
+ const updated = {
246
+ ...intent,
247
+ status: "settlement_unavailable",
248
+ settlement_blocker: reason,
249
+ updated_at: new Date().toISOString()
250
+ };
251
+ await this.store.saveIntent(updated);
252
+ if (reason === "x402_challenge_client_unavailable") {
253
+ await this.audit?.record({
254
+ agent_id: updated.agent_id,
255
+ event_type: "payment.settlement_unavailable",
256
+ subject_type: "payment",
257
+ subject_id: updated.id,
258
+ severity: "warn",
259
+ metadata: {
260
+ policy_id: updated.policy_id,
261
+ method: updated.method,
262
+ url: updated.url,
263
+ reason
264
+ }
265
+ });
266
+ }
267
+ return updated;
268
+ }
269
+ async settleApprovedRequirement(intent, challenge, selectedRequirement, selectedRequirementHash) {
270
+ if (!this.settlementProvider) {
271
+ const unavailable = await this.markSettlementUnavailable(intent, "x402_settlement_provider_unavailable");
272
+ await this.persistSettlementReceipt(unavailable, {
273
+ status: "unavailable",
274
+ blocker: "x402_settlement_provider_unavailable",
275
+ signed_payload_hash: null,
276
+ response_status: null,
277
+ casper_transaction_hash: null,
278
+ receipt_json: JSON.stringify({
279
+ status: "settlement_unavailable",
280
+ blocker: "x402_settlement_provider_unavailable",
281
+ payment_id: unavailable.id,
282
+ selected_requirement_hash: selectedRequirementHash
283
+ })
284
+ }, challenge.facilitator_url);
285
+ return this.resultFromIntent(unavailable, challenge, "x402_settlement_provider_unavailable");
286
+ }
287
+ const selectedAmount = amountFromRequirement(selectedRequirement) ?? intent.amount;
288
+ if (selectedAmount) {
289
+ const periodCheck = await this.checkCurrentPeriodSpend(intent, selectedAmount);
290
+ if (!periodCheck.allowed) {
291
+ const unavailable = await this.markSettlementUnavailable(intent, periodCheck.reason);
292
+ await this.audit?.record({
293
+ agent_id: unavailable.agent_id,
294
+ event_type: "payment.settlement_unavailable",
295
+ subject_type: "payment",
296
+ subject_id: unavailable.id,
297
+ severity: "warn",
298
+ metadata: {
299
+ policy_id: unavailable.policy_id,
300
+ method: unavailable.method,
301
+ url: unavailable.url,
302
+ selected_requirement_hash: selectedRequirementHash,
303
+ reason: periodCheck.reason
304
+ }
305
+ });
306
+ return this.resultFromIntent(unavailable, challenge, periodCheck.reason);
307
+ }
308
+ }
309
+ const settlement = await this.settlementProvider.settle({
310
+ payment_id: intent.id,
311
+ facilitator_url: challenge.facilitator_url,
312
+ method: intent.method,
313
+ url: intent.url,
314
+ selected_requirement: selectedRequirement,
315
+ selected_requirement_hash: selectedRequirementHash,
316
+ policy_hash: intent.policy_hash ?? ""
317
+ });
318
+ if (settlement.status === "settled") {
319
+ const settledIntent = {
320
+ ...intent,
321
+ status: "settled",
322
+ signed_payload_hash: settlement.signed_payload_hash,
323
+ settlement_blocker: null,
324
+ updated_at: new Date().toISOString()
325
+ };
326
+ await this.store.saveIntent(settledIntent);
327
+ await this.persistSettlementReceipt(settledIntent, settlement, challenge.facilitator_url);
328
+ if (selectedAmount) {
329
+ await this.grimoireService.recordPolicySpend(settledIntent.agent_id, settledIntent.policy_id, selectedAmount);
330
+ }
331
+ await this.audit?.record({
332
+ agent_id: settledIntent.agent_id,
333
+ event_type: "payment.settled",
334
+ subject_type: "payment",
335
+ subject_id: settledIntent.id,
336
+ metadata: {
337
+ policy_id: settledIntent.policy_id,
338
+ method: settledIntent.method,
339
+ url: settledIntent.url,
340
+ selected_requirement_hash: selectedRequirementHash,
341
+ signed_payload_hash: settlement.signed_payload_hash,
342
+ casper_transaction_hash: settlement.casper_transaction_hash
343
+ }
344
+ });
345
+ return this.resultFromIntent(settledIntent, challenge);
346
+ }
347
+ const unavailable = {
348
+ ...intent,
349
+ status: "settlement_unavailable",
350
+ signed_payload_hash: settlement.signed_payload_hash,
351
+ settlement_blocker: settlement.blocker,
352
+ updated_at: new Date().toISOString()
353
+ };
354
+ await this.store.saveIntent(unavailable);
355
+ await this.persistSettlementReceipt(unavailable, settlement, challenge.facilitator_url);
356
+ await this.audit?.record({
357
+ agent_id: unavailable.agent_id,
358
+ event_type: "payment.settlement_unavailable",
359
+ subject_type: "payment",
360
+ subject_id: unavailable.id,
361
+ severity: settlement.status === "failed" ? "error" : "warn",
362
+ metadata: {
363
+ policy_id: unavailable.policy_id,
364
+ method: unavailable.method,
365
+ url: unavailable.url,
366
+ selected_requirement_hash: selectedRequirementHash,
367
+ signed_payload_hash: settlement.signed_payload_hash,
368
+ reason: settlement.blocker
369
+ }
370
+ });
371
+ return this.resultFromIntent(unavailable, challenge, settlement.blocker);
372
+ }
373
+ async checkCurrentPeriodSpend(intent, amount) {
374
+ const policy = await this.grimoireService.getPolicy(intent.agent_id, intent.policy_id);
375
+ if (!policy) {
376
+ return { allowed: false, reason: "policy_period_amount_invalid" };
377
+ }
378
+ const amountParts = parseDecimalAmount(amount);
379
+ const maxPeriod = parseDecimalAmount(policy.max_amount_per_period);
380
+ const currentSpend = parseDecimalAmount(currentPeriodSpend(policy));
381
+ if (!amountParts || !maxPeriod || !currentSpend) {
382
+ return { allowed: false, reason: "policy_period_amount_invalid" };
383
+ }
384
+ if (compareDecimal(addDecimalParts(currentSpend, amountParts), maxPeriod) > 0) {
385
+ return { allowed: false, reason: "policy_period_limit_exceeded" };
386
+ }
387
+ return { allowed: true };
388
+ }
389
+ async persistSettlementReceipt(intent, settlement, facilitatorUrl) {
390
+ await this.store.saveReceipt({
391
+ id: createPaymentReceiptId(),
392
+ payment_id: intent.id,
393
+ facilitator_url: facilitatorUrl ?? "unconfigured",
394
+ casper_transaction_hash: settlement.casper_transaction_hash,
395
+ settlement_status: settlement.status === "settled"
396
+ ? "settled"
397
+ : settlement.status === "failed"
398
+ ? "failed"
399
+ : "settlement_unavailable",
400
+ response_hash: settlement.receipt_json ? hashReceipt(settlement.receipt_json) : null,
401
+ response_status: settlement.response_status,
402
+ receipt_json: settlement.receipt_json,
403
+ created_at: new Date().toISOString()
404
+ });
405
+ }
406
+ resultFromIntent(intent, challenge, settlementBlocker) {
407
+ if (intent.status === "policy_denied") {
408
+ return {
409
+ allowed: false,
410
+ payment_id: intent.id,
411
+ status: "policy_denied",
412
+ reason: intent.denial_reason ?? "policy_not_found",
413
+ agent_id: intent.agent_id,
414
+ policy_id: intent.policy_id,
415
+ method: intent.method,
416
+ url: intent.url,
417
+ expected_amount: intent.amount,
418
+ idempotency_key: intent.idempotency_key,
419
+ persisted: true
420
+ };
421
+ }
422
+ const requirements = parseRequirementsJson(intent.requirements_json);
423
+ const result = {
424
+ allowed: true,
425
+ payment_id: intent.id,
426
+ status: intent.status === "created" ? "policy_checked" : intent.status,
427
+ next_state: nextStateFor(intent, challenge),
428
+ agent_id: intent.agent_id,
429
+ policy_id: intent.policy_id,
430
+ method: intent.method,
431
+ url: intent.url,
432
+ expected_amount: intent.amount,
433
+ policy_hash: intent.policy_hash ?? "",
434
+ idempotency_key: intent.idempotency_key,
435
+ persisted: true,
436
+ settlement: settlementFor(intent, challenge),
437
+ requirements_json: intent.requirements_json
438
+ };
439
+ if (requirements.parsed) {
440
+ result.requirements = requirements.value;
441
+ }
442
+ if (challenge) {
443
+ result.challenge = summarizeChallenge(challenge);
444
+ }
445
+ const blocker = settlementBlocker ?? intent.settlement_blocker ?? settlementBlockerFor(intent, challenge);
446
+ if (blocker) {
447
+ result.settlement_blocker = blocker;
448
+ }
449
+ return result;
450
+ }
451
+ }
452
+ function summarizeChallenge(challenge) {
453
+ const summary = {
454
+ status: challenge.status,
455
+ status_code: challenge.status_code,
456
+ facilitator_url: challenge.facilitator_url,
457
+ resource_url: challenge.resource_url,
458
+ request_url: challenge.request_url,
459
+ settlement_status: challenge.settlement_status
460
+ };
461
+ if (challenge.status === "payment_required") {
462
+ summary.requirements = challenge.requirements;
463
+ summary.requirements_json = challenge.requirements_json;
464
+ summary.requirements_source = challenge.requirements_source;
465
+ }
466
+ else {
467
+ summary.response_hash = challenge.response_hash;
468
+ }
469
+ return summary;
470
+ }
471
+ function parseRequirementsJson(requirementsJson) {
472
+ if (!requirementsJson) {
473
+ return { parsed: false };
474
+ }
475
+ try {
476
+ return { parsed: true, value: JSON.parse(requirementsJson) };
477
+ }
478
+ catch {
479
+ return { parsed: false };
480
+ }
481
+ }
482
+ function nextStateFor(intent, challenge) {
483
+ if (challenge?.status === "free_response") {
484
+ return null;
485
+ }
486
+ switch (intent.status) {
487
+ case "policy_checked":
488
+ return "challenge_received";
489
+ case "challenge_received":
490
+ return "settlement_unavailable";
491
+ case "created":
492
+ case "policy_denied":
493
+ case "settlement_unavailable":
494
+ case "settled":
495
+ return null;
496
+ }
497
+ }
498
+ function settlementFor(intent, challenge) {
499
+ if (intent.status === "settled") {
500
+ return "settled";
501
+ }
502
+ if (challenge?.status === "free_response") {
503
+ return "not_required";
504
+ }
505
+ if (intent.status === "settlement_unavailable") {
506
+ return "unavailable";
507
+ }
508
+ return "not_started";
509
+ }
510
+ function settlementBlockerFor(intent, challenge) {
511
+ if (challenge?.status === "free_response" || intent.status === "settled") {
512
+ return undefined;
513
+ }
514
+ if (intent.status === "challenge_received") {
515
+ return "x402_settlement_provider_unavailable";
516
+ }
517
+ if (intent.status === "settlement_unavailable") {
518
+ return "x402_settlement_unavailable";
519
+ }
520
+ return undefined;
521
+ }
522
+ function errorMessage(error) {
523
+ return error instanceof Error ? error.message : String(error);
524
+ }
525
+ function amountFromRequirement(requirement) {
526
+ for (const key of ["maxAmountRequired", "amount", "max_amount_required"]) {
527
+ const value = requirement[key];
528
+ if (typeof value === "string" && value.trim()) {
529
+ return value.trim();
530
+ }
531
+ }
532
+ return null;
533
+ }
534
+ function validatePolicy(input, method, policy) {
535
+ if (!policy.enabled) {
536
+ return "policy_disabled";
537
+ }
538
+ const expectedAmount = input.expected_amount
539
+ ? parseDecimalAmount(input.expected_amount)
540
+ : null;
541
+ const maxPerCall = parseDecimalAmount(policy.max_amount_per_call);
542
+ if (input.expected_amount !== undefined && !expectedAmount) {
543
+ return "invalid_amount";
544
+ }
545
+ if (!maxPerCall) {
546
+ return "invalid_amount";
547
+ }
548
+ if (!policy.allowed_urls.includes(input.url)) {
549
+ return "url_not_allowed";
550
+ }
551
+ if (!policy.allowed_methods.includes(method)) {
552
+ return "method_not_allowed";
553
+ }
554
+ if (expectedAmount && compareDecimal(expectedAmount, maxPerCall) > 0) {
555
+ return "amount_over_limit";
556
+ }
557
+ if (expectedAmount) {
558
+ const maxPeriod = parseDecimalAmount(policy.max_amount_per_period);
559
+ const currentSpend = parseDecimalAmount(currentPeriodSpend(policy));
560
+ if (!maxPeriod || !currentSpend) {
561
+ return "invalid_amount";
562
+ }
563
+ if (compareDecimal(addDecimalParts(currentSpend, expectedAmount), maxPeriod) > 0) {
564
+ return "period_limit_exceeded";
565
+ }
566
+ }
567
+ return null;
568
+ }
569
+ function createPaymentId() {
570
+ return `pay_${randomUUID().replaceAll("-", "")}`;
571
+ }
572
+ function createPaymentReceiptId() {
573
+ return `receipt_${randomUUID().replaceAll("-", "")}`;
574
+ }
575
+ function hashReceipt(receiptJson) {
576
+ return createHash("sha256").update(receiptJson).digest("hex");
577
+ }
578
+ function compareDecimal(leftParts, rightParts) {
579
+ const scale = Math.max(leftParts.scale, rightParts.scale);
580
+ const leftValue = leftParts.value * 10n ** BigInt(scale - leftParts.scale);
581
+ const rightValue = rightParts.value * 10n ** BigInt(scale - rightParts.scale);
582
+ if (leftValue === rightValue) {
583
+ return 0;
584
+ }
585
+ return leftValue > rightValue ? 1 : -1;
586
+ }
587
+ function addDecimalParts(leftParts, rightParts) {
588
+ const scale = Math.max(leftParts.scale, rightParts.scale);
589
+ const leftValue = leftParts.value * 10n ** BigInt(scale - leftParts.scale);
590
+ const rightValue = rightParts.value * 10n ** BigInt(scale - rightParts.scale);
591
+ return {
592
+ value: leftValue + rightValue,
593
+ scale
594
+ };
595
+ }
596
+ function parseDecimalAmount(value) {
597
+ if (!/^\d+(\.\d+)?$/.test(value)) {
598
+ return null;
599
+ }
600
+ const [whole, fraction = ""] = value.split(".");
601
+ return {
602
+ value: BigInt(`${whole}${fraction}`),
603
+ scale: fraction.length
604
+ };
605
+ }
606
+ function currentPeriodSpend(policy, now = new Date()) {
607
+ const periodStartedAt = Date.parse(policy.period_started_at);
608
+ if (Number.isNaN(periodStartedAt) ||
609
+ now.getTime() - periodStartedAt >= policy.period_seconds * 1000) {
610
+ return "0";
611
+ }
612
+ return policy.current_period_spend;
613
+ }
@@ -0,0 +1,10 @@
1
+ import type { PaymentIntentRecord, PaymentReceiptRecord, PaymentStore } from "./types.js";
2
+ export declare class FilePaymentStore implements PaymentStore {
3
+ private readonly store;
4
+ constructor(dataDir: string);
5
+ saveIntent(intent: PaymentIntentRecord): Promise<void>;
6
+ getIntent(paymentId: string): Promise<PaymentIntentRecord | null>;
7
+ findIntentByIdempotencyKey(agentId: string, idempotencyKey: string): Promise<PaymentIntentRecord | null>;
8
+ saveReceipt(receipt: PaymentReceiptRecord): Promise<void>;
9
+ getReceipt(paymentId: string): Promise<PaymentReceiptRecord | null>;
10
+ }
@@ -0,0 +1,64 @@
1
+ import { join } from "node:path";
2
+ import { JsonFileStore } from "../storage/json-file-store.js";
3
+ export class FilePaymentStore {
4
+ store;
5
+ constructor(dataDir) {
6
+ this.store = new JsonFileStore({
7
+ filePath: join(dataDir, "payments.json"),
8
+ empty: emptyStore,
9
+ normalize: normalizeStore
10
+ });
11
+ }
12
+ async saveIntent(intent) {
13
+ await this.store.update((data) => {
14
+ const existingIndex = data.intents.findIndex((item) => item.id === intent.id);
15
+ if (existingIndex >= 0) {
16
+ data.intents[existingIndex] = intent;
17
+ }
18
+ else {
19
+ data.intents.push(intent);
20
+ }
21
+ });
22
+ }
23
+ async getIntent(paymentId) {
24
+ const data = await this.store.read();
25
+ return data.intents.find((intent) => intent.id === paymentId) ?? null;
26
+ }
27
+ async findIntentByIdempotencyKey(agentId, idempotencyKey) {
28
+ const data = await this.store.read();
29
+ return (data.intents.find((intent) => intent.agent_id === agentId && intent.idempotency_key === idempotencyKey) ?? null);
30
+ }
31
+ async saveReceipt(receipt) {
32
+ await this.store.update((data) => {
33
+ const existingIndex = data.receipts.findIndex((item) => item.id === receipt.id);
34
+ if (existingIndex >= 0) {
35
+ data.receipts[existingIndex] = receipt;
36
+ }
37
+ else {
38
+ data.receipts.push(receipt);
39
+ }
40
+ });
41
+ }
42
+ async getReceipt(paymentId) {
43
+ const data = await this.store.read();
44
+ return data.receipts.find((receipt) => receipt.payment_id === paymentId) ?? null;
45
+ }
46
+ }
47
+ function emptyStore() {
48
+ return {
49
+ schema_version: "sigil.payment-store.v1",
50
+ intents: [],
51
+ receipts: []
52
+ };
53
+ }
54
+ function normalizeStore(parsed) {
55
+ const data = asStoreObject(parsed);
56
+ return {
57
+ schema_version: "sigil.payment-store.v1",
58
+ intents: Array.isArray(data.intents) ? data.intents : [],
59
+ receipts: Array.isArray(data.receipts) ? data.receipts : []
60
+ };
61
+ }
62
+ function asStoreObject(parsed) {
63
+ return parsed && typeof parsed === "object" ? parsed : {};
64
+ }