@openparachute/hub 0.7.3-rc.1 → 0.7.3-rc.11
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-agent-grants.test.ts +113 -0
- package/src/__tests__/admin-vaults.test.ts +20 -5
- package/src/__tests__/api-modules.test.ts +65 -10
- package/src/__tests__/api-vault-caps.test.ts +232 -0
- package/src/__tests__/cli.test.ts +20 -0
- package/src/__tests__/hub-command.test.ts +136 -0
- package/src/__tests__/hub-origins-env-set.test.ts +273 -0
- package/src/__tests__/hub-server.test.ts +240 -0
- package/src/__tests__/init.test.ts +148 -0
- package/src/__tests__/jwt-sign.test.ts +79 -0
- package/src/__tests__/module-manifest.test.ts +3 -1
- package/src/__tests__/oauth-handlers.test.ts +413 -5
- package/src/__tests__/public-signup.test.ts +619 -0
- package/src/__tests__/rate-limit.test.ts +86 -0
- package/src/__tests__/scribe-config.test.ts +117 -0
- package/src/__tests__/serve-boot.test.ts +45 -0
- package/src/__tests__/serve.test.ts +67 -1
- package/src/__tests__/service-spec-discovery.test.ts +22 -2
- package/src/__tests__/setup-wizard.test.ts +18 -0
- package/src/__tests__/setup.test.ts +129 -15
- package/src/__tests__/two-factor-flow.test.ts +94 -0
- package/src/__tests__/users.test.ts +33 -0
- package/src/__tests__/vault-caps.test.ts +89 -0
- package/src/__tests__/vault-hub-origin-env.test.ts +38 -4
- package/src/__tests__/wizard-transcription.test.ts +334 -0
- package/src/__tests__/wizard.test.ts +146 -0
- package/src/account-setup.ts +118 -32
- package/src/admin-agent-grants.ts +51 -3
- package/src/admin-handlers.ts +53 -16
- package/src/admin-login-ui.ts +30 -4
- package/src/admin-vaults.ts +9 -1
- package/src/api-invites.ts +163 -3
- package/src/api-modules-ops.ts +12 -0
- package/src/api-modules.ts +47 -13
- package/src/api-users.ts +3 -0
- package/src/api-vault-caps.ts +206 -0
- package/src/cli.ts +35 -1
- package/src/commands/hub.ts +173 -0
- package/src/commands/init.ts +73 -2
- package/src/commands/serve-boot.ts +16 -2
- package/src/commands/serve.ts +39 -3
- package/src/commands/setup.ts +31 -6
- package/src/commands/wizard-transcription.ts +296 -0
- package/src/commands/wizard.ts +73 -3
- package/src/help.ts +8 -0
- package/src/hub-db.ts +108 -1
- package/src/hub-origin.ts +64 -0
- package/src/hub-server.ts +73 -0
- package/src/invites.ts +155 -31
- package/src/jwt-sign.ts +72 -3
- package/src/module-manifest.ts +23 -9
- package/src/oauth-flows-store.ts +12 -0
- package/src/oauth-handlers.ts +145 -20
- package/src/rate-limit.ts +111 -20
- package/src/scribe-config.ts +145 -0
- package/src/service-spec.ts +31 -12
- package/src/setup-wizard.ts +37 -4
- package/src/users.ts +62 -3
- package/src/vault-caps.ts +109 -0
- package/src/vault-hub-origin-env.ts +109 -16
- package/web/ui/dist/assets/{index-DR6R8EFf.css → index--728BX3j.css} +1 -1
- package/web/ui/dist/assets/index-DZzX_Enf.js +61 -0
- package/web/ui/dist/index.html +2 -2
- package/web/ui/dist/assets/index-B5AUE359.js +0 -61
|
@@ -960,6 +960,154 @@ describe("hasNoDisplay heuristic (Fix 2 — headless browser-open guard)", () =>
|
|
|
960
960
|
});
|
|
961
961
|
});
|
|
962
962
|
|
|
963
|
+
describe("init --hub-origin (Caddy-direct zero-SSH boot, onboarding-streamline 2026-06-25)", () => {
|
|
964
|
+
test("persists the origin BEFORE ensureHub so modules spawn with it in one pass", async () => {
|
|
965
|
+
const h = makeHarness();
|
|
966
|
+
try {
|
|
967
|
+
const order: string[] = [];
|
|
968
|
+
const code = await init({
|
|
969
|
+
configDir: h.configDir,
|
|
970
|
+
ensureHubVersion: async () => ({
|
|
971
|
+
outcome: "match" as const,
|
|
972
|
+
installedVersion: "test",
|
|
973
|
+
messages: [],
|
|
974
|
+
}),
|
|
975
|
+
manifestPath: h.manifestPath,
|
|
976
|
+
log: () => {},
|
|
977
|
+
alive: () => false,
|
|
978
|
+
hubOrigin: "https://box.sslip.io",
|
|
979
|
+
// The load-bearing ordering assertion: the origin write MUST happen
|
|
980
|
+
// before the hub unit starts (which boots + spawns vault/scribe), so
|
|
981
|
+
// the boot-time issuer + child env pick it up without a restart.
|
|
982
|
+
setHubOriginImpl: (_dir, origin) => {
|
|
983
|
+
order.push(`set-origin:${origin}`);
|
|
984
|
+
},
|
|
985
|
+
ensureHub: async () => {
|
|
986
|
+
order.push("ensureHub");
|
|
987
|
+
writeHubPort(1939, h.configDir);
|
|
988
|
+
return { pid: 5555, port: 1939, started: true };
|
|
989
|
+
},
|
|
990
|
+
readExposeStateFn: () => undefined,
|
|
991
|
+
isTty: false,
|
|
992
|
+
platform: "linux",
|
|
993
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
994
|
+
});
|
|
995
|
+
expect(code).toBe(0);
|
|
996
|
+
expect(order).toEqual(["set-origin:https://box.sslip.io", "ensureHub"]);
|
|
997
|
+
} finally {
|
|
998
|
+
h.cleanup();
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
test("default impl writes hub_settings.hub_origin to the real DB", async () => {
|
|
1003
|
+
const h = makeHarness();
|
|
1004
|
+
try {
|
|
1005
|
+
const code = await init({
|
|
1006
|
+
configDir: h.configDir,
|
|
1007
|
+
ensureHubVersion: async () => ({
|
|
1008
|
+
outcome: "match" as const,
|
|
1009
|
+
installedVersion: "test",
|
|
1010
|
+
messages: [],
|
|
1011
|
+
}),
|
|
1012
|
+
manifestPath: h.manifestPath,
|
|
1013
|
+
log: () => {},
|
|
1014
|
+
alive: () => false,
|
|
1015
|
+
hubOrigin: "https://box.sslip.io",
|
|
1016
|
+
// No setHubOriginImpl override → exercise the production default
|
|
1017
|
+
// (openHubDb + setHubOrigin) against the harness's temp configDir.
|
|
1018
|
+
ensureHub: async () => {
|
|
1019
|
+
writeHubPort(1939, h.configDir);
|
|
1020
|
+
return { pid: 5555, port: 1939, started: true };
|
|
1021
|
+
},
|
|
1022
|
+
readExposeStateFn: () => undefined,
|
|
1023
|
+
isTty: false,
|
|
1024
|
+
platform: "linux",
|
|
1025
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
1026
|
+
});
|
|
1027
|
+
expect(code).toBe(0);
|
|
1028
|
+
const { hubDbPath, openHubDb } = await import("../hub-db.ts");
|
|
1029
|
+
const { getHubOrigin } = await import("../hub-settings.ts");
|
|
1030
|
+
const db = openHubDb(hubDbPath(h.configDir));
|
|
1031
|
+
try {
|
|
1032
|
+
expect(getHubOrigin(db)).toBe("https://box.sslip.io");
|
|
1033
|
+
} finally {
|
|
1034
|
+
db.close();
|
|
1035
|
+
}
|
|
1036
|
+
} finally {
|
|
1037
|
+
h.cleanup();
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
test("no --hub-origin → no persistence call (laptop/loopback path unchanged)", async () => {
|
|
1042
|
+
const h = makeHarness();
|
|
1043
|
+
try {
|
|
1044
|
+
let setCalls = 0;
|
|
1045
|
+
const code = await init({
|
|
1046
|
+
configDir: h.configDir,
|
|
1047
|
+
ensureHubVersion: async () => ({
|
|
1048
|
+
outcome: "match" as const,
|
|
1049
|
+
installedVersion: "test",
|
|
1050
|
+
messages: [],
|
|
1051
|
+
}),
|
|
1052
|
+
manifestPath: h.manifestPath,
|
|
1053
|
+
log: () => {},
|
|
1054
|
+
alive: () => false,
|
|
1055
|
+
setHubOriginImpl: () => {
|
|
1056
|
+
setCalls++;
|
|
1057
|
+
},
|
|
1058
|
+
ensureHub: async () => {
|
|
1059
|
+
writeHubPort(1939, h.configDir);
|
|
1060
|
+
return { pid: 5555, port: 1939, started: true };
|
|
1061
|
+
},
|
|
1062
|
+
readExposeStateFn: () => undefined,
|
|
1063
|
+
isTty: false,
|
|
1064
|
+
platform: "linux",
|
|
1065
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
1066
|
+
});
|
|
1067
|
+
expect(code).toBe(0);
|
|
1068
|
+
expect(setCalls).toBe(0);
|
|
1069
|
+
} finally {
|
|
1070
|
+
h.cleanup();
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
test("a persistence failure is non-fatal — init still reaches the wizard", async () => {
|
|
1075
|
+
const h = makeHarness();
|
|
1076
|
+
try {
|
|
1077
|
+
const logs: string[] = [];
|
|
1078
|
+
const code = await init({
|
|
1079
|
+
configDir: h.configDir,
|
|
1080
|
+
ensureHubVersion: async () => ({
|
|
1081
|
+
outcome: "match" as const,
|
|
1082
|
+
installedVersion: "test",
|
|
1083
|
+
messages: [],
|
|
1084
|
+
}),
|
|
1085
|
+
manifestPath: h.manifestPath,
|
|
1086
|
+
log: (l) => logs.push(l),
|
|
1087
|
+
alive: () => false,
|
|
1088
|
+
hubOrigin: "https://box.sslip.io",
|
|
1089
|
+
setHubOriginImpl: () => {
|
|
1090
|
+
throw new Error("disk full");
|
|
1091
|
+
},
|
|
1092
|
+
ensureHub: async () => {
|
|
1093
|
+
writeHubPort(1939, h.configDir);
|
|
1094
|
+
return { pid: 5555, port: 1939, started: true };
|
|
1095
|
+
},
|
|
1096
|
+
readExposeStateFn: () => undefined,
|
|
1097
|
+
isTty: false,
|
|
1098
|
+
platform: "linux",
|
|
1099
|
+
installVaultModuleImpl: noopVaultInstall,
|
|
1100
|
+
});
|
|
1101
|
+
expect(code).toBe(0);
|
|
1102
|
+
const joined = logs.join("\n");
|
|
1103
|
+
expect(joined).toContain("Couldn't persist the hub origin");
|
|
1104
|
+
expect(joined).toContain("http://127.0.0.1:1939/admin/");
|
|
1105
|
+
} finally {
|
|
1106
|
+
h.cleanup();
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
});
|
|
1110
|
+
|
|
963
1111
|
describe("init exposure chain", () => {
|
|
964
1112
|
test("TTY + no exposure + no flags → prompt is shown", async () => {
|
|
965
1113
|
const h = makeHarness();
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
RefreshTokenInsertError,
|
|
11
11
|
findRefreshToken,
|
|
12
12
|
findTokenRowByJti,
|
|
13
|
+
linkRotation,
|
|
13
14
|
listActiveRevocations,
|
|
15
|
+
liveFamilyRefreshRows,
|
|
14
16
|
recordTokenMint,
|
|
15
17
|
revokeTokenByJti,
|
|
16
18
|
signAccessToken,
|
|
@@ -623,3 +625,80 @@ describe("token registry (hub#212 Phase 1)", () => {
|
|
|
623
625
|
}
|
|
624
626
|
});
|
|
625
627
|
});
|
|
628
|
+
|
|
629
|
+
// hub#685 — rotation-grace primitives. The grace decision in
|
|
630
|
+
// `handleTokenRefresh` is built on these two helpers; unit-cover them
|
|
631
|
+
// directly so the security-load-bearing predecessor check has a tight test.
|
|
632
|
+
describe("rotation grace primitives (hub#685)", () => {
|
|
633
|
+
test("rotatedTo is null on a freshly minted row, set by linkRotation", async () => {
|
|
634
|
+
const { db, cleanup } = makeDb();
|
|
635
|
+
try {
|
|
636
|
+
rotateSigningKey(db);
|
|
637
|
+
const u = await createUser(db, "owner", "pw");
|
|
638
|
+
signRefreshToken(db, {
|
|
639
|
+
jti: "pred",
|
|
640
|
+
userId: u.id,
|
|
641
|
+
clientId: "parachute-hub",
|
|
642
|
+
scopes: ["vault:read"],
|
|
643
|
+
familyId: "fam-1",
|
|
644
|
+
});
|
|
645
|
+
expect(findTokenRowByJti(db, "pred")?.rotatedTo).toBeNull();
|
|
646
|
+
|
|
647
|
+
signRefreshToken(db, {
|
|
648
|
+
jti: "succ",
|
|
649
|
+
userId: u.id,
|
|
650
|
+
clientId: "parachute-hub",
|
|
651
|
+
scopes: ["vault:read"],
|
|
652
|
+
familyId: "fam-1",
|
|
653
|
+
});
|
|
654
|
+
linkRotation(db, "pred", "succ");
|
|
655
|
+
expect(findTokenRowByJti(db, "pred")?.rotatedTo).toBe("succ");
|
|
656
|
+
// Successor itself has not rotated yet.
|
|
657
|
+
expect(findTokenRowByJti(db, "succ")?.rotatedTo).toBeNull();
|
|
658
|
+
} finally {
|
|
659
|
+
cleanup();
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("liveFamilyRefreshRows returns only un-revoked, un-expired refresh rows in the family", async () => {
|
|
664
|
+
const { db, cleanup } = makeDb();
|
|
665
|
+
try {
|
|
666
|
+
rotateSigningKey(db);
|
|
667
|
+
const u = await createUser(db, "owner", "pw");
|
|
668
|
+
const t0 = new Date("2026-06-24T00:00:00Z");
|
|
669
|
+
for (const jti of ["a", "b", "c"]) {
|
|
670
|
+
signRefreshToken(db, {
|
|
671
|
+
jti,
|
|
672
|
+
userId: u.id,
|
|
673
|
+
clientId: "parachute-hub",
|
|
674
|
+
scopes: ["vault:read"],
|
|
675
|
+
familyId: "fam-x",
|
|
676
|
+
now: () => t0,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
// A different family — must never leak in.
|
|
680
|
+
signRefreshToken(db, {
|
|
681
|
+
jti: "other",
|
|
682
|
+
userId: u.id,
|
|
683
|
+
clientId: "parachute-hub",
|
|
684
|
+
scopes: ["vault:read"],
|
|
685
|
+
familyId: "fam-y",
|
|
686
|
+
now: () => t0,
|
|
687
|
+
});
|
|
688
|
+
// Revoke one of the three.
|
|
689
|
+
revokeTokenByJti(db, "b", t0);
|
|
690
|
+
|
|
691
|
+
const live = liveFamilyRefreshRows(db, "fam-x", t0)
|
|
692
|
+
.map((r) => r.jti)
|
|
693
|
+
.sort();
|
|
694
|
+
expect(live).toEqual(["a", "c"]);
|
|
695
|
+
|
|
696
|
+
// Evaluated PAST the 30-day TTL: every row in the family is expired,
|
|
697
|
+
// so none count as a live tip — the grace path can't rotate into one.
|
|
698
|
+
const afterExpiry = new Date(t0.getTime() + REFRESH_TOKEN_TTL_MS + 1);
|
|
699
|
+
expect(liveFamilyRefreshRows(db, "fam-x", afterExpiry)).toEqual([]);
|
|
700
|
+
} finally {
|
|
701
|
+
cleanup();
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
});
|
|
@@ -277,11 +277,13 @@ describe("validateModuleManifest", () => {
|
|
|
277
277
|
// --- 2026-06-09 modular-UI architecture P1 fields: focus / configUiUrl /
|
|
278
278
|
// adminCapabilities / events / actions. All optional + additive. ---
|
|
279
279
|
|
|
280
|
-
test("focus accepts core / experimental and rejects anything else", () => {
|
|
280
|
+
test("focus accepts core / experimental / deprecated and rejects anything else", () => {
|
|
281
281
|
expect(validateModuleManifest({ ...VALID, focus: "core" }, "x").focus).toBe("core");
|
|
282
282
|
expect(validateModuleManifest({ ...VALID, focus: "experimental" }, "x").focus).toBe(
|
|
283
283
|
"experimental",
|
|
284
284
|
);
|
|
285
|
+
// `deprecated` tier (2026-06-25) — a module can self-declare it.
|
|
286
|
+
expect(validateModuleManifest({ ...VALID, focus: "deprecated" }, "x").focus).toBe("deprecated");
|
|
285
287
|
// Absent stays absent (hub falls back to its default map downstream).
|
|
286
288
|
expect(validateModuleManifest(VALID, "x").focus).toBeUndefined();
|
|
287
289
|
expect(() => validateModuleManifest({ ...VALID, focus: "headline" }, "x")).toThrow(/focus/);
|