From 6f050a19186b67243f752ae9127dcda535cffebd Mon Sep 17 00:00:00 2001 From: James Sadler Date: Fri, 6 Nov 2026 22:05:23 +1100 Subject: [PATCH] fix(auth): take the backend names that stash keyset prints keysetCreate took "aws" as the backend, but its result named the same backend "aws-kms". A value from the output was not a valid input, and the error message wrote the list of short names again by hand. The input now takes both the short name and the full name (KeyVendor::name). One table holds the short names, and the error message comes from that table. The short names still work, so no command line or skill example breaks. The stash CLI checks --backend before it calls the binding, so it takes the full names too, from one map of short name to full name. The --backend help and the stash-cli skill say so. --- .changeset/keyset-backend-full-names.md | 6 +++ .../typescript/packages/auth/native.d.ts | 6 ++- .../typescript/packages/auth/src/keyset.rs | 50 +++++++++++++++++-- .../packages/cli/src/cli/registry.ts | 3 +- .../commands/keyset/__tests__/keyset.test.ts | 15 ++++++ .../packages/cli/src/commands/keyset/index.ts | 43 ++++++++++++---- skills/stash-cli/SKILL.md | 3 ++ 7 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 .changeset/keyset-backend-full-names.md diff --git a/.changeset/keyset-backend-full-names.md b/.changeset/keyset-backend-full-names.md new file mode 100644 index 000000000..b1be09877 --- /dev/null +++ b/.changeset/keyset-backend-full-names.md @@ -0,0 +1,6 @@ +--- +'@cipherstash/auth': patch +'stash': patch +--- + +`keysetCreate` and `stash keyset create --backend` now also take the full backend names that their output gives: `aws-kms`, `gcp-kms`, `azure-key-vault` and `vault-transit`. The short names (`aws`, `gcp`, `azure`, `vault`, `static`) still work. Before, the output named a backend `aws-kms`, but the input refused that name. The error for an unknown backend now lists both forms. diff --git a/languages/typescript/packages/auth/native.d.ts b/languages/typescript/packages/auth/native.d.ts index 0c163a481..ae6de36e5 100644 --- a/languages/typescript/packages/auth/native.d.ts +++ b/languages/typescript/packages/auth/native.d.ts @@ -5,7 +5,11 @@ /** The options of [`keyset_create`]. */ export interface KeysetCreateOptions { - /** The key backend: `aws`, `gcp`, `azure`, `vault` or `static`. */ + /** + * The key backend: `aws`, `gcp`, `azure`, `vault` or `static`, or the + * full name that [`KeysetResult::backend`] gives (`aws-kms`, + * `gcp-kms`, `azure-key-vault`, `vault-transit`). + */ backend: string /** * The vendor key that wraps the keyset's key: an AWS KMS key ARN, a diff --git a/languages/typescript/packages/auth/src/keyset.rs b/languages/typescript/packages/auth/src/keyset.rs index 3a19d426f..bfc8983e3 100644 --- a/languages/typescript/packages/auth/src/keyset.rs +++ b/languages/typescript/packages/auth/src/keyset.rs @@ -40,7 +40,9 @@ const STATIC_KEY_LEN: usize = 32; #[napi(object)] #[derive(Debug, Clone)] pub struct KeysetCreateOptions { - /// The key backend: `aws`, `gcp`, `azure`, `vault` or `static`. + /// The key backend: `aws`, `gcp`, `azure`, `vault` or `static`, or the + /// full name that [`KeysetResult::backend`] gives (`aws-kms`, + /// `gcp-kms`, `azure-key-vault`, `vault-transit`). pub backend: String, /// The vendor key that wraps the keyset's key: an AWS KMS key ARN, a /// Cloud KMS crypto key name, an Azure Key Vault key identifier, a Vault @@ -230,7 +232,11 @@ fn vendor_config(vendor: KeyVendor, error: impl std::error::Error + 'static) -> // Parsing the options // --------------------------------------------------------------------------- -/// The `--backend` values, in the order the help text lists them. +/// The backends `stash keyset` can use, in the order the help text lists +/// them, each with its short name (the `--backend` value). The output +/// names a backend with [`KeyVendor::name`] (`aws-kms`, …), and the input +/// takes that name too, so a value the output gives is a valid input. +/// This table is the only list of the short names. pub(crate) const BACKENDS: [(&str, KeyVendor); 5] = [ ("aws", KeyVendor::Aws), ("gcp", KeyVendor::Gcp), @@ -239,18 +245,36 @@ pub(crate) const BACKENDS: [(&str, KeyVendor); 5] = [ ("static", KeyVendor::Static), ]; +/// The backend that `text` names: a short name of [`BACKENDS`] or the +/// [`KeyVendor::name`] of one of its vendors. fn parse_backend(text: &str) -> Result { BACKENDS .iter() - .find(|(name, _)| *name == text) + .find(|(short, vendor)| *short == text || vendor.name() == text) .map(|(_, vendor)| *vendor) .ok_or_else(|| { KeysetError::Options(format!( - "Unknown key backend {text:?}. Use one of: aws, gcp, azure, vault, static." + "Unknown key backend {text:?}. {}", + backend_choices() )) }) } +/// The accepted backend names, for an error message, from [`BACKENDS`]. +fn backend_choices() -> String { + let short: Vec<&str> = BACKENDS.iter().map(|(short, _)| *short).collect(); + let full: Vec<&str> = BACKENDS + .iter() + .map(|(_, vendor)| vendor.name()) + .filter(|name| !short.contains(name)) + .collect(); + format!( + "Use one of: {}. The full names {} also work.", + short.join(", "), + full.join(", ") + ) +} + fn parse_mode(text: &str) -> Result { KeyMode::ALL .into_iter() @@ -871,6 +895,24 @@ mod tests { } } + /// The input takes every name the output gives, and the table has every + /// vendor. + #[test] + fn the_backend_input_takes_the_output_names() { + for vendor in KeyVendor::ALL { + assert_eq!(parse_backend(vendor.name()).unwrap(), vendor); + assert!(BACKENDS.iter().any(|(_, v)| *v == vendor), "{vendor}"); + } + let Err(KeysetError::Options(message)) = parse_backend("zerokms") else { + panic!("zerokms is not a key vendor backend"); + }; + assert_eq!( + message, + "Unknown key backend \"zerokms\". Use one of: aws, gcp, azure, vault, static. \ + The full names aws-kms, gcp-kms, azure-key-vault, vault-transit also work." + ); + } + #[test] fn a_vendor_config_error_names_the_vendor() { let envelope = envelope(KeysetError::VendorConfig { diff --git a/languages/typescript/packages/cli/src/cli/registry.ts b/languages/typescript/packages/cli/src/cli/registry.ts index 46e681aa3..dd00658b3 100644 --- a/languages/typescript/packages/cli/src/cli/registry.ts +++ b/languages/typescript/packages/cli/src/cli/registry.ts @@ -881,7 +881,8 @@ export const registry: CommandGroup[] = [ { name: '--backend', value: '', - description: 'The key vendor backend. Required.', + description: + 'The key vendor backend. The full names that the output gives (aws-kms, gcp-kms, azure-key-vault, vault-transit) also work. Required.', }, { name: '--vendor-key', diff --git a/languages/typescript/packages/cli/src/commands/keyset/__tests__/keyset.test.ts b/languages/typescript/packages/cli/src/commands/keyset/__tests__/keyset.test.ts index bcf1853c5..7e9359ead 100644 --- a/languages/typescript/packages/cli/src/commands/keyset/__tests__/keyset.test.ts +++ b/languages/typescript/packages/cli/src/commands/keyset/__tests__/keyset.test.ts @@ -135,10 +135,25 @@ describe('stash keyset create', () => { 1, ) expect(errors()).toContain(messages.keyset.unknownBackend) + expect(errors()).toContain( + 'Use one of: aws, gcp, azure, vault, static. The full names aws-kms, gcp-kms, azure-key-vault, vault-transit also work.', + ) const hints = clack.log.info.mock.calls.map((call) => String(call[0])) expect(hints).toContain(messages.keyset.zeroKmsNote) }) + it('takes the backend name that its own output gives', async () => { + authMock.keysetCreate.mockResolvedValue({ data: result() }) + await keysetCreateCommand({ + backend: 'aws-kms', + vendorKey: 'arn:aws:kms:us-east-1:111122223333:key/k', + mode: 'wrapped', + }) + expect(authMock.keysetCreate).toHaveBeenCalledWith( + expect.objectContaining({ backend: 'aws-kms' }), + ) + }) + it('refuses wrapped data keys on the static backend', async () => { await expectExit( keysetCreateCommand({ diff --git a/languages/typescript/packages/cli/src/commands/keyset/index.ts b/languages/typescript/packages/cli/src/commands/keyset/index.ts index 329b667d2..f16441972 100644 --- a/languages/typescript/packages/cli/src/commands/keyset/index.ts +++ b/languages/typescript/packages/cli/src/commands/keyset/index.ts @@ -22,14 +22,37 @@ import { emitJsonError, emitJsonEvent } from '../auth/events.js' */ const CHROME = { output: process.stderr } as const -/** The `--backend` values. The Rust side holds the same list. */ -export const KEYSET_BACKENDS = [ - 'aws', - 'gcp', - 'azure', - 'vault', - 'static', -] as const +/** + * The `--backend` values: each short name, and the full name that the output + * (`# backend: …`, the `backend` of `--json`) and the SDKs use. `--backend` + * takes either, so a value the output gives is a valid input. The Rust side + * (`BACKENDS` in `@cipherstash/auth`) holds the same table. + */ +export const KEYSET_BACKEND_NAMES = { + aws: 'aws-kms', + gcp: 'gcp-kms', + azure: 'azure-key-vault', + vault: 'vault-transit', + static: 'static', +} as const + +/** The short `--backend` names, in the order the help text lists them. */ +export const KEYSET_BACKENDS = Object.keys(KEYSET_BACKEND_NAMES) as Array< + keyof typeof KEYSET_BACKEND_NAMES +> + +/** The full names that are not also a short name. */ +const KEYSET_FULL_BACKENDS = Object.values(KEYSET_BACKEND_NAMES).filter( + (name) => !(KEYSET_BACKENDS as readonly string[]).includes(name), +) + +/** Whether `--backend` takes `value`. */ +export function isKeysetBackend(value: string): boolean { + return ( + (KEYSET_BACKENDS as readonly string[]).includes(value) || + (KEYSET_FULL_BACKENDS as readonly string[]).includes(value) + ) +} /** The `--mode` values. */ export const KEYSET_MODES = ['wrapped', 'derived'] as const @@ -296,10 +319,10 @@ function validateCreate(options: KeysetCreateOptions) { ) } const backend = options.backend as string - if (!(KEYSET_BACKENDS as readonly string[]).includes(backend)) { + if (!isKeysetBackend(backend)) { throw new KeysetError( 'invalid_backend', - `${messages.keyset.unknownBackend} '${backend}'. Use one of: ${KEYSET_BACKENDS.join(', ')}.`, + `${messages.keyset.unknownBackend} '${backend}'. Use one of: ${KEYSET_BACKENDS.join(', ')}. The full names ${KEYSET_FULL_BACKENDS.join(', ')} also work.`, messages.keyset.zeroKmsNote, ) } diff --git a/skills/stash-cli/SKILL.md b/skills/stash-cli/SKILL.md index b7fe1bbcc..d163170a1 100644 --- a/skills/stash-cli/SKILL.md +++ b/skills/stash-cli/SKILL.md @@ -835,6 +835,9 @@ CS_KEYSET_RECORD_USERS=stack-keyset:… - `--backend `, `--vendor-key ` and `--mode ` are required. `--name ` labels the keyset and names the variable (`CS_KEYSET_RECORD_`, else `CS_KEYSET_RECORD`). + The output names the backend in full (`aws-kms`, `gcp-kms`, + `azure-key-vault`, `vault-transit`, `static`), and `--backend` takes those + full names too. - **The record is not secret.** Only the vendor key can unwrap its key. Keep it in config or an env var. **No SDK reads `CS_KEYSET_RECORD*` by itself**: the app passes the value to `encrypt.WithKeysetRecords` (Go) or