burnedsecret-cli 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. burnedsecret_cli-1.0.0/.gitignore +56 -0
  2. burnedsecret_cli-1.0.0/PKG-INFO +278 -0
  3. burnedsecret_cli-1.0.0/README.md +262 -0
  4. burnedsecret_cli-1.0.0/burnedsecret_cli.spec +120 -0
  5. burnedsecret_cli-1.0.0/docs/exit-codes.md +35 -0
  6. burnedsecret_cli-1.0.0/packaging/homebrew/bs.rb.template +48 -0
  7. burnedsecret_cli-1.0.0/packaging/install/install.ps1 +104 -0
  8. burnedsecret_cli-1.0.0/packaging/install/install.sh +130 -0
  9. burnedsecret_cli-1.0.0/packaging/scoop/bs.json.template +36 -0
  10. burnedsecret_cli-1.0.0/pyproject.toml +28 -0
  11. burnedsecret_cli-1.0.0/src/burnedsecret_cli/__init__.py +3 -0
  12. burnedsecret_cli-1.0.0/src/burnedsecret_cli/auth_flow.py +369 -0
  13. burnedsecret_cli-1.0.0/src/burnedsecret_cli/client_factory.py +41 -0
  14. burnedsecret_cli-1.0.0/src/burnedsecret_cli/commands/__init__.py +1 -0
  15. burnedsecret_cli-1.0.0/src/burnedsecret_cli/commands/_ttl.py +77 -0
  16. burnedsecret_cli-1.0.0/src/burnedsecret_cli/commands/auth.py +216 -0
  17. burnedsecret_cli-1.0.0/src/burnedsecret_cli/commands/key.py +147 -0
  18. burnedsecret_cli-1.0.0/src/burnedsecret_cli/commands/request.py +271 -0
  19. burnedsecret_cli-1.0.0/src/burnedsecret_cli/commands/secret.py +302 -0
  20. burnedsecret_cli-1.0.0/src/burnedsecret_cli/config.py +251 -0
  21. burnedsecret_cli-1.0.0/src/burnedsecret_cli/errors.py +120 -0
  22. burnedsecret_cli-1.0.0/src/burnedsecret_cli/main.py +159 -0
  23. burnedsecret_cli-1.0.0/src/burnedsecret_cli/output.py +91 -0
  24. burnedsecret_cli-1.0.0/tests/test_auth_command.py +171 -0
  25. burnedsecret_cli-1.0.0/tests/test_auth_flow.py +360 -0
  26. burnedsecret_cli-1.0.0/tests/test_client_factory.py +99 -0
  27. burnedsecret_cli-1.0.0/tests/test_config.py +157 -0
  28. burnedsecret_cli-1.0.0/tests/test_key_command.py +107 -0
  29. burnedsecret_cli-1.0.0/tests/test_output_discipline.py +60 -0
  30. burnedsecret_cli-1.0.0/tests/test_request_command.py +202 -0
  31. burnedsecret_cli-1.0.0/tests/test_secret_command.py +217 -0
@@ -0,0 +1,56 @@
1
+ # Flutter
2
+ .dart_tool/
3
+ .flutter-plugins
4
+ .flutter-plugins-dependencies
5
+ build/
6
+ pubspec.lock
7
+
8
+ # gen-l10n output — generated from lib/l10n/*.arb at build time (generate: true)
9
+ lib/l10n/app_localizations*.dart
10
+ # gen-l10n untranslated-messages-file report (l10n.yaml). Project-root-relative
11
+ # JSON listing, per locale, every key still missing from a target ARB. A
12
+ # generated WARNING report regenerated on every gen-l10n run — never committed
13
+ # (avoids churn); drained locally via scripts/translate_arb.py.
14
+ untranslated_messages.json
15
+
16
+ # Phase 15 (D-02): generated by scripts/build_docs.py in CI; source is docs/site/
17
+ web/docs/
18
+ web/security/
19
+ .venv-docs/
20
+
21
+ # Firebase
22
+ .firebase/
23
+ **/ui-debug.log
24
+ **/firestore-debug.log
25
+ **/firebase-debug.log
26
+
27
+ # Service account keys — NEVER commit
28
+ *-adminsdk-*.json
29
+ service-account*.json
30
+ *sa-key*.json
31
+
32
+ # Python
33
+ __pycache__/
34
+ *.pyc
35
+ functions/lib/
36
+ functions/venv/
37
+ functions/.venv-test/
38
+ .pytest_cache/
39
+
40
+ # MaxMind GeoLite2 DB — large binary, CC BY-SA 4.0 license terms,
41
+ # fetched at CI build time by Codemagic, never committed.
42
+ functions/GeoLite2-Country.mmdb
43
+ functions/GeoLite2-Country.tar.gz
44
+ functions/.geoip_cache/
45
+
46
+ # IDE
47
+ .idea/
48
+ .vscode/
49
+ *.iml
50
+
51
+ # OS
52
+ .DS_Store
53
+ Thumbs.db
54
+
55
+ # local CLI dev venv
56
+ cli/.venv/
@@ -0,0 +1,278 @@
1
+ Metadata-Version: 2.5
2
+ Name: burnedsecret-cli
3
+ Version: 1.0.0
4
+ Summary: Command-line interface for the burnedsecret.com zero-knowledge secrets API
5
+ Author: BurnedSecret
6
+ License: MIT
7
+ Keywords: burnedsecret,cli,encryption,secrets,zero-knowledge
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: burnedsecret<2,>=1
10
+ Requires-Dist: rich>=13.8
11
+ Requires-Dist: tomli-w>=1.2
12
+ Requires-Dist: typer>=0.26
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # burnedsecret-cli (`bs`)
18
+
19
+ Command-line interface wrapping the [burnedsecret](https://burnedsecret.com)
20
+ zero-knowledge secrets REST API. All encryption happens client-side; the server
21
+ never sees plaintext or decryption keys — the key travels only in the URL
22
+ `#k=...` fragment, which the server never receives.
23
+
24
+ ```bash
25
+ pip install burnedsecret-cli
26
+ bs auth login
27
+ echo "my-secret" | bs secret create
28
+ ```
29
+
30
+ ## Install
31
+
32
+ The CLI ships through four channels:
33
+
34
+ ```bash
35
+ # 1. pip (Python 3.9+)
36
+ pip install burnedsecret-cli
37
+
38
+ # 2. Homebrew (macOS / Linux)
39
+ brew install JensrudJ/burnedsecret/bs
40
+
41
+ # 3. Scoop (Windows)
42
+ scoop bucket add burnedsecret https://github.com/JensrudJ/scoop-burnedsecret
43
+ scoop install bs
44
+
45
+ # 4. Standalone binary (no Python needed)
46
+ curl -fsSL https://burnedsecret.com/install/cli/install.sh | sh # POSIX
47
+ irm https://burnedsecret.com/install/cli/install.ps1 | iex # Windows PowerShell
48
+ ```
49
+
50
+ > **macOS first run:** the standalone binary is currently shipped unsigned. On
51
+ > first launch macOS Gatekeeper may block it; right-click → Open, or
52
+ > `xattr -d com.apple.quarantine $(which bs)` to clear the quarantine flag.
53
+ > (Signed/notarized builds are a follow-up — see the v1 gaps below.)
54
+
55
+ ## Quickstart
56
+
57
+ ```bash
58
+ # 1. Authenticate (opens your browser to authorize this machine)
59
+ bs auth login
60
+
61
+ # 2. Create a secret — prints ONLY the share URL on stdout
62
+ url=$(echo "the launch codes" | bs secret create --ttl 1h)
63
+ echo "$url"
64
+
65
+ # 3. Read (and burn) it
66
+ bs secret read "$url"
67
+ ```
68
+
69
+ Because the share URL is the only thing on stdout, `url=$(... )` captures
70
+ exactly the URL; every prompt, table, and diagnostic goes to stderr.
71
+
72
+ ## Global options
73
+
74
+ | Option | Env var | Description |
75
+ | ------------------- | ------------------------ | -------------------------------------------------------- |
76
+ | `--profile NAME` | `BURNEDSECRET_PROFILE` | Select a config profile (default: `default`). |
77
+ | `--json` | — | Emit the raw SDK shape as JSON on stdout. |
78
+ | `--verbose` / `-v` | — | Extra diagnostics on stderr. |
79
+ | `--version` | — | Print the CLI version and exit. |
80
+
81
+ The API key is resolved as `BURNEDSECRET_API_KEY` (env) > the stored
82
+ `config[profile].api_key`.
83
+
84
+ ## Command reference
85
+
86
+ ### `bs secret create`
87
+
88
+ Creates a burn-after-reading secret and prints **only** the share URL.
89
+
90
+ | Flag | Description |
91
+ | -------------------------- | ------------------------------------------------------------------------ |
92
+ | `--content` / `-c` | Secret text. If omitted: stdin pipe → `--edit` → hidden prompt. |
93
+ | `--edit` / `-e` | Compose the secret in `$EDITOR`. |
94
+ | `--file` / `-f` | Encrypt a file as the secret (max 10 MB). Excludes text input. |
95
+ | `--passphrase` / `-p` | Add a PBKDF2 passphrase layer (bare flag prompts hidden). |
96
+ | `--max-views` | Reads before the secret burns (1..5, default 1). |
97
+ | `--ttl` | Time to live: `5m` / `1h` / `1d` / `7d` (default `1d`). |
98
+ | `--webhook-url` | Notify this URL when the secret is read. |
99
+ | `--notification-email` | Email this address when the secret is read. |
100
+
101
+ ```bash
102
+ echo "hunter2" | bs secret create --max-views 3 --ttl 7d
103
+ bs secret create --file ./id_rsa --ttl 1h
104
+ bs secret create --content "deploy key" --passphrase --ttl 1h # prompts for passphrase
105
+ ```
106
+
107
+ > **Web-viewer non-interop caveat:** secrets created with `--file` or
108
+ > `--passphrase` are decryptable only by an SDK/CLI client. Their share URL will
109
+ > **NOT** open in the burnedsecret.com web viewer. Plain text secrets without a
110
+ > passphrase remain web-viewer compatible. (After a `--file`/`--passphrase`
111
+ > create the CLI also prints a one-line reminder to stderr.)
112
+
113
+ ### `bs secret read`
114
+
115
+ Reads and **burns** a secret from its share URL.
116
+
117
+ | Flag | Description |
118
+ | --------------------- | ------------------------------------------------------------------- |
119
+ | `URL` (argument) | Share URL — **must** include the `#k=...` key fragment. |
120
+ | `--passphrase` / `-p` | Passphrase for a protected secret (bare flag prompts hidden). |
121
+ | `--output` / `-o` | Write file-secret bytes to PATH, or `-` to stream to stdout. |
122
+
123
+ ```bash
124
+ bs secret read "https://burnedsecret.com/s/abc#k=..."
125
+ bs secret read "$url" --passphrase hunter2
126
+ bs secret read "$url" --output ./downloaded.bin
127
+ ```
128
+
129
+ ### `bs request create | read`
130
+
131
+ Create a request-for-secret link, then read its fulfillment. The SDK generates an
132
+ RSA keypair locally; the **private key is never sent to the server and cannot be
133
+ regenerated** — without it the fulfillment is permanently unrecoverable. By
134
+ default `create` writes the key to `<config_dir>/requests/<id>.key` (absolute
135
+ path echoed to stderr).
136
+
137
+ | `create` flag | Description |
138
+ | ---------------------- | --------------------------------------------------------------------- |
139
+ | `--prompt` | What you are asking the recipient to send (required). |
140
+ | `--private-key` | Key destination: a `PATH`, `-` (stdout), or `env` (shell export). |
141
+ | `--fields` | Custom fields: `name:type:required;...` (type = text\|password\|textarea). |
142
+ | `--ttl` | `5m` / `1h` / `1d` / `7d`. |
143
+ | `--webhook-url` | Notify this URL when the request is fulfilled. |
144
+ | `--notification-email` | Email this address when the request is fulfilled. |
145
+
146
+ ```bash
147
+ url=$(bs request create --prompt "send the AWS key" --ttl 1d)
148
+ # ... recipient fulfills it via the link ...
149
+ bs request read <request-id> --private-key ~/.config/burnedsecret/requests/<id>.key
150
+ ```
151
+
152
+ `read` requires `--private-key PATH | - (stdin) | env`. Free-text fulfillments
153
+ print as text; custom-field fulfillments print as JSON.
154
+
155
+ ### `bs key create | list | revoke | whoami`
156
+
157
+ Key management is **browser/dashboard-assisted** in v1 (see the gaps below).
158
+
159
+ | Command | Behavior |
160
+ | ------------- | ------------------------------------------------------------------------------ |
161
+ | `key create` | Alias of `bs auth login` — the PKCE exchange mints the labeled key. |
162
+ | `key list` | Prints your dashboard `/keys` URL (view/revoke there). |
163
+ | `key revoke` | Prints the dashboard `/keys` URL; the CLI never network-revokes a key. |
164
+ | `key whoami` | Shows the stored `key_prefix`/`hostname`/`base_url` — **no network call**. |
165
+
166
+ The CLI never mints sibling keys from a stored key: minting from a leaked key
167
+ would be a privilege-escalation surface.
168
+
169
+ ### `bs auth login | logout | status`
170
+
171
+ | Command | Behavior |
172
+ | -------------- | ----------------------------------------------------------------------------- |
173
+ | `auth login` | Browser PKCE flow; stores a hostname-labeled key. The key is **never printed**. |
174
+ | `auth logout` | Forgets the local key (does **not** revoke server-side — use the dashboard). |
175
+ | `auth status` | Shows the profile's identity (configured?, source env/file, prefix, host). |
176
+
177
+ ### `bs config list-profiles`
178
+
179
+ Lists configured profiles with their `key_prefix`/`base_url`.
180
+
181
+ ## Configuration & storage
182
+
183
+ Config lives at:
184
+
185
+ - **Windows:** `%APPDATA%\burnedsecret\config.toml`
186
+ - **POSIX:** `${XDG_CONFIG_HOME:-~/.config}/burnedsecret/config.toml`
187
+
188
+ On POSIX the config (and any persisted request key) is written with `0600`
189
+ permissions so only the owner can read the stored API key. Request private keys
190
+ default to `<config_dir>/requests/<id>.key`.
191
+
192
+ Environment overrides: `BURNEDSECRET_API_KEY`, `BURNEDSECRET_PROFILE`.
193
+
194
+ ## Exit codes
195
+
196
+ Result data is always on stdout; diagnostics/errors/prompts are on stderr.
197
+
198
+ | Code | Meaning |
199
+ | ---- | ------------------------- |
200
+ | 0 | success |
201
+ | 1 | generic error |
202
+ | 2 | invalid arguments / usage |
203
+ | 3 | authentication error |
204
+ | 4 | not found |
205
+ | 5 | rate limited |
206
+
207
+ The same table is shown in `bs --help`; see [`docs/exit-codes.md`](docs/exit-codes.md)
208
+ for the canonical reference and scripting examples.
209
+
210
+ ## The browser-auth recipe (PKCE + loopback)
211
+
212
+ `bs auth login` implements a **reusable** pattern for authenticating a CLI/native
213
+ app through the browser without ever putting a secret in a URL or asking the user
214
+ to paste a token. It is a textbook implementation of **PKCE (RFC 7636)** plus the
215
+ **loopback-redirect native-app flow (RFC 8252)**, and the recipe transfers to any
216
+ CLI that needs browser auth — not just BurnedSecret:
217
+
218
+ 1. **Generate ephemeral secrets locally.** A random CSRF `state` and a PKCE pair:
219
+ `verifier = base64url(32 random bytes)`, `challenge = base64url(sha256(verifier))`
220
+ (S256). The verifier never leaves the machine; only the challenge is sent.
221
+ 2. **Bind a one-shot loopback server.** Grab a free `127.0.0.1:<port>` and start a
222
+ single-request HTTP server in a thread. This is the redirect target — no public
223
+ callback host, no firewall holes.
224
+ 3. **Open the browser to the consent page**, carrying only public values: the
225
+ loopback `callback`, the CSRF `state`, the S256 `code_challenge`, and a
226
+ `hostname` label for the key. The URL is also echoed to stderr so a headless or
227
+ no-default-browser user can paste it manually.
228
+ 4. **Capture and validate the callback.** The browser redirects to the loopback
229
+ server with `?code=&state=`. The handler **constant-comparison-checks the
230
+ returned `state`** against the one it generated — a mismatch is a hard abort
231
+ that captures no code, so no exchange ever happens. The callback carries only
232
+ `code` + `state`; the API key is **never** in any URL.
233
+ 5. **Exchange the single-use code for the key.** POST `{code, code_verifier}` to
234
+ the exchange endpoint. The server recomputes `sha256(verifier)` and
235
+ constant-time compares it to the stored challenge, proving the same client that
236
+ started the flow is finishing it. The **API key is returned only in the
237
+ response body**, then stored chmod-600 and never printed.
238
+
239
+ Security rationale, in one line each:
240
+
241
+ - **CSRF `state`** — prevents a forged callback from injecting an attacker's code.
242
+ - **PKCE `verifier`/`challenge`** — binds the code to this client; a stolen code
243
+ is useless without the verifier, which never traveled over the network.
244
+ - **Key never in the callback URL** — URLs leak (history, logs, referrers); the
245
+ key lives only in the exchange response body and on-disk chmod-600.
246
+ - **Single-use, short-TTL code** — narrows the window for replay.
247
+
248
+ ## Documented v1 gaps & follow-ups
249
+
250
+ These are deliberate v1 scope decisions, recorded here rather than silently
251
+ dropped:
252
+
253
+ - **Access-restriction flags are NOT in v1.** `--access-cidrs`,
254
+ `--access-countries`, and `--require-captcha` are not available — the shipped
255
+ REST `create_secret` shape has no such parameters; adding them needs an SDK
256
+ extension (D-06 deviation).
257
+ - **OS keyring storage is deferred to v2.** Keys are stored in a chmod-600
258
+ config file, not the OS keychain/credential manager.
259
+ - **`bs secret read --copy` (clipboard) is deferred.** Pipe to your platform's
260
+ clipboard tool (`pbcopy` / `clip` / `wl-copy`) in the meantime.
261
+ - **`bs secret create --bulk` is deferred.** Create secrets one at a time (or
262
+ loop in your shell) for now.
263
+ - **Polished tab-completion install is deferred.** Typer's completion machinery
264
+ is wired, but a one-command shell-completion installer is a follow-up.
265
+ - **`bs key list` / `bs key revoke` are browser/dashboard-assisted in v1.** They
266
+ print your dashboard `/keys` URL rather than calling an API-key-authenticated
267
+ keys endpoint (which would be a privilege-escalation surface). A
268
+ token-cache-backed headless listing is a v2 option.
269
+ - **Standalone macOS binaries are currently unsigned** (see the install note);
270
+ signing/notarization is a follow-up.
271
+ - **Chocolatey** is a deliberate fast-follow; Scoop covers the Windows
272
+ package-manager path for v1.
273
+
274
+ ### CI usage
275
+
276
+ Automation does **not** run the browser flow. CI uses a pre-minted
277
+ `BURNEDSECRET_API_KEY` env var; none of the `bs auth`/`bs key` commands are
278
+ needed in a pipeline.
@@ -0,0 +1,262 @@
1
+ # burnedsecret-cli (`bs`)
2
+
3
+ Command-line interface wrapping the [burnedsecret](https://burnedsecret.com)
4
+ zero-knowledge secrets REST API. All encryption happens client-side; the server
5
+ never sees plaintext or decryption keys — the key travels only in the URL
6
+ `#k=...` fragment, which the server never receives.
7
+
8
+ ```bash
9
+ pip install burnedsecret-cli
10
+ bs auth login
11
+ echo "my-secret" | bs secret create
12
+ ```
13
+
14
+ ## Install
15
+
16
+ The CLI ships through four channels:
17
+
18
+ ```bash
19
+ # 1. pip (Python 3.9+)
20
+ pip install burnedsecret-cli
21
+
22
+ # 2. Homebrew (macOS / Linux)
23
+ brew install JensrudJ/burnedsecret/bs
24
+
25
+ # 3. Scoop (Windows)
26
+ scoop bucket add burnedsecret https://github.com/JensrudJ/scoop-burnedsecret
27
+ scoop install bs
28
+
29
+ # 4. Standalone binary (no Python needed)
30
+ curl -fsSL https://burnedsecret.com/install/cli/install.sh | sh # POSIX
31
+ irm https://burnedsecret.com/install/cli/install.ps1 | iex # Windows PowerShell
32
+ ```
33
+
34
+ > **macOS first run:** the standalone binary is currently shipped unsigned. On
35
+ > first launch macOS Gatekeeper may block it; right-click → Open, or
36
+ > `xattr -d com.apple.quarantine $(which bs)` to clear the quarantine flag.
37
+ > (Signed/notarized builds are a follow-up — see the v1 gaps below.)
38
+
39
+ ## Quickstart
40
+
41
+ ```bash
42
+ # 1. Authenticate (opens your browser to authorize this machine)
43
+ bs auth login
44
+
45
+ # 2. Create a secret — prints ONLY the share URL on stdout
46
+ url=$(echo "the launch codes" | bs secret create --ttl 1h)
47
+ echo "$url"
48
+
49
+ # 3. Read (and burn) it
50
+ bs secret read "$url"
51
+ ```
52
+
53
+ Because the share URL is the only thing on stdout, `url=$(... )` captures
54
+ exactly the URL; every prompt, table, and diagnostic goes to stderr.
55
+
56
+ ## Global options
57
+
58
+ | Option | Env var | Description |
59
+ | ------------------- | ------------------------ | -------------------------------------------------------- |
60
+ | `--profile NAME` | `BURNEDSECRET_PROFILE` | Select a config profile (default: `default`). |
61
+ | `--json` | — | Emit the raw SDK shape as JSON on stdout. |
62
+ | `--verbose` / `-v` | — | Extra diagnostics on stderr. |
63
+ | `--version` | — | Print the CLI version and exit. |
64
+
65
+ The API key is resolved as `BURNEDSECRET_API_KEY` (env) > the stored
66
+ `config[profile].api_key`.
67
+
68
+ ## Command reference
69
+
70
+ ### `bs secret create`
71
+
72
+ Creates a burn-after-reading secret and prints **only** the share URL.
73
+
74
+ | Flag | Description |
75
+ | -------------------------- | ------------------------------------------------------------------------ |
76
+ | `--content` / `-c` | Secret text. If omitted: stdin pipe → `--edit` → hidden prompt. |
77
+ | `--edit` / `-e` | Compose the secret in `$EDITOR`. |
78
+ | `--file` / `-f` | Encrypt a file as the secret (max 10 MB). Excludes text input. |
79
+ | `--passphrase` / `-p` | Add a PBKDF2 passphrase layer (bare flag prompts hidden). |
80
+ | `--max-views` | Reads before the secret burns (1..5, default 1). |
81
+ | `--ttl` | Time to live: `5m` / `1h` / `1d` / `7d` (default `1d`). |
82
+ | `--webhook-url` | Notify this URL when the secret is read. |
83
+ | `--notification-email` | Email this address when the secret is read. |
84
+
85
+ ```bash
86
+ echo "hunter2" | bs secret create --max-views 3 --ttl 7d
87
+ bs secret create --file ./id_rsa --ttl 1h
88
+ bs secret create --content "deploy key" --passphrase --ttl 1h # prompts for passphrase
89
+ ```
90
+
91
+ > **Web-viewer non-interop caveat:** secrets created with `--file` or
92
+ > `--passphrase` are decryptable only by an SDK/CLI client. Their share URL will
93
+ > **NOT** open in the burnedsecret.com web viewer. Plain text secrets without a
94
+ > passphrase remain web-viewer compatible. (After a `--file`/`--passphrase`
95
+ > create the CLI also prints a one-line reminder to stderr.)
96
+
97
+ ### `bs secret read`
98
+
99
+ Reads and **burns** a secret from its share URL.
100
+
101
+ | Flag | Description |
102
+ | --------------------- | ------------------------------------------------------------------- |
103
+ | `URL` (argument) | Share URL — **must** include the `#k=...` key fragment. |
104
+ | `--passphrase` / `-p` | Passphrase for a protected secret (bare flag prompts hidden). |
105
+ | `--output` / `-o` | Write file-secret bytes to PATH, or `-` to stream to stdout. |
106
+
107
+ ```bash
108
+ bs secret read "https://burnedsecret.com/s/abc#k=..."
109
+ bs secret read "$url" --passphrase hunter2
110
+ bs secret read "$url" --output ./downloaded.bin
111
+ ```
112
+
113
+ ### `bs request create | read`
114
+
115
+ Create a request-for-secret link, then read its fulfillment. The SDK generates an
116
+ RSA keypair locally; the **private key is never sent to the server and cannot be
117
+ regenerated** — without it the fulfillment is permanently unrecoverable. By
118
+ default `create` writes the key to `<config_dir>/requests/<id>.key` (absolute
119
+ path echoed to stderr).
120
+
121
+ | `create` flag | Description |
122
+ | ---------------------- | --------------------------------------------------------------------- |
123
+ | `--prompt` | What you are asking the recipient to send (required). |
124
+ | `--private-key` | Key destination: a `PATH`, `-` (stdout), or `env` (shell export). |
125
+ | `--fields` | Custom fields: `name:type:required;...` (type = text\|password\|textarea). |
126
+ | `--ttl` | `5m` / `1h` / `1d` / `7d`. |
127
+ | `--webhook-url` | Notify this URL when the request is fulfilled. |
128
+ | `--notification-email` | Email this address when the request is fulfilled. |
129
+
130
+ ```bash
131
+ url=$(bs request create --prompt "send the AWS key" --ttl 1d)
132
+ # ... recipient fulfills it via the link ...
133
+ bs request read <request-id> --private-key ~/.config/burnedsecret/requests/<id>.key
134
+ ```
135
+
136
+ `read` requires `--private-key PATH | - (stdin) | env`. Free-text fulfillments
137
+ print as text; custom-field fulfillments print as JSON.
138
+
139
+ ### `bs key create | list | revoke | whoami`
140
+
141
+ Key management is **browser/dashboard-assisted** in v1 (see the gaps below).
142
+
143
+ | Command | Behavior |
144
+ | ------------- | ------------------------------------------------------------------------------ |
145
+ | `key create` | Alias of `bs auth login` — the PKCE exchange mints the labeled key. |
146
+ | `key list` | Prints your dashboard `/keys` URL (view/revoke there). |
147
+ | `key revoke` | Prints the dashboard `/keys` URL; the CLI never network-revokes a key. |
148
+ | `key whoami` | Shows the stored `key_prefix`/`hostname`/`base_url` — **no network call**. |
149
+
150
+ The CLI never mints sibling keys from a stored key: minting from a leaked key
151
+ would be a privilege-escalation surface.
152
+
153
+ ### `bs auth login | logout | status`
154
+
155
+ | Command | Behavior |
156
+ | -------------- | ----------------------------------------------------------------------------- |
157
+ | `auth login` | Browser PKCE flow; stores a hostname-labeled key. The key is **never printed**. |
158
+ | `auth logout` | Forgets the local key (does **not** revoke server-side — use the dashboard). |
159
+ | `auth status` | Shows the profile's identity (configured?, source env/file, prefix, host). |
160
+
161
+ ### `bs config list-profiles`
162
+
163
+ Lists configured profiles with their `key_prefix`/`base_url`.
164
+
165
+ ## Configuration & storage
166
+
167
+ Config lives at:
168
+
169
+ - **Windows:** `%APPDATA%\burnedsecret\config.toml`
170
+ - **POSIX:** `${XDG_CONFIG_HOME:-~/.config}/burnedsecret/config.toml`
171
+
172
+ On POSIX the config (and any persisted request key) is written with `0600`
173
+ permissions so only the owner can read the stored API key. Request private keys
174
+ default to `<config_dir>/requests/<id>.key`.
175
+
176
+ Environment overrides: `BURNEDSECRET_API_KEY`, `BURNEDSECRET_PROFILE`.
177
+
178
+ ## Exit codes
179
+
180
+ Result data is always on stdout; diagnostics/errors/prompts are on stderr.
181
+
182
+ | Code | Meaning |
183
+ | ---- | ------------------------- |
184
+ | 0 | success |
185
+ | 1 | generic error |
186
+ | 2 | invalid arguments / usage |
187
+ | 3 | authentication error |
188
+ | 4 | not found |
189
+ | 5 | rate limited |
190
+
191
+ The same table is shown in `bs --help`; see [`docs/exit-codes.md`](docs/exit-codes.md)
192
+ for the canonical reference and scripting examples.
193
+
194
+ ## The browser-auth recipe (PKCE + loopback)
195
+
196
+ `bs auth login` implements a **reusable** pattern for authenticating a CLI/native
197
+ app through the browser without ever putting a secret in a URL or asking the user
198
+ to paste a token. It is a textbook implementation of **PKCE (RFC 7636)** plus the
199
+ **loopback-redirect native-app flow (RFC 8252)**, and the recipe transfers to any
200
+ CLI that needs browser auth — not just BurnedSecret:
201
+
202
+ 1. **Generate ephemeral secrets locally.** A random CSRF `state` and a PKCE pair:
203
+ `verifier = base64url(32 random bytes)`, `challenge = base64url(sha256(verifier))`
204
+ (S256). The verifier never leaves the machine; only the challenge is sent.
205
+ 2. **Bind a one-shot loopback server.** Grab a free `127.0.0.1:<port>` and start a
206
+ single-request HTTP server in a thread. This is the redirect target — no public
207
+ callback host, no firewall holes.
208
+ 3. **Open the browser to the consent page**, carrying only public values: the
209
+ loopback `callback`, the CSRF `state`, the S256 `code_challenge`, and a
210
+ `hostname` label for the key. The URL is also echoed to stderr so a headless or
211
+ no-default-browser user can paste it manually.
212
+ 4. **Capture and validate the callback.** The browser redirects to the loopback
213
+ server with `?code=&state=`. The handler **constant-comparison-checks the
214
+ returned `state`** against the one it generated — a mismatch is a hard abort
215
+ that captures no code, so no exchange ever happens. The callback carries only
216
+ `code` + `state`; the API key is **never** in any URL.
217
+ 5. **Exchange the single-use code for the key.** POST `{code, code_verifier}` to
218
+ the exchange endpoint. The server recomputes `sha256(verifier)` and
219
+ constant-time compares it to the stored challenge, proving the same client that
220
+ started the flow is finishing it. The **API key is returned only in the
221
+ response body**, then stored chmod-600 and never printed.
222
+
223
+ Security rationale, in one line each:
224
+
225
+ - **CSRF `state`** — prevents a forged callback from injecting an attacker's code.
226
+ - **PKCE `verifier`/`challenge`** — binds the code to this client; a stolen code
227
+ is useless without the verifier, which never traveled over the network.
228
+ - **Key never in the callback URL** — URLs leak (history, logs, referrers); the
229
+ key lives only in the exchange response body and on-disk chmod-600.
230
+ - **Single-use, short-TTL code** — narrows the window for replay.
231
+
232
+ ## Documented v1 gaps & follow-ups
233
+
234
+ These are deliberate v1 scope decisions, recorded here rather than silently
235
+ dropped:
236
+
237
+ - **Access-restriction flags are NOT in v1.** `--access-cidrs`,
238
+ `--access-countries`, and `--require-captcha` are not available — the shipped
239
+ REST `create_secret` shape has no such parameters; adding them needs an SDK
240
+ extension (D-06 deviation).
241
+ - **OS keyring storage is deferred to v2.** Keys are stored in a chmod-600
242
+ config file, not the OS keychain/credential manager.
243
+ - **`bs secret read --copy` (clipboard) is deferred.** Pipe to your platform's
244
+ clipboard tool (`pbcopy` / `clip` / `wl-copy`) in the meantime.
245
+ - **`bs secret create --bulk` is deferred.** Create secrets one at a time (or
246
+ loop in your shell) for now.
247
+ - **Polished tab-completion install is deferred.** Typer's completion machinery
248
+ is wired, but a one-command shell-completion installer is a follow-up.
249
+ - **`bs key list` / `bs key revoke` are browser/dashboard-assisted in v1.** They
250
+ print your dashboard `/keys` URL rather than calling an API-key-authenticated
251
+ keys endpoint (which would be a privilege-escalation surface). A
252
+ token-cache-backed headless listing is a v2 option.
253
+ - **Standalone macOS binaries are currently unsigned** (see the install note);
254
+ signing/notarization is a follow-up.
255
+ - **Chocolatey** is a deliberate fast-follow; Scoop covers the Windows
256
+ package-manager path for v1.
257
+
258
+ ### CI usage
259
+
260
+ Automation does **not** run the browser flow. CI uses a pre-minted
261
+ `BURNEDSECRET_API_KEY` env var; none of the `bs auth`/`bs key` commands are
262
+ needed in a pipeline.