@visa/cli 4.1.0-rc.9 → 4.1.0-rc.91

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 (70) hide show
  1. package/README.md +178 -231
  2. package/dist/checkout-engine/adapters/generic.d.ts +23 -0
  3. package/dist/checkout-engine/adapters/generic.js +216 -0
  4. package/dist/checkout-engine/adapters/index.d.ts +8 -0
  5. package/dist/checkout-engine/adapters/index.js +21 -0
  6. package/dist/checkout-engine/adapters/shopify.d.ts +31 -0
  7. package/dist/checkout-engine/adapters/shopify.js +423 -0
  8. package/dist/checkout-engine/adapters/stripe-like.d.ts +10 -0
  9. package/dist/checkout-engine/adapters/stripe-like.js +21 -0
  10. package/dist/checkout-engine/amount.d.ts +15 -0
  11. package/dist/checkout-engine/amount.js +72 -0
  12. package/dist/checkout-engine/browser-launch.d.ts +46 -0
  13. package/dist/checkout-engine/browser-launch.js +81 -0
  14. package/dist/checkout-engine/ceremony.d.ts +64 -0
  15. package/dist/checkout-engine/ceremony.js +261 -0
  16. package/dist/checkout-engine/cli-engine.d.ts +227 -0
  17. package/dist/checkout-engine/cli-engine.js +779 -0
  18. package/dist/checkout-engine/detect.d.ts +61 -0
  19. package/dist/checkout-engine/detect.js +398 -0
  20. package/dist/checkout-engine/evidence.d.ts +25 -0
  21. package/dist/checkout-engine/evidence.js +104 -0
  22. package/dist/checkout-engine/executor.d.ts +176 -0
  23. package/dist/checkout-engine/executor.js +1322 -0
  24. package/dist/checkout-engine/hosted-approval.d.ts +187 -0
  25. package/dist/checkout-engine/hosted-approval.js +478 -0
  26. package/dist/checkout-engine/index.d.ts +6 -0
  27. package/dist/checkout-engine/index.js +8 -0
  28. package/dist/checkout-engine/inline-target.d.ts +13 -0
  29. package/dist/checkout-engine/inline-target.js +37 -0
  30. package/dist/checkout-engine/instrument.d.ts +61 -0
  31. package/dist/checkout-engine/instrument.js +87 -0
  32. package/dist/checkout-engine/live-fill-approval.d.ts +43 -0
  33. package/dist/checkout-engine/live-fill-approval.js +90 -0
  34. package/dist/checkout-engine/mandate/card-mandate.d.ts +121 -0
  35. package/dist/checkout-engine/mandate/card-mandate.js +227 -0
  36. package/dist/checkout-engine/mandate/mandate-ledger.d.ts +142 -0
  37. package/dist/checkout-engine/mandate/mandate-ledger.js +338 -0
  38. package/dist/checkout-engine/mandate.d.ts +25 -0
  39. package/dist/checkout-engine/mandate.js +100 -0
  40. package/dist/checkout-engine/outcome.d.ts +30 -0
  41. package/dist/checkout-engine/outcome.js +225 -0
  42. package/dist/checkout-engine/owner-only-file.d.ts +19 -0
  43. package/dist/checkout-engine/owner-only-file.js +41 -0
  44. package/dist/checkout-engine/package.json +3 -0
  45. package/dist/checkout-engine/receipt.d.ts +81 -0
  46. package/dist/checkout-engine/receipt.js +109 -0
  47. package/dist/checkout-engine/repo-env.d.ts +11 -0
  48. package/dist/checkout-engine/repo-env.js +23 -0
  49. package/dist/checkout-engine/trace-handles.d.ts +8 -0
  50. package/dist/checkout-engine/trace-handles.js +12 -0
  51. package/dist/checkout-engine/types.d.ts +44 -0
  52. package/dist/checkout-engine/types.js +2 -0
  53. package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +74 -0
  54. package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +248 -0
  55. package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +82 -0
  56. package/dist/checkout-engine/vgs-gateway/server-mint-client.js +180 -0
  57. package/dist/checkout-engine/vgs-live-instrument.d.ts +179 -0
  58. package/dist/checkout-engine/vgs-live-instrument.js +296 -0
  59. package/dist/checkout-engine/vic-confirmation.d.ts +34 -0
  60. package/dist/checkout-engine/vic-confirmation.js +39 -0
  61. package/dist/cli.js +448 -433
  62. package/dist/mcp-server/index.js +366 -170
  63. package/dist/skills/pair-visa-agent/RUNTIMES.md +92 -0
  64. package/dist/skills/pair-visa-agent/SKILL.md +468 -0
  65. package/dist/skills/pair-visa-agent/scripts/setup.mjs +48 -0
  66. package/install.ps1 +3 -41
  67. package/install.sh +3 -35
  68. package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
  69. package/package.json +16 -12
  70. package/server.json +3 -3
@@ -0,0 +1,6 @@
1
+ export { createCliCheckoutEngine, type CliReviewInput, type CliReviewFacts, type CliPayInput, type CliReceiptFacts, type CliStartMandateInput, type CliMandateFacts, type CliEngineDeps, } from './cli-engine.js';
2
+ export { prepareCheckout, submitApprovedCheckout, runCheckout, InMemoryPreparedCheckoutStore, } from './executor.js';
3
+ export type { CheckoutResult, CheckoutReview, CheckoutOutcome } from './executor.js';
4
+ export { HostedApprovalDeclinedError, sanitizeApprovalIntent, APPROVAL_INTENT_MAX_CHARS, } from './hosted-approval.js';
5
+ export { createCardMandate, drawFromMandate, MandateDrawDeclinedError, DEFAULT_MANDATE_MAX_DRAWS, type CreateCardMandateInput, type CreateCardMandateDeps, type CardMandateFacts, type DrawFromMandateInput, type DrawFromMandateDeps, type DrawResult, type CardMandateMerchant, } from './mandate/card-mandate.js';
6
+ export { MandateLedger, remainingMinor, defaultLedgerPath, CARD_MANDATE_LEDGER_VERSION, type CardMandateRecord, type CardMandateLedgerFile, type CardMandateDraw, type CardMandateReservation, } from './mandate/mandate-ledger.js';
@@ -0,0 +1,8 @@
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';
6
+ export { HostedApprovalDeclinedError, sanitizeApprovalIntent, APPROVAL_INTENT_MAX_CHARS, } from './hosted-approval.js';
7
+ export { createCardMandate, drawFromMandate, MandateDrawDeclinedError, DEFAULT_MANDATE_MAX_DRAWS, } from './mandate/card-mandate.js';
8
+ export { MandateLedger, remainingMinor, defaultLedgerPath, CARD_MANDATE_LEDGER_VERSION, } from './mandate/mandate-ledger.js';
@@ -0,0 +1,13 @@
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;
@@ -0,0 +1,37 @@
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
+ }
@@ -0,0 +1,61 @@
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
+ * Non-sensitive, bounded request-correlation handles returned by the mint
11
+ * route. These are evidence pointers, not card credential material.
12
+ */
13
+ vgsTraceId?: string;
14
+ networkCorrelationId?: string;
15
+ };
16
+ export type InstrumentContext = {
17
+ merchantHost: string;
18
+ amountMinor: number;
19
+ currency: string;
20
+ };
21
+ export type InstrumentKind = 'test-card' | 'vgs-alias' | 'agentic-token';
22
+ export interface Instrument {
23
+ kind: InstrumentKind;
24
+ getCredential(ctx: InstrumentContext): Promise<CardCredential>;
25
+ }
26
+ export declare const OFFICIAL_TEST_PANS: {
27
+ readonly visa: "4242424242424242";
28
+ readonly visaAlt: "4111111111111111";
29
+ readonly visaDecline: "4000000000000002";
30
+ readonly visaSlowConfirm: "4000000000000069";
31
+ readonly visaUnknownOutcome: "4000000000000044";
32
+ readonly visaChallenge: "4000000000000010";
33
+ readonly visaEmailOtp: "4000000000000077";
34
+ };
35
+ export type TestCardOptions = {
36
+ pan?: string;
37
+ cardholderName?: string;
38
+ cvc?: string;
39
+ expMonth?: number;
40
+ expYear?: number;
41
+ };
42
+ export declare class TestCardInstrument implements Instrument {
43
+ readonly kind: "test-card";
44
+ private readonly credential;
45
+ constructor(opts?: TestCardOptions);
46
+ getCredential(_ctx: InstrumentContext): Promise<CardCredential>;
47
+ }
48
+ export declare class VgsAliasInstrument implements Instrument {
49
+ private readonly _alias;
50
+ readonly kind: "vgs-alias";
51
+ constructor(_alias: string);
52
+ getCredential(_ctx: InstrumentContext): Promise<CardCredential>;
53
+ }
54
+ export declare class AgenticTokenInstrument implements Instrument {
55
+ private readonly tokenRef;
56
+ private readonly mintCredential?;
57
+ readonly kind: "agentic-token";
58
+ private used;
59
+ constructor(tokenRef: string, mintCredential?: ((ctx: InstrumentContext) => Promise<CardCredential>) | undefined);
60
+ getCredential(ctx: InstrumentContext): Promise<CardCredential>;
61
+ }
@@ -0,0 +1,87 @@
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
+ // Ending 0077: the fixture acquirer emails a one-time code to the agent
26
+ // inbox and shows a 'check your email for a code' page with a one-time-code
27
+ // field — exercises the executor's agent-resolvable email-OTP subroutine.
28
+ visaEmailOtp: '4000000000000077',
29
+ };
30
+ function futureExpiry() {
31
+ const now = new Date();
32
+ return { expMonth: 12, expYear: now.getFullYear() + 3 };
33
+ }
34
+ export class TestCardInstrument {
35
+ kind = 'test-card';
36
+ credential;
37
+ constructor(opts = {}) {
38
+ const exp = futureExpiry();
39
+ this.credential = {
40
+ pan: opts.pan ?? OFFICIAL_TEST_PANS.visa,
41
+ expMonth: opts.expMonth ?? exp.expMonth,
42
+ expYear: opts.expYear ?? exp.expYear,
43
+ cvc: opts.cvc ?? '123',
44
+ cardholderName: opts.cardholderName ?? 'Test Agent',
45
+ };
46
+ }
47
+ async getCredential(_ctx) {
48
+ return this.credential;
49
+ }
50
+ }
51
+ const NOT_ENABLED = 'not enabled: pending VGS alias reveal support';
52
+ // Stub. Once VGS enablement lands, this resolves a network token / alias into a
53
+ // fillable credential (a virtual PAN plus its verification value).
54
+ export class VgsAliasInstrument {
55
+ _alias;
56
+ kind = 'vgs-alias';
57
+ constructor(_alias) {
58
+ this._alias = _alias;
59
+ }
60
+ async getCredential(_ctx) {
61
+ throw new Error(`${NOT_ENABLED}. VgsAliasInstrument cannot resolve alias "${this._alias}" ` +
62
+ 'to a fillable PAN until VGS exposes the reveal seam for this spike.');
63
+ }
64
+ }
65
+ // A server-side provider mints the transaction-bound credential whose pan
66
+ // carries the DPAN and whose cvc carries the short DAVV. Omitting the provider
67
+ // preserves a clear fail-closed result for callers that have only a token ref.
68
+ export class AgenticTokenInstrument {
69
+ tokenRef;
70
+ mintCredential;
71
+ kind = 'agentic-token';
72
+ used = false;
73
+ constructor(tokenRef, mintCredential) {
74
+ this.tokenRef = tokenRef;
75
+ this.mintCredential = mintCredential;
76
+ }
77
+ async getCredential(ctx) {
78
+ if (!this.mintCredential) {
79
+ throw new Error('not enabled: no agentic credential provider configured. ' +
80
+ `Token ${this.tokenRef} needs a server-side VGS credential provider.`);
81
+ }
82
+ if (this.used)
83
+ throw new Error('agentic credential instrument is single-use');
84
+ this.used = true;
85
+ return this.mintCredential(ctx);
86
+ }
87
+ }
@@ -0,0 +1,43 @@
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
+ /** Distinct phrases per mode so dry-run muscle memory can never authorize a payment. */
20
+ export declare function approvalPhrase(mode: CheckoutMode, reviewId: string): string;
21
+ /** Integer-only minor→display formatting (two-decimal currencies, the same assumption as decimalToMinor). */
22
+ export declare function formatAmountMinor(amountMinor: number, currency: string): string;
23
+ /** The operator question. In submit mode it names the exact charge — amount and merchant. */
24
+ export declare function approvalQuestion(mode: CheckoutMode, review: {
25
+ merchantHost: string;
26
+ amountMinor: number;
27
+ currency: string;
28
+ }, phrase: string): string;
29
+ /** The one outcome each mode may exit 0 with. */
30
+ export declare function isRunSuccess(mode: CheckoutMode, outcome: CheckoutOutcome): boolean;
31
+ /**
32
+ * True when the pay control was actually clicked but the run ended with an
33
+ * UNKNOWN outcome — neither 'confirmed' nor 'declined' (e.g. a slow redirect
34
+ * or confirmation copy the heuristics don't recognize). The charge may still
35
+ * have gone through; the operator must verify with the merchant before any
36
+ * retry, because a blind rerun risks a double charge. A 'declined' outcome is
37
+ * a definitive negative from the merchant, so it does not warn. Neither does
38
+ * 'action-required': a 3-D Secure challenge intercepts BEFORE authorization,
39
+ * so no charge can exist until it is completed — the double-charge warning
40
+ * would misdirect the operator, and the runner prints the challenge-specific
41
+ * notice instead.
42
+ */
43
+ export declare function submitClickedWithoutConfirmation(result: CheckoutResult): boolean;
@@ -0,0 +1,90 @@
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
+ /** Distinct phrases per mode so dry-run muscle memory can never authorize a payment. */
50
+ export function approvalPhrase(mode, reviewId) {
51
+ return `${mode === 'submit' ? 'PAY' : 'CHECK'} ${reviewId}`;
52
+ }
53
+ /** Integer-only minor→display formatting (two-decimal currencies, the same assumption as decimalToMinor). */
54
+ export function formatAmountMinor(amountMinor, currency) {
55
+ return `${Math.floor(amountMinor / 100)}.${String(amountMinor % 100).padStart(2, '0')} ${currency}`;
56
+ }
57
+ /** The operator question. In submit mode it names the exact charge — amount and merchant. */
58
+ export function approvalQuestion(mode, review, phrase) {
59
+ if (mode === 'submit') {
60
+ return (`Inspect the visible checkout. This WILL charge ` +
61
+ `${formatAmountMinor(review.amountMinor, review.currency)} at ${review.merchantHost}. ` +
62
+ `Type ${JSON.stringify(phrase)} to disclose the credential AND submit the order: `);
63
+ }
64
+ return (`Inspect the visible checkout. Type ${JSON.stringify(phrase)} ` +
65
+ `to validate the reviewed page without minting, filling, or submitting a credential: `);
66
+ }
67
+ /** The one outcome each mode may exit 0 with. */
68
+ export function isRunSuccess(mode, outcome) {
69
+ return outcome === (mode === 'submit' ? 'confirmed' : 'reviewed-dry-run');
70
+ }
71
+ /**
72
+ * True when the pay control was actually clicked but the run ended with an
73
+ * UNKNOWN outcome — neither 'confirmed' nor 'declined' (e.g. a slow redirect
74
+ * or confirmation copy the heuristics don't recognize). The charge may still
75
+ * have gone through; the operator must verify with the merchant before any
76
+ * retry, because a blind rerun risks a double charge. A 'declined' outcome is
77
+ * a definitive negative from the merchant, so it does not warn. Neither does
78
+ * 'action-required': a 3-D Secure challenge intercepts BEFORE authorization,
79
+ * so no charge can exist until it is completed — the double-charge warning
80
+ * would misdirect the operator, and the runner prints the challenge-specific
81
+ * notice instead.
82
+ */
83
+ export function submitClickedWithoutConfirmation(result) {
84
+ if (result.outcome === 'confirmed' ||
85
+ result.outcome === 'declined' ||
86
+ result.outcome === 'action-required') {
87
+ return false;
88
+ }
89
+ return result.evidence.getSteps().some((s) => s.type === 'submit' && s.data.clicked === true);
90
+ }
@@ -0,0 +1,121 @@
1
+ import { type VgsCheckoutTarget, type VgsPaymentCredential } from '../vgs-live-instrument.js';
2
+ import { MandateLedger } from './mandate-ledger.js';
3
+ /**
4
+ * Default number of draws a ceiling intent may fulfil (VGS mandate quantity).
5
+ * This is a purchase-COUNT cap, NOT the exposure bound: VGS enforces the
6
+ * declineThreshold as a CUMULATIVE cap across draws (live-verified 2026-07-23),
7
+ * so the total the network will authorize is the ceiling REGARDLESS of how many
8
+ * draws it is spread across — maxDraws only limits the number of separate
9
+ * purchases, it does not multiply the exposure. Kept SMALL as a sensible default
10
+ * purchase count (the owner-side client ledger also caps cumulative spend at ONE
11
+ * ceiling). This is the single default; callers that don't set maxDraws inherit
12
+ * it (do not re-duplicate it upstream).
13
+ */
14
+ export declare const DEFAULT_MANDATE_MAX_DRAWS = 3;
15
+ export type CardMandateMerchant = {
16
+ name: string;
17
+ url: string;
18
+ countryCode: string;
19
+ };
20
+ export type CreateCardMandateInput = {
21
+ /** Exact current runtime request key selected before human approval. */
22
+ agentJkt: string;
23
+ tokenId: string;
24
+ assuranceData: unknown;
25
+ ceilingMinor: number;
26
+ merchant: CardMandateMerchant;
27
+ currencyCode: string;
28
+ expiresAt: string;
29
+ /** Max draws the ceiling intent may fulfil; defaults to DEFAULT_MANDATE_MAX_DRAWS. */
30
+ maxDraws?: number;
31
+ /** Cross-host retail budget; network category eligibility still applies. */
32
+ crossMerchant?: boolean;
33
+ };
34
+ export type CreateIntentFn = (input: {
35
+ tokenId: string;
36
+ assuranceData: unknown;
37
+ transaction: VgsCheckoutTarget;
38
+ mandate: {
39
+ declineThresholdAmount: string;
40
+ quantity: number;
41
+ effectiveUntil: string;
42
+ consumerPrompt: string;
43
+ };
44
+ }) => Promise<{
45
+ intentId: string;
46
+ status: string | null;
47
+ }>;
48
+ export type CreateCardMandateDeps = {
49
+ /** Mint the ceiling intent — wire serverCreateIntent(base, mintToken, …). */
50
+ createIntent: CreateIntentFn;
51
+ ledger: MandateLedger;
52
+ /** verify-web origin the intent is minted against (persisted for later draws). */
53
+ approvalBaseUrl: string;
54
+ /** Scoped mint token to persist for tap-free draws. */
55
+ mintToken?: string;
56
+ now?: () => Date;
57
+ };
58
+ export type CardMandateFacts = {
59
+ mandateId: string;
60
+ ceilingMinor: number;
61
+ remainingMinor: number;
62
+ currencyCode: string;
63
+ merchant: CardMandateMerchant;
64
+ expiresAt: string;
65
+ /** Network-enforced maximum purchase count disclosed at approval. */
66
+ maxDraws: number;
67
+ };
68
+ /**
69
+ * Create a card mandate: approve a spend CEILING once (the passkey assurance
70
+ * passed here was scoped to the ceiling + merchant), mint one VGS intent with
71
+ * that ceiling as its decline threshold, and record the cumulative-budget
72
+ * ledger entry. Integer minor units only.
73
+ */
74
+ export declare function createCardMandate(input: CreateCardMandateInput, deps: CreateCardMandateDeps): Promise<CardMandateFacts>;
75
+ export type FetchCryptogramFn = (input: {
76
+ tokenId: string;
77
+ intentId: string;
78
+ transaction: VgsCheckoutTarget;
79
+ }) => Promise<VgsPaymentCredential>;
80
+ export type DrawFromMandateInput = {
81
+ mandateId: string;
82
+ amountMinor: number;
83
+ /** The exact per-draw checkout target (merchant + amount + currency). */
84
+ transaction: VgsCheckoutTarget;
85
+ };
86
+ export type DrawFromMandateDeps = {
87
+ /** Draw a cryptogram against the mandate intent — wire serverFetchCryptogram. */
88
+ fetchCryptogram: FetchCryptogramFn;
89
+ ledger: MandateLedger;
90
+ /** Override the payable-credential gate (default validateCredential). */
91
+ validate?: (credential: VgsPaymentCredential) => void;
92
+ now?: () => Date;
93
+ };
94
+ export type DrawResult = {
95
+ payment: VgsPaymentCredential;
96
+ intentId: string;
97
+ remainingMinor: number;
98
+ };
99
+ /**
100
+ * Thrown ONLY for the post-reservation network/validation rejection class — the
101
+ * cryptogram fetch failed or returned a non-payable credential (the DRAW_REMEDY
102
+ * path). This is the sole draw failure that should DISABLE the mandate: it means
103
+ * the acquirer would not honor a ceiling-scoped assurance for a sub-amount draw.
104
+ *
105
+ * Pre-network failures — a fail-closed `reserve()` (concurrent over-budget race
106
+ * or expiry) or a `commit()` file-I/O error — throw a plain Error instead, so
107
+ * the caller (cli-engine) can rethrow them WITHOUT marking a HEALTHY mandate
108
+ * unhonored. Discriminating on this type is what keeps a transient race from
109
+ * permanently disabling a good mandate (michaelyang1 M1).
110
+ */
111
+ export declare class MandateDrawDeclinedError extends Error {
112
+ readonly cause?: unknown;
113
+ constructor(message: string, cause?: unknown);
114
+ }
115
+ /**
116
+ * Draw a cryptogram against an existing card mandate with NO fresh tap. The
117
+ * reservation is atomic and the budget is decremented ONLY after a payable
118
+ * cryptogram returns; any rejection releases the reservation so remaining is
119
+ * unchanged. Validates merchant + currency + amount against the mandate first.
120
+ */
121
+ export declare function drawFromMandate(input: DrawFromMandateInput, deps: DrawFromMandateDeps): Promise<DrawResult>;