bravenlab 0.0.0-stage → 0.1.0
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.
- package/README.md +248 -2
- package/dist/auth.js +626 -0
- package/dist/auth.js.map +7 -0
- package/dist/bin.js +14313 -0
- package/dist/bin.js.map +7 -0
- package/dist/cli.js +8805 -0
- package/dist/cli.js.map +7 -0
- package/package.json +54 -4
package/README.md
CHANGED
|
@@ -1,3 +1,249 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bravenlab
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The `braven` command: **sign in once per machine** and let the CLI and the
|
|
4
|
+
Python SDK use the same identity.
|
|
5
|
+
|
|
6
|
+
This package owns no credential convention. The config file's path, its
|
|
7
|
+
version-1 schema and the OS keychain naming belong to
|
|
8
|
+
[`braven-identity`](../identity/README.md) (`packages/identity`), the contract
|
|
9
|
+
every surface mirrors — so a `braven login` here is the same login
|
|
10
|
+
`braven.login()` sees. The TypeScript implementation lives in
|
|
11
|
+
[`@braven/identity-store`](../identity-store/README.md), which this CLI and
|
|
12
|
+
`braven-agent` share; the Python package stays the reference.
|
|
13
|
+
|
|
14
|
+
Nothing in this package ever prints, logs or writes a secret: secrets live in
|
|
15
|
+
the OS keychain and are handed only to the store.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx bravenlab --help
|
|
21
|
+
npx bravenlab agent --folder ./my-data-folder --project-id <id>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The package installs two names for one bin: `npx bravenlab` (the package name)
|
|
25
|
+
and `braven` once it is installed globally. The old package name
|
|
26
|
+
`braven-cli@0.1.0` is superseded by `bravenlab`; the OAuth client id stays
|
|
27
|
+
`braven-cli` on the wire.
|
|
28
|
+
|
|
29
|
+
`braven` and `braven-agent` publish together from a `v*` tag
|
|
30
|
+
(`.github/workflows/release-npm.yml`); the tag must equal both manifests
|
|
31
|
+
(`scripts/check-release-tag.ts`), and the release drops the `workspace:` ranges
|
|
32
|
+
npm cannot resolve (`scripts/prune-workspace-deps.ts`) because the build inlines
|
|
33
|
+
those packages.
|
|
34
|
+
|
|
35
|
+
The workflow **stages** the release (`npm stage publish`) rather than publishing
|
|
36
|
+
it: a maintainer approves the staged version with 2FA — `npm stage approve
|
|
37
|
+
<stage-id>`, or the **Staged Packages** tab on npmjs.com — and only then does
|
|
38
|
+
`0.1.0` become installable. A brand-new name gets a public `0.0.0-stage`
|
|
39
|
+
placeholder while its real version waits.
|
|
40
|
+
|
|
41
|
+
The published artifact is a bundle, not the source tree: `braven` is
|
|
42
|
+
`dist/bin.js`, a plain ESM file with `#!/usr/bin/env node`, its third-party
|
|
43
|
+
dependencies external and declared in `dependencies`. `files` ships `dist/` and
|
|
44
|
+
this README only; source maps embed their sources, so a stack trace still points
|
|
45
|
+
into the TypeScript without publishing `src/`.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
nub run --filter bravenlab build # esbuild → dist/bin.js + dist/{cli,auth}.js
|
|
49
|
+
node dist/bin.js --help # exactly what an install runs
|
|
50
|
+
npm pack --dry-run # dist/ + README.md
|
|
51
|
+
nub exec -F braven braven --help # the workspace bin, after a build
|
|
52
|
+
nub run --filter bravenlab test # builds in `pretest`, then the suite
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`src/bin.test.ts` spawns `dist/bin.js` with an empty home directory, so a bundle
|
|
56
|
+
that drifts from the source (or loses its shebang or a runtime dependency) fails
|
|
57
|
+
the suite rather than a user. Development still runs the source directly — Node
|
|
58
|
+
24 strips its types — but `braven agent` resolves the `braven-agent` package, so
|
|
59
|
+
build the agent once first:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
nub run --filter braven-agent build
|
|
63
|
+
node --experimental-strip-types src/bin.ts --help
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Commands
|
|
67
|
+
|
|
68
|
+
| Command | What it does |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `braven login [--origin URL] [--web-origin URL]` | Opens the browser, waits for the signed-in session token on a loopback listener, proves it with `GET {origin}/api/auth/get-session`, stores it in the keychain as `kind="session"`, makes the account current, then prints whoami. **This is the whole sign-in: no password is typed into the terminal.** |
|
|
71
|
+
| `braven login --watcher` | Uses the sign-in the Braven app already made on this machine: the app (and the agent it runs) and the CLI share one store, so this only adopts the account and makes it current — no browser, no keychain prompt. Refuses when the app never signed in to the resolved server. |
|
|
72
|
+
| `braven login --api [--key bvn_api_…] [--email EMAIL]` | Stores a long-lived API key as `kind="apiKey"`; the key comes from `--key` or `$BRAVEN_API_KEY`. For CI, scripts and machines that cannot open a browser. |
|
|
73
|
+
| `braven login --device [--client-id ID]` | Better-auth's device authorization grant: prints a user code and verification URL, opens it, polls `/api/auth/device/token` on the server's interval, then proves and stores the session token exactly as the loopback flow does. For headless machines and machines without a reliable loopback callback. |
|
|
74
|
+
| `braven push <file> [--override] [--path PATH] [--project-id ID]` | Uploads one local file through `POST {origin}/files/ingest` with `X-Project-Id` and the stored credential as a bearer. `--path` defaults to the file's basename; `--override` reuses that path's Experiment/File instead of deduping or adding a sibling. |
|
|
75
|
+
| `braven key add [--origin URL] [--email EMAIL] [--label LABEL] <bvn_api_...>` | Stores a long-lived API key as `kind="apiKey"`. The owner comes from `--email`, or from `GET {origin}/api/auth/get-session` with the key as a bearer. |
|
|
76
|
+
| `braven whoami` | The effective identity: origin, email, kind, label. **Never the secret.** Honours `BRAVEN_SESSION_TOKEN` / `BRAVEN_API_KEY` and says "signed in from the environment" when one is in play. |
|
|
77
|
+
| `braven config` / `braven config show` | The stored accounts, which one is `current`, and the resolved `company` / `project` defaults. |
|
|
78
|
+
| `braven config set company\|project <value>` | Writes one default. |
|
|
79
|
+
| `braven config use <email>` | Switches `current`, wherever that account's origin is. |
|
|
80
|
+
| `braven logout [--email EMAIL]` | Forgets that account — keychain secret included (default: the current one), rotating `current` to a survivor. A stored session is revoked server-side; a stored API key is only forgotten. |
|
|
81
|
+
| `braven agent [agent flags]` | Runs the headless watch daemon (`braven-agent`) with the arguments handed over verbatim. Same program, same flags — `braven agent --help` prints the daemon's usage. |
|
|
82
|
+
|
|
83
|
+
`--origin` beats `$BRAVEN_API_URL`, which beats the hosted
|
|
84
|
+
`https://api.bravenlab.com`. A trailing slash is normalised away, so origins
|
|
85
|
+
never produce a doubled path. `--web-origin` beats `$BRAVEN_WEB_URL`, which
|
|
86
|
+
beats `https://bravenlab.com`.
|
|
87
|
+
|
|
88
|
+
## Three ways in
|
|
89
|
+
|
|
90
|
+
`braven login` picks the flow with a flag: `--watcher` (the running app's
|
|
91
|
+
sign-in), nothing/`--browser` (the loopback handoff), or `--api` (a stored key).
|
|
92
|
+
All three end in the same identity store, so `whoami`, `push`, `agent` and the
|
|
93
|
+
Python SDK see the same account.
|
|
94
|
+
|
|
95
|
+
## How `login` works
|
|
96
|
+
|
|
97
|
+
1. Bind an `http.Server` on `127.0.0.1` with **port 0**, and read the real
|
|
98
|
+
ephemeral port from `HttpServer.address` (narrowed to an inet address).
|
|
99
|
+
2. `GET /?token=<session token>` resolves an Effect `Deferred` and answers
|
|
100
|
+
`<h1>Authenticated. You can close this window.</h1>`; every other request is
|
|
101
|
+
`400`. The listener is loopback-only and lives in the command's scope, so it
|
|
102
|
+
closes on success, on the timeout, and on Ctrl-C.
|
|
103
|
+
3. Open `{webOrigin}/cli-login?port=<port>` with the `open` package. The web app
|
|
104
|
+
redirects the better-auth session token to the loopback URL.
|
|
105
|
+
4. Wait five minutes (`Effect.timeoutOrElse`). On timeout the listener closes
|
|
106
|
+
and the CLI fails with one actionable line.
|
|
107
|
+
5. With the token, call `GET {origin}/api/auth/get-session` with
|
|
108
|
+
`Authorization: Bearer <token>`. Only a `200` whose body has `user.email`
|
|
109
|
+
becomes an account; any other answer fails loudly and stores nothing.
|
|
110
|
+
|
|
111
|
+
## How `login --device` works
|
|
112
|
+
|
|
113
|
+
The same identity, without the token-in-a-URL handoff. It needs better-auth's
|
|
114
|
+
`deviceAuthorization()` plugin on the API — this repo configures it, along with
|
|
115
|
+
the `deviceCode` table and the `/device` approval page.
|
|
116
|
+
|
|
117
|
+
1. `POST {origin}/api/auth/device/code` with `{client_id, scope}`. The client
|
|
118
|
+
id is `--client-id`, `$BRAVEN_CLIENT_ID`, or `braven-cli`; the scope is
|
|
119
|
+
`openid profile email`.
|
|
120
|
+
2. Print the `user_code` and open
|
|
121
|
+
`verification_uri_complete ?? verification_uri` through the same `Browser`
|
|
122
|
+
seam.
|
|
123
|
+
3. Poll `POST {origin}/api/auth/device/token` with the RFC 8628 grant
|
|
124
|
+
(`grant_type`, `device_code`, `client_id`) on a `Schedule` seeded from the
|
|
125
|
+
server's `interval`. `authorization_pending` keeps polling; `slow_down` adds
|
|
126
|
+
five seconds to every subsequent interval. `expired_token`,
|
|
127
|
+
`access_denied` and `invalid_grant` stop with one actionable line.
|
|
128
|
+
4. The whole wait is bounded by `expires_in` (30 minutes when omitted) through
|
|
129
|
+
`Effect.timeoutOrElse`.
|
|
130
|
+
5. On success `access_token` is a better-auth session token: prove it with
|
|
131
|
+
`GET /api/auth/get-session`, store `{origin, kind: "session", secret, email}`
|
|
132
|
+
and print whoami. Nothing is stored on expiry, denial or failure.
|
|
133
|
+
|
|
134
|
+
## How `push` works
|
|
135
|
+
|
|
136
|
+
`files.ingest` is plan 015's one ingest wire. It is mounted as a plain
|
|
137
|
+
multipart route so scripts share one write path with the oRPC procedure the
|
|
138
|
+
webapp uses:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
POST {origin}/files/ingest
|
|
142
|
+
Authorization: Bearer <session token or API key>
|
|
143
|
+
X-Project-Id: <project>
|
|
144
|
+
Content-Type: multipart/form-data
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The body carries `path`, `override` (`"true"` / `"false"`) and the bytes as
|
|
148
|
+
`file`. `path` defaults to the file's basename; the server takes its basename
|
|
149
|
+
as the `filename` that keys the Experiment and File. Both doors end at
|
|
150
|
+
`saveUpload`, so hashing, dedup, versioning and `--override` have one
|
|
151
|
+
implementation (`apps/api/src/workerRoutes.ts`). Re-pushing the same bytes never
|
|
152
|
+
makes a second File in that Experiment; `--override` reuses the path's existing
|
|
153
|
+
Experiment instead of creating a sibling.
|
|
154
|
+
|
|
155
|
+
The project comes from `--project-id`, `$BRAVEN_PROJECT_ID`, or the store's
|
|
156
|
+
`defaults.project` resolved to an id through `GET /projects` (narrowed by
|
|
157
|
+
`defaults.company` when both are set). An unknown or ambiguous project name
|
|
158
|
+
fails with exit 2 instead of guessing; a server refusal is one line and exit 1.
|
|
159
|
+
|
|
160
|
+
## How `logout` works
|
|
161
|
+
|
|
162
|
+
The stored secret is read **before** the account is removed, so the command can
|
|
163
|
+
revoke it. Only a stored `session` is revoked — `POST
|
|
164
|
+
{origin}/api/auth/sign-out` with the token as a bearer; a `200` or a `401` both
|
|
165
|
+
mean it is dead. A `kind="apiKey"` account is only forgotten locally and stays
|
|
166
|
+
valid until it is revoked under **Settings → API keys**.
|
|
167
|
+
|
|
168
|
+
The keychain secret and config row then go, and `current` rotates to a survivor
|
|
169
|
+
(or none). Because the local account is already gone when revocation runs, an
|
|
170
|
+
unreachable API is a warning — "the session stays valid until it expires" — not
|
|
171
|
+
a failure; there is nothing left to retry. With no stored account the command
|
|
172
|
+
exits 3.
|
|
173
|
+
|
|
174
|
+
## Identity store
|
|
175
|
+
|
|
176
|
+
Config (never a secret), at the OS convention:
|
|
177
|
+
|
|
178
|
+
| OS | Path |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| macOS | `~/Library/Application Support/braven/config.json` |
|
|
181
|
+
| Linux | `${XDG_CONFIG_HOME:-~/.config}/braven/config.json` |
|
|
182
|
+
| Windows | `%APPDATA%\braven\config.json` |
|
|
183
|
+
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"version": 1,
|
|
187
|
+
"current": "https://api.bravenlab.com|me@example.com",
|
|
188
|
+
"accounts": [
|
|
189
|
+
{ "origin": "https://api.bravenlab.com", "email": "me@example.com", "kind": "session" }
|
|
190
|
+
],
|
|
191
|
+
"defaults": { "company": "Gelion" },
|
|
192
|
+
"watcher": {}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Writes are atomic (`config.json.tmp` + rename) and preserve `watcher` and the
|
|
197
|
+
other defaults. Secret, in the OS keychain via `@napi-rs/keyring` (macOS
|
|
198
|
+
Keychain, Windows Credential Manager, libsecret): service `braven`, account
|
|
199
|
+
`{origin}|{email}`. **No plaintext fallback**; a refused keychain surfaces as
|
|
200
|
+
one line and nothing is written.
|
|
201
|
+
|
|
202
|
+
Precedence for the secret is `env var > store`: `BRAVEN_SESSION_TOKEN` or
|
|
203
|
+
`BRAVEN_API_KEY` (with `BRAVEN_API_URL`) win and touch neither file nor
|
|
204
|
+
keychain. The origin is `flag > $BRAVEN_API_URL > hosted`; `--email` selects a
|
|
205
|
+
stored account.
|
|
206
|
+
|
|
207
|
+
## Exit codes
|
|
208
|
+
|
|
209
|
+
| Code | Meaning |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| `0` | It worked. Help and `--version` too. |
|
|
212
|
+
| `1` | The command failed: a rejected session, an unreachable API, a keychain that refused, a corrupt config file. |
|
|
213
|
+
| `2` | The arguments cannot work: an unknown flag, a missing one, a key without the `bvn_api_` prefix, a key whose owner the probe could not prove. |
|
|
214
|
+
| `3` | Not signed in, or no stored account for the email asked about — so a script can `braven login` and retry. |
|
|
215
|
+
|
|
216
|
+
Failures print exactly one `braven: <what to do>` line on stderr.
|
|
217
|
+
|
|
218
|
+
## Layout
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
src/
|
|
222
|
+
bin.ts # entry point: `agent` dispatch, then Node services + runMain
|
|
223
|
+
cli.ts # the effect/cli command tree and the exit-code boundary
|
|
224
|
+
commands.ts # login (browser handoff), whoami, config, logout, key add
|
|
225
|
+
auth.ts # loopback listener, device grant, session lookup + revoke
|
|
226
|
+
files.ts # files.ingest multipart wire + project resolution
|
|
227
|
+
testing.ts # browser/sessions/HTTP doubles (the store's doubles come from
|
|
228
|
+
# @braven/identity-store/testing)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The config file, its schema, the keychain (`Keychain`, `StoreConfig`,
|
|
232
|
+
`Environment`) and the `CliFailure` vocabulary live in
|
|
233
|
+
[`@braven/identity-store`](../identity-store/README.md), so the CLI and
|
|
234
|
+
`braven-agent` read one store instead of depending on each other. The seams the
|
|
235
|
+
CLI adds are `Browser`, `Sessions` and `HttpClient`. Tests replace all of them,
|
|
236
|
+
so the suite touches no real keychain, no real browser and no network except
|
|
237
|
+
the loopback listener the sign-in test deliberately starts.
|
|
238
|
+
|
|
239
|
+
`braven agent` is not an `effect/cli` command: `bin.ts` checks `argv[0]` first
|
|
240
|
+
and hands the remaining arguments to `runAgent` from `braven-agent`, so the
|
|
241
|
+
daemon's flags never have to be modelled twice.
|
|
242
|
+
|
|
243
|
+
## Development
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
nub run --filter bravenlab check-types
|
|
247
|
+
nub run --filter bravenlab test
|
|
248
|
+
nub run lint && nub run fmt
|
|
249
|
+
```
|