Skip to content

Commit a77e3d4

Browse files
authored
Document embedded per-purchase CVC collection (#717)
* Document embedded per-purchase CVC collection * Clarify CVC capability, origin errors and submit timeout
1 parent 1ac9add commit a77e3d4

3 files changed

Lines changed: 236 additions & 5 deletions

File tree

‎docs.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@
208208
"integrations/wallets/overview",
209209
"integrations/wallets/stripe-link",
210210
"integrations/wallets/agentcard",
211-
"integrations/wallets/embedded-card-capture"
211+
"integrations/wallets/embedded-card-capture",
212+
"integrations/wallets/embedded-card-cvc"
212213
]
213214
},
214215
"config-registry"

‎integrations/wallets/embedded-card-capture.mdx‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ use the embedded component when:
1515
use `action.url` instead when a hosted page is acceptable. it needs no frontend work.
1616

1717
<Note>
18-
only card entry is embeddable. visa purchases still send the cardholder to
19-
the KERNEL-hosted `spend_approval` page, because visa's authentication runs
20-
in its own iframe and passkey window there. mastercard purchases don't use
21-
that page.
18+
card enrollment and [per-purchase CVC
19+
collection](/integrations/wallets/embedded-card-cvc) are embeddable.
20+
alias-mode purchases on any card network require fresh CVC collection, either
21+
embedded or through the hosted fallback. Visa network-token authentication
22+
still uses the KERNEL-hosted `spend_approval` page with its own iframe and
23+
passkey window. Network-token Mastercard purchases don't need that hosted
24+
approval step.
2225
</Note>
2326

2427
## Install
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
---
2+
title: "Embedded Card CVC"
3+
description: "Collect a per-purchase security code on your own branded page"
4+
---
5+
6+
Alias-mode purchases with a KERNEL wallet (`provider: "kernel"`) require a fresh CVC for each purchase, on any card network. Authorization returns `pending_authorization` with a `spend_approval` action. Render [`@onkernel/vault-react`](https://www.npmjs.com/package/@onkernel/vault-react)'s `KernelCardCVC` component to keep this step inside your application, or open `action.url` for the hosted fallback.
7+
8+
The cardholder enters the code in a Very Good Security (VGS) iframe. Your page, your servers, and KERNEL never receive the raw CVC. VGS sends KERNEL a temporary alias bound to this purchase. The component doesn't collect a card number, enroll a wallet, or submit merchant checkout.
9+
10+
<Note>
11+
Use this component only for the CVC action whose URL points to `/card-cvc` on
12+
your configured KERNEL vault origin. Other spend approvals, including Visa
13+
network-token authentication, still use the hosted URL. Don't select a CVC
14+
flow by action name or card network alone. Use [embedded card
15+
capture](/integrations/wallets/embedded-card-capture) for enrollment.
16+
</Note>
17+
18+
## Install
19+
20+
```bash
21+
bun add @onkernel/vault-react@^0.4.0
22+
# or: npm install @onkernel/vault-react@^0.4.0
23+
```
24+
25+
The package requires React 18 or later and ships no CSS.
26+
27+
## Deliver the capability from your backend
28+
29+
After authorizing the purchase, retrieve it from your authenticated backend. There isn't a dedicated `action.cvc_token` field yet. The fragment extraction below, including the `token` parameter name and validation regex, is provisional and will be replaced by a dedicated API field. For this interim CVC flow, extract the `token` parameter from the action URL's fragment and deliver it only to the intended cardholder's authenticated application. Validate the origin and `/card-cvc` path before selecting this component:
30+
31+
<CodeGroup>
32+
33+
```typescript TypeScript
34+
const purchase = await kernel.vaults.items.retrieve("purchase-12345", {
35+
id_or_name: "user-12345",
36+
});
37+
38+
if (
39+
purchase.type === "card" &&
40+
purchase.state.status === "pending_authorization" &&
41+
purchase.action?.name === "spend_approval"
42+
) {
43+
const actionURL = new URL(purchase.action.url);
44+
if (
45+
actionURL.origin === "https://vault.kernel.sh" &&
46+
actionURL.pathname === "/card-cvc"
47+
) {
48+
const cvcToken = new URLSearchParams(actionURL.hash.slice(1)).get("token");
49+
if (cvcToken && /^[a-z0-9]{1,64}\.[A-Za-z0-9_-]{43}$/.test(cvcToken)) {
50+
// Return cvcToken only to this purchase's cardholder.
51+
}
52+
}
53+
}
54+
```
55+
56+
```python Python
57+
import re
58+
from urllib.parse import parse_qs, urlparse
59+
60+
purchase = kernel.vaults.items.retrieve(
61+
"purchase-12345",
62+
id_or_name="user-12345",
63+
)
64+
65+
if (
66+
purchase.type == "card"
67+
and purchase.state.status == "pending_authorization"
68+
and purchase.action is not None
69+
and purchase.action.name == "spend_approval"
70+
):
71+
action_url = urlparse(purchase.action.url)
72+
if (
73+
action_url.scheme == "https"
74+
and action_url.netloc == "vault.kernel.sh"
75+
and action_url.path == "/card-cvc"
76+
):
77+
cvc_token = parse_qs(action_url.fragment).get("token", [None])[0]
78+
if cvc_token and re.fullmatch(r"[a-z0-9]{1,64}\.[A-Za-z0-9_-]{43}", cvc_token):
79+
# Return cvc_token only to this purchase's cardholder.
80+
pass
81+
```
82+
83+
</CodeGroup>
84+
85+
Use your configured vault origin instead of `https://vault.kernel.sh` when targeting another environment. This fragment extraction is specific to the current CVC integration; it doesn't change enrollment's `action.capture_token` contract. `cvcToken` is a one-purchase capability, not an API key or enrollment token. Keep the action URL available as a hosted fallback. Don't send either capability to your automation agent.
86+
87+
## Render the component
88+
89+
```tsx ConfirmPurchase.tsx
90+
"use client";
91+
92+
import { KernelCardCVC } from "@onkernel/vault-react";
93+
94+
export function ConfirmPurchase({ cvcToken }: { cvcToken: string }) {
95+
return (
96+
<KernelCardCVC
97+
token={cvcToken}
98+
className="purchase-cvc"
99+
fieldCss={{
100+
"box-sizing": "border-box",
101+
height: "100%",
102+
padding: "0",
103+
"font-family": "system-ui, sans-serif",
104+
"font-size": "16px",
105+
color: "#1f1147",
106+
"&::placeholder": { color: "#9a8fbf" },
107+
}}
108+
labels={{
109+
title: "Verify your purchase",
110+
description: "Enter your card's security code for this purchase.",
111+
completeTitle: "Code collected",
112+
completeDescription: "Continue checkout before this purchase expires.",
113+
submit: "Continue",
114+
}}
115+
onComplete={() => {
116+
// Ask your backend to retrieve the purchase before resuming checkout.
117+
}}
118+
onError={(code) => {
119+
// Handle the fixed code, not raw VGS or API responses.
120+
}}
121+
/>
122+
);
123+
}
124+
```
125+
126+
The server-derived merchant, amount, and card's last four digits remain visible. Callbacks never include the code, alias, token, or provider response body.
127+
128+
| Prop | Description |
129+
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
130+
| `token` | Required purchase capability from the CVC action URL fragment |
131+
| `baseUrl` | API origin; defaults to `https://api.onkernel.com`. HTTPS required; HTTP loopback is allowed for local development. Paths, query strings, fragments, and URL credentials aren't allowed. |
132+
| `className` | Root class in both form and completion views |
133+
| `fieldCss` | CSS inside the single VGS CVC iframe; read at mount |
134+
| `labels` | Partial text overrides; see below |
135+
| `onComplete` | Runs once after a 2xx response with `status: "approved"` confirms collection, not payment |
136+
| `onError` | Receives only a fixed error code |
137+
138+
`KernelCardCVC` requires `baseUrl` to be a bare origin, unlike `KernelCardCapture`, which accepts a path prefix. An invalid `baseUrl` throws a developer error during render, before sending the capability; it doesn't call `onError`.
139+
140+
The component reads `GET /vault/card-cvc/current` directly with the bearer and `credentials: "omit"`. Don't route reads through your backend: the API rate-limits per client address, so a proxy makes cardholders share its limit. The iframe submits through VGS, not through your page or backend.
141+
142+
Changing `token` or `baseUrl` unmounts the old field and resets the component. Don't change them during submission. An unmounted component ignores late callbacks; retrieve purchase state from your backend if you don't know whether collection completed.
143+
144+
## Style the component
145+
146+
Style the surrounding form through your `className`. Give the iframe container an explicit height:
147+
148+
```css
149+
.purchase-cvc .credential-field {
150+
margin-bottom: 16px;
151+
}
152+
.purchase-cvc .credential-label {
153+
display: block;
154+
margin-bottom: 8px;
155+
}
156+
.purchase-cvc .card-field {
157+
height: 48px;
158+
border: 1px solid #d9d0f5;
159+
padding: 0 12px;
160+
}
161+
.purchase-cvc .card-field iframe {
162+
display: block;
163+
width: 100%;
164+
height: 100%;
165+
border: 0;
166+
}
167+
```
168+
169+
| Class | Element |
170+
| -------------------------------------------- | ---------------------------------------- |
171+
| `card-cvc` / `credential-complete` | Root in form / completion view |
172+
| `credential-header` | Title, purchase summary, and description |
173+
| `credential-title`, `credential-description` | Headings and description text |
174+
| `approval-summary` | Server-derived purchase summary |
175+
| `credential-form` | Form |
176+
| `credential-field`, `credential-label` | Field wrapper and label |
177+
| `card-field` | VGS iframe container |
178+
| `credential-primary`, `credential-retry` | Submit and initialization-retry buttons |
179+
| `credential-error` | Fixed error message |
180+
181+
Page CSS can't reach into the iframe. Use `fieldCss` for input styling, including nested selectors such as `&:focus`, `&.invalid.touched`, `&::placeholder`, media queries, and `@font-face`. Supply it before mounting; remount to change it.
182+
183+
Override these `labels` keys: `title`, `description`, `completeTitle`, `completeDescription`, `cvc`, `cvcPlaceholder`, `submit`, `submitting`, `loading`, `retry`, and `missingToken`. Set the title, descriptions, and completion copy to your application's wording; defaults use KERNEL's lowercase copy and mention KERNEL. Error messages are fixed.
184+
185+
## Allow the component in your CSP
186+
187+
Merge these directives into your existing policy:
188+
189+
```text
190+
script-src 'self' 'nonce-<per-request-nonce>' https://js.verygoodvault.com;
191+
frame-src https://js.verygoodvault.com;
192+
connect-src 'self' https://api.onkernel.com;
193+
```
194+
195+
Collect loads a pinned, integrity-checked script and submits from its iframe origin. You don't need the inbound proxy hostname or Collect analytics endpoints in your page's `connect-src`. If you use external fonts, allow their source in the appropriate context and test the iframe rendering. The hosted fallback cannot itself be framed.
196+
197+
## Handle completion and expiry
198+
199+
`onComplete` confirms CVC collection, not a merchant payment. Retrieve the purchase from your backend and inspect `state.status === "ready"` and `expires_at` before resuming checkout. Fill and **submit checkout before `expires_at`**.
200+
201+
The unused collection capability lasts 30 minutes. After capture, the purchase expires at the temporary CVC alias deadline, **five minutes by default**. Repeated fill or collection doesn't extend that deadline. An expired alias-mode purchase requires a new purchase and fresh authorization; refreshing an enrollment token won't recover it.
202+
203+
Submission times out after 60 seconds: the component hides the form, calls `onError('unconfirmed')`, and ignores late approval callbacks.
204+
205+
If a dispatched submission has an unknown outcome, retrieve the existing purchase before taking any further action. Don't automatically recollect, create a second purchase, or repeat payment.
206+
207+
## Handle errors
208+
209+
| Code | Meaning | What to do |
210+
| ------------------ | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
211+
| `invalid` | Field validation failed | Correct the code and submit again |
212+
| `rate_limited` | Too many attempts | Wait, then retry |
213+
| `network` | View read or Collect initialization failed | Use the initialization retry button |
214+
| `expired` | Purchase expired | Create and authorize a new purchase |
215+
| `unavailable` | Capability is invalid or purchase is no longer available | Retrieve purchase state; obtain a new purchase if needed |
216+
| `invalid_response` | Purchase view couldn't be decoded safely | Stop and check your integration; keep the link for investigation |
217+
| `unconfirmed` | A dispatched submission timed out, failed ambiguously, or returned an unrecognized response | Retrieve the existing purchase; don't automatically recollect or create another payment |
218+
219+
The form is removed from use after `expired`, `unavailable`, `invalid_response`, or `unconfirmed`. The component doesn't resend CVC submissions on unmount or unload. A known field validation failure or definitive rate limit permits retry.
220+
221+
## Security
222+
223+
- Serve your page over HTTPS with no-store and no-referrer policies.
224+
- Treat the CVC capability as a secret. Never put it in a path, query string, cookie, browser storage, logs, traces, or analytics. If you deliver a link, use a fragment and clear it after completion or terminal invalidation.
225+
- Don't add native CVC inputs, raw-value callbacks, analytics, or session replay around the component. Control scripts and dependencies that can modify the surrounding page.
226+
- Raw CVC stays in the VGS-hosted field and VGS processing. Your page and KERNEL never receive it. This preserves the isolated-field boundary used by embedded card capture; it doesn't certify your PCI scope or SAQ eligibility. Confirm your integration's eligibility and script-security requirements with your acquirer or QSA.
227+
- Don't configure VGS to accept every integrator origin. The submission origin is `https://js.verygoodvault.com`; no VGS credentials or callback secrets belong in your frontend.

0 commit comments

Comments
 (0)