@openparachute/hub 0.3.0-rc.1
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/LICENSE +661 -0
- package/README.md +284 -0
- package/package.json +31 -0
- package/src/__tests__/auth.test.ts +101 -0
- package/src/__tests__/auto-wire.test.ts +283 -0
- package/src/__tests__/cli.test.ts +192 -0
- package/src/__tests__/cloudflare-config.test.ts +54 -0
- package/src/__tests__/cloudflare-detect.test.ts +68 -0
- package/src/__tests__/cloudflare-state.test.ts +92 -0
- package/src/__tests__/cloudflare-tunnel.test.ts +207 -0
- package/src/__tests__/config.test.ts +18 -0
- package/src/__tests__/env-file.test.ts +125 -0
- package/src/__tests__/expose-auth-preflight.test.ts +201 -0
- package/src/__tests__/expose-cloudflare.test.ts +484 -0
- package/src/__tests__/expose-interactive.test.ts +703 -0
- package/src/__tests__/expose-last-provider.test.ts +113 -0
- package/src/__tests__/expose-off-auto.test.ts +269 -0
- package/src/__tests__/expose-state.test.ts +101 -0
- package/src/__tests__/expose.test.ts +1581 -0
- package/src/__tests__/hub-control.test.ts +346 -0
- package/src/__tests__/hub-server.test.ts +157 -0
- package/src/__tests__/hub.test.ts +116 -0
- package/src/__tests__/install.test.ts +1145 -0
- package/src/__tests__/lifecycle.test.ts +608 -0
- package/src/__tests__/migrate.test.ts +422 -0
- package/src/__tests__/notes-serve.test.ts +135 -0
- package/src/__tests__/port-assign.test.ts +178 -0
- package/src/__tests__/process-state.test.ts +140 -0
- package/src/__tests__/scribe-config.test.ts +193 -0
- package/src/__tests__/scribe-provider-interactive.test.ts +361 -0
- package/src/__tests__/services-manifest.test.ts +177 -0
- package/src/__tests__/status.test.ts +347 -0
- package/src/__tests__/tailscale-commands.test.ts +111 -0
- package/src/__tests__/tailscale-detect.test.ts +64 -0
- package/src/__tests__/vault-auth-status.test.ts +164 -0
- package/src/__tests__/vault-tokens-create-interactive.test.ts +183 -0
- package/src/__tests__/well-known.test.ts +214 -0
- package/src/auto-wire.ts +184 -0
- package/src/cli.ts +482 -0
- package/src/cloudflare/config.ts +58 -0
- package/src/cloudflare/detect.ts +58 -0
- package/src/cloudflare/state.ts +96 -0
- package/src/cloudflare/tunnel.ts +135 -0
- package/src/commands/auth.ts +69 -0
- package/src/commands/expose-auth-preflight.ts +217 -0
- package/src/commands/expose-cloudflare.ts +329 -0
- package/src/commands/expose-interactive.ts +428 -0
- package/src/commands/expose-off-auto.ts +199 -0
- package/src/commands/expose.ts +522 -0
- package/src/commands/install.ts +422 -0
- package/src/commands/lifecycle.ts +324 -0
- package/src/commands/migrate.ts +253 -0
- package/src/commands/scribe-provider-interactive.ts +269 -0
- package/src/commands/status.ts +238 -0
- package/src/commands/vault-tokens-create-interactive.ts +137 -0
- package/src/commands/vault.ts +17 -0
- package/src/config.ts +16 -0
- package/src/env-file.ts +76 -0
- package/src/expose-last-provider.ts +71 -0
- package/src/expose-state.ts +125 -0
- package/src/help.ts +279 -0
- package/src/hub-control.ts +254 -0
- package/src/hub-origin.ts +44 -0
- package/src/hub-server.ts +113 -0
- package/src/hub.ts +674 -0
- package/src/notes-serve.ts +135 -0
- package/src/port-assign.ts +125 -0
- package/src/process-state.ts +111 -0
- package/src/scribe-config.ts +149 -0
- package/src/service-spec.ts +296 -0
- package/src/services-manifest.ts +171 -0
- package/src/tailscale/commands.ts +41 -0
- package/src/tailscale/detect.ts +107 -0
- package/src/tailscale/run.ts +28 -0
- package/src/vault/auth-status.ts +179 -0
- package/src/well-known.ts +127 -0
package/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Parachute Hub
|
|
2
|
+
|
|
3
|
+
`@openparachute/hub` — the local hub for the [Parachute](https://parachute.computer) ecosystem. The `parachute` binary is one of its surfaces.
|
|
4
|
+
|
|
5
|
+
The hub coordinates the modules running on your machine: it installs them, runs them as background processes, exposes them over Tailscale, serves the discovery document at `/.well-known/parachute.json`, and (soon) issues OAuth tokens. Each module (vault, notes, scribe, channel, …) stays a standalone package; the hub stitches them together.
|
|
6
|
+
|
|
7
|
+
> Previously published as `@openparachute/cli`. Renamed 2026-04-26 to better reflect the role — see [parachute-patterns/hub-as-issuer](https://github.com/ParachuteComputer/parachute-patterns/blob/main/patterns/hub-as-issuer.md). The `parachute` binary name is unchanged.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
bun add -g @openparachute/hub
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Prereqs: [Bun](https://bun.sh) 1.3.0 or later. `parachute expose` also requires [Tailscale](https://tailscale.com/download) **1.82 or newer** (installed + `tailscale up` run once); the `expose` path is under active polish for launch, so expect rough edges.
|
|
16
|
+
|
|
17
|
+
## First 5 minutes
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# 1. Install the hub (one line — installs the `parachute` binary)
|
|
21
|
+
bun add -g @openparachute/hub
|
|
22
|
+
|
|
23
|
+
# 2. Install a service (runs `bun add -g @openparachute/vault` + `parachute-vault init`)
|
|
24
|
+
parachute install vault
|
|
25
|
+
|
|
26
|
+
# 3. Start the service in the background (PID + logs tracked under ~/.parachute/vault/)
|
|
27
|
+
parachute start vault
|
|
28
|
+
|
|
29
|
+
# 4. Check it landed — reads ~/.parachute/services.json, shows process state + probes health
|
|
30
|
+
parachute status
|
|
31
|
+
# SERVICE PORT VERSION PROCESS PID UPTIME HEALTH LATENCY
|
|
32
|
+
# parachute-vault 1940 0.2.4 running 12345 12s ok 2ms
|
|
33
|
+
|
|
34
|
+
# 5. Use it. Vault is up on 127.0.0.1:1940; Claude Code picked up the MCP
|
|
35
|
+
# on your next session. Point any other local MCP client (Codex, Goose,
|
|
36
|
+
# OpenCode, Cursor, Zed, Cline, your own agent) at:
|
|
37
|
+
# http://127.0.0.1:1940/vault/default/mcp
|
|
38
|
+
|
|
39
|
+
# 6. Expose beyond localhost — Tailscale Funnel or Cloudflare Tunnel.
|
|
40
|
+
# Polishing for broad launch, but live today for early testers:
|
|
41
|
+
parachute expose --help
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Tear down with `parachute expose tailnet off` or `parachute expose public off`. Layers are independent — `off` only affects the layer you name.
|
|
45
|
+
|
|
46
|
+
## Service lifecycle
|
|
47
|
+
|
|
48
|
+
`parachute start`, `stop`, `restart`, and `logs` manage services as background processes — no launchd, no manual `bun serve`, no hunting for PIDs.
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
parachute start # start every installed service
|
|
52
|
+
parachute start vault # just one
|
|
53
|
+
parachute stop # SIGTERM, then SIGKILL after 10s if stuck
|
|
54
|
+
parachute restart vault # stop + start
|
|
55
|
+
parachute logs vault # last 200 lines
|
|
56
|
+
parachute logs vault -f # tail (like `tail -f`)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
State lives under `~/.parachute/<service>/`:
|
|
60
|
+
|
|
61
|
+
- `run/<service>.pid` — child PID; `parachute status` uses this to report running/stopped + uptime
|
|
62
|
+
- `logs/<service>.log` — stdout + stderr (appended)
|
|
63
|
+
|
|
64
|
+
`parachute start` is idempotent: if the service is already running, it's a no-op. Stale PID files (process died without cleanup) are cleared on the next start. Services whose PID file is absent are treated as *unknown* — status still probes their port, so externally-managed services (e.g. you ran `parachute-vault serve` directly) aren't misreported as stopped.
|
|
65
|
+
|
|
66
|
+
### Migrating from launchd (pre-launch beta)
|
|
67
|
+
|
|
68
|
+
If you previously ran vault under launchd, switch to `parachute start`:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
launchctl unload ~/Library/LaunchAgents/computer.parachute.vault.plist
|
|
72
|
+
rm ~/Library/LaunchAgents/computer.parachute.vault.plist
|
|
73
|
+
parachute start vault
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
An at-login auto-start mode (`parachute start --boot`) is on the post-launch roadmap.
|
|
77
|
+
|
|
78
|
+
### Migrating from pre-CLI installs
|
|
79
|
+
|
|
80
|
+
If you've been running Parachute services by hand for a while, `~/.parachute/` may contain files from before the per-service restructure — top-level `daily.db`, `server.yaml`, a stray `logs/` directory, and so on. `parachute install` will print a one-line notice when it sees anything like that; run `parachute migrate` to sweep them:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
parachute migrate --dry-run # see the plan
|
|
84
|
+
parachute migrate # interactive (prompts before moving)
|
|
85
|
+
parachute migrate --yes # unattended
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Anything swept goes to `~/.parachute/.archive-<YYYY-MM-DD>/` with its original name — nothing is deleted. Recognized entries (per-service dirs, `services.json`, `expose-state.json`, `well-known/`) are left in place, and so is anything starting with a dot (so `.env` and prior `.archive-*` dirs are safe).
|
|
89
|
+
|
|
90
|
+
## Three layers of addressability
|
|
91
|
+
|
|
92
|
+
Each additive; each can be turned off without affecting the layer below.
|
|
93
|
+
|
|
94
|
+
- **Local** — services on loopback. Zero config. Browsers treat `localhost` as a secure context, so OAuth, PKCE, and Web Crypto all just work out of the box.
|
|
95
|
+
- **Tailnet** — `parachute expose tailnet` wraps `tailscale serve` for every registered service. HTTPS via Tailscale's MagicDNS cert. Only machines on your tailnet can reach the URL.
|
|
96
|
+
- **Public** — `parachute expose public` routes each handler through `tailscale funnel` so the same URLs become reachable from the public internet. At launch, Funnel is the only supported backend; Caddy + your-own-domain and cloudflared tunnels are planned post-launch.
|
|
97
|
+
|
|
98
|
+
Under the hood, tailnet mode uses `tailscale serve` and public mode uses `tailscale funnel`; both write into the same node-level serve config. The CLI records which layer is live so that `expose <other-layer> off` is a no-op rather than a surprise teardown of the active layer.
|
|
99
|
+
|
|
100
|
+
## Path-routing (and why)
|
|
101
|
+
|
|
102
|
+
Every service mounts under a path on a single canonical hostname. The root `/` is a hub page that auto-discovers everything installed on this node:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
https://parachute.<tailnet>.ts.net/ → hub (service directory)
|
|
106
|
+
https://parachute.<tailnet>.ts.net/vault/default → parachute-vault API
|
|
107
|
+
https://parachute.<tailnet>.ts.net/lens → parachute-lens
|
|
108
|
+
https://parachute.<tailnet>.ts.net/scribe → parachute-scribe
|
|
109
|
+
https://parachute.<tailnet>.ts.net/.well-known/parachute.json ← discovery
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The hub page fetches the discovery doc at load, then each service's `/.parachute/info` endpoint for display name, tagline, and icon. Adding a new service is zero CLI code — drop in its manifest entry and the hub picks it up.
|
|
113
|
+
|
|
114
|
+
Under the hood, `/` and `/.well-known/parachute.json` are proxied by a tiny internal HTTP server (`parachute-hub`) that `parachute expose` spawns on the loopback interface. Tailscale's file-serve mode is sandbox-restricted on macOS, so a localhost proxy is the portable shape. The hub process is stopped automatically when the last exposure layer is torn down; `parachute status` lists it under `(internal)`.
|
|
115
|
+
|
|
116
|
+
The `/.well-known/parachute.json` document is an always-present descriptor — flat `services[]` array that the hub iterates, plus top-level keys for legacy clients:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"vaults": [
|
|
121
|
+
{ "name": "default", "url": "https://parachute.taildf9ce2.ts.net/vault/default", "version": "0.2.4" }
|
|
122
|
+
],
|
|
123
|
+
"services": [
|
|
124
|
+
{
|
|
125
|
+
"name": "parachute-vault",
|
|
126
|
+
"url": "https://parachute.taildf9ce2.ts.net/vault/default",
|
|
127
|
+
"path": "/vault/default",
|
|
128
|
+
"version": "0.2.4",
|
|
129
|
+
"infoUrl": "https://parachute.taildf9ce2.ts.net/vault/default/.parachute/info"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"name": "parachute-lens",
|
|
133
|
+
"url": "https://parachute.taildf9ce2.ts.net/lens",
|
|
134
|
+
"path": "/lens",
|
|
135
|
+
"version": "0.0.1",
|
|
136
|
+
"infoUrl": "https://parachute.taildf9ce2.ts.net/lens/.parachute/info"
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"lens": { "url": "https://parachute.taildf9ce2.ts.net/lens", "version": "0.0.1" }
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Why path-routing and not subdomain-per-service? Two reasons:
|
|
144
|
+
|
|
145
|
+
1. **Tailscale Funnel HTTPS is capped at three ports per node** (443, 8443, 10000). Pinning every service to 443 behind a path means you can install any number of services without ever hitting that cap.
|
|
146
|
+
2. **Subdomain-per-service requires the Tailscale Services feature** (virtual-IP advertisement per service), which is more than a MagicDNS wildcard — it needs admin-side setup that's out of scope for a one-command install. When it's a launch-grade path, we'll add `parachute expose tailnet --mode subdomain`.
|
|
147
|
+
|
|
148
|
+
Funnel has bandwidth quotas on Tailscale's free tier. See [tailscale.com/kb/1223/funnel](https://tailscale.com/kb/1223/funnel) for current limits; for heavy traffic, the post-launch Caddy / cloudflared modes will be the answer.
|
|
149
|
+
|
|
150
|
+
## Ports
|
|
151
|
+
|
|
152
|
+
Parachute services reserve a block of loopback ports in the canonical range **1939–1949**. One range, one firewall rule, no surprises.
|
|
153
|
+
|
|
154
|
+
| Port | Service |
|
|
155
|
+
| ---- | ------------------ |
|
|
156
|
+
| 1939 | parachute-hub (internal proxy + static) |
|
|
157
|
+
| 1940 | parachute-vault |
|
|
158
|
+
| 1941 | parachute-channel |
|
|
159
|
+
| 1942 | parachute-notes |
|
|
160
|
+
| 1943 | parachute-scribe |
|
|
161
|
+
| 1944–1949 | *unassigned (CLI fallback range)* |
|
|
162
|
+
|
|
163
|
+
The hub pins 1939 — no fallback. If something else is on 1939 when you run `parachute expose`, the command fails with a pointer to `lsof -iTCP:1939` rather than walking up into another service's slot.
|
|
164
|
+
|
|
165
|
+
**The CLI is the port authority.** `parachute install <svc>` picks the port at install time and writes `PORT=<port>` into `~/.parachute/<svc>/.env`; lifecycle.start merges that .env into the spawn env so the next daemon boot binds the port the CLI assigned. The algorithm:
|
|
166
|
+
|
|
167
|
+
1. Prefer the canonical slot (e.g. vault → 1940).
|
|
168
|
+
2. On collision, walk the unassigned range (1944–1949).
|
|
169
|
+
3. Range exhausted: assign past 1949 with a warning.
|
|
170
|
+
|
|
171
|
+
Idempotent: an existing `PORT=` in `~/.parachute/<svc>/.env` wins, so re-installs and operator-edited ports survive across upgrades. Services keep their compiled-in fallbacks (vault → 1940 etc.) so a stand-alone `bun run` still works without a CLI-managed .env.
|
|
172
|
+
|
|
173
|
+
`parachute expose` probes every service's port at bringup. A service that isn't responding still gets exposed, but you get a `⚠ parachute-<svc> (port …) is not responding` line so proxied requests never silently 502 without explanation.
|
|
174
|
+
|
|
175
|
+
## How services register
|
|
176
|
+
|
|
177
|
+
Each Parachute service writes a manifest entry to `~/.parachute/services.json` on install. The CLI reads that manifest to drive `parachute status`, `parachute expose tailnet`, and `parachute expose public`.
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"services": [
|
|
182
|
+
{
|
|
183
|
+
"name": "parachute-vault",
|
|
184
|
+
"port": 1940,
|
|
185
|
+
"paths": ["/vault/default"],
|
|
186
|
+
"health": "/vault/default/health",
|
|
187
|
+
"version": "0.2.4"
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Optional `displayName` and `tagline` may be added to personalize the hub-page card; if absent, the hub falls back to the short name and the service's own `/.parachute/info` response.
|
|
194
|
+
|
|
195
|
+
The schema is a bit-for-bit contract shared between the CLI and every service. Services own their write side; the CLI owns the read + exposure side.
|
|
196
|
+
|
|
197
|
+
### Claiming `/` — legacy manifests
|
|
198
|
+
|
|
199
|
+
Pre-hub services wrote `paths: ["/"]` (when there was only one service at `/`). On `parachute expose`, any such entry is remapped in-memory to `/<shortname>` with a one-line warning; re-running `parachute install <svc>` updates the on-disk manifest permanently. The hub always owns `/`.
|
|
200
|
+
|
|
201
|
+
If you want the CLI (and every service you install) to use a config directory other than `~/.parachute`, set `PARACHUTE_HOME`:
|
|
202
|
+
|
|
203
|
+
```sh
|
|
204
|
+
export PARACHUTE_HOME=/some/other/path
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Already have parachute-vault installed?
|
|
208
|
+
|
|
209
|
+
Install the hub and `parachute vault ...` forwards to your existing `parachute-vault` binary:
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
bun add -g @openparachute/hub
|
|
213
|
+
parachute vault init # dispatches to parachute-vault init
|
|
214
|
+
parachute vault --help # dispatches to parachute-vault --help
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Nothing about your existing vault moves or needs reconfiguring.
|
|
218
|
+
|
|
219
|
+
## Smoke walkthrough (post-install)
|
|
220
|
+
|
|
221
|
+
Copy-paste to verify the whole chain. Everything here is idempotent.
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
# Install
|
|
225
|
+
bun add -g @openparachute/hub
|
|
226
|
+
|
|
227
|
+
# Verify CLI
|
|
228
|
+
parachute --version
|
|
229
|
+
parachute --help
|
|
230
|
+
|
|
231
|
+
# Install a service
|
|
232
|
+
parachute install vault
|
|
233
|
+
|
|
234
|
+
# Manifest should now exist
|
|
235
|
+
cat ~/.parachute/services.json
|
|
236
|
+
|
|
237
|
+
# Start it in the background
|
|
238
|
+
parachute start vault
|
|
239
|
+
|
|
240
|
+
# Status should show vault as running + healthy
|
|
241
|
+
parachute status
|
|
242
|
+
|
|
243
|
+
# Peek at the service's logs
|
|
244
|
+
parachute logs vault
|
|
245
|
+
|
|
246
|
+
# Expose across your tailnet (requires tailscale + `tailscale up`)
|
|
247
|
+
parachute expose tailnet
|
|
248
|
+
|
|
249
|
+
# Open the URL printed above in a browser on any tailnet peer.
|
|
250
|
+
# Also confirm the discovery document:
|
|
251
|
+
curl -s https://parachute.<tailnet>.ts.net/.well-known/parachute.json | jq .
|
|
252
|
+
|
|
253
|
+
# Flip to public (Funnel)
|
|
254
|
+
parachute expose public
|
|
255
|
+
# Open the same URL in a browser NOT on your tailnet — phone on cell, say.
|
|
256
|
+
|
|
257
|
+
# Tear down
|
|
258
|
+
parachute expose public off
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Subcommand reference
|
|
262
|
+
|
|
263
|
+
Run `parachute --help` for the top-level list, and `parachute <subcommand> --help` for details on any individual command.
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
parachute install <service> install and register a service
|
|
267
|
+
parachute status show installed services, process state, health
|
|
268
|
+
parachute start [service] start services in the background
|
|
269
|
+
parachute stop [service] stop services (SIGTERM → 10s → SIGKILL)
|
|
270
|
+
parachute restart [service] stop + start
|
|
271
|
+
parachute logs <service> [-f] print/tail service logs
|
|
272
|
+
parachute expose tailnet [off] HTTPS across your tailnet
|
|
273
|
+
parachute expose public [off] HTTPS on the public internet (Funnel)
|
|
274
|
+
parachute migrate [--dry-run] archive legacy files at ecosystem root
|
|
275
|
+
parachute vault <args...> dispatch to parachute-vault
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Status
|
|
279
|
+
|
|
280
|
+
Pre-alpha. API surface is stabilizing but not frozen.
|
|
281
|
+
|
|
282
|
+
## License
|
|
283
|
+
|
|
284
|
+
AGPL-3.0 — same as the rest of the Parachute ecosystem.
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openparachute/hub",
|
|
3
|
+
"version": "0.3.0-rc.1",
|
|
4
|
+
"description": "parachute — the local hub for the Parachute ecosystem (discovery, ports, lifecycle, soon OAuth).",
|
|
5
|
+
"license": "AGPL-3.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"module": "src/cli.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"parachute": "src/cli.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": ["src", "README.md", "LICENSE"],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/ParachuteComputer/parachute-hub.git"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "bun src/cli.ts",
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"lint": "biome check .",
|
|
20
|
+
"lint:fix": "biome check --write .",
|
|
21
|
+
"format": "biome format --write .",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@biomejs/biome": "^1.9.4",
|
|
26
|
+
"@types/bun": "latest"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"typescript": "^5"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { type Runner, auth, authHelp } from "../commands/auth.ts";
|
|
3
|
+
|
|
4
|
+
function makeRunner(result: number | (() => Promise<number>) = 0): {
|
|
5
|
+
runner: Runner;
|
|
6
|
+
calls: Array<readonly string[]>;
|
|
7
|
+
} {
|
|
8
|
+
const calls: Array<readonly string[]> = [];
|
|
9
|
+
const runner: Runner = {
|
|
10
|
+
async run(cmd) {
|
|
11
|
+
calls.push(cmd);
|
|
12
|
+
return typeof result === "function" ? await result() : result;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
return { runner, calls };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("parachute auth", () => {
|
|
19
|
+
test("set-password forwards to parachute-vault set-password", async () => {
|
|
20
|
+
const { runner, calls } = makeRunner(0);
|
|
21
|
+
const code = await auth(["set-password"], runner);
|
|
22
|
+
expect(code).toBe(0);
|
|
23
|
+
expect(calls).toEqual([["parachute-vault", "set-password"]]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("set-password --clear forwards the flag", async () => {
|
|
27
|
+
const { runner, calls } = makeRunner(0);
|
|
28
|
+
const code = await auth(["set-password", "--clear"], runner);
|
|
29
|
+
expect(code).toBe(0);
|
|
30
|
+
expect(calls).toEqual([["parachute-vault", "set-password", "--clear"]]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("2fa enroll forwards to parachute-vault 2fa enroll", async () => {
|
|
34
|
+
const { runner, calls } = makeRunner(0);
|
|
35
|
+
const code = await auth(["2fa", "enroll"], runner);
|
|
36
|
+
expect(code).toBe(0);
|
|
37
|
+
expect(calls).toEqual([["parachute-vault", "2fa", "enroll"]]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("2fa enroll --some-flag forwards every arg after the subcommand", async () => {
|
|
41
|
+
const { runner, calls } = makeRunner(0);
|
|
42
|
+
const code = await auth(["2fa", "enroll", "--some-flag", "value"], runner);
|
|
43
|
+
expect(code).toBe(0);
|
|
44
|
+
expect(calls).toEqual([["parachute-vault", "2fa", "enroll", "--some-flag", "value"]]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("exit code from parachute-vault is propagated", async () => {
|
|
48
|
+
const { runner } = makeRunner(3);
|
|
49
|
+
const code = await auth(["2fa", "status"], runner);
|
|
50
|
+
expect(code).toBe(3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("ENOENT surfaces install hint and exit 127", async () => {
|
|
54
|
+
const runner: Runner = {
|
|
55
|
+
async run() {
|
|
56
|
+
throw new Error("ENOENT: spawn parachute-vault");
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const code = await auth(["set-password"], runner);
|
|
60
|
+
expect(code).toBe(127);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("bogus subcommand exits 1 without spawning vault", async () => {
|
|
64
|
+
const { runner, calls } = makeRunner(0);
|
|
65
|
+
const code = await auth(["whoami"], runner);
|
|
66
|
+
expect(code).toBe(1);
|
|
67
|
+
expect(calls).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("no args prints help and exits 0 without spawning vault", async () => {
|
|
71
|
+
const { runner, calls } = makeRunner(0);
|
|
72
|
+
const code = await auth([], runner);
|
|
73
|
+
expect(code).toBe(0);
|
|
74
|
+
expect(calls).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("--help and help both route to the same help surface", async () => {
|
|
78
|
+
const { runner, calls } = makeRunner(0);
|
|
79
|
+
expect(await auth(["--help"], runner)).toBe(0);
|
|
80
|
+
expect(await auth(["-h"], runner)).toBe(0);
|
|
81
|
+
expect(await auth(["help"], runner)).toBe(0);
|
|
82
|
+
expect(calls).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("authHelp", () => {
|
|
87
|
+
const h = authHelp();
|
|
88
|
+
|
|
89
|
+
test("lists every blessed subcommand", () => {
|
|
90
|
+
expect(h).toContain("parachute auth set-password");
|
|
91
|
+
expect(h).toContain("--clear");
|
|
92
|
+
expect(h).toContain("parachute auth 2fa status");
|
|
93
|
+
expect(h).toContain("parachute auth 2fa enroll");
|
|
94
|
+
expect(h).toContain("parachute auth 2fa disable");
|
|
95
|
+
expect(h).toContain("parachute auth 2fa backup-codes");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("mentions the vault-install hint", () => {
|
|
99
|
+
expect(h).toContain("parachute install vault");
|
|
100
|
+
});
|
|
101
|
+
});
|