@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.
- package/README.md +178 -231
- package/dist/checkout-engine/adapters/generic.d.ts +23 -0
- package/dist/checkout-engine/adapters/generic.js +216 -0
- package/dist/checkout-engine/adapters/index.d.ts +8 -0
- package/dist/checkout-engine/adapters/index.js +21 -0
- package/dist/checkout-engine/adapters/shopify.d.ts +31 -0
- package/dist/checkout-engine/adapters/shopify.js +423 -0
- package/dist/checkout-engine/adapters/stripe-like.d.ts +10 -0
- package/dist/checkout-engine/adapters/stripe-like.js +21 -0
- package/dist/checkout-engine/amount.d.ts +15 -0
- package/dist/checkout-engine/amount.js +72 -0
- package/dist/checkout-engine/browser-launch.d.ts +46 -0
- package/dist/checkout-engine/browser-launch.js +81 -0
- package/dist/checkout-engine/ceremony.d.ts +64 -0
- package/dist/checkout-engine/ceremony.js +261 -0
- package/dist/checkout-engine/cli-engine.d.ts +227 -0
- package/dist/checkout-engine/cli-engine.js +779 -0
- package/dist/checkout-engine/detect.d.ts +61 -0
- package/dist/checkout-engine/detect.js +398 -0
- package/dist/checkout-engine/evidence.d.ts +25 -0
- package/dist/checkout-engine/evidence.js +104 -0
- package/dist/checkout-engine/executor.d.ts +176 -0
- package/dist/checkout-engine/executor.js +1322 -0
- package/dist/checkout-engine/hosted-approval.d.ts +187 -0
- package/dist/checkout-engine/hosted-approval.js +478 -0
- package/dist/checkout-engine/index.d.ts +6 -0
- package/dist/checkout-engine/index.js +8 -0
- package/dist/checkout-engine/inline-target.d.ts +13 -0
- package/dist/checkout-engine/inline-target.js +37 -0
- package/dist/checkout-engine/instrument.d.ts +61 -0
- package/dist/checkout-engine/instrument.js +87 -0
- package/dist/checkout-engine/live-fill-approval.d.ts +43 -0
- package/dist/checkout-engine/live-fill-approval.js +90 -0
- package/dist/checkout-engine/mandate/card-mandate.d.ts +121 -0
- package/dist/checkout-engine/mandate/card-mandate.js +227 -0
- package/dist/checkout-engine/mandate/mandate-ledger.d.ts +142 -0
- package/dist/checkout-engine/mandate/mandate-ledger.js +338 -0
- package/dist/checkout-engine/mandate.d.ts +25 -0
- package/dist/checkout-engine/mandate.js +100 -0
- package/dist/checkout-engine/outcome.d.ts +30 -0
- package/dist/checkout-engine/outcome.js +225 -0
- package/dist/checkout-engine/owner-only-file.d.ts +19 -0
- package/dist/checkout-engine/owner-only-file.js +41 -0
- package/dist/checkout-engine/package.json +3 -0
- package/dist/checkout-engine/receipt.d.ts +81 -0
- package/dist/checkout-engine/receipt.js +109 -0
- package/dist/checkout-engine/repo-env.d.ts +11 -0
- package/dist/checkout-engine/repo-env.js +23 -0
- package/dist/checkout-engine/trace-handles.d.ts +8 -0
- package/dist/checkout-engine/trace-handles.js +12 -0
- package/dist/checkout-engine/types.d.ts +44 -0
- package/dist/checkout-engine/types.js +2 -0
- package/dist/checkout-engine/vgs-gateway/fetch-credential.d.mts +74 -0
- package/dist/checkout-engine/vgs-gateway/fetch-credential.mjs +248 -0
- package/dist/checkout-engine/vgs-gateway/server-mint-client.d.ts +82 -0
- package/dist/checkout-engine/vgs-gateway/server-mint-client.js +180 -0
- package/dist/checkout-engine/vgs-live-instrument.d.ts +179 -0
- package/dist/checkout-engine/vgs-live-instrument.js +296 -0
- package/dist/checkout-engine/vic-confirmation.d.ts +34 -0
- package/dist/checkout-engine/vic-confirmation.js +39 -0
- package/dist/cli.js +448 -433
- package/dist/mcp-server/index.js +366 -170
- package/dist/skills/pair-visa-agent/RUNTIMES.md +92 -0
- package/dist/skills/pair-visa-agent/SKILL.md +468 -0
- package/dist/skills/pair-visa-agent/scripts/setup.mjs +48 -0
- package/install.ps1 +3 -41
- package/install.sh +3 -35
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +16 -12
- package/server.json +3 -3
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Card mandate (budget) layer — the split the north-star card flow needs:
|
|
2
|
+
//
|
|
3
|
+
// BUDGET = start_card_mandate: approve a CEILING once with the passkey. This
|
|
4
|
+
// mints ONE VGS intent whose decline threshold is the ceiling (not
|
|
5
|
+
// amount+10) and whose quantity permits many draws. The passkey
|
|
6
|
+
// assurance is scoped to the ceiling + merchant, not one charge.
|
|
7
|
+
// DRAW = pay_merchant (tap-free): pull a cryptogram against that intent for
|
|
8
|
+
// an in-budget purchase, WITHOUT a fresh tap, and decrement the
|
|
9
|
+
// cumulative budget in the owner-only ledger.
|
|
10
|
+
//
|
|
11
|
+
// This mirrors x402's start_session -> wallet_pay shape on the card rail.
|
|
12
|
+
//
|
|
13
|
+
// PROVEN (live probe 2026-07-23): VGS honors a ceiling-scoped assurance across
|
|
14
|
+
// multiple sub-amount cryptogram draws against one intent, AND enforces the
|
|
15
|
+
// decline threshold as a CUMULATIVE cap — draws summing to the ceiling are
|
|
16
|
+
// payable, the next draw over the cumulative ceiling is declined. So the total
|
|
17
|
+
// the network authorizes is the ceiling, not maxDraws × ceiling. drawFromMandate
|
|
18
|
+
// stays FAIL-CLEAN as defense-in-depth: the budget is decremented only after a
|
|
19
|
+
// payable cryptogram returns; a network rejection leaves the budget untouched.
|
|
20
|
+
// The caller (cli-engine) then marks the mandate unhonored so the NEXT
|
|
21
|
+
// pay_merchant skips it and takes a fresh per-purchase tap — the same call is
|
|
22
|
+
// not retried in-flight. Never fail-open.
|
|
23
|
+
import { decimalToMinor, minorToDecimal, validateCredential, } from '../vgs-live-instrument.js';
|
|
24
|
+
import { CARD_MANDATE_LEDGER_VERSION, remainingMinor, } from './mandate-ledger.js';
|
|
25
|
+
/**
|
|
26
|
+
* Default number of draws a ceiling intent may fulfil (VGS mandate quantity).
|
|
27
|
+
* This is a purchase-COUNT cap, NOT the exposure bound: VGS enforces the
|
|
28
|
+
* declineThreshold as a CUMULATIVE cap across draws (live-verified 2026-07-23),
|
|
29
|
+
* so the total the network will authorize is the ceiling REGARDLESS of how many
|
|
30
|
+
* draws it is spread across — maxDraws only limits the number of separate
|
|
31
|
+
* purchases, it does not multiply the exposure. Kept SMALL as a sensible default
|
|
32
|
+
* purchase count (the owner-side client ledger also caps cumulative spend at ONE
|
|
33
|
+
* ceiling). This is the single default; callers that don't set maxDraws inherit
|
|
34
|
+
* it (do not re-duplicate it upstream).
|
|
35
|
+
*/
|
|
36
|
+
export const DEFAULT_MANDATE_MAX_DRAWS = 3;
|
|
37
|
+
function assertPositiveInteger(value, label) {
|
|
38
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
39
|
+
throw new Error(`${label} must be a positive integer (minor units)`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function merchantHost(url) {
|
|
43
|
+
try {
|
|
44
|
+
return new URL(url).hostname;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error(`mandate merchant URL is invalid: ${url}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a card mandate: approve a spend CEILING once (the passkey assurance
|
|
52
|
+
* passed here was scoped to the ceiling + merchant), mint one VGS intent with
|
|
53
|
+
* that ceiling as its decline threshold, and record the cumulative-budget
|
|
54
|
+
* ledger entry. Integer minor units only.
|
|
55
|
+
*/
|
|
56
|
+
export async function createCardMandate(input, deps) {
|
|
57
|
+
assertPositiveInteger(input.ceilingMinor, 'mandate ceiling');
|
|
58
|
+
const currencyCode = String(input.currencyCode).toUpperCase();
|
|
59
|
+
if (!/^[A-Z]{3}$/.test(currencyCode)) {
|
|
60
|
+
throw new Error('mandate currency must be a 3-letter ISO 4217 code');
|
|
61
|
+
}
|
|
62
|
+
if (!input.tokenId?.trim())
|
|
63
|
+
throw new Error('mandate requires a tokenId');
|
|
64
|
+
if (!/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/.test(input.agentJkt)) {
|
|
65
|
+
throw new Error('mandate requires a canonical agent request-key JKT');
|
|
66
|
+
}
|
|
67
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
68
|
+
const expiryMs = Date.parse(input.expiresAt);
|
|
69
|
+
if (!Number.isFinite(expiryMs))
|
|
70
|
+
throw new Error('mandate expiresAt must be ISO 8601');
|
|
71
|
+
if (expiryMs <= now.getTime())
|
|
72
|
+
throw new Error('mandate expiresAt is already in the past');
|
|
73
|
+
const host = merchantHost(input.merchant.url);
|
|
74
|
+
const maxDraws = input.maxDraws ?? DEFAULT_MANDATE_MAX_DRAWS;
|
|
75
|
+
if (!Number.isSafeInteger(maxDraws) || maxDraws < 1) {
|
|
76
|
+
throw new Error('mandate maxDraws must be a positive integer');
|
|
77
|
+
}
|
|
78
|
+
// Pass the currency so a non-2-decimal currency is REFUSED here rather than
|
|
79
|
+
// silently rendered with a wrong wire amount (minorToDecimal assumes /100).
|
|
80
|
+
const ceilingDecimal = minorToDecimal(input.ceilingMinor, currencyCode);
|
|
81
|
+
// The passkey prompt must state the REAL grant. VGS enforces the
|
|
82
|
+
// declineThreshold as a CUMULATIVE cap across draws (live-verified 2026-07-23),
|
|
83
|
+
// so the total this single approval can authorize is the CEILING — not
|
|
84
|
+
// maxDraws × ceiling. maxDraws only caps how many separate purchases the intent
|
|
85
|
+
// may fulfil. The prompt therefore names the ceiling as the total and maxDraws
|
|
86
|
+
// as the purchase-count limit (H1: consent must match the true grant).
|
|
87
|
+
// The intent transaction is scoped to the CEILING itself: the decline
|
|
88
|
+
// threshold IS the ceiling (not the historical ceil(amount)+10), and quantity
|
|
89
|
+
// permits multiple draws.
|
|
90
|
+
const ceilingTarget = {
|
|
91
|
+
merchantName: input.merchant.name,
|
|
92
|
+
merchantUrl: input.merchant.url,
|
|
93
|
+
merchantCountryCode: input.merchant.countryCode.toUpperCase(),
|
|
94
|
+
transactionAmount: ceilingDecimal,
|
|
95
|
+
transactionCurrencyCode: currencyCode,
|
|
96
|
+
};
|
|
97
|
+
const { intentId } = await deps.createIntent({
|
|
98
|
+
tokenId: input.tokenId,
|
|
99
|
+
assuranceData: input.assuranceData,
|
|
100
|
+
transaction: ceilingTarget,
|
|
101
|
+
mandate: {
|
|
102
|
+
declineThresholdAmount: ceilingDecimal,
|
|
103
|
+
quantity: maxDraws,
|
|
104
|
+
effectiveUntil: new Date(expiryMs).toISOString(),
|
|
105
|
+
consumerPrompt: `Approve up to ${ceilingDecimal} ${currencyCode} total (across up to ` +
|
|
106
|
+
`${maxDraws} purchase${maxDraws === 1 ? '' : 's'}) at ${input.merchant.name} ` +
|
|
107
|
+
`for my Visa agent`,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
if (!intentId?.trim())
|
|
111
|
+
throw new Error('mandate intent creation returned no intentId');
|
|
112
|
+
const record = {
|
|
113
|
+
version: CARD_MANDATE_LEDGER_VERSION,
|
|
114
|
+
mandateId: intentId,
|
|
115
|
+
agentJkt: input.agentJkt,
|
|
116
|
+
tokenId: input.tokenId,
|
|
117
|
+
ceilingMinor: input.ceilingMinor,
|
|
118
|
+
spentMinor: 0,
|
|
119
|
+
reservations: [],
|
|
120
|
+
currencyCode,
|
|
121
|
+
merchantName: input.merchant.name,
|
|
122
|
+
merchantUrl: input.merchant.url,
|
|
123
|
+
merchantHost: host,
|
|
124
|
+
merchantCountryCode: input.merchant.countryCode.toUpperCase(),
|
|
125
|
+
approvalBaseUrl: deps.approvalBaseUrl,
|
|
126
|
+
...(deps.mintToken ? { mintToken: deps.mintToken } : {}),
|
|
127
|
+
expiresAt: new Date(expiryMs).toISOString(),
|
|
128
|
+
maxDraws,
|
|
129
|
+
createdAt: now.toISOString(),
|
|
130
|
+
draws: [],
|
|
131
|
+
...(input.crossMerchant ? { crossMerchant: true } : {}),
|
|
132
|
+
};
|
|
133
|
+
await deps.ledger.create(record, now);
|
|
134
|
+
return {
|
|
135
|
+
mandateId: record.mandateId,
|
|
136
|
+
ceilingMinor: record.ceilingMinor,
|
|
137
|
+
remainingMinor: remainingMinor(record),
|
|
138
|
+
currencyCode,
|
|
139
|
+
merchant: input.merchant,
|
|
140
|
+
expiresAt: record.expiresAt,
|
|
141
|
+
maxDraws,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const DRAW_REMEDY = 'the card-mandate draw did not complete — a ceiling-scoped assurance may not be honored ' +
|
|
145
|
+
'for this sub-amount draw. The budget is untouched. If this was a transient network error ' +
|
|
146
|
+
'(gateway 5xx / try again), retry — the mandate stays active; a persistent decline disables ' +
|
|
147
|
+
'the mandate and your next pay_merchant uses a fresh per-purchase tap';
|
|
148
|
+
/**
|
|
149
|
+
* Thrown ONLY for the post-reservation network/validation rejection class — the
|
|
150
|
+
* cryptogram fetch failed or returned a non-payable credential (the DRAW_REMEDY
|
|
151
|
+
* path). This is the sole draw failure that should DISABLE the mandate: it means
|
|
152
|
+
* the acquirer would not honor a ceiling-scoped assurance for a sub-amount draw.
|
|
153
|
+
*
|
|
154
|
+
* Pre-network failures — a fail-closed `reserve()` (concurrent over-budget race
|
|
155
|
+
* or expiry) or a `commit()` file-I/O error — throw a plain Error instead, so
|
|
156
|
+
* the caller (cli-engine) can rethrow them WITHOUT marking a HEALTHY mandate
|
|
157
|
+
* unhonored. Discriminating on this type is what keeps a transient race from
|
|
158
|
+
* permanently disabling a good mandate (michaelyang1 M1).
|
|
159
|
+
*/
|
|
160
|
+
export class MandateDrawDeclinedError extends Error {
|
|
161
|
+
cause;
|
|
162
|
+
constructor(message, cause) {
|
|
163
|
+
super(message);
|
|
164
|
+
this.name = 'MandateDrawDeclinedError';
|
|
165
|
+
this.cause = cause;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Draw a cryptogram against an existing card mandate with NO fresh tap. The
|
|
170
|
+
* reservation is atomic and the budget is decremented ONLY after a payable
|
|
171
|
+
* cryptogram returns; any rejection releases the reservation so remaining is
|
|
172
|
+
* unchanged. Validates merchant + currency + amount against the mandate first.
|
|
173
|
+
*/
|
|
174
|
+
export async function drawFromMandate(input, deps) {
|
|
175
|
+
assertPositiveInteger(input.amountMinor, 'draw amount');
|
|
176
|
+
const validate = deps.validate ?? validateCredential;
|
|
177
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
178
|
+
const record = await deps.ledger.get(input.mandateId);
|
|
179
|
+
if (!record)
|
|
180
|
+
throw new Error(`no such mandate ${input.mandateId}`);
|
|
181
|
+
// The wire amount and the accounting amount MUST agree — a mismatch would
|
|
182
|
+
// draw one figure while reserving another. Fail before any reservation.
|
|
183
|
+
const wireMinor = decimalToMinor(input.transaction.transactionAmount);
|
|
184
|
+
if (wireMinor === null || wireMinor !== input.amountMinor) {
|
|
185
|
+
throw new Error(`draw amount ${input.amountMinor} disagrees with transaction amount ` +
|
|
186
|
+
`${JSON.stringify(input.transaction.transactionAmount)} (minor units)`);
|
|
187
|
+
}
|
|
188
|
+
// Currency must always match; merchant must match UNLESS this is a budget
|
|
189
|
+
// (crossMerchant) mandate the owner approved for eligible retail merchants. The network
|
|
190
|
+
// honors a ceiling-scoped cryptogram cross-merchant, so a budget mandate is
|
|
191
|
+
// deliberately not host-restricted — the ceiling + per-transaction limit still
|
|
192
|
+
// bound it.
|
|
193
|
+
const targetHost = merchantHost(input.transaction.merchantUrl);
|
|
194
|
+
if (!record.crossMerchant && targetHost !== record.merchantHost) {
|
|
195
|
+
throw new Error(`draw merchant ${targetHost} does not match mandate merchant ${record.merchantHost}`);
|
|
196
|
+
}
|
|
197
|
+
if (String(input.transaction.transactionCurrencyCode).toUpperCase() !==
|
|
198
|
+
record.currencyCode.toUpperCase()) {
|
|
199
|
+
throw new Error(`draw currency ${input.transaction.transactionCurrencyCode} does not match mandate ` +
|
|
200
|
+
`currency ${record.currencyCode}`);
|
|
201
|
+
}
|
|
202
|
+
// Atomic reserve — throws (fail-closed) if expired or over remaining budget.
|
|
203
|
+
const reservationId = await deps.ledger.reserve(input.mandateId, input.amountMinor, now);
|
|
204
|
+
let payment;
|
|
205
|
+
try {
|
|
206
|
+
payment = await deps.fetchCryptogram({
|
|
207
|
+
tokenId: record.tokenId,
|
|
208
|
+
intentId: record.mandateId,
|
|
209
|
+
transaction: input.transaction,
|
|
210
|
+
});
|
|
211
|
+
// A 2xx that is not actually a payable DAVV must NOT decrement the budget.
|
|
212
|
+
validate(payment);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
await deps.ledger.release(input.mandateId, reservationId, now);
|
|
216
|
+
// Typed so the caller disables the mandate ONLY for this post-reservation
|
|
217
|
+
// network/validation decline — never for a pre-network reserve()/commit()
|
|
218
|
+
// failure, which throws a plain Error above/below this try.
|
|
219
|
+
throw new MandateDrawDeclinedError(`${DRAW_REMEDY}: ${err.message}`, err);
|
|
220
|
+
}
|
|
221
|
+
const committed = await deps.ledger.commit(input.mandateId, reservationId, { intentId: record.mandateId }, now);
|
|
222
|
+
return {
|
|
223
|
+
payment,
|
|
224
|
+
intentId: record.mandateId,
|
|
225
|
+
remainingMinor: remainingMinor(committed),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export declare const CARD_MANDATE_LEDGER_VERSION: 1;
|
|
2
|
+
export type CardMandateReservation = {
|
|
3
|
+
reservationId: string;
|
|
4
|
+
amountMinor: number;
|
|
5
|
+
reservedAt: string;
|
|
6
|
+
};
|
|
7
|
+
export type CardMandateDraw = {
|
|
8
|
+
drawId: string;
|
|
9
|
+
reservationId: string;
|
|
10
|
+
amountMinor: number;
|
|
11
|
+
intentId: string;
|
|
12
|
+
status: 'committed' | 'released';
|
|
13
|
+
at: string;
|
|
14
|
+
};
|
|
15
|
+
export type CardMandateRecord = {
|
|
16
|
+
version: typeof CARD_MANDATE_LEDGER_VERSION;
|
|
17
|
+
/** == the VGS intentId the ceiling approval created. */
|
|
18
|
+
mandateId: string;
|
|
19
|
+
/** Exact runtime request key this budget was issued to. Absent on legacy rows. */
|
|
20
|
+
agentJkt?: string;
|
|
21
|
+
tokenId: string;
|
|
22
|
+
ceilingMinor: number;
|
|
23
|
+
/** Permanently committed (drawn) spend, integer minor units. */
|
|
24
|
+
spentMinor: number;
|
|
25
|
+
/** In-flight reservations (reserved but not yet committed/released). */
|
|
26
|
+
reservations: CardMandateReservation[];
|
|
27
|
+
currencyCode: string;
|
|
28
|
+
merchantName: string;
|
|
29
|
+
merchantUrl: string;
|
|
30
|
+
merchantHost: string;
|
|
31
|
+
merchantCountryCode: string;
|
|
32
|
+
/** verify-web origin the ceiling intent was minted against. */
|
|
33
|
+
approvalBaseUrl: string;
|
|
34
|
+
/**
|
|
35
|
+
* Scoped token released by the ceiling approval. It bootstraps the one intent
|
|
36
|
+
* + register ceremony only. Each cryptogram AND its outcome confirmation use
|
|
37
|
+
* the exact per-draw verdict instead. Owner-only (0600).
|
|
38
|
+
*/
|
|
39
|
+
mintToken?: string;
|
|
40
|
+
expiresAt: string;
|
|
41
|
+
/** Network purchase-count cap disclosed at approval. Absent on legacy rows. */
|
|
42
|
+
maxDraws?: number;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
draws: CardMandateDraw[];
|
|
45
|
+
/**
|
|
46
|
+
* Set when the network DECLINED a tap-free draw against this mandate (a
|
|
47
|
+
* ceiling-scoped assurance the acquirer would not honor for a sub-amount
|
|
48
|
+
* draw). Distinct from a reservation `status`: it disables the whole mandate.
|
|
49
|
+
* Once set, findCovering() SKIPS this mandate so the caller's next
|
|
50
|
+
* pay_merchant falls through to a fresh per-purchase tap instead of
|
|
51
|
+
* re-selecting the same failing mandate forever. ISO 8601.
|
|
52
|
+
*/
|
|
53
|
+
unhonoredAt?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Set when the delegated-draw register handshake FAILED at mandate-start (the
|
|
56
|
+
* server-side `card_mandate_spend` row was never created), so a later
|
|
57
|
+
* delegated (verdict-signed) draw would 404 `no_mandate`. Like `unhonoredAt`,
|
|
58
|
+
* findCovering() SKIPS a register-failed mandate so the next checkout falls
|
|
59
|
+
* through to a fresh per-purchase tap instead of surfacing a confusing
|
|
60
|
+
* `no_mandate`. Mandate-start requires delegated card authority, so every
|
|
61
|
+
* created record attempted registration. ISO 8601.
|
|
62
|
+
*/
|
|
63
|
+
registerFailedAt?: string;
|
|
64
|
+
/**
|
|
65
|
+
* A cross-host retail budget, not scoped to one `merchantHost`. The local
|
|
66
|
+
* selector may attempt it at any host, while the provider/network still
|
|
67
|
+
* applies the approved Retail/5999 category plus amount/count/time controls.
|
|
68
|
+
* `crossMerchant` is retained as the version-1 persisted field name.
|
|
69
|
+
*/
|
|
70
|
+
crossMerchant?: boolean;
|
|
71
|
+
};
|
|
72
|
+
export type CardMandateLedgerFile = {
|
|
73
|
+
version: typeof CARD_MANDATE_LEDGER_VERSION;
|
|
74
|
+
mandates: CardMandateRecord[];
|
|
75
|
+
};
|
|
76
|
+
export type CoverQuery = {
|
|
77
|
+
merchantHost: string;
|
|
78
|
+
currencyCode: string;
|
|
79
|
+
amountMinor: number;
|
|
80
|
+
now?: Date;
|
|
81
|
+
};
|
|
82
|
+
/** Available headroom = ceiling - committed - reserved. Integer minor units. */
|
|
83
|
+
export declare function remainingMinor(record: CardMandateRecord): number;
|
|
84
|
+
/**
|
|
85
|
+
* The persisted card-mandate ledger. All mutating operations are serialized on
|
|
86
|
+
* an in-process promise chain so two concurrent draws cannot both observe the
|
|
87
|
+
* same remaining balance (the read-modify-write is atomic within the process).
|
|
88
|
+
*/
|
|
89
|
+
export declare class MandateLedger {
|
|
90
|
+
private readonly path;
|
|
91
|
+
private chain;
|
|
92
|
+
constructor(path?: string);
|
|
93
|
+
/** Serialize a read-modify-write so concurrent draws can't race the file. */
|
|
94
|
+
private run;
|
|
95
|
+
private load;
|
|
96
|
+
private save;
|
|
97
|
+
/**
|
|
98
|
+
* Append a freshly-created mandate record. `now` drives the expired-mandate
|
|
99
|
+
* prune below; it defaults to the wall clock but is injectable so callers (and
|
|
100
|
+
* tests) can pin it — every other mutating method takes the same clock, and a
|
|
101
|
+
* real `new Date()` here would otherwise make the prune non-deterministic
|
|
102
|
+
* against fixtures dated relative to a fixed reference time.
|
|
103
|
+
*/
|
|
104
|
+
create(record: CardMandateRecord, now?: Date): Promise<CardMandateRecord>;
|
|
105
|
+
/** Read a single mandate (no lock — a snapshot copy). */
|
|
106
|
+
get(mandateId: string): Promise<CardMandateRecord | null>;
|
|
107
|
+
/**
|
|
108
|
+
* First ACTIVE mandate (not expired) whose merchant + currency match and whose
|
|
109
|
+
* remaining headroom covers amountMinor. Used by pay_merchant to decide the
|
|
110
|
+
* tap-free draw path vs a fresh per-purchase tap.
|
|
111
|
+
*/
|
|
112
|
+
findCovering(query: CoverQuery): Promise<CardMandateRecord | null>;
|
|
113
|
+
/**
|
|
114
|
+
* Mark a mandate as unhonored — the network declined a ceiling-scoped draw
|
|
115
|
+
* against it, so it must never be selected again. Atomic, owner-only write on
|
|
116
|
+
* the same serialized chain as every other mutation. Idempotent: a second
|
|
117
|
+
* call keeps the first timestamp. Throws only if the mandate is unknown.
|
|
118
|
+
*/
|
|
119
|
+
markUnhonored(mandateId: string, now?: Date): Promise<CardMandateRecord>;
|
|
120
|
+
/**
|
|
121
|
+
* Mark a mandate as register-failed — the delegated-draw register handshake
|
|
122
|
+
* did not create the server row at mandate-start, so it must never be selected
|
|
123
|
+
* for a (delegated) draw. Atomic, owner-only write on the same serialized chain
|
|
124
|
+
* as every other mutation. Idempotent: a second call keeps the first timestamp.
|
|
125
|
+
* Throws only if the mandate is unknown.
|
|
126
|
+
*/
|
|
127
|
+
markRegisterFailed(mandateId: string, now?: Date): Promise<CardMandateRecord>;
|
|
128
|
+
/**
|
|
129
|
+
* Atomically reserve headroom for a draw. Fail-closed: refuses when the
|
|
130
|
+
* mandate is unknown, expired, or the amount exceeds remaining headroom. The
|
|
131
|
+
* reservation counts against availability immediately, closing the window in
|
|
132
|
+
* which two concurrent draws both see the same remaining balance.
|
|
133
|
+
*/
|
|
134
|
+
reserve(mandateId: string, amountMinor: number, now?: Date): Promise<string>;
|
|
135
|
+
/** Commit a reservation to permanent spend and record the payable draw. */
|
|
136
|
+
commit(mandateId: string, reservationId: string, meta: {
|
|
137
|
+
intentId: string;
|
|
138
|
+
}, now?: Date): Promise<CardMandateRecord>;
|
|
139
|
+
/** Release a reservation back to availability (draw failed / not payable). */
|
|
140
|
+
release(mandateId: string, reservationId: string, now?: Date): Promise<CardMandateRecord>;
|
|
141
|
+
}
|
|
142
|
+
export declare function defaultLedgerPath(): string;
|