@visa/cli 4.1.0-rc.25 → 4.1.0-rc.4

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 (63) hide show
  1. package/dist/cli.js +329 -364
  2. package/dist/mcp-server/index.js +148 -148
  3. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  4. package/package.json +3 -5
  5. package/server.json +2 -2
  6. package/dist/checkout-engine/adapters/generic.d.ts +0 -19
  7. package/dist/checkout-engine/adapters/generic.js +0 -201
  8. package/dist/checkout-engine/adapters/index.d.ts +0 -7
  9. package/dist/checkout-engine/adapters/index.js +0 -17
  10. package/dist/checkout-engine/adapters/stripe-like.d.ts +0 -10
  11. package/dist/checkout-engine/adapters/stripe-like.js +0 -21
  12. package/dist/checkout-engine/browser-launch.d.ts +0 -46
  13. package/dist/checkout-engine/browser-launch.js +0 -81
  14. package/dist/checkout-engine/ceremony.d.ts +0 -64
  15. package/dist/checkout-engine/ceremony.js +0 -261
  16. package/dist/checkout-engine/cli-engine.d.ts +0 -60
  17. package/dist/checkout-engine/cli-engine.js +0 -242
  18. package/dist/checkout-engine/detect.d.ts +0 -61
  19. package/dist/checkout-engine/detect.js +0 -372
  20. package/dist/checkout-engine/evidence.d.ts +0 -22
  21. package/dist/checkout-engine/evidence.js +0 -59
  22. package/dist/checkout-engine/executor.d.ts +0 -172
  23. package/dist/checkout-engine/executor.js +0 -1233
  24. package/dist/checkout-engine/hosted-approval.d.ts +0 -78
  25. package/dist/checkout-engine/hosted-approval.js +0 -171
  26. package/dist/checkout-engine/index.d.ts +0 -3
  27. package/dist/checkout-engine/index.js +0 -5
  28. package/dist/checkout-engine/inline-target.d.ts +0 -13
  29. package/dist/checkout-engine/inline-target.js +0 -37
  30. package/dist/checkout-engine/instrument.d.ts +0 -54
  31. package/dist/checkout-engine/instrument.js +0 -83
  32. package/dist/checkout-engine/live-fill-approval.d.ts +0 -52
  33. package/dist/checkout-engine/live-fill-approval.js +0 -107
  34. package/dist/checkout-engine/mandate.d.ts +0 -25
  35. package/dist/checkout-engine/mandate.js +0 -100
  36. package/dist/checkout-engine/outcome.d.ts +0 -30
  37. package/dist/checkout-engine/outcome.js +0 -190
  38. package/dist/checkout-engine/owner-only-file.d.ts +0 -10
  39. package/dist/checkout-engine/owner-only-file.js +0 -22
  40. package/dist/checkout-engine/package.json +0 -3
  41. package/dist/checkout-engine/pay-args.d.ts +0 -14
  42. package/dist/checkout-engine/pay-args.js +0 -44
  43. package/dist/checkout-engine/pay.d.ts +0 -1
  44. package/dist/checkout-engine/pay.js +0 -13
  45. package/dist/checkout-engine/receipt.d.ts +0 -81
  46. package/dist/checkout-engine/receipt.js +0 -109
  47. package/dist/checkout-engine/repo-env.d.ts +0 -11
  48. package/dist/checkout-engine/repo-env.js +0 -23
  49. package/dist/checkout-engine/run-live-fill.d.ts +0 -1
  50. package/dist/checkout-engine/run-live-fill.js +0 -443
  51. package/dist/checkout-engine/types.d.ts +0 -26
  52. package/dist/checkout-engine/types.js +0 -2
  53. package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
  54. package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
  55. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +0 -49
  56. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +0 -150
  57. package/dist/checkout-engine/vgs-live-instrument.d.ts +0 -141
  58. package/dist/checkout-engine/vgs-live-instrument.js +0 -252
  59. package/dist/checkout-engine/vic-confirmation.d.ts +0 -34
  60. package/dist/checkout-engine/vic-confirmation.js +0 -39
  61. package/dist/skills/pair-visa-agent/RUNTIMES.md +0 -79
  62. package/dist/skills/pair-visa-agent/SKILL.md +0 -360
  63. package/dist/skills/pair-visa-agent/scripts/setup.mjs +0 -48
@@ -1 +0,0 @@
1
- export {};
@@ -1,443 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { createInterface } from 'node:readline/promises';
5
- import { fileURLToPath } from 'node:url';
6
- import { launchCheckoutBrowser } from './browser-launch.js';
7
- import { CURRENCY_NUMERIC, currencyNumeric, runInteractiveCeremony } from './ceremony.js';
8
- import { assertApprovalBaseUrl, resolveApprovalBaseUrl, runHostedApproval, } from './hosted-approval.js';
9
- import { inlineTargetFromFlags } from './inline-target.js';
10
- import { createIntent, fetchCryptogram, postConfirmation } from './vgs-gateway/fetch-credential.mjs';
11
- import { cancelPreparedCheckout, prepareCheckout, submitApprovedCheckout, } from './executor.js';
12
- import { approvalPhrase, approvalQuestion, assertLegacyReferenceAllowed, assertSubmitAllowed, isRunSuccess, parseCheckoutMode, submitClickedWithoutConfirmation, } from './live-fill-approval.js';
13
- import { assertOwnerOnlyFile, readOwnerOnlyJson } from './owner-only-file.js';
14
- import { buildReceipt, writeReceipt } from './receipt.js';
15
- import { loadRepoEnvDefaults } from './repo-env.js';
16
- import { reportVicOutcome } from './vic-confirmation.js';
17
- import { decimalToMinor, VgsAssuranceInstrument, VgsLiveInstrument, } from './vgs-live-instrument.js';
18
- function usage() {
19
- throw new Error('usage: pnpm fill:live --checkout-file <path> --contact-file <path> ' +
20
- '[--purchase-assurance-file <path>] [--approval-base-url <url>] ' +
21
- '[--agent-credential-file <path>] [--mode dry-run|submit] [--receipt-dir <path>]\n' +
22
- 'inline (any merchant, no checkout file): pnpm fill:live --merchant-url <url> ' +
23
- '--amount <decimal> [--merchant-name <name>] [--country <CC>] [--currency <ISO>] — ' +
24
- 'merchant name defaults to the URL hostname, country to US, currency to USD; ' +
25
- 'positional sugar: pnpm pay -- <url> <amount> [flags]\n' +
26
- '--contact-file defaults to ~/.visa-mcp/contact.json\n' +
27
- 'the repo-root .env is loaded as env DEFAULTS (already-set variables win), e.g. for ' +
28
- 'CHECKOUT_AGENT_MODE (--mode fallback); hosted approval on the deployed verify site ' +
29
- "is the built-in default (CHECKOUT_APPROVAL_BASE_URL='' forces the loopback ceremony)\n" +
30
- 'legacy: pnpm fill:live --reference-file <path> --contact-file <path> ' +
31
- '(requires CHECKOUT_AGENT_ALLOW_LEGACY_REFERENCE=1 — bypasses purchase-assurance ' +
32
- 'validation, #5709)\n' +
33
- 'Without --purchase-assurance-file the runner performs the device-binding ' +
34
- 'ceremony itself after the typed approval. With --approval-base-url (or ' +
35
- 'CHECKOUT_APPROVAL_BASE_URL) the passkey tap happens on the deployed verify ' +
36
- "site's /approve page (no localhost anywhere); otherwise it opens a loopback " +
37
- 'Visa approval page. Either way the assurance arrives in-process (seconds old ' +
38
- 'at mint, never written to disk).\n' +
39
- '--purchase-assurance-file holds the output of a fresh device-binding ceremony ' +
40
- 'scoped to this exact merchant, amount, and currency ({assuranceData, mintedAt, ' +
41
- 'merchantHost, transactionAmount, transactionCurrencyCode}); the enrollment ' +
42
- 'artifact is never purchase authorization (#5709)\n' +
43
- '--mode submit clicks the pay control after a typed PAY approval; non-interactive ' +
44
- '(scripted) submits additionally require CHECKOUT_AGENT_ALLOW_SUBMIT=1 ' +
45
- '(default: dry-run, never submits)');
46
- }
47
- function args() {
48
- const parsed = new Map();
49
- for (let i = 2; i < process.argv.length; i++) {
50
- const key = process.argv[i];
51
- // pnpm forwards the literal `--` separator from `pnpm fill:live -- …`;
52
- // every documented invocation uses it, so skip the bare token.
53
- if (key === '--')
54
- continue;
55
- if (!key.startsWith('--'))
56
- usage();
57
- const value = process.argv[++i];
58
- if (!value || value.startsWith('--'))
59
- usage();
60
- parsed.set(key, value);
61
- }
62
- return parsed;
63
- }
64
- // Repo-root .env as DEFAULTS, before any env read below: stable per-machine
65
- // config (VGS creds, CHECKOUT_AGENT_MODE, the scripted-run submit arm) lives
66
- // there once; explicitly-set environment always wins, and a missing file
67
- // changes nothing.
68
- loadRepoEnvDefaults();
69
- const input = args();
70
- const referenceFile = input.get('--reference-file');
71
- const checkoutFile = input.get('--checkout-file');
72
- // Inline any-merchant mode: --merchant-url + --amount build the target
73
- // directly, so no checkout JSON has to exist. Throws its own scoped errors;
74
- // the shared validation block below still governs the resulting shape.
75
- const inlineTarget = inlineTargetFromFlags(input);
76
- const contactFile = input.get('--contact-file') ?? join(homedir(), '.visa-mcp', 'contact.json');
77
- const agentCredentialFile = input.get('--agent-credential-file') ?? join(homedir(), '.visa-mcp', 'agent-credential.json');
78
- // Optional in assurance (checkout-file) mode: purchase authorization is a
79
- // FRESH ceremony scoped to this purchase, never the enrollment artifact
80
- // (#5709). Without the file, the runner performs the ceremony itself after
81
- // the typed approval (interactive ceremony mode).
82
- const purchaseAssuranceFile = input.get('--purchase-assurance-file');
83
- // Exactly ONE target source: legacy reference file, checkout file, or the
84
- // inline flags.
85
- const targetSources = [
86
- typeof referenceFile === 'string',
87
- typeof checkoutFile === 'string',
88
- inlineTarget !== null,
89
- ].filter(Boolean).length;
90
- if (targetSources !== 1 || typeof agentCredentialFile !== 'string') {
91
- usage();
92
- }
93
- const receiptDir = input.get('--receipt-dir') ?? join(homedir(), '.visa-mcp', 'checkout-receipts');
94
- const mode = parseCheckoutMode(input.get('--mode'));
95
- // Opt-in human-in-the-loop for issuer challenges: CHECKOUT_AGENT_CHALLENGE_HOLD=<minutes>
96
- // keeps a detected 3DS challenge on screen so the operator can complete it
97
- // (e.g. type an SMS one-time code) instead of ending the run.
98
- const challengeHoldMinutes = Number(process.env.CHECKOUT_AGENT_CHALLENGE_HOLD ?? '');
99
- const challengeHoldMs = Number.isFinite(challengeHoldMinutes) && challengeHoldMinutes > 0
100
- ? challengeHoldMinutes * 60_000
101
- : undefined;
102
- // Refuse an unarmed submit before anything else runs — no file is read, no
103
- // browser launches, no credential can possibly be minted.
104
- assertSubmitAllowed(mode);
105
- // The legacy path bypasses purchase-assurance validation (#5709) — default-off,
106
- // same fail-fast posture as the submit gate.
107
- assertLegacyReferenceAllowed(typeof referenceFile === 'string');
108
- const reference = typeof referenceFile === 'string'
109
- ? await readOwnerOnlyJson(referenceFile, 'reference file')
110
- : null;
111
- const target = reference
112
- ? reference
113
- : (inlineTarget ??
114
- (await readOwnerOnlyJson(checkoutFile, 'checkout file')));
115
- const contact = await readOwnerOnlyJson(contactFile, 'contact file');
116
- if (reference && (!reference.tokenId || !reference.intentId)) {
117
- throw new Error('legacy reference requires tokenId and intentId');
118
- }
119
- if (typeof target.merchantName !== 'string' || !target.merchantName.trim()) {
120
- throw new Error('checkout target requires merchantName');
121
- }
122
- if (typeof target.merchantCountryCode !== 'string' ||
123
- !/^[A-Z]{2}$/.test(target.merchantCountryCode.toUpperCase())) {
124
- throw new Error('checkout target merchantCountryCode must be a 2-letter ISO code');
125
- }
126
- // Normalize once so the instrument's strict uppercase re-check (which runs
127
- // after browser launch + operator confirmation) can't fail on a lowercase code
128
- // this loader already accepted. `target` aliases `reference` on the legacy
129
- // path, so both instruments see the normalized code.
130
- target.merchantCountryCode = target.merchantCountryCode.toUpperCase();
131
- if (!contact.fullName && !(contact.firstName && contact.lastName)) {
132
- throw new Error('contact file needs fullName or firstName + lastName');
133
- }
134
- // || not ??: an empty-string fullName must fall through to first+last.
135
- const cardholderName = contact.fullName || `${contact.firstName} ${contact.lastName}`;
136
- if (typeof target.merchantUrl !== 'string') {
137
- throw new Error('checkout target requires merchantUrl');
138
- }
139
- const url = new URL(target.merchantUrl);
140
- if (url.protocol !== 'https:' && url.hostname !== '127.0.0.1' && url.hostname !== 'localhost') {
141
- throw new Error('checkout URL must use HTTPS (localhost is allowed for fixtures)');
142
- }
143
- if (typeof target.transactionAmount !== 'string') {
144
- throw new Error('checkout target requires transactionAmount');
145
- }
146
- const parsedAmountMinor = decimalToMinor(target.transactionAmount);
147
- if (parsedAmountMinor === null || parsedAmountMinor <= 0) {
148
- throw new Error('checkout target transactionAmount must be a positive decimal');
149
- }
150
- // Re-bound after the guard so the narrowed `number` type reaches the hoisted
151
- // persistReceipt declaration below.
152
- const amountMinor = parsedAmountMinor;
153
- if (typeof target.transactionCurrencyCode !== 'string') {
154
- throw new Error('checkout target transactionCurrencyCode must be a 3-letter ISO code');
155
- }
156
- const currency = target.transactionCurrencyCode.toUpperCase();
157
- if (!/^[A-Z]{3}$/.test(currency)) {
158
- throw new Error('checkout target transactionCurrencyCode must be a 3-letter ISO code');
159
- }
160
- // Normalize once on the target (same pattern as merchantCountryCode above) so
161
- // every downstream consumer — the hosted-approval registration, the assurance
162
- // scope, the instrument's re-check — sees the exact value this loader
163
- // validated; a lowercase code in the file must not 400 at the hosted relay.
164
- target.transactionCurrencyCode = currency;
165
- // Preflight the agent-credential + purchase-assurance artifacts (assurance
166
- // mode) BEFORE the browser launches: lstat-only — existence, regular file,
167
- // 0600, size cap. A missing or mis-permissioned artifact fails fast here
168
- // instead of after the operator has completed the manual checkout review and
169
- // typed the FILL phrase (review nit). Contents stay unread until after
170
- // approval — the guard reads nothing.
171
- if (!reference) {
172
- await assertOwnerOnlyFile(agentCredentialFile, 'CLI agent credential', 40 * 1024);
173
- if (typeof purchaseAssuranceFile === 'string') {
174
- await assertOwnerOnlyFile(purchaseAssuranceFile, 'purchase assurance', 40 * 1024);
175
- }
176
- }
177
- // Interactive ceremony mode (no --purchase-assurance-file): the runner performs
178
- // the device-binding ceremony itself after the typed approval. With an
179
- // approval base URL the ceremony is HOSTED on the deployed verify site (no
180
- // loopback page, no local TLS/SDK/VGS-credential prerequisites); otherwise it
181
- // runs on the loopback page. Fail fast on the active mode's prerequisites
182
- // here, before any browser launches.
183
- const ceremonyMode = !reference && typeof purchaseAssuranceFile !== 'string';
184
- // Hosted approval on the deployed verify site is the DEFAULT; set
185
- // CHECKOUT_APPROVAL_BASE_URL='' (empty) to force the loopback ceremony.
186
- const approvalBaseUrl = resolveApprovalBaseUrl(input.get('--approval-base-url'));
187
- const hostedApproval = ceremonyMode && approvalBaseUrl !== '';
188
- // HTTPS-except-loopback — the same policy the checkout URL gets above; the
189
- // approval claim carries the verifier credential and never travels cleartext.
190
- if (hostedApproval)
191
- assertApprovalBaseUrl(approvalBaseUrl);
192
- let ceremonyTls = null;
193
- let ceremonyVendorSdkJs = '';
194
- let ceremonyConsumerEmail = '';
195
- if (ceremonyMode) {
196
- // The hosted page collects the enrollment email itself when the contact file
197
- // carries none; the loopback page cannot, so local mode still requires it.
198
- if (!hostedApproval && (typeof contact.email !== 'string' || !contact.email.trim())) {
199
- throw new Error('interactive ceremony needs an email in the contact file');
200
- }
201
- ceremonyConsumerEmail = typeof contact.email === 'string' ? contact.email : '';
202
- if (!currencyNumeric(currency)) {
203
- throw new Error(`interactive ceremony supports ${Object.keys(CURRENCY_NUMERIC).join('/')} — ` +
204
- `pass --purchase-assurance-file for ${currency}`);
205
- }
206
- // Loopback mode only: the Visa device-binding iframe requires an HTTPS
207
- // parent page (dev-harness certs) and the vendored SDK. The hosted page
208
- // brings its own origin and SDK — no local prerequisites.
209
- if (!hostedApproval) {
210
- const harnessDir = fileURLToPath(new URL('../../../apps/v4-verify-web/dev-harness/', import.meta.url));
211
- try {
212
- ceremonyTls = {
213
- key: readFileSync(join(harnessDir, 'certs', 'key.pem')),
214
- cert: readFileSync(join(harnessDir, 'certs', 'cert.pem')),
215
- };
216
- }
217
- catch {
218
- throw new Error('interactive ceremony needs the dev-harness HTTPS certs — run ' +
219
- 'apps/v4-verify-web/dev-harness/generate-certs.sh once');
220
- }
221
- const vendorSdkPath = fileURLToPath(new URL('../../../apps/v4-verify-web/public/vendor/vgs-agentic-auth.js', import.meta.url));
222
- try {
223
- ceremonyVendorSdkJs = readFileSync(vendorSdkPath, 'utf8');
224
- }
225
- catch {
226
- throw new Error(`missing vendored VgsAgenticAuth module at ${vendorSdkPath}`);
227
- }
228
- }
229
- }
230
- // Client-credentials mint for the ceremony page's short-lived browser token —
231
- // same wire call as the dev-harness /api/token, served on loopback only.
232
- async function mintCeremonyAccessToken() {
233
- const clientId = process.env.VGS_USERNAME;
234
- const clientSecret = process.env.VGS_PASSWORD;
235
- if (!clientId || !clientSecret) {
236
- throw new Error('Set VGS_USERNAME and VGS_PASSWORD (the VGS service-account id/secret).');
237
- }
238
- const body = new URLSearchParams({
239
- client_id: clientId,
240
- client_secret: clientSecret,
241
- grant_type: 'client_credentials',
242
- });
243
- const r = await fetch('https://auth.verygoodsecurity.com/auth/realms/vgs/protocol/openid-connect/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body });
244
- if (!r.ok)
245
- throw new Error(`VGS token grant failed (${r.status})`);
246
- const doc = (await r.json());
247
- if (!doc.access_token)
248
- throw new Error('VGS token grant returned no access_token');
249
- return doc.access_token;
250
- }
251
- // Persist the redacted receipt for this attempt (LIVE_VIC_GUEST_CHECKOUT.md
252
- // step 8) and return the path for the printed result. Persisting never throws:
253
- // a failed write degrades to a stderr warning — the run result (and any
254
- // completed charge) must not be lost to a full disk or a permission error.
255
- async function persistReceipt(reviewId, result, vicConfirmation) {
256
- const report = await writeReceipt(receiptDir, buildReceipt({
257
- mode,
258
- reviewId,
259
- merchant: { name: target.merchantName, host: url.hostname },
260
- transaction: { amount: target.transactionAmount, amountMinor, currency },
261
- result,
262
- vicConfirmation,
263
- }));
264
- if (!report.written) {
265
- process.stderr.write(`WARNING: receipt not written — ${report.reason}\n`);
266
- return null;
267
- }
268
- if (report.panRedactions > 0) {
269
- process.stderr.write(`WARNING: ${report.panRedactions} PAN-like digit run(s) were redacted from the receipt — ` +
270
- 'upstream evidence must never contain one; investigate before the next run.\n');
271
- }
272
- return report.path;
273
- }
274
- const browser = await launchCheckoutBrowser();
275
- try {
276
- const mandate = {
277
- maxAmountMinor: amountMinor,
278
- currency,
279
- merchantHost: url.hostname,
280
- expiresAt: new Date(Date.now() + 15 * 60 * 1000).toISOString(),
281
- };
282
- const debugShotsDir = join(receiptDir, 'shots');
283
- process.stdout.write(`debug screenshots (credential fields masked): ${debugShotsDir}\n`);
284
- const preparation = await prepareCheckout({
285
- url: target.merchantUrl,
286
- mandate,
287
- amountMinor,
288
- currency,
289
- browser,
290
- debugShotsDir,
291
- });
292
- if (preparation.status === 'finished') {
293
- // Refused before a review existed (blocked-by-mandate, adapter-required,
294
- // …). Still a recorded attempt: the merchant-test matrix reconciles
295
- // compatibility findings, not just submissions.
296
- const receipt = await persistReceipt(null, preparation.result, null);
297
- process.stdout.write(`${JSON.stringify({ ...preparation.result, receipt }, null, 2)}\n`);
298
- process.exitCode = 1;
299
- }
300
- else {
301
- const review = preparation.checkout.review;
302
- // The full review contract (LIVE_VIC_GUEST_CHECKOUT.md step 1): merchant,
303
- // exact total, currency, mandate cap/expiry, and the fingerprint of the
304
- // exact control a submit-mode approval would click. Contains no credential.
305
- process.stdout.write(`${JSON.stringify({ reviewId: review.id, merchantHost: review.merchantHost, amountMinor: review.amountMinor, currency: review.currency, mandateMaxAmountMinor: review.mandateMaxAmountMinor, mandateExpiresAt: review.mandateExpiresAt, submitTarget: review.submitTarget, submitTargetFingerprint: review.submitTargetFingerprint, detectedRoles: review.detectedRoles }, null, 2)}\n`);
306
- const terminal = createInterface({ input: process.stdin, output: process.stdout });
307
- const expected = approvalPhrase(mode, review.id);
308
- const answer = await terminal.question(approvalQuestion(mode, review, expected));
309
- terminal.close();
310
- let result;
311
- let vicConfirmation = null;
312
- if (answer === expected) {
313
- // Assurance-mode inputs. In interactive ceremony mode the assurance is
314
- // produced NOW — after the typed approval, seconds before the mint (the
315
- // freshest possible configuration; see the 2026-07-17 PENDING-intent
316
- // contrast) — and exists in process memory only.
317
- let ceremonyFailure = null;
318
- let credential = null;
319
- let purchase = null;
320
- if (!reference) {
321
- credential = await readOwnerOnlyJson(agentCredentialFile, 'CLI agent credential', 40 * 1024);
322
- if (typeof purchaseAssuranceFile === 'string') {
323
- // Fresh, purchase-scoped assurance — the instrument validates its
324
- // declared merchant/amount/currency scope + freshness against the
325
- // checkout target before any intent is minted (#5709).
326
- purchase = await readOwnerOnlyJson(purchaseAssuranceFile, 'purchase assurance', 40 * 1024);
327
- }
328
- else if (typeof credential.tokenId !== 'string' || !credential.tokenId.trim()) {
329
- ceremonyFailure = 'CLI agent credential requires tokenId';
330
- }
331
- else {
332
- process.stdout.write('approval received — complete the passkey on the Visa approval page…\n');
333
- try {
334
- purchase = hostedApproval
335
- ? await runHostedApproval({
336
- baseUrl: approvalBaseUrl,
337
- tokenId: credential.tokenId,
338
- target,
339
- ...(ceremonyConsumerEmail ? { consumerEmail: ceremonyConsumerEmail } : {}),
340
- log: (line) => process.stdout.write(`${line}\n`),
341
- })
342
- : await runInteractiveCeremony({
343
- page: {
344
- tokenId: credential.tokenId,
345
- environment: process.env.VGS_ENVIRONMENT === 'sandbox' ? 'sandbox' : 'live',
346
- consumerEmail: ceremonyConsumerEmail,
347
- merchantName: target.merchantName,
348
- amount: target.transactionAmount,
349
- currency,
350
- currencyNumericCode: currencyNumeric(currency),
351
- },
352
- target,
353
- vendorSdkJs: ceremonyVendorSdkJs,
354
- tls: ceremonyTls,
355
- mintAccessToken: mintCeremonyAccessToken,
356
- log: (line) => process.stdout.write(`${line}\n`),
357
- });
358
- process.stdout.write('passkey approved — minting the credential…\n');
359
- }
360
- catch (err) {
361
- ceremonyFailure = err.message;
362
- }
363
- }
364
- }
365
- if (!reference && ceremonyFailure !== null) {
366
- result = await cancelPreparedCheckout(review.id, `device-binding ceremony failed: ${ceremonyFailure}`);
367
- }
368
- else {
369
- const instrument = reference
370
- ? new VgsLiveInstrument(reference, cardholderName, fetchCryptogram)
371
- : new VgsAssuranceInstrument(credential, purchase, target, cardholderName,
372
- // The minter surfaces the fresh intentId (unlike getCredential,
373
- // which discards it) so the VIC confirmation below can target it.
374
- async (input) => {
375
- const { intentId, status } = await createIntent(input);
376
- try {
377
- const payment = await fetchCryptogram({
378
- tokenId: input.tokenId,
379
- intentId,
380
- transaction: input.transaction,
381
- });
382
- return { payment, intentId };
383
- }
384
- catch (err) {
385
- // HTTP 201 at intent creation ≠ an authorized intent — carry
386
- // the creation-time status so a not-completed cryptogram is
387
- // diagnosable in place (#5709).
388
- throw new Error(`${err.message} (intent status at creation: ${status ?? 'unknown'})`);
389
- }
390
- });
391
- result = await submitApprovedCheckout(review.id, {
392
- approval: { approved: true, reviewId: review.id },
393
- instrument,
394
- contact,
395
- mode,
396
- challengeHoldMs,
397
- onChallengeHold: (signal) => process.stdout.write(signal === 'body:link-wallet'
398
- ? `Stripe Link wallet login appeared despite network suppression — this means Link engaged through an endpoint isStripeLinkConsumerRequest does not yet match. Do NOT enter the code (it would pay with a Link-saved card, not the minted credential): close the modal to continue as guest, then pull the Link request URL from the evidence log (note "linkSuppressed"/network trace) and extend the route predicate to cover it. Holding up to ${challengeHoldMinutes} min…\n`
399
- : `issuer challenge detected (${signal ?? 'challenge'}) — complete the bank verification in the browser window (enter the code your bank sent). Holding up to ${challengeHoldMinutes} min…\n`),
400
- });
401
- if (mode === 'submit') {
402
- // Report the OBSERVED outcome to VIC for the consumed intent. Only
403
- // definitive answers post (confirmed → APPROVED, declined → DECLINED);
404
- // unknown outcomes are resolved with the merchant first, never
405
- // guessed. A ceremony failure never reaches here — no intent was
406
- // consumed, so there is nothing to confirm.
407
- vicConfirmation = await reportVicOutcome({
408
- target: instrument.confirmationTarget(),
409
- outcome: result.outcome,
410
- transaction: {
411
- transactionAmount: target.transactionAmount,
412
- transactionCurrencyCode: currency,
413
- },
414
- post: postConfirmation,
415
- });
416
- }
417
- }
418
- }
419
- else {
420
- result = await cancelPreparedCheckout(review.id, 'operator declined the checkout review');
421
- }
422
- const receipt = await persistReceipt(review.id, result, vicConfirmation);
423
- process.stdout.write(`${JSON.stringify({ outcome: result.outcome, confirmationRef: result.confirmationRef ?? null, vicConfirmation, receipt, credentialLifecycle: result.credentialLifecycle, credentialTiming: result.credentialTiming, detectedRoles: Object.keys(result.fields), requiresAdapter: result.requiresAdapter, detail: result.detail ?? null }, null, 2)}\n`);
424
- if (submitClickedWithoutConfirmation(result)) {
425
- process.stderr.write('WARNING: the pay control was clicked but no confirmation was detected — the charge ' +
426
- 'may still have gone through. Verify with the merchant before any retry; a blind ' +
427
- 'rerun risks a double charge.\n');
428
- }
429
- if (result.outcome === 'action-required') {
430
- process.stderr.write('NOTICE: the issuer asked for human verification (3-D Secure) and the run stopped ' +
431
- 'there — no charge exists until the challenge is completed. Complete the purchase ' +
432
- 'manually if still wanted; any rerun needs a new review and a fresh credential.\n');
433
- }
434
- if (vicConfirmation && !vicConfirmation.posted && isRunSuccess(mode, result.outcome)) {
435
- process.stderr.write(`WARNING: purchase confirmed but the VIC confirmation was not posted — ${vicConfirmation.reason}\n`);
436
- }
437
- if (!isRunSuccess(mode, result.outcome))
438
- process.exitCode = 1;
439
- }
440
- }
441
- finally {
442
- await browser.close();
443
- }
@@ -1,26 +0,0 @@
1
- export type Contact = {
2
- email?: string;
3
- firstName?: string;
4
- lastName?: string;
5
- fullName?: string;
6
- addressLine1?: string;
7
- addressLine2?: string;
8
- city?: string;
9
- state?: string;
10
- postalCode?: string;
11
- country?: string;
12
- };
13
- export type FilledField = {
14
- role: string;
15
- locator: string;
16
- confidence: number;
17
- source: string;
18
- frame?: string;
19
- value: string;
20
- ok: boolean;
21
- error?: string;
22
- };
23
- export type FillResult = {
24
- ok: boolean;
25
- filled: FilledField[];
26
- };
@@ -1,2 +0,0 @@
1
- // Shared value types used across the engine.
2
- export {};
@@ -1,74 +0,0 @@
1
- export type VgsTransaction = {
2
- merchantName: string
3
- merchantUrl: string
4
- merchantCountryCode: string
5
- transactionAmount: string
6
- transactionCurrencyCode: string
7
- }
8
-
9
- export function createIntent(
10
- input: {
11
- tokenId: string
12
- assuranceData: unknown
13
- transaction: VgsTransaction
14
- },
15
- env?: Record<string, string | undefined>
16
- ): Promise<{
17
- intentId: string
18
- /**
19
- * Creation-time status attribute (null when VGS omits it). HTTP 201 alone
20
- * does not mean the intent is authorized — surface this when a later
21
- * cryptogram fails (#5709).
22
- */
23
- status: string | null
24
- }>
25
-
26
- export function fetchCryptogram(
27
- input: {
28
- tokenId: string
29
- intentId: string
30
- transaction: VgsTransaction
31
- },
32
- env?: Record<string, string | undefined>
33
- ): Promise<{
34
- networkToken: string
35
- expMonth: number
36
- expYear: number
37
- cryptogramType: string
38
- cryptogramValue: string
39
- cryptogramExpiresAt?: string
40
- }>
41
-
42
- export function getCredential(
43
- input: {
44
- tokenId: string
45
- assuranceData: unknown
46
- transaction: VgsTransaction
47
- },
48
- env?: Record<string, string | undefined>
49
- ): Promise<{
50
- networkToken: string
51
- expMonth: number
52
- expYear: number
53
- cryptogramType: string
54
- cryptogramValue: string
55
- cryptogramExpiresAt?: string
56
- }>
57
-
58
- export function postConfirmation(input: {
59
- tokenId: string
60
- intentId: string
61
- transactionStatus: 'APPROVED' | 'DECLINED' | 'PENDING' | 'ERROR' | 'CANCELLED'
62
- transactionType:
63
- | 'PURCHASE'
64
- | 'AUTHORIZATION'
65
- | 'CAPTURE'
66
- | 'REFUND'
67
- | 'REVERSAL'
68
- | 'VERIFICATION'
69
- | 'CHARGEBACK'
70
- | 'FRAUD'
71
- /** ISO 8601; converted to the guide's quoted epoch-seconds on the wire. */
72
- transactionTimestamp: string
73
- transaction: Pick<VgsTransaction, 'transactionAmount' | 'transactionCurrencyCode'>
74
- }, env?: Record<string, string | undefined>): Promise<{ ok: true }>