@openparachute/hub 0.5.2 → 0.5.9-rc.6
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/package.json +1 -1
- package/src/__tests__/admin-clients.test.ts +275 -0
- package/src/__tests__/admin-handlers.test.ts +159 -320
- package/src/__tests__/admin-host-admin-token.test.ts +52 -4
- package/src/__tests__/api-me.test.ts +149 -0
- package/src/__tests__/api-mint-token.test.ts +381 -0
- package/src/__tests__/api-revocation-list.test.ts +198 -0
- package/src/__tests__/api-revoke-token.test.ts +320 -0
- package/src/__tests__/api-tokens.test.ts +629 -0
- package/src/__tests__/auth.test.ts +680 -16
- package/src/__tests__/expose-2fa-warning.test.ts +123 -0
- package/src/__tests__/expose-cloudflare.test.ts +101 -0
- package/src/__tests__/expose.test.ts +199 -340
- package/src/__tests__/hub-server.test.ts +986 -66
- package/src/__tests__/hub.test.ts +108 -55
- package/src/__tests__/install-source.test.ts +249 -0
- package/src/__tests__/install.test.ts +50 -31
- package/src/__tests__/jwt-sign.test.ts +205 -0
- package/src/__tests__/lifecycle.test.ts +97 -2
- package/src/__tests__/module-manifest.test.ts +48 -0
- package/src/__tests__/notes-serve.test.ts +154 -2
- package/src/__tests__/oauth-handlers.test.ts +1000 -3
- package/src/__tests__/operator-token.test.ts +379 -3
- package/src/__tests__/origin-check.test.ts +220 -0
- package/src/__tests__/port-assign.test.ts +41 -52
- package/src/__tests__/rate-limit.test.ts +190 -0
- package/src/__tests__/services-manifest.test.ts +341 -0
- package/src/__tests__/setup.test.ts +12 -9
- package/src/__tests__/status.test.ts +372 -0
- package/src/__tests__/well-known.test.ts +69 -0
- package/src/admin-clients.ts +139 -0
- package/src/admin-handlers.ts +63 -260
- package/src/admin-host-admin-token.ts +25 -10
- package/src/admin-login-ui.ts +256 -0
- package/src/admin-vault-admin-token.ts +1 -1
- package/src/api-me.ts +124 -0
- package/src/api-mint-token.ts +239 -0
- package/src/api-revocation-list.ts +59 -0
- package/src/api-revoke-token.ts +153 -0
- package/src/api-tokens.ts +224 -0
- package/src/commands/auth.ts +408 -51
- package/src/commands/expose-2fa-warning.ts +82 -0
- package/src/commands/expose-cloudflare.ts +27 -0
- package/src/commands/expose-public-auto.ts +3 -7
- package/src/commands/expose.ts +88 -173
- package/src/commands/install.ts +11 -13
- package/src/commands/lifecycle.ts +53 -4
- package/src/commands/status.ts +99 -8
- package/src/csrf.ts +6 -3
- package/src/help.ts +13 -7
- package/src/hub-db.ts +63 -0
- package/src/hub-server.ts +572 -106
- package/src/hub.ts +272 -149
- package/src/install-source.ts +291 -0
- package/src/jwt-sign.ts +265 -5
- package/src/module-manifest.ts +48 -10
- package/src/notes-serve.ts +70 -9
- package/src/oauth-handlers.ts +395 -29
- package/src/oauth-ui.ts +188 -0
- package/src/operator-token.ts +272 -18
- package/src/origin-check.ts +127 -0
- package/src/port-assign.ts +28 -35
- package/src/rate-limit.ts +166 -0
- package/src/scope-explanations.ts +33 -2
- package/src/service-spec.ts +58 -13
- package/src/services-manifest.ts +62 -3
- package/src/sessions.ts +19 -0
- package/src/well-known.ts +54 -1
- package/web/ui/dist/assets/index-Bv6Bq_wx.js +60 -0
- package/web/ui/dist/assets/index-D54otIhv.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/admin-config.test.ts +0 -281
- package/src/admin-config-ui.ts +0 -534
- package/src/admin-config.ts +0 -226
- package/web/ui/dist/assets/index-BKzPDdB0.js +0 -60
- package/web/ui/dist/assets/index-Dyk6g7vT.css +0 -1
|
@@ -136,6 +136,24 @@ export interface LifecycleOpts {
|
|
|
136
136
|
killWaitMs?: number;
|
|
137
137
|
/** Poll interval while waiting for SIGTERM to land. */
|
|
138
138
|
pollIntervalMs?: number;
|
|
139
|
+
/**
|
|
140
|
+
* How long `start` sleeps before re-checking `alive(pid)` to catch the
|
|
141
|
+
* spawn-then-immediately-die failure shape (hub#194: notes-serve crashed
|
|
142
|
+
* 50ms in on Bun.resolveSync, but `start` reported success because the
|
|
143
|
+
* spawn returned a pid). 250ms is the default in production — long
|
|
144
|
+
* enough to catch real silent-crashes (resolve failures, port
|
|
145
|
+
* collisions, missing args) without making `parachute start` feel
|
|
146
|
+
* laggy.
|
|
147
|
+
*
|
|
148
|
+
* Defaulting policy: if `alive` is not overridden, the settle defaults
|
|
149
|
+
* to 0 (skipped). Stub spawners hand back fake pids that the real
|
|
150
|
+
* `defaultAlive` would mark as dead, which would make every existing
|
|
151
|
+
* stub-spawner test fail spuriously. Tests that want to exercise the
|
|
152
|
+
* settle path inject both `alive` and `startSettleMs` explicitly.
|
|
153
|
+
* Production paths use the real `defaultAlive` and get the real 250ms
|
|
154
|
+
* settle.
|
|
155
|
+
*/
|
|
156
|
+
startSettleMs?: number;
|
|
139
157
|
/**
|
|
140
158
|
* Override the hub origin passed to services as PARACHUTE_HUB_ORIGIN. If
|
|
141
159
|
* unset, `start` derives it from `expose-state.json` (when exposed) or
|
|
@@ -168,6 +186,7 @@ interface Resolved {
|
|
|
168
186
|
log: (line: string) => void;
|
|
169
187
|
killWaitMs: number;
|
|
170
188
|
pollIntervalMs: number;
|
|
189
|
+
startSettleMs: number;
|
|
171
190
|
hubOrigin: string | undefined;
|
|
172
191
|
ensureHub: (opts: EnsureHubOpts) => Promise<EnsureHubResult>;
|
|
173
192
|
stopHubFn: (opts: StopHubOpts) => Promise<boolean>;
|
|
@@ -186,6 +205,14 @@ function resolve(opts: LifecycleOpts): Resolved {
|
|
|
186
205
|
log: opts.log ?? ((line) => console.log(line)),
|
|
187
206
|
killWaitMs: opts.killWaitMs ?? 10_000,
|
|
188
207
|
pollIntervalMs: opts.pollIntervalMs ?? 200,
|
|
208
|
+
// See `LifecycleOpts.startSettleMs` doc. Production (no spawner
|
|
209
|
+
// override, no alive override) gets the 250ms settle. Tests that
|
|
210
|
+
// inject a stub spawner without a stub alive get 0 — `defaultAlive`
|
|
211
|
+
// against a fake pid would always report dead and break unrelated
|
|
212
|
+
// tests. Tests that want to exercise the settle path explicitly
|
|
213
|
+
// override `alive`, which re-enables the default 250ms.
|
|
214
|
+
startSettleMs:
|
|
215
|
+
opts.startSettleMs ?? (opts.spawner === undefined || opts.alive !== undefined ? 250 : 0),
|
|
189
216
|
hubOrigin: resolveHubOrigin(opts.hubOrigin, configDir),
|
|
190
217
|
ensureHub: opts.hub?.ensureRunning ?? ensureHubRunning,
|
|
191
218
|
stopHubFn: opts.hub?.stop ?? stopHub,
|
|
@@ -371,16 +398,38 @@ export async function start(svc: string | undefined, opts: LifecycleOpts = {}):
|
|
|
371
398
|
if (entry.installDir) spawnerOpts.cwd = entry.installDir;
|
|
372
399
|
const passOpts =
|
|
373
400
|
spawnerOpts.env !== undefined || spawnerOpts.cwd !== undefined ? spawnerOpts : undefined;
|
|
401
|
+
let pid: number;
|
|
374
402
|
try {
|
|
375
|
-
|
|
376
|
-
writePid(short, pid, r.configDir);
|
|
377
|
-
r.log(`✓ ${short} started (pid ${pid}); logs: ${logFile}`);
|
|
378
|
-
if (r.hubOrigin) r.log(` ${HUB_ORIGIN_ENV}=${r.hubOrigin}`);
|
|
403
|
+
pid = r.spawner.spawn(cmd, logFile, passOpts);
|
|
379
404
|
} catch (err) {
|
|
380
405
|
failures++;
|
|
381
406
|
const msg = err instanceof Error ? err.message : String(err);
|
|
382
407
|
r.log(`✗ ${short} failed to start: ${msg}`);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
writePid(short, pid, r.configDir);
|
|
411
|
+
|
|
412
|
+
// Settle-poll for spawn-then-immediately-die (hub#194). A spawn returning
|
|
413
|
+
// a pid only proves the kernel forked the process; the child may exit
|
|
414
|
+
// microseconds later if its main code path throws before listening
|
|
415
|
+
// (e.g. notes-serve's Bun.resolveSync failing for bun-linked installs).
|
|
416
|
+
// Without this poll, we'd report success and the operator would chase
|
|
417
|
+
// a phantom 502.
|
|
418
|
+
if (r.startSettleMs > 0) {
|
|
419
|
+
await r.sleep(r.startSettleMs);
|
|
420
|
+
if (!r.alive(pid)) {
|
|
421
|
+
clearPid(short, r.configDir);
|
|
422
|
+
failures++;
|
|
423
|
+
r.log(
|
|
424
|
+
`✗ ${short} failed to start: spawned pid ${pid} but the process exited within ${r.startSettleMs}ms.`,
|
|
425
|
+
);
|
|
426
|
+
r.log(` Tail the log for details: tail -50 ${logFile}`);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
383
429
|
}
|
|
430
|
+
|
|
431
|
+
r.log(`✓ ${short} started (pid ${pid}); logs: ${logFile}`);
|
|
432
|
+
if (r.hubOrigin) r.log(` ${HUB_ORIGIN_ENV}=${r.hubOrigin}`);
|
|
384
433
|
}
|
|
385
434
|
return failures === 0 ? 0 : 1;
|
|
386
435
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { CONFIG_DIR, SERVICES_MANIFEST_PATH } from "../config.ts";
|
|
2
2
|
import { HUB_SVC, readHubPort } from "../hub-control.ts";
|
|
3
|
+
import {
|
|
4
|
+
type DetectInstallSourceDeps,
|
|
5
|
+
detectHubInstallSource,
|
|
6
|
+
detectInstallSource,
|
|
7
|
+
formatInstallSourceLabel,
|
|
8
|
+
isStale,
|
|
9
|
+
} from "../install-source.ts";
|
|
3
10
|
import { type AliveFn, defaultAlive, formatUptime, processState } from "../process-state.ts";
|
|
4
|
-
import { getSpec, shortNameForManifest } from "../service-spec.ts";
|
|
11
|
+
import { canonicalPortForManifest, getSpec, shortNameForManifest } from "../service-spec.ts";
|
|
5
12
|
import { type ServiceEntry, readManifest } from "../services-manifest.ts";
|
|
6
13
|
|
|
7
14
|
export type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
|
|
@@ -14,6 +21,19 @@ export interface StatusOpts {
|
|
|
14
21
|
configDir?: string;
|
|
15
22
|
alive?: AliveFn;
|
|
16
23
|
now?: () => Date;
|
|
24
|
+
/**
|
|
25
|
+
* Test seam for install-source detection. Production reads the filesystem
|
|
26
|
+
* + shells out to git; tests inject stubs so each case (npm / bun-linked /
|
|
27
|
+
* unknown / stale) is exercised deterministically without depending on
|
|
28
|
+
* the operator's actual bun globals.
|
|
29
|
+
*/
|
|
30
|
+
installSourceDeps?: DetectInstallSourceDeps;
|
|
31
|
+
/**
|
|
32
|
+
* Directory containing the running hub source. Defaults to `import.meta.dir`
|
|
33
|
+
* (the directory of this file). Tests override so the hub row's install
|
|
34
|
+
* source classification doesn't depend on the test runner's location.
|
|
35
|
+
*/
|
|
36
|
+
hubSrcDir?: string;
|
|
17
37
|
}
|
|
18
38
|
|
|
19
39
|
export interface ProbeResult {
|
|
@@ -71,9 +91,25 @@ interface StatusRow {
|
|
|
71
91
|
uptimeLabel: string;
|
|
72
92
|
healthLabel: string;
|
|
73
93
|
latencyLabel: string;
|
|
94
|
+
sourceLabel: string;
|
|
74
95
|
url: string | undefined;
|
|
75
96
|
healthy: boolean;
|
|
76
97
|
skipped: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Canonical-port drift warning. Set when the entry has a known canonical
|
|
100
|
+
* port (first-party / known short) AND the actual port differs. Surfaced
|
|
101
|
+
* as a continuation line under the row so operators see a silent miswire
|
|
102
|
+
* (e.g. parachute-hub#195: scribe + agent both at 1944) without us
|
|
103
|
+
* hard-erroring on a deliberate operator port change.
|
|
104
|
+
*/
|
|
105
|
+
driftWarning?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Version-drift indicator (hub#243). Set when a bun-linked service's
|
|
108
|
+
* `services.json.version` lags the live `package.json` version at its
|
|
109
|
+
* checkout. Surfaced as a continuation line so operators can spot a
|
|
110
|
+
* stale-after-rebuild row without comparing columns by eye.
|
|
111
|
+
*/
|
|
112
|
+
staleNote?: string;
|
|
77
113
|
}
|
|
78
114
|
|
|
79
115
|
/**
|
|
@@ -90,7 +126,13 @@ function urlForEntry(entry: ServiceEntry, short: string | undefined): string | u
|
|
|
90
126
|
return `http://127.0.0.1:${entry.port}${first}`;
|
|
91
127
|
}
|
|
92
128
|
|
|
93
|
-
function hubRow(
|
|
129
|
+
function hubRow(
|
|
130
|
+
configDir: string,
|
|
131
|
+
alive: AliveFn,
|
|
132
|
+
nowDate: Date,
|
|
133
|
+
hubSrcDir: string,
|
|
134
|
+
installSourceDeps: DetectInstallSourceDeps,
|
|
135
|
+
): StatusRow | undefined {
|
|
94
136
|
const proc = processState(HUB_SVC, configDir, alive);
|
|
95
137
|
if (proc.status === "unknown") return undefined;
|
|
96
138
|
const port = readHubPort(configDir);
|
|
@@ -99,15 +141,17 @@ function hubRow(configDir: string, alive: AliveFn, nowDate: Date): StatusRow | u
|
|
|
99
141
|
const pidLabel = proc.status === "running" && proc.pid !== undefined ? String(proc.pid) : "-";
|
|
100
142
|
const uptimeLabel =
|
|
101
143
|
proc.status === "running" && proc.startedAt ? formatUptime(proc.startedAt, nowDate) : "-";
|
|
144
|
+
const source = detectHubInstallSource(hubSrcDir, installSourceDeps);
|
|
102
145
|
return {
|
|
103
146
|
service: "parachute-hub (internal)",
|
|
104
147
|
port: portLabel,
|
|
105
|
-
version: "-",
|
|
148
|
+
version: source.livePackageVersion ?? "-",
|
|
106
149
|
processLabel,
|
|
107
150
|
pidLabel,
|
|
108
151
|
uptimeLabel,
|
|
109
152
|
healthLabel: "-",
|
|
110
153
|
latencyLabel: "-",
|
|
154
|
+
sourceLabel: formatInstallSourceLabel(source),
|
|
111
155
|
url: port !== undefined ? `http://127.0.0.1:${port}` : undefined,
|
|
112
156
|
healthy: true,
|
|
113
157
|
skipped: true,
|
|
@@ -122,6 +166,8 @@ export async function status(opts: StatusOpts = {}): Promise<number> {
|
|
|
122
166
|
const configDir = opts.configDir ?? CONFIG_DIR;
|
|
123
167
|
const alive = opts.alive ?? defaultAlive;
|
|
124
168
|
const now = opts.now ?? (() => new Date());
|
|
169
|
+
const installSourceDeps = opts.installSourceDeps ?? {};
|
|
170
|
+
const hubSrcDir = opts.hubSrcDir ?? import.meta.dir;
|
|
125
171
|
|
|
126
172
|
const manifest = readManifest(manifestPath);
|
|
127
173
|
if (manifest.services.length === 0) {
|
|
@@ -157,6 +203,30 @@ export async function status(opts: StatusOpts = {}): Promise<number> {
|
|
|
157
203
|
|
|
158
204
|
const url = urlForEntry(entry, short);
|
|
159
205
|
|
|
206
|
+
// Canonical-port drift detection (hub#195). Only fires for known
|
|
207
|
+
// first-party services where we have a canonical assignment. Third-party
|
|
208
|
+
// rows have no canonical to compare against. Warning is informational —
|
|
209
|
+
// operators may have moved a service off canonical deliberately.
|
|
210
|
+
// Note: multi-vault instance rows (`parachute-vault-<instance>`) don't
|
|
211
|
+
// match a canonical manifest name, so drift warnings don't fire for
|
|
212
|
+
// them. Intentional — see `canonicalPortForManifest` for the rationale.
|
|
213
|
+
const canonical = canonicalPortForManifest(entry.name);
|
|
214
|
+
const driftWarning =
|
|
215
|
+
canonical !== undefined && canonical !== entry.port
|
|
216
|
+
? `canonical port is ${canonical}`
|
|
217
|
+
: undefined;
|
|
218
|
+
|
|
219
|
+
// Install-source detection (hub#243). One filesystem walk + maybe one
|
|
220
|
+
// `git rev-parse` per row. Failures degrade silently to `unknown` —
|
|
221
|
+
// status output should never error out on a missing checkout dir.
|
|
222
|
+
const detectArgs: { entryName: string; installDir?: string } = { entryName: entry.name };
|
|
223
|
+
if (entry.installDir !== undefined) detectArgs.installDir = entry.installDir;
|
|
224
|
+
const source = detectInstallSource(detectArgs, installSourceDeps);
|
|
225
|
+
const sourceLabel = formatInstallSourceLabel(source);
|
|
226
|
+
const staleNote = isStale(entry.version, source)
|
|
227
|
+
? `STALE: services.json cached ${entry.version}; live package.json ${source.livePackageVersion}`
|
|
228
|
+
: undefined;
|
|
229
|
+
|
|
160
230
|
// Only skip probe when we know the process is dead (PID file was
|
|
161
231
|
// present but kill(pid, 0) failed). "unknown" status (no PID file)
|
|
162
232
|
// still probes — externally-managed services should report health.
|
|
@@ -170,9 +240,12 @@ export async function status(opts: StatusOpts = {}): Promise<number> {
|
|
|
170
240
|
uptimeLabel,
|
|
171
241
|
healthLabel: "-",
|
|
172
242
|
latencyLabel: "-",
|
|
243
|
+
sourceLabel,
|
|
173
244
|
url,
|
|
174
245
|
healthy: false,
|
|
175
246
|
skipped: true,
|
|
247
|
+
driftWarning,
|
|
248
|
+
staleNote,
|
|
176
249
|
};
|
|
177
250
|
}
|
|
178
251
|
|
|
@@ -191,19 +264,32 @@ export async function status(opts: StatusOpts = {}): Promise<number> {
|
|
|
191
264
|
uptimeLabel,
|
|
192
265
|
healthLabel,
|
|
193
266
|
latencyLabel: `${p.latencyMs}ms`,
|
|
267
|
+
sourceLabel,
|
|
194
268
|
url,
|
|
195
269
|
healthy: p.healthy,
|
|
196
270
|
skipped: false,
|
|
271
|
+
driftWarning,
|
|
272
|
+
staleNote,
|
|
197
273
|
};
|
|
198
274
|
}),
|
|
199
275
|
);
|
|
200
276
|
|
|
201
277
|
// Hub is an internal service — not in services.json, but users notice
|
|
202
278
|
// when it's dead. Only show it if we've seen it run.
|
|
203
|
-
const hub = hubRow(configDir, alive, nowDate);
|
|
279
|
+
const hub = hubRow(configDir, alive, nowDate, hubSrcDir, installSourceDeps);
|
|
204
280
|
if (hub) rows.push(hub);
|
|
205
281
|
|
|
206
|
-
const header = [
|
|
282
|
+
const header = [
|
|
283
|
+
"SERVICE",
|
|
284
|
+
"PORT",
|
|
285
|
+
"VERSION",
|
|
286
|
+
"PROCESS",
|
|
287
|
+
"PID",
|
|
288
|
+
"UPTIME",
|
|
289
|
+
"HEALTH",
|
|
290
|
+
"LATENCY",
|
|
291
|
+
"SOURCE",
|
|
292
|
+
];
|
|
207
293
|
const textRows = rows.map((r) => [
|
|
208
294
|
r.service,
|
|
209
295
|
r.port,
|
|
@@ -213,14 +299,17 @@ export async function status(opts: StatusOpts = {}): Promise<number> {
|
|
|
213
299
|
r.uptimeLabel,
|
|
214
300
|
r.healthLabel,
|
|
215
301
|
r.latencyLabel,
|
|
302
|
+
r.sourceLabel,
|
|
216
303
|
]);
|
|
217
304
|
const widths = header.map((_, i) =>
|
|
218
305
|
Math.max(header[i]?.length ?? 0, ...textRows.map((r) => r[i]?.length ?? 0)),
|
|
219
306
|
);
|
|
220
307
|
print(formatRow(header, widths));
|
|
221
|
-
// URL
|
|
222
|
-
// (vault's MCP path runs ~40 chars)
|
|
223
|
-
//
|
|
308
|
+
// URL, drift, and stale notes stay on continuation lines rather than
|
|
309
|
+
// columns. URLs are long (vault's MCP path runs ~40 chars); SOURCE labels
|
|
310
|
+
// can be long for bun-linked rows. Spreading them across columns would
|
|
311
|
+
// push the table well past 80 cols on every install — continuation lines
|
|
312
|
+
// keep the table scannable. The " → " / " ! " prefixes group visually
|
|
224
313
|
// with the row above without misleading the table widths.
|
|
225
314
|
for (let i = 0; i < textRows.length; i++) {
|
|
226
315
|
const cells = textRows[i];
|
|
@@ -228,6 +317,8 @@ export async function status(opts: StatusOpts = {}): Promise<number> {
|
|
|
228
317
|
if (!cells || !row) continue;
|
|
229
318
|
print(formatRow(cells, widths));
|
|
230
319
|
if (row.url) print(` → ${row.url}`);
|
|
320
|
+
if (row.driftWarning) print(` ! ${row.driftWarning}`);
|
|
321
|
+
if (row.staleNote) print(` ! ${row.staleNote}`);
|
|
231
322
|
}
|
|
232
323
|
|
|
233
324
|
/**
|
package/src/csrf.ts
CHANGED
|
@@ -19,9 +19,12 @@
|
|
|
19
19
|
* pre-login and post-login forms, and it works no matter how many tabs the
|
|
20
20
|
* operator has open.
|
|
21
21
|
*
|
|
22
|
-
* The cookie is HttpOnly
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* The cookie is HttpOnly: consumers receive the token value via either the
|
|
23
|
+
* server-rendered HTML form (cookie + embedded value, classic double-submit)
|
|
24
|
+
* or via the JSON body of `/api/me` (cookie alongside body — same pattern,
|
|
25
|
+
* just JSON instead of HTML). Neither path needs JS to read the cookie
|
|
26
|
+
* directly. SameSite=Lax (matches the session cookie), Secure, and Path=/
|
|
27
|
+
* (covers every admin form, OAuth flow, and `/api/me` consumer).
|
|
25
28
|
*
|
|
26
29
|
* Token entropy: 32 random bytes, base64url-encoded — same shape as session
|
|
27
30
|
* IDs. No HMAC needed: the value is opaque to the client and only ever
|
package/src/help.ts
CHANGED
|
@@ -44,9 +44,9 @@ Services:
|
|
|
44
44
|
What it does:
|
|
45
45
|
1. bun add -g @openparachute/<service>[@<tag>]
|
|
46
46
|
2. run any service-specific init (e.g. \`parachute-vault init\`)
|
|
47
|
-
3. assign a canonical port (1939–1949) and
|
|
48
|
-
\`~/.parachute
|
|
49
|
-
|
|
47
|
+
3. assign a canonical port (1939–1949) and reflect it in
|
|
48
|
+
\`~/.parachute/services.json\` — the single source of truth at boot
|
|
49
|
+
(services follow a 4-tier resolvePort ladder; services.json wins).
|
|
50
50
|
4. verify the service registered itself in ~/.parachute/services.json
|
|
51
51
|
5. for scribe in a TTY: prompt for transcription provider + API key
|
|
52
52
|
(or take \`--scribe-provider\` / \`--scribe-key\`)
|
|
@@ -124,7 +124,7 @@ Examples:
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export function statusHelp(): string {
|
|
127
|
-
return `parachute status — show installed services, process state,
|
|
127
|
+
return `parachute status — show installed services, process state, health, install source
|
|
128
128
|
|
|
129
129
|
Usage:
|
|
130
130
|
parachute status
|
|
@@ -133,22 +133,28 @@ What it does:
|
|
|
133
133
|
Reads ~/.parachute/services.json. For each registered service:
|
|
134
134
|
- checks PID file at ~/.parachute/<svc>/run/<svc>.pid → running/stopped
|
|
135
135
|
- probes http://localhost:<port><health> (skipped for known-stopped processes)
|
|
136
|
+
- classifies the install source as bun-linked (local checkout) or npm
|
|
136
137
|
|
|
137
138
|
Stopped services show "-" for health and don't count toward the exit
|
|
138
139
|
code — they're an expected state after fresh install before \`parachute
|
|
139
140
|
start\`. Running or externally-managed services that fail health checks
|
|
140
141
|
do exit 1.
|
|
141
142
|
|
|
143
|
+
A "STALE: services.json cached … live package.json …" continuation line
|
|
144
|
+
appears under a row when a bun-linked service has been rebuilt but the
|
|
145
|
+
manifest's cached version hasn't caught up — re-install (\`parachute
|
|
146
|
+
install <pkg>\`) refreshes the row.
|
|
147
|
+
|
|
142
148
|
Exit codes:
|
|
143
149
|
0 all probed services healthy (or none running)
|
|
144
150
|
1 one or more probed services unhealthy
|
|
145
151
|
|
|
146
152
|
Example:
|
|
147
153
|
$ parachute status
|
|
148
|
-
SERVICE PORT VERSION PROCESS PID UPTIME HEALTH LATENCY
|
|
149
|
-
parachute-vault 1940 0.2.4 running 12345 2h 13m ok 2ms
|
|
154
|
+
SERVICE PORT VERSION PROCESS PID UPTIME HEALTH LATENCY SOURCE
|
|
155
|
+
parachute-vault 1940 0.2.4 running 12345 2h 13m ok 2ms bun-linked → parachute-vault @ 8aa167b
|
|
150
156
|
→ http://127.0.0.1:1940/vault/default/mcp
|
|
151
|
-
parachute-notes 1942 0.0.1 stopped - - - -
|
|
157
|
+
parachute-notes 1942 0.0.1 stopped - - - - npm (0.3.15-rc.1)
|
|
152
158
|
→ http://127.0.0.1:1942/notes
|
|
153
159
|
`;
|
|
154
160
|
}
|
package/src/hub-db.ts
CHANGED
|
@@ -130,6 +130,69 @@ const MIGRATIONS: readonly Migration[] = [
|
|
|
130
130
|
CREATE INDEX tokens_family ON tokens (family_id) WHERE family_id IS NOT NULL;
|
|
131
131
|
`,
|
|
132
132
|
},
|
|
133
|
+
{
|
|
134
|
+
version: 6,
|
|
135
|
+
sql: `
|
|
136
|
+
-- Token registry generalization (closes hub#212 Phase 1). Until v6 the
|
|
137
|
+
-- tokens table only held OAuth refresh tokens; v6 generalizes it to a
|
|
138
|
+
-- single registry across every issued JWT class (refresh, access,
|
|
139
|
+
-- operator, mint-token). Three structural changes:
|
|
140
|
+
--
|
|
141
|
+
-- 1. user_id becomes NULLABLE. OAuth-issued rows still set it to the
|
|
142
|
+
-- caller's user (canonical identity field). CLI-minted /
|
|
143
|
+
-- operator-minted rows leave user_id NULL and put the operator/
|
|
144
|
+
-- service name in the new \`subject\` column.
|
|
145
|
+
-- 2. Three new columns: \`permissions\` (JSON, custom claim per
|
|
146
|
+
-- auth-architecture-shape.md §11.3), \`created_via\` (provenance
|
|
147
|
+
-- tag: oauth_refresh / cli_mint / operator_mint), \`subject\`
|
|
148
|
+
-- (non-user identity for service / operator mints).
|
|
149
|
+
-- 3. Existing rows backfill \`created_via='oauth_refresh'\` because
|
|
150
|
+
-- the table was OAuth-refresh-only before v6.
|
|
151
|
+
--
|
|
152
|
+
-- SQLite has no ALTER COLUMN to drop NOT NULL, so we use the
|
|
153
|
+
-- recreate-and-rename pattern. Inside the migration transaction the
|
|
154
|
+
-- whole swap is atomic; concurrent reads (there are none — hub is
|
|
155
|
+
-- single-writer) wouldn't see a half-state. FKs from tokens → users
|
|
156
|
+
-- stay enforced for non-NULL user_id values; nothing references
|
|
157
|
+
-- tokens, so the drop is safe.
|
|
158
|
+
CREATE TABLE tokens_new (
|
|
159
|
+
jti TEXT PRIMARY KEY,
|
|
160
|
+
user_id TEXT REFERENCES users(id),
|
|
161
|
+
client_id TEXT NOT NULL,
|
|
162
|
+
scopes TEXT NOT NULL,
|
|
163
|
+
refresh_token_hash TEXT,
|
|
164
|
+
family_id TEXT,
|
|
165
|
+
expires_at TEXT NOT NULL,
|
|
166
|
+
revoked_at TEXT,
|
|
167
|
+
created_at TEXT NOT NULL,
|
|
168
|
+
permissions TEXT,
|
|
169
|
+
created_via TEXT NOT NULL DEFAULT 'oauth_refresh',
|
|
170
|
+
subject TEXT
|
|
171
|
+
);
|
|
172
|
+
INSERT INTO tokens_new (
|
|
173
|
+
jti, user_id, client_id, scopes, refresh_token_hash, family_id,
|
|
174
|
+
expires_at, revoked_at, created_at,
|
|
175
|
+
permissions, created_via, subject
|
|
176
|
+
)
|
|
177
|
+
SELECT
|
|
178
|
+
jti, user_id, client_id, scopes, refresh_token_hash, family_id,
|
|
179
|
+
expires_at, revoked_at, created_at,
|
|
180
|
+
NULL, 'oauth_refresh', NULL
|
|
181
|
+
FROM tokens;
|
|
182
|
+
DROP TABLE tokens;
|
|
183
|
+
ALTER TABLE tokens_new RENAME TO tokens;
|
|
184
|
+
-- Recreate indexes (DROP TABLE took them with it).
|
|
185
|
+
CREATE INDEX tokens_user ON tokens (user_id) WHERE user_id IS NOT NULL;
|
|
186
|
+
CREATE INDEX tokens_active_refresh ON tokens (refresh_token_hash)
|
|
187
|
+
WHERE refresh_token_hash IS NOT NULL AND revoked_at IS NULL;
|
|
188
|
+
CREATE INDEX tokens_family ON tokens (family_id) WHERE family_id IS NOT NULL;
|
|
189
|
+
-- New: revocation list endpoint queries on (revoked_at, expires_at).
|
|
190
|
+
CREATE INDEX tokens_revoked ON tokens (revoked_at)
|
|
191
|
+
WHERE revoked_at IS NOT NULL;
|
|
192
|
+
-- Subject lookup for non-user mints (operator name, service name).
|
|
193
|
+
CREATE INDEX tokens_subject ON tokens (subject) WHERE subject IS NOT NULL;
|
|
194
|
+
`,
|
|
195
|
+
},
|
|
133
196
|
];
|
|
134
197
|
|
|
135
198
|
export function openHubDb(path: string = hubDbPath()): Database {
|