@visa/cli 4.1.0-rc.229 → 4.1.0-rc.230
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 +17 -9
- package/dist/checkout-engine/adapters/shopify.d.ts +27 -2
- package/dist/checkout-engine/adapters/shopify.js +194 -20
- package/dist/checkout-engine/cli-engine.d.ts +35 -2
- package/dist/checkout-engine/cli-engine.js +146 -12
- package/dist/checkout-engine/executor.d.ts +17 -0
- package/dist/checkout-engine/executor.js +212 -40
- package/dist/checkout-engine/hosted-approval.js +4 -1
- package/dist/checkout-engine/index.d.ts +3 -2
- package/dist/checkout-engine/index.js +1 -0
- package/dist/checkout-engine/receipt.d.ts +14 -0
- package/dist/checkout-engine/receipt.js +13 -3
- package/dist/checkout-engine/unresolved-charges.js +9 -0
- package/dist/checkout-engine/web-bot-auth.d.ts +7 -1
- package/dist/checkout-engine/web-bot-auth.js +60 -1
- package/dist/cli.js +540 -474
- package/dist/mcp-server/index.js +451 -396
- package/dist/skills/pair-visa-agent/RUNTIMES.md +9 -1
- package/dist/skills/pair-visa-agent/scripts/__tests__/setup.test.mjs +407 -0
- package/dist/skills/pair-visa-agent/scripts/setup.mjs +310 -30
- package/install.ps1 +5 -4
- package/install.sh +1 -1
- package/native/bin/win32-x64/visa-keychain-win.exe +0 -0
- package/package.json +8 -10
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ Shopify checkout skill, and both Visa and Shopify UCP MCP entries in one step:
|
|
|
76
76
|
visa agent skill --commerce --runtime codex
|
|
77
77
|
# runtimes with writable MCP config: codex, openclaw, hermes
|
|
78
78
|
|
|
79
|
-
#
|
|
79
|
+
# Inspect the installed local bundle without changing skills or MCP config:
|
|
80
80
|
visa agent skill --commerce --runtime codex --check
|
|
81
81
|
```
|
|
82
82
|
|
|
@@ -87,13 +87,20 @@ reports both MCP entries as mounted or already mounted; restart or reload the
|
|
|
87
87
|
agent before using the new tools. The installer never performs discovery or a
|
|
88
88
|
checkout itself. The install-only command exits successfully after writing the
|
|
89
89
|
bundle, but reports `commerceReady: false` / `ucp_check_required` until the
|
|
90
|
-
explicit `--check` gate passes.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
explicit `--check` gate passes. `commerceReady` is a compatibility alias for
|
|
91
|
+
local bundle readiness only; it never proves that a merchant purchase can be
|
|
92
|
+
completed. The JSON result keeps `purchase.ready: false` until a separate live
|
|
93
|
+
checkout preflight is performed.
|
|
94
|
+
|
|
95
|
+
Checked readiness is read-only: missing or modified skills and missing,
|
|
96
|
+
conflicting, or malformed MCP entries are reported without installing or
|
|
97
|
+
rewriting anything. It parses the active local profile and its pinned Shopify
|
|
98
|
+
profile contract directly from disk, without starting `npx`, and reports the
|
|
99
|
+
exact profile initialization command if local state is missing or invalid. It
|
|
100
|
+
also refuses to claim local natural-language shopping readiness when a
|
|
101
|
+
top-level `ucp` or `shop` skill with a `SKILL.md` is present in the same runtime
|
|
102
|
+
directory. The installer reports those paths but never deletes or disables
|
|
103
|
+
user-owned skills.
|
|
97
104
|
|
|
98
105
|
If the selected runtime already has a non-equivalent `shopify-ucp` entry, the
|
|
99
106
|
command fails without changing that entry or its environment, authentication,
|
|
@@ -234,7 +241,8 @@ is required so the CLI never guesses which agent can spend.
|
|
|
234
241
|
| `setup_status` | Read the server's one `nextAction` for a setup operation, plus the cadence to poll it at |
|
|
235
242
|
| `setup_resume` | Put the review link and compare code back in front of the human after a restart or a closed tab |
|
|
236
243
|
| `setup_cancel` | Abandon a setup still waiting on the human; never undoes an approval |
|
|
237
|
-
| `agent_connect`
|
|
244
|
+
| `agent_connect` | Initiate an owner-approved spending grant for a paired agent |
|
|
245
|
+
| `agent_connect_poll` | Poll that grant ceremony to activation (returns caps, and the funding address for wallet) |
|
|
238
246
|
| `wallet_status` | Delegated wallet address, network, and policy state |
|
|
239
247
|
| `wallet_policy_set` | Set per-transaction / daily caps (with human approval) |
|
|
240
248
|
| `wallet_discover` | Sweep x402 directories for services, with live re-probing |
|
|
@@ -17,9 +17,34 @@ export declare function parseShopifySummary(text: string): ShopifySummary;
|
|
|
17
17
|
export declare function readShopifyAmount(page: Page, requireVerified: boolean): Promise<PageAmountRead>;
|
|
18
18
|
export declare function readStableShopifyAmount(page: Page, timeoutMs?: number): Promise<PageAmountRead>;
|
|
19
19
|
export declare function isShopifyCheckoutPage(page: Page): Promise<boolean>;
|
|
20
|
-
export
|
|
20
|
+
export type ShopifyCheckoutSurface = {
|
|
21
21
|
signal: string;
|
|
22
|
-
|
|
22
|
+
subtype: 'recognition' | 'authentication' | 'wallet';
|
|
23
|
+
};
|
|
24
|
+
export type ShopifyGuestCheckoutResult = {
|
|
25
|
+
status: 'guest-ready';
|
|
26
|
+
signal: 'guest-card-form';
|
|
27
|
+
} | {
|
|
28
|
+
status: 'transitioned';
|
|
29
|
+
signal: string;
|
|
30
|
+
} | {
|
|
31
|
+
status: 'action-required';
|
|
32
|
+
signal: string;
|
|
33
|
+
detail: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function detectShopifyCheckoutSurface(page: Page): Promise<ShopifyCheckoutSurface | null>;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve Shopify's reversible Shop-wallet takeover before review. The helper
|
|
38
|
+
* only clicks an exact guest action or a scoped accessible close control inside
|
|
39
|
+
* a positively identified Shop surface. It never solves authentication, uses a
|
|
40
|
+
* Shop-saved payment method, force-clicks, or mutates the DOM.
|
|
41
|
+
*/
|
|
42
|
+
export declare function ensureShopifyGuestCheckout(page: Page, options?: {
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
stableMs?: number;
|
|
45
|
+
}): Promise<ShopifyGuestCheckoutResult>;
|
|
46
|
+
/** Read-only assertion for the irreversible credential boundary. */
|
|
47
|
+
export declare function assertShopifyGuestCheckout(page: Page): Promise<ShopifyGuestCheckoutResult>;
|
|
23
48
|
/**
|
|
24
49
|
* The en-US variant of a localized Shopify checkout URL, or null when it is
|
|
25
50
|
* already English (or not locale-suffixed). Shopify renders the checkout in
|
|
@@ -122,41 +122,215 @@ export async function isShopifyCheckoutPage(page) {
|
|
|
122
122
|
})
|
|
123
123
|
.catch(() => false);
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
|
|
125
|
+
const SHOP_SURFACE_SELECTOR = [
|
|
126
|
+
'dialog',
|
|
127
|
+
'[role="dialog"]',
|
|
128
|
+
'[aria-modal="true"]',
|
|
129
|
+
'[data-shop-pay-modal]',
|
|
130
|
+
'[data-shopify-pay-modal]',
|
|
131
|
+
].join(', ');
|
|
132
|
+
const SHOP_CODE_TEXT = /enter (?:the )?(?:verification|one[- ]?time)?\s*code|code (?:was |has been )?sent|verification code/i;
|
|
133
|
+
const SHOP_AUTH_TEXT = /sign in|log in|confirm (?:it(?:'|’)s )?you|continue with shop|shop account/i;
|
|
134
|
+
const SHOP_RECOGNITION_TEXT = /recognized|faster checkout|remembered|saved (?:payment|details)/i;
|
|
135
|
+
async function classifyBodyShopSurface(page) {
|
|
136
|
+
const roots = page.locator(SHOP_SURFACE_SELECTOR);
|
|
137
|
+
for (let index = 0; index < (await roots.count().catch(() => 0)); index += 1) {
|
|
138
|
+
const root = roots.nth(index);
|
|
139
|
+
if (!(await root.isVisible().catch(() => false)))
|
|
140
|
+
continue;
|
|
141
|
+
const [text, attrs, shopMark] = await Promise.all([
|
|
142
|
+
root.innerText().catch(() => ''),
|
|
143
|
+
Promise.all(['id', 'class', 'aria-label', 'data-testid'].map((name) => root.getAttribute(name).catch(() => ''))).then((values) => values.join(' ')),
|
|
144
|
+
root
|
|
145
|
+
.locator('img[alt="Shop" i], img[alt="Shop Pay" i], [aria-label="Shop" i]')
|
|
146
|
+
.count()
|
|
147
|
+
.then((count) => count > 0)
|
|
148
|
+
.catch(() => false),
|
|
149
|
+
]);
|
|
150
|
+
// A generic dialog, generic Close button, or marketing use of "shop" is
|
|
151
|
+
// not enough. Positive identification requires Shop Pay copy/attributes,
|
|
152
|
+
// or a Shop mark paired with authentication/guest-checkout language.
|
|
153
|
+
const positiveShopSignal = /\bshop\s*pay\b/i.test(`${text} ${attrs}`) ||
|
|
154
|
+
/shop[-_ ]?(?:pay|ify-pay)/i.test(attrs) ||
|
|
155
|
+
(shopMark && (SHOP_AUTH_TEXT.test(text) || /checkout as guest/i.test(text)));
|
|
156
|
+
if (!positiveShopSignal)
|
|
157
|
+
continue;
|
|
158
|
+
const subtype = SHOP_CODE_TEXT.test(text)
|
|
159
|
+
? 'authentication'
|
|
160
|
+
: /checkout as guest/i.test(text)
|
|
161
|
+
? 'wallet'
|
|
162
|
+
: SHOP_AUTH_TEXT.test(text)
|
|
163
|
+
? 'authentication'
|
|
164
|
+
: SHOP_RECOGNITION_TEXT.test(text)
|
|
165
|
+
? 'recognition'
|
|
166
|
+
: 'wallet';
|
|
167
|
+
return {
|
|
168
|
+
root,
|
|
169
|
+
subtype,
|
|
170
|
+
signal: `body:shop-${subtype}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
async function classifyFrameShopSurface(page) {
|
|
141
176
|
const frames = page.locator('iframe');
|
|
142
177
|
for (let index = 0; index < (await frames.count().catch(() => 0)); index += 1) {
|
|
143
178
|
const frame = frames.nth(index);
|
|
144
179
|
if (!(await frame.isVisible().catch(() => false)))
|
|
145
180
|
continue;
|
|
146
181
|
const src = (await frame.getAttribute('src').catch(() => '')) ?? '';
|
|
147
|
-
if (
|
|
182
|
+
if (!/^https:\/\/(?:[^/]+\.)?shop\.app\/(?:accounts\/login|pay|checkout)\b/i.test(src))
|
|
148
183
|
continue;
|
|
149
184
|
const accessibleName = [
|
|
150
185
|
await frame.getAttribute('title').catch(() => ''),
|
|
151
186
|
await frame.getAttribute('aria-label').catch(() => ''),
|
|
152
187
|
await frame.getAttribute('name').catch(() => ''),
|
|
153
188
|
].join(' ');
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
189
|
+
const subtype = SHOP_CODE_TEXT.test(accessibleName) ? 'authentication' : 'wallet';
|
|
190
|
+
return { frame, subtype, signal: `frame:shop-${subtype}` };
|
|
157
191
|
}
|
|
158
192
|
return null;
|
|
159
193
|
}
|
|
194
|
+
export async function detectShopifyCheckoutSurface(page) {
|
|
195
|
+
const located = (await classifyBodyShopSurface(page)) ?? (await classifyFrameShopSurface(page));
|
|
196
|
+
return located ? { signal: located.signal, subtype: located.subtype } : null;
|
|
197
|
+
}
|
|
198
|
+
async function locateShopifyCheckoutSurface(page) {
|
|
199
|
+
return (await classifyBodyShopSurface(page)) ?? (await classifyFrameShopSurface(page));
|
|
200
|
+
}
|
|
201
|
+
async function visibleGuestCardForm(page) {
|
|
202
|
+
const detected = await detectFields(page).catch(() => null);
|
|
203
|
+
return Boolean(detected?.fields.number?.visible);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Resolve Shopify's reversible Shop-wallet takeover before review. The helper
|
|
207
|
+
* only clicks an exact guest action or a scoped accessible close control inside
|
|
208
|
+
* a positively identified Shop surface. It never solves authentication, uses a
|
|
209
|
+
* Shop-saved payment method, force-clicks, or mutates the DOM.
|
|
210
|
+
*/
|
|
211
|
+
export async function ensureShopifyGuestCheckout(page, options = {}) {
|
|
212
|
+
const timeoutMs = options.timeoutMs ?? 1_800;
|
|
213
|
+
const stableMs = options.stableMs ?? 650;
|
|
214
|
+
const startedAt = Date.now();
|
|
215
|
+
let guestStableSince = null;
|
|
216
|
+
let located = null;
|
|
217
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
218
|
+
located = await locateShopifyCheckoutSurface(page);
|
|
219
|
+
if (located)
|
|
220
|
+
break;
|
|
221
|
+
if (await visibleGuestCardForm(page)) {
|
|
222
|
+
guestStableSince ??= Date.now();
|
|
223
|
+
if (Date.now() - guestStableSince >= stableMs) {
|
|
224
|
+
return { status: 'guest-ready', signal: 'guest-card-form' };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
guestStableSince = null;
|
|
229
|
+
}
|
|
230
|
+
await page.waitForTimeout(75);
|
|
231
|
+
}
|
|
232
|
+
if (!located) {
|
|
233
|
+
return (await visibleGuestCardForm(page))
|
|
234
|
+
? { status: 'guest-ready', signal: 'guest-card-form' }
|
|
235
|
+
: {
|
|
236
|
+
status: 'action-required',
|
|
237
|
+
signal: 'shopify:guest-card-form-unavailable',
|
|
238
|
+
detail: 'Shopify guest card form did not become stable before review; no payment credential was requested',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const originBefore = new URL(page.url()).origin;
|
|
242
|
+
let action = null;
|
|
243
|
+
let actionSignal = '';
|
|
244
|
+
if (located.root) {
|
|
245
|
+
const exactGuest = located.root
|
|
246
|
+
.getByRole('button', { name: /^checkout as guest$/i })
|
|
247
|
+
.or(located.root.getByRole('link', { name: /^checkout as guest$/i }))
|
|
248
|
+
.first();
|
|
249
|
+
if ((await exactGuest.count().catch(() => 0)) > 0 &&
|
|
250
|
+
(await exactGuest.isVisible().catch(() => false))) {
|
|
251
|
+
action = exactGuest;
|
|
252
|
+
actionSignal = `${located.signal}:checkout-as-guest`;
|
|
253
|
+
}
|
|
254
|
+
else if (located.subtype !== 'authentication' ||
|
|
255
|
+
!(await located.root.innerText()).match(SHOP_CODE_TEXT)) {
|
|
256
|
+
const scopedClose = located.root
|
|
257
|
+
.getByRole('button', { name: /^close(?: shop(?: pay)?)?$/i })
|
|
258
|
+
.first();
|
|
259
|
+
if ((await scopedClose.count().catch(() => 0)) > 0 &&
|
|
260
|
+
(await scopedClose.isVisible().catch(() => false))) {
|
|
261
|
+
action = scopedClose;
|
|
262
|
+
actionSignal = `${located.signal}:scoped-close`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else if (located.frame) {
|
|
267
|
+
const frame = located.frame.contentFrame();
|
|
268
|
+
const exactGuest = frame.getByRole('button', { name: /^checkout as guest$/i }).first();
|
|
269
|
+
if ((await exactGuest.count().catch(() => 0)) > 0 &&
|
|
270
|
+
(await exactGuest.isVisible().catch(() => false))) {
|
|
271
|
+
action = exactGuest;
|
|
272
|
+
actionSignal = `${located.signal}:checkout-as-guest`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!action) {
|
|
276
|
+
return {
|
|
277
|
+
status: 'action-required',
|
|
278
|
+
signal: located.signal,
|
|
279
|
+
detail: `Shopify Shop surface could not be safely exited (${located.signal}); no payment credential was requested`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
await action.click();
|
|
283
|
+
const surfaceGone = await page
|
|
284
|
+
.waitForFunction((selector) => !Array.from(document.querySelectorAll(selector)).some((element) => {
|
|
285
|
+
const style = getComputedStyle(element);
|
|
286
|
+
const text = element.innerText ?? '';
|
|
287
|
+
const attrs = [
|
|
288
|
+
element.id,
|
|
289
|
+
element.className,
|
|
290
|
+
element.getAttribute('aria-label'),
|
|
291
|
+
element.getAttribute('data-testid'),
|
|
292
|
+
].join(' ');
|
|
293
|
+
const visible = element.offsetParent !== null &&
|
|
294
|
+
style.display !== 'none' &&
|
|
295
|
+
style.visibility !== 'hidden';
|
|
296
|
+
return visible && /\bshop\s*pay\b|shop[-_ ]?(?:pay|ify-pay)/i.test(`${text} ${attrs}`);
|
|
297
|
+
}), SHOP_SURFACE_SELECTOR, { timeout: 2_000 })
|
|
298
|
+
.then(() => true)
|
|
299
|
+
.catch(() => false);
|
|
300
|
+
const sameOrigin = new URL(page.url()).origin === originBefore;
|
|
301
|
+
const surfaceRemains = Boolean(await locateShopifyCheckoutSurface(page));
|
|
302
|
+
if (!surfaceGone || surfaceRemains || !sameOrigin || !(await visibleGuestCardForm(page))) {
|
|
303
|
+
const signal = !sameOrigin
|
|
304
|
+
? 'shopify:guest-transition-origin-changed'
|
|
305
|
+
: !surfaceGone || surfaceRemains
|
|
306
|
+
? 'shopify:shop-surface-remained'
|
|
307
|
+
: 'shopify:guest-card-form-unavailable';
|
|
308
|
+
return {
|
|
309
|
+
status: 'action-required',
|
|
310
|
+
signal,
|
|
311
|
+
detail: `Shopify guest transition could not be verified (${signal}); no payment credential was requested`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return { status: 'transitioned', signal: actionSignal };
|
|
315
|
+
}
|
|
316
|
+
/** Read-only assertion for the irreversible credential boundary. */
|
|
317
|
+
export async function assertShopifyGuestCheckout(page) {
|
|
318
|
+
const surface = await locateShopifyCheckoutSurface(page);
|
|
319
|
+
if (surface) {
|
|
320
|
+
return {
|
|
321
|
+
status: 'action-required',
|
|
322
|
+
signal: surface.signal,
|
|
323
|
+
detail: `Shopify Shop surface reappeared after review (${surface.signal}); no payment credential was requested`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return (await visibleGuestCardForm(page))
|
|
327
|
+
? { status: 'guest-ready', signal: 'guest-card-form' }
|
|
328
|
+
: {
|
|
329
|
+
status: 'action-required',
|
|
330
|
+
signal: 'shopify:guest-card-form-unavailable',
|
|
331
|
+
detail: 'Shopify guest card form is no longer available after review; no payment credential was requested',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
160
334
|
/**
|
|
161
335
|
* The en-US variant of a localized Shopify checkout URL, or null when it is
|
|
162
336
|
* already English (or not locale-suffixed). Shopify renders the checkout in
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Browser } from 'playwright-core';
|
|
2
|
-
import { prepareCheckout as realPrepareCheckout, submitApprovedCheckout as realSubmitApprovedCheckout, type CheckoutOutcome, type CheckoutFailureCode, type CheckoutResult, type PreparedCheckoutSessionStore } from './executor.js';
|
|
2
|
+
import { prepareCheckout as realPrepareCheckout, submitApprovedCheckout as realSubmitApprovedCheckout, type CheckoutMode, type CheckoutOutcome, type CheckoutFailureCode, type CheckoutResult, type PreparedCheckoutSessionStore } from './executor.js';
|
|
3
3
|
import { claimMandatePickup as realClaimMandatePickup, runHostedApproval as realRunHostedApproval } from './hosted-approval.js';
|
|
4
4
|
import { type VgsCheckoutTarget } from './vgs-live-instrument.js';
|
|
5
5
|
import { serverFetchCryptogram, serverPostConfirmation } from './vgs-gateway/server-mint-client.js';
|
|
@@ -23,10 +23,12 @@ export declare function isTransientDrawFailure(err: unknown): boolean;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function classifyCardDrawVerdictFailure(err: unknown): {
|
|
25
25
|
transient: boolean;
|
|
26
|
+
grantCapacity: boolean;
|
|
26
27
|
reasons: string[];
|
|
27
28
|
} | null;
|
|
28
29
|
export type CliReviewInput = {
|
|
29
30
|
url: string;
|
|
31
|
+
checkoutRoute: 'guest-card';
|
|
30
32
|
amount: string;
|
|
31
33
|
currency: string;
|
|
32
34
|
credentialPath: string;
|
|
@@ -42,6 +44,12 @@ export type CliReviewInput = {
|
|
|
42
44
|
approvalBaseUrl: string;
|
|
43
45
|
merchantName?: string;
|
|
44
46
|
merchantCountryCode?: string;
|
|
47
|
+
trustedMerchantIdentity?: Readonly<{
|
|
48
|
+
handoffId: string;
|
|
49
|
+
checkoutId: string;
|
|
50
|
+
allowedOrigins: readonly string[];
|
|
51
|
+
expiresAt: string;
|
|
52
|
+
}>;
|
|
45
53
|
};
|
|
46
54
|
export type CliReviewFacts = {
|
|
47
55
|
reviewId: string;
|
|
@@ -63,8 +71,9 @@ export declare class CheckoutReviewRefusedError extends Error {
|
|
|
63
71
|
readonly failureCode?: CheckoutFailureCode;
|
|
64
72
|
readonly requiresAdapter: string[];
|
|
65
73
|
readonly detectedRoles: string[];
|
|
74
|
+
readonly receiptWrite: ReceiptWriteObservation;
|
|
66
75
|
readonly detail?: string;
|
|
67
|
-
constructor(result: CheckoutResult);
|
|
76
|
+
constructor(result: CheckoutResult, receiptWrite: ReceiptWriteObservation);
|
|
68
77
|
}
|
|
69
78
|
export type CliPayInput = CliReviewInput & {
|
|
70
79
|
reviewId: string;
|
|
@@ -102,6 +111,8 @@ export type CliReceiptFacts = {
|
|
|
102
111
|
credentialIssued: boolean;
|
|
103
112
|
/** True only when at least one payment field was successfully filled. */
|
|
104
113
|
credentialDisclosed: boolean;
|
|
114
|
+
/** Privacy-safe local receipt persistence outcome for centralized correlation. */
|
|
115
|
+
receiptWrite?: ReceiptWriteObservation;
|
|
105
116
|
};
|
|
106
117
|
type PayAttempt = {
|
|
107
118
|
fingerprint: string;
|
|
@@ -180,6 +191,9 @@ export type CliMandateFacts = CardMandateFacts & {
|
|
|
180
191
|
};
|
|
181
192
|
type Session = {
|
|
182
193
|
browser: Browser;
|
|
194
|
+
/** Exact caller URL repeated at pay time; may contain a UCP capability. */
|
|
195
|
+
requestUrl: string;
|
|
196
|
+
checkoutRoute: 'guest-card';
|
|
183
197
|
target: VgsCheckoutTarget;
|
|
184
198
|
amountMinor: number;
|
|
185
199
|
currency: string;
|
|
@@ -260,6 +274,14 @@ export type CliEngineDeps = {
|
|
|
260
274
|
onApprovalUrl?: (url: string) => void;
|
|
261
275
|
reportVicOutcome?: typeof realReportVicOutcome;
|
|
262
276
|
writeReceipt?: typeof realWriteReceipt;
|
|
277
|
+
/**
|
|
278
|
+
* Privacy-safe receipt persistence lifecycle signal. The event deliberately
|
|
279
|
+
* excludes the receipt body, checkout URL, filesystem path, and raw error.
|
|
280
|
+
* Observer failures are swallowed so logging can never alter checkout state.
|
|
281
|
+
*/
|
|
282
|
+
onReceiptWrite?: (event: ReceiptWriteObservation) => void | Promise<void>;
|
|
283
|
+
/** Injectable privacy-safe id source for terminal observations before reviewId exists. */
|
|
284
|
+
createObservationId?: () => string;
|
|
263
285
|
store?: PreparedCheckoutSessionStore;
|
|
264
286
|
sessions?: Map<string, Session>;
|
|
265
287
|
/** Exact-review singleflight registry; tests inject a fresh map for isolation. */
|
|
@@ -295,6 +317,17 @@ export type CliEngineDeps = {
|
|
|
295
317
|
*/
|
|
296
318
|
resolveEmailOtp?: OtpResolver;
|
|
297
319
|
};
|
|
320
|
+
export type ReceiptWriteObservation = {
|
|
321
|
+
event: 'checkout_receipt_write';
|
|
322
|
+
status: 'written' | 'failed';
|
|
323
|
+
mode: CheckoutMode;
|
|
324
|
+
checkoutOutcome: CheckoutOutcome;
|
|
325
|
+
merchantHost: string;
|
|
326
|
+
observationId: string;
|
|
327
|
+
failureCode: string | null;
|
|
328
|
+
errorCode: string | null;
|
|
329
|
+
panRedactions: number | null;
|
|
330
|
+
};
|
|
298
331
|
export declare function createCliCheckoutEngine(deps?: CliEngineDeps): {
|
|
299
332
|
startCardMandate(input: CliStartMandateInput): Promise<CliMandateFacts>;
|
|
300
333
|
claimCardMandate(input: CliClaimMandateInput): Promise<CliMandateFacts>;
|