@visa/cli 4.1.0-rc.83 → 4.1.0-rc.85

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.
@@ -236,7 +236,13 @@ export async function runHostedApproval(opts) {
236
236
  if (doc?.status === 'declined')
237
237
  throw new HostedApprovalDeclinedError();
238
238
  if (doc?.status === 'completed') {
239
- if (doc.assuranceData === undefined || doc.assuranceData === null) {
239
+ // A passkey-exempt token ('otp' cardholder ID&V / 'none') runs no
240
+ // ceremony and produces no assurance — the approval server marks the
241
+ // claim explicitly (derived from the owner's verified card record at
242
+ // complete-time). Only that marker excuses a null assuranceData; a
243
+ // silently dropped payload still fails exactly as before.
244
+ const assuranceExempt = doc.cardholderVerification === 'otp' || doc.cardholderVerification === 'none';
245
+ if ((doc.assuranceData === undefined || doc.assuranceData === null) && !assuranceExempt) {
240
246
  throw new Error('hosted approval completed but carried no assuranceData');
241
247
  }
242
248
  // The context the approval was COMPLETED against must be exactly this
@@ -304,9 +310,12 @@ export async function runHostedApproval(opts) {
304
310
  else if (doc.validUntil !== undefined) {
305
311
  throw new Error('hosted one-purchase approval unexpectedly carried a budget expiry');
306
312
  }
307
- log('passkey approval received from the hosted page.');
313
+ log(assuranceExempt
314
+ ? 'approval received from the hosted page (code-verified card, no passkey).'
315
+ : 'passkey approval received from the hosted page.');
308
316
  return {
309
- ...assuranceFromCeremony(target, doc.assuranceData),
317
+ ...assuranceFromCeremony(target, doc.assuranceData ?? null),
318
+ ...(assuranceExempt ? { assuranceExempt: true } : {}),
310
319
  ...(typeof doc.mintToken === 'string' && doc.mintToken
311
320
  ? { mintToken: doc.mintToken }
312
321
  : {}),
@@ -42,6 +42,15 @@ export type PurchaseAssurance = {
42
42
  transactionAmount: string;
43
43
  /** ISO 4217 currency the ceremony was scoped to. */
44
44
  transactionCurrencyCode: string;
45
+ /**
46
+ * True iff the approval server marked this token passkey-exempt ('otp'
47
+ * cardholder ID&V / 'none' — verified against the owner's account card
48
+ * record at complete-time): no ceremony ran, `assuranceData` is null by
49
+ * design, and the intent is minted without it (the signed mint token
50
+ * carries the same exemption for the server's own check). Absent/false →
51
+ * assuranceData is REQUIRED, exactly the historical contract.
52
+ */
53
+ assuranceExempt?: boolean;
45
54
  };
46
55
  /**
47
56
  * Our fail-closed freshness bound, matching the runner's 15-minute checkout
@@ -106,7 +106,10 @@ export function validatePurchaseAssurance(value, target, now = new Date()) {
106
106
  if (value == null || typeof value !== 'object') {
107
107
  throw new Error(`purchase assurance is missing — ${remedy}`);
108
108
  }
109
- if (value.assuranceData == null) {
109
+ // A passkey-exempt approval (marked by the approval SERVER, never assumed
110
+ // locally) carries no assurance by design — every scope/freshness check
111
+ // below still applies to it unchanged.
112
+ if (value.assuranceData == null && value.assuranceExempt !== true) {
110
113
  throw new Error(`purchase assurance requires assuranceData — ${remedy}`);
111
114
  }
112
115
  const mintedAtMs = typeof value.mintedAt === 'string' ? Date.parse(value.mintedAt) : NaN;