@visa/cli 4.1.0-rc.147 → 4.1.0-rc.149
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/checkout-engine/cli-engine.d.ts +4 -4
- package/dist/checkout-engine/cli-engine.js +17 -19
- package/dist/checkout-engine/hosted-approval.d.ts +14 -13
- package/dist/checkout-engine/hosted-approval.js +21 -35
- package/dist/checkout-engine/mandate/card-mandate.js +1 -1
- package/dist/checkout-engine/mandate/mandate-ledger.d.ts +4 -4
- package/dist/checkout-engine/mandate/mandate-ledger.js +2 -2
- package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +7 -7
- package/dist/checkout-engine/vgs-live-instrument.d.ts +0 -35
- package/dist/checkout-engine/vgs-live-instrument.js +0 -67
- package/dist/cli.js +157 -157
- package/dist/mcp-server/index.js +126 -126
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -65,11 +65,11 @@ export type CliReceiptFacts = {
|
|
|
65
65
|
vicConfirmation: VicConfirmationReport | null;
|
|
66
66
|
/**
|
|
67
67
|
* Which credential path actually ran: `mandate` = tap-free draw against an
|
|
68
|
-
* existing card mandate; `
|
|
69
|
-
*
|
|
70
|
-
*
|
|
68
|
+
* existing card mandate; `null` = none ran (a pre-flight refusal, e.g. no
|
|
69
|
+
* prepared review or no covering mandate). Transparency, never magic — the
|
|
70
|
+
* caller can always see whether a passkey was skipped.
|
|
71
71
|
*/
|
|
72
|
-
source: 'mandate' |
|
|
72
|
+
source: 'mandate' | null;
|
|
73
73
|
/** Remaining mandate budget (minor units) after a mandate draw; else null. */
|
|
74
74
|
remainingMinor: number | null;
|
|
75
75
|
/**
|
|
@@ -318,8 +318,7 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
318
318
|
};
|
|
319
319
|
// BUDGET mode: the ceiling target's amount IS the approved ceiling, so the
|
|
320
320
|
// server mints a budget mint token bound to that ceiling — later draws pull
|
|
321
|
-
// sub-ceiling amounts against it tap-free
|
|
322
|
-
// path below stays non-budget).
|
|
321
|
+
// sub-ceiling amounts against it tap-free.
|
|
323
322
|
const assurance = await runHostedApproval({
|
|
324
323
|
baseUrl: input.approvalBaseUrl,
|
|
325
324
|
tokenId: credential.tokenId,
|
|
@@ -623,14 +622,15 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
623
622
|
try {
|
|
624
623
|
// NO instrument read here. A tap-free mandate draw spends `covering
|
|
625
624
|
// .tokenId` (frozen into the mandate at start) and needs neither the
|
|
626
|
-
// legacy credential file nor a grant token
|
|
627
|
-
//
|
|
628
|
-
//
|
|
625
|
+
// legacy credential file nor a grant token, so a grant-only device with
|
|
626
|
+
// an as-yet-unrefreshed token can still draw on a mandate it already
|
|
627
|
+
// holds.
|
|
629
628
|
const host = new URL(session.target.merchantUrl).hostname;
|
|
630
629
|
// Does an ACTIVE card mandate already cover this exact purchase? If so,
|
|
631
|
-
// draw against it TAP-FREE (no hosted passkey). Else
|
|
632
|
-
//
|
|
633
|
-
// it is consumed by intent creation + register and has no draw
|
|
630
|
+
// draw against it TAP-FREE (no hosted passkey). Else refuse with the
|
|
631
|
+
// mandate remedy (#7348). Mandates no longer persist the bootstrap
|
|
632
|
+
// token: it is consumed by intent creation + register and has no draw
|
|
633
|
+
// power.
|
|
634
634
|
const covering = await ledger.findCovering({
|
|
635
635
|
merchantHost: host,
|
|
636
636
|
currencyCode: session.currency,
|
|
@@ -640,9 +640,8 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
640
640
|
});
|
|
641
641
|
let source;
|
|
642
642
|
let confirmBase;
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
// the exact per-draw verdict used to mint the credential.
|
|
643
|
+
// Mandate draws set this only after the local reserve succeeds and auth
|
|
644
|
+
// returns the exact per-draw verdict used to mint the credential.
|
|
646
645
|
let confirmationAuthority = null;
|
|
647
646
|
let drawnRemaining = null;
|
|
648
647
|
let instrument;
|
|
@@ -667,7 +666,7 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
667
666
|
confirmationRef: null,
|
|
668
667
|
receiptPath: null,
|
|
669
668
|
detail: 'this mandate cannot draw because its delegated card authority is unavailable; ' +
|
|
670
|
-
'restore the
|
|
669
|
+
're-pair this runtime to restore the binding, or start a new mandate for this agent',
|
|
671
670
|
vicConfirmation: null,
|
|
672
671
|
source,
|
|
673
672
|
remainingMinor: null,
|
|
@@ -679,7 +678,7 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
679
678
|
confirmationRef: null,
|
|
680
679
|
receiptPath: null,
|
|
681
680
|
detail: 'this budget belongs to a different request key than the selected card capability; ' +
|
|
682
|
-
'restore that exact runtime key or
|
|
681
|
+
'restore that exact runtime key, or start a new mandate from the current runtime',
|
|
683
682
|
vicConfirmation: null,
|
|
684
683
|
source,
|
|
685
684
|
remainingMinor: null,
|
|
@@ -739,7 +738,8 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
739
738
|
// (MandateDrawDeclinedError). That case leaves the mandate
|
|
740
739
|
// active+covering, so a naive retry would re-select it and fail
|
|
741
740
|
// identically — trapping the caller; marking it unhonored makes the
|
|
742
|
-
// next pay_merchant skip it and
|
|
741
|
+
// next pay_merchant skip it and surface the no-covering-mandate refusal
|
|
742
|
+
// (#7348) instead of re-failing identically. We do
|
|
743
743
|
// NOT attempt an unsafe same-call browser fallback mid-submit.
|
|
744
744
|
//
|
|
745
745
|
// A PRE-network failure — reserve() failing closed on a concurrent
|
|
@@ -765,9 +765,7 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
765
765
|
}
|
|
766
766
|
throw new Error(verdictFailure.transient
|
|
767
767
|
? `the card-mandate draw could not be authorized right now (${verdictFailure.reasons.join(', ') || 'temporary error'}) — retry shortly`
|
|
768
|
-
: `this card mandate can no longer be drawn (${verdictFailure.reasons.join(', ') || 'refused'}); it has been disabled —
|
|
769
|
-
? 'create and claim a new mandate before retrying this v4 checkout'
|
|
770
|
-
: 'retry the checkout to use a legacy fresh per-purchase approval'}`, { cause: err });
|
|
768
|
+
: `this card mandate can no longer be drawn (${verdictFailure.reasons.join(', ') || 'refused'}); it has been disabled — create and claim a new mandate before retrying this checkout`, { cause: err });
|
|
771
769
|
}
|
|
772
770
|
if (!isTransientDrawFailure(err.cause ?? err)) {
|
|
773
771
|
await ledger.markUnhonored(mandateId, now());
|
|
@@ -785,8 +783,8 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
785
783
|
instrument = new VgsLiveInstrument(reference, session.contact.fullName ?? '', fetchCredential);
|
|
786
784
|
}
|
|
787
785
|
else {
|
|
788
|
-
// #7348
|
|
789
|
-
// runtime. The legacy 1:1
|
|
786
|
+
// #7348 single-purchase retirement: card spend is mandate-only for
|
|
787
|
+
// EVERY runtime. The legacy 1:1 single-purchase flow here minted a
|
|
790
788
|
// payable credential outside the grant/mandate ledger — invisible to
|
|
791
789
|
// spending controls. The server now refuses non-budget approvals at
|
|
792
790
|
// registration and non-budget mint tokens at verification; this
|
|
@@ -34,23 +34,24 @@ export type HostedApprovalOptions = {
|
|
|
34
34
|
/** Prefills the approval page's enrollment-email field (optional). */
|
|
35
35
|
consumerEmail?: string;
|
|
36
36
|
/**
|
|
37
|
-
* BUDGET/mandate approval
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* BUDGET/mandate approval — the only kind since #7348 retired the
|
|
38
|
+
* single-purchase plane: `target.transactionAmount` is the approved spend
|
|
39
|
+
* CEILING (not one charge), so the server mints a BUDGET mint token that
|
|
40
|
+
* later accepts many sub-ceiling draws tap-free.
|
|
40
41
|
*/
|
|
41
|
-
budget
|
|
42
|
+
budget: true;
|
|
42
43
|
/**
|
|
43
|
-
* Exact current request-key thumbprint receiving
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* Exact current request-key thumbprint receiving the budget; the
|
|
45
|
+
* authenticated page resolves it to the owner's stable agent before
|
|
46
|
+
* displaying or signing.
|
|
46
47
|
*/
|
|
47
|
-
agentJkt
|
|
48
|
-
/**
|
|
49
|
-
maxDraws
|
|
48
|
+
agentJkt: string;
|
|
49
|
+
/** Max draws the ceiling intent may fulfil — carried onto the token. */
|
|
50
|
+
maxDraws: number;
|
|
50
51
|
/**
|
|
51
|
-
* Per-purchase cap (decimal string, <= the ceiling) on
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Per-purchase cap (decimal string, <= the ceiling) — shown on the approval
|
|
53
|
+
* page as a worst-case term and carried onto the mint token so the displayed
|
|
54
|
+
* cap is the enforced cap.
|
|
54
55
|
*/
|
|
55
56
|
perTransaction?: string;
|
|
56
57
|
/**
|
|
@@ -128,7 +128,7 @@ function stripTrailingSlashes(value) {
|
|
|
128
128
|
* rather than falling back to a client-held secret.
|
|
129
129
|
*/
|
|
130
130
|
export async function runHostedApproval(opts) {
|
|
131
|
-
const { baseUrl, tokenId, target, consumerEmail,
|
|
131
|
+
const { baseUrl, tokenId, target, consumerEmail, agentJkt, maxDraws, perTransaction, intent,
|
|
132
132
|
// Default to stderr, NOT a no-op: the approval URL is the one thing a remote /
|
|
133
133
|
// SSH / headless-terminal human needs to proceed, and swallowing it (the old
|
|
134
134
|
// `() => {}` default) left it emitted nowhere readable. Tests inject their own.
|
|
@@ -138,12 +138,9 @@ export async function runHostedApproval(opts) {
|
|
|
138
138
|
// override without threading a flag through every caller.
|
|
139
139
|
timeoutMs = resolveApprovalTimeoutMs(), } = opts;
|
|
140
140
|
const canonicalJkt = typeof agentJkt === 'string' && /^[A-Za-z0-9_-]{43}$/.test(agentJkt);
|
|
141
|
-
if (
|
|
141
|
+
if (!canonicalJkt) {
|
|
142
142
|
throw new Error('a budget approval requires the exact current agent request-key JKT');
|
|
143
143
|
}
|
|
144
|
-
if (!budget && agentJkt !== undefined) {
|
|
145
|
-
throw new Error('agentJkt is accepted only for a budget approval');
|
|
146
|
-
}
|
|
147
144
|
const base = stripTrailingSlashes(assertApprovalBaseUrl(baseUrl));
|
|
148
145
|
// The server's currency map is uppercase ISO 4217; the loader normalizes the
|
|
149
146
|
// target once, but normalize here too so a direct caller with a lowercase
|
|
@@ -194,10 +191,10 @@ export async function runHostedApproval(opts) {
|
|
|
194
191
|
amount: target.transactionAmount,
|
|
195
192
|
currency,
|
|
196
193
|
...(consumerEmail ? { consumerEmail } : {}),
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
...(
|
|
194
|
+
budget: true,
|
|
195
|
+
agentJkt,
|
|
196
|
+
maxDraws,
|
|
197
|
+
...(perTransaction !== undefined ? { perTransaction } : {}),
|
|
201
198
|
...(intentSanitized !== undefined ? { intent: intentSanitized } : {}),
|
|
202
199
|
},
|
|
203
200
|
}),
|
|
@@ -214,9 +211,7 @@ export async function runHostedApproval(opts) {
|
|
|
214
211
|
// effort browser open for the interactive case.
|
|
215
212
|
onApprovalUrl?.(approveUrl);
|
|
216
213
|
log(`approve the purchase in your browser: ${approveUrl}`);
|
|
217
|
-
|
|
218
|
-
log(`budget recipient request key: ${agentJkt.slice(0, 8)}…${agentJkt.slice(-6)}`);
|
|
219
|
-
}
|
|
214
|
+
log(`budget recipient request key: ${agentJkt.slice(0, 8)}…${agentJkt.slice(-6)}`);
|
|
220
215
|
openUrl(approveUrl);
|
|
221
216
|
let lastHeartbeat = now();
|
|
222
217
|
// Keep the event loop alive for the whole poll wait. `defaultSleep` unref()'s
|
|
@@ -270,33 +265,29 @@ export async function runHostedApproval(opts) {
|
|
|
270
265
|
}
|
|
271
266
|
}
|
|
272
267
|
// A budget approval must complete AS a budget approval — a relay that
|
|
273
|
-
// dropped the flag
|
|
274
|
-
//
|
|
275
|
-
// string
|
|
276
|
-
if (
|
|
268
|
+
// dropped the flag did not complete OUR registration. Compare the
|
|
269
|
+
// boolean explicitly (it is not a string, so it lives outside the
|
|
270
|
+
// string-map loop above).
|
|
271
|
+
if (doc.context?.budget !== true) {
|
|
277
272
|
throw new Error('hosted approval context mismatch on budget — the approval was not for this ' +
|
|
278
273
|
'exact purchase; run the checkout again for a fresh link');
|
|
279
274
|
}
|
|
280
|
-
if (
|
|
275
|
+
if (doc.context?.agentJkt !== agentJkt) {
|
|
281
276
|
throw new Error('hosted approval context mismatch on agentJkt — the approval was not for this ' +
|
|
282
277
|
'exact request key; run the checkout again for a fresh link');
|
|
283
278
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// The advisory draw ceiling must survive the relay intact too — a relay
|
|
288
|
-
// that dropped or altered maxDraws would mint a budget token whose draw
|
|
289
|
-
// count no longer matches what the operator approved. Only the budget
|
|
290
|
-
// path registers maxDraws, so only enforce it there; compare exactly,
|
|
279
|
+
// The draw ceiling must survive the relay intact too — a relay that
|
|
280
|
+
// dropped or altered maxDraws would mint a budget token whose draw
|
|
281
|
+
// count no longer matches what the operator approved. Compare exactly,
|
|
291
282
|
// the omitted/undefined case included, so a silently dropped value is
|
|
292
283
|
// caught the same as a mutated one.
|
|
293
|
-
if (
|
|
284
|
+
if (doc.context?.maxDraws !== maxDraws) {
|
|
294
285
|
throw new Error('hosted approval context mismatch on maxDraws — the approval was not for this ' +
|
|
295
286
|
'exact purchase; run the checkout again for a fresh link');
|
|
296
287
|
}
|
|
297
288
|
// Same drop-or-mutate rule for the per-purchase cap: the term the
|
|
298
289
|
// operator read must be the term the token enforces.
|
|
299
|
-
if (
|
|
290
|
+
if (doc.context?.perTransaction !== perTransaction) {
|
|
300
291
|
throw new Error('hosted approval context mismatch on perTransaction — the approval was not for ' +
|
|
301
292
|
'this exact purchase; run the checkout again for a fresh link');
|
|
302
293
|
}
|
|
@@ -306,14 +297,9 @@ export async function runHostedApproval(opts) {
|
|
|
306
297
|
throw new Error('hosted approval context mismatch on intent — the approval was not for this ' +
|
|
307
298
|
'exact purchase; run the checkout again for a fresh link');
|
|
308
299
|
}
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
throw new Error('hosted budget approval completed without a valid future expiry');
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
else if (doc.validUntil !== undefined) {
|
|
316
|
-
throw new Error('hosted one-purchase approval unexpectedly carried a budget expiry');
|
|
300
|
+
if (!Number.isSafeInteger(doc.validUntil) ||
|
|
301
|
+
doc.validUntil <= Math.floor(now() / 1000)) {
|
|
302
|
+
throw new Error('hosted budget approval completed without a valid future expiry');
|
|
317
303
|
}
|
|
318
304
|
log(assuranceExempt
|
|
319
305
|
? 'approval received from the hosted page (code-verified card, no passkey).'
|
|
@@ -324,7 +310,7 @@ export async function runHostedApproval(opts) {
|
|
|
324
310
|
...(typeof doc.mintToken === 'string' && doc.mintToken
|
|
325
311
|
? { mintToken: doc.mintToken }
|
|
326
312
|
: {}),
|
|
327
|
-
|
|
313
|
+
validUntil: doc.validUntil,
|
|
328
314
|
};
|
|
329
315
|
}
|
|
330
316
|
// status 'pending' — the operator is still signing in / tapping.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// stays FAIL-CLEAN as defense-in-depth: the budget is decremented only after a
|
|
19
19
|
// payable cryptogram returns; a network rejection leaves the budget untouched.
|
|
20
20
|
// The caller (cli-engine) then marks the mandate unhonored so the NEXT
|
|
21
|
-
// pay_merchant skips it and
|
|
21
|
+
// pay_merchant skips it and surfaces the no-covering refusal (#7348) — the same call is
|
|
22
22
|
// not retried in-flight. Never fail-open.
|
|
23
23
|
import { decimalToMinor, minorToDecimal, validateCredential, } from '../vgs-live-instrument.js';
|
|
24
24
|
import { CARD_MANDATE_LEDGER_VERSION, remainingMinor, } from './mandate-ledger.js';
|
|
@@ -47,7 +47,7 @@ export type CardMandateRecord = {
|
|
|
47
47
|
* ceiling-scoped assurance the acquirer would not honor for a sub-amount
|
|
48
48
|
* draw). Distinct from a reservation `status`: it disables the whole mandate.
|
|
49
49
|
* Once set, findCovering() SKIPS this mandate so the caller's next
|
|
50
|
-
* pay_merchant
|
|
50
|
+
* pay_merchant surfaces the no-covering-mandate refusal (#7348) instead of
|
|
51
51
|
* re-selecting the same failing mandate forever. ISO 8601.
|
|
52
52
|
*/
|
|
53
53
|
unhonoredAt?: string;
|
|
@@ -55,8 +55,8 @@ export type CardMandateRecord = {
|
|
|
55
55
|
* Set when the delegated-draw register handshake FAILED at mandate-start (the
|
|
56
56
|
* server-side `card_mandate_spend` row was never created), so a later
|
|
57
57
|
* delegated (verdict-signed) draw would 404 `no_mandate`. Like `unhonoredAt`,
|
|
58
|
-
* findCovering() SKIPS a register-failed mandate so the next checkout
|
|
59
|
-
*
|
|
58
|
+
* findCovering() SKIPS a register-failed mandate so the next checkout
|
|
59
|
+
* surfaces the no-covering-mandate refusal (#7348) instead of a confusing
|
|
60
60
|
* `no_mandate`. Mandate-start requires delegated card authority, so every
|
|
61
61
|
* created record attempted registration. ISO 8601.
|
|
62
62
|
*/
|
|
@@ -109,7 +109,7 @@ export declare class MandateLedger {
|
|
|
109
109
|
/**
|
|
110
110
|
* First ACTIVE mandate (not expired) whose merchant + currency match and whose
|
|
111
111
|
* remaining headroom covers amountMinor. Used by pay_merchant to decide the
|
|
112
|
-
* tap-free draw path vs
|
|
112
|
+
* tap-free draw path vs the no-covering-mandate refusal (#7348).
|
|
113
113
|
*/
|
|
114
114
|
findCovering(query: CoverQuery): Promise<CardMandateRecord | null>;
|
|
115
115
|
/**
|
|
@@ -174,7 +174,7 @@ export class MandateLedger {
|
|
|
174
174
|
/**
|
|
175
175
|
* First ACTIVE mandate (not expired) whose merchant + currency match and whose
|
|
176
176
|
* remaining headroom covers amountMinor. Used by pay_merchant to decide the
|
|
177
|
-
* tap-free draw path vs
|
|
177
|
+
* tap-free draw path vs the no-covering-mandate refusal (#7348).
|
|
178
178
|
*/
|
|
179
179
|
async findCovering(query) {
|
|
180
180
|
const now = query.now ?? new Date();
|
|
@@ -184,7 +184,7 @@ export class MandateLedger {
|
|
|
184
184
|
// mandate the network refused is skipped via `!m.unhonoredAt` so it can never
|
|
185
185
|
// be re-selected. A register-failed mandate is skipped the same way
|
|
186
186
|
// (`!m.registerFailedAt`): it has no server row, so a delegated draw would 404
|
|
187
|
-
// `no_mandate` — better to
|
|
187
|
+
// `no_mandate` — better to surface the no-covering refusal (#7348).
|
|
188
188
|
// Selection order among covering candidates:
|
|
189
189
|
// 1. Prefer a merchant-SCOPED mandate over a crossMerchant budget one —
|
|
190
190
|
// spend the dedicated grant for this merchant first and keep the broader
|
|
@@ -37,13 +37,13 @@ export declare class ServerCryptogramRefusedError extends Error {
|
|
|
37
37
|
constructor(status: number, detail: string);
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
* Optional mandate override for serverCreateIntent. Absent → the
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
40
|
+
* Optional mandate override for serverCreateIntent. Absent → the default
|
|
41
|
+
* intent shape (cap = ceil(amount)+10 min 25, quantity 1, 30-day window).
|
|
42
|
+
* Present → the card-mandate (budget) layer sets a CEILING decline threshold
|
|
43
|
+
* and a multi-draw quantity so one passkey approves a spend ceiling and later
|
|
44
|
+
* draws pull cryptograms under it without a fresh tap. Whether the network
|
|
45
|
+
* honors a ceiling-scoped assurance across multiple sub-amount draws is
|
|
46
|
+
* UNPROVEN — see packages/checkout-engine/src/mandate/.
|
|
47
47
|
*/
|
|
48
48
|
export type ServerIntentMandateOverride = {
|
|
49
49
|
/** Wire decline-threshold amount (major-unit decimal string), e.g. "500.00". */
|
|
@@ -80,14 +80,6 @@ export type FetchVgsPaymentCredential = (input: {
|
|
|
80
80
|
transactionCurrencyCode: string;
|
|
81
81
|
};
|
|
82
82
|
}) => Promise<VgsPaymentCredential>;
|
|
83
|
-
export type MintFreshVgsPaymentCredential = (input: {
|
|
84
|
-
tokenId: string;
|
|
85
|
-
assuranceData: unknown;
|
|
86
|
-
transaction: VgsCheckoutTarget;
|
|
87
|
-
}) => Promise<{
|
|
88
|
-
payment: VgsPaymentCredential;
|
|
89
|
-
intentId: string;
|
|
90
|
-
}>;
|
|
91
83
|
/** The (token, intent) pair a VIC confirmation is posted against. */
|
|
92
84
|
export type VicConfirmationTarget = {
|
|
93
85
|
tokenId: string;
|
|
@@ -150,30 +142,3 @@ export declare class VgsLiveInstrument implements Instrument {
|
|
|
150
142
|
confirmationTarget(): VicConfirmationTarget | null;
|
|
151
143
|
getCredential(ctx: InstrumentContext): Promise<CardCredential>;
|
|
152
144
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Consumes #5614's claimed CLI enrollment artifact (for the tokenId) plus a
|
|
155
|
-
* FRESH purchase-scoped assurance, and creates a fresh, transaction-scoped VIC
|
|
156
|
-
* intent + credential after checkout approval. This is the production-shaped
|
|
157
|
-
* bridge; unlike VgsLiveInstrument it never needs a pre-created intent ID.
|
|
158
|
-
*
|
|
159
|
-
* The enrollment artifact's stored assuranceData is deliberately never sent:
|
|
160
|
-
* replaying it makes intent creation succeed (HTTP 201) while the cryptogram
|
|
161
|
-
* deterministically never completes — the #5709 dead end. Purchase
|
|
162
|
-
* authorization is the fresh, merchant+amount+currency-scoped assurance,
|
|
163
|
-
* validated against the checkout target BEFORE any intent is minted so a
|
|
164
|
-
* doomed intent is never created.
|
|
165
|
-
*/
|
|
166
|
-
export declare class VgsAssuranceInstrument implements Instrument {
|
|
167
|
-
private readonly enrollment;
|
|
168
|
-
private readonly purchase;
|
|
169
|
-
private readonly target;
|
|
170
|
-
private readonly cardholderName;
|
|
171
|
-
private readonly mintCredential;
|
|
172
|
-
readonly kind: "agentic-token";
|
|
173
|
-
private used;
|
|
174
|
-
private minted;
|
|
175
|
-
constructor(enrollment: CliAgentCredential, purchase: PurchaseAssurance, target: VgsCheckoutTarget, cardholderName: string, mintCredential: MintFreshVgsPaymentCredential);
|
|
176
|
-
/** See VgsLiveInstrument.confirmationTarget — same contract. */
|
|
177
|
-
confirmationTarget(): VicConfirmationTarget | null;
|
|
178
|
-
getCredential(ctx: InstrumentContext): Promise<CardCredential>;
|
|
179
|
-
}
|
|
@@ -87,13 +87,6 @@ function validateTarget(reference, ctx) {
|
|
|
87
87
|
throw new Error(`VGS credential currency mismatch: ${currency} vs ${ctx.currency}`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
function validateCliCredential(value) {
|
|
91
|
-
if (typeof value.tokenId !== 'string' || !value.tokenId.trim()) {
|
|
92
|
-
throw new Error('CLI agent credential requires tokenId');
|
|
93
|
-
}
|
|
94
|
-
// Deliberately no assuranceData requirement: the artifact's enrollment-time
|
|
95
|
-
// assurance is identity/enrollment material and is never read here (#5709).
|
|
96
|
-
}
|
|
97
90
|
/**
|
|
98
91
|
* Refuse to mint an intent unless the assurance is fresh and its declared
|
|
99
92
|
* scope matches the checkout target exactly. `now` is injectable so the
|
|
@@ -234,63 +227,3 @@ export class VgsLiveInstrument {
|
|
|
234
227
|
};
|
|
235
228
|
}
|
|
236
229
|
}
|
|
237
|
-
/**
|
|
238
|
-
* Consumes #5614's claimed CLI enrollment artifact (for the tokenId) plus a
|
|
239
|
-
* FRESH purchase-scoped assurance, and creates a fresh, transaction-scoped VIC
|
|
240
|
-
* intent + credential after checkout approval. This is the production-shaped
|
|
241
|
-
* bridge; unlike VgsLiveInstrument it never needs a pre-created intent ID.
|
|
242
|
-
*
|
|
243
|
-
* The enrollment artifact's stored assuranceData is deliberately never sent:
|
|
244
|
-
* replaying it makes intent creation succeed (HTTP 201) while the cryptogram
|
|
245
|
-
* deterministically never completes — the #5709 dead end. Purchase
|
|
246
|
-
* authorization is the fresh, merchant+amount+currency-scoped assurance,
|
|
247
|
-
* validated against the checkout target BEFORE any intent is minted so a
|
|
248
|
-
* doomed intent is never created.
|
|
249
|
-
*/
|
|
250
|
-
export class VgsAssuranceInstrument {
|
|
251
|
-
enrollment;
|
|
252
|
-
purchase;
|
|
253
|
-
target;
|
|
254
|
-
cardholderName;
|
|
255
|
-
mintCredential;
|
|
256
|
-
kind = 'agentic-token';
|
|
257
|
-
used = false;
|
|
258
|
-
minted = null;
|
|
259
|
-
constructor(enrollment, purchase, target, cardholderName, mintCredential) {
|
|
260
|
-
this.enrollment = enrollment;
|
|
261
|
-
this.purchase = purchase;
|
|
262
|
-
this.target = target;
|
|
263
|
-
this.cardholderName = cardholderName;
|
|
264
|
-
this.mintCredential = mintCredential;
|
|
265
|
-
}
|
|
266
|
-
/** See VgsLiveInstrument.confirmationTarget — same contract. */
|
|
267
|
-
confirmationTarget() {
|
|
268
|
-
return this.minted;
|
|
269
|
-
}
|
|
270
|
-
async getCredential(ctx) {
|
|
271
|
-
if (this.used)
|
|
272
|
-
throw new Error('VGS assurance instrument is single-use');
|
|
273
|
-
this.used = true;
|
|
274
|
-
validateCliCredential(this.enrollment);
|
|
275
|
-
validateTarget(this.target, ctx);
|
|
276
|
-
validatePurchaseAssurance(this.purchase, this.target);
|
|
277
|
-
if (!this.cardholderName.trim())
|
|
278
|
-
throw new Error('cardholder name is required');
|
|
279
|
-
const { payment: value, intentId } = await this.mintCredential({
|
|
280
|
-
tokenId: this.enrollment.tokenId,
|
|
281
|
-
assuranceData: this.purchase.assuranceData,
|
|
282
|
-
transaction: this.target,
|
|
283
|
-
});
|
|
284
|
-
this.minted = { tokenId: this.enrollment.tokenId, intentId };
|
|
285
|
-
validateCredential(value);
|
|
286
|
-
return {
|
|
287
|
-
pan: value.networkToken,
|
|
288
|
-
expMonth: value.expMonth,
|
|
289
|
-
expYear: value.expYear,
|
|
290
|
-
cvc: value.cryptogramValue,
|
|
291
|
-
cardholderName: this.cardholderName.trim(),
|
|
292
|
-
...(value.cryptogramExpiresAt ? { credentialExpiresAt: value.cryptogramExpiresAt } : {}),
|
|
293
|
-
...traceHandleFields(value),
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
}
|