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
49 changes: 33 additions & 16 deletions cmd/vaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package cmd

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"time"

"github.com/kernel/cli/pkg/interactive"
Expand Down Expand Up @@ -136,13 +136,13 @@ func (c VaultsCmd) ListItems(ctx context.Context, vault, output string) error {
pterm.Info.Println("No vault items found")
return nil
}
rows := pterm.TableData{{"Key", "Type", "Provider", "Status", "Action"}}
rows := pterm.TableData{{"Key", "Type", "Provider", "Status", "Action", "Description"}}
for _, item := range *items {
actions, err := effectiveVaultItemActions(&item)
if err != nil {
return err
}
rows = append(rows, []string{item.Key, item.Type, item.Spec.Provider, item.State.Status, util.OrDash(actions.RequiredAction)})
rows = append(rows, []string{item.Key, item.Type, item.Spec.Provider, item.State.Status, util.OrDash(actions.RequiredAction), vaultItemDescription(&item)})
}
PrintTableNoPad(rows, true)
return nil
Expand Down Expand Up @@ -187,23 +187,30 @@ 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, 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 c.showItem(item, output, false)
}

func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation, output string, open bool) error {
if strings.TrimSpace(operation) == "" {
return fmt.Errorf("operation must not be empty")
func (c VaultsCmd) CreatePaymentToken(ctx context.Context, vault, key string, spec map[string]json.RawMessage, output string) error {
body, err := json.Marshal(map[string]any{"type": "payment_token", "spec": spec})
if err != nil {
return err
}
var item kernel.VaultItemUnion
_, err = c.vaults.Items.Upsert(ctx, key, kernel.VaultItemUpsertParams{IDOrName: vault}, option.WithRequestBody("application/json", body), option.WithResponseBodyInto(&item), option.WithMaxRetries(0))
if err != nil {
return vaultPaymentTokenError(err)
}
return c.showItem(&item, output, false)
}

func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation string, spec map[string]json.RawMessage, output string) error {
if operation != "fill" {
return fmt.Errorf("operation must be fill")
}
item, err := c.vaults.Items.Get(ctx, key, kernel.VaultItemGetParams{IDOrName: vault}, option.WithMaxRetries(0))
if err != nil {
Expand All @@ -229,11 +236,21 @@ func (c VaultsCmd) Invoke(ctx context.Context, vault, key, operation, output str
if !available {
return fmt.Errorf("operation %q is not advertised in available_operations; inspect the item", operation)
}
item, err = c.vaults.Items.PerformOperation(ctx, key, kernel.VaultItemPerformOperationParams{IDOrName: vault, Type: kernel.VaultItemPerformOperationParamsType(operation)}, option.WithMaxRetries(0))
body := make(map[string]any, len(spec)+1)
body["type"] = operation
for name, value := range spec {
body[name] = value
}
encoded, err := json.Marshal(body)
if err != nil {
return err
}
var result vaultFillOperationResult
_, err = c.vaults.Items.PerformOperation(ctx, key, kernel.VaultItemPerformOperationParams{IDOrName: vault, Type: kernel.VaultItemPerformOperationParamsType("fill")}, option.WithRequestBody("application/json", encoded), option.WithResponseBodyInto(&result), option.WithMaxRetries(0))
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
return c.showItem(item, output, open)
return printVaultOperationResult(result, output)
}

func (c VaultsCmd) Events(ctx context.Context, vault, key, after string, wait int64, output string) error {
Expand Down
118 changes: 87 additions & 31 deletions cmd/vaults_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ 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>. Use only returned
non-secret aliases in that browser. Inspect items get/events for the outcome.
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. Try payment-tokens create with that browser ID and exact page URL. Only
lpt_not_supported means create a card instead. Credential PUT starts human approval.
4. Share the returned approval URL and retrieve the item until fill is advertised.
5. Invoke fill with its browser/page parameters. Fill never submits payment; inspect the
checkout and submit separately when ready. 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 @@ -131,14 +132,19 @@ JSON output preserves returned public fields but omits unknown/opaque provider d
itemEvents.Flags().String("after", "", "Return events after this event ID (use the last ID from the previous response)")
itemEvents.Flags().Int64("wait", 0, "Long-poll once for new events (0-60 seconds)")
addVaultJSONOutputFlag(itemEvents)
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.\nRead its description with items get before invoking; follow any approval requirements.\nThe API determines availability regardless of item type, provider, or state.\nRequests are not automatically retried. The updated item may contain a required user action.\nThe current API accepts only {\"type\":\"authorize\"}; there are no operation parameters or --spec flag.",
Example: " kernel vaults items get checkout order-1\n kernel vaults items invoke checkout order-1 authorize",
invoke := &cobra.Command{Use: "invoke <vault> <key> fill --spec '<json>'", Short: "Fill an approved credential without submitting payment", Args: cobra.ExactArgs(3), PreRunE: vaultPreRun,
Long: "Retrieve the item and invoke fill only when listed in available_operations.\n--spec is the fill parameters object without type: browser_id, page_url, and card fields when required.\nPayment tokens discover their hidden provider field and reject caller-supplied fields.\nA completed fill supplies credentials but never clicks Pay or submits the purchase.\nFailed and unknown outcomes are not automatically retried.",
Example: ` kernel vaults items get checkout order-1
kernel vaults items invoke checkout order-1 fill --spec '{"browser_id":"browser-session-id","page_url":"https://shop.example/checkout","fields":[{"field":"number","selector":"#card-number"}]}'`,
RunE: func(cmd *cobra.Command, args []string) error {
open, _ := cmd.Flags().GetBool("open")
return getVaultsHandler(cmd).Invoke(cmd.Context(), args[0], args[1], args[2], vaultOutput(cmd), open)
spec, err := vaultOperationSpec(cmd)
if err != nil {
return err
}
return getVaultsHandler(cmd).Invoke(cmd.Context(), args[0], args[1], args[2], spec, vaultOutput(cmd))
}}
invoke.Flags().Bool("open", false, "Open a returned HTTPS action URL in your browser")
invoke.Flags().String("spec", "", "Raw fill parameters JSON without the type field (required)")
_ = invoke.MarkFlagRequired("spec")
addVaultJSONOutputFlag(invoke)
items.AddCommand(itemList, itemGet, itemEvents, invoke, newVaultDeleteCommand(true))

Expand Down Expand Up @@ -179,9 +185,11 @@ JSON output preserves returned public fields but omits unknown/opaque provider d
addVaultJSONOutputFlag(methods)
wallets.AddCommand(walletCreate, methods)

cards := &cobra.Command{Use: "cards", Short: "Configure card requests"}
cards.AddCommand(newVaultCardCommand(false), newVaultCardCommand(true))
cmd.AddCommand(items, wallets, cards)
cards := &cobra.Command{Use: "cards", Short: "Create immutable card requests at final checkout"}
cards.AddCommand(newVaultCardCommand())
paymentTokens := &cobra.Command{Use: "payment-tokens", Short: "Create merchant-bound Link payment tokens at final checkout"}
paymentTokens.AddCommand(newVaultPaymentTokenCommand())
cmd.AddCommand(items, wallets, cards, paymentTokens)
return cmd
}

Expand All @@ -203,21 +211,10 @@ 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 to retry a failed, timed-out, rejected, or indeterminate payment.
` + vaultSpecHelp + vaultCardSpecHelp,
Example: " kernel vaults cards " + use + ` checkout order-1 \
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. Link 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 agentcard --spec '{
"wallet": "wallet-1",
"merchant": "Example Shop",
Expand All @@ -229,13 +226,72 @@ Never reconfigure to retry a failed, timed-out, rejected, or indeterminate payme
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)
return cmd
}

func newVaultPaymentTokenCommand() *cobra.Command {
cmd := &cobra.Command{Use: "create <vault> <key> --spec '<json>'", Short: "Create an immutable Link payment token and start approval", Args: cobra.ExactArgs(2), PreRunE: vaultPreRun,
Long: "Create a payment token from the active final checkout. Kernel discovers Link support and the Stripe merchant binding from the browser.\n" + vaultSpecHelp + vaultPaymentTokenSpecHelp + vaultLinkPurchaseTypesHelp,
Example: ` kernel vaults payment-tokens create checkout order-1 --spec '{
"wallet":"wallet-1",
"browser_id":"browser-session-id",
"page_url":"https://shop.example/checkout",
"payment_method_id":"pm-1",
"amount":1234,
"currency":"usd",
"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 := vaultPaymentTokenSpecFromFlags(cmd)
if err != nil {
return err
}
return getVaultsHandler(cmd).CreatePaymentToken(cmd.Context(), args[0], args[1], spec, vaultOutput(cmd))
}}
cmd.Flags().String("spec", "", "Raw Link payment-token specification object (required)")
_ = cmd.MarkFlagRequired("spec")
addVaultJSONOutputFlag(cmd)
return cmd
}

func vaultPaymentTokenSpecFromFlags(cmd *cobra.Command) (map[string]json.RawMessage, error) {
raw, _ := cmd.Flags().GetString("spec")
var spec map[string]json.RawMessage
if err := json.Unmarshal([]byte(raw), &spec); err != nil || spec == nil {
return nil, fmt.Errorf("--spec must be a JSON object")
}
if _, exists := spec["provider"]; exists {
return nil, fmt.Errorf("payment-token provider is Link; omit spec.provider")
}
if _, exists := spec["merchant_account_id"]; exists {
return nil, fmt.Errorf("omit merchant_account_id; Kernel discovers it from the checkout")
}
if vaultSpecHasSecrets(json.RawMessage(raw)) {
return nil, fmt.Errorf("--spec must not contain credentials or tokens")
}
spec["provider"] = json.RawMessage(`"link"`)
return spec, nil
}

func vaultOperationSpec(cmd *cobra.Command) (map[string]json.RawMessage, error) {
raw, _ := cmd.Flags().GetString("spec")
var spec map[string]json.RawMessage
if err := json.Unmarshal([]byte(raw), &spec); err != nil || spec == nil {
return nil, fmt.Errorf("--spec must be a JSON object")
}
if _, exists := spec["type"]; exists {
return nil, fmt.Errorf("omit type from --spec; it comes from the operation argument")
}
if vaultSpecHasSecrets(json.RawMessage(raw)) {
return nil, fmt.Errorf("--spec must not contain credential values or tokens")
}
return spec, nil
}

func addVaultSpecFlags(cmd *cobra.Command) {
cmd.Flags().String("provider", "", "Provider: link or agentcard (required)")
cmd.Flags().String("spec", "", "Raw JSON specification object (required); see types and examples above")
Expand Down
32 changes: 31 additions & 1 deletion cmd/vaults_help.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ type AgentCardWalletSpec = {
`

const vaultCardSpecHelp = `
Create the card only after reaching final checkout and gathering the final spend details.
Creation starts human approval. Card requests are immutable; changed purchase details
require cancellation and a new item. Never replace an uncertain payment.

type LinkCardSpec = {
provider: "link";
wallet: string; // wallet item key
Expand All @@ -72,6 +76,10 @@ type AgentCardCardSpec = {
card_id?: string; // vc_...; otherwise chosen at approval
};

Permitted domains are provider-assigned, not configurable in the spec.
`

const vaultLinkPurchaseTypesHelp = `
type LinkLineItem = {
name: string;
quantity?: number; // integer >= 1
Expand All @@ -89,6 +97,28 @@ type LinkTotal = {
display_text: string;
amount: number; // integer minor units
};
`

Permitted domains are provider-assigned, not configurable in the spec.
const vaultPaymentTokenSpecHelp = `
Create a Link payment token only after reaching final checkout. Kernel inspects the
vault-linked browser, reveals Link's agent controls, and binds the request to the
observed Stripe merchant. Do not inspect hidden controls or provide merchant_account_id.
If creation returns lpt_not_supported, create a card instead. No other error is a
fallback signal. Creation starts human approval and requests are immutable.
After approval, invoke fill; filling authenticates the checkout but does not submit it.

type LinkPaymentTokenSpec = {
provider: "link";
wallet: string; // connected wallet item key
browser_id: string; // active vault-linked browser session ID
page_url: string; // exact final checkout page URL
payment_method_id: string; // from wallets payment-methods
amount: number; // integer minor units; 1..500000
currency: string; // three letters
context: string; // at least 100 characters
line_items?: LinkLineItem[];
totals?: LinkTotal[];
metadata?: Record<string, string>;
expires_at?: number; // int64
};
`
Loading
Loading