@agent-cards/checkout 0.1.0

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 ADDED
@@ -0,0 +1,126 @@
1
+ # @agent-cards/checkout
2
+
3
+ Let your browser agents pay with **the user's own card**, without your
4
+ infrastructure ever touching card data.
5
+
6
+ Your agent drives checkout normally. When the page tries to tokenize a card, we
7
+ pause that one request, ask the cardholder to approve on their device, and their
8
+ device supplies the card and calls the merchant. You get back the response to
9
+ replay. A real card never enters your process, your logs, or your network.
10
+
11
+ ```
12
+ your agent ──drives──> merchant checkout
13
+ │ tokenization request
14
+
15
+ [ paused by this SDK ]
16
+ │ template only, dummy card
17
+
18
+ Agentcard ──notify──> cardholder's device
19
+ │ decrypts card locally
20
+
21
+ merchant's card vault
22
+ ┌────────token────────┘
23
+
24
+ [ request resumed ] ──> order completes
25
+ ```
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm i @agent-cards/checkout
31
+ ```
32
+
33
+ ## Use it
34
+
35
+ Two lines against a CDP session you already have:
36
+
37
+ ```ts
38
+ import { VaultClient, attachToCdp } from '@agent-cards/checkout';
39
+
40
+ const vault = new VaultClient({
41
+ clientId: process.env.AGENTCARD_CLIENT_ID!,
42
+ clientSecret: process.env.AGENTCARD_CLIENT_SECRET!,
43
+ });
44
+
45
+ await attachToCdp(cdp, pageSessionId, {
46
+ vault,
47
+ user: 'usr_123', // whose card should pay
48
+ merchant: 'vanman.shop',
49
+ amount: '$5.83',
50
+ onApprovalUrl: (url) => sendToUser(url), // iMessage, SMS, push, your call
51
+ });
52
+ ```
53
+
54
+ Then let your agent click "Pay" like it always does. `attachToCdp` holds the
55
+ request open until the cardholder approves, so the checkout simply continues.
56
+
57
+ Playwright:
58
+
59
+ ```ts
60
+ import { attachToPlaywright } from '@agent-cards/checkout/playwright';
61
+ await attachToPlaywright(page, { vault, user, merchant, amount });
62
+ ```
63
+
64
+ ## Credentials
65
+
66
+ Use your **OAuth client credentials**, not an `sk_` API key — those are retired,
67
+ and the checkout endpoints reject them (`client_credentials_required`) because an
68
+ authorization is bound to the confidential client that created it. The SDK does
69
+ the `client_credentials` exchange for you, caches the token, and refreshes it
70
+ once on a 401. Create a client from the dashboard Credentials page or with
71
+ `agent-cards-admin oauth-clients create`.
72
+
73
+ ## Why you need the SDK and not just `Fetch.enable`
74
+
75
+ Card fields render in **cross-origin iframes**, which are separate CDP targets.
76
+ Enabling `Fetch` on the page session never sees the tokenization request. You
77
+ need recursive `Target.setAutoAttach({ flatten: true })` on every nested target,
78
+ then `Fetch.enable` on each, then `Runtime.runIfWaitingForDebugger` to unpause
79
+ them. That, plus which headers a merchant requires you to replay verbatim, is
80
+ what this package encapsulates.
81
+
82
+ ## What runs where
83
+
84
+ | | Sees the real card |
85
+ |---|---|
86
+ | Your agent / browser | **no** — only a dummy PAN and a token |
87
+ | Agentcard servers | **no** — a request template and a token |
88
+ | Cardholder's device | yes — decrypts locally, calls the merchant directly |
89
+
90
+ Because your process only ever handles a dummy card and an opaque token, this
91
+ integration is designed to keep you out of PCI scope. Get your own QSA's read
92
+ before you put that in writing.
93
+
94
+ ## Supported processors
95
+
96
+ Merchants inherit their processor, so one entry covers every store on it.
97
+
98
+ | Processor | Status |
99
+ |---|---|
100
+ | Shopify | supported, verified end to end |
101
+ | Stripe | supported, verified end to end |
102
+ | Braintree / PayPal | supported, verified end to end |
103
+ | Checkout.com | supported |
104
+ | Adyen | not yet — the card is encrypted in-page, so a paused request carries a blob. Throws `CardEncryptedError` |
105
+
106
+ The recognizer list is fetched from the API at runtime (`vault.syncRegistry()`),
107
+ so new processors work without you shipping a release.
108
+
109
+ ## Errors worth handling
110
+
111
+ - `ApprovalTimeoutError` — the user never approved. Default window is 15 minutes;
112
+ we have completed checkouts after a 5.5 minute approval delay.
113
+ - `ApprovalDeclinedError` — the user said no.
114
+ - `CardEncryptedError` — this processor needs vault-side crypto; route the
115
+ purchase to an Agentcard-issued card instead.
116
+
117
+ ## Building this package
118
+
119
+ It declares **no dependencies**, matching `packages/vault`, so the workspace
120
+ lockfile needs no importer entry for it (a new package with its own deps cannot
121
+ be installed here without regenerating the lockfile, and a full regen drifts
122
+ unrelated transitive versions). Build it with the workspace TypeScript:
123
+
124
+ ```bash
125
+ pnpm --filter backend exec tsc -p packages/checkout/tsconfig.json
126
+ ```
package/dist/cdp.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { VaultClient } from './client.js';
2
+ /**
3
+ * Minimal shape of a CDP connection. Works with a raw websocket client, a
4
+ * Puppeteer CDPSession, or Playwright's CDPSession.
5
+ */
6
+ export interface CdpLike {
7
+ send(method: string, params?: any, sessionId?: string): Promise<any>;
8
+ on(handler: (method: string, params: any, sessionId?: string) => void): void;
9
+ }
10
+ export interface AttachOptions {
11
+ vault: VaultClient;
12
+ user: string;
13
+ merchant: string;
14
+ amount: string;
15
+ onApprovalUrl?: (url: string) => void;
16
+ onEvent?: (e: {
17
+ type: string;
18
+ detail?: unknown;
19
+ }) => void;
20
+ }
21
+ /**
22
+ * Take over card tokenization for a page.
23
+ *
24
+ * IMPORTANT: card fields render in cross-origin iframes, which are separate CDP
25
+ * targets. Enabling Fetch on the page session alone will never see the
26
+ * tokenization request. This attaches recursively so every nested target is
27
+ * armed, which is the whole reason this adapter exists.
28
+ */
29
+ export declare function attachToCdp(cdp: CdpLike, pageSessionId: string, opts: AttachOptions): Promise<void>;
30
+ /**
31
+ * Playwright convenience wrapper — the path for cloud browsers that hand you a
32
+ * CDP websocket (Kernel's `cdp_ws_url`, Browserbase, etc.):
33
+ *
34
+ * const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
35
+ * const page = await browser.contexts()[0].newPage();
36
+ * await attachToPlaywright(page, { vault, user, merchant, amount });
37
+ *
38
+ * Uses Playwright's own request routing, which already spans subframes — see
39
+ * the note in the body for why a hand-rolled CDPSession does not work here.
40
+ */
41
+ export declare function attachToPlaywright(page: any, opts: AttachOptions): Promise<void>;
package/dist/cdp.js ADDED
@@ -0,0 +1,123 @@
1
+ const CARD_PATTERNS = [
2
+ '*pci.shopifyinc.com/sessions*',
3
+ '*shopifycs.com/sessions*',
4
+ '*api.stripe.com/v1/payment_methods*',
5
+ '*api.stripe.com/v1/tokens*',
6
+ '*braintree-api.com/graphql*',
7
+ '*checkout.com/tokens*',
8
+ ];
9
+ /**
10
+ * Take over card tokenization for a page.
11
+ *
12
+ * IMPORTANT: card fields render in cross-origin iframes, which are separate CDP
13
+ * targets. Enabling Fetch on the page session alone will never see the
14
+ * tokenization request. This attaches recursively so every nested target is
15
+ * armed, which is the whole reason this adapter exists.
16
+ */
17
+ export async function attachToCdp(cdp, pageSessionId, opts) {
18
+ const armed = new Set();
19
+ const arm = async (sessionId) => {
20
+ const key = sessionId ?? '__root__';
21
+ if (armed.has(key))
22
+ return;
23
+ armed.add(key);
24
+ await cdp.send('Fetch.enable', {
25
+ patterns: CARD_PATTERNS.map((urlPattern) => ({ urlPattern, requestStage: 'Request' })),
26
+ }, sessionId).catch(() => { });
27
+ // Descend into this target's own children (iframes inside iframes).
28
+ await cdp.send('Target.setAutoAttach', {
29
+ autoAttach: true, waitForDebuggerOnStart: true, flatten: true,
30
+ }, sessionId).catch(() => { });
31
+ };
32
+ cdp.on(async (method, params, sessionId) => {
33
+ if (method === 'Target.attachedToTarget') {
34
+ const child = params.sessionId;
35
+ await arm(child);
36
+ // Child targets start paused when waitForDebuggerOnStart is set.
37
+ await cdp.send('Runtime.runIfWaitingForDebugger', {}, child).catch(() => { });
38
+ return;
39
+ }
40
+ if (method !== 'Fetch.requestPaused')
41
+ return;
42
+ const { requestId, request } = params;
43
+ if (!opts.vault.isCardRequest(request.url, request.method)) {
44
+ await cdp.send('Fetch.continueRequest', { requestId }, sessionId).catch(() => { });
45
+ return;
46
+ }
47
+ let body = request.postData ?? '';
48
+ if (!body && request.hasPostData) {
49
+ body = (await cdp.send('Fetch.getRequestPostData', { requestId }, sessionId).catch(() => ({ postData: '' }))).postData ?? '';
50
+ }
51
+ opts.onEvent?.({ type: 'card_request_paused', detail: { url: request.url } });
52
+ try {
53
+ const replay = await opts.vault.authorize({
54
+ user: opts.user,
55
+ merchant: opts.merchant,
56
+ amount: opts.amount,
57
+ onApprovalUrl: opts.onApprovalUrl,
58
+ request: { url: request.url, method: request.method, headers: request.headers, body },
59
+ });
60
+ await cdp.send('Fetch.fulfillRequest', {
61
+ requestId,
62
+ responseCode: replay.status,
63
+ responseHeaders: Object.entries(replay.headers).map(([name, value]) => ({ name, value: String(value) })),
64
+ body: Buffer.from(replay.body).toString('base64'),
65
+ }, sessionId);
66
+ opts.onEvent?.({ type: 'authorized' });
67
+ }
68
+ catch (err) {
69
+ opts.onEvent?.({ type: 'failed', detail: String(err) });
70
+ await cdp.send('Fetch.failRequest', { requestId, errorReason: 'Aborted' }, sessionId).catch(() => { });
71
+ }
72
+ });
73
+ await arm(pageSessionId);
74
+ }
75
+ /**
76
+ * Playwright convenience wrapper — the path for cloud browsers that hand you a
77
+ * CDP websocket (Kernel's `cdp_ws_url`, Browserbase, etc.):
78
+ *
79
+ * const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
80
+ * const page = await browser.contexts()[0].newPage();
81
+ * await attachToPlaywright(page, { vault, user, merchant, amount });
82
+ *
83
+ * Uses Playwright's own request routing, which already spans subframes — see
84
+ * the note in the body for why a hand-rolled CDPSession does not work here.
85
+ */
86
+ export async function attachToPlaywright(page, opts) {
87
+ // Playwright's own routing, NOT a hand-rolled CDP session.
88
+ //
89
+ // A CDPSession from `newCDPSession(page)` is bound to the PAGE target and its
90
+ // send() takes no session id, so every command meant for an attached child
91
+ // target lands on the page instead — the nested card iframes are never armed
92
+ // and the tokenization request sails past. Card fields are cross-origin
93
+ // iframes essentially always, so that adapter was broken for the only case
94
+ // that matters.
95
+ //
96
+ // page.route already spans subframes, so Playwright does the target
97
+ // bookkeeping that attachToCdp has to do by hand for a raw connection.
98
+ await page.route((url) => opts.vault.isCardRequest(url.toString()), async (route) => {
99
+ const request = route.request();
100
+ // The matcher only sees the URL; a preflight or a GET must pass through
101
+ // untouched or the browser's CORS check fails on our synthetic answer.
102
+ if (!opts.vault.isCardRequest(request.url(), request.method())) {
103
+ return route.fallback();
104
+ }
105
+ const body = request.postData() ?? '';
106
+ opts.onEvent?.({ type: 'card_request_paused', detail: { url: request.url() } });
107
+ try {
108
+ const replay = await opts.vault.authorize({
109
+ user: opts.user,
110
+ merchant: opts.merchant,
111
+ amount: opts.amount,
112
+ onApprovalUrl: opts.onApprovalUrl,
113
+ request: { url: request.url(), method: request.method(), headers: request.headers(), body },
114
+ });
115
+ await route.fulfill({ status: replay.status, headers: replay.headers, body: replay.body });
116
+ opts.onEvent?.({ type: 'authorized' });
117
+ }
118
+ catch (err) {
119
+ opts.onEvent?.({ type: 'failed', detail: String(err) });
120
+ await route.abort();
121
+ }
122
+ });
123
+ }
@@ -0,0 +1,82 @@
1
+ import { type Recognizer } from './registry.js';
2
+ export interface PausedRequest {
3
+ url: string;
4
+ method: string;
5
+ headers: Record<string, string>;
6
+ body: string;
7
+ }
8
+ export interface ReplayResponse {
9
+ status: number;
10
+ headers: Record<string, string>;
11
+ /** Body to hand back to the browser, verbatim. */
12
+ body: string;
13
+ }
14
+ export interface AuthorizeInput {
15
+ /** Your identifier for the person whose card should pay. */
16
+ user: string;
17
+ /** Shown to the user on the approval screen. */
18
+ merchant: string;
19
+ amount: string;
20
+ request: PausedRequest;
21
+ /** Abort if the user has not approved within this many ms. Default 15 min. */
22
+ timeoutMs?: number;
23
+ /** Called once with the URL to surface to the user, if you deliver it yourself. */
24
+ onApprovalUrl?: (url: string) => void;
25
+ }
26
+ export declare class CardEncryptedError extends Error {
27
+ psp: string;
28
+ constructor(psp: string);
29
+ }
30
+ export declare class ApprovalTimeoutError extends Error {
31
+ constructor(ms: number);
32
+ }
33
+ export declare class ApprovalDeclinedError extends Error {
34
+ constructor(reason: string);
35
+ }
36
+ export interface VaultClientOptions {
37
+ /**
38
+ * Your Agentcard OAuth client credentials. The SDK exchanges them for a
39
+ * short-lived access token and refreshes it when it expires.
40
+ *
41
+ * NOT an `sk_` API key: those are retired, and the checkout endpoints reject
42
+ * them with `client_credentials_required` because an authorization has to be
43
+ * bound to the confidential client that created it.
44
+ */
45
+ clientId: string;
46
+ clientSecret: string;
47
+ baseUrl?: string;
48
+ /** Override the PSP registry (tests, or pinning). Defaults to the hosted list. */
49
+ registry?: Recognizer[];
50
+ fetchImpl?: typeof fetch;
51
+ pollIntervalMs?: number;
52
+ }
53
+ export declare class VaultClient {
54
+ private readonly opts;
55
+ private readonly baseUrl;
56
+ private readonly fetch;
57
+ private readonly pollIntervalMs;
58
+ private registry;
59
+ constructor(opts: VaultClientOptions);
60
+ /** Refresh recognizers from the API so new PSPs work without a redeploy. */
61
+ syncRegistry(): Promise<void>;
62
+ /** True when this request is a card tokenization we can take over. */
63
+ isCardRequest(url: string, method?: string): boolean;
64
+ /**
65
+ * Hand us a paused tokenization request. We ask the cardholder to approve,
66
+ * their device supplies the card and calls the merchant, and you get back the
67
+ * response to replay into the browser. Your process never sees a card.
68
+ */
69
+ authorize(input: AuthorizeInput): Promise<ReplayResponse>;
70
+ private token;
71
+ private inflight;
72
+ /**
73
+ * Exchange client credentials for an access token, reusing the cached one
74
+ * until it is nearly expired. Concurrent callers share a single in-flight
75
+ * exchange rather than each minting their own token.
76
+ */
77
+ private accessToken;
78
+ /** Authenticated request that retries ONCE on a 401 with a fresh token. */
79
+ private call;
80
+ private post;
81
+ private get;
82
+ }
package/dist/client.js ADDED
@@ -0,0 +1,164 @@
1
+ import { BUILTIN_REGISTRY, findRecognizer } from './registry.js';
2
+ export class CardEncryptedError extends Error {
3
+ psp;
4
+ constructor(psp) {
5
+ super(`${psp} encrypts the card in-page; a paused request carries a blob, not a PAN. ` +
6
+ `Agentcard must run this PSP's client-side crypto in the vault.`);
7
+ this.psp = psp;
8
+ this.name = 'CardEncryptedError';
9
+ }
10
+ }
11
+ export class ApprovalTimeoutError extends Error {
12
+ constructor(ms) { super(`user did not approve within ${ms}ms`); this.name = 'ApprovalTimeoutError'; }
13
+ }
14
+ export class ApprovalDeclinedError extends Error {
15
+ constructor(reason) { super(`user declined: ${reason}`); this.name = 'ApprovalDeclinedError'; }
16
+ }
17
+ export class VaultClient {
18
+ opts;
19
+ baseUrl;
20
+ fetch;
21
+ pollIntervalMs;
22
+ registry;
23
+ constructor(opts) {
24
+ this.opts = opts;
25
+ this.baseUrl = (opts.baseUrl ?? 'https://api.agentcard.sh').replace(/\/$/, '');
26
+ this.fetch = opts.fetchImpl ?? globalThis.fetch;
27
+ this.registry = opts.registry ?? BUILTIN_REGISTRY;
28
+ this.pollIntervalMs = opts.pollIntervalMs ?? 2000;
29
+ }
30
+ /** Refresh recognizers from the API so new PSPs work without a redeploy. */
31
+ async syncRegistry() {
32
+ // Never break checkout over a registry fetch — an auth blip or a bad
33
+ // response leaves the built-in recognizers in place.
34
+ let raw;
35
+ try {
36
+ raw = await this.get('/v2/checkout/recognizers');
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ if (!Array.isArray(raw))
42
+ return;
43
+ this.registry = raw.map((e) => ({
44
+ ...e,
45
+ match: new RegExp(e.match, 'i'),
46
+ passthroughHeaders: e.passthroughHeaders.map((h) => new RegExp(h, 'i')),
47
+ }));
48
+ }
49
+ /** True when this request is a card tokenization we can take over. */
50
+ isCardRequest(url, method = 'POST') {
51
+ return method.toUpperCase() === 'POST' && findRecognizer(url, this.registry) !== null;
52
+ }
53
+ /**
54
+ * Hand us a paused tokenization request. We ask the cardholder to approve,
55
+ * their device supplies the card and calls the merchant, and you get back the
56
+ * response to replay into the browser. Your process never sees a card.
57
+ */
58
+ async authorize(input) {
59
+ const rec = findRecognizer(input.request.url, this.registry);
60
+ if (!rec)
61
+ throw new Error(`not a known tokenization endpoint: ${input.request.url}`);
62
+ if (rec.clientSideEncrypted)
63
+ throw new CardEncryptedError(rec.psp);
64
+ const timeoutMs = input.timeoutMs ?? 15 * 60_000;
65
+ const created = await this.post('/v2/checkout/authorizations', {
66
+ user: input.user,
67
+ merchant: input.merchant,
68
+ amount: input.amount,
69
+ psp: rec.psp,
70
+ request: {
71
+ url: input.request.url,
72
+ method: input.request.method,
73
+ headers: pickHeaders(input.request.headers, rec.passthroughHeaders),
74
+ body: input.request.body,
75
+ },
76
+ });
77
+ input.onApprovalUrl?.(created.approvalUrl);
78
+ const deadline = Date.now() + timeoutMs;
79
+ while (Date.now() < deadline) {
80
+ await sleep(this.pollIntervalMs);
81
+ const s = await this.get(`/v2/checkout/authorizations/${created.id}`);
82
+ if (s.status === 'approved')
83
+ return s.response;
84
+ if (s.status === 'declined')
85
+ throw new ApprovalDeclinedError(s.reason ?? 'no reason given');
86
+ }
87
+ throw new ApprovalTimeoutError(timeoutMs);
88
+ }
89
+ // --- auth: client_credentials, cached until just before it expires --------
90
+ token = null;
91
+ inflight = null;
92
+ /**
93
+ * Exchange client credentials for an access token, reusing the cached one
94
+ * until it is nearly expired. Concurrent callers share a single in-flight
95
+ * exchange rather than each minting their own token.
96
+ */
97
+ async accessToken(force = false) {
98
+ if (!force && this.token && Date.now() < this.token.expiresAt)
99
+ return this.token.value;
100
+ if (!force && this.inflight)
101
+ return this.inflight;
102
+ this.inflight = (async () => {
103
+ const body = new URLSearchParams({
104
+ grant_type: 'client_credentials',
105
+ client_id: this.opts.clientId,
106
+ client_secret: this.opts.clientSecret,
107
+ });
108
+ const r = await this.fetch(`${this.baseUrl}/api/v1/oauth/token`, {
109
+ method: 'POST',
110
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
111
+ body: body.toString(),
112
+ });
113
+ if (!r.ok) {
114
+ // RFC 6749 error shape, which real OAuth clients expect verbatim.
115
+ const d = await r.json().catch(() => ({}));
116
+ throw new Error(`agentcard auth failed: ${d.error ?? r.status} ${d.error_description ?? ''}`.trim());
117
+ }
118
+ const d = (await r.json());
119
+ // Renew a minute early so a token never expires mid-checkout.
120
+ const ttl = Math.max(60, (d.expires_in ?? 3600) - 60);
121
+ this.token = { value: d.access_token, expiresAt: Date.now() + ttl * 1000 };
122
+ return this.token.value;
123
+ })().finally(() => { this.inflight = null; });
124
+ return this.inflight;
125
+ }
126
+ /** Authenticated request that retries ONCE on a 401 with a fresh token. */
127
+ async call(path, init = {}, retried = false) {
128
+ const token = await this.accessToken();
129
+ const r = await this.fetch(`${this.baseUrl}${path}`, {
130
+ ...init,
131
+ headers: { ...(init.headers ?? {}), authorization: `Bearer ${token}`, 'content-type': 'application/json' },
132
+ });
133
+ // A token can be revoked or expire early; one forced refresh, then give up.
134
+ if (r.status === 401 && !retried) {
135
+ await this.accessToken(true);
136
+ return this.call(path, init, true);
137
+ }
138
+ if (!r.ok)
139
+ throw new Error(`agentcard ${path} -> ${r.status} ${await r.text()}`);
140
+ return r.json();
141
+ }
142
+ post(path, body) {
143
+ return this.call(path, { method: 'POST', body: JSON.stringify(body) });
144
+ }
145
+ get(path) {
146
+ return this.call(path);
147
+ }
148
+ }
149
+ /**
150
+ * Forward only the headers the merchant needs to accept the replay. Everything
151
+ * else (cookies, UA, tracing) is dropped so we transmit as little as possible.
152
+ */
153
+ function pickHeaders(headers, allow) {
154
+ const out = {};
155
+ for (const [k, v] of Object.entries(headers)) {
156
+ const lk = k.toLowerCase();
157
+ if (lk === 'content-type' || allow.some((re) => re.test(lk)))
158
+ out[lk] = v;
159
+ }
160
+ if (!out['content-type'])
161
+ out['content-type'] = 'application/json';
162
+ return out;
163
+ }
164
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -0,0 +1,6 @@
1
+ export { VaultClient, CardEncryptedError, ApprovalTimeoutError, ApprovalDeclinedError } from './client.js';
2
+ export type { PausedRequest, ReplayResponse, AuthorizeInput, VaultClientOptions } from './client.js';
3
+ export { attachToCdp, attachToPlaywright } from './cdp.js';
4
+ export type { CdpLike, AttachOptions } from './cdp.js';
5
+ export { BUILTIN_REGISTRY, findRecognizer } from './registry.js';
6
+ export type { Recognizer } from './registry.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { VaultClient, CardEncryptedError, ApprovalTimeoutError, ApprovalDeclinedError } from './client.js';
2
+ export { attachToCdp, attachToPlaywright } from './cdp.js';
3
+ export { BUILTIN_REGISTRY, findRecognizer } from './registry.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Which outbound requests carry a raw card, and where the card sits inside them.
3
+ *
4
+ * This is the only merchant-specific knowledge in the system, and it is keyed by
5
+ * PSP rather than by merchant: every Shopify store shares one entry, every Stripe
6
+ * site shares another. The list is served from the Agentcard API at runtime so
7
+ * integrators pick up new processors without shipping a release.
8
+ */
9
+ export type Encoding = 'json' | 'form';
10
+ export interface Recognizer {
11
+ /** Stable id, e.g. "shopify" */
12
+ psp: string;
13
+ /** Matches the tokenization endpoint. */
14
+ match: RegExp;
15
+ encoding: Encoding;
16
+ /**
17
+ * Request headers that must be replayed verbatim for the merchant to accept
18
+ * the call (signatures, client tokens). Matched case-insensitively.
19
+ */
20
+ passthroughHeaders: RegExp[];
21
+ /**
22
+ * True when the card is encrypted inside the page before the request leaves.
23
+ * A digit swap is useless here; the vault must re-run the PSP's client-side
24
+ * crypto instead. Kept in the registry so callers can fail loudly.
25
+ */
26
+ clientSideEncrypted?: boolean;
27
+ }
28
+ export declare const BUILTIN_REGISTRY: Recognizer[];
29
+ export declare function findRecognizer(url: string, registry?: Recognizer[]): Recognizer | null;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Which outbound requests carry a raw card, and where the card sits inside them.
3
+ *
4
+ * This is the only merchant-specific knowledge in the system, and it is keyed by
5
+ * PSP rather than by merchant: every Shopify store shares one entry, every Stripe
6
+ * site shares another. The list is served from the Agentcard API at runtime so
7
+ * integrators pick up new processors without shipping a release.
8
+ */
9
+ export const BUILTIN_REGISTRY = [
10
+ {
11
+ psp: 'shopify',
12
+ match: /(checkout\.pci\.shopifyinc\.com|deposit\.[a-z0-9-]+\.shopifycs\.com)\/sessions/i,
13
+ encoding: 'json',
14
+ passthroughHeaders: [/^shopify-identification-signature$/i],
15
+ },
16
+ {
17
+ psp: 'stripe',
18
+ match: /api\.stripe\.com\/v1\/(payment_methods|tokens)/i,
19
+ encoding: 'form',
20
+ // Elements' own surface markers; without these Stripe rejects the surface.
21
+ passthroughHeaders: [/^authorization$/i, /^stripe-version$/i, /^x-stripe-client-user-agent$/i],
22
+ },
23
+ {
24
+ psp: 'braintree',
25
+ match: /payments(\.sandbox)?\.braintree-api\.com\/graphql/i,
26
+ encoding: 'json',
27
+ passthroughHeaders: [/^authorization$/i, /^braintree-version$/i],
28
+ },
29
+ {
30
+ psp: 'checkout_com',
31
+ match: /api(\.sandbox)?\.checkout\.com\/tokens/i,
32
+ encoding: 'json',
33
+ passthroughHeaders: [/^authorization$/i],
34
+ },
35
+ {
36
+ psp: 'adyen',
37
+ match: /(checkoutshopper-[a-z]+\.adyen\.com|adyenpayments\.com)/i,
38
+ encoding: 'json',
39
+ passthroughHeaders: [],
40
+ clientSideEncrypted: true,
41
+ },
42
+ ];
43
+ export function findRecognizer(url, registry = BUILTIN_REGISTRY) {
44
+ return registry.find((r) => r.match.test(url)) ?? null;
45
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@agent-cards/checkout",
3
+ "version": "0.1.0",
4
+ "description": "Let browser agents pay with the user's own card, without your infrastructure ever touching card data.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./cdp": "./dist/cdp.js",
11
+ "./playwright": "./dist/cdp.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "_comment_build": "TypeScript is fetched rather than declared as a devDependency ON PURPOSE. This package ships zero dependencies, which is why pnpm writes no importer for it in the workspace lockfile; adding any dep here creates one, and an importer the lockfile has not been regenerated for fails every Vercel build with ERR_PNPM_OUTDATED_LOCKFILE. Pinned so the published output is reproducible.",
22
+ "build": "npx -y -p typescript@5.9.3 tsc",
23
+ "prepublishOnly": "pnpm build",
24
+ "test": "node test.mjs"
25
+ },
26
+ "keywords": [
27
+ "payments",
28
+ "agents",
29
+ "browser-automation",
30
+ "pci",
31
+ "checkout"
32
+ ],
33
+ "engines": {
34
+ "node": ">=22"
35
+ },
36
+ "license": "UNLICENSED"
37
+ }