@reinconsole/mock-rails 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -47,6 +47,8 @@ var DecisionId = prefixedId("dec");
47
47
  var ReceiptId = prefixedId("rcp");
48
48
  var SessionId = prefixedId("ses");
49
49
  var GateReceiptId = prefixedId("grc");
50
+ var ApiKeyId = prefixedId("key");
51
+ var ApproverKeyId = prefixedId("apk");
50
52
  var EnforcementMode = z.enum(["observed", "sdk", "session-key"]);
51
53
  var AgentWallet = z.object({
52
54
  chain: Chain,
@@ -54,12 +56,15 @@ var AgentWallet = z.object({
54
56
  mode: EnforcementMode
55
57
  });
56
58
  var AgentStatus = z.enum(["active", "frozen"]);
59
+ var AgentLabel = z.string().max(40).regex(/^[a-z0-9][a-z0-9-]*$/, 'label must be a lowercase slug like "research" or "prod-trading"');
57
60
  var Agent = z.object({
58
61
  id: AgentId,
59
62
  orgId: OrgId,
60
63
  name: z.string().min(1).max(200),
61
64
  /** On-chain ERC-8004 identity, if the agent is registered. */
62
65
  erc8004Id: z.string().optional(),
66
+ /** Semantic grouping labels — the targets of policy `appliesTo.labels`. */
67
+ labels: z.array(AgentLabel).max(16).refine((ls) => new Set(ls).size === ls.length, { message: "labels must be unique" }).default([]),
63
68
  wallets: z.array(AgentWallet).default([]),
64
69
  status: AgentStatus.default("active"),
65
70
  createdAt: z.coerce.date()
@@ -150,6 +155,17 @@ var SettledPayment = z.object({
150
155
  feePaid: DecimalString.optional(),
151
156
  confirmedAt: z.coerce.date()
152
157
  });
158
+ var SettlementReport = z.object({
159
+ intentId: IntentId,
160
+ /** The settling transaction, when the observer saw one. */
161
+ txHash: z.string().optional(),
162
+ chain: Chain.optional(),
163
+ /** The amount as SETTLED, when known — may differ from the amount allowed. */
164
+ amount: DecimalString.optional(),
165
+ /** Free-form observer tag, e.g. "indexer", "guard", "facilitator". */
166
+ source: z.string().min(1).max(64).optional(),
167
+ confirmedAt: z.coerce.date()
168
+ });
153
169
  var ReceiptSettlement = z.object({
154
170
  txHash: z.string().optional(),
155
171
  networkId: z.string().optional(),
@@ -208,15 +224,58 @@ var Condition = z.object({
208
224
  txCount: z.object({ window: Window, gt: z.number().int().nonnegative() }).optional(),
209
225
  /** Vendor host matches one of these patterns (supports `*` globs). */
210
226
  vendorHostIn: z.array(z.string()).optional(),
227
+ /**
228
+ * Resource PATH matches one of these patterns (supports `*` globs, e.g.
229
+ * "/v1/reports/*"). A full-URL resource is reduced to its pathname before
230
+ * matching (see `resourcePathOf`), so the same pattern works whether the
231
+ * vendor declares "https://api.x.com/v1/answer" or "/v1/answer". Pair
232
+ * with `vendorHostIn` to scope a path rule to specific vendors.
233
+ */
234
+ resourceIn: z.array(z.string()).optional(),
211
235
  /** First time Rein has seen this vendor for the agent. */
212
236
  vendorFirstSeen: z.boolean().optional(),
213
237
  /** Vendor reputation score is below this threshold (Phase 3 hook). */
214
238
  vendorReputationLt: z.number().min(0).max(100).optional(),
215
239
  /** Amount is more than `gt`-times the observed median for this resource. */
216
- amountVsResourceMedian: z.object({ gt: Multiplier }).optional()
240
+ amountVsResourceMedian: z.object({ gt: Multiplier }).optional(),
241
+ /**
242
+ * Cumulative spend attributed to THIS intent's `taskContext.taskId`
243
+ * (prior spend on the task plus this payment) exceeds `gt`. Scopes a
244
+ * budget to one unit of work rather than to a window: a research run that
245
+ * is meant to cost a dollar cannot quietly cost fifty, however slowly.
246
+ *
247
+ * An intent carrying no `taskId` cannot be attributed to a task, so the
248
+ * predicate never triggers for one. Requiring attribution is a separate,
249
+ * deliberate rule (deny on `taskIdMissing`), not a side effect of setting
250
+ * a budget — otherwise every untagged probe payment would trip every
251
+ * task budget in the policy.
252
+ */
253
+ taskBudget: z.object({ gt: DecimalString }).optional(),
254
+ /** The intent carries no `taskContext.taskId` (or an empty one). */
255
+ taskIdMissing: z.boolean().optional()
217
256
  }).refine((c) => Object.values(c).some((v) => v !== void 0), {
218
257
  message: "a condition must specify at least one predicate"
219
258
  });
259
+ var Breaker = z.object({
260
+ id: z.string().min(1),
261
+ /** The trailing span the tripwires measure over. */
262
+ window: Window,
263
+ /**
264
+ * Trip when the window would hold MORE than this many transactions.
265
+ * `10` permits ten in the window and escalates the eleventh.
266
+ */
267
+ txCount: z.number().int().positive().optional(),
268
+ /**
269
+ * Trip when the window's spend would exceed this total. `"50.00"` permits
270
+ * a window totalling exactly 50.00 and escalates the payment that would
271
+ * carry it past. Prospective, like every other spend predicate: the
272
+ * payment that would breach the cap is the one that escalates, rather
273
+ * than the innocent one after it.
274
+ */
275
+ valueCap: DecimalString.optional()
276
+ }).refine((b) => b.txCount !== void 0 || b.valueCap !== void 0, {
277
+ message: "a breaker must specify at least one of txCount / valueCap"
278
+ });
220
279
  var Rule = z.object({
221
280
  id: z.string().min(1),
222
281
  allow: Condition.optional(),
@@ -229,6 +288,13 @@ var PolicyDefault = z.enum(["allow", "deny"]);
229
288
  var AppliesTo = z.object({
230
289
  /** Agent id patterns (supports `*` globs, e.g. "agt_research_*"). */
231
290
  agents: z.array(z.string()).optional(),
291
+ /**
292
+ * Agent label patterns (supports `*` globs) — semantic targeting. Matches
293
+ * when the agent carries at least one label matching any pattern. Requires
294
+ * the agent DOCUMENT: an intent from an unregistered agent never matches a
295
+ * labels-targeted policy (and so fails closed when no other policy applies).
296
+ */
297
+ labels: z.array(z.string()).optional(),
232
298
  chains: z.array(Chain).optional()
233
299
  });
234
300
  var Escalation = z.object({
@@ -241,11 +307,134 @@ var Policy = z.object({
241
307
  version: z.string().default("1"),
242
308
  appliesTo: AppliesTo.default({}),
243
309
  rules: z.array(Rule).default([]),
310
+ /**
311
+ * Behavioral breakers evaluated alongside the rules. A tripped breaker
312
+ * escalates at the same precedence as an `escalate` rule — so an explicit
313
+ * DENY still wins, and an ALLOW rule cannot wave past a tripped breaker.
314
+ */
315
+ breakers: z.array(Breaker).default([]),
244
316
  default: PolicyDefault.default("deny"),
245
317
  /** Below this amount, fail-open is permitted during a policy-service outage. */
246
318
  denyFloor: DecimalString.default("0.05"),
247
319
  escalation: Escalation.optional()
248
320
  });
321
+ var LivenessSource = z.enum([
322
+ /** It submitted an intent — ANY outcome, including a denial. */
323
+ "intent",
324
+ /** It reported in explicitly, having nothing to buy. */
325
+ "heartbeat"
326
+ ]);
327
+ var DEFAULT_LIVENESS_GRACE_MS = 6e4;
328
+ var LivenessExpectation = z.object({
329
+ agentId: AgentId,
330
+ /** Longest silence still considered normal, e.g. '15m'. */
331
+ interval: Window,
332
+ /** Extra slack past `interval` before the silence is an alarm. */
333
+ graceMs: z.number().int().nonnegative().default(DEFAULT_LIVENESS_GRACE_MS),
334
+ /**
335
+ * When watching began. An agent that has NEVER been seen is measured from
336
+ * here, so a watch on an agent that never starts does raise an alarm — a
337
+ * detector that never came up is exactly as dead as one that stopped.
338
+ */
339
+ since: z.coerce.date(),
340
+ /** What this agent is supposed to be doing, shown beside the alarm. */
341
+ note: z.string().max(200).optional()
342
+ });
343
+ var LivenessWatchInput = LivenessExpectation.omit({ since: true }).extend({
344
+ graceMs: z.number().int().nonnegative().optional()
345
+ });
346
+ var Heartbeat = z.object({
347
+ agentId: AgentId,
348
+ /** When the agent was alive. Defaults to arrival time. */
349
+ at: z.coerce.date().optional(),
350
+ /** Free-form note from the reporter, e.g. "poll cycle 41 ok". */
351
+ note: z.string().max(200).optional()
352
+ });
353
+ var LivenessStatus = z.enum(["alive", "late", "missing", "unknown"]);
354
+ var ApiKeyScope = z.enum(["read", "evaluate", "approve", "admin"]);
355
+ var ApiKey = z.object({
356
+ id: ApiKeyId,
357
+ name: z.string().min(1).max(200),
358
+ scopes: z.array(ApiKeyScope).min(1),
359
+ createdAt: z.coerce.date(),
360
+ /** Last successful authentication, for spotting keys that can be retired. */
361
+ lastUsedAt: z.coerce.date().optional(),
362
+ /** Set by revoke(); a revoked key never authenticates again. */
363
+ revokedAt: z.coerce.date().optional(),
364
+ /** Set by rotate(); with `previousSecretExpiresAt` it dates the overlap. */
365
+ rotatedAt: z.coerce.date().optional()
366
+ });
367
+ var ApiKeyRecord = ApiKey.extend({
368
+ secretHash: z.string(),
369
+ previousSecretHash: z.string().optional(),
370
+ previousSecretExpiresAt: z.coerce.date().optional()
371
+ });
372
+ var ApprovalVerdict = z.enum(["approve", "reject"]);
373
+ var ApprovalStatus = z.enum(["pending", "approved", "rejected", "expired"]);
374
+ var ApproverKey = z.object({
375
+ id: ApproverKeyId,
376
+ orgId: OrgId,
377
+ name: z.string().min(1).max(200),
378
+ /** ed25519 public key, SPKI PEM — the same encoding as the decision log's. */
379
+ publicKey: z.string().min(1),
380
+ algorithm: z.literal("ed25519").default("ed25519"),
381
+ createdAt: z.coerce.date(),
382
+ revokedAt: z.coerce.date().optional()
383
+ });
384
+ var ApprovalRequest = z.object({
385
+ /** The `escalate` decision this request answers (primary key). */
386
+ decisionId: DecisionId,
387
+ intentId: IntentId,
388
+ /** Binds the approval to the exact transfer — the signed bytes commit to it. */
389
+ intentHash: z.string(),
390
+ agentId: AgentId,
391
+ vendorHost: z.string(),
392
+ resource: z.string(),
393
+ amount: DecimalString,
394
+ asset: Asset,
395
+ chain: Chain,
396
+ /**
397
+ * Task attribution copied from the intent. Carried so an approved payment
398
+ * still counts against its `taskBudget` — an escalation that lost its task
399
+ * id on the way through would make the budget silently under-count exactly
400
+ * the payments a human had to look at.
401
+ */
402
+ taskId: z.string().optional(),
403
+ /** Why policy escalated, verbatim from the decision. */
404
+ reason: z.string(),
405
+ /**
406
+ * Breaker ids whose trip contributed to this escalation. An approval of
407
+ * this request resets exactly these — moving their counting floor to the
408
+ * approval instant — so a human waving one payment through also clears the
409
+ * behavior that stopped it, rather than being asked again immediately.
410
+ * Empty when ordinary rules did the escalating.
411
+ */
412
+ breakers: z.array(z.string()).default([]),
413
+ status: ApprovalStatus,
414
+ createdAt: z.coerce.date(),
415
+ /** Past this instant the request denies, whatever arrives afterwards. */
416
+ expiresAt: z.coerce.date(),
417
+ resolvedAt: z.coerce.date().optional(),
418
+ /** Which registered key signed — absent when the TTL lapsed instead. */
419
+ approverKeyId: ApproverKeyId.optional(),
420
+ /**
421
+ * The follow-up decision appended to the chain when this resolved. That
422
+ * decision, not this record, is the voucher a signer verifies: the original
423
+ * `escalate` decision is never rewritten.
424
+ */
425
+ finalDecisionId: DecisionId.optional()
426
+ });
427
+ var ApprovalChallenges = z.object({
428
+ approve: z.string(),
429
+ reject: z.string()
430
+ });
431
+ var ApprovalGrant = z.object({
432
+ decisionId: DecisionId,
433
+ intentHash: z.string(),
434
+ verdict: ApprovalVerdict,
435
+ approverKeyId: ApproverKeyId,
436
+ signature: z.string().min(1)
437
+ });
249
438
  var ReputationSubject = z.object({
250
439
  kind: z.enum(["agent", "vendor"]),
251
440
  id: z.string()
@@ -307,6 +496,36 @@ var ReinEvent = z.discriminatedUnion("type", [
307
496
  network: z.string()
308
497
  }),
309
498
  z.object({ type: z.literal("gate.settled"), at: z.coerce.date(), receipt: GateReceipt }),
499
+ z.object({
500
+ type: z.literal("approval.requested"),
501
+ at: z.coerce.date(),
502
+ request: ApprovalRequest
503
+ }),
504
+ z.object({
505
+ type: z.literal("approval.resolved"),
506
+ at: z.coerce.date(),
507
+ request: ApprovalRequest,
508
+ /** The follow-up allow/deny decision this resolution appended. */
509
+ decision: Decision
510
+ }),
511
+ z.object({
512
+ type: z.literal("liveness.missing"),
513
+ at: z.coerce.date(),
514
+ agentId: AgentId,
515
+ expectation: LivenessExpectation,
516
+ /** How long the agent has been silent when the alarm was raised. */
517
+ silentMs: z.number().int().nonnegative(),
518
+ /** Last sighting, absent when the agent was never seen at all. */
519
+ lastSeenAt: z.coerce.date().optional()
520
+ }),
521
+ z.object({
522
+ type: z.literal("liveness.recovered"),
523
+ at: z.coerce.date(),
524
+ agentId: AgentId,
525
+ /** How long the silence lasted, measured to this sighting. */
526
+ silentMs: z.number().int().nonnegative(),
527
+ source: LivenessSource
528
+ }),
310
529
  z.object({
311
530
  type: z.literal("gate.refused"),
312
531
  at: z.coerce.date(),
@@ -351,8 +570,79 @@ var MockLedger = class {
351
570
 
352
571
  // ../../packages/sdk/dist/index.js
353
572
  import { z as z2 } from "zod";
354
- var Health = z2.object({ status: z2.string(), publicKey: z2.string() });
355
- var EvaluateResponse = z2.object({ intent: PaymentIntent, decision: Decision });
573
+ var BreakerState = z2.object({
574
+ breaker: Breaker,
575
+ policyId: z2.string(),
576
+ txCount: z2.number(),
577
+ sum: z2.string(),
578
+ countingFrom: z2.number(),
579
+ resetAt: z2.number().optional(),
580
+ tripped: z2.boolean(),
581
+ reason: z2.string().optional()
582
+ });
583
+ var LivenessState = z2.object({
584
+ agentId: z2.string(),
585
+ expectation: LivenessExpectation,
586
+ lastSeenAt: z2.number().optional(),
587
+ lastSource: LivenessSource.optional(),
588
+ silentSince: z2.number(),
589
+ silentMs: z2.number(),
590
+ dueAt: z2.number(),
591
+ overdueAt: z2.number(),
592
+ status: LivenessStatus,
593
+ alertedAt: z2.number().optional()
594
+ });
595
+ var Health = z2.object({
596
+ status: z2.string(),
597
+ publicKey: z2.string(),
598
+ /** "api-key" once the engine demands a credential; "none" while it does not. */
599
+ auth: z2.string().optional(),
600
+ approvals: z2.string().optional()
601
+ });
602
+ var EvaluateResponse = z2.object({
603
+ intent: PaymentIntent,
604
+ decision: Decision,
605
+ /** Present when an escalation was parked for a signed verdict. */
606
+ approval: ApprovalRequest.optional()
607
+ });
608
+ var ApprovalView = z2.object({
609
+ request: ApprovalRequest,
610
+ challenges: ApprovalChallenges,
611
+ /** The follow-up allow/deny decision, once the request has resolved. */
612
+ decision: Decision.optional()
613
+ });
614
+ var ResolveResponse = z2.object({ request: ApprovalRequest, decision: Decision });
615
+ var AllowanceGap = z2.object({
616
+ intentId: z2.string(),
617
+ decisionId: z2.string().optional(),
618
+ agentId: z2.string(),
619
+ host: z2.string(),
620
+ resource: z2.string(),
621
+ amount: z2.string(),
622
+ taskId: z2.string().optional(),
623
+ allowedAt: z2.number(),
624
+ ageMs: z2.number(),
625
+ state: z2.enum(["in-flight", "unsettled"])
626
+ });
627
+ var ReconciliationReport = z2.object({
628
+ from: z2.number(),
629
+ to: z2.number(),
630
+ window: z2.string(),
631
+ graceMs: z2.number(),
632
+ allowed: z2.number(),
633
+ allowedValue: z2.string(),
634
+ settled: z2.number(),
635
+ settledValue: z2.string(),
636
+ inFlight: z2.number(),
637
+ inFlightValue: z2.string(),
638
+ unsettled: z2.number(),
639
+ unsettledValue: z2.string(),
640
+ unattributed: z2.number(),
641
+ settlementsSeen: z2.number(),
642
+ gaps: z2.array(AllowanceGap),
643
+ truncated: z2.boolean()
644
+ });
645
+ var IssuedApiKey = z2.object({ key: ApiKey, secret: z2.string() });
356
646
  var PaymentRequirement = z2.object({
357
647
  scheme: z2.string(),
358
648
  network: z2.string(),
@@ -417,6 +707,81 @@ function requirementDecimals(requirement) {
417
707
  const decimals = requirement.extra?.["decimals"];
418
708
  return typeof decimals === "number" && Number.isInteger(decimals) && decimals >= 0 ? decimals : 6;
419
709
  }
710
+ var CAIP2_ALIASES = {
711
+ base: "eip155:8453",
712
+ "base-sepolia": "eip155:84532",
713
+ polygon: "eip155:137",
714
+ "polygon-amoy": "eip155:80002",
715
+ bnb: "eip155:56",
716
+ bsc: "eip155:56",
717
+ solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
718
+ "solana-devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"
719
+ };
720
+ function caip2Of(network) {
721
+ const key = network.toLowerCase();
722
+ return CAIP2_ALIASES[key] ?? key;
723
+ }
724
+ function sameNetwork(a, b) {
725
+ return caip2Of(a) === caip2Of(b);
726
+ }
727
+ var PaymentRequirementsV2 = z2.object({
728
+ scheme: z2.string(),
729
+ /** CAIP-2, e.g. "eip155:84532". */
730
+ network: z2.string(),
731
+ /** Atomic units — v2's rename of v1's maxAmountRequired. */
732
+ amount: z2.string().regex(/^\d+$/, "atomic amount must be an integer string"),
733
+ asset: z2.string().min(1),
734
+ payTo: z2.string().min(1),
735
+ maxTimeoutSeconds: z2.number().optional(),
736
+ extra: z2.record(z2.unknown()).optional()
737
+ });
738
+ var ResourceInfoV2 = z2.object({
739
+ url: z2.string(),
740
+ description: z2.string().optional(),
741
+ mimeType: z2.string().optional()
742
+ });
743
+ var PaymentRequiredV2 = z2.object({
744
+ x402Version: z2.literal(2),
745
+ error: z2.string().optional(),
746
+ resource: ResourceInfoV2.optional(),
747
+ accepts: z2.array(PaymentRequirementsV2).min(1),
748
+ extensions: z2.record(z2.unknown()).optional()
749
+ });
750
+ function v2Requirements(requirement) {
751
+ return {
752
+ scheme: requirement.scheme,
753
+ network: caip2Of(requirement.network),
754
+ amount: requirement.maxAmountRequired,
755
+ asset: requirement.asset,
756
+ payTo: requirement.payTo,
757
+ ...requirement.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: requirement.maxTimeoutSeconds } : {},
758
+ ...requirement.extra ? { extra: requirement.extra } : {}
759
+ };
760
+ }
761
+ function buildPaymentRequiredV2(requirement, error) {
762
+ return {
763
+ x402Version: 2,
764
+ error,
765
+ ...requirement.resource ? {
766
+ resource: {
767
+ url: requirement.resource,
768
+ ...requirement.description ? { description: requirement.description } : {},
769
+ ...requirement.mimeType ? { mimeType: requirement.mimeType } : {}
770
+ }
771
+ } : {},
772
+ accepts: [v2Requirements(requirement)],
773
+ extensions: {}
774
+ };
775
+ }
776
+ function encodeBase64Json(value) {
777
+ return Buffer.from(JSON.stringify(value)).toString("base64");
778
+ }
779
+ var V1Envelope = z2.object({
780
+ x402Version: z2.literal(1),
781
+ scheme: z2.string(),
782
+ network: z2.string(),
783
+ payload: z2.record(z2.unknown())
784
+ });
420
785
 
421
786
  // src/errors.ts
422
787
  var FacilitatorError = class extends Error {
@@ -450,6 +815,17 @@ var MockPaymentHeader = z3.object({
450
815
  network: z3.string(),
451
816
  payload: MockExactPayload
452
817
  });
818
+ var MockPaymentHeaderV2 = z3.object({
819
+ x402Version: z3.literal(2),
820
+ accepted: z3.object({
821
+ scheme: z3.string(),
822
+ network: z3.string(),
823
+ amount: z3.string().regex(/^\d+$/, "atomic amount must be an integer string"),
824
+ asset: z3.string().min(1),
825
+ payTo: z3.string().min(1)
826
+ }).passthrough(),
827
+ payload: MockExactPayload
828
+ }).passthrough();
453
829
  function encodePaymentHeader(header) {
454
830
  return Buffer.from(JSON.stringify(header)).toString("base64");
455
831
  }
@@ -460,11 +836,37 @@ function decodePaymentHeader(raw) {
460
836
  } catch {
461
837
  throw new FacilitatorError("malformed_payment", "X-PAYMENT is not base64-encoded JSON");
462
838
  }
839
+ if (json?.x402Version === 2) {
840
+ const parsed2 = MockPaymentHeaderV2.safeParse(json);
841
+ if (!parsed2.success) {
842
+ throw new FacilitatorError(
843
+ "malformed_payment",
844
+ `invalid v2 payment envelope: ${parsed2.error.message}`
845
+ );
846
+ }
847
+ if (parsed2.data.payload.value !== parsed2.data.accepted.amount) {
848
+ throw new FacilitatorError(
849
+ "malformed_payment",
850
+ `v2 envelope contradicts itself: accepted.amount ${parsed2.data.accepted.amount} vs payload value ${parsed2.data.payload.value}`
851
+ );
852
+ }
853
+ return {
854
+ version: 2,
855
+ scheme: parsed2.data.accepted.scheme,
856
+ network: parsed2.data.accepted.network,
857
+ payload: parsed2.data.payload
858
+ };
859
+ }
463
860
  const parsed = MockPaymentHeader.safeParse(json);
464
861
  if (!parsed.success) {
465
862
  throw new FacilitatorError("malformed_payment", `invalid X-PAYMENT body: ${parsed.error.message}`);
466
863
  }
467
- return parsed.data;
864
+ return {
865
+ version: 1,
866
+ scheme: parsed.data.scheme,
867
+ network: parsed.data.network,
868
+ payload: parsed.data.payload
869
+ };
468
870
  }
469
871
  var SettlementResponse = z3.object({
470
872
  success: z3.boolean(),
@@ -507,7 +909,8 @@ var MockFacilitator = class {
507
909
  }
508
910
  });
509
911
  }
510
- /** Verify an X-PAYMENT header against the requirement it claims to satisfy. */
912
+ /** Verify a payment header (either dialect) against the requirement it
913
+ * claims to satisfy. */
511
914
  verify(paymentHeader, requirement) {
512
915
  const header = decodePaymentHeader(paymentHeader);
513
916
  if (header.scheme.toLowerCase() !== "exact" || requirement.scheme.toLowerCase() !== "exact") {
@@ -516,7 +919,7 @@ var MockFacilitator = class {
516
919
  `mock facilitator only settles "exact", got "${header.scheme}"/"${requirement.scheme}"`
517
920
  );
518
921
  }
519
- if (header.network !== requirement.network) {
922
+ if (!sameNetwork(header.network, requirement.network)) {
520
923
  throw new FacilitatorError(
521
924
  "network_mismatch",
522
925
  `payment is on "${header.network}" but the requirement wants "${requirement.network}"`
@@ -675,6 +1078,7 @@ function sameAddress(a, b) {
675
1078
  // src/vendor.ts
676
1079
  function createMockVendor(options) {
677
1080
  const calls = [];
1081
+ const advertise = options.advertise ?? "v1";
678
1082
  const requirementFor = (url) => PaymentRequirement.parse({
679
1083
  scheme: options.scheme ?? "exact",
680
1084
  network: options.network ?? "base",
@@ -683,18 +1087,27 @@ function createMockVendor(options) {
683
1087
  payTo: options.payTo,
684
1088
  asset: options.asset ?? "USDC"
685
1089
  });
686
- const paymentRequired = (url, error) => new Response(
687
- JSON.stringify({ x402Version: 1, accepts: [requirementFor(url)], error }),
688
- { status: 402, headers: { "content-type": "application/json" } }
689
- );
1090
+ const paymentRequired = (url, error) => {
1091
+ const requirement = requirementFor(url);
1092
+ const body = advertise === "v2" ? { error } : { x402Version: 1, accepts: [requirement], error };
1093
+ return new Response(JSON.stringify(body), {
1094
+ status: 402,
1095
+ headers: {
1096
+ "content-type": "application/json",
1097
+ ...advertise !== "v1" ? {
1098
+ "PAYMENT-REQUIRED": encodeBase64Json(buildPaymentRequiredV2(requirement, error))
1099
+ } : {}
1100
+ }
1101
+ });
1102
+ };
690
1103
  const fetch = async (input, init) => {
691
1104
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
692
1105
  const headers = new Headers(
693
1106
  init?.headers ?? (input instanceof Request ? input.headers : void 0)
694
1107
  );
695
- const payment = headers.get("X-PAYMENT");
1108
+ const payment = headers.get("PAYMENT-SIGNATURE") ?? headers.get("X-PAYMENT");
696
1109
  calls.push({ url, payment });
697
- if (payment === null) return paymentRequired(url, "X-PAYMENT header is required");
1110
+ if (payment === null) return paymentRequired(url, "payment is required");
698
1111
  let settled;
699
1112
  try {
700
1113
  settled = options.facilitator.settle(payment, requirementFor(url));
@@ -704,7 +1117,13 @@ function createMockVendor(options) {
704
1117
  }
705
1118
  return new Response(JSON.stringify(options.body ?? { ok: true }), {
706
1119
  status: 200,
707
- headers: { "content-type": "application/json", "X-PAYMENT-RESPONSE": settled.header }
1120
+ headers: {
1121
+ "content-type": "application/json",
1122
+ // Both dialect names carry the same base64 settlement object; the
1123
+ // strict v2 vendor sets ONLY the v2 name.
1124
+ ...advertise !== "v2" ? { "X-PAYMENT-RESPONSE": settled.header } : {},
1125
+ "PAYMENT-RESPONSE": settled.header
1126
+ }
708
1127
  });
709
1128
  };
710
1129
  return { fetch, calls, requirementFor };
@@ -716,6 +1135,7 @@ export {
716
1135
  MockIndexer,
717
1136
  MockLedger,
718
1137
  MockPaymentHeader,
1138
+ MockPaymentHeaderV2,
719
1139
  SettlementResponse,
720
1140
  createMockVendor,
721
1141
  decodePaymentHeader,