Skip to content

Commit 3622895

Browse files
authored
Support Link Pay Token checkout in vault card commands (#298)
## Summary Brings the vault card commands in line with the Link Pay Token vault API (kernel/kernel#3950). - **Authorize at checkout:** `items invoke <vault> <key> authorize` takes optional `--browser-id` and `--page-url`, sent as `browser_id` and `page_url`. The CLI rejects one without the other, a blank browser ID, a non-https or credential-bearing page URL, and either flag on any other operation, all before calling the API. Without the flags, authorize still sends `{"type":"authorize"}`. - **Authorize errors:** Link card authorize failures print the API's code and message plus a recovery step. Checkout inspection errors (`page_not_found`, `ambiguous_page`, `browser_not_found`, `browser_unavailable`, `destination_denied`, `timeout`, `browser_error`) say no payment request was made, to fix the tab and authorize again, and to fall back to a virtual card if inspection keeps failing. `invalid_request` covers the virtual card limit; 429 says wait and authorize again. - **Fill without fields:** card fills may omit `fields` (a Link Pay Token fill requires this); the request then omits `fields`, and the item's fill description and API validation decide. Credential fills still require fields client-side. A field-less result (`"fields":[]`) is accepted for `completed`, `failed`, and `unknown`, and human output says to submit the checkout yourself. - **Card update:** unchanged PATCH path, now tested for success and for the 409 a Link card returns after it is authorized at a checkout. Its help says it works only before a Link card is authorized at a checkout; after that, delete and recreate. - **Help text:** one shared Link checkout flow (`linkCardFlowHelp`) appears in `vaults`, `wallets create`, `cards create`, `cards update`, and `items invoke`, in the order reach checkout → create card → authorize with browser ID and page URL → share approval URL → wait for fill → fill → submit yourself. Removed or reworded text that conflicted with it (browser attached after card creation, "authorize takes no parameters", unconditional "checkout cards can be edited between authorizations", provider-assigned domains for Link, bare "never retry"). README's Link section follows the same flow. - **Runtime output:** the wallet's server-generated `description` is shown in human and JSON output. `items get` on a requested Link card suggests `authorize --browser-id ... --page-url ...`. Link card guidance now gives recovery steps instead of "never retry automatically". ## SDK Bumps `github.com/kernel/kernel-go-sdk` to v0.126.0. ## Testing - `go vet ./...` and `go test ./...` pass. - `golangci-lint run`: no findings on changed lines (pre-existing findings elsewhere are unchanged). - New tests in `cmd/vaults_link_test.go` cover: the shared flow appearing in every command's help and its step order; retired wording; the authorize request body with and without checkout flags; client-side flag validation (no API call); error guidance for each authorize error code; Pay Token fill with no `fields` in the request and field-less results; credential fill still requiring fields; Link card update success and 409; and wallet description output. - Not run against the production API. --------- Co-authored-by: rgarcia <72655+rgarcia@users.noreply.github.com>
1 parent 44913c5 commit 3622895

16 files changed

Lines changed: 620 additions & 80 deletions

‎README.md‎

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -416,10 +416,10 @@ cannot switch projects.
416416
| `kernel vaults wallets get <vault> <key>` | Observe wallet state and its hosted action; `--wait 0..60`, `--open` |
417417
| `kernel vaults wallets payment-methods <vault> <key>` | Fetch advertised live payment methods; JSON is the item with `expanded.payment_methods` |
418418
| `kernel vaults cards create <vault> <key> --provider link\|agentcard\|kernel --spec '<json>'` | Create a card request without authorizing it |
419-
| `kernel vaults cards update <vault> <key> --provider link\|agentcard --spec '<json>'` | Update a card spec; pending issuance preserves omitted optional fields, and the API enforces state/provider constraints |
419+
| `kernel vaults cards update <vault> <key> --provider link\|agentcard --spec '<json>'` | Update a card spec before it is authorized at a checkout; pending issuance preserves omitted optional fields, and the API enforces state/provider constraints |
420420
| `kernel vaults items list <vault>` | List item keys, types, providers, status, and required actions |
421421
| `kernel vaults items get <vault> <key>` | Inspect state/actions/returned AgentCard aliases and copyable operation commands; `--wait 0..60`, `--expand payment_methods`, `--open` |
422-
| `kernel vaults items invoke <vault> <key> <operation>` | GET the item, then POST an advertised operation; `authorize --open` opens a returned HTTPS action; `prepare_checkout --params '<json>'` prepares an unused AgentCard card for Square Pay; `fill --params '<json>'` fills checkout or login fields; `collect --open` opens a credential item's hosted form |
422+
| `kernel vaults items invoke <vault> <key> <operation>` | GET the item, then POST an advertised operation; `authorize --browser-id <id> --page-url <url>` authorizes a Link card at checkout; `authorize --open` opens a returned HTTPS action; `prepare_checkout --params '<json>'` prepares an unused AgentCard card for Square Pay; `fill --params '<json>'` fills checkout or login fields; `collect --open` opens a credential item's hosted form |
423423
| `kernel vaults items webmcp invoke <vault> <key>` | Invoke a live WebMCP tool with item fields bound to null input slots; `--browser-id`, `--tool-ref`, `--page-url`, `--input`/`--input-file`, repeatable `--bind <field>=<json-pointer>`, `--timeout-sec 1..120`. Same as `items invoke <vault> <key> webmcp_invoke --params '<json>'` |
424424
| `kernel vaults items events <vault> <key>` | Read ordered audit events; `--after <event-id>`, `--wait 0..60` |
425425
| `kernel vaults items delete <vault> <key>` | Invalidate an item; `--yes` skips confirmation |
@@ -454,15 +454,18 @@ TypeScript-style types, which must stay in sync with the [API spec](https://api.
454454
`merchant_name`, and HTTPS `merchant_url`. Supply two-letter `merchant_country` for Visa.
455455
Kernel card updates are unsupported.
456456
- **Link card:** include the required fields shown in help. Optional `line_items`, `totals`,
457-
`metadata`, and `expires_at` are supported through JSON.
457+
`metadata`, and `expires_at` are supported through JSON. `amount` is at most 500000 minor
458+
units; a virtual card allows at most 50000.
458459
- **AgentCard card:** uses `merchant`, not Link's `merchant_name`. Its optional `card_id` selects
459460
a vaulted card; otherwise the cardholder selects one at approval.
460461

461462
`cards update` replaces the spec for supported providers' requested cards; Kernel cards
462463
cannot be updated. Pending issuance updates preserve omitted optional fields; explicit empty lists clear them. Wallet/provider bindings and unsupported fields
463-
cannot change after authorization starts. Checkout cards can be edited between authorizations.
464-
An uncertain update enters `recovery_required` and must not be retried. Identical card creation
465-
returns its existing state without polling, reauthorizing, or resetting recovery. Link and
464+
cannot change after authorization starts. Checkout cards can be edited between authorizations,
465+
but a Link card authorized at a checkout cannot be updated (the API returns 409): delete it and
466+
create a new one. An uncertain update enters `recovery_required` and must not be retried.
467+
Identical card creation returns its existing state without polling, reauthorizing, or resetting
468+
recovery. Link fill is limited to the origin of the checkout the card was authorized at;
466469
AgentCard checkout domains remain provider-assigned; Kernel fill is locked to the exact origin
467470
of `merchant_url`. Neither command submits a merchant payment.
468471

@@ -651,12 +654,12 @@ once more only if freshly advertised. Authorize 409 requires state inspection, n
651654
changes. For 429 wait for Retry-After before re-checking availability. No automatic retries.
652655
Never put PAN, CVC, expiry, or enrollment credentials in CLI arguments or logs.
653656

654-
#### Link checkout preparation
657+
#### Link card checkout
655658

656-
Link OAuth, user approval, and provider-issued single-use card issuance precede browser checkout.
657-
Issued Link cards use only the advertised vault-item `fill` operation for checkout fields.
659+
Each item's `available_operations` descriptions and the Link wallet's `description` are
660+
authoritative; `items get` prints them. The flow:
658661

659-
1. Create/select a vault in the effective project. Connect the wallet in the provider's UI:
662+
1. Connect the wallet once, in the provider's UI:
660663

661664
```bash
662665
kernel vaults create --name checkout
@@ -665,11 +668,18 @@ Issued Link cards use only the advertised vault-item `fill` operation for checko
665668
kernel vaults items get checkout wallet-1 --wait 60
666669
```
667670

668-
2. Once connected, list methods and explicitly choose a returned ID:
671+
2. Reach the final checkout page in a browser created with the vault attached. Note the amount,
672+
currency, merchant name and URL, and choose a funding method:
669673

670674
```bash
675+
kernel browsers create --vault checkout
671676
kernel vaults wallets payment-methods checkout wallet-1
672-
# Equivalent: kernel vaults items get checkout wallet-1 --expand payment_methods
677+
```
678+
679+
3. Create the card. This does not contact Link; the card is `requested` and advertises
680+
`authorize`. To change the amount or merchant before authorizing, use `cards update`.
681+
682+
```bash
673683
kernel vaults cards create checkout order-1 --provider link --spec '{
674684
"wallet": "wallet-1",
675685
"payment_method_id": "<returned-id>",
@@ -681,30 +691,53 @@ Issued Link cards use only the advertised vault-item `fill` operation for checko
681691
}'
682692
```
683693

684-
3. After explicit user approval, authorize **only if the item advertises it**. Follow the
685-
returned approval action, then observe:
694+
4. Once the user has approved this purchase, and only when the item advertises `authorize`,
695+
authorize at the checkout with the browser session ID and the exact current top-level
696+
checkout URL. Pass both flags or neither. KERNEL inspects the checkout, chooses how the
697+
credential is delivered (a Link Pay Token or a virtual card), limits fill to that checkout's
698+
origin, and sends the user an approval request. Do not inspect or invoke WebMCP yourself.
686699

687700
```bash
688-
kernel vaults items get checkout order-1
689-
kernel vaults items invoke checkout order-1 authorize --open
690-
kernel vaults items get checkout order-1 --wait 60
701+
kernel vaults items invoke checkout order-1 authorize \
702+
--browser-id <browser-session-id> --page-url https://shop.example/checkout
691703
```
692704

693-
4. When ready, attach the same vault to a new browser, navigate to checkout, and use
694-
the advertised `fill` operation below. Respect returned permitted domains:
705+
5. Share the approval URL from the item's action with the user, then wait until `fill` is
706+
advertised. To re-check sooner, invoke `authorize` again; checkout context is not needed,
707+
and re-checking never creates another payment request.
695708

696709
```bash
697-
kernel browsers create --vault checkout
710+
kernel vaults items get checkout order-1 --wait 60
698711
```
699712

700-
5. Observe outcomes independently of merchant checkout submission:
713+
6. Fill as the item's `fill` description says, in any browser with the vault attached. A Link
714+
Pay Token takes `browser_id` and `page_url` and omits `fields`; a virtual card also takes
715+
field/selector bindings (see [Fill browser fields](#fill-browser-fields)).
701716

702717
```bash
703-
kernel vaults items get checkout order-1
704-
kernel vaults items events checkout order-1
705-
kernel vaults items events checkout order-1 --after <last-event-id> --wait 60
718+
kernel vaults items invoke checkout order-1 fill \
719+
--params '{"browser_id":"<browser-session-id>","page_url":"https://shop.example/checkout"}' -o json
706720
```
707721

722+
7. Submit the checkout yourself; fill never submits payment. Failed or unknown fills are safe
723+
to retry, but only after checking the page and not in an automatic loop.
724+
725+
Recovery:
726+
727+
- Authorize errors `page_not_found`, `ambiguous_page`, `browser_not_found`, `browser_unavailable`,
728+
`destination_denied`, `timeout`, and `browser_error` happen before any payment request. The CLI
729+
prints the code and message. Make sure the checkout is open in exactly one tab at `page_url`,
730+
then authorize again. If checkout inspection keeps failing, authorize without `--browser-id`
731+
and `--page-url` to issue a virtual card that fills any page on the `merchant_url` origin.
732+
- Once a card is authorized at a checkout, `cards update` returns 409: delete the card and
733+
create a new one.
734+
- If authorization is declined or expires, read `status_reason`, then delete the card and
735+
create a new one.
736+
- If the amount exceeds the 50000 minor-unit virtual card limit on a checkout without Link Pay
737+
Token support, delete the card and tell the user.
738+
- If the result of submitting payment is unclear, check the merchant's order confirmation or
739+
order history before paying again. `items events` shows the card's recorded events.
740+
708741
#### AgentCard checkout preparation
709742

710743
For a separate AgentCard flow, create a vault and complete the wallet enrollment action:
@@ -763,7 +796,8 @@ Stale action URLs in recovery are not opened; a successful restart
763796
returns a fresh approval action.
764797

765798
`authorize` sends `{"type":"authorize"}` without `--params` and returns the updated item,
766-
possibly with a required user action. `collect` is also parameterless and returns a credential
799+
possibly with a required user action. For a Link card at checkout, `--browser-id` and
800+
`--page-url` (both or neither) add `browser_id` and `page_url`. `collect` is also parameterless and returns a credential
767801
collection URL. `restart_verification` is parameterless and sends `{"type":"restart_verification"}`.
768802
`--open` is supported for authorize, collect, prepare_checkout, 1pw_recover, and restart_verification.
769803
`fill`, `prepare_checkout`, and `confirm_transaction` accept inputs in
@@ -815,7 +849,8 @@ Do not automatically retry or switch checkout paths after an uncertain result.
815849
Fill supports credential items and ready Link or Kernel cards when advertised by the API, not AgentCard.
816850
All use the same execution and outcome handling. Credential bindings use declared field names,
817851
including TOTP fields, and must omit `format`. Credentials may omit `page_url` only when the API
818-
can resolve a unique page. Card bindings require an exact HTTPS `page_url` and the card fields
852+
can resolve a unique page. Cards require an exact HTTPS `page_url`. Follow the item's advertised
853+
`fill` description for the rest: a Link Pay Token omits `fields`; other cards use the card fields
819854
listed below. The vault must already be attached to the browser for fill authorization.
820855
Fill writes real stored values into the browser without returning them in the result;
821856
unrestricted browser/CDP access can read those values. It does not explicitly submit forms
@@ -830,10 +865,10 @@ kernel vaults items invoke checkout order-1 fill --params '{"browser_id":"browse
830865
the CLI does not resolve names.
831866
- For cards, `page_url` is the exact current top-level HTTPS URL, including path, query, and fragment,
832867
without embedded credentials. It must match exactly one open page; no prefix/glob matching.
833-
- `fields` contains 1-32 bindings in write order. Each has `field` and a nonempty CSS
868+
- `fields`, when the item's fill description asks for it, contains 1-32 bindings in write order. Each has `field` and a nonempty CSS
834869
`selector` targeting an editable input/select or its container. The API searches the selected
835870
page and descendants, including payment iframes. Do not supply frame IDs or literal values.
836-
- Link stored card fields: `number`, `cvc`, `exp_month` (MM), `exp_year` (YYYY), `billing_name`,
871+
- Link virtual card fields: `number`, `cvc`, `exp_month` (MM), `exp_year` (YYYY), `billing_name`,
837872
`billing_line1`, `billing_line2`, `billing_city`, `billing_state`, `billing_postal_code`,
838873
`billing_country`. Billing fields use the stored address without reformatting; request only
839874
needed fields. Missing requested billing data fails validation before browser writes.

‎cmd/vaults.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,14 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
309309
return fmt.Errorf("confirm_transaction currency must match the approved purchase currency and amount must not exceed the approved purchase amount; operation was not invoked")
310310
}
311311
}
312+
linkCard := item.Type == "card" && item.Spec.Provider == "link"
313+
if params != nil && params.Authorize != nil && !linkCard {
314+
return fmt.Errorf("--browser-id and --page-url only apply to Link cards; authorize was not invoked")
315+
}
312316
if operation == "fill" {
317+
if !linkCard && len(params.Fill.Fields) == 0 {
318+
return fmt.Errorf("fill for this item requires 1-32 fields binding field names to selectors; only a Link card's fill may omit them. fill was not invoked")
319+
}
313320
return c.fill(ctx, vault, key, params.Fill, output, item.Spec.Provider)
314321
}
315322
if operation == "1pw_fill" {
@@ -337,6 +344,8 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
337344
request.OfPrepareCheckout = &kernel.PrepareCheckoutVaultItemOperationRequestParam{Type: "prepare_checkout", Checkout: *params.Checkout}
338345
} else if operation == "collect" {
339346
request.OfCollect = &kernel.CollectVaultItemOperationRequestParam{Type: "collect"}
347+
} else if params != nil && params.Authorize != nil {
348+
request.OfAuthorize = params.Authorize
340349
} else {
341350
// Preserve support for other advertised parameterless operations.
342351
request.OfAuthorize = &kernel.AuthorizeVaultItemOperationRequestParam{Type: kernel.AuthorizeVaultItemOperationRequestType(operation)}
@@ -352,6 +361,9 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
352361
if item.Spec.Provider == "kernel" {
353362
return kernelVaultRequestError(err, operation)
354363
}
364+
if operation == "authorize" && linkCard {
365+
return linkAuthorizeError(err, params != nil && params.Authorize != nil)
366+
}
355367
return util.CleanedUpSdkError{Err: err}
356368
}
357369
if response == nil || (response.Type != "card" && response.Type != "wallet" && response.Type != "credential" && response.Type != "credential_account") {

0 commit comments

Comments
 (0)