diff --git a/README.md b/README.md index eb863c84..bef93f6d 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ Commands with JSON output support: - **Proxies**: `create`, `list`, `get`, `update`, `check` - **API Keys**: `create`, `list`, `get`, `update`, `rotate` - **Auth Connections**: `timeline` -- **Vaults**: `create`, `list`, `get`, `credentials create/update`, `items list/get/events/invoke` (including `collect`, `fill`, and `prepare_checkout`), `wallets create/payment-methods`, `cards create/update` (display-safe public fields only) +- **Vaults**: `create`, `list`, `get`, `credentials create/update`, `items list/get/events/invoke` (including `collect`, `fill`, `webmcp_invoke`, and `prepare_checkout`), `wallets create/payment-methods`, `cards create/update` (display-safe public fields only) - **Projects**: `update` - **Org**: `limits get/set` - **Apps**: `list`, `history` @@ -383,6 +383,17 @@ text/email values, definitions, version, and `has_value`. Sensitive values and T seeds are omitted. Credential spec input is capped at 128 KiB; write errors are redacted. +To reuse a managed auth connection's saved credential, create a credential with +provider `managed_auth` and the connection ID from `kernel auth connections list`. +The item stores no values and reads the connection's credential at fill time; it is +created `ready`, `state.fields` lists fill binding names, and `update` returns 409: + +```sh +kernel vaults credentials create user-vault amazon --spec-file - <<'JSON' +{"provider":"managed_auth","connection_id":"","description":"Amazon"} +JSON +``` + Vault names, item keys, and project ownership are immutable. Optionally select a project with `--project ` or `KERNEL_PROJECT`; otherwise, the API resolves the project from your credentials and its defaults (the default project for org-wide credentials, not all projects). @@ -394,7 +405,7 @@ cannot switch projects. | Command | Purpose / flags | | --- | --- | | `kernel vaults create --name ` | Create or retrieve the vault with that immutable name | -| `kernel vaults list` | `--limit 1..100` (default 20), `--offset`; JSON includes `vaults` and optional `next_offset` | +| `kernel vaults list` | `--limit 1..100` (default 20), `--offset`, `--query` (name substring or exact ID); JSON includes `vaults` and optional `next_offset` | | `kernel vaults get ` | Get by ID or name | | `kernel vaults delete ` | Invalidate the vault and all its items; `--yes` skips confirmation | | `kernel vaults wallets create --provider link\|agentcard --spec ''` | Connect/enroll a wallet using its provider's spec; `--open` opens a returned HTTPS action URL | diff --git a/cmd/credentials.go b/cmd/credentials.go index 3c7d1564..bf982534 100644 --- a/cmd/credentials.go +++ b/cmd/credentials.go @@ -173,10 +173,13 @@ func (c CredentialsCmd) Get(ctx context.Context, in CredentialsGetInput) error { {"Name", cred.Name}, {"Domain", cred.Domain}, {"Has TOTP Secret", hasTOTP}, + } + tableData = append(tableData, credentialTotpRows(cred)...) + tableData = append(tableData, pterm.TableData{ {"SSO Provider", ssoProvider}, {"Created At", util.FormatLocal(cred.CreatedAt)}, {"Updated At", util.FormatLocal(cred.UpdatedAt)}, - } + }...) PrintTableNoPad(tableData, true) return nil @@ -276,8 +279,9 @@ func (c CredentialsCmd) Create(ctx context.Context, in CredentialsCreateInput) e {"Name", cred.Name}, {"Domain", cred.Domain}, {"Has TOTP Secret", hasTOTP}, - {"SSO Provider", ssoProvider}, } + tableData = append(tableData, credentialTotpRows(cred)...) + tableData = append(tableData, []string{"SSO Provider", ssoProvider}) PrintTableNoPad(tableData, true) @@ -289,6 +293,36 @@ func (c CredentialsCmd) Create(ctx context.Context, in CredentialsCreateInput) e return nil } +// normalizeTotpAlgorithm validates a TOTP HMAC algorithm and returns its +// canonical upper-case form (SHA1, SHA256, or SHA512). +func normalizeTotpAlgorithm(algorithm string) (string, error) { + normalized := strings.ToUpper(strings.TrimSpace(algorithm)) + switch normalized { + case "SHA1", "SHA256", "SHA512": + return normalized, nil + default: + return "", fmt.Errorf("invalid --totp-algorithm %q (must be one of SHA1, SHA256, SHA512)", algorithm) + } +} + +// credentialTotpRows returns TOTP metadata rows for credentials with a TOTP secret. +func credentialTotpRows(cred *kernel.Credential) pterm.TableData { + if !cred.HasTotpSecret { + return nil + } + rows := pterm.TableData{} + if cred.TotpAlgorithm != "" { + rows = append(rows, []string{"TOTP Algorithm", string(cred.TotpAlgorithm)}) + } + if cred.TotpDigits > 0 { + rows = append(rows, []string{"TOTP Digits", fmt.Sprintf("%d", cred.TotpDigits)}) + } + if cred.TotpPeriod > 0 { + rows = append(rows, []string{"TOTP Period", fmt.Sprintf("%ds", cred.TotpPeriod)}) + } + return rows +} + func (c CredentialsCmd) Update(ctx context.Context, in CredentialsUpdateInput) error { if err := validateJSONOutput(in.Output); err != nil { return err @@ -427,6 +461,9 @@ Examples: # Create a credential with TOTP for 2FA kernel credentials create --name "my-2fa-site" --domain "example.com" --value "username=myuser" --value "password=mypass" --totp-secret "JBSWY3DPEHPK3PXP" + # Create a credential with custom TOTP parameters + kernel credentials create --name "my-8digit-site" --domain "example.com" --value "username=myuser" --totp-secret "JBSWY3DPEHPK3PXP" --totp-algorithm SHA256 --totp-digits 8 --totp-period 60 + # Create a credential with SSO provider kernel credentials create --name "google-sso" --domain "example.com" --value "email=user@gmail.com" --value "password=mypass" --sso-provider google`, Args: cobra.NoArgs, diff --git a/cmd/credentials_test.go b/cmd/credentials_test.go new file mode 100644 index 00000000..a38477b8 --- /dev/null +++ b/cmd/credentials_test.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNormalizeTotpAlgorithm(t *testing.T) { + for input, want := range map[string]string{"SHA1": "SHA1", "sha256": "SHA256", " Sha512 ": "SHA512"} { + got, err := normalizeTotpAlgorithm(input) + require.NoError(t, err) + assert.Equal(t, want, got) + } + + _, err := normalizeTotpAlgorithm("md5") + assert.Error(t, err) +} diff --git a/cmd/logs.go b/cmd/logs.go index 4715eeb1..13fc92bf 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -65,7 +65,12 @@ func runLogs(cmd *cobra.Command, args []string) error { pterm.Info.Println("Showing recent logs (timeout after 3s with no events)") } - stream := client.Invocations.FollowStreaming(cmd.Context(), inv.ID, kernel.InvocationFollowParams{}, option.WithMaxRetries(0)) + // Only forward --since when explicitly set so older invocations still show their full logs + invParams := kernel.InvocationFollowParams{} + if cmd.Flags().Changed("since") { + invParams.Since = kernel.Opt(since) + } + stream := client.Invocations.FollowStreaming(cmd.Context(), inv.ID, invParams, option.WithMaxRetries(0)) if stream.Err() != nil { return fmt.Errorf("failed to follow streaming: %w", stream.Err()) } diff --git a/cmd/offset_pagination_test.go b/cmd/offset_pagination_test.go index fed03c9c..2256aa45 100644 --- a/cmd/offset_pagination_test.go +++ b/cmd/offset_pagination_test.go @@ -72,7 +72,7 @@ func TestOffsetPaginationListCommands(t *testing.T) { case "projects": err = (ProjectsCmd{projects: &client.Projects}).List(context.Background(), ProjectsListInput{Limit: 20, Offset: 20, Output: "json"}) case "vaults": - err = (VaultsCmd{vaults: &client.Vaults}).List(context.Background(), 20, 20, "", "json") + err = (VaultsCmd{vaults: &client.Vaults}).List(context.Background(), 20, 20, "", "", "json") case "vault-provider-configs": err = (VaultProviderConfigsCmd{configs: &client.VaultProviderConfigs}).List(context.Background(), 20, 20, "json") } @@ -87,3 +87,16 @@ func TestOffsetPaginationListCommands(t *testing.T) { } } } + +func TestVaultsListQuery(t *testing.T) { + client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "my vault", r.URL.Query().Get("query")) + w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-Has-More", "true") + w.Header().Set("X-Next-Offset", "20") + _, _ = io.WriteString(w, "[]") + }) + setupStdoutCapture(t) + require.NoError(t, (VaultsCmd{vaults: &client.Vaults}).List(context.Background(), 20, 0, "my vault", "", "table")) + assert.Contains(t, outBuf.String(), `--query "my vault"`) +} diff --git a/cmd/vaults.go b/cmd/vaults.go index 59c4e35f..dbd7968d 100644 --- a/cmd/vaults.go +++ b/cmd/vaults.go @@ -51,12 +51,16 @@ func (c VaultsCmd) Get(ctx context.Context, vault, output string) error { return printVault(v, output) } -func (c VaultsCmd) List(ctx context.Context, limit, offset int64, project, output string) error { +func (c VaultsCmd) List(ctx context.Context, limit, offset int64, query, project, output string) error { if limit < 1 || limit > 100 || offset < 0 { return fmt.Errorf("--limit must be between 1 and 100; --offset must be non-negative") } var response *http.Response - page, err := c.vaults.List(ctx, kernel.VaultListParams{Limit: kernel.Opt(limit), Offset: kernel.Opt(offset)}, option.WithMaxRetries(0), option.WithResponseInto(&response)) + params := kernel.VaultListParams{Limit: kernel.Opt(limit), Offset: kernel.Opt(offset)} + if query != "" { + params.Query = kernel.Opt(query) + } + page, err := c.vaults.List(ctx, params, option.WithMaxRetries(0), option.WithResponseInto(&response)) if err != nil { return util.CleanedUpSdkError{Err: err} } @@ -88,7 +92,11 @@ func (c VaultsCmd) List(ctx context.Context, limit, offset int64, project, outpu if project != "" { projectFlag = fmt.Sprintf(" --project %q", project) } - pterm.Printf("Next: kernel%s vaults list --limit %d --offset %d\n", projectFlag, limit, pagination.NextOffset) + queryFlag := "" + if query != "" { + queryFlag = fmt.Sprintf(" --query %q", query) + } + pterm.Printf("Next: kernel%s vaults list --limit %d --offset %d%s\n", projectFlag, limit, pagination.NextOffset, queryFlag) } return nil } diff --git a/cmd/vaults_commands.go b/cmd/vaults_commands.go index 43c99312..0402b6a1 100644 --- a/cmd/vaults_commands.go +++ b/cmd/vaults_commands.go @@ -118,11 +118,13 @@ JSON output preserves returned public fields but omits unknown/opaque provider d RunE: func(cmd *cobra.Command, args []string) error { limit, _ := cmd.Flags().GetInt64("limit") offset, _ := cmd.Flags().GetInt64("offset") + query, _ := cmd.Flags().GetString("query") project, _ := cmd.Flags().GetString("project") - return getVaultsHandler(cmd).List(cmd.Context(), limit, offset, resolveProjectSelection(project), vaultOutput(cmd)) + return getVaultsHandler(cmd).List(cmd.Context(), limit, offset, query, resolveProjectSelection(project), vaultOutput(cmd)) }} list.Flags().Int64("limit", 20, "Maximum vaults to return (1-100)") list.Flags().Int64("offset", 0, "Number of vaults to skip") + list.Flags().String("query", "", "Case-insensitive substring match against vault name; IDs match by exact value") addVaultJSONOutputFlag(list) get := &cobra.Command{Use: "get ", Short: "Get a vault by ID or name", Args: cobra.ExactArgs(1), PreRunE: vaultPreRun, @@ -273,7 +275,7 @@ JSON items.AddCommand(itemList, itemGet, itemEvents, invoke, newVaultWebMCPCommand(), newVaultDeleteCommand(true)) wallets := &cobra.Command{Use: "wallets", Short: "Connect provider wallets and inspect funding methods"} - walletCreate := &cobra.Command{Use: "create --provider --spec ''", Short: "Create a wallet and display its connection or enrollment action", Args: cobra.ExactArgs(2), PreRunE: vaultPreRun, + walletCreate := &cobra.Command{Use: "create --provider --spec ''", Short: "Create a wallet and display its connection or enrollment action", Args: cobra.ExactArgs(2), PreRunE: vaultPreRun, Long: "Create a wallet at an immutable key and follow the returned provider action.\n" + vaultSpecHelp + vaultWalletSpecHelp, Example: ` kernel vaults wallets create checkout wallet-1 \ --provider link --spec '{ @@ -284,7 +286,10 @@ JSON }' --open kernel vaults wallets create checkout wallet-1 \ - --provider agentcard --spec '{}'`, + --provider agentcard --spec '{}' + + kernel vaults wallets create checkout wallet-2 \ + --provider kernel --spec '{}' --open`, RunE: func(cmd *cobra.Command, args []string) error { spec, err := vaultWalletSpecFromFlags(cmd) if err != nil { @@ -338,13 +343,14 @@ func newVaultCardCommand(update bool) *cobra.Command { if update { use, short = "update", "Update a card spec when the API permits configuration" } - cmd := &cobra.Command{Use: use + " --provider --spec ''", Short: short, Args: cobra.ExactArgs(2), PreRunE: vaultPreRun, - Long: short + `. Neither create nor update authorizes a Link card. + cmd := &cobra.Command{Use: use + " --provider --spec ''", Short: short, Args: cobra.ExactArgs(2), PreRunE: vaultPreRun, + Long: short + `. Neither create nor update authorizes a Link or Kernel 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. +between authorizations. Kernel cards cannot be updated; delete and create a new item. +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, @@ -360,6 +366,9 @@ A recovery item that permits abandonment must be deleted after explicit user con if err != nil { return err } + if provider, _ := cmd.Flags().GetString("provider"); update && provider == "kernel" { + return fmt.Errorf("Kernel cards cannot be updated; delete the item and create a new one") + } return getVaultsHandler(cmd).SaveCard(cmd.Context(), args[0], args[1], param.Override[kernel.CardVaultItemSpecUnionParam](spec), update, vaultOutput(cmd)) }} addVaultSpecFlags(cmd) @@ -368,7 +377,7 @@ A recovery item that permits abandonment must be deleted after explicit user con } func addVaultSpecFlags(cmd *cobra.Command) { - cmd.Flags().String("provider", "", "Provider: link or agentcard (required)") + cmd.Flags().String("provider", "", "Provider: link, agentcard, or kernel (required)") cmd.Flags().String("spec", "", "Raw JSON specification object (required); see types and examples above") _ = cmd.MarkFlagRequired("provider") _ = cmd.MarkFlagRequired("spec") @@ -376,8 +385,8 @@ func addVaultSpecFlags(cmd *cobra.Command) { func vaultSpecFromFlags(cmd *cobra.Command) (map[string]json.RawMessage, error) { provider, _ := cmd.Flags().GetString("provider") - if provider != "link" && provider != "agentcard" { - return nil, fmt.Errorf("--provider must be link or agentcard") + if provider != "link" && provider != "agentcard" && provider != "kernel" { + return nil, fmt.Errorf("--provider must be link, agentcard, or kernel") } raw, _ := cmd.Flags().GetString("spec") var spec map[string]json.RawMessage diff --git a/cmd/vaults_credentials.go b/cmd/vaults_credentials.go index c4a30e51..1ffeaade 100644 --- a/cmd/vaults_credentials.go +++ b/cmd/vaults_credentials.go @@ -79,6 +79,14 @@ and requests instead of account. Supply either account or both secrets, never bo or stdin; they are write-only and never displayed. Never ask an end user for them. Replace the token with items invoke 1pw_update_access_token --spec-file.` +const vaultManagedAuthCredentialHelp = `Managed auth credentials (spec provider "managed_auth"): reference a managed auth +connection in the vault's project that already has a saved Kernel credential, with +connection_id (from auth connections list) and an optional description. The item +stores no values; fill reads the connection's saved credential at fill time, so +managed auth updates apply immediately. Items are created ready; state.fields lists +fill binding names without values. No collection form is offered and update returns +409. Deleting the item leaves the connection and its credential unchanged.` + const vaultCredentialHelp = `Create credentials for a website. ` + vaultCredentialPathsHelp + ` @@ -119,7 +127,9 @@ Collection URLs are bearer credentials: share only with the intended user. ` + vaultOnePasswordCredentialHelp + ` -` + vaultOnePasswordStoredTokenHelp +` + vaultOnePasswordStoredTokenHelp + ` + +` + vaultManagedAuthCredentialHelp func newVaultCredentialsCommand() *cobra.Command { group := &cobra.Command{Use: "credentials", Short: "Collect, update, and fill user credentials", Long: vaultCredentialHelp} @@ -158,6 +168,11 @@ JSON # 1Password brokered approval (account is the connected credential_account key) kernel vaults credentials create user-vault github --spec-file - <<'JSON' {"provider":"1password","account":"onepassword","requests":{"version":2,"entries":[{"type":"login","parameters":{"website":"https://github.com"}}]}} +JSON + + # Managed auth connection with a saved credential + kernel vaults credentials create user-vault amazon --spec-file - <<'JSON' +{"provider":"managed_auth","connection_id":"ma_abc123xyz","description":"Amazon"} JSON` } cmd.Flags().String("spec-file", "", "Credential spec JSON file (use '-' for stdin; maximum 128 KiB)") @@ -293,8 +308,15 @@ func credentialSpecInput(data []byte) (kernel.CredentialVaultItemSpecInputUnionP return kernel.CredentialVaultItemSpecInputUnionParam{}, fmt.Errorf("1Password credential spec requires requests with 1-5 login entries") } return kernel.CredentialVaultItemSpecInputUnionParam{Of1password: &spec}, nil + case "managed_auth": + var spec kernel.ManagedAuthCredentialVaultItemSpecInputParam + if json.Unmarshal(data, &spec) != nil || strings.TrimSpace(spec.ConnectionID) == "" { + return kernel.CredentialVaultItemSpecInputUnionParam{}, fmt.Errorf("managed auth credential spec requires connection_id") + } + spec.Provider = kernel.ManagedAuthCredentialVaultItemSpecInputProviderManagedAuth + return kernel.CredentialVaultItemSpecInputUnionParam{OfManagedAuth: &spec}, nil default: - return kernel.CredentialVaultItemSpecInputUnionParam{}, fmt.Errorf("credential spec provider must be kernel or 1password") + return kernel.CredentialVaultItemSpecInputUnionParam{}, fmt.Errorf("credential spec provider must be kernel, 1password, or managed_auth") } } diff --git a/cmd/vaults_credentials_test.go b/cmd/vaults_credentials_test.go index 0fcaf9a0..e0c26857 100644 --- a/cmd/vaults_credentials_test.go +++ b/cmd/vaults_credentials_test.go @@ -218,5 +218,5 @@ func TestCredentialSpecInputProvider(t *testing.T) { assert.EqualValues(t, "kernel", spec.OfKernel.Provider) _, err = credentialSpecInput([]byte(`{"provider":"bitwarden","fields":[{"name":"password","type":"password"}]}`)) - assert.EqualError(t, err, "credential spec provider must be kernel or 1password") + assert.EqualError(t, err, "credential spec provider must be kernel, 1password, or managed_auth") } diff --git a/cmd/vaults_help.go b/cmd/vaults_help.go index ea948f44..96e82466 100644 --- a/cmd/vaults_help.go +++ b/cmd/vaults_help.go @@ -45,6 +45,13 @@ type AgentCardWalletSpec = { provider_config?: ProviderConfigReference; // omit for Kernel-managed credentials user_id?: string; // usr_...; enrolled in this organization under the SAME config }; + +// Kernel-managed Visa/Mastercard agentic network token enrollment. Creation returns a +// card_enrollment action: the cardholder enters the card on a Kernel-hosted page. +// The card number never reaches Kernel or the CLI. No provider config or tokens. +type KernelWalletSpec = { + provider: "kernel"; +}; ` const vaultCardSpecHelp = ` @@ -73,6 +80,18 @@ type AgentCardCardSpec = { checkout_origin?: string; // top-level checkout origin for autopilot matching; update omission removes it }; +// One live purchase with a Kernel-enrolled card. Authorize obtains a network token and +// one-time code for fill on merchant_url's origin until expires_at. Visa purchases are +// not yet supported (authorize returns 400). Updates are not supported. +type KernelCardSpec = { + provider: "kernel"; + wallet: string; // Kernel wallet item key + amount: number; // integer minor units; 1..50000 + currency: string; // ISO 4217 three letters + merchant_name: string; // 1..255 characters + merchant_url: string; // HTTPS merchant checkout URL; fill is locked to its origin +}; + type LinkLineItem = { name: string; quantity?: number; // integer >= 1 diff --git a/cmd/vaults_managed_auth_test.go b/cmd/vaults_managed_auth_test.go new file mode 100644 index 00000000..5de2da2a --- /dev/null +++ b/cmd/vaults_managed_auth_test.go @@ -0,0 +1,47 @@ +package cmd + +import ( + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const managedAuthCredentialFixture = `{"id":"credential-3","key":"amazon","type":"credential","version":1,"spec":{"provider":"managed_auth","connection_id":"ma_abc123xyz","description":"Amazon"},"state":{"provider":"managed_auth","status":"ready","fields":{"username":{"type":"email"},"password":{"type":"password"}}},"available_operations":[{"type":"fill","description":"Fill the login form."}],"available_expansions":[],"created_at":"2026-09-01T00:00:00Z","updated_at":"2026-09-01T00:00:00Z"}` + +func TestCredentialSpecInputManagedAuth(t *testing.T) { + spec, err := credentialSpecInput([]byte(`{"provider":"managed_auth","connection_id":"ma_abc123xyz","description":"Amazon"}`)) + require.NoError(t, err) + require.NotNil(t, spec.OfManagedAuth) + assert.Equal(t, "ma_abc123xyz", spec.OfManagedAuth.ConnectionID) + assert.Equal(t, "Amazon", spec.OfManagedAuth.Description.Value) + + _, err = credentialSpecInput([]byte(`{"provider":"managed_auth"}`)) + assert.EqualError(t, err, "managed auth credential spec requires connection_id") +} + +func TestCredentialCreateManagedAuth(t *testing.T) { + t.Setenv("KERNEL_PROJECT", "") + client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + assert.Equal(t, "/vaults/user/items/amazon", r.URL.Path) + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.JSONEq(t, `{"type":"credential","spec":{"provider":"managed_auth","connection_id":"ma_abc123xyz","description":"Amazon"}}`, string(body)) + w.Header().Set("Content-Type", "application/json") + io.WriteString(w, managedAuthCredentialFixture) + }) + spec := `{"provider":"managed_auth","connection_id":"ma_abc123xyz","description":"Amazon"}` + out, _, err := executeVaultInputCommand(t, client, spec, "vaults", "credentials", "create", "user", "amazon", "--spec-file", "-", "-o", "json") + require.NoError(t, err) + assert.Contains(t, out, `"connection_id": "ma_abc123xyz"`) + assert.Contains(t, out, `"type": "password"`) + assert.NotContains(t, out, `"has_value"`) + + out, text, err := executeVaultInputCommand(t, client, spec, "vaults", "credentials", "create", "user", "amazon", "--spec-file", "-") + require.NoError(t, err) + assert.Contains(t, out+text, "Managed auth connection (immutable)") + assert.Contains(t, out+text, "ma_abc123xyz") +} diff --git a/cmd/vaults_onepassword_test.go b/cmd/vaults_onepassword_test.go index ec7d6b72..e6f3aef7 100644 --- a/cmd/vaults_onepassword_test.go +++ b/cmd/vaults_onepassword_test.go @@ -79,7 +79,7 @@ func TestCredentialCreateOnePassword(t *testing.T) { {`{"provider":"1password","account":"onepassword"}`, "1-5 login entries"}, {`{"provider":"1password","access_token":"token-secret","integration_key":"key-secret","website":"https://github.com"}`, "1-5 login entries"}, {`{"provider":"1password","account":"onepassword","requests":{"version":2,"entries":[` + strings.Repeat(entry+",", 5) + entry + `]}}`, "1-5 login entries"}, - {`{"provider":"lastpass","fields":[{"name":"password","type":"password"}]}`, "kernel or 1password"}, + {`{"provider":"lastpass","fields":[{"name":"password","type":"password"}]}`, "kernel, 1password, or managed_auth"}, } { _, _, err := executeVaultCommand(t, client, "vaults", "credentials", "create", "user", "github", "--spec-file", credentialSpecFile(t, tc.spec)) require.ErrorContains(t, err, tc.err, tc.spec) diff --git a/cmd/vaults_output.go b/cmd/vaults_output.go index aa5ecff8..09f19569 100644 --- a/cmd/vaults_output.go +++ b/cmd/vaults_output.go @@ -45,7 +45,7 @@ var vaultItemFields = vaultOutputFields{ "spec": { "provider": nil, "wallet": nil, "user_id": nil, "payment_method_id": nil, "card_id": nil, "checkout_origin": nil, "amount": nil, "currency": nil, "merchant": nil, "merchant_name": nil, "merchant_url": nil, - "context": nil, "expires_at": nil, "description": nil, "account": nil, + "context": nil, "expires_at": nil, "description": nil, "account": nil, "connection_id": nil, "requests": onePasswordRequestFields, "fields": vaultFieldsOf("name label type required sensitive"), "provider_config": vaultFieldsOf("id name"), @@ -62,8 +62,8 @@ var vaultItemFields = vaultOutputFields{ "id": nil, "state": nil, "goal": nil, "createdAt": nil, "has_autofill_token": nil, "granted_count": nil, "request": onePasswordRequestFields, "entries": onePasswordRequestEntryFields, }, - "fields": {"*": vaultFieldsOf("has_value")}, - "masks": vaultFieldsOf("brand last4"), + "fields": {"*": vaultFieldsOf("has_value type")}, + "masks": vaultFieldsOf("brand last4 token_last4"), "aliases": vaultFieldsOf("number cvc exp_month exp_year"), "preparation": vaultFieldsOf("id status browser_id merchant_origin environment psp created_at expires_at approval_url"), "authorization": vaultFieldsOf("id status psp merchant amount amount_cents currency created_at expires_at approval_url browser_id reason psp_error_code expected_cents actual_cents amount_authority amount_verified charged_amount_cents charged_currency charged_kind replay_attempted replay_status replay_delivered"), @@ -155,7 +155,8 @@ func preservePublicCredentialValues(source, result vaultJSON) error { Sensitive *bool `json:"sensitive"` } var spec struct { - Fields []definition `json:"fields"` + Provider string `json:"provider"` + Fields []definition `json:"fields"` } var values struct { Fields map[string]struct { @@ -166,6 +167,10 @@ func preservePublicCredentialValues(source, result vaultJSON) error { if json.Unmarshal(source["spec"], &spec) != nil || json.Unmarshal(source["state"], &values) != nil || values.Fields == nil { return nil } + // Managed auth state lists binding names and types only; values are never returned. + if spec.Provider == "managed_auth" { + return nil + } definitions := make(map[string]definition, len(spec.Fields)) for _, field := range spec.Fields { definitions[field.Name] = field @@ -339,6 +344,8 @@ func printVaultItem(item *kernel.VaultItemUnion, output string) error { } } } + } else if item.Spec.Provider == "managed_auth" { + rows = append(rows, []string{"Managed auth connection (immutable)", item.Spec.ConnectionID}) } else { pterm.Info.Println("Use -o json for field definitions, presence, and non-sensitive values; sensitive values are omitted") } @@ -371,6 +378,15 @@ func printVaultItem(item *kernel.VaultItemUnion, output string) error { if item.Spec.Provider == "agentcard" && item.Spec.CheckoutOrigin != "" { rows = append(rows, []string{"Checkout origin", item.Spec.CheckoutOrigin}) } + if item.Spec.Provider == "kernel" && item.Spec.MerchantURL != "" { + rows = append(rows, []string{"Merchant URL", item.Spec.MerchantURL}) + } + if masks := item.State.Masks; masks.Last4 != "" || masks.TokenLast4 != "" { + rows = append(rows, []string{"Card last4", util.OrDash(masks.Last4)}) + if masks.TokenLast4 != "" { + rows = append(rows, []string{"Network token last4", masks.TokenLast4}) + } + } } if item.State.JSON.Domains.Valid() { rows = append(rows, []string{"Permitted domains (provider-assigned)", strings.Join(item.State.Domains, ", ")}) @@ -447,6 +463,10 @@ func printVaultItemGuidance(item *kernel.VaultItemUnion, actions vaultItemAction pterm.Info.Println("Ready means the account owner approved access, not that sign-in succeeded. 1pw_fill submits the form; inspect the page afterward. Never retry a request or fill automatically; after an uncertain outcome, do not delete and recreate the item.") return } + if item.Type == "credential" && item.Spec.Provider == "managed_auth" { + pterm.Info.Println("Fill reads the managed auth connection's saved credential at fill time; use -o json for fill binding names. Ready means a saved credential exists, not that login succeeded. Fill only when advertised; fill does not submit the form.") + return + } if item.Type == "credential" { if actions.RequiredAction != "" { pterm.Info.Println("Share the collection URL with the user to complete the credential form. Observe readiness with items get --wait 60; for edits to an already-ready item, compare versions without --wait.") diff --git a/cmd/vaults_wallet_spec.go b/cmd/vaults_wallet_spec.go index 8136b3a1..1cf5ded1 100644 --- a/cmd/vaults_wallet_spec.go +++ b/cmd/vaults_wallet_spec.go @@ -41,7 +41,11 @@ func vaultWalletSpecFromFlags(cmd *cobra.Command) (kernel.VaultItemUpsertParamsB return kernel.VaultItemUpsertParamsBodyWalletSpecUnion{}, err } provider, _ := cmd.Flags().GetString("provider") - if provider == "agentcard" { + if provider == "kernel" { + if reference != nil || cmd.Flags().Changed("tokens-file") { + return kernel.VaultItemUpsertParamsBodyWalletSpecUnion{}, fmt.Errorf("Kernel wallets use Kernel-managed credentials; omit provider config and --tokens-file") + } + } else if provider == "agentcard" { if cmd.Flags().Changed("tokens-file") { return kernel.VaultItemUpsertParamsBodyWalletSpecUnion{}, fmt.Errorf("--tokens-file is only for imported Link wallet grants") } diff --git a/cmd/vaults_wallet_spec_test.go b/cmd/vaults_wallet_spec_test.go index 9711ddba..0d6f2be1 100644 --- a/cmd/vaults_wallet_spec_test.go +++ b/cmd/vaults_wallet_spec_test.go @@ -63,3 +63,37 @@ func TestVaultImportedAuthorizationPreservesRawFields(t *testing.T) { }) } } + +func TestVaultKernelWalletSendsProviderOnly(t *testing.T) { + client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) { + var body struct { + Type string `json:"type"` + Spec map[string]json.RawMessage `json:"spec"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + assert.Equal(t, "wallet", body.Type) + raw, err := json.Marshal(body.Spec) + require.NoError(t, err) + assert.JSONEq(t, `{"provider":"kernel"}`, string(raw)) + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, connectedWalletFixture) + }) + _, _, err := executeVaultInputCommand(t, client, "", "vaults", "wallets", "create", "checkout", "wallet-1", "--provider", "kernel", "--spec", "{}", "-o", "json") + require.NoError(t, err) +} + +func TestVaultKernelProviderRejectsUnsupportedInputs(t *testing.T) { + for name, args := range map[string][]string{ + "provider config": {"wallets", "create", "checkout", "wallet-1", "--provider", "kernel", "--spec", "{}", "--provider-config-name", "cfg"}, + "tokens file": {"wallets", "create", "checkout", "wallet-1", "--provider", "kernel", "--spec", "{}", "--tokens-file", "-"}, + "card update": {"cards", "update", "checkout", "card-1", "--provider", "kernel", "--spec", "{}"}, + } { + t.Run(name, func(t *testing.T) { + client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) + }) + _, _, err := executeVaultInputCommand(t, client, "", append([]string{"vaults"}, args...)...) + require.Error(t, err) + }) + } +} diff --git a/go.mod b/go.mod index 2696f88c..f87545e0 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/joho/godotenv v1.5.1 - github.com/kernel/kernel-go-sdk v0.117.0 + github.com/kernel/kernel-go-sdk v0.119.1-0.20261005141955-acf8805822d0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 080d8770..4732060d 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kernel/kernel-go-sdk v0.117.0 h1:b6/am7RkJyhadi/98pMHAyuiwyEoDD6smZ4V92pUJ40= -github.com/kernel/kernel-go-sdk v0.117.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.119.1-0.20261005141955-acf8805822d0 h1:e2QwRkufUWlupG+ICraZZTWOBULDXMW5cMKIwAYko0E= +github.com/kernel/kernel-go-sdk v0.119.1-0.20261005141955-acf8805822d0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=