Skip to content
Merged
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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 3 additions & 0 deletions cmd/vaults_credential_steering_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
}
8 changes: 5 additions & 3 deletions cmd/vaults_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)")
Expand Down
7 changes: 4 additions & 3 deletions cmd/vaults_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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}}}`)
Expand All @@ -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")
})
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/vaults_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions cmd/vaults_public_values_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]}`)
Expand Down Expand Up @@ -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"`
}
Expand All @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion cmd/vaults_sdk_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading