@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
|
@@ -3158,6 +3158,54 @@ describe("typed vault name (hub#267)", () => {
|
|
|
3158
3158
|
}
|
|
3159
3159
|
});
|
|
3160
3160
|
|
|
3161
|
+
test("vault POST rejects every consolidated reserved name (B2h: list, new, assets, admin)", async () => {
|
|
3162
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
3163
|
+
try {
|
|
3164
|
+
const user = await createUser(db, "owner", "pw");
|
|
3165
|
+
const { createSession } = await import("../sessions.ts");
|
|
3166
|
+
const session = createSession(db, { userId: user.id });
|
|
3167
|
+
const get = handleSetupGet(req("/admin/setup"), {
|
|
3168
|
+
db,
|
|
3169
|
+
manifestPath: h.manifestPath,
|
|
3170
|
+
configDir: h.dir,
|
|
3171
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3172
|
+
issuer: "https://hub.example",
|
|
3173
|
+
registry: getDefaultOperationsRegistry(),
|
|
3174
|
+
});
|
|
3175
|
+
const csrf = setCookie(get, CSRF_COOKIE_NAME) ?? "";
|
|
3176
|
+
for (const name of ["list", "new", "assets", "admin"]) {
|
|
3177
|
+
const post = await handleSetupVaultPost(
|
|
3178
|
+
req("/admin/setup/vault", {
|
|
3179
|
+
method: "POST",
|
|
3180
|
+
body: new URLSearchParams({
|
|
3181
|
+
[CSRF_FIELD_NAME]: csrf,
|
|
3182
|
+
vault_name: name,
|
|
3183
|
+
}).toString(),
|
|
3184
|
+
headers: {
|
|
3185
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
3186
|
+
cookie: `${CSRF_COOKIE_NAME}=${csrf}; ${SESSION_COOKIE_NAME}=${session.id}`,
|
|
3187
|
+
},
|
|
3188
|
+
}),
|
|
3189
|
+
{
|
|
3190
|
+
db,
|
|
3191
|
+
manifestPath: h.manifestPath,
|
|
3192
|
+
configDir: h.dir,
|
|
3193
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
3194
|
+
issuer: "https://hub.example",
|
|
3195
|
+
supervisor: makeSupervisor(),
|
|
3196
|
+
registry: getDefaultOperationsRegistry(),
|
|
3197
|
+
},
|
|
3198
|
+
);
|
|
3199
|
+
expect(post.status).toBe(400);
|
|
3200
|
+
const html = await post.text();
|
|
3201
|
+
expect(html).toContain("reserved");
|
|
3202
|
+
expect(getSetting(db, "setup_vault_name")).toBeUndefined();
|
|
3203
|
+
}
|
|
3204
|
+
} finally {
|
|
3205
|
+
db.close();
|
|
3206
|
+
}
|
|
3207
|
+
});
|
|
3208
|
+
|
|
3161
3209
|
test("vault POST with empty name falls back to 'default' + omits the env override", async () => {
|
|
3162
3210
|
const db = openHubDb(hubDbPath(h.dir));
|
|
3163
3211
|
try {
|
|
@@ -4350,19 +4398,198 @@ describe("setup-wizard JSON surface (hub#168 Cuts 2/3)", () => {
|
|
|
4350
4398
|
expect(body.step).toBe("expose");
|
|
4351
4399
|
// The skip flag is persisted.
|
|
4352
4400
|
expect(getSetting(db, "setup_vault_skipped")).toBe("true");
|
|
4353
|
-
// deriveWizardState advances past the vault step
|
|
4401
|
+
// deriveWizardState advances past the vault step — but hasRealVault
|
|
4402
|
+
// stays false (no instance exists; only the skip marker is set). The
|
|
4403
|
+
// distinction is what the re-enterable vault step (B5) keys on.
|
|
4354
4404
|
const s = deriveWizardState({
|
|
4355
4405
|
db,
|
|
4356
4406
|
manifestPath: h.manifestPath,
|
|
4357
4407
|
readExposeStateFn: h.readExposeStateFn,
|
|
4358
4408
|
});
|
|
4359
4409
|
expect(s.hasVault).toBe(true);
|
|
4410
|
+
expect(s.hasRealVault).toBe(false);
|
|
4360
4411
|
expect(s.step).toBe("expose");
|
|
4361
4412
|
} finally {
|
|
4362
4413
|
db.close();
|
|
4363
4414
|
}
|
|
4364
4415
|
});
|
|
4365
4416
|
|
|
4417
|
+
// --- B5 (2026-06-09 hub-module-boundary): re-enterable vault step --------
|
|
4418
|
+
//
|
|
4419
|
+
// A wizard-skip leaves the vault module installed with zero instances and
|
|
4420
|
+
// `setup_vault_skipped` satisfying hasVault — pre-B5 the create form was
|
|
4421
|
+
// unreachable forever after. The hub-side "create your first vault"
|
|
4422
|
+
// affordances (Home's vault card, the legacy /admin/vaults empty state)
|
|
4423
|
+
// deep-link `/admin/setup?step=vault`, which must re-enter the form.
|
|
4424
|
+
|
|
4425
|
+
test("GET ?step=vault re-enters the create form after a wizard-skip (session-gated)", async () => {
|
|
4426
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
4427
|
+
try {
|
|
4428
|
+
await createUser(db, "owner", "pw");
|
|
4429
|
+
setSetting(db, "setup_vault_skipped", "true");
|
|
4430
|
+
const { createSession } = await import("../sessions.ts");
|
|
4431
|
+
const user = getUserByUsername(db, "owner");
|
|
4432
|
+
if (!user) throw new Error("user missing");
|
|
4433
|
+
const session = createSession(db, { userId: user.id });
|
|
4434
|
+
const res = handleSetupGet(
|
|
4435
|
+
req("/admin/setup?step=vault", {
|
|
4436
|
+
headers: { cookie: `${SESSION_COOKIE_NAME}=${session.id}` },
|
|
4437
|
+
}),
|
|
4438
|
+
{
|
|
4439
|
+
db,
|
|
4440
|
+
manifestPath: h.manifestPath,
|
|
4441
|
+
configDir: h.dir,
|
|
4442
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
4443
|
+
issuer: "http://127.0.0.1:1939",
|
|
4444
|
+
registry: getDefaultOperationsRegistry(),
|
|
4445
|
+
},
|
|
4446
|
+
);
|
|
4447
|
+
expect(res.status).toBe(200);
|
|
4448
|
+
const html = await res.text();
|
|
4449
|
+
// The vault create/import/skip form — not the expose step the plain
|
|
4450
|
+
// GET would resume at post-skip.
|
|
4451
|
+
expect(html).toContain('action="/admin/setup/vault"');
|
|
4452
|
+
} finally {
|
|
4453
|
+
db.close();
|
|
4454
|
+
}
|
|
4455
|
+
});
|
|
4456
|
+
|
|
4457
|
+
test("GET ?step=vault without a session 302s to /login (post-skip)", async () => {
|
|
4458
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
4459
|
+
try {
|
|
4460
|
+
await createUser(db, "owner", "pw");
|
|
4461
|
+
setSetting(db, "setup_vault_skipped", "true");
|
|
4462
|
+
const res = handleSetupGet(req("/admin/setup?step=vault"), {
|
|
4463
|
+
db,
|
|
4464
|
+
manifestPath: h.manifestPath,
|
|
4465
|
+
configDir: h.dir,
|
|
4466
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
4467
|
+
issuer: "http://127.0.0.1:1939",
|
|
4468
|
+
registry: getDefaultOperationsRegistry(),
|
|
4469
|
+
});
|
|
4470
|
+
expect(res.status).toBe(302);
|
|
4471
|
+
expect(res.headers.get("location")).toBe(
|
|
4472
|
+
`/login?next=${encodeURIComponent("/admin/setup?step=vault")}`,
|
|
4473
|
+
);
|
|
4474
|
+
} finally {
|
|
4475
|
+
db.close();
|
|
4476
|
+
}
|
|
4477
|
+
});
|
|
4478
|
+
|
|
4479
|
+
test("GET ?step=vault with NO admin falls through to the welcome step (hasAdmin guard)", async () => {
|
|
4480
|
+
// Fresh box, no user rows: the re-entry branch is gated on
|
|
4481
|
+
// `state.hasAdmin` — the param must not jump a brand-new operator past
|
|
4482
|
+
// account creation into a provisioning form (the POST would reject the
|
|
4483
|
+
// session-less submit anyway, but the GET shouldn't render out of order
|
|
4484
|
+
// either).
|
|
4485
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
4486
|
+
try {
|
|
4487
|
+
const res = handleSetupGet(req("/admin/setup?step=vault"), {
|
|
4488
|
+
db,
|
|
4489
|
+
manifestPath: h.manifestPath,
|
|
4490
|
+
configDir: h.dir,
|
|
4491
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
4492
|
+
issuer: "http://127.0.0.1:1939",
|
|
4493
|
+
registry: getDefaultOperationsRegistry(),
|
|
4494
|
+
});
|
|
4495
|
+
expect(res.status).toBe(200);
|
|
4496
|
+
// The welcome/account form — not the vault create form, not a redirect.
|
|
4497
|
+
const html = await res.text();
|
|
4498
|
+
expect(html).toContain('action="/admin/setup/account"');
|
|
4499
|
+
expect(html).not.toContain('action="/admin/setup/vault"');
|
|
4500
|
+
} finally {
|
|
4501
|
+
db.close();
|
|
4502
|
+
}
|
|
4503
|
+
});
|
|
4504
|
+
|
|
4505
|
+
test("GET ?step=vault is ignored when a real vault instance exists", async () => {
|
|
4506
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
4507
|
+
try {
|
|
4508
|
+
await createUser(db, "owner", "pw");
|
|
4509
|
+
setSetting(db, "setup_expose_mode", "localhost");
|
|
4510
|
+
writeManifest(
|
|
4511
|
+
{
|
|
4512
|
+
services: [
|
|
4513
|
+
{
|
|
4514
|
+
name: "parachute-vault",
|
|
4515
|
+
version: "0.1.0",
|
|
4516
|
+
port: 1940,
|
|
4517
|
+
paths: ["/vault/default"],
|
|
4518
|
+
health: "/health",
|
|
4519
|
+
},
|
|
4520
|
+
],
|
|
4521
|
+
},
|
|
4522
|
+
h.manifestPath,
|
|
4523
|
+
);
|
|
4524
|
+
const res = handleSetupGet(req("/admin/setup?step=vault"), {
|
|
4525
|
+
db,
|
|
4526
|
+
manifestPath: h.manifestPath,
|
|
4527
|
+
configDir: h.dir,
|
|
4528
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
4529
|
+
issuer: "http://127.0.0.1:1939",
|
|
4530
|
+
registry: getDefaultOperationsRegistry(),
|
|
4531
|
+
});
|
|
4532
|
+
// Setup is fully complete — the param must not reopen a provisioning
|
|
4533
|
+
// form; the normal completed flow (301 → /login) runs instead.
|
|
4534
|
+
expect(res.status).toBe(301);
|
|
4535
|
+
expect(res.headers.get("location")).toBe("/login");
|
|
4536
|
+
} finally {
|
|
4537
|
+
db.close();
|
|
4538
|
+
}
|
|
4539
|
+
});
|
|
4540
|
+
|
|
4541
|
+
test("vault POST mode=create proceeds after a skip (short-circuit keys on hasRealVault)", async () => {
|
|
4542
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
4543
|
+
try {
|
|
4544
|
+
await createUser(db, "owner", "pw");
|
|
4545
|
+
setSetting(db, "setup_vault_skipped", "true");
|
|
4546
|
+
// No supervisor in deps: a create that gets PAST the short-circuit hits
|
|
4547
|
+
// the supervisor gate and 503s. Pre-B5 this returned 200
|
|
4548
|
+
// `{ step: "expose", message: "vault already provisioned" }` because the
|
|
4549
|
+
// skip marker satisfied hasVault — the form was a dead end.
|
|
4550
|
+
const baseDeps = {
|
|
4551
|
+
db,
|
|
4552
|
+
manifestPath: h.manifestPath,
|
|
4553
|
+
configDir: h.dir,
|
|
4554
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
4555
|
+
issuer: "http://127.0.0.1:1939",
|
|
4556
|
+
registry: getDefaultOperationsRegistry(),
|
|
4557
|
+
};
|
|
4558
|
+
const getRes = handleSetupGet(
|
|
4559
|
+
req("/admin/setup", { headers: { accept: "application/json" } }),
|
|
4560
|
+
baseDeps,
|
|
4561
|
+
);
|
|
4562
|
+
const csrf = setCookie(getRes, CSRF_COOKIE_NAME) ?? "";
|
|
4563
|
+
const envelope = (await getRes.json()) as { csrfToken: string };
|
|
4564
|
+
const { createSession } = await import("../sessions.ts");
|
|
4565
|
+
const user = getUserByUsername(db, "owner");
|
|
4566
|
+
if (!user) throw new Error("user missing");
|
|
4567
|
+
const session = createSession(db, { userId: user.id });
|
|
4568
|
+
const cookieHeader = `${SESSION_COOKIE_NAME}=${session.id}; ${CSRF_COOKIE_NAME}=${csrf}`;
|
|
4569
|
+
const postRes = await handleSetupVaultPost(
|
|
4570
|
+
req("/admin/setup/vault", {
|
|
4571
|
+
method: "POST",
|
|
4572
|
+
headers: {
|
|
4573
|
+
accept: "application/json",
|
|
4574
|
+
"content-type": "application/json",
|
|
4575
|
+
cookie: cookieHeader,
|
|
4576
|
+
},
|
|
4577
|
+
body: JSON.stringify({
|
|
4578
|
+
[CSRF_FIELD_NAME]: envelope.csrfToken,
|
|
4579
|
+
mode: "create",
|
|
4580
|
+
vault_name: "second-chance",
|
|
4581
|
+
}),
|
|
4582
|
+
}),
|
|
4583
|
+
baseDeps,
|
|
4584
|
+
);
|
|
4585
|
+
expect(postRes.status).toBe(503);
|
|
4586
|
+
const body = (await postRes.json()) as { error: string };
|
|
4587
|
+
expect(body.error).toContain("supervisor");
|
|
4588
|
+
} finally {
|
|
4589
|
+
db.close();
|
|
4590
|
+
}
|
|
4591
|
+
});
|
|
4592
|
+
|
|
4366
4593
|
test("vault step import mode requires remote_url (400 on empty)", async () => {
|
|
4367
4594
|
const db = openHubDb(hubDbPath(h.dir));
|
|
4368
4595
|
try {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, expect, test } from "bun:test";
|
|
9
|
-
import { DEFAULT_VAULT_NAME, validateVaultName } from "../vault-name.ts";
|
|
9
|
+
import { DEFAULT_VAULT_NAME, RESERVED_VAULT_NAMES, validateVaultName } from "../vault-name.ts";
|
|
10
10
|
|
|
11
11
|
describe("validateVaultName", () => {
|
|
12
12
|
test("accepts lowercase alphanumeric + hyphens/underscores", () => {
|
|
@@ -64,10 +64,25 @@ describe("validateVaultName", () => {
|
|
|
64
64
|
expect(validateVaultName(" ").ok).toBe(false);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
test("rejects
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
test("rejects every consolidated reserved name (B2h: list, new, assets, admin)", () => {
|
|
68
|
+
// One set for every hub edge — wizard, invite redemption, POST /vaults.
|
|
69
|
+
// `list` mirrors vault's CLI reservation; `new`/`assets` shadow SPA
|
|
70
|
+
// routes; `admin` shadows the daemon-level /vault/admin mount (B-route).
|
|
71
|
+
for (const name of ["list", "new", "assets", "admin"]) {
|
|
72
|
+
const result = validateVaultName(name);
|
|
73
|
+
expect(result.ok).toBe(false);
|
|
74
|
+
if (!result.ok) expect(result.error).toContain("reserved");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("near-miss names of the reserved set still pass (admins, listing, my-admin)", () => {
|
|
79
|
+
for (const name of ["admins", "listing", "my-admin", "assets2", "newer"]) {
|
|
80
|
+
expect(validateVaultName(name).ok).toBe(true);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("RESERVED_VAULT_NAMES is the consolidated four-name set", () => {
|
|
85
|
+
expect([...RESERVED_VAULT_NAMES].sort()).toEqual(["admin", "assets", "list", "new"]);
|
|
71
86
|
});
|
|
72
87
|
|
|
73
88
|
test("DEFAULT_VAULT_NAME is 'default'", () => {
|
|
@@ -361,26 +361,27 @@ describe("buildWellKnown", () => {
|
|
|
361
361
|
expect(notesSvc?.uiUrl).toBe("https://x.example/notes");
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
|
|
364
|
+
// B4 unified semantics (2026-06-09 hub-module-boundary): relative
|
|
365
|
+
// (no leading slash) = the per-instance form, mount-joined per vault
|
|
366
|
+
// instance; leading-"/" = origin-absolute pass-through. The literal legacy
|
|
367
|
+
// `"/admin/"` on a vault entry rides the one-release COMPAT SHIM
|
|
368
|
+
// (mount-join + deprecation log) because deployed vaults still declare it.
|
|
369
|
+
test("vault RELATIVE uiUrl mount-joins per instance (B4 per-instance form)", () => {
|
|
369
370
|
const doc = buildWellKnown({
|
|
370
371
|
services: [vault],
|
|
371
372
|
canonicalOrigin: "https://x.example",
|
|
372
|
-
uiUrlFor: () => "
|
|
373
|
+
uiUrlFor: () => "admin/",
|
|
373
374
|
});
|
|
374
375
|
const svc = doc.services.find((s) => s.name === "parachute-vault");
|
|
375
376
|
expect(svc?.uiUrl).toBe("https://x.example/vault/default/admin/");
|
|
376
377
|
});
|
|
377
378
|
|
|
378
|
-
test("vault uiUrl
|
|
379
|
+
test("vault RELATIVE uiUrl mount-joins per instance for multi-path vault entries", () => {
|
|
379
380
|
const multi: ServiceEntry = { ...vault, paths: ["/vault/default", "/vault/techne"] };
|
|
380
381
|
const doc = buildWellKnown({
|
|
381
382
|
services: [multi],
|
|
382
383
|
canonicalOrigin: "https://x.example",
|
|
383
|
-
uiUrlFor: () => "
|
|
384
|
+
uiUrlFor: () => "admin/",
|
|
384
385
|
});
|
|
385
386
|
const rows = doc.services.filter((s) => s.name === "parachute-vault");
|
|
386
387
|
expect(rows.length).toBe(2);
|
|
@@ -389,6 +390,41 @@ describe("buildWellKnown", () => {
|
|
|
389
390
|
expect(uiUrls[1]).toBe("https://x.example/vault/techne/admin/");
|
|
390
391
|
});
|
|
391
392
|
|
|
393
|
+
test('COMPAT SHIM: vault legacy "/admin/" uiUrl still mount-joins per instance (one release)', () => {
|
|
394
|
+
// Deployed vaults declare `uiUrl: "/admin/"` (the OLD per-instance form).
|
|
395
|
+
// Origin-absolute resolution would point every tile at the daemon-level
|
|
396
|
+
// /vault/admin mount — so the literal "/admin"/"/admin/" keeps the old
|
|
397
|
+
// mount-join for one release, with a deprecation log. Remove the shim
|
|
398
|
+
// once vault's new manifest ("admin/") reaches @latest.
|
|
399
|
+
const multi: ServiceEntry = { ...vault, paths: ["/vault/default", "/vault/techne"] };
|
|
400
|
+
const doc = buildWellKnown({
|
|
401
|
+
services: [multi],
|
|
402
|
+
canonicalOrigin: "https://x.example",
|
|
403
|
+
uiUrlFor: () => "/admin/",
|
|
404
|
+
});
|
|
405
|
+
const rows = doc.services.filter((s) => s.name === "parachute-vault");
|
|
406
|
+
const uiUrls = rows.map((r) => r.uiUrl).sort();
|
|
407
|
+
expect(uiUrls[0]).toBe("https://x.example/vault/default/admin/");
|
|
408
|
+
expect(uiUrls[1]).toBe("https://x.example/vault/techne/admin/");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("vault LEADING-SLASH uiUrl (non-shim) is origin-absolute pass-through (B4)", () => {
|
|
412
|
+
// The daemon-level surface form: `/vault/admin/` resolves against the
|
|
413
|
+
// origin — NOT per-instance — so a multi-path vault emits the same
|
|
414
|
+
// daemon-level URL on each row.
|
|
415
|
+
const multi: ServiceEntry = { ...vault, paths: ["/vault/default", "/vault/techne"] };
|
|
416
|
+
const doc = buildWellKnown({
|
|
417
|
+
services: [multi],
|
|
418
|
+
canonicalOrigin: "https://x.example",
|
|
419
|
+
uiUrlFor: () => "/vault/admin/",
|
|
420
|
+
});
|
|
421
|
+
const rows = doc.services.filter((s) => s.name === "parachute-vault");
|
|
422
|
+
expect(rows.length).toBe(2);
|
|
423
|
+
for (const r of rows) {
|
|
424
|
+
expect(r.uiUrl).toBe("https://x.example/vault/admin/");
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
392
428
|
test("vault uiUrl absolute URL still passes through verbatim (no prefix)", () => {
|
|
393
429
|
const doc = buildWellKnown({
|
|
394
430
|
services: [vault],
|