@visa/cli 4.1.0-rc.24 → 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.
- package/dist/cli.js +295 -364
- package/dist/mcp-server/index.js +148 -148
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +3 -5
- package/server.json +2 -2
- package/dist/checkout-engine/adapters/generic.d.ts +0 -19
- package/dist/checkout-engine/adapters/generic.js +0 -201
- package/dist/checkout-engine/adapters/index.d.ts +0 -7
- package/dist/checkout-engine/adapters/index.js +0 -17
- package/dist/checkout-engine/adapters/stripe-like.d.ts +0 -10
- package/dist/checkout-engine/adapters/stripe-like.js +0 -21
- package/dist/checkout-engine/browser-launch.d.ts +0 -46
- package/dist/checkout-engine/browser-launch.js +0 -81
- package/dist/checkout-engine/ceremony.d.ts +0 -64
- package/dist/checkout-engine/ceremony.js +0 -261
- package/dist/checkout-engine/cli-engine.d.ts +0 -60
- package/dist/checkout-engine/cli-engine.js +0 -242
- package/dist/checkout-engine/detect.d.ts +0 -61
- package/dist/checkout-engine/detect.js +0 -372
- package/dist/checkout-engine/evidence.d.ts +0 -22
- package/dist/checkout-engine/evidence.js +0 -59
- package/dist/checkout-engine/executor.d.ts +0 -172
- package/dist/checkout-engine/executor.js +0 -1233
- package/dist/checkout-engine/hosted-approval.d.ts +0 -78
- package/dist/checkout-engine/hosted-approval.js +0 -171
- package/dist/checkout-engine/index.d.ts +0 -3
- package/dist/checkout-engine/index.js +0 -5
- package/dist/checkout-engine/inline-target.d.ts +0 -13
- package/dist/checkout-engine/inline-target.js +0 -37
- package/dist/checkout-engine/instrument.d.ts +0 -54
- package/dist/checkout-engine/instrument.js +0 -83
- package/dist/checkout-engine/live-fill-approval.d.ts +0 -52
- package/dist/checkout-engine/live-fill-approval.js +0 -107
- package/dist/checkout-engine/mandate.d.ts +0 -25
- package/dist/checkout-engine/mandate.js +0 -100
- package/dist/checkout-engine/outcome.d.ts +0 -30
- package/dist/checkout-engine/outcome.js +0 -190
- package/dist/checkout-engine/owner-only-file.d.ts +0 -10
- package/dist/checkout-engine/owner-only-file.js +0 -22
- package/dist/checkout-engine/package.json +0 -3
- package/dist/checkout-engine/pay-args.d.ts +0 -14
- package/dist/checkout-engine/pay-args.js +0 -44
- package/dist/checkout-engine/pay.d.ts +0 -1
- package/dist/checkout-engine/pay.js +0 -13
- package/dist/checkout-engine/receipt.d.ts +0 -81
- package/dist/checkout-engine/receipt.js +0 -109
- package/dist/checkout-engine/repo-env.d.ts +0 -11
- package/dist/checkout-engine/repo-env.js +0 -23
- package/dist/checkout-engine/run-live-fill.d.ts +0 -1
- package/dist/checkout-engine/run-live-fill.js +0 -443
- package/dist/checkout-engine/types.d.ts +0 -26
- package/dist/checkout-engine/types.js +0 -2
- package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +0 -74
- package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +0 -248
- package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +0 -49
- package/dist/checkout-engine/vgs-gateway/server-mint-client.js +0 -150
- package/dist/checkout-engine/vgs-live-instrument.d.ts +0 -141
- package/dist/checkout-engine/vgs-live-instrument.js +0 -252
- package/dist/checkout-engine/vic-confirmation.d.ts +0 -34
- package/dist/checkout-engine/vic-confirmation.js +0 -39
- package/dist/skills/pair-visa-agent/RUNTIMES.md +0 -79
- package/dist/skills/pair-visa-agent/SKILL.md +0 -329
- 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;
|