diff --git a/README.md b/README.md index 1da76b2a..46483a8c 100644 --- a/README.md +++ b/README.md @@ -276,13 +276,15 @@ Vault commands **collect user credentials and manage payment credentials; fill d Create a vault for the end user, attach it when creating a browser, then navigate to the sensitive form. Define the observed fields in the website's natural top-to-bottom order; -the collection form renders this array order unchanged. Omit values for user collection: +the collection form renders this array order unchanged. Add optional non-secret labels for +human-readable form text; stable names remain the state, update, and fill keys. Omit values +for user collection: ```sh kernel vaults create --name user-vault kernel browsers create --vault user-vault kernel vaults credentials create user-vault login --spec-file - <<'JSON' -{"description":"Hacker News","fields":[{"name":"username","type":"text","required":true,"sensitive":false},{"name":"password","type":"password","required":true,"sensitive":true}]} +{"description":"Hacker News","fields":[{"name":"username","label":"Username","type":"text","required":true,"sensitive":false},{"name":"password","label":"Password","type":"password","required":true,"sensitive":true}]} JSON kernel vaults items get user-vault login --wait 60 -o json kernel vaults items invoke user-vault login fill --spec-file - <<'JSON' diff --git a/cmd/vaults_credential_steering_test.go b/cmd/vaults_credential_steering_test.go index 7f2e1667..4ca0b47b 100644 --- a/cmd/vaults_credential_steering_test.go +++ b/cmd/vaults_credential_steering_test.go @@ -32,8 +32,11 @@ func TestCredentialHelpSteering(t *testing.T) { assert.Contains(t, cmd.Long, "recognizable site name only") assert.Contains(t, cmd.Long, "natural top-to-bottom order") assert.Contains(t, cmd.Long, "collection form renders that order unchanged") + assert.Contains(t, cmd.Long, "optional non-secret human-readable label") + assert.Contains(t, cmd.Long, "browser fills always use name") assert.Contains(t, cmd.Long, "sensitive:false explicitly for ordinary usernames and email addresses") assert.Contains(t, cmd.Example, `"description":"Hacker News"`) assert.Contains(t, cmd.Example, `"fields":[{"name":"username"`) + assert.Contains(t, cmd.Example, `"label":"Username"`) assert.Contains(t, cmd.Example, `"sensitive":false`) } diff --git a/cmd/vaults_credentials.go b/cmd/vaults_credentials.go index 5c446523..03b697c8 100644 --- a/cmd/vaults_credentials.go +++ b/cmd/vaults_credentials.go @@ -23,8 +23,10 @@ First create a vault for the end user and attach it with browsers create --vault Use a protected JSON file or stdin, never secret values in shell arguments. The spec contains description and fields as an ordered array of named definitions. Inspect the website and list fields in its natural top-to-bottom order because the -user-facing collection form renders that order unchanged. Field types are text, -email, password, and totp; definitions accept name, required, sensitive, and value. +user-facing collection form renders that order unchanged. Definitions accept a stable +name and an optional non-secret human-readable label; forms fall back to name. Updates, +state, and browser fills always use name. Field types are text, email, password, and +totp; definitions also accept required, sensitive, and value. Set description to the recognizable site name only, e.g. "Hacker News", not "Hacker News sign-in credentials". This text is the user-facing form title. Set sensitive:false explicitly for ordinary usernames and email addresses. @@ -68,7 +70,7 @@ func newVaultCredentialsCommand() *cobra.Command { cmd.Example = " kernel vaults credentials update user-vault login --version 2 --spec-file changes.json" } else { cmd.Example = ` kernel vaults credentials create user-vault login --spec-file - <<'JSON' -{"description":"Hacker News","fields":[{"name":"username","type":"text","required":true,"sensitive":false},{"name":"password","type":"password","required":true,"sensitive":true}]} +{"description":"Hacker News","fields":[{"name":"username","label":"Username","type":"text","required":true,"sensitive":false},{"name":"password","label":"Password","type":"password","required":true,"sensitive":true}]} JSON` } cmd.Flags().String("spec-file", "", "Credential spec JSON file (use '-' for stdin; maximum 128 KiB)") diff --git a/cmd/vaults_credentials_test.go b/cmd/vaults_credentials_test.go index 206392d0..298b715e 100644 --- a/cmd/vaults_credentials_test.go +++ b/cmd/vaults_credentials_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -const credentialFixture = `{"id":"credential-1","key":"login","type":"credential","version":2,"spec":{"description":"Website login","fields":[{"name":"password","type":"password","required":true,"sensitive":true,"value":"never-print"}]},"state":{"status":"pending_collection","fields":{"password":{"has_value":false,"value":"never-print"}}},"action":{"name":"collect","url":"https://vault.kernel.sh/collect#token=item.random","expires_at":"2026-10-01T00:00:00Z"},"available_operations":[{"type":"collect","description":"Open the form"},{"type":"fill","description":"Fill the form"}],"available_expansions":[]}` +const credentialFixture = `{"id":"credential-1","key":"login","type":"credential","version":2,"spec":{"description":"Website login","fields":[{"name":"password","label":"Account Password","type":"password","required":true,"sensitive":true,"value":"never-print"}]},"state":{"status":"pending_collection","fields":{"password":{"has_value":false,"value":"never-print"}}},"action":{"name":"collect","url":"https://vault.kernel.sh/collect#token=item.random","expires_at":"2026-10-01T00:00:00Z"},"available_operations":[{"type":"collect","description":"Open the form"},{"type":"fill","description":"Fill the form"}],"available_expansions":[]}` func credentialSpecFile(t *testing.T, data string) string { t.Helper() @@ -42,12 +42,12 @@ func TestCredentialCreateAndUpdate(t *testing.T) { assert.JSONEq(t, `{"fields":{"password":{"value":null}}}`, string(body["spec"])) } else { assert.Equal(t, "PUT", r.Method) - assert.JSONEq(t, `{"fields":[{"name":"password","type":"password","required":true}]}`, string(body["spec"])) + assert.JSONEq(t, `{"fields":[{"name":"password","label":"Account Password","type":"password","required":true}]}`, string(body["spec"])) } w.Header().Set("Content-Type", "application/json") io.WriteString(w, credentialFixture) }) - args := []string{"vaults", "credentials", "create", "user", "login", "--spec-file", credentialSpecFile(t, `{"fields":[{"name":"password","type":"password","required":true}]}`), "-o", "json"} + args := []string{"vaults", "credentials", "create", "user", "login", "--spec-file", credentialSpecFile(t, `{"fields":[{"name":"password","label":"Account Password","type":"password","required":true}]}`), "-o", "json"} if update { args[2] = "update" args[6] = credentialSpecFile(t, `{"fields":{"password":{"value":null}}}`) @@ -59,6 +59,7 @@ func TestCredentialCreateAndUpdate(t *testing.T) { assert.NotContains(t, out, "never-print") assert.Contains(t, out, `"has_value": false`) assert.Contains(t, out, `"version": 2`) + assert.Contains(t, out, `"label": "Account Password"`) assert.Contains(t, out, "#token=item.random") }) } diff --git a/cmd/vaults_output.go b/cmd/vaults_output.go index 49270fcf..9d1c60bd 100644 --- a/cmd/vaults_output.go +++ b/cmd/vaults_output.go @@ -41,7 +41,7 @@ var vaultItemFields = vaultOutputFields{ "provider": nil, "wallet": nil, "user_id": nil, "payment_method_id": nil, "card_id": nil, "amount": nil, "currency": nil, "merchant": nil, "merchant_name": nil, "merchant_url": nil, "context": nil, "expires_at": nil, "description": nil, - "fields": vaultFieldsOf("name type required sensitive"), + "fields": vaultFieldsOf("name label type required sensitive"), "provider_config": vaultFieldsOf("id name"), "authorization": {"method": nil, "client": {"type": nil, "provider_config": vaultFieldsOf("id name")}}, "totals": vaultTotalFields, diff --git a/cmd/vaults_public_values_test.go b/cmd/vaults_public_values_test.go index 669e6e63..b88bc680 100644 --- a/cmd/vaults_public_values_test.go +++ b/cmd/vaults_public_values_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -const publicCredentialFixture = `{"id":"credential-1","key":"login","type":"credential","version":2,"spec":{"description":"Example","fields":[{"name":"username","type":"text","sensitive":false},{"name":"email","type":"email","sensitive":false},{"name":"password","type":"password"},{"name":"otp","type":"totp","sensitive":true}]},"state":{"status":"ready","fields":{"username":{"has_value":true,"value":"user-123"},"email":{"has_value":true,"value":"user@example.com"},"password":{"has_value":true,"value":"private-password"},"otp":{"has_value":true,"value":"private-seed"}}},"action":{"name":"collect","url":"https://vault.example/collect#token=user-123.token"},"available_operations":[{"type":"collect","description":"Open form"}],"available_expansions":[]}` +const publicCredentialFixture = `{"id":"credential-1","key":"login","type":"credential","version":2,"spec":{"description":"Example","fields":[{"name":"username","label":"Membership Number or Username","type":"text","sensitive":false},{"name":"email","type":"email","sensitive":false},{"name":"password","type":"password"},{"name":"otp","type":"totp","sensitive":true}]},"state":{"status":"ready","fields":{"username":{"has_value":true,"value":"user-123"},"email":{"has_value":true,"value":"user@example.com"},"password":{"has_value":true,"value":"private-password"},"otp":{"has_value":true,"value":"private-seed"}}},"action":{"name":"collect","url":"https://vault.example/collect#token=user-123.token"},"available_operations":[{"type":"collect","description":"Open form"}],"available_expansions":[]}` func TestVaultPublicValuesAcrossCommands(t *testing.T) { spec := credentialSpecFile(t, `{"fields":[{"name":"username","type":"text","sensitive":false,"value":"user-123"}]}`) @@ -50,7 +50,8 @@ func TestVaultCredentialDefinitionOrderIsPreserved(t *testing.T) { var item struct { Spec struct { Fields []struct { - Name string `json:"name"` + Name string `json:"name"` + Label string `json:"label"` } `json:"fields"` } `json:"spec"` } @@ -60,6 +61,8 @@ func TestVaultCredentialDefinitionOrderIsPreserved(t *testing.T) { names = append(names, field.Name) } assert.Equal(t, []string{"username", "email", "password", "otp"}, names) + assert.Equal(t, "Membership Number or Username", item.Spec.Fields[0].Label) + assert.Empty(t, item.Spec.Fields[1].Label) } func TestVaultPublicValueBoundary(t *testing.T) { diff --git a/cmd/vaults_sdk_contract_test.go b/cmd/vaults_sdk_contract_test.go index 94d7ec74..8fa3a16c 100644 --- a/cmd/vaults_sdk_contract_test.go +++ b/cmd/vaults_sdk_contract_test.go @@ -66,7 +66,7 @@ func TestCredentialUpdateIdentityPrecondition(t *testing.T) { func TestCredentialInitialValuesWithGeneratedSDK(t *testing.T) { t.Setenv("KERNEL_PROJECT", "") - spec := `{"description":"Example","fields":[{"name":"username","type":"text","sensitive":false,"value":"synthetic-user"},{"name":"email","type":"email","sensitive":false,"value":"test@example.com"},{"name":"password","type":"password","sensitive":true,"value":"synthetic-password"},{"name":"otp","type":"totp","sensitive":true,"value":"JBSWY3DPEHPK3PXP"}]}` + spec := `{"description":"Example","fields":[{"name":"username","label":"Membership Number or Username","type":"text","sensitive":false,"value":"synthetic-user"},{"name":"email","type":"email","sensitive":false,"value":"test@example.com"},{"name":"password","type":"password","sensitive":true,"value":"synthetic-password"},{"name":"otp","type":"totp","sensitive":true,"value":"JBSWY3DPEHPK3PXP"}]}` calls := 0 client := vaultTestClient(t, func(w http.ResponseWriter, r *http.Request) { calls++ @@ -84,6 +84,7 @@ func TestCredentialInitialValuesWithGeneratedSDK(t *testing.T) { assert.NotContains(t, out, value) } assert.Contains(t, out, `"has_value": true`) + assert.Contains(t, out, `"label": "Membership Number or Username"`) } func TestVaultPreparationApprovalURLIsPrintedInFull(t *testing.T) { diff --git a/go.mod b/go.mod index 196dbe1f..dcae6b62 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.109.0 + github.com/kernel/kernel-go-sdk v0.110.0 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 c34e2e2f..1f47cec9 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,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.109.0 h1:en9sI4w4AW0CptJn92KjpdvrQtzWohKcCu0ahmI6Ncg= -github.com/kernel/kernel-go-sdk v0.109.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.110.0 h1:2KkE0hAlJav5xg2818Eg+mIK2p1F2nDZ0rZdA2EO1QQ= +github.com/kernel/kernel-go-sdk v0.110.0/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=