@visa/cli 4.1.0-rc.112 → 4.1.0-rc.114

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.
@@ -98,6 +98,12 @@ export type CliClaimMandateInput = {
98
98
  agentRef?: string;
99
99
  /** Single-use pickup code the owner handed over from the panel. */
100
100
  pickupCode: string;
101
+ /**
102
+ * The agent handoff this runtime just redeemed, when the pickup code rode
103
+ * inside its claim code. A dialog-minted budget pins that handoff instead of
104
+ * a key — there was no key yet — so this is what the pin is checked against.
105
+ */
106
+ expectedHandoffId?: string;
101
107
  credentialPath: string;
102
108
  /** See {@link CardInstrumentSource}. */
103
109
  cardTokenId?: string;
@@ -374,6 +374,7 @@ export function createCliCheckoutEngine(deps = {}) {
374
374
  baseUrl: input.approvalBaseUrl,
375
375
  pickupCode: input.pickupCode,
376
376
  agentJkt: registerCap.agentJkt,
377
+ ...(input.expectedHandoffId ? { expectedHandoffId: input.expectedHandoffId } : {}),
377
378
  tokenId: credential.tokenId,
378
379
  });
379
380
  const ceilingMinor = decimalToMinor(claim.ceiling);
@@ -174,6 +174,13 @@ export type ClaimMandatePickupOptions = {
174
174
  pickupCode: string;
175
175
  /** This runtime's exact current request-key JKT — the only key the code may fund. */
176
176
  agentJkt: string;
177
+ /**
178
+ * The agent handoff this runtime just redeemed, when the code came from one.
179
+ * A budget minted in the Add-agent dialog exists before the agent does, so it
180
+ * pins the handoff rather than a key; this is the value that pin is compared
181
+ * against. Absent for the panel flow, whose budgets pin a key instead.
182
+ */
183
+ expectedHandoffId?: string;
177
184
  /** The card token id this runtime's card authority points at. */
178
185
  tokenId: string;
179
186
  fetchImpl?: typeof fetch;
@@ -358,7 +358,7 @@ function pickupMismatch(field, detail) {
358
358
  'mandate in the panel for this agent');
359
359
  }
360
360
  export async function claimMandatePickup(opts) {
361
- const { baseUrl, pickupCode, agentJkt, tokenId, fetchImpl = fetch, sleep = defaultSleep, now = Date.now, attempts = 3, retryDelayMs = 1_000, } = opts;
361
+ const { baseUrl, pickupCode, agentJkt, expectedHandoffId, tokenId, fetchImpl = fetch, sleep = defaultSleep, now = Date.now, attempts = 3, retryDelayMs = 1_000, } = opts;
362
362
  // Both producers (runHostedApproval and the panel initiate route) mint the
363
363
  // verifier as randomBytes(32).base64url = 43 chars; refuse anything else
364
364
  // before it travels.
@@ -412,7 +412,34 @@ export async function claimMandatePickup(opts) {
412
412
  if (ctx.budget !== true) {
413
413
  throw pickupMismatch('budget', 'the approval is a one-purchase approval, not a spend budget');
414
414
  }
415
- if (ctx.agentJkt !== agentJkt) {
415
+ // A budget names exactly ONE agent, by one of two pins.
416
+ //
417
+ // - `agentJkt` — the panel flow: the owner picked an agent that already
418
+ // existed, so the approval names its key and this runtime compares keys.
419
+ // - `handoffId` — the Add-agent dialog: the budget was minted before any
420
+ // agent existed, so it names the handoff the agent would go on to redeem.
421
+ // This runtime redeemed one moments ago and knows which, so the comparison
422
+ // is just as concrete — it is simply a different identifier.
423
+ //
424
+ // EXACTLY one, enforced before either is compared. Neither pin is an unbound
425
+ // budget, and "no pin" must never be the quiet way past a binding check.
426
+ // BOTH pins is worse: it names two agents, and checking whichever one happens
427
+ // to match would let the weaker claim decide. The signer refuses to mint such
428
+ // a context and the approval store refuses to hold one, so this is the third
429
+ // and last place to say it — the one that runs on material already in hand.
430
+ const hasHandoffPin = typeof ctx.handoffId === 'string';
431
+ const hasAgentPin = typeof ctx.agentJkt === 'string';
432
+ if (hasHandoffPin === hasAgentPin) {
433
+ throw pickupMismatch('agentJkt', hasHandoffPin
434
+ ? 'this budget names two different agents at once — it binds to a handoff or to a key, never both'
435
+ : 'this budget names no agent at all, so nothing can prove it belongs to this runtime');
436
+ }
437
+ if (hasHandoffPin) {
438
+ if (!expectedHandoffId || ctx.handoffId !== expectedHandoffId) {
439
+ throw pickupMismatch('handoffId', 'this budget belongs to a different agent handoff than the one this runtime just claimed');
440
+ }
441
+ }
442
+ else if (ctx.agentJkt !== agentJkt) {
416
443
  throw pickupMismatch('agentJkt', 'the owner approved this mandate for a different agent key than this runtime holds');
417
444
  }
418
445
  if (ctx.tokenId !== tokenId) {