@openparachute/hub 0.6.2 → 0.6.3-rc.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__/api-modules-ops.test.ts +359 -3
- package/src/__tests__/api-modules.test.ts +54 -0
- package/src/__tests__/hub-unit.test.ts +574 -0
- package/src/__tests__/init.test.ts +219 -2
- package/src/__tests__/lifecycle.test.ts +423 -0
- package/src/__tests__/managed-unit.test.ts +575 -0
- package/src/__tests__/module-ops-client.test.ts +556 -0
- package/src/__tests__/port-probe.test.ts +23 -0
- package/src/__tests__/setup-wizard.test.ts +130 -0
- package/src/__tests__/status-supervisor.test.ts +569 -0
- package/src/__tests__/supervisor.test.ts +471 -6
- package/src/api-modules-ops.ts +221 -0
- package/src/api-modules.ts +18 -2
- package/src/cli.ts +14 -4
- package/src/cloudflare/connector-service.ts +117 -322
- package/src/commands/init.ts +225 -12
- package/src/commands/lifecycle.ts +366 -38
- package/src/commands/serve-boot.ts +71 -25
- package/src/commands/status.ts +596 -49
- package/src/hub-server.ts +11 -0
- package/src/hub-unit.ts +735 -0
- package/src/managed-unit.ts +674 -0
- package/src/module-ops-client.ts +457 -0
- package/src/port-probe.ts +50 -0
- package/src/setup-wizard.ts +80 -1
- package/src/supervisor.ts +360 -14
|
@@ -27,6 +27,12 @@ import { type ExposeState, readExposeState, writeExposeState } from "../expose-s
|
|
|
27
27
|
import { hubDbPath, openHubDb } from "../hub-db.ts";
|
|
28
28
|
import { hubFetch } from "../hub-server.ts";
|
|
29
29
|
import { getSetting, setSetting } from "../hub-settings.ts";
|
|
30
|
+
import { validateAccessToken } from "../jwt-sign.ts";
|
|
31
|
+
import {
|
|
32
|
+
OPERATOR_TOKEN_SCOPE_SET_CLAIM,
|
|
33
|
+
readOperatorTokenFile,
|
|
34
|
+
writeOperatorTokenFile,
|
|
35
|
+
} from "../operator-token.ts";
|
|
30
36
|
import { writeManifest } from "../services-manifest.ts";
|
|
31
37
|
import { SESSION_COOKIE_NAME } from "../sessions.ts";
|
|
32
38
|
import {
|
|
@@ -39,6 +45,7 @@ import {
|
|
|
39
45
|
handleSetupVaultPost,
|
|
40
46
|
postVaultImportImpl,
|
|
41
47
|
} from "../setup-wizard.ts";
|
|
48
|
+
import { rotateSigningKey } from "../signing-keys.ts";
|
|
42
49
|
import { Supervisor } from "../supervisor.ts";
|
|
43
50
|
import { createUser, getUserByUsername, userCount } from "../users.ts";
|
|
44
51
|
|
|
@@ -1007,6 +1014,129 @@ describe("handleSetupAccountPost", () => {
|
|
|
1007
1014
|
});
|
|
1008
1015
|
});
|
|
1009
1016
|
|
|
1017
|
+
// --- Phase 3b Deliverable A: fresh-box operator-token closure (§3.1) ------
|
|
1018
|
+
//
|
|
1019
|
+
// After the wizard creates the first admin, it persists ~/.parachute/operator.token
|
|
1020
|
+
// so the box has a CLI operator credential immediately — otherwise the Phase 3b
|
|
1021
|
+
// per-module verbs (start/stop/restart <svc> over the module-ops API) would 401.
|
|
1022
|
+
|
|
1023
|
+
describe("handleSetupAccountPost — operator-token closure (Phase 3b §3.1)", () => {
|
|
1024
|
+
let h: Harness;
|
|
1025
|
+
beforeEach(() => {
|
|
1026
|
+
h = makeHarness();
|
|
1027
|
+
_resetOperationsRegistryForTests();
|
|
1028
|
+
});
|
|
1029
|
+
afterEach(() => h.cleanup());
|
|
1030
|
+
|
|
1031
|
+
/** Drive a valid account-creation POST against the given deps. */
|
|
1032
|
+
async function createFirstAdmin(
|
|
1033
|
+
db: ReturnType<typeof openHubDb>,
|
|
1034
|
+
deps: Partial<Parameters<typeof handleSetupAccountPost>[1]> = {},
|
|
1035
|
+
username = "ops",
|
|
1036
|
+
): Promise<Response> {
|
|
1037
|
+
const baseDeps = {
|
|
1038
|
+
db,
|
|
1039
|
+
manifestPath: h.manifestPath,
|
|
1040
|
+
configDir: h.dir,
|
|
1041
|
+
readExposeStateFn: h.readExposeStateFn,
|
|
1042
|
+
issuer: "https://hub.example",
|
|
1043
|
+
registry: getDefaultOperationsRegistry(),
|
|
1044
|
+
};
|
|
1045
|
+
const get = handleSetupGet(req("/admin/setup"), baseDeps);
|
|
1046
|
+
const csrf = setCookie(get, CSRF_COOKIE_NAME) ?? "";
|
|
1047
|
+
const form = formBody({
|
|
1048
|
+
username,
|
|
1049
|
+
password: "correct horse battery",
|
|
1050
|
+
password_confirm: "correct horse battery",
|
|
1051
|
+
[CSRF_FIELD_NAME]: csrf,
|
|
1052
|
+
});
|
|
1053
|
+
return handleSetupAccountPost(
|
|
1054
|
+
req("/admin/setup/account", {
|
|
1055
|
+
method: "POST",
|
|
1056
|
+
body: form.body,
|
|
1057
|
+
headers: { ...form.headers, cookie: `${CSRF_COOKIE_NAME}=${csrf}` },
|
|
1058
|
+
}),
|
|
1059
|
+
{ ...baseDeps, ...deps },
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
test("persists operator.token (admin scope-set, carries parachute:host:admin)", async () => {
|
|
1064
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
1065
|
+
try {
|
|
1066
|
+
rotateSigningKey(db); // real issuance needs a signing key
|
|
1067
|
+
const post = await createFirstAdmin(db);
|
|
1068
|
+
expect(post.status).toBe(303);
|
|
1069
|
+
// The token file now exists on disk…
|
|
1070
|
+
const token = await readOperatorTokenFile(h.dir);
|
|
1071
|
+
expect(token).not.toBeNull();
|
|
1072
|
+
// …and decodes with the admin scope (the scope module-ops gates on).
|
|
1073
|
+
// The JWT carries the OAuth `scope` claim as a space-delimited string.
|
|
1074
|
+
const { payload } = await validateAccessToken(db, token ?? "", "https://hub.example");
|
|
1075
|
+
const scopes = String(payload.scope ?? "").split(" ");
|
|
1076
|
+
expect(scopes).toContain("parachute:host:admin");
|
|
1077
|
+
expect((payload as Record<string, unknown>)[OPERATOR_TOKEN_SCOPE_SET_CLAIM]).toBe("admin");
|
|
1078
|
+
} finally {
|
|
1079
|
+
db.close();
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
test("does NOT clobber an existing operator.token", async () => {
|
|
1084
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
1085
|
+
try {
|
|
1086
|
+
rotateSigningKey(db);
|
|
1087
|
+
// Plant a sentinel token before the wizard runs.
|
|
1088
|
+
await writeOperatorTokenFile("sentinel.preexisting.token", h.dir);
|
|
1089
|
+
// Use a stub issuer that fails the test if it's ever called.
|
|
1090
|
+
const post = await createFirstAdmin(db, {
|
|
1091
|
+
issueOperatorToken: async () => {
|
|
1092
|
+
throw new Error("issueOperatorToken must NOT run when a token already exists");
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
expect(post.status).toBe(303);
|
|
1096
|
+
// The pre-existing token is untouched.
|
|
1097
|
+
expect(await readOperatorTokenFile(h.dir)).toBe("sentinel.preexisting.token");
|
|
1098
|
+
} finally {
|
|
1099
|
+
db.close();
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
test("no admin created (already-bootstrapped guard) → no token written", async () => {
|
|
1104
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
1105
|
+
try {
|
|
1106
|
+
rotateSigningKey(db);
|
|
1107
|
+
// An admin already exists, so the wizard's already-bootstrapped guard
|
|
1108
|
+
// returns early (303 to /admin/setup) WITHOUT reaching createUser — and
|
|
1109
|
+
// therefore WITHOUT minting a token. The closure only fires for a
|
|
1110
|
+
// genuinely-created first admin.
|
|
1111
|
+
await createUser(db, "owner", "pw");
|
|
1112
|
+
const post = await createFirstAdmin(db, {}, "interloper");
|
|
1113
|
+
expect(post.status).toBe(303);
|
|
1114
|
+
expect(await readOperatorTokenFile(h.dir)).toBeNull();
|
|
1115
|
+
} finally {
|
|
1116
|
+
db.close();
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
test("token-write failure is non-fatal — account creation still succeeds", async () => {
|
|
1121
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
1122
|
+
try {
|
|
1123
|
+
const post = await createFirstAdmin(db, {
|
|
1124
|
+
issueOperatorToken: async () => {
|
|
1125
|
+
throw new Error("disk full");
|
|
1126
|
+
},
|
|
1127
|
+
});
|
|
1128
|
+
// The admin + session were committed despite the token-write failure.
|
|
1129
|
+
expect(post.status).toBe(303);
|
|
1130
|
+
expect(setCookie(post, SESSION_COOKIE_NAME)).toBeDefined();
|
|
1131
|
+
expect(userCount(db)).toBe(1);
|
|
1132
|
+
// No token landed (the issuer threw), but that didn't fail the request.
|
|
1133
|
+
expect(await readOperatorTokenFile(h.dir)).toBeNull();
|
|
1134
|
+
} finally {
|
|
1135
|
+
db.close();
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1010
1140
|
// --- POST /admin/setup/vault ---------------------------------------------
|
|
1011
1141
|
|
|
1012
1142
|
describe("handleSetupVaultPost", () => {
|