@openparachute/hub 0.5.10-rc.9 → 0.5.10
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-handlers.test.ts +141 -6
- package/src/__tests__/api-account.test.ts +463 -0
- package/src/__tests__/api-modules-ops.test.ts +74 -0
- package/src/__tests__/api-modules.test.ts +134 -0
- package/src/__tests__/api-users.test.ts +522 -0
- package/src/__tests__/cors.test.ts +587 -0
- package/src/__tests__/hub-db.test.ts +126 -1
- package/src/__tests__/hub-settings.test.ts +152 -0
- package/src/__tests__/jwt-sign.test.ts +59 -0
- package/src/__tests__/oauth-handlers.test.ts +912 -10
- package/src/__tests__/oauth-ui.test.ts +210 -0
- package/src/__tests__/scope-explanations.test.ts +23 -0
- package/src/__tests__/serve.test.ts +8 -1
- package/src/__tests__/setup-wizard.test.ts +216 -3
- package/src/__tests__/users.test.ts +196 -0
- package/src/__tests__/vault-names.test.ts +172 -0
- package/src/account-change-password-ui.ts +379 -0
- package/src/admin-handlers.ts +68 -2
- package/src/admin-host-admin-token.ts +5 -0
- package/src/admin-vault-admin-token.ts +7 -0
- package/src/api-account.ts +443 -0
- package/src/api-mint-token.ts +6 -0
- package/src/api-modules-ops.ts +15 -6
- package/src/api-modules.ts +101 -0
- package/src/api-users.ts +393 -0
- package/src/commands/auth.ts +10 -1
- package/src/commands/serve.ts +5 -1
- package/src/cors.ts +263 -0
- package/src/hub-db.ts +30 -0
- package/src/hub-server.ts +138 -18
- package/src/hub-settings.ts +98 -1
- package/src/jwt-sign.ts +17 -1
- package/src/oauth-handlers.ts +237 -29
- package/src/oauth-ui.ts +451 -38
- package/src/operator-token.ts +4 -0
- package/src/scope-explanations.ts +26 -1
- package/src/setup-wizard.ts +134 -16
- package/src/users.ts +210 -3
- package/src/vault-names.ts +57 -0
- package/web/ui/dist/assets/index-XhxYXDT5.js +61 -0
- package/web/ui/dist/assets/{index-D54otIhv.css → index-p6DkOcsk.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-AX_UHJ5e.js +0 -61
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
parseModulesPath,
|
|
14
14
|
} from "../api-modules-ops.ts";
|
|
15
15
|
import { hubDbPath, openHubDb } from "../hub-db.ts";
|
|
16
|
+
import { setModuleInstallChannel } from "../hub-settings.ts";
|
|
16
17
|
import { recordTokenMint, signAccessToken } from "../jwt-sign.ts";
|
|
17
18
|
import { rotateSigningKey } from "../signing-keys.ts";
|
|
18
19
|
import { type SpawnRequest, type SupervisedProc, Supervisor } from "../supervisor.ts";
|
|
@@ -298,6 +299,54 @@ describe("POST /api/modules/:short/install", () => {
|
|
|
298
299
|
expect(op.status).toBe("succeeded");
|
|
299
300
|
});
|
|
300
301
|
|
|
302
|
+
test("uses the rc channel when hub_settings.module_install_channel = rc (hub#275)", async () => {
|
|
303
|
+
// Operator's set the channel via the SPA toggle / env var bootstrap;
|
|
304
|
+
// the next install must construct `<pkg>@rc` rather than `<pkg>@latest`.
|
|
305
|
+
setModuleInstallChannel(h.db, "rc");
|
|
306
|
+
const { supervisor } = makeIdleSupervisor();
|
|
307
|
+
const { run, calls } = alwaysOkRun();
|
|
308
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
309
|
+
const res = await handleInstall(
|
|
310
|
+
postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
|
|
311
|
+
"vault",
|
|
312
|
+
{
|
|
313
|
+
db: h.db,
|
|
314
|
+
issuer: ISSUER,
|
|
315
|
+
manifestPath: h.manifestPath,
|
|
316
|
+
configDir: h.dir,
|
|
317
|
+
supervisor,
|
|
318
|
+
run,
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
expect(res.status).toBe(202);
|
|
322
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
323
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
324
|
+
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("toggling channel back to latest takes effect on next install (no restart)", async () => {
|
|
328
|
+
setModuleInstallChannel(h.db, "rc");
|
|
329
|
+
setModuleInstallChannel(h.db, "latest");
|
|
330
|
+
const { supervisor } = makeIdleSupervisor();
|
|
331
|
+
const { run, calls } = alwaysOkRun();
|
|
332
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
333
|
+
await handleInstall(
|
|
334
|
+
postReq("/api/modules/vault/install", { authorization: `Bearer ${bearer}` }),
|
|
335
|
+
"vault",
|
|
336
|
+
{
|
|
337
|
+
db: h.db,
|
|
338
|
+
issuer: ISSUER,
|
|
339
|
+
manifestPath: h.manifestPath,
|
|
340
|
+
configDir: h.dir,
|
|
341
|
+
supervisor,
|
|
342
|
+
run,
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
346
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
347
|
+
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
348
|
+
});
|
|
349
|
+
|
|
301
350
|
test("failed bun-add surfaces failed status on the operation", async () => {
|
|
302
351
|
const { supervisor } = makeIdleSupervisor();
|
|
303
352
|
// Run returns 1 + findGlobalInstall returns null = real failure.
|
|
@@ -416,6 +465,31 @@ describe("POST /api/modules/:short/upgrade", () => {
|
|
|
416
465
|
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
417
466
|
});
|
|
418
467
|
|
|
468
|
+
test("uses the rc channel when hub_settings.module_install_channel = rc (hub#275)", async () => {
|
|
469
|
+
setModuleInstallChannel(h.db, "rc");
|
|
470
|
+
const { supervisor } = makeIdleSupervisor();
|
|
471
|
+
await supervisor.start({ short: "vault", cmd: ["parachute-vault", "serve"] });
|
|
472
|
+
|
|
473
|
+
const { run, calls } = alwaysOkRun();
|
|
474
|
+
const bearer = await mintBearer(h, [API_MODULES_OPS_REQUIRED_SCOPE]);
|
|
475
|
+
const res = await handleUpgrade(
|
|
476
|
+
postReq("/api/modules/vault/upgrade", { authorization: `Bearer ${bearer}` }),
|
|
477
|
+
"vault",
|
|
478
|
+
{
|
|
479
|
+
db: h.db,
|
|
480
|
+
issuer: ISSUER,
|
|
481
|
+
manifestPath: h.manifestPath,
|
|
482
|
+
configDir: h.dir,
|
|
483
|
+
supervisor,
|
|
484
|
+
run,
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
expect(res.status).toBe(202);
|
|
488
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
489
|
+
expect(calls).toContainEqual(["bun", "add", "-g", "@openparachute/vault@rc"]);
|
|
490
|
+
expect(calls).not.toContainEqual(["bun", "add", "-g", "@openparachute/vault@latest"]);
|
|
491
|
+
});
|
|
492
|
+
|
|
419
493
|
test("fails with 'try install first' when module is installed but never supervised", async () => {
|
|
420
494
|
// Module has a services.json row (e.g. seeded by `parachute install`
|
|
421
495
|
// pre-supervisor era) but the supervisor never spawned it.
|
|
@@ -3,11 +3,14 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
6
|
+
API_MODULES_CHANNEL_REQUIRED_SCOPE,
|
|
6
7
|
API_MODULES_REQUIRED_SCOPE,
|
|
7
8
|
_clearLatestVersionCacheForTests,
|
|
8
9
|
handleApiModules,
|
|
10
|
+
handleApiModulesChannel,
|
|
9
11
|
} from "../api-modules.ts";
|
|
10
12
|
import { hubDbPath, openHubDb } from "../hub-db.ts";
|
|
13
|
+
import { getSetting, setModuleInstallChannel } from "../hub-settings.ts";
|
|
11
14
|
import { recordTokenMint, signAccessToken } from "../jwt-sign.ts";
|
|
12
15
|
import { rotateSigningKey } from "../signing-keys.ts";
|
|
13
16
|
import { type SpawnRequest, type SupervisedProc, Supervisor } from "../supervisor.ts";
|
|
@@ -289,4 +292,135 @@ describe("GET /api/modules", () => {
|
|
|
289
292
|
expect(callsAfterFirst).toBeGreaterThan(0);
|
|
290
293
|
expect(calls).toBe(callsAfterFirst);
|
|
291
294
|
});
|
|
295
|
+
|
|
296
|
+
test("surfaces module_install_channel in the response (hub#275)", async () => {
|
|
297
|
+
// Default — first read seeds with `latest`.
|
|
298
|
+
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
299
|
+
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
300
|
+
db: h.db,
|
|
301
|
+
issuer: ISSUER,
|
|
302
|
+
manifestPath: h.manifestPath,
|
|
303
|
+
fetchLatestVersion: async () => null,
|
|
304
|
+
});
|
|
305
|
+
expect(res.status).toBe(200);
|
|
306
|
+
const body = (await res.json()) as { module_install_channel: string };
|
|
307
|
+
expect(body.module_install_channel).toBe("latest");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("module_install_channel reflects toggled value on next GET", async () => {
|
|
311
|
+
setModuleInstallChannel(h.db, "rc");
|
|
312
|
+
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
313
|
+
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
314
|
+
db: h.db,
|
|
315
|
+
issuer: ISSUER,
|
|
316
|
+
manifestPath: h.manifestPath,
|
|
317
|
+
fetchLatestVersion: async () => null,
|
|
318
|
+
});
|
|
319
|
+
const body = (await res.json()) as { module_install_channel: string };
|
|
320
|
+
expect(body.module_install_channel).toBe("rc");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe("PUT /api/modules/channel — hub#275 channel toggle", () => {
|
|
325
|
+
let h: Harness;
|
|
326
|
+
|
|
327
|
+
beforeEach(async () => {
|
|
328
|
+
h = await makeHarness();
|
|
329
|
+
});
|
|
330
|
+
afterEach(() => h.cleanup());
|
|
331
|
+
|
|
332
|
+
function putReq(body: unknown, headers: Record<string, string> = {}): Request {
|
|
333
|
+
return new Request("http://localhost/api/modules/channel", {
|
|
334
|
+
method: "PUT",
|
|
335
|
+
headers: { "content-type": "application/json", ...headers },
|
|
336
|
+
body: typeof body === "string" ? body : JSON.stringify(body),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
test("405 on non-PUT", async () => {
|
|
341
|
+
const bearer = await mintBearer(h, [API_MODULES_CHANNEL_REQUIRED_SCOPE]);
|
|
342
|
+
const res = await handleApiModulesChannel(
|
|
343
|
+
new Request("http://localhost/api/modules/channel", {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: { authorization: `Bearer ${bearer}` },
|
|
346
|
+
}),
|
|
347
|
+
{ db: h.db, issuer: ISSUER },
|
|
348
|
+
);
|
|
349
|
+
expect(res.status).toBe(405);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("401 on missing bearer", async () => {
|
|
353
|
+
const res = await handleApiModulesChannel(putReq({ channel: "rc" }), {
|
|
354
|
+
db: h.db,
|
|
355
|
+
issuer: ISSUER,
|
|
356
|
+
});
|
|
357
|
+
expect(res.status).toBe(401);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("403 on bearer without parachute:host:admin", async () => {
|
|
361
|
+
// `:host:auth` reads the GET catalog — it must NOT be allowed to
|
|
362
|
+
// flip the install channel. Boundary matches install/upgrade/uninstall.
|
|
363
|
+
const bearer = await mintBearer(h, ["parachute:host:auth"]);
|
|
364
|
+
const res = await handleApiModulesChannel(
|
|
365
|
+
putReq({ channel: "rc" }, { authorization: `Bearer ${bearer}` }),
|
|
366
|
+
{ db: h.db, issuer: ISSUER },
|
|
367
|
+
);
|
|
368
|
+
expect(res.status).toBe(403);
|
|
369
|
+
const body = (await res.json()) as { error: string; error_description: string };
|
|
370
|
+
expect(body.error).toBe("insufficient_scope");
|
|
371
|
+
expect(body.error_description).toContain("parachute:host:admin");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("400 on malformed body (not JSON)", async () => {
|
|
375
|
+
const bearer = await mintBearer(h, [API_MODULES_CHANNEL_REQUIRED_SCOPE]);
|
|
376
|
+
const res = await handleApiModulesChannel(
|
|
377
|
+
putReq("not-json", { authorization: `Bearer ${bearer}` }),
|
|
378
|
+
{ db: h.db, issuer: ISSUER },
|
|
379
|
+
);
|
|
380
|
+
expect(res.status).toBe(400);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("400 on invalid channel value", async () => {
|
|
384
|
+
const bearer = await mintBearer(h, [API_MODULES_CHANNEL_REQUIRED_SCOPE]);
|
|
385
|
+
const res = await handleApiModulesChannel(
|
|
386
|
+
putReq({ channel: "stable" }, { authorization: `Bearer ${bearer}` }),
|
|
387
|
+
{ db: h.db, issuer: ISSUER },
|
|
388
|
+
);
|
|
389
|
+
expect(res.status).toBe(400);
|
|
390
|
+
const body = (await res.json()) as { error: string; error_description: string };
|
|
391
|
+
expect(body.error).toBe("invalid_channel");
|
|
392
|
+
expect(body.error_description).toMatch(/latest, rc/);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("400 on missing channel field", async () => {
|
|
396
|
+
const bearer = await mintBearer(h, [API_MODULES_CHANNEL_REQUIRED_SCOPE]);
|
|
397
|
+
const res = await handleApiModulesChannel(
|
|
398
|
+
putReq({ foo: "bar" }, { authorization: `Bearer ${bearer}` }),
|
|
399
|
+
{ db: h.db, issuer: ISSUER },
|
|
400
|
+
);
|
|
401
|
+
expect(res.status).toBe(400);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("200 + writes the new channel to hub_settings", async () => {
|
|
405
|
+
const bearer = await mintBearer(h, [API_MODULES_CHANNEL_REQUIRED_SCOPE]);
|
|
406
|
+
const res = await handleApiModulesChannel(
|
|
407
|
+
putReq({ channel: "rc" }, { authorization: `Bearer ${bearer}` }),
|
|
408
|
+
{ db: h.db, issuer: ISSUER },
|
|
409
|
+
);
|
|
410
|
+
expect(res.status).toBe(200);
|
|
411
|
+
const body = (await res.json()) as { channel: string };
|
|
412
|
+
expect(body.channel).toBe("rc");
|
|
413
|
+
expect(getSetting(h.db, "module_install_channel")).toBe("rc");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("200 + can toggle back to latest", async () => {
|
|
417
|
+
setModuleInstallChannel(h.db, "rc");
|
|
418
|
+
const bearer = await mintBearer(h, [API_MODULES_CHANNEL_REQUIRED_SCOPE]);
|
|
419
|
+
const res = await handleApiModulesChannel(
|
|
420
|
+
putReq({ channel: "latest" }, { authorization: `Bearer ${bearer}` }),
|
|
421
|
+
{ db: h.db, issuer: ISSUER },
|
|
422
|
+
);
|
|
423
|
+
expect(res.status).toBe(200);
|
|
424
|
+
expect(getSetting(h.db, "module_install_channel")).toBe("latest");
|
|
425
|
+
});
|
|
292
426
|
});
|