Commit f188c7a
authored
feat(cli): make
* feat(cli): make `stash env` mint real deployment credentials
The command was a gated stub (STASH_EXPERIMENTAL_ENV_CMD) waiting on "the
CTS mint endpoint" — which turned out to already exist. It now mints
working credentials from the device-code session using the same APIs the
dashboard uses, all plain fetch against the session's own service URLs:
1. GET {cts}/api/workspaces → server-authoritative region → CS_WORKSPACE_CRN
2. POST {zerokms}/create-client → CS_CLIENT_ID / CS_CLIENT_KEY (hex-transcoded)
3. POST {cts}/api/access-keys → CS_CLIENT_ACCESS_KEY (member role, always —
the CLI deliberately cannot mint admin keys; role is omitted so the
server owns the default)
Design points:
- The ZeroKMS client is created before the access key, so a partial
failure leaves an inert client record, never an unaccounted-for live
credential.
- The key name resolves before any profile/network access, so the
non-interactive missing-name failure is credential-free — that is the
seam the new pty-less e2e exercises (minting live keys from a dev
machine's real session in tests is deliberately impossible).
- --write emits .env.production.local with mode 0600 and refuses to
overwrite non-interactively; --json emits one minted object or the
shared { status: 'error' } envelope.
Closes #663 (the wasm/edge credential gap, CIP-2997): edge runtimes that
cannot read ~/.cipherstash (containerised supabase functions serve,
Workers) now have a supported path — stash env + --env-file/secret store.
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
* docs(skills): point stash-encryption's CI/prod config at `stash env`
The Configuration section listed the four CS_* variables but left how to
obtain them to the dashboard. Both the stash-cli and stash-supabase skills
now document `stash env`; this closes the loop in the skill the other two
reference for configuration.
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
* docs(skills): agent-driven `auth login` — the operational loop, live-verified
Ran the flow for real (agent starts `auth login --json --region us-west-2`
backgrounded, human approves the relayed URL): authorization_required →
authorized → device_bound, exit 0, session verified via getToken. The event
table in stash-cli was already accurate; what was missing was the part that
bites an agent — the command blocks polling for up to ~15 minutes, so it
must run as a background task with a generous timeout, and success is
confirmed from the event stream (never by inspecting ~/.cipherstash).
stash-encryption's Local Development section now points agents at the flow.
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
* fix(cli): harden `stash env` — all 10 findings from the branch review
An 8-angle adversarial review of this branch confirmed 10 defects (plus one
skill nit); this commit fixes all of them:
1. --write's overwrite decision (refusal AND interactive confirm) now runs
BEFORE minting — a declined overwrite previously discarded a live,
shown-exactly-once access key, contradicting the file's own contract.
2. Stdout is now actually pipe-clean: every clack call routes chrome to
stderr via { output: process.stderr }, so `stash env > prod.env` and
pipes into dotenv consumers capture only the block. (clack defaults
everything to stdout — the old comment's claim was wrong.)
3. This also fixes prompts rendering into redirected stdout (isInteractive
is stdin-only): prompts on stderr stay visible on the terminal.
4. `--write <path>` is now a feature, not a parse trap: parseArgs put the
path in values.write leaving flags.write undefined, silently printing
secrets to stdout. main.ts passes values.write ?? flags.write and the
command accepts a target path.
5. `--name` followed by another flag gets its own name_requires_value
diagnostic instead of a false missing_name.
6. `stash env <positional>` is rejected with did-you-mean --name guidance
instead of silently vanishing into the subcommand slot.
7. --json + --write now compose: the file is written and the JSON
confirmation ({ status: 'written' }) is deliberately secret-free.
8. 0600 is enforced on overwrite too (writeFileSync's mode only applies on
creation) via an explicit chmod.
9. All three API responses are zod-validated; non-base64 client_key is
refused before Node's lenient decoder can transcode garbage; a service
shape change can no longer print undefined into a credentials file.
10. The member role is pinned in the request (wire value verified lowercase
in cts-common) AND asserted on the response — a non-member key is
refused with revocation guidance, so the documented 'cannot mint admin
keys' invariant is enforced, not assumed.
11. All exits go through CliExit (never deep process.exit), so run()
records env outcomes for telemetry; cancel paths use CliExit(0). The
three divergent dead-code exit trailers are gone.
12. skills/stash-cli: the bare --env-file mention is attributed to
`supabase functions serve`, per the manifest-resolution rule.
Tests: 21 env unit tests (preflight-before-mint asserted via zero fetch
calls; chrome-on-stderr; role pinning; validation refusals; json+write
secret-free event; chmod-on-overwrite) and 4 e2e cases (all credential-free
pre-mint failures, including asserting error chrome lands on stderr, not
stdout). Full suite: 534 unit / 62 e2e green, code:check exit 0.
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
* fix(cli): address PR #682 review comments (timeout, name validation, doc nits)
- apiFetch now carries a 30s AbortSignal.timeout; timeouts surface as
request_timeout and connection failures as network_error (naming the
host, and the undici cause rather than the opaque 'fetch failed').
A stalled CTS/ZeroKMS endpoint no longer hangs the CLI. (CodeRabbit)
- The credential name rejects control characters (invalid_name) — a
newline could break out of the dotenv comment line and inject a line
into a written credentials file. Enforced pre-mint for both the flag
and the interactive prompt. (Copilot)
- skills/stash-cli: dotenv language on the output fence (MD040);
skills/stash-supabase: keep the example command in one code span.
Unit tests: 24 env tests (invalid_name pre-profile, request_timeout,
network_error). Suite 537/62 green, code:check clean.
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
* fix(cli): address James's review notes on stash env (#682)
- Note 2: treat an ABSENT access-key role as member (server default) rather
than skipping the check — `(role ?? 'member')` — so the documented
'verified on the response' guarantee doesn't depend on the field always
being present.
- Note 3: only append the 'name already taken, use --name' hint on 400/409
(where duplicates land), not every non-403 — a 500 no longer carries a
misleading suggestion.
- Note 4 (defense-in-depth): reject any server-provided emitted value
(CRN/region, client id, access key) that contains a control character,
closing the dotenv line-injection surface the name guard already covered.
clientKey is hex by construction.
Tests: absent-role → success; 500 omits the --name hint (keeps the
leftover-client note); a newline in the server region → refused, no
injected line on stdout. 27 env unit tests, suite 540/62 green.
Note 1 (CRN round-trip) needs no code change — the pre-merge live smoke
already inits a wasm-inline client with the emitted workspaceCrn (see PR
comment); replying there.
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94wstash env mint real deployment credentials (rc.2 B3) (#682)1 parent 8b2551a commit f188c7a
10 files changed
Lines changed: 1357 additions & 64 deletions
File tree
- .changeset
- packages/cli
- src
- bin
- cli
- commands/env
- __tests__
- tests/e2e
- skills
- stash-cli
- stash-encryption
- stash-supabase
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | | - | |
| 128 | + | |
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| |||
532 | 532 | | |
533 | 533 | | |
534 | 534 | | |
535 | | - | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
536 | 548 | | |
537 | 549 | | |
538 | 550 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
530 | 530 | | |
531 | 531 | | |
532 | 532 | | |
533 | | - | |
| 533 | + | |
534 | 534 | | |
535 | 535 | | |
536 | 536 | | |
537 | | - | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
538 | 559 | | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
539 | 566 | | |
540 | 567 | | |
541 | | - | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
542 | 576 | | |
543 | 577 | | |
544 | 578 | | |
| |||
0 commit comments