Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions cmd/vaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,10 @@ func (c VaultsCmd) CreateWallet(ctx context.Context, vault, key string, spec ker
return c.showItem(item, output, open)
}

func (c VaultsCmd) SaveCard(ctx context.Context, vault, key string, spec kernel.CardVaultItemSpecUnionParam, update bool, output string) error {
var item *kernel.VaultItemUnion
var err error
if update {
item, err = c.vaults.Items.Update(ctx, key, kernel.VaultItemUpdateParams{IDOrName: vault, OfCardVaultItemUpdateRequest: &kernel.VaultItemUpdateParamsBodyCardVaultItemUpdateRequest{Type: "card", Spec: spec}}, option.WithMaxRetries(0))
} else {
item, err = c.vaults.Items.Upsert(ctx, key, kernel.VaultItemUpsertParams{IDOrName: vault, OfCard: &kernel.VaultItemUpsertParamsBodyCard{Spec: spec}}, option.WithMaxRetries(0))
}
func (c VaultsCmd) SaveCard(ctx context.Context, vault, key string, spec kernel.CardVaultItemSpecUnionParam, output string) error {
item, err := c.vaults.Items.Upsert(ctx, key, kernel.VaultItemUpsertParams{IDOrName: vault, OfCard: &kernel.VaultItemUpsertParamsBodyCard{Spec: spec}}, option.WithMaxRetries(0))
if err != nil {
return util.CleanedUpSdkError{Err: err}
return vaultCardError(err)
}
return c.showItem(item, output, false)
}
Expand Down Expand Up @@ -243,6 +237,12 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
return fmt.Errorf("operation %q is not advertised in available_operations; inspect the item", operation)
}
if operation == "fill" {
if item.Type == "credential" && len(params.Fill.Fields) == 0 {
return fmt.Errorf("credential fill requires 1-32 field bindings")
}
if item.Type == "card" && params.Fill.PageURL == "" {
return fmt.Errorf("card fill requires page_url")
}
return c.fill(ctx, vault, key, params.Fill, output)
}
request := kernel.VaultItemPerformOperationParams{IDOrName: vault}
Expand All @@ -254,8 +254,7 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, par
} else if operation == "collect" {
request.OfCollect = &kernel.CollectVaultItemOperationRequestParam{Type: "collect"}
} else {
// Preserve support for other advertised parameterless operations.
request.OfAuthorize = &kernel.AuthorizeVaultItemOperationRequestParam{Type: kernel.AuthorizeVaultItemOperationRequestType(operation)}
return fmt.Errorf("unsupported vault item operation %q", operation)
}
response, err := c.vaults.Items.PerformOperation(ctx, key, request, option.WithMaxRetries(0))
if err != nil {
Expand Down
85 changes: 40 additions & 45 deletions cmd/vaults_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Use wallet and card item types for credit cards and payment checkout instead.

User credential flow:
1. Create a vault per end user and create a browser with --vault <id-or-name>.
2. Navigate to a sensitive form and define its fields in natural top-to-bottom order with credentials create --spec-file; that array order controls the user-facing collection form.
2. Navigate to a sensitive form and define its fields with credentials create --spec-file.
3. Present the returned collection URL to the user. Poll items get --wait 60 for ready.
4. Use items invoke <vault> <key> fill --spec-file with browser_id and field selectors.
Use credentials update --version for edits, or items invoke collect to reopen the form.
Expand All @@ -73,16 +73,18 @@ Otherwise, the API resolves the project from your credentials and its defaults.
Vault names, item keys, and project ownership are immutable.

1. Create/select a vault, then create a provider wallet and follow its returned action.
2. For Link, list wallet payment methods and select an ID explicitly.
3. Create a card request with --provider and --spec JSON.
4. Inspect items get, then use items invoke <vault> <key> <operation> only when advertised.
Follow the operation description and any returned provider action.
5. Attach the vault with browsers create --vault <id-or-name>; attachment is required for fill.
Ready Link cards use only advertised fill with --params for browser checkout.
Link cards do not expose aliases or support egress substitution.
AgentCard-only checkout aliases support egress substitution with checkout hold,
approval, and replay; they are not a fallback after fill.
Inspect items get/events for payment outcomes.
2. For Link, list wallet payment methods and select an ID explicitly. Create a browser
with --vault <id-or-name>, navigate to final checkout, and gather final spend details.
3. Create one Link card with that browser ID and exact page URL. Kernel inspects the
checkout and internally selects a Link payment token or virtual card. Creation starts approval.
4. Share the returned approval URL and retrieve the item until fill is advertised.
5. Invoke fill with the parameters described by the advertised operation; browser-vault
attachment is required for fill. Ready Link cards use only advertised fill. Link cards
do not expose aliases or support egress substitution. AgentCard-only checkout aliases
support egress substitution with checkout hold, approval, and replay.
6. Fill never submits payment; inspect the checkout and submit separately when ready.
Virtual-card selection is creation-time fallback, not a fallback after fill. Inspect
items get/events for the outcome.

Permitted checkout domains are provider-assigned and displayed when returned;
there is no domain-setting API.
Expand Down Expand Up @@ -153,10 +155,11 @@ JSON output preserves returned public fields but omits unknown/opaque provider d
invoke := &cobra.Command{Use: "invoke <vault> <key> <operation>", Short: "Invoke an operation advertised by an item", Args: cobra.ExactArgs(3), PreRunE: vaultPreRun,
Long: `Retrieve the item and invoke only an operation listed in available_operations.
collect returns a time-scoped URL for the full credential form without clearing values.
authorize sends {"type":"authorize"} for payment authorization.
Read the operation description and follow any approval requirements before invoking.
fill requires --params JSON or --spec-file <path|-> with browser_id (session ID, not name)
and 1-32 ordered fields (field, selector). Do not include type, values, or frame IDs.
Each operation's description lists the inputs that item needs; read it before invoking.
fill requires --params JSON or --spec-file <path|-> with browser_id (session ID, not name).
Credentials require 1-32 ordered fields (field, selector). Link cards require an exact
page_url; include fields only when the item's advertised fill description asks for them.
Do not include type, values, or frame IDs.
The vault must already be attached to the browser. page_url selects an existing page;
fill never navigates. Credentials use declared field names, must omit format, and may
omit page_url only when the API can resolve a unique page. TOTP codes stay server-generated.
Expand All @@ -169,23 +172,18 @@ Fill is available for credential items and ready Link cards when advertised, not
Link cards do not expose aliases or support egress substitution.
Fill writes real values into the browser; unrestricted browser/CDP access can read them.
Fill never explicitly submits forms or clicks buttons, but input/change events may trigger site behavior.
completed means fields were filled, not website acceptance, login, or payment success.
completed means credentials were supplied, not website acceptance, login, or payment success.
failed may leave partial writes; unknown quarantines the browser. Never automatically
retry or fall back to aliases. Requests are not automatically retried.
API validation errors (400/403/404/409) include HTTP status, recognized error codes,
and corrective guidance; no fields were written by that request. Inspect and correct
the cause before deciding on a new fill. Transport loss remains an uncertain outcome.
prepare_checkout requires checkout.browser_id, checkout.merchant_origin (canonical HTTPS
origin of the top-level merchant page, not a processor iframe), and checkout.environment
(production, sandbox, or shared). Optional checkout.psp selects the tokenization processor:
square, braintree, worldpay, bambora, or mercado_pago. Omit psp for Square; non-Square
processors require multi-processor preparation enablement. Use production or sandbox for
square, braintree and worldpay; shared for bambora and mercado_pago. Shared endpoints do not
establish test mode; merchant credentials determine it.
origin of the top-level merchant page), and checkout.environment (production or sandbox).
Use only when advertised for an AgentCard card. Keep the returned approval page open,
poll until ready_to_submit, then submit native Pay before preparation.expires_at.
Preparations are single-use, including after failure or expiry; never retry automatically.
collect/authorize/prepare_checkout may use --open. Fill returns value-free per-field outcomes;
collect/prepare_checkout may use --open. Fill returns value-free per-field outcomes;
completed exits 0, failed/unknown exit nonzero with valid JSON retained on stdout in -o json.`,
Example: ` kernel vaults items invoke user-vault login collect
kernel vaults items invoke user-vault login fill --spec-file - <<'JSON'
Expand Down Expand Up @@ -256,8 +254,8 @@ JSON
addVaultJSONOutputFlag(methods)
wallets.AddCommand(walletCreate, methods)

cards := &cobra.Command{Use: "cards", Short: "Configure card requests"}
cards.AddCommand(newVaultCardCommand(false), newVaultCardCommand(true))
cards := &cobra.Command{Use: "cards", Short: "Create immutable card requests at final checkout"}
cards.AddCommand(newVaultCardCommand())
cmd.AddCommand(items, wallets, cards, newVaultCredentialsCommand())
return cmd
}
Expand All @@ -280,34 +278,26 @@ func newVaultDeleteCommand(item bool) *cobra.Command {
return cmd
}

func newVaultCardCommand(update bool) *cobra.Command {
use, short := "create", "Create a card request without authorizing it"
if update {
use, short = "update", "Update a card spec when the API permits configuration"
}
cmd := &cobra.Command{Use: use + " <vault> <key> --provider <link|agentcard> --spec '<json>'", Short: short, Args: cobra.ExactArgs(2), PreRunE: vaultPreRun,
Long: short + `. Neither create nor update authorizes a Link card.
Requested cards accept a replacement spec. Pending issuance updates preserve omitted
optional fields; explicit empty lists clear them. The API restricts fields after
authorization starts; wallet/provider bindings cannot change. An uncertain update
enters recovery_required and must not be retried. Checkout cards can be edited
between authorizations. Identical creates return existing state without resetting it.
Never reconfigure the same item to retry a failed, timed-out, rejected, or indeterminate payment.
A recovery item that permits abandonment must be deleted after explicit user confirmation before creating a replacement.
` + vaultSpecHelp + vaultCardSpecHelp,
Example: " kernel vaults cards " + use + ` checkout order-1 \
--provider agentcard --spec '{
func newVaultCardCommand() *cobra.Command {
cmd := &cobra.Command{Use: "create <vault> <key> --provider <link|agentcard> --spec '<json>'", Short: "Create an immutable card request and start approval", Args: cobra.ExactArgs(2), PreRunE: vaultPreRun,
Long: "Create a card after reaching final checkout. For Link, Kernel inspects the checkout and internally selects a Link payment token or virtual card. Creation starts human approval; share the returned URL and retrieve the item until fill appears.\n" + vaultSpecHelp + vaultCardSpecHelp + vaultLinkPurchaseTypesHelp,
Example: " kernel vaults cards create" + ` checkout order-1 \
--provider link --spec '{
"wallet": "wallet-1",
"merchant": "Example Shop",
"browser_id": "browser-session-id",
"page_url": "https://shop.example/checkout",
"payment_method_id": "pm-1",
"amount": 1234,
"currency": "usd"
"currency": "usd",
"merchant_name": "Example Shop",
"context": "Final checkout for one item totaling USD 12.34. This is a new purchase and not a retry of an uncertain payment."
}'`,
RunE: func(cmd *cobra.Command, args []string) error {
spec, err := vaultSpecFromFlags(cmd)
if err != nil {
return err
}
return getVaultsHandler(cmd).SaveCard(cmd.Context(), args[0], args[1], param.Override[kernel.CardVaultItemSpecUnionParam](spec), update, vaultOutput(cmd))
return getVaultsHandler(cmd).SaveCard(cmd.Context(), args[0], args[1], param.Override[kernel.CardVaultItemSpecUnionParam](spec), vaultOutput(cmd))
}}
addVaultSpecFlags(cmd)
addVaultJSONOutputFlag(cmd)
Expand All @@ -331,6 +321,11 @@ func vaultSpecFromFlags(cmd *cobra.Command) (map[string]json.RawMessage, error)
if err := json.Unmarshal([]byte(raw), &spec); err != nil || spec == nil {
return nil, fmt.Errorf("--spec must be a JSON object")
}
if provider == "link" {
if _, exists := spec["merchant_account_id"]; exists {
return nil, fmt.Errorf("omit merchant_account_id; Kernel discovers it from the checkout")
}
}
if value, ok := spec["provider"]; ok {
var embedded string
if err := json.Unmarshal(value, &embedded); err != nil || embedded != provider {
Expand Down
17 changes: 10 additions & 7 deletions cmd/vaults_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,18 @@ func (c VaultsCmd) saveCredential(ctx context.Context, vault, key string, data [
if version < 1 {
return fmt.Errorf("--version must be positive")
}
var spec kernel.CredentialVaultItemSpecUpdateParam
if json.Unmarshal(data, &spec) != nil {
return fmt.Errorf("invalid credential update spec")
}
request := kernel.CredentialVaultItemUpdateRequestParam{Type: "credential", Version: version, Spec: spec}
// The preview SDK for payment tokens omits the item update method; send the
// credential PATCH through the generic client until it is regenerated.
request := map[string]any{"type": "credential", "version": version, "spec": json.RawMessage(data)}
if expectedID != "" {
request.ExpectedItemID = kernel.String(expectedID)
request["expected_item_id"] = expectedID
}
body, marshalErr := json.Marshal(request)
if marshalErr != nil {
return fmt.Errorf("invalid credential update spec")
}
item, err = c.vaults.Items.Update(ctx, key, kernel.VaultItemUpdateParams{IDOrName: vault, OfCredentialVaultItemUpdateRequest: &request}, option.WithMaxRetries(0))
client := kernel.Client{Options: c.vaults.Items.Options}
err = client.Patch(ctx, fmt.Sprintf("vaults/%s/items/%s", vault, key), nil, &item, option.WithRequestBody("application/json", body), option.WithMaxRetries(0))
} else {
var spec kernel.CredentialVaultItemSpecInputParam
if json.Unmarshal(data, &spec) != nil || len(spec.Fields) == 0 {
Expand Down
22 changes: 16 additions & 6 deletions cmd/vaults_fill.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var vaultFillResultFields = vaultOutputFields{
"fields": vaultFieldsOf("index status error_code"),
}

const vaultFillUncertain = "browser fields may have been written; inspect the browser and do not retry or fall back to aliases"
const vaultFillUncertain = "browser fields may have been written or a payment credential may have been supplied; inspect the browser and do not retry or fall back to aliases"

var vaultFillErrorMessages = map[string]string{
"invalid_request": "check field names, formats, and browser parameters",
Expand All @@ -46,6 +46,7 @@ var vaultFillErrorMessages = map[string]string{
"field_unavailable": "a field has no usable stored value; inspect definitions and presence, and collect missing values",
"conflict": "the item or browser is not ready; inspect readiness, binding, and unresolved prior operations",
"destination_denied": "destination or browser vault binding is not authorized; check the bound browser and destination",
"browser_unavailable": "the browser session is not available; check that it is still running",
"not_found": "check the vault, item, browser identifiers, and project",
"execution_failed": "fill execution failed",
}
Expand Down Expand Up @@ -77,7 +78,6 @@ func (c VaultsCmd) fill(ctx context.Context, vault, key string, params *vaultFil
request := kernel.FillVaultItemOperationRequestParam{
BrowserID: params.BrowserID,
Type: kernel.FillVaultItemOperationRequestTypeFill,
Fields: make([]kernel.VaultFillFieldParam, 0, len(params.Fields)),
}
if params.PageURL != "" {
request.PageURL = kernel.Opt(params.PageURL)
Expand Down Expand Up @@ -109,11 +109,13 @@ func (c VaultsCmd) fill(ctx context.Context, vault, key string, params *vaultFil
}
} else {
pterm.Printf("Fill: %s\n", result.Status)
rows := pterm.TableData{{"Field index", "Status", "Error code"}}
for _, field := range result.Fields {
rows = append(rows, []string{strconv.Itoa(*field.Index), field.Status, field.ErrorCode})
if len(result.Fields) > 0 {
rows := pterm.TableData{{"Field index", "Status", "Error code"}}
for _, field := range result.Fields {
rows = append(rows, []string{strconv.Itoa(*field.Index), field.Status, field.ErrorCode})
}
PrintTableNoPad(rows, true)
}
PrintTableNoPad(rows, true)
if result.Status == "completed" {
pterm.Println("Fields filled; this does not confirm website acceptance or form submission.")
} else {
Expand Down Expand Up @@ -142,6 +144,14 @@ func parseVaultFillResult(raw json.RawMessage, count int) (*vaultFillResult, err
if json.Unmarshal(safe, &result) != nil || result.Type != "fill" || len(result.Fields) != count {
return nil, invalid
}
if count == 0 {
switch result.Status {
case "completed", "failed", "unknown":
return &result, nil
default:
return nil, invalid
}
}
status := "completed"
stopped := false
for i, field := range result.Fields {
Expand Down
Loading
Loading