@openparachute/hub 0.6.5-rc.8 → 0.7.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/package.json +1 -1
- package/src/__tests__/account-setup.test.ts +310 -6
- package/src/__tests__/account-vault-admin-token.test.ts +35 -3
- package/src/__tests__/admin-channel-token.test.ts +173 -0
- package/src/__tests__/admin-connections-credentials.test.ts +1320 -0
- package/src/__tests__/admin-connections.test.ts +1154 -0
- package/src/__tests__/admin-csrf-belt.test.ts +346 -0
- package/src/__tests__/admin-module-token.test.ts +311 -0
- package/src/__tests__/admin-vaults.test.ts +590 -0
- package/src/__tests__/api-invites.test.ts +166 -6
- package/src/__tests__/api-modules-ops.test.ts +70 -5
- package/src/__tests__/api-modules.test.ts +262 -79
- package/src/__tests__/audience-gate.test.ts +752 -0
- package/src/__tests__/hub-db.test.ts +36 -0
- package/src/__tests__/hub-server.test.ts +585 -21
- package/src/__tests__/invites.test.ts +91 -1
- package/src/__tests__/lifecycle.test.ts +238 -3
- package/src/__tests__/module-manifest.test.ts +305 -8
- package/src/__tests__/serve-boot.test.ts +133 -2
- package/src/__tests__/service-spec-discovery.test.ts +109 -0
- package/src/__tests__/setup-gate.test.ts +13 -7
- package/src/__tests__/setup-wizard.test.ts +228 -1
- package/src/__tests__/vault-name.test.ts +20 -5
- package/src/__tests__/well-known.test.ts +44 -8
- package/src/__tests__/ws-bridge.test.ts +573 -0
- package/src/__tests__/ws-connection-caps.test.ts +456 -0
- package/src/account-setup.ts +94 -23
- package/src/account-vault-admin-token.ts +43 -14
- package/src/admin-channel-token.ts +135 -0
- package/src/admin-connections.ts +1882 -0
- package/src/admin-login-ui.ts +64 -15
- package/src/admin-module-token.ts +197 -0
- package/src/admin-vaults.ts +399 -12
- package/src/api-hub-upgrade.ts +4 -3
- package/src/api-invites.ts +92 -12
- package/src/api-modules-ops.ts +41 -16
- package/src/api-modules.ts +238 -116
- package/src/api-tokens.ts +8 -5
- package/src/audience-gate.ts +268 -0
- package/src/chrome-strip.ts +8 -1
- package/src/commands/lifecycle.ts +187 -47
- package/src/commands/serve-boot.ts +80 -3
- package/src/commands/setup.ts +4 -4
- package/src/connections-store.ts +191 -0
- package/src/grants.ts +50 -0
- package/src/help.ts +13 -6
- package/src/host-admin-token-validation.ts +6 -2
- package/src/hub-db.ts +26 -1
- package/src/hub-server.ts +849 -70
- package/src/invites.ts +91 -2
- package/src/jwt-sign.ts +47 -1
- package/src/module-manifest.ts +536 -23
- package/src/origin-check.ts +109 -0
- package/src/proxy-error-ui.ts +1 -1
- package/src/service-spec.ts +132 -41
- package/src/services-manifest.ts +97 -0
- package/src/setup-wizard.ts +68 -6
- package/src/users.ts +11 -0
- package/src/vault-name.ts +27 -7
- package/src/well-known.ts +41 -33
- package/src/ws-bridge.ts +256 -0
- package/src/ws-connection-caps.ts +170 -0
- package/web/ui/dist/assets/index-Cxtod68O.js +61 -0
- package/web/ui/dist/assets/index-E_9wqjEm.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/api-modules-config.test.ts +0 -882
- package/src/api-modules-config.ts +0 -421
- package/web/ui/dist/assets/index-BYYUeLGA.css +0 -1
- package/web/ui/dist/assets/index-D3cDUOOj.js +0 -61
|
@@ -214,14 +214,14 @@ describe("GET /api/modules", () => {
|
|
|
214
214
|
expect(res.status).toBe(401);
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
test("200 +
|
|
217
|
+
test("200 + full self-registration catalog on fresh container (empty services.json)", async () => {
|
|
218
218
|
// The v0.6 hot path: brand-new Render container, no services.json
|
|
219
|
-
// yet. UI
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// (
|
|
223
|
-
//
|
|
224
|
-
//
|
|
219
|
+
// yet. Post-2026-06-09 (modular-UI architecture, P2) discovery is driven
|
|
220
|
+
// by the UNION of the bootstrap registries (KNOWN_MODULES ∪
|
|
221
|
+
// FIRST_PARTY_FALLBACKS), NOT a curated whitelist. Every known module
|
|
222
|
+
// surfaces — core (vault/scribe/surface) in the headline tier, the rest
|
|
223
|
+
// (channel/runner/notes) as `experimental` — so the channel-not-installed
|
|
224
|
+
// class (running but invisible) can't recur.
|
|
225
225
|
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
226
226
|
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
227
227
|
db: h.db,
|
|
@@ -233,16 +233,32 @@ describe("GET /api/modules", () => {
|
|
|
233
233
|
const body = (await res.json()) as {
|
|
234
234
|
modules: Array<{
|
|
235
235
|
short: string;
|
|
236
|
+
focus: "core" | "experimental";
|
|
236
237
|
available: boolean;
|
|
237
238
|
installed: boolean;
|
|
238
239
|
latest_version: string | null;
|
|
239
240
|
}>;
|
|
240
241
|
supervisor_available: boolean;
|
|
241
242
|
};
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
expect(
|
|
243
|
+
const shorts = body.modules.map((m) => m.short);
|
|
244
|
+
// The core tier leads, in the recommended install order (vault → scribe),
|
|
245
|
+
// ahead of every experimental module.
|
|
246
|
+
expect(shorts.indexOf("vault")).toBeLessThan(shorts.indexOf("scribe"));
|
|
247
|
+
expect(shorts.indexOf("scribe")).toBeLessThan(shorts.indexOf("channel"));
|
|
248
|
+
expect(shorts.indexOf("scribe")).toBeLessThan(shorts.indexOf("runner"));
|
|
249
|
+
// Every known module is discoverable — vault/scribe/surface (core) +
|
|
250
|
+
// channel/runner/notes (experimental).
|
|
251
|
+
for (const s of ["vault", "scribe", "surface", "channel", "runner", "notes"]) {
|
|
252
|
+
expect(shorts).toContain(s);
|
|
253
|
+
}
|
|
254
|
+
// Focus tier resolves from the default map.
|
|
255
|
+
const byShort = new Map(body.modules.map((m) => [m.short, m]));
|
|
256
|
+
expect(byShort.get("vault")?.focus).toBe("core");
|
|
257
|
+
expect(byShort.get("scribe")?.focus).toBe("core");
|
|
258
|
+
expect(byShort.get("surface")?.focus).toBe("core");
|
|
259
|
+
expect(byShort.get("channel")?.focus).toBe("experimental");
|
|
260
|
+
expect(byShort.get("runner")?.focus).toBe("experimental");
|
|
261
|
+
expect(byShort.get("notes")?.focus).toBe("experimental");
|
|
246
262
|
expect(body.modules.every((m) => m.available)).toBe(true);
|
|
247
263
|
expect(body.modules.every((m) => !m.installed)).toBe(true);
|
|
248
264
|
expect(body.modules.every((m) => m.latest_version === "0.9.9")).toBe(true);
|
|
@@ -288,39 +304,56 @@ describe("GET /api/modules", () => {
|
|
|
288
304
|
expect(scribe?.available).toBe(true);
|
|
289
305
|
});
|
|
290
306
|
|
|
291
|
-
test("
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
307
|
+
test("channel (running + self-registered) appears as installed + experimental — regression for the channel-not-installed bug", async () => {
|
|
308
|
+
// THE bug this PR fixes (2026-06-09 modular-UI architecture, P2): channel
|
|
309
|
+
// was running, proxied, supervised, and self-registered in services.json
|
|
310
|
+
// yet invisible on the Modules screen — because the old CURATED_MODULES =
|
|
311
|
+
// ["vault","scribe"] whitelist gated discovery. Now discovery is driven by
|
|
312
|
+
// self-registration ∪ the known registries, so a self-registered channel
|
|
313
|
+
// row surfaces as installed, in the experimental tier, with its run-state.
|
|
314
|
+
writeManifest(h.manifestPath, [
|
|
315
|
+
{
|
|
316
|
+
name: "parachute-channel",
|
|
317
|
+
port: 1941,
|
|
318
|
+
paths: ["/channel"],
|
|
319
|
+
health: "/channel/health",
|
|
320
|
+
version: "0.3.1",
|
|
321
|
+
},
|
|
322
|
+
]);
|
|
323
|
+
const { supervisor } = makeIdleSupervisor();
|
|
324
|
+
await supervisor.start({ short: "channel", cmd: ["parachute-channel", "daemon"] });
|
|
325
|
+
|
|
298
326
|
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
299
327
|
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
300
328
|
db: h.db,
|
|
301
329
|
issuer: ISSUER,
|
|
302
330
|
manifestPath: h.manifestPath,
|
|
331
|
+
supervisor,
|
|
303
332
|
fetchLatestVersion: async () => null,
|
|
304
333
|
});
|
|
305
|
-
const body = (await res.json()) as {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
expect(
|
|
316
|
-
expect(
|
|
317
|
-
expect(
|
|
334
|
+
const body = (await res.json()) as {
|
|
335
|
+
modules: Array<{
|
|
336
|
+
short: string;
|
|
337
|
+
focus: "core" | "experimental";
|
|
338
|
+
installed: boolean;
|
|
339
|
+
installed_version: string | null;
|
|
340
|
+
supervisor_status: string | null;
|
|
341
|
+
}>;
|
|
342
|
+
};
|
|
343
|
+
const channel = body.modules.find((m) => m.short === "channel");
|
|
344
|
+
expect(channel).toBeDefined();
|
|
345
|
+
expect(channel?.installed).toBe(true);
|
|
346
|
+
expect(channel?.installed_version).toBe("0.3.1");
|
|
347
|
+
expect(channel?.focus).toBe("experimental");
|
|
348
|
+
expect(channel?.supervisor_status).toBe("running");
|
|
318
349
|
});
|
|
319
350
|
|
|
320
|
-
test("
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
351
|
+
test("every self-registered + known module appears in `modules` — no running-but-invisible class", async () => {
|
|
352
|
+
// The two-registry-disagreement (services.json says installed, the curated
|
|
353
|
+
// whitelist says invisible) is gone: a self-registered surface row + a
|
|
354
|
+
// supervised channel both surface in `modules` (2026-06-09 modular-UI
|
|
355
|
+
// architecture). `supervised` still mirrors the run-state for every
|
|
356
|
+
// tracked module (hub#539) — consumers dedupe by short.
|
|
324
357
|
writeManifest(h.manifestPath, [
|
|
325
358
|
{
|
|
326
359
|
name: "parachute-vault",
|
|
@@ -329,6 +362,13 @@ describe("GET /api/modules", () => {
|
|
|
329
362
|
health: "/vault/default/health",
|
|
330
363
|
version: "0.4.5",
|
|
331
364
|
},
|
|
365
|
+
{
|
|
366
|
+
name: "parachute-surface",
|
|
367
|
+
port: 1946,
|
|
368
|
+
paths: ["/surface"],
|
|
369
|
+
health: "/surface/healthz",
|
|
370
|
+
version: "0.2.0",
|
|
371
|
+
},
|
|
332
372
|
]);
|
|
333
373
|
const { supervisor } = makeIdleSupervisor();
|
|
334
374
|
await supervisor.start({ short: "vault", cmd: ["parachute-vault", "serve"] });
|
|
@@ -343,15 +383,17 @@ describe("GET /api/modules", () => {
|
|
|
343
383
|
fetchLatestVersion: async () => null,
|
|
344
384
|
});
|
|
345
385
|
const body = (await res.json()) as {
|
|
346
|
-
modules: Array<{ short: string }>;
|
|
386
|
+
modules: Array<{ short: string; installed: boolean }>;
|
|
347
387
|
supervised: Array<{ short: string; supervisor_status: string | null; pid: number | null }>;
|
|
348
388
|
};
|
|
349
|
-
// surface
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
389
|
+
// surface is now IN the catalog (it was excluded under the whitelist), and
|
|
390
|
+
// reflects installed:true from its services.json row.
|
|
391
|
+
const surf = body.modules.find((m) => m.short === "surface");
|
|
392
|
+
expect(surf?.installed).toBe(true);
|
|
393
|
+
// …and its run-state is still in `supervised`, marked running with a pid.
|
|
394
|
+
const surfSup = body.supervised.find((m) => m.short === "surface");
|
|
395
|
+
expect(surfSup?.supervisor_status).toBe("running");
|
|
396
|
+
expect(typeof surfSup?.pid).toBe("number");
|
|
355
397
|
// Curated modules appear in `supervised` too (consumers dedupe by short).
|
|
356
398
|
expect(body.supervised.find((m) => m.short === "vault")?.supervisor_status).toBe("running");
|
|
357
399
|
});
|
|
@@ -485,10 +527,12 @@ describe("GET /api/modules", () => {
|
|
|
485
527
|
expect(scribe?.supervisor_start_error).toBeNull();
|
|
486
528
|
});
|
|
487
529
|
|
|
488
|
-
test("populates management_url from a
|
|
489
|
-
// Vault declares `managementUrl: "/
|
|
490
|
-
//
|
|
491
|
-
//
|
|
530
|
+
test("populates management_url from a RELATIVE managementUrl + module mount (B4 per-instance form)", async () => {
|
|
531
|
+
// Vault's new manifest declares `managementUrl: "admin/"` — relative, no
|
|
532
|
+
// leading slash: the per-instance form under the B4 unified semantics
|
|
533
|
+
// (2026-06-09 hub-module-boundary). Hub joins it under the entry's mount
|
|
534
|
+
// path (`/vault/default`) to produce the absolute admin URL the SPA's
|
|
535
|
+
// "Open" button targets.
|
|
492
536
|
writeManifest(h.manifestPath, [
|
|
493
537
|
{
|
|
494
538
|
name: "parachute-vault",
|
|
@@ -510,6 +554,55 @@ describe("GET /api/modules", () => {
|
|
|
510
554
|
// shape via `as unknown as ...` because the test only exercises
|
|
511
555
|
// the consumer-side resolver, not the validator (which lives in
|
|
512
556
|
// module-manifest.ts and has its own test suite).
|
|
557
|
+
if (installDir === "/install/dir/vault") {
|
|
558
|
+
return {
|
|
559
|
+
name: "parachute-vault",
|
|
560
|
+
manifestName: "parachute-vault",
|
|
561
|
+
displayName: "Vault",
|
|
562
|
+
tagline: "",
|
|
563
|
+
port: 1940,
|
|
564
|
+
paths: ["/vault/default"],
|
|
565
|
+
health: "/health",
|
|
566
|
+
managementUrl: "admin/",
|
|
567
|
+
} as unknown as Awaited<
|
|
568
|
+
ReturnType<typeof import("../module-manifest.ts").readModuleManifest>
|
|
569
|
+
>;
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
expect(res.status).toBe(200);
|
|
575
|
+
const body = (await res.json()) as {
|
|
576
|
+
modules: Array<{ short: string; management_url: string | null }>;
|
|
577
|
+
};
|
|
578
|
+
const vault = body.modules.find((m) => m.short === "vault");
|
|
579
|
+
expect(vault?.management_url).toBe("/vault/default/admin/");
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('COMPAT SHIM: the literal legacy "/admin" on a VAULT entry still mount-joins (one release)', async () => {
|
|
583
|
+
// Deployed vaults still declare `managementUrl: "/admin"` — the OLD
|
|
584
|
+
// per-instance relative form. Under the new semantics a leading-"/" is
|
|
585
|
+
// origin-absolute (which would point at the daemon-level /vault/admin
|
|
586
|
+
// mount, not the instance), so the literal "/admin"/"/admin/" on a vault
|
|
587
|
+
// entry keeps the old mount-join behavior for one release, with a
|
|
588
|
+
// deprecation log. Remove the shim once vault's new manifest is @latest.
|
|
589
|
+
writeManifest(h.manifestPath, [
|
|
590
|
+
{
|
|
591
|
+
name: "parachute-vault",
|
|
592
|
+
port: 1940,
|
|
593
|
+
paths: ["/vault/default"],
|
|
594
|
+
health: "/vault/default/health",
|
|
595
|
+
version: "0.4.5",
|
|
596
|
+
installDir: "/install/dir/vault",
|
|
597
|
+
},
|
|
598
|
+
]);
|
|
599
|
+
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
600
|
+
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
601
|
+
db: h.db,
|
|
602
|
+
issuer: ISSUER,
|
|
603
|
+
manifestPath: h.manifestPath,
|
|
604
|
+
fetchLatestVersion: async () => null,
|
|
605
|
+
readModuleManifest: async (installDir) => {
|
|
513
606
|
if (installDir === "/install/dir/vault") {
|
|
514
607
|
return {
|
|
515
608
|
name: "parachute-vault",
|
|
@@ -532,24 +625,117 @@ describe("GET /api/modules", () => {
|
|
|
532
625
|
modules: Array<{ short: string; management_url: string | null }>;
|
|
533
626
|
};
|
|
534
627
|
const vault = body.modules.find((m) => m.short === "vault");
|
|
628
|
+
// Mount-joined (legacy behavior preserved), NOT origin-absolute "/admin".
|
|
535
629
|
expect(vault?.management_url).toBe("/vault/default/admin");
|
|
536
630
|
});
|
|
537
631
|
|
|
538
|
-
test("
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
632
|
+
test("populates config_ui_url from a module's configUiUrl (2026-06-09 modular-UI P3)", async () => {
|
|
633
|
+
// Channel declares `configUiUrl: "/channel/admin"` (a single-instance,
|
|
634
|
+
// origin-absolute path) + `uiUrl: "/channel/ui"`. The hub surfaces
|
|
635
|
+
// both: `config_ui_url` drives the Modules page Configure action,
|
|
636
|
+
// `management_url` drives Open. configUiUrl resolves identically to
|
|
637
|
+
// managementUrl (same B4 unified semantics).
|
|
638
|
+
writeManifest(h.manifestPath, [
|
|
639
|
+
{
|
|
640
|
+
name: "channel",
|
|
641
|
+
port: 1941,
|
|
642
|
+
paths: ["/channel"],
|
|
643
|
+
health: "/health",
|
|
644
|
+
version: "0.1.0",
|
|
645
|
+
installDir: "/install/dir/channel",
|
|
646
|
+
},
|
|
647
|
+
]);
|
|
648
|
+
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
649
|
+
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
650
|
+
db: h.db,
|
|
651
|
+
issuer: ISSUER,
|
|
652
|
+
manifestPath: h.manifestPath,
|
|
653
|
+
fetchLatestVersion: async () => null,
|
|
654
|
+
readModuleManifest: async (installDir) => {
|
|
655
|
+
if (installDir === "/install/dir/channel") {
|
|
656
|
+
return {
|
|
657
|
+
name: "channel",
|
|
658
|
+
manifestName: "parachute-channel",
|
|
659
|
+
displayName: "Channel",
|
|
660
|
+
tagline: "",
|
|
661
|
+
port: 1941,
|
|
662
|
+
paths: ["/channel"],
|
|
663
|
+
health: "/health",
|
|
664
|
+
uiUrl: "/channel/ui",
|
|
665
|
+
configUiUrl: "/channel/admin",
|
|
666
|
+
} as unknown as Awaited<
|
|
667
|
+
ReturnType<typeof import("../module-manifest.ts").readModuleManifest>
|
|
668
|
+
>;
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
expect(res.status).toBe(200);
|
|
674
|
+
const body = (await res.json()) as {
|
|
675
|
+
modules: Array<{
|
|
676
|
+
short: string;
|
|
677
|
+
config_ui_url: string | null;
|
|
678
|
+
management_url: string | null;
|
|
679
|
+
}>;
|
|
680
|
+
};
|
|
681
|
+
const channel = body.modules.find((m) => m.short === "channel");
|
|
682
|
+
// Origin-absolute — verbatim, never double-prepends `/channel`.
|
|
683
|
+
expect(channel?.config_ui_url).toBe("/channel/admin");
|
|
684
|
+
// uiUrl (no managementUrl) drives the Open action's management_url.
|
|
685
|
+
expect(channel?.management_url).toBe("/channel/ui");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("config_ui_url is null when the module declares no configUiUrl", async () => {
|
|
689
|
+
writeManifest(h.manifestPath, [
|
|
690
|
+
{
|
|
691
|
+
name: "parachute-vault",
|
|
692
|
+
port: 1940,
|
|
693
|
+
paths: ["/vault/default"],
|
|
694
|
+
health: "/vault/default/health",
|
|
695
|
+
version: "0.4.5",
|
|
696
|
+
installDir: "/install/dir/vault",
|
|
697
|
+
},
|
|
698
|
+
]);
|
|
699
|
+
const bearer = await mintBearer(h, [API_MODULES_REQUIRED_SCOPE]);
|
|
700
|
+
const res = await handleApiModules(getReq({ authorization: `Bearer ${bearer}` }), {
|
|
701
|
+
db: h.db,
|
|
702
|
+
issuer: ISSUER,
|
|
703
|
+
manifestPath: h.manifestPath,
|
|
704
|
+
fetchLatestVersion: async () => null,
|
|
705
|
+
readModuleManifest: async (installDir) => {
|
|
706
|
+
if (installDir === "/install/dir/vault") {
|
|
707
|
+
return {
|
|
708
|
+
name: "parachute-vault",
|
|
709
|
+
manifestName: "parachute-vault",
|
|
710
|
+
displayName: "Vault",
|
|
711
|
+
tagline: "",
|
|
712
|
+
port: 1940,
|
|
713
|
+
paths: ["/vault/default"],
|
|
714
|
+
health: "/health",
|
|
715
|
+
managementUrl: "/admin",
|
|
716
|
+
} as unknown as Awaited<
|
|
717
|
+
ReturnType<typeof import("../module-manifest.ts").readModuleManifest>
|
|
718
|
+
>;
|
|
719
|
+
}
|
|
720
|
+
return null;
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
expect(res.status).toBe(200);
|
|
724
|
+
const body = (await res.json()) as {
|
|
725
|
+
modules: Array<{ short: string; config_ui_url: string | null }>;
|
|
726
|
+
};
|
|
727
|
+
const vault = body.modules.find((m) => m.short === "vault");
|
|
728
|
+
expect(vault?.config_ui_url).toBeNull();
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test("management_url passes a leading-slash path through verbatim (origin-absolute, B4)", async () => {
|
|
732
|
+
// Historical context (hub#380): surface declared `managementUrl:
|
|
733
|
+
// "/surface/admin/"` (full hub-origin path) and the resolver
|
|
734
|
+
// double-prepended the mount (`/surface/surface/admin/` → 404), patched
|
|
735
|
+
// then by an already-mount-prefixed heuristic. Under the B4 unified
|
|
736
|
+
// semantics the heuristic is gone: ANY leading-"/" path is
|
|
737
|
+
// ORIGIN-ABSOLUTE and passes through verbatim (except the vault "/admin"
|
|
738
|
+
// compat shim) — same result here, simpler rule.
|
|
553
739
|
writeManifest(h.manifestPath, [
|
|
554
740
|
{
|
|
555
741
|
name: "parachute-vault",
|
|
@@ -576,8 +762,8 @@ describe("GET /api/modules", () => {
|
|
|
576
762
|
port: 1940,
|
|
577
763
|
paths: ["/vault/default"],
|
|
578
764
|
health: "/vault/default/health",
|
|
579
|
-
//
|
|
580
|
-
// mount prepended
|
|
765
|
+
// Origin-absolute managementUrl — passes through verbatim,
|
|
766
|
+
// never gets the mount prepended.
|
|
581
767
|
managementUrl: "/vault/default/admin/",
|
|
582
768
|
} as unknown as Awaited<
|
|
583
769
|
ReturnType<typeof import("../module-manifest.ts").readModuleManifest>
|
|
@@ -595,14 +781,12 @@ describe("GET /api/modules", () => {
|
|
|
595
781
|
expect(vault?.management_url).toBe("/vault/default/admin/");
|
|
596
782
|
});
|
|
597
783
|
|
|
598
|
-
test("management_url
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
// pass-through behavior and `/app` mount would skip its prepend.
|
|
605
|
-
// Tests the trailing-slash discriminator stays load-bearing.
|
|
784
|
+
test("management_url: a leading-slash path NOT under the mount is still origin-absolute (B4 inverts hub#380)", async () => {
|
|
785
|
+
// INVERTED PIN (B4). Pre-B4 the resolver prepended the mount onto any
|
|
786
|
+
// leading-"/" candidate that didn't look already-mount-prefixed:
|
|
787
|
+
// mount=/surface + "/app-foo/admin" → "/surface/app-foo/admin". Under
|
|
788
|
+
// the unified semantics a leading-"/" is ORIGIN-ABSOLUTE, verbatim —
|
|
789
|
+
// the module says exactly where its surface lives on the origin.
|
|
606
790
|
writeManifest(h.manifestPath, [
|
|
607
791
|
{
|
|
608
792
|
name: "parachute-vault",
|
|
@@ -629,8 +813,8 @@ describe("GET /api/modules", () => {
|
|
|
629
813
|
port: 1940,
|
|
630
814
|
paths: ["/surface"],
|
|
631
815
|
health: "/surface/health",
|
|
632
|
-
//
|
|
633
|
-
//
|
|
816
|
+
// Origin-absolute path outside this module's own mount —
|
|
817
|
+
// verbatim under B4 (pre-B4 this was mount-prepended).
|
|
634
818
|
managementUrl: "/app-foo/admin",
|
|
635
819
|
} as unknown as Awaited<
|
|
636
820
|
ReturnType<typeof import("../module-manifest.ts").readModuleManifest>
|
|
@@ -644,14 +828,13 @@ describe("GET /api/modules", () => {
|
|
|
644
828
|
modules: Array<{ short: string; management_url: string | null }>;
|
|
645
829
|
};
|
|
646
830
|
const vault = body.modules.find((m) => m.short === "vault");
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
expect(vault?.management_url).toBe("/surface/app-foo/admin");
|
|
831
|
+
// Origin-absolute, verbatim — NOT /surface/app-foo/admin.
|
|
832
|
+
expect(vault?.management_url).toBe("/app-foo/admin");
|
|
650
833
|
});
|
|
651
834
|
|
|
652
|
-
test("management_url equality edge:
|
|
653
|
-
// mount=/foo, candidate=/foo →
|
|
654
|
-
//
|
|
835
|
+
test("management_url equality edge: candidate equals mount exactly", async () => {
|
|
836
|
+
// mount=/foo, candidate=/foo → origin-absolute, verbatim — same output
|
|
837
|
+
// as the pre-B4 equality branch, simpler rule.
|
|
655
838
|
writeManifest(h.manifestPath, [
|
|
656
839
|
{
|
|
657
840
|
name: "parachute-vault",
|