@openparachute/hub 0.3.0-rc.1 → 0.5.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/README.md +19 -17
- package/package.json +15 -4
- package/src/__tests__/admin-auth.test.ts +197 -0
- package/src/__tests__/admin-config.test.ts +281 -0
- package/src/__tests__/admin-grants.test.ts +271 -0
- package/src/__tests__/admin-handlers.test.ts +530 -0
- package/src/__tests__/admin-host-admin-token.test.ts +115 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +190 -0
- package/src/__tests__/admin-vaults.test.ts +615 -0
- package/src/__tests__/auth-codes.test.ts +253 -0
- package/src/__tests__/auth.test.ts +1063 -17
- package/src/__tests__/cli.test.ts +50 -0
- package/src/__tests__/clients.test.ts +264 -0
- package/src/__tests__/cloudflare-state.test.ts +167 -7
- package/src/__tests__/csrf.test.ts +117 -0
- package/src/__tests__/expose-cloudflare.test.ts +232 -37
- package/src/__tests__/expose-off-auto.test.ts +15 -9
- package/src/__tests__/expose-public-auto.test.ts +153 -0
- package/src/__tests__/expose.test.ts +216 -24
- package/src/__tests__/grants.test.ts +164 -0
- package/src/__tests__/hub-db.test.ts +153 -0
- package/src/__tests__/hub-server.test.ts +984 -26
- package/src/__tests__/hub.test.ts +56 -49
- package/src/__tests__/install.test.ts +327 -3
- package/src/__tests__/jwks.test.ts +37 -0
- package/src/__tests__/jwt-sign.test.ts +361 -0
- package/src/__tests__/lifecycle.test.ts +616 -5
- package/src/__tests__/module-manifest.test.ts +183 -0
- package/src/__tests__/oauth-handlers.test.ts +3112 -0
- package/src/__tests__/oauth-ui.test.ts +253 -0
- package/src/__tests__/operator-token.test.ts +140 -0
- package/src/__tests__/providers-detect.test.ts +158 -0
- package/src/__tests__/scope-explanations.test.ts +108 -0
- package/src/__tests__/scope-registry.test.ts +220 -0
- package/src/__tests__/services-manifest.test.ts +137 -1
- package/src/__tests__/sessions.test.ts +116 -0
- package/src/__tests__/setup.test.ts +361 -0
- package/src/__tests__/signing-keys.test.ts +153 -0
- package/src/__tests__/upgrade.test.ts +541 -0
- package/src/__tests__/users.test.ts +154 -0
- package/src/__tests__/well-known.test.ts +127 -10
- package/src/admin-auth.ts +126 -0
- package/src/admin-config-ui.ts +534 -0
- package/src/admin-config.ts +226 -0
- package/src/admin-grants.ts +160 -0
- package/src/admin-handlers.ts +365 -0
- package/src/admin-host-admin-token.ts +83 -0
- package/src/admin-vault-admin-token.ts +98 -0
- package/src/admin-vaults.ts +359 -0
- package/src/auth-codes.ts +189 -0
- package/src/cli.ts +202 -25
- package/src/clients.ts +210 -0
- package/src/cloudflare/config.ts +25 -6
- package/src/cloudflare/state.ts +108 -28
- package/src/commands/auth.ts +851 -19
- package/src/commands/expose-cloudflare.ts +85 -45
- package/src/commands/expose-interactive.ts +20 -44
- package/src/commands/expose-off-auto.ts +27 -11
- package/src/commands/expose-public-auto.ts +179 -0
- package/src/commands/expose.ts +63 -32
- package/src/commands/install.ts +337 -48
- package/src/commands/lifecycle.ts +269 -38
- package/src/commands/setup.ts +366 -0
- package/src/commands/status.ts +4 -1
- package/src/commands/upgrade.ts +429 -0
- package/src/csrf.ts +101 -0
- package/src/grants.ts +142 -0
- package/src/help.ts +133 -19
- package/src/hub-control.ts +12 -0
- package/src/hub-db.ts +164 -0
- package/src/hub-server.ts +643 -22
- package/src/hub.ts +97 -390
- package/src/jwks.ts +41 -0
- package/src/jwt-audience.ts +40 -0
- package/src/jwt-sign.ts +275 -0
- package/src/module-manifest.ts +435 -0
- package/src/oauth-handlers.ts +1175 -0
- package/src/oauth-ui.ts +582 -0
- package/src/operator-token.ts +129 -0
- package/src/providers/detect.ts +97 -0
- package/src/scope-explanations.ts +137 -0
- package/src/scope-registry.ts +158 -0
- package/src/service-spec.ts +270 -97
- package/src/services-manifest.ts +57 -1
- package/src/sessions.ts +115 -0
- package/src/signing-keys.ts +120 -0
- package/src/users.ts +144 -0
- package/src/well-known.ts +62 -26
- package/web/ui/dist/assets/index-BKzPDdB0.js +60 -0
- package/web/ui/dist/assets/index-Dyk6g7vT.css +1 -0
- package/web/ui/dist/index.html +14 -0
package/src/help.ts
CHANGED
|
@@ -6,15 +6,17 @@ export function topLevelHelp(): string {
|
|
|
6
6
|
return `parachute ${pkg.version} — top-level CLI for the Parachute ecosystem
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
|
+
parachute setup interactive walk-through: install services + configure
|
|
9
10
|
parachute install <service> install and register a service
|
|
10
11
|
services: ${services}
|
|
11
12
|
parachute status show installed services, process state, health
|
|
12
13
|
parachute start [service] start all services (or one) in the background
|
|
13
14
|
parachute stop [service] stop all services (or one) — SIGTERM then SIGKILL
|
|
14
15
|
parachute restart [service] stop + start
|
|
16
|
+
parachute upgrade [service] pull / re-install + restart (skips if no changes)
|
|
15
17
|
parachute logs <service> [-f] print service logs; -f to tail
|
|
16
|
-
parachute expose tailnet [off] HTTPS across your tailnet
|
|
17
|
-
parachute expose public [off] HTTPS on the public internet (
|
|
18
|
+
parachute expose tailnet [off] HTTPS across your tailnet (supported)
|
|
19
|
+
parachute expose public [off] HTTPS on the public internet (exploratory)
|
|
18
20
|
parachute migrate [--dry-run] archive legacy files at ecosystem root
|
|
19
21
|
parachute auth <cmd> identity (set password, manage 2FA)
|
|
20
22
|
parachute vault <args...> vault-specific ops (tokens, 2fa, config, init,
|
|
@@ -80,6 +82,47 @@ Aliases:
|
|
|
80
82
|
`;
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
export function setupHelp(): string {
|
|
86
|
+
return `parachute setup — interactive walk-through to install + configure services
|
|
87
|
+
|
|
88
|
+
Usage:
|
|
89
|
+
parachute setup [--tag <name>] [--no-start]
|
|
90
|
+
|
|
91
|
+
What it does:
|
|
92
|
+
1. surveys ~/.parachute/services.json — already-installed services are
|
|
93
|
+
reported, then skipped from the picker
|
|
94
|
+
2. shows a numbered multi-select for the remaining first-party services
|
|
95
|
+
(vault, notes, scribe; channel is exploratory and only offered by name)
|
|
96
|
+
3. pre-collects all interactive answers up front so the installs can run
|
|
97
|
+
without further prompting:
|
|
98
|
+
- vault: vault name (default \`default\`)
|
|
99
|
+
- scribe: transcription provider + API key for cloud providers
|
|
100
|
+
4. iterates \`parachute install <svc>\` per pick, threading the collected
|
|
101
|
+
answers and the shared --tag / --no-start flags
|
|
102
|
+
5. prints a summary banner with the running URLs (hub, vault, notes, scribe)
|
|
103
|
+
and a hint for connecting Claude Code
|
|
104
|
+
|
|
105
|
+
Behavior:
|
|
106
|
+
- Partial success is preserved: if one install fails, prior successful
|
|
107
|
+
installs are NOT rolled back. The exit code reflects the FIRST failure
|
|
108
|
+
(root cause), so subsequent fallout doesn't mask the original problem.
|
|
109
|
+
- Non-TTY / piped invocations should use \`parachute install <svc>\` per
|
|
110
|
+
service instead — \`setup\` assumes a terminal for the prompts.
|
|
111
|
+
- Selection accepts numbers (\`1,3\`), names (\`vault, notes\`), or \`all\`.
|
|
112
|
+
|
|
113
|
+
Flags:
|
|
114
|
+
--tag <name> npm dist-tag or exact version, applied to every install
|
|
115
|
+
in this walk-through (e.g. \`--tag rc\`)
|
|
116
|
+
--no-start skip the post-install daemon start for every service.
|
|
117
|
+
For CI / scripted bring-up.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
parachute setup # interactive walk-through
|
|
121
|
+
parachute setup --tag rc # bootstrap to the rc dist-tag
|
|
122
|
+
parachute setup --no-start # install without auto-starting (CI)
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
83
126
|
export function statusHelp(): string {
|
|
84
127
|
return `parachute status — show installed services, process state, and health
|
|
85
128
|
|
|
@@ -116,19 +159,34 @@ export function exposeHelp(): string {
|
|
|
116
159
|
Usage:
|
|
117
160
|
parachute expose tailnet [off]
|
|
118
161
|
parachute expose public [off]
|
|
162
|
+
parachute expose public --tailnet
|
|
119
163
|
parachute expose public --cloudflare --domain <hostname>
|
|
120
164
|
parachute expose public off --cloudflare
|
|
121
165
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
166
|
+
Status:
|
|
167
|
+
tailnet is the supported exposure shape. The hub's OAuth + per-module
|
|
168
|
+
scope work is being designed against tailnet first (already auth'd at
|
|
169
|
+
the network layer, every user's tailnet is their own).
|
|
170
|
+
public is exploratory — the flag still works for early testers, but
|
|
171
|
+
the public-internet posture (DNS, cross-internet OAuth, Funnel quirks)
|
|
172
|
+
hasn't been hardened. Prefer tailnet until public re-enters the
|
|
173
|
+
documented narrative post-OAuth.
|
|
174
|
+
|
|
175
|
+
Provider auto-detect (\`expose public\`):
|
|
176
|
+
- In a terminal with no provider flag, walks an interactive picker
|
|
177
|
+
(Tailscale Funnel vs. Cloudflare Tunnel), offers to install missing
|
|
178
|
+
dependencies on macOS, and prompts for the Cloudflare hostname when
|
|
179
|
+
needed.
|
|
180
|
+
- In a non-TTY (CI / piped), detects which provider is set up:
|
|
181
|
+
* exactly one configured → uses it.
|
|
182
|
+
* both configured → fails with a hint at --tailnet/--cloudflare.
|
|
183
|
+
* neither configured → fails with install pointers for both.
|
|
184
|
+
\`--skip-provider-check\` bypasses detection and falls through to
|
|
185
|
+
today's Tailscale-Funnel default (CI escape hatch).
|
|
128
186
|
|
|
129
187
|
Layers:
|
|
130
|
-
tailnet HTTPS across your tailnet (tailscale serve)
|
|
131
|
-
public HTTPS on the public internet
|
|
188
|
+
tailnet HTTPS across your tailnet (tailscale serve) — supported
|
|
189
|
+
public HTTPS on the public internet — exploratory
|
|
132
190
|
- default: Tailscale Funnel (no domain needed, *.ts.net URL)
|
|
133
191
|
- --cloudflare + --domain: named Cloudflare tunnel on your own
|
|
134
192
|
domain (stable URL, free, no bandwidth caps)
|
|
@@ -137,17 +195,25 @@ Tailscale and Cloudflare modes share no state. Either can be up without the
|
|
|
137
195
|
other. Inside each mode, switching on/off is idempotent.
|
|
138
196
|
|
|
139
197
|
Flags:
|
|
140
|
-
--hub-origin <url>
|
|
141
|
-
|
|
142
|
-
--
|
|
143
|
-
|
|
144
|
-
--
|
|
145
|
-
|
|
146
|
-
|
|
198
|
+
--hub-origin <url> override the OAuth issuer URL advertised to clients
|
|
199
|
+
(default: https://<fqdn> when exposed, else http://127.0.0.1:<hub-port>)
|
|
200
|
+
--tailnet pin \`expose public\` to Tailscale Funnel,
|
|
201
|
+
bypassing the picker / auto-detect
|
|
202
|
+
--cloudflare pin \`expose public\` to a named Cloudflare tunnel
|
|
203
|
+
(requires --domain)
|
|
204
|
+
--domain <hostname> fully-qualified hostname to route through the tunnel
|
|
205
|
+
(e.g. vault.example.com). The apex must be a zone on
|
|
206
|
+
your Cloudflare account.
|
|
207
|
+
--tunnel-name <name> Cloudflare tunnel name (default: \`parachute\`).
|
|
208
|
+
Use to coexist multiple named tunnels on one box.
|
|
209
|
+
--skip-provider-check bypass non-TTY auto-detect, default to Tailscale
|
|
210
|
+
Funnel as before. Intended for CI / scripts whose
|
|
211
|
+
environment is already pre-flighted.
|
|
147
212
|
|
|
148
213
|
Examples:
|
|
149
214
|
parachute expose tailnet # tailnet HTTPS
|
|
150
|
-
parachute expose public #
|
|
215
|
+
parachute expose public # auto-pick / picker
|
|
216
|
+
parachute expose public --tailnet # force Tailscale Funnel
|
|
151
217
|
parachute expose public off # stop the Funnel
|
|
152
218
|
parachute expose public --cloudflare --domain vault.example.com
|
|
153
219
|
# stable URL via cloudflared
|
|
@@ -177,6 +243,7 @@ export function startHelp(): string {
|
|
|
177
243
|
Usage:
|
|
178
244
|
parachute start start every installed service
|
|
179
245
|
parachute start <service> start just that one
|
|
246
|
+
parachute start hub start the internal hub (port 1939)
|
|
180
247
|
|
|
181
248
|
What it does:
|
|
182
249
|
For each target service, spawns its start command detached, redirects
|
|
@@ -187,16 +254,23 @@ What it does:
|
|
|
187
254
|
If a stale PID file exists (process died without cleanup), it's cleared
|
|
188
255
|
and the service starts fresh.
|
|
189
256
|
|
|
257
|
+
\`parachute start hub\` brings up the internal hub directly (normally
|
|
258
|
+
spawned implicitly by \`parachute expose\`). Useful when restarting a
|
|
259
|
+
hub that crashed without an active expose layer.
|
|
260
|
+
|
|
190
261
|
Flags:
|
|
191
262
|
--hub-origin <url> override PARACHUTE_HUB_ORIGIN passed to services
|
|
192
|
-
(default: current expose-state hub origin, else loopback)
|
|
263
|
+
(default: current expose-state hub origin, else loopback).
|
|
264
|
+
For \`start hub\`, also doubles as the hub's --issuer.
|
|
193
265
|
|
|
194
266
|
Examples:
|
|
195
267
|
parachute start bring everything up
|
|
196
268
|
parachute start vault just vault
|
|
269
|
+
parachute start hub just the internal hub
|
|
197
270
|
parachute logs vault watch what just started
|
|
198
271
|
|
|
199
272
|
Start commands by service:
|
|
273
|
+
hub bun <cli>/hub-server.ts --port <picked> ...
|
|
200
274
|
vault parachute-vault serve
|
|
201
275
|
scribe parachute-scribe serve
|
|
202
276
|
channel parachute-channel daemon
|
|
@@ -210,6 +284,7 @@ export function stopHelp(): string {
|
|
|
210
284
|
Usage:
|
|
211
285
|
parachute stop stop every installed service
|
|
212
286
|
parachute stop <service> stop just that one
|
|
287
|
+
parachute stop hub stop the internal hub
|
|
213
288
|
|
|
214
289
|
What it does:
|
|
215
290
|
Sends SIGTERM, waits up to 10s for a clean exit, then escalates to
|
|
@@ -217,9 +292,13 @@ What it does:
|
|
|
217
292
|
|
|
218
293
|
No-op if the service wasn't running.
|
|
219
294
|
|
|
295
|
+
Bare \`parachute stop\` (no service) does NOT stop the hub — that's
|
|
296
|
+
managed by the active expose layer (or \`parachute stop hub\` directly).
|
|
297
|
+
|
|
220
298
|
Examples:
|
|
221
299
|
parachute stop stop everything before sleep
|
|
222
300
|
parachute stop vault just vault
|
|
301
|
+
parachute stop hub just the internal hub
|
|
223
302
|
`;
|
|
224
303
|
}
|
|
225
304
|
|
|
@@ -229,18 +308,53 @@ export function restartHelp(): string {
|
|
|
229
308
|
Usage:
|
|
230
309
|
parachute restart restart every installed service
|
|
231
310
|
parachute restart <service> restart just that one
|
|
311
|
+
parachute restart hub restart the internal hub
|
|
232
312
|
|
|
233
313
|
What it does:
|
|
234
314
|
Equivalent to \`parachute stop <svc> && parachute start <svc>\`.
|
|
235
315
|
`;
|
|
236
316
|
}
|
|
237
317
|
|
|
318
|
+
export function upgradeHelp(): string {
|
|
319
|
+
return `parachute upgrade — pull / re-install + restart in one step
|
|
320
|
+
|
|
321
|
+
Usage:
|
|
322
|
+
parachute upgrade upgrade every installed service
|
|
323
|
+
parachute upgrade <service> upgrade just that one
|
|
324
|
+
parachute upgrade [svc] --tag <name>
|
|
325
|
+
npm-installed services only — pin a dist-tag
|
|
326
|
+
(default: latest). Ignored when bun-linked.
|
|
327
|
+
|
|
328
|
+
What it does:
|
|
329
|
+
Detects whether the target service is bun-linked from a local checkout
|
|
330
|
+
(the dev-mode shape) or npm-installed from a published artifact:
|
|
331
|
+
|
|
332
|
+
bun-linked git pull --ff-only in the checkout, bun install if
|
|
333
|
+
package.json/bun.lock changed, bun run build for frontend
|
|
334
|
+
modules with a build script, then \`parachute restart\`.
|
|
335
|
+
Refuses on a dirty working tree — commit or stash first.
|
|
336
|
+
|
|
337
|
+
npm bun add -g <pkg>@<tag>, then \`parachute restart\` if the
|
|
338
|
+
installed version actually moved.
|
|
339
|
+
|
|
340
|
+
Idempotent: if the source didn't change (HEAD unchanged after pull, or
|
|
341
|
+
package.json version unchanged after bun add -g), the restart is skipped.
|
|
342
|
+
Re-running on an up-to-date install is a fast no-op.
|
|
343
|
+
|
|
344
|
+
Examples:
|
|
345
|
+
parachute upgrade sweep every installed service
|
|
346
|
+
parachute upgrade vault just vault
|
|
347
|
+
parachute upgrade vault --tag rc pin the rc dist-tag (npm path only)
|
|
348
|
+
`;
|
|
349
|
+
}
|
|
350
|
+
|
|
238
351
|
export function logsHelp(): string {
|
|
239
352
|
return `parachute logs — print service logs
|
|
240
353
|
|
|
241
354
|
Usage:
|
|
242
355
|
parachute logs <service> print the last 200 lines
|
|
243
356
|
parachute logs <service> -f tail the log (like \`tail -f\`)
|
|
357
|
+
parachute logs hub logs for the internal hub
|
|
244
358
|
|
|
245
359
|
Log file:
|
|
246
360
|
~/.parachute/<service>/logs/<service>.log
|
package/src/hub-control.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createServer } from "node:net";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { CONFIG_DIR } from "./config.ts";
|
|
6
|
+
import { hubDbPath } from "./hub-db.ts";
|
|
6
7
|
import {
|
|
7
8
|
type AliveFn,
|
|
8
9
|
clearPid,
|
|
@@ -123,6 +124,14 @@ export interface EnsureHubOpts {
|
|
|
123
124
|
reservedPorts?: Iterable<number>;
|
|
124
125
|
/** How long to wait after spawn before claiming readiness. Short — tests set to 0. */
|
|
125
126
|
readyWaitMs?: number;
|
|
127
|
+
/**
|
|
128
|
+
* Public origin to use as the OAuth `iss` claim and as the base for the
|
|
129
|
+
* authorization-server metadata document. Forwarded to the hub server as
|
|
130
|
+
* `--issuer <url>`. When omitted, the hub falls back to the request's own
|
|
131
|
+
* origin — fine for loopback testing, wrong under tailscale where the
|
|
132
|
+
* request origin is `http://127.0.0.1:<port>`.
|
|
133
|
+
*/
|
|
134
|
+
issuer?: string;
|
|
126
135
|
log?: (line: string) => void;
|
|
127
136
|
}
|
|
128
137
|
|
|
@@ -180,6 +189,9 @@ export async function ensureHubRunning(opts: EnsureHubOpts = {}): Promise<Ensure
|
|
|
180
189
|
String(chosenPort),
|
|
181
190
|
"--well-known-dir",
|
|
182
191
|
wellKnownDir,
|
|
192
|
+
"--db",
|
|
193
|
+
hubDbPath(configDir),
|
|
194
|
+
...(opts.issuer ? ["--issuer", opts.issuer] : []),
|
|
183
195
|
];
|
|
184
196
|
const pid = spawner.spawn(cmd, logFile);
|
|
185
197
|
writePid(HUB_SVC, pid, configDir);
|
package/src/hub-db.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub-local SQLite database. Opens `~/.parachute/hub.db` (overridable via
|
|
3
|
+
* `$PARACHUTE_HOME`). Holds everything the hub owns as the ecosystem's OAuth
|
|
4
|
+
* issuer — signing keys (v1), users + opaque refresh tokens (v2), OAuth
|
|
5
|
+
* clients + auth-codes + grants + browser sessions (v3).
|
|
6
|
+
*
|
|
7
|
+
* Each open() runs `migrate()` to bring the schema up to date. A
|
|
8
|
+
* `schema_version` table records every applied migration so re-opens are
|
|
9
|
+
* cheap and idempotent. Migrations are append-only — never edit a prior
|
|
10
|
+
* entry; add a new one with a higher number.
|
|
11
|
+
*/
|
|
12
|
+
import { Database } from "bun:sqlite";
|
|
13
|
+
import { mkdirSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { CONFIG_DIR } from "./config.ts";
|
|
16
|
+
|
|
17
|
+
export function hubDbPath(configDir: string = CONFIG_DIR): string {
|
|
18
|
+
return join(configDir, "hub.db");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Migration {
|
|
22
|
+
version: number;
|
|
23
|
+
sql: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MIGRATIONS: readonly Migration[] = [
|
|
27
|
+
{
|
|
28
|
+
version: 1,
|
|
29
|
+
sql: `
|
|
30
|
+
CREATE TABLE signing_keys (
|
|
31
|
+
kid TEXT PRIMARY KEY,
|
|
32
|
+
public_key_pem TEXT NOT NULL,
|
|
33
|
+
private_key_pem TEXT NOT NULL,
|
|
34
|
+
algorithm TEXT NOT NULL,
|
|
35
|
+
created_at TEXT NOT NULL,
|
|
36
|
+
retired_at TEXT
|
|
37
|
+
);
|
|
38
|
+
CREATE INDEX signing_keys_active ON signing_keys (retired_at)
|
|
39
|
+
WHERE retired_at IS NULL;
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
version: 2,
|
|
44
|
+
sql: `
|
|
45
|
+
CREATE TABLE users (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
username TEXT UNIQUE NOT NULL,
|
|
48
|
+
password_hash TEXT NOT NULL,
|
|
49
|
+
created_at TEXT NOT NULL,
|
|
50
|
+
updated_at TEXT NOT NULL
|
|
51
|
+
);
|
|
52
|
+
CREATE TABLE tokens (
|
|
53
|
+
jti TEXT PRIMARY KEY,
|
|
54
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
55
|
+
client_id TEXT NOT NULL,
|
|
56
|
+
scopes TEXT NOT NULL,
|
|
57
|
+
refresh_token_hash TEXT,
|
|
58
|
+
expires_at TEXT NOT NULL,
|
|
59
|
+
revoked_at TEXT,
|
|
60
|
+
created_at TEXT NOT NULL
|
|
61
|
+
);
|
|
62
|
+
CREATE INDEX tokens_user ON tokens (user_id);
|
|
63
|
+
CREATE INDEX tokens_active_refresh ON tokens (refresh_token_hash)
|
|
64
|
+
WHERE refresh_token_hash IS NOT NULL AND revoked_at IS NULL;
|
|
65
|
+
`,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
version: 3,
|
|
69
|
+
sql: `
|
|
70
|
+
CREATE TABLE clients (
|
|
71
|
+
client_id TEXT PRIMARY KEY,
|
|
72
|
+
client_secret_hash TEXT,
|
|
73
|
+
redirect_uris TEXT NOT NULL,
|
|
74
|
+
scopes TEXT NOT NULL,
|
|
75
|
+
client_name TEXT,
|
|
76
|
+
registered_at TEXT NOT NULL
|
|
77
|
+
);
|
|
78
|
+
CREATE TABLE auth_codes (
|
|
79
|
+
code TEXT PRIMARY KEY,
|
|
80
|
+
client_id TEXT NOT NULL REFERENCES clients(client_id),
|
|
81
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
82
|
+
redirect_uri TEXT NOT NULL,
|
|
83
|
+
scopes TEXT NOT NULL,
|
|
84
|
+
code_challenge TEXT NOT NULL,
|
|
85
|
+
code_challenge_method TEXT NOT NULL,
|
|
86
|
+
expires_at TEXT NOT NULL,
|
|
87
|
+
used_at TEXT,
|
|
88
|
+
created_at TEXT NOT NULL
|
|
89
|
+
);
|
|
90
|
+
CREATE TABLE grants (
|
|
91
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
92
|
+
client_id TEXT NOT NULL REFERENCES clients(client_id),
|
|
93
|
+
scopes TEXT NOT NULL,
|
|
94
|
+
granted_at TEXT NOT NULL,
|
|
95
|
+
PRIMARY KEY (user_id, client_id)
|
|
96
|
+
);
|
|
97
|
+
CREATE TABLE sessions (
|
|
98
|
+
id TEXT PRIMARY KEY,
|
|
99
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
100
|
+
expires_at TEXT NOT NULL,
|
|
101
|
+
created_at TEXT NOT NULL
|
|
102
|
+
);
|
|
103
|
+
CREATE INDEX sessions_user ON sessions (user_id);
|
|
104
|
+
`,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
version: 4,
|
|
108
|
+
sql: `
|
|
109
|
+
-- DCR approval gate (closes #74). Public DCR was unauthenticated before
|
|
110
|
+
-- this migration; pre-existing rows are grandfathered as 'approved' so
|
|
111
|
+
-- already-trusted clients keep working. New rows default to 'pending'
|
|
112
|
+
-- unless the registrant authenticates with an operator token carrying
|
|
113
|
+
-- hub:admin scope.
|
|
114
|
+
ALTER TABLE clients ADD COLUMN status TEXT NOT NULL DEFAULT 'pending';
|
|
115
|
+
UPDATE clients SET status = 'approved';
|
|
116
|
+
CREATE INDEX clients_status ON clients (status);
|
|
117
|
+
`,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
version: 5,
|
|
121
|
+
sql: `
|
|
122
|
+
-- Refresh-token rotation + replay detection (closes #73). Each chain
|
|
123
|
+
-- of rotated refresh tokens shares a family_id; replaying a revoked
|
|
124
|
+
-- refresh token in a family signals theft and revokes every row in
|
|
125
|
+
-- that family (RFC 6819 §5.2.2.3). Pre-existing rows are backfilled
|
|
126
|
+
-- with their own jti as the family — grandfathered as singletons so
|
|
127
|
+
-- in-flight tokens keep working without spurious family revocation.
|
|
128
|
+
ALTER TABLE tokens ADD COLUMN family_id TEXT;
|
|
129
|
+
UPDATE tokens SET family_id = jti WHERE family_id IS NULL;
|
|
130
|
+
CREATE INDEX tokens_family ON tokens (family_id) WHERE family_id IS NOT NULL;
|
|
131
|
+
`,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
export function openHubDb(path: string = hubDbPath()): Database {
|
|
136
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
137
|
+
const db = new Database(path);
|
|
138
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
139
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
140
|
+
migrate(db);
|
|
141
|
+
return db;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function migrate(db: Database): void {
|
|
145
|
+
db.exec(`
|
|
146
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
147
|
+
version INTEGER PRIMARY KEY,
|
|
148
|
+
applied_at TEXT NOT NULL
|
|
149
|
+
);
|
|
150
|
+
`);
|
|
151
|
+
const applied = new Set<number>(
|
|
152
|
+
(db.query("SELECT version FROM schema_version").all() as { version: number }[]).map(
|
|
153
|
+
(r) => r.version,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
const insert = db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (?, ?)");
|
|
157
|
+
for (const m of MIGRATIONS) {
|
|
158
|
+
if (applied.has(m.version)) continue;
|
|
159
|
+
db.transaction(() => {
|
|
160
|
+
db.exec(m.sql);
|
|
161
|
+
insert.run(m.version, new Date().toISOString());
|
|
162
|
+
})();
|
|
163
|
+
}
|
|
164
|
+
}
|