@visa/cli 4.1.0-rc.108 → 4.1.0-rc.109
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/checkout-engine/cli-engine.d.ts +6 -0
- package/dist/checkout-engine/cli-engine.js +62 -3
- package/dist/checkout-engine/mandate/mandate-ledger.d.ts +21 -0
- package/dist/checkout-engine/mandate/mandate-ledger.js +34 -0
- package/dist/cli.js +143 -143
- package/dist/mcp-server/index.js +147 -147
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -177,6 +177,12 @@ export interface CardMandateRegisterSeam {
|
|
|
177
177
|
}) => Promise<{
|
|
178
178
|
ok: boolean;
|
|
179
179
|
reason?: string;
|
|
180
|
+
/**
|
|
181
|
+
* The ceiling the server committed — `min(requested, the owner's live card
|
|
182
|
+
* grant cap)`. Omitted by an older auth that does not report it, in which
|
|
183
|
+
* case the requested ceiling stands.
|
|
184
|
+
*/
|
|
185
|
+
approvedCeilingMinor?: number;
|
|
180
186
|
}>;
|
|
181
187
|
}
|
|
182
188
|
export type CliEngineDeps = {
|
|
@@ -16,7 +16,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
16
16
|
import { launchCheckoutBrowser } from './browser-launch.js';
|
|
17
17
|
import { prepareCheckout as realPrepareCheckout, submitApprovedCheckout as realSubmitApprovedCheckout, InMemoryPreparedCheckoutStore, } from './executor.js';
|
|
18
18
|
import { claimMandatePickup as realClaimMandatePickup, runHostedApproval as realRunHostedApproval, } from './hosted-approval.js';
|
|
19
|
-
import { VgsAssuranceInstrument, VgsLiveInstrument, decimalToMinor, } from './vgs-live-instrument.js';
|
|
19
|
+
import { VgsAssuranceInstrument, VgsLiveInstrument, decimalToMinor, minorToDecimal, } from './vgs-live-instrument.js';
|
|
20
20
|
import { serverCreateIntent, serverFetchCryptogram, serverPostConfirmation, } from './vgs-gateway/server-mint-client.js';
|
|
21
21
|
import { createCardMandate, DEFAULT_MANDATE_MAX_DRAWS, drawFromMandate, MandateDrawDeclinedError, } from './mandate/card-mandate.js';
|
|
22
22
|
import { MandateLedger } from './mandate/mandate-ledger.js';
|
|
@@ -140,6 +140,21 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
140
140
|
transactionCurrencyCode: input.currency,
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Restate the facts at the ceiling the SERVER approved, when it lowered the
|
|
145
|
+
* one that was requested. Nothing has been spent or reserved on a mandate this
|
|
146
|
+
* new, so its remaining headroom IS its ceiling — see `createCardMandate`,
|
|
147
|
+
* which records `spentMinor: 0` with no reservations.
|
|
148
|
+
*
|
|
149
|
+
* Only ever lowers, mirroring `applyApprovedCeiling`: the server clamps
|
|
150
|
+
* downward, so a higher number means an unexpected response and is ignored
|
|
151
|
+
* rather than reported as headroom no human approved.
|
|
152
|
+
*/
|
|
153
|
+
function approvedCeilingFacts(facts, approvedCeilingMinor) {
|
|
154
|
+
if (approvedCeilingMinor === undefined || approvedCeilingMinor >= facts.ceilingMinor)
|
|
155
|
+
return {};
|
|
156
|
+
return { ceilingMinor: approvedCeilingMinor, remainingMinor: approvedCeilingMinor };
|
|
157
|
+
}
|
|
143
158
|
// Seed the one server-authoritative cumulative store keyed by the VGS intent
|
|
144
159
|
// ID (#5942). On failure the local record is marked register-failed so
|
|
145
160
|
// findCovering() SKIPS it — the mandate exists but is never drawn tap-free —
|
|
@@ -162,8 +177,50 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
162
177
|
ok: false,
|
|
163
178
|
reason: err instanceof Error ? err.message : String(err),
|
|
164
179
|
}));
|
|
165
|
-
if (reg.ok)
|
|
166
|
-
|
|
180
|
+
if (reg.ok) {
|
|
181
|
+
// ONE SPENDING LIMIT: auth just clamped the requested ceiling to the
|
|
182
|
+
// owner's live grant cap and told us what it committed. Adopt it, so the
|
|
183
|
+
// local record — which findCovering() selects on and `mandate list`
|
|
184
|
+
// prints — states the budget the owner actually approved. Best-effort:
|
|
185
|
+
// the mandate is registered and drawable either way, and auth's verdict
|
|
186
|
+
// remains the enforcing cap, so a failed local write must not abort the
|
|
187
|
+
// ceremony. It leaves the record overstating headroom, which is exactly
|
|
188
|
+
// the pre-existing behaviour.
|
|
189
|
+
//
|
|
190
|
+
// If that write fails the returned facts still carry the approved figure —
|
|
191
|
+
// telling the human the truth beats echoing a ceiling their grant refused,
|
|
192
|
+
// and the ledger is left exactly as overstated as it was before this
|
|
193
|
+
// existed. But the two then disagree, so say so out loud rather than let
|
|
194
|
+
// `mandate list` quietly contradict what `mandate start` just printed.
|
|
195
|
+
// Same posture as the mark-failed escalation below.
|
|
196
|
+
if (reg.approvedCeilingMinor !== undefined) {
|
|
197
|
+
const applied = await ledger
|
|
198
|
+
.applyApprovedCeiling(args.mandateId, reg.approvedCeilingMinor)
|
|
199
|
+
.then(() => true)
|
|
200
|
+
.catch(() => false);
|
|
201
|
+
if (!applied) {
|
|
202
|
+
// Rendered through minorToDecimal, never raw /100 — and only when the
|
|
203
|
+
// value is a sane integer, because THIS branch is also where an
|
|
204
|
+
// unusable value lands (applyApprovedCeiling rejects it). A warning
|
|
205
|
+
// must not throw on its way out. The currency is omitted deliberately:
|
|
206
|
+
// both mandate paths refuse anything but USD long before register, so
|
|
207
|
+
// it is known, and passing it would let a non-2-decimal code throw here.
|
|
208
|
+
const approved = Number.isSafeInteger(reg.approvedCeilingMinor) && reg.approvedCeilingMinor > 0
|
|
209
|
+
? ` (${args.currency} ${minorToDecimal(reg.approvedCeilingMinor)})`
|
|
210
|
+
: '';
|
|
211
|
+
process.stderr.write(`warning: your owner's approved limit for this budget${approved} could NOT be saved ` +
|
|
212
|
+
`locally — 'mandate list' will overstate the remaining balance until you re-run ` +
|
|
213
|
+
`'mandate start'. Spending is still capped at the approved limit; a draw over it ` +
|
|
214
|
+
`is refused. mandateId=${args.mandateId}\n`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
registerFailed: false,
|
|
219
|
+
...(reg.approvedCeilingMinor !== undefined
|
|
220
|
+
? { approvedCeilingMinor: reg.approvedCeilingMinor }
|
|
221
|
+
: {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
167
224
|
// Register failed: the server `card_mandate_spend` row was never created, so
|
|
168
225
|
// a delegated draw against this mandate would 404 `no_mandate`. Mark it
|
|
169
226
|
// register-failed so findCovering() SKIPS it and the owner's next checkout
|
|
@@ -293,6 +350,7 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
293
350
|
});
|
|
294
351
|
return {
|
|
295
352
|
...facts,
|
|
353
|
+
...approvedCeilingFacts(facts, registered.approvedCeilingMinor),
|
|
296
354
|
merchantHost: new URL(merchant.url).hostname,
|
|
297
355
|
registerFailed: registered.registerFailed,
|
|
298
356
|
...(registered.registerFailureReason !== undefined
|
|
@@ -353,6 +411,7 @@ export function createCliCheckoutEngine(deps = {}) {
|
|
|
353
411
|
});
|
|
354
412
|
return {
|
|
355
413
|
...facts,
|
|
414
|
+
...approvedCeilingFacts(facts, registered.approvedCeilingMinor),
|
|
356
415
|
merchantHost: new URL(claim.merchant.url).hostname,
|
|
357
416
|
registerFailed: registered.registerFailed,
|
|
358
417
|
...(registered.registerFailureReason !== undefined
|
|
@@ -127,6 +127,27 @@ export declare class MandateLedger {
|
|
|
127
127
|
* Throws only if the mandate is unknown.
|
|
128
128
|
*/
|
|
129
129
|
markRegisterFailed(mandateId: string, now?: Date): Promise<CardMandateRecord>;
|
|
130
|
+
/**
|
|
131
|
+
* ONE SPENDING LIMIT: adopt the ceiling the SERVER actually approved.
|
|
132
|
+
*
|
|
133
|
+
* The requested ceiling is only a request. At register, auth clamps it to the
|
|
134
|
+
* owner's live card-grant cap (`min(requested, grant daily limit)`) and writes
|
|
135
|
+
* that. The local record used to keep the requested figure, so a runtime whose
|
|
136
|
+
* request exceeded the grant reported — and selected against — a budget the
|
|
137
|
+
* owner never approved. The draw still failed closed at auth's verdict, so it
|
|
138
|
+
* was never over-spend; it was the runtime lying about its own headroom and
|
|
139
|
+
* then hitting a decline it could not explain.
|
|
140
|
+
*
|
|
141
|
+
* LOWERS ONLY. A value at or above the current ceiling is ignored, not
|
|
142
|
+
* written: the server clamps downward, so a higher number means an unexpected
|
|
143
|
+
* response, and honouring it would let a client-observed value hand a runtime
|
|
144
|
+
* headroom no human approved. Cap authority stays server-side either way —
|
|
145
|
+
* this only stops the local copy from overstating it.
|
|
146
|
+
*
|
|
147
|
+
* Atomic and idempotent on the same serialized chain as every other mutation.
|
|
148
|
+
* Throws only if the mandate is unknown.
|
|
149
|
+
*/
|
|
150
|
+
applyApprovedCeiling(mandateId: string, approvedCeilingMinor: number): Promise<CardMandateRecord>;
|
|
130
151
|
/**
|
|
131
152
|
* Atomically reserve headroom for a draw. Fail-closed: refuses when the
|
|
132
153
|
* mandate is unknown, expired, or the amount exceeds remaining headroom. The
|
|
@@ -252,6 +252,40 @@ export class MandateLedger {
|
|
|
252
252
|
return record;
|
|
253
253
|
});
|
|
254
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* ONE SPENDING LIMIT: adopt the ceiling the SERVER actually approved.
|
|
257
|
+
*
|
|
258
|
+
* The requested ceiling is only a request. At register, auth clamps it to the
|
|
259
|
+
* owner's live card-grant cap (`min(requested, grant daily limit)`) and writes
|
|
260
|
+
* that. The local record used to keep the requested figure, so a runtime whose
|
|
261
|
+
* request exceeded the grant reported — and selected against — a budget the
|
|
262
|
+
* owner never approved. The draw still failed closed at auth's verdict, so it
|
|
263
|
+
* was never over-spend; it was the runtime lying about its own headroom and
|
|
264
|
+
* then hitting a decline it could not explain.
|
|
265
|
+
*
|
|
266
|
+
* LOWERS ONLY. A value at or above the current ceiling is ignored, not
|
|
267
|
+
* written: the server clamps downward, so a higher number means an unexpected
|
|
268
|
+
* response, and honouring it would let a client-observed value hand a runtime
|
|
269
|
+
* headroom no human approved. Cap authority stays server-side either way —
|
|
270
|
+
* this only stops the local copy from overstating it.
|
|
271
|
+
*
|
|
272
|
+
* Atomic and idempotent on the same serialized chain as every other mutation.
|
|
273
|
+
* Throws only if the mandate is unknown.
|
|
274
|
+
*/
|
|
275
|
+
applyApprovedCeiling(mandateId, approvedCeilingMinor) {
|
|
276
|
+
return this.run(async () => {
|
|
277
|
+
assertPositiveInteger(approvedCeilingMinor, 'approved mandate ceiling');
|
|
278
|
+
const file = await this.load();
|
|
279
|
+
const record = file.mandates.find((m) => m.mandateId === mandateId);
|
|
280
|
+
if (!record)
|
|
281
|
+
throw new Error(`no such mandate ${mandateId}`);
|
|
282
|
+
if (approvedCeilingMinor >= record.ceilingMinor)
|
|
283
|
+
return record;
|
|
284
|
+
record.ceilingMinor = approvedCeilingMinor;
|
|
285
|
+
await this.save(file);
|
|
286
|
+
return record;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
255
289
|
/**
|
|
256
290
|
* Atomically reserve headroom for a draw. Fail-closed: refuses when the
|
|
257
291
|
* mandate is unknown, expired, or the amount exceeds remaining headroom. The
|