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

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 +295 -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,78 +0,0 @@
1
- import type { PurchaseAssurance, VgsCheckoutTarget } from './vgs-live-instrument.js';
2
- /**
3
- * Hosted purchase approval — the deployed-site replacement for the loopback
4
- * ceremony page (ceremony.ts). The passkey tap happens on the verify-web
5
- * deployment's /approve page instead of a 127.0.0.1 tab; this module is the
6
- * runner's side of that relay (PKCE / device-code shape, mirroring the app's
7
- * lib/server/agent-approval.ts):
8
- *
9
- * 1. Mint a 32-byte VERIFIER in process memory (never on disk — an approval
10
- * lives and dies inside one run), derive challenge = sha256(verifier).
11
- * 2. Register the purchase context under the challenge, then open
12
- * <baseUrl>/approve?req=<challenge> in the operator's browser.
13
- * 3. Poll the claim route with the verifier: `pending` while the operator
14
- * signs in and taps the passkey; `completed` returns the assuranceData
15
- * ONCE and the entry is gone.
16
- *
17
- * The URL only ever carries the challenge — useless without the verifier —
18
- * and the verifier only ever travels to an HTTPS (or explicit-loopback)
19
- * origin. The claim response echoes the context the approval was COMPLETED
20
- * against; it is verified field-by-field against this run's checkout target
21
- * before the assuranceData is accepted, so relay corruption or a divergent
22
- * registration can never smuggle in an assurance scoped to something else
23
- * (#5709 would reject it at cryptogram time anyway — this fails minutes
24
- * earlier, with the divergent field named). Every network wait is bounded by
25
- * the remaining deadline via AbortController — a stalled connection cannot
26
- * hang the runner past the advertised timeout.
27
- */
28
- export type HostedApprovalOptions = {
29
- /** The verify-web deployment origin, e.g. https://v4-verify-web-….up.railway.app */
30
- baseUrl: string;
31
- /** Durable agentic token id from the CLI agent credential. */
32
- tokenId: string;
33
- target: VgsCheckoutTarget;
34
- /** Prefills the approval page's enrollment-email field (optional). */
35
- consumerEmail?: string;
36
- log?: (line: string) => void;
37
- /** Injectable for tests; default opens the operator's default browser (macOS). */
38
- openUrl?: (url: string) => void;
39
- fetchImpl?: typeof fetch;
40
- /** Injectable for tests — never wall-clock-sleep in a unit test. */
41
- sleep?: (ms: number) => Promise<void>;
42
- now?: () => number;
43
- pollIntervalMs?: number;
44
- timeoutMs?: number;
45
- };
46
- export declare const HOSTED_APPROVAL_TIMEOUT_MS: number;
47
- export declare const HOSTED_APPROVAL_POLL_MS = 3000;
48
- /**
49
- * The one deployed verify site — hosted approval is the NORMAL path (no local
50
- * certs, no vendored SDK, no localhost anywhere), so its origin is a built-in
51
- * default rather than per-machine config. Not a secret: it's the public page
52
- * the operator's browser opens anyway.
53
- */
54
- export declare const DEFAULT_APPROVAL_BASE_URL = "https://v4-verify-web-visa-code-preview.up.railway.app";
55
- /**
56
- * Flag > environment > built-in default. An explicitly EMPTY value
57
- * (CHECKOUT_APPROVAL_BASE_URL='') opts out of hosted approval entirely —
58
- * that is the loopback-ceremony escape hatch, so the default must not
59
- * resurrect over it (?? not ||).
60
- */
61
- export declare function resolveApprovalBaseUrl(flagValue: string | undefined, env?: Record<string, string | undefined>): string;
62
- /**
63
- * Validate the approval-page origin: HTTPS everywhere except explicit
64
- * loopback hosts — the SAME policy the loader applies to the checkout URL.
65
- * The claim call sends the verifier (the credential that releases the
66
- * assurance), so a cleartext remote origin is never acceptable.
67
- */
68
- export declare function assertApprovalBaseUrl(value: string): string;
69
- /**
70
- * The claim also releases the scoped MINT TOKEN (server-side mint, Phase 1) —
71
- * bound to this exact approved purchase — so the runner can call the gateway
72
- * mint routes without the VGS secret. Absent when the deployment ran the
73
- * dev-auth stub (it mints no token); the caller then surfaces a clear error
74
- * rather than falling back to a client-held secret.
75
- */
76
- export declare function runHostedApproval(opts: HostedApprovalOptions): Promise<PurchaseAssurance & {
77
- mintToken?: string;
78
- }>;
@@ -1,171 +0,0 @@
1
- import { createHash, randomBytes } from 'node:crypto';
2
- import { exec } from 'node:child_process';
3
- import { assuranceFromCeremony } from './ceremony.js';
4
- export const HOSTED_APPROVAL_TIMEOUT_MS = 4 * 60 * 1000;
5
- export const HOSTED_APPROVAL_POLL_MS = 3_000;
6
- /** Unref'd so a raced-and-abandoned deadline timer can never hold the process open. */
7
- const defaultSleep = (ms) => new Promise((r) => {
8
- const t = setTimeout(r, ms);
9
- t.unref?.();
10
- });
11
- function defaultOpenUrl(url) {
12
- // Quoting: the URL is server-origin + our own base64url challenge — no
13
- // shell-hostile characters — but single-quote anyway.
14
- exec(`open '${url.replaceAll("'", "'\\''")}'`);
15
- }
16
- /**
17
- * The one deployed verify site — hosted approval is the NORMAL path (no local
18
- * certs, no vendored SDK, no localhost anywhere), so its origin is a built-in
19
- * default rather than per-machine config. Not a secret: it's the public page
20
- * the operator's browser opens anyway.
21
- */
22
- export const DEFAULT_APPROVAL_BASE_URL = 'https://v4-verify-web-visa-code-preview.up.railway.app';
23
- /**
24
- * Flag > environment > built-in default. An explicitly EMPTY value
25
- * (CHECKOUT_APPROVAL_BASE_URL='') opts out of hosted approval entirely —
26
- * that is the loopback-ceremony escape hatch, so the default must not
27
- * resurrect over it (?? not ||).
28
- */
29
- export function resolveApprovalBaseUrl(flagValue, env = process.env) {
30
- return (flagValue ?? env.CHECKOUT_APPROVAL_BASE_URL ?? DEFAULT_APPROVAL_BASE_URL).trim();
31
- }
32
- /**
33
- * Validate the approval-page origin: HTTPS everywhere except explicit
34
- * loopback hosts — the SAME policy the loader applies to the checkout URL.
35
- * The claim call sends the verifier (the credential that releases the
36
- * assurance), so a cleartext remote origin is never acceptable.
37
- */
38
- export function assertApprovalBaseUrl(value) {
39
- let url;
40
- try {
41
- url = new URL(value);
42
- }
43
- catch {
44
- throw new Error('--approval-base-url must be a valid http(s) origin');
45
- }
46
- const loopback = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
47
- if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
48
- throw new Error('--approval-base-url must use HTTPS (localhost is allowed for local testing) — ' +
49
- 'the approval claim carries a credential and never travels cleartext');
50
- }
51
- return value;
52
- }
53
- function stripTrailingSlashes(value) {
54
- let out = value;
55
- while (out.endsWith('/'))
56
- out = out.slice(0, -1);
57
- return out;
58
- }
59
- /**
60
- * The claim also releases the scoped MINT TOKEN (server-side mint, Phase 1) —
61
- * bound to this exact approved purchase — so the runner can call the gateway
62
- * mint routes without the VGS secret. Absent when the deployment ran the
63
- * dev-auth stub (it mints no token); the caller then surfaces a clear error
64
- * rather than falling back to a client-held secret.
65
- */
66
- export async function runHostedApproval(opts) {
67
- const { baseUrl, tokenId, target, consumerEmail, log = () => { }, openUrl = defaultOpenUrl, fetchImpl = fetch, sleep = defaultSleep, now = Date.now, pollIntervalMs = HOSTED_APPROVAL_POLL_MS, timeoutMs = HOSTED_APPROVAL_TIMEOUT_MS, } = opts;
68
- const base = stripTrailingSlashes(assertApprovalBaseUrl(baseUrl));
69
- // The server's currency map is uppercase ISO 4217; the loader normalizes the
70
- // target once, but normalize here too so a direct caller with a lowercase
71
- // code registers (and verifies) the same value the relay stores.
72
- const currency = target.transactionCurrencyCode.toUpperCase();
73
- const verifier = randomBytes(32).toString('base64url');
74
- const challenge = createHash('sha256').update(verifier, 'utf8').digest('base64url');
75
- const deadline = now() + timeoutMs;
76
- const timeoutError = () => new Error(`no passkey approval within ${Math.round(timeoutMs / 1000)}s — ` +
77
- 'the checkout was cancelled; run again to retry');
78
- // Bound EVERY request by the remaining deadline: race the fetch against the
79
- // (injectable) sleep and abort the request when the deadline wins, so a
80
- // stalled connection can never hang the runner past the advertised timeout.
81
- const TIMED_OUT = Symbol('timed-out');
82
- async function fetchWithDeadline(url, init) {
83
- const remaining = deadline - now();
84
- if (remaining <= 0)
85
- throw timeoutError();
86
- const controller = new AbortController();
87
- const winner = await Promise.race([
88
- fetchImpl(url, { ...init, signal: controller.signal }),
89
- sleep(remaining).then(() => TIMED_OUT),
90
- ]);
91
- if (typeof winner === 'symbol') {
92
- controller.abort();
93
- throw timeoutError();
94
- }
95
- return winner;
96
- }
97
- const registerRes = await fetchWithDeadline(`${base}/api/vgs/agent-approval`, {
98
- method: 'POST',
99
- headers: { 'content-type': 'application/json' },
100
- body: JSON.stringify({
101
- challenge,
102
- context: {
103
- tokenId,
104
- merchantName: target.merchantName,
105
- merchantUrl: target.merchantUrl,
106
- merchantCountryCode: target.merchantCountryCode,
107
- amount: target.transactionAmount,
108
- currency,
109
- ...(consumerEmail ? { consumerEmail } : {}),
110
- },
111
- }),
112
- });
113
- if (!registerRes.ok) {
114
- const body = (await registerRes.json().catch(() => ({})));
115
- throw new Error(`could not register the hosted approval (${registerRes.status})` +
116
- (body.error ? `: ${body.error}` : ''));
117
- }
118
- const approveUrl = `${base}/approve?req=${challenge}`;
119
- log(`approve the purchase in your browser: ${approveUrl}`);
120
- openUrl(approveUrl);
121
- for (;;) {
122
- const res = await fetchWithDeadline(`${base}/api/vgs/agent-approval/claim`, {
123
- method: 'POST',
124
- headers: { 'content-type': 'application/json' },
125
- body: JSON.stringify({ verifier }),
126
- });
127
- if (res.ok) {
128
- const doc = (await res.json().catch(() => null));
129
- if (doc?.status === 'completed') {
130
- if (doc.assuranceData === undefined || doc.assuranceData === null) {
131
- throw new Error('hosted approval completed but carried no assuranceData');
132
- }
133
- // The context the approval was COMPLETED against must be exactly this
134
- // run's checkout target — a divergent field means the relay entry was
135
- // not ours (corruption, or a mutated registration) and the assurance
136
- // is scoped to something else. Fail naming the field; no secrets here
137
- // (merchant facts only).
138
- const expected = {
139
- tokenId,
140
- merchantName: target.merchantName,
141
- merchantUrl: target.merchantUrl,
142
- merchantCountryCode: target.merchantCountryCode,
143
- amount: target.transactionAmount,
144
- currency,
145
- };
146
- for (const [field, want] of Object.entries(expected)) {
147
- if (doc.context?.[field] !== want) {
148
- throw new Error(`hosted approval context mismatch on ${field} — the approval was not for this ` +
149
- 'exact purchase; run the checkout again for a fresh link');
150
- }
151
- }
152
- log('passkey approval received from the hosted page.');
153
- return {
154
- ...assuranceFromCeremony(target, doc.assuranceData),
155
- ...(typeof doc.mintToken === 'string' && doc.mintToken
156
- ? { mintToken: doc.mintToken }
157
- : {}),
158
- };
159
- }
160
- // status 'pending' — the operator is still signing in / tapping.
161
- }
162
- else if (res.status === 404) {
163
- // Registered moments ago, so absent now means expired or already claimed.
164
- throw new Error('the hosted approval expired or was already used — run the checkout again for a fresh link');
165
- }
166
- // Any other status (429 rate bucket, transient 5xx) polls through.
167
- if (now() >= deadline)
168
- throw timeoutError();
169
- await sleep(pollIntervalMs);
170
- }
171
- }
@@ -1,3 +0,0 @@
1
- export { createCliCheckoutEngine, type CliReviewInput, type CliReviewFacts, type CliPayInput, type CliReceiptFacts, } from './cli-engine.js';
2
- export { prepareCheckout, submitApprovedCheckout, runCheckout, InMemoryPreparedCheckoutStore, } from './executor.js';
3
- export type { CheckoutResult, CheckoutReview, CheckoutOutcome } from './executor.js';
@@ -1,5 +0,0 @@
1
- // Public API of @visa/checkout-engine. The pay_merchant tool in @visa/cli
2
- // consumes createCliCheckoutEngine() through a structural seam; the core engine
3
- // primitives are re-exported for direct/embedded use.
4
- export { createCliCheckoutEngine, } from './cli-engine.js';
5
- export { prepareCheckout, submitApprovedCheckout, runCheckout, InMemoryPreparedCheckoutStore, } from './executor.js';
@@ -1,13 +0,0 @@
1
- import type { VgsCheckoutTarget } from './vgs-live-instrument.js';
2
- /**
3
- * Build a checkout target from inline CLI flags (`--merchant-url` +
4
- * `--amount`, with optional `--merchant-name` / `--country` / `--currency`)
5
- * so any merchant can be paid without authoring a checkout JSON file first.
6
- *
7
- * Returns null when neither driving flag is present (file mode). Shape-only:
8
- * the returned target flows through run-live-fill's existing validation block
9
- * (positive-decimal amount, ISO codes, HTTPS), which stays the single source
10
- * of truth — nothing is double-validated here except the URL parse, which
11
- * must happen early because the merchant-name default derives from it.
12
- */
13
- export declare function inlineTargetFromFlags(input: Map<string, string>): VgsCheckoutTarget | null;
@@ -1,37 +0,0 @@
1
- /**
2
- * Build a checkout target from inline CLI flags (`--merchant-url` +
3
- * `--amount`, with optional `--merchant-name` / `--country` / `--currency`)
4
- * so any merchant can be paid without authoring a checkout JSON file first.
5
- *
6
- * Returns null when neither driving flag is present (file mode). Shape-only:
7
- * the returned target flows through run-live-fill's existing validation block
8
- * (positive-decimal amount, ISO codes, HTTPS), which stays the single source
9
- * of truth — nothing is double-validated here except the URL parse, which
10
- * must happen early because the merchant-name default derives from it.
11
- */
12
- export function inlineTargetFromFlags(input) {
13
- const merchantUrl = input.get('--merchant-url');
14
- const amount = input.get('--amount');
15
- if (merchantUrl === undefined && amount === undefined)
16
- return null;
17
- if (!merchantUrl || !amount) {
18
- throw new Error('inline checkout needs both --merchant-url and --amount');
19
- }
20
- let host;
21
- try {
22
- host = new URL(merchantUrl).hostname;
23
- }
24
- catch {
25
- throw new Error('--merchant-url must be a valid URL');
26
- }
27
- if (!host) {
28
- throw new Error('--merchant-url must have a hostname');
29
- }
30
- return {
31
- merchantName: input.get('--merchant-name') ?? host,
32
- merchantUrl,
33
- merchantCountryCode: input.get('--country') ?? 'US',
34
- transactionAmount: amount,
35
- transactionCurrencyCode: input.get('--currency') ?? 'USD',
36
- };
37
- }
@@ -1,54 +0,0 @@
1
- export type CardCredential = {
2
- pan: string;
3
- expMonth: number;
4
- expYear: number;
5
- cvc: string;
6
- cardholderName: string;
7
- /** Dynamic payment credential expiry, distinct from the card expiration. */
8
- credentialExpiresAt?: string;
9
- };
10
- export type InstrumentContext = {
11
- merchantHost: string;
12
- amountMinor: number;
13
- currency: string;
14
- };
15
- export type InstrumentKind = 'test-card' | 'vgs-alias' | 'agentic-token';
16
- export interface Instrument {
17
- kind: InstrumentKind;
18
- getCredential(ctx: InstrumentContext): Promise<CardCredential>;
19
- }
20
- export declare const OFFICIAL_TEST_PANS: {
21
- readonly visa: "4242424242424242";
22
- readonly visaAlt: "4111111111111111";
23
- readonly visaDecline: "4000000000000002";
24
- readonly visaSlowConfirm: "4000000000000069";
25
- readonly visaUnknownOutcome: "4000000000000044";
26
- readonly visaChallenge: "4000000000000010";
27
- };
28
- export type TestCardOptions = {
29
- pan?: string;
30
- cardholderName?: string;
31
- cvc?: string;
32
- expMonth?: number;
33
- expYear?: number;
34
- };
35
- export declare class TestCardInstrument implements Instrument {
36
- readonly kind: "test-card";
37
- private readonly credential;
38
- constructor(opts?: TestCardOptions);
39
- getCredential(_ctx: InstrumentContext): Promise<CardCredential>;
40
- }
41
- export declare class VgsAliasInstrument implements Instrument {
42
- private readonly _alias;
43
- readonly kind: "vgs-alias";
44
- constructor(_alias: string);
45
- getCredential(_ctx: InstrumentContext): Promise<CardCredential>;
46
- }
47
- export declare class AgenticTokenInstrument implements Instrument {
48
- private readonly tokenRef;
49
- private readonly mintCredential?;
50
- readonly kind: "agentic-token";
51
- private used;
52
- constructor(tokenRef: string, mintCredential?: ((ctx: InstrumentContext) => Promise<CardCredential>) | undefined);
53
- getCredential(ctx: InstrumentContext): Promise<CardCredential>;
54
- }
@@ -1,83 +0,0 @@
1
- // Instrument abstraction: the checkout engine is deliberately blind to WHERE
2
- // card credentials come from. Official test PANs, callback-backed agentic
3
- // credentials, and owner-only VIC harness files are enabled; VGS aliases stay
4
- // stubbed until a reveal seam exists. The executor remains unchanged across
5
- // every instrument.
6
- // Official public test PANs only. These are the sanctioned Visa/network test
7
- // numbers used against local fixtures; they carry no funds and pass Luhn.
8
- export const OFFICIAL_TEST_PANS = {
9
- visa: '4242424242424242',
10
- visaAlt: '4111111111111111',
11
- // PAN whose last four are 0002 is treated as a forced decline by the
12
- // fixture acquirer, for exercising the declined outcome path.
13
- visaDecline: '4000000000000002',
14
- // Ending 0069: the fixture acquirer shows a processing interstitial, then
15
- // JS-redirects to a Shopify-style thank-you URL with no confirmation text —
16
- // exercises the outcome observer's late, URL-signal confirmation path.
17
- visaSlowConfirm: '4000000000000069',
18
- // Ending 0044: the fixture acquirer returns a neutral page with neither a
19
- // confirmation nor a decline signal — exercises the unknown-outcome deadline.
20
- visaUnknownOutcome: '4000000000000044',
21
- // Ending 0010: the fixture acquirer answers with a 3DS-style verification
22
- // page embedding an ACS challenge iframe that never completes — exercises
23
- // the observer's action-required (issuer challenge) path.
24
- visaChallenge: '4000000000000010',
25
- };
26
- function futureExpiry() {
27
- const now = new Date();
28
- return { expMonth: 12, expYear: now.getFullYear() + 3 };
29
- }
30
- export class TestCardInstrument {
31
- kind = 'test-card';
32
- credential;
33
- constructor(opts = {}) {
34
- const exp = futureExpiry();
35
- this.credential = {
36
- pan: opts.pan ?? OFFICIAL_TEST_PANS.visa,
37
- expMonth: opts.expMonth ?? exp.expMonth,
38
- expYear: opts.expYear ?? exp.expYear,
39
- cvc: opts.cvc ?? '123',
40
- cardholderName: opts.cardholderName ?? 'Test Agent',
41
- };
42
- }
43
- async getCredential(_ctx) {
44
- return this.credential;
45
- }
46
- }
47
- const NOT_ENABLED = 'not enabled: pending VGS alias reveal support';
48
- // Stub. Once VGS enablement lands, this resolves a network token / alias into a
49
- // fillable credential (a virtual PAN plus its verification value).
50
- export class VgsAliasInstrument {
51
- _alias;
52
- kind = 'vgs-alias';
53
- constructor(_alias) {
54
- this._alias = _alias;
55
- }
56
- async getCredential(_ctx) {
57
- throw new Error(`${NOT_ENABLED}. VgsAliasInstrument cannot resolve alias "${this._alias}" ` +
58
- 'to a fillable PAN until VGS exposes the reveal seam for this spike.');
59
- }
60
- }
61
- // A server-side provider mints the transaction-bound credential whose pan
62
- // carries the DPAN and whose cvc carries the short DAVV. Omitting the provider
63
- // preserves a clear fail-closed result for callers that have only a token ref.
64
- export class AgenticTokenInstrument {
65
- tokenRef;
66
- mintCredential;
67
- kind = 'agentic-token';
68
- used = false;
69
- constructor(tokenRef, mintCredential) {
70
- this.tokenRef = tokenRef;
71
- this.mintCredential = mintCredential;
72
- }
73
- async getCredential(ctx) {
74
- if (!this.mintCredential) {
75
- throw new Error('not enabled: no agentic credential provider configured. ' +
76
- `Token ${this.tokenRef} needs a server-side VGS credential provider.`);
77
- }
78
- if (this.used)
79
- throw new Error('agentic credential instrument is single-use');
80
- this.used = true;
81
- return this.mintCredential(ctx);
82
- }
83
- }
@@ -1,52 +0,0 @@
1
- import type { CheckoutMode, CheckoutOutcome, CheckoutResult } from './executor.js';
2
- export declare const SUBMIT_ENV_FLAG = "CHECKOUT_AGENT_ALLOW_SUBMIT";
3
- export declare const MODE_ENV_FLAG = "CHECKOUT_AGENT_MODE";
4
- /**
5
- * `--mode` values map 1:1 onto CheckoutMode; an absent flag falls back to the
6
- * CHECKOUT_AGENT_MODE environment default, then to the safe dry-run default.
7
- */
8
- export declare function parseCheckoutMode(value: string | undefined, env?: Record<string, string | undefined>): CheckoutMode;
9
- /**
10
- * Fail fast (before any file read or browser launch) when submit lacks an
11
- * enablement. The env arm exists to stop AUTOMATION from reaching a live
12
- * charge by accident — so it is required only when stdin is not a real TTY
13
- * (scripts, agents, pipes). An interactive human needs no standing config:
14
- * their per-run consent is the typed PAY phrase plus the passkey tap, both of
15
- * which a non-interactive caller cannot fake (the phrase names the exact
16
- * review id; the passkey is a FIDO ceremony on the verify site).
17
- */
18
- export declare function assertSubmitAllowed(mode: CheckoutMode, env?: Record<string, string | undefined>, isInteractive?: boolean): void;
19
- export declare const LEGACY_REFERENCE_ENV_FLAG = "CHECKOUT_AGENT_ALLOW_LEGACY_REFERENCE";
20
- /**
21
- * The legacy --reference-file path replays a PRE-CREATED intent, so the
22
- * purchase-assurance gate (#5709) cannot vet the ceremony that backed it —
23
- * scope/freshness problems surface only as the opaque cryptogram failure the
24
- * gate exists to prevent. Default-off escape hatch, same shape as the submit
25
- * gate: fail fast before any file is read.
26
- */
27
- export declare function assertLegacyReferenceAllowed(usingReferenceFile: boolean, env?: Record<string, string | undefined>): void;
28
- /** Distinct phrases per mode so dry-run muscle memory can never authorize a payment. */
29
- export declare function approvalPhrase(mode: CheckoutMode, reviewId: string): string;
30
- /** Integer-only minor→display formatting (two-decimal currencies, the same assumption as decimalToMinor). */
31
- export declare function formatAmountMinor(amountMinor: number, currency: string): string;
32
- /** The operator question. In submit mode it names the exact charge — amount and merchant. */
33
- export declare function approvalQuestion(mode: CheckoutMode, review: {
34
- merchantHost: string;
35
- amountMinor: number;
36
- currency: string;
37
- }, phrase: string): string;
38
- /** The one outcome each mode may exit 0 with. */
39
- export declare function isRunSuccess(mode: CheckoutMode, outcome: CheckoutOutcome): boolean;
40
- /**
41
- * True when the pay control was actually clicked but the run ended with an
42
- * UNKNOWN outcome — neither 'confirmed' nor 'declined' (e.g. a slow redirect
43
- * or confirmation copy the heuristics don't recognize). The charge may still
44
- * have gone through; the operator must verify with the merchant before any
45
- * retry, because a blind rerun risks a double charge. A 'declined' outcome is
46
- * a definitive negative from the merchant, so it does not warn. Neither does
47
- * 'action-required': a 3-D Secure challenge intercepts BEFORE authorization,
48
- * so no charge can exist until it is completed — the double-charge warning
49
- * would misdirect the operator, and the runner prints the challenge-specific
50
- * notice instead.
51
- */
52
- export declare function submitClickedWithoutConfirmation(result: CheckoutResult): boolean;
@@ -1,107 +0,0 @@
1
- // Deliberate-enablement seam for run-live-fill's submit mode. A real submit
2
- // moves real money, so it requires:
3
- // 1. an interactive human at a real TTY — or, for scripted/agent runs,
4
- // the explicit CHECKOUT_AGENT_ALLOW_SUBMIT=1 environment arm,
5
- // 2. the invocation opts in (--mode submit, or the CHECKOUT_AGENT_MODE
6
- // environment default for operators who always run one mode),
7
- // 3. the operator types the PAY phrase against the exact review id (and a
8
- // hosted run additionally requires the passkey tap on the verify site).
9
- // The env arm is an anti-automation gate, not a human ritual: a TTY human's
10
- // consent is act 3 — the PAY phrase differs from dry-run's FILL phrase, so a
11
- // muscle-memory rerun of the dry-run procedure still cannot drift into
12
- // charging a card — while a non-interactive caller (script, agent, pipe) can
13
- // fake neither the phrase's review id nor the FIDO ceremony and must also be
14
- // explicitly armed. Fresh checkouts therefore need ZERO persisted config.
15
- export const SUBMIT_ENV_FLAG = 'CHECKOUT_AGENT_ALLOW_SUBMIT';
16
- export const MODE_ENV_FLAG = 'CHECKOUT_AGENT_MODE';
17
- /**
18
- * `--mode` values map 1:1 onto CheckoutMode; an absent flag falls back to the
19
- * CHECKOUT_AGENT_MODE environment default, then to the safe dry-run default.
20
- */
21
- export function parseCheckoutMode(value, env = process.env) {
22
- const effective = value ?? env[MODE_ENV_FLAG];
23
- if (effective === undefined || effective === 'dry-run')
24
- return 'dry-run';
25
- if (effective === 'submit')
26
- return 'submit';
27
- throw new Error(`--mode (or ${MODE_ENV_FLAG}) must be dry-run or submit (got ${JSON.stringify(effective)})`);
28
- }
29
- /**
30
- * Fail fast (before any file read or browser launch) when submit lacks an
31
- * enablement. The env arm exists to stop AUTOMATION from reaching a live
32
- * charge by accident — so it is required only when stdin is not a real TTY
33
- * (scripts, agents, pipes). An interactive human needs no standing config:
34
- * their per-run consent is the typed PAY phrase plus the passkey tap, both of
35
- * which a non-interactive caller cannot fake (the phrase names the exact
36
- * review id; the passkey is a FIDO ceremony on the verify site).
37
- */
38
- export function assertSubmitAllowed(mode, env = process.env, isInteractive = process.stdin.isTTY === true) {
39
- if (mode !== 'submit')
40
- return;
41
- if (env[SUBMIT_ENV_FLAG] === '1')
42
- return;
43
- if (isInteractive)
44
- return;
45
- throw new Error(`--mode submit refused: this is not an interactive terminal, and scripted submits ` +
46
- `require ${SUBMIT_ENV_FLAG}=1. The gate is default-off so automation cannot ` +
47
- 'reach a live charge by accident.');
48
- }
49
- export const LEGACY_REFERENCE_ENV_FLAG = 'CHECKOUT_AGENT_ALLOW_LEGACY_REFERENCE';
50
- /**
51
- * The legacy --reference-file path replays a PRE-CREATED intent, so the
52
- * purchase-assurance gate (#5709) cannot vet the ceremony that backed it —
53
- * scope/freshness problems surface only as the opaque cryptogram failure the
54
- * gate exists to prevent. Default-off escape hatch, same shape as the submit
55
- * gate: fail fast before any file is read.
56
- */
57
- export function assertLegacyReferenceAllowed(usingReferenceFile, env = process.env) {
58
- if (!usingReferenceFile)
59
- return;
60
- if (env[LEGACY_REFERENCE_ENV_FLAG] !== '1') {
61
- throw new Error(`--reference-file refused: this legacy path bypasses purchase-assurance validation ` +
62
- `(#5709) — prefer --checkout-file with --purchase-assurance-file. Set ` +
63
- `${LEGACY_REFERENCE_ENV_FLAG}=1 to proceed anyway.`);
64
- }
65
- }
66
- /** Distinct phrases per mode so dry-run muscle memory can never authorize a payment. */
67
- export function approvalPhrase(mode, reviewId) {
68
- return `${mode === 'submit' ? 'PAY' : 'FILL'} ${reviewId}`;
69
- }
70
- /** Integer-only minor→display formatting (two-decimal currencies, the same assumption as decimalToMinor). */
71
- export function formatAmountMinor(amountMinor, currency) {
72
- return `${Math.floor(amountMinor / 100)}.${String(amountMinor % 100).padStart(2, '0')} ${currency}`;
73
- }
74
- /** The operator question. In submit mode it names the exact charge — amount and merchant. */
75
- export function approvalQuestion(mode, review, phrase) {
76
- if (mode === 'submit') {
77
- return (`Inspect the visible checkout. This WILL charge ` +
78
- `${formatAmountMinor(review.amountMinor, review.currency)} at ${review.merchantHost}. ` +
79
- `Type ${JSON.stringify(phrase)} to disclose the credential AND submit the order: `);
80
- }
81
- return (`Inspect the visible checkout. Type ${JSON.stringify(phrase)} ` +
82
- `to disclose the credential to this page without submitting: `);
83
- }
84
- /** The one outcome each mode may exit 0 with. */
85
- export function isRunSuccess(mode, outcome) {
86
- return outcome === (mode === 'submit' ? 'confirmed' : 'filled-dry-run');
87
- }
88
- /**
89
- * True when the pay control was actually clicked but the run ended with an
90
- * UNKNOWN outcome — neither 'confirmed' nor 'declined' (e.g. a slow redirect
91
- * or confirmation copy the heuristics don't recognize). The charge may still
92
- * have gone through; the operator must verify with the merchant before any
93
- * retry, because a blind rerun risks a double charge. A 'declined' outcome is
94
- * a definitive negative from the merchant, so it does not warn. Neither does
95
- * 'action-required': a 3-D Secure challenge intercepts BEFORE authorization,
96
- * so no charge can exist until it is completed — the double-charge warning
97
- * would misdirect the operator, and the runner prints the challenge-specific
98
- * notice instead.
99
- */
100
- export function submitClickedWithoutConfirmation(result) {
101
- if (result.outcome === 'confirmed' ||
102
- result.outcome === 'declined' ||
103
- result.outcome === 'action-required') {
104
- return false;
105
- }
106
- return result.evidence.getSteps().some((s) => s.type === 'submit' && s.data.clicked === true);
107
- }
@@ -1,25 +0,0 @@
1
- export type Mandate = {
2
- maxAmountMinor: number;
3
- currency: string;
4
- merchantHost?: string;
5
- expiresAt: string;
6
- };
7
- export type MandateContext = {
8
- merchantHost: string;
9
- amountMinor: number;
10
- currency: string;
11
- now?: Date;
12
- };
13
- export type MandatePreFillContext = {
14
- merchantHost: string;
15
- currency?: string | null;
16
- now?: Date;
17
- };
18
- export type MandateVerdict = {
19
- ok: true;
20
- } | {
21
- ok: false;
22
- reason: string;
23
- };
24
- export declare function checkMandatePreFill(mandate: Mandate | null | undefined, ctx: MandatePreFillContext): MandateVerdict;
25
- export declare function checkMandate(mandate: Mandate | null | undefined, ctx: MandateContext): MandateVerdict;