@openparachute/hub 0.3.0-rc.1 → 0.5.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/README.md +19 -17
- package/package.json +15 -4
- package/src/__tests__/admin-auth.test.ts +197 -0
- package/src/__tests__/admin-config.test.ts +281 -0
- package/src/__tests__/admin-grants.test.ts +271 -0
- package/src/__tests__/admin-handlers.test.ts +530 -0
- package/src/__tests__/admin-host-admin-token.test.ts +115 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +190 -0
- package/src/__tests__/admin-vaults.test.ts +615 -0
- package/src/__tests__/auth-codes.test.ts +253 -0
- package/src/__tests__/auth.test.ts +1063 -17
- package/src/__tests__/cli.test.ts +50 -0
- package/src/__tests__/clients.test.ts +264 -0
- package/src/__tests__/cloudflare-state.test.ts +167 -7
- package/src/__tests__/csrf.test.ts +117 -0
- package/src/__tests__/expose-cloudflare.test.ts +232 -37
- package/src/__tests__/expose-off-auto.test.ts +15 -9
- package/src/__tests__/expose-public-auto.test.ts +153 -0
- package/src/__tests__/expose.test.ts +216 -24
- package/src/__tests__/grants.test.ts +164 -0
- package/src/__tests__/hub-db.test.ts +153 -0
- package/src/__tests__/hub-server.test.ts +984 -26
- package/src/__tests__/hub.test.ts +56 -49
- package/src/__tests__/install.test.ts +327 -3
- package/src/__tests__/jwks.test.ts +37 -0
- package/src/__tests__/jwt-sign.test.ts +361 -0
- package/src/__tests__/lifecycle.test.ts +616 -5
- package/src/__tests__/module-manifest.test.ts +183 -0
- package/src/__tests__/oauth-handlers.test.ts +3112 -0
- package/src/__tests__/oauth-ui.test.ts +253 -0
- package/src/__tests__/operator-token.test.ts +140 -0
- package/src/__tests__/providers-detect.test.ts +158 -0
- package/src/__tests__/scope-explanations.test.ts +108 -0
- package/src/__tests__/scope-registry.test.ts +220 -0
- package/src/__tests__/services-manifest.test.ts +137 -1
- package/src/__tests__/sessions.test.ts +116 -0
- package/src/__tests__/setup.test.ts +361 -0
- package/src/__tests__/signing-keys.test.ts +153 -0
- package/src/__tests__/upgrade.test.ts +541 -0
- package/src/__tests__/users.test.ts +154 -0
- package/src/__tests__/well-known.test.ts +127 -10
- package/src/admin-auth.ts +126 -0
- package/src/admin-config-ui.ts +534 -0
- package/src/admin-config.ts +226 -0
- package/src/admin-grants.ts +160 -0
- package/src/admin-handlers.ts +365 -0
- package/src/admin-host-admin-token.ts +83 -0
- package/src/admin-vault-admin-token.ts +98 -0
- package/src/admin-vaults.ts +359 -0
- package/src/auth-codes.ts +189 -0
- package/src/cli.ts +202 -25
- package/src/clients.ts +210 -0
- package/src/cloudflare/config.ts +25 -6
- package/src/cloudflare/state.ts +108 -28
- package/src/commands/auth.ts +851 -19
- package/src/commands/expose-cloudflare.ts +85 -45
- package/src/commands/expose-interactive.ts +20 -44
- package/src/commands/expose-off-auto.ts +27 -11
- package/src/commands/expose-public-auto.ts +179 -0
- package/src/commands/expose.ts +63 -32
- package/src/commands/install.ts +337 -48
- package/src/commands/lifecycle.ts +269 -38
- package/src/commands/setup.ts +366 -0
- package/src/commands/status.ts +4 -1
- package/src/commands/upgrade.ts +429 -0
- package/src/csrf.ts +101 -0
- package/src/grants.ts +142 -0
- package/src/help.ts +133 -19
- package/src/hub-control.ts +12 -0
- package/src/hub-db.ts +164 -0
- package/src/hub-server.ts +643 -22
- package/src/hub.ts +97 -390
- package/src/jwks.ts +41 -0
- package/src/jwt-audience.ts +40 -0
- package/src/jwt-sign.ts +275 -0
- package/src/module-manifest.ts +435 -0
- package/src/oauth-handlers.ts +1175 -0
- package/src/oauth-ui.ts +582 -0
- package/src/operator-token.ts +129 -0
- package/src/providers/detect.ts +97 -0
- package/src/scope-explanations.ts +137 -0
- package/src/scope-registry.ts +158 -0
- package/src/service-spec.ts +270 -97
- package/src/services-manifest.ts +57 -1
- package/src/sessions.ts +115 -0
- package/src/signing-keys.ts +120 -0
- package/src/users.ts +144 -0
- package/src/well-known.ts +62 -26
- package/web/ui/dist/assets/index-BKzPDdB0.js +60 -0
- package/web/ui/dist/assets/index-Dyk6g7vT.css +1 -0
- package/web/ui/dist/index.html +14 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the per-vault session→bearer mint endpoint. Mirrors
|
|
3
|
+
* `admin-host-admin-token.test.ts` shape; differences:
|
|
4
|
+
* - Per-vault scope (`vault:<name>:admin`).
|
|
5
|
+
* - Vault name validated against the caller-supplied known-names set.
|
|
6
|
+
* - 404 on unknown name; 400 on syntactically invalid name.
|
|
7
|
+
*/
|
|
8
|
+
import type { Database } from "bun:sqlite";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
VAULT_ADMIN_TOKEN_TTL_SECONDS,
|
|
15
|
+
handleVaultAdminToken,
|
|
16
|
+
} from "../admin-vault-admin-token.ts";
|
|
17
|
+
import { hubDbPath, openHubDb } from "../hub-db.ts";
|
|
18
|
+
import { validateAccessToken } from "../jwt-sign.ts";
|
|
19
|
+
import { SESSION_TTL_MS, buildSessionCookie, createSession, deleteSession } from "../sessions.ts";
|
|
20
|
+
import { createUser } from "../users.ts";
|
|
21
|
+
|
|
22
|
+
const ISSUER = "https://hub.test";
|
|
23
|
+
|
|
24
|
+
interface Harness {
|
|
25
|
+
db: Database;
|
|
26
|
+
cleanup: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeHarness(): Harness {
|
|
30
|
+
const dir = mkdtempSync(join(tmpdir(), "phub-vault-admin-token-"));
|
|
31
|
+
const db = openHubDb(hubDbPath(dir));
|
|
32
|
+
return {
|
|
33
|
+
db,
|
|
34
|
+
cleanup: () => {
|
|
35
|
+
db.close();
|
|
36
|
+
rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let harness: Harness;
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
harness = makeHarness();
|
|
44
|
+
});
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
harness.cleanup();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
async function withSession(): Promise<{ cookie: string; userId: string }> {
|
|
50
|
+
const user = await createUser(harness.db, "operator", "hunter2");
|
|
51
|
+
const session = createSession(harness.db, { userId: user.id });
|
|
52
|
+
const cookie = buildSessionCookie(session.id, Math.floor(SESSION_TTL_MS / 1000));
|
|
53
|
+
return { cookie, userId: user.id };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const known = (...names: string[]): ReadonlySet<string> => new Set(names);
|
|
57
|
+
|
|
58
|
+
describe("handleVaultAdminToken", () => {
|
|
59
|
+
test("401 when no session cookie is present", async () => {
|
|
60
|
+
const req = new Request(`${ISSUER}/admin/vault-admin-token/default`);
|
|
61
|
+
const res = await handleVaultAdminToken(req, "default", {
|
|
62
|
+
db: harness.db,
|
|
63
|
+
issuer: ISSUER,
|
|
64
|
+
knownVaultNames: known("default"),
|
|
65
|
+
});
|
|
66
|
+
expect(res.status).toBe(401);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("401 when the cookie names a deleted session", async () => {
|
|
70
|
+
const { cookie } = await withSession();
|
|
71
|
+
const sid = cookie.match(/parachute_hub_session=([^;]+)/)?.[1] ?? "";
|
|
72
|
+
deleteSession(harness.db, sid);
|
|
73
|
+
const req = new Request(`${ISSUER}/admin/vault-admin-token/default`, { headers: { cookie } });
|
|
74
|
+
const res = await handleVaultAdminToken(req, "default", {
|
|
75
|
+
db: harness.db,
|
|
76
|
+
issuer: ISSUER,
|
|
77
|
+
knownVaultNames: known("default"),
|
|
78
|
+
});
|
|
79
|
+
expect(res.status).toBe(401);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("405 on POST", async () => {
|
|
83
|
+
const { cookie } = await withSession();
|
|
84
|
+
const req = new Request(`${ISSUER}/admin/vault-admin-token/default`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { cookie },
|
|
87
|
+
});
|
|
88
|
+
const res = await handleVaultAdminToken(req, "default", {
|
|
89
|
+
db: harness.db,
|
|
90
|
+
issuer: ISSUER,
|
|
91
|
+
knownVaultNames: known("default"),
|
|
92
|
+
});
|
|
93
|
+
expect(res.status).toBe(405);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("404 when the vault name isn't installed on this hub", async () => {
|
|
97
|
+
const { cookie } = await withSession();
|
|
98
|
+
const req = new Request(`${ISSUER}/admin/vault-admin-token/ghost`, { headers: { cookie } });
|
|
99
|
+
const res = await handleVaultAdminToken(req, "ghost", {
|
|
100
|
+
db: harness.db,
|
|
101
|
+
issuer: ISSUER,
|
|
102
|
+
knownVaultNames: known("default"),
|
|
103
|
+
});
|
|
104
|
+
expect(res.status).toBe(404);
|
|
105
|
+
const body = (await res.json()) as { error: string };
|
|
106
|
+
expect(body.error).toBe("not_found");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("400 when the vault name is syntactically invalid", async () => {
|
|
110
|
+
const { cookie } = await withSession();
|
|
111
|
+
// The router slice can hand us anything; reject names that can't be a real
|
|
112
|
+
// services.json key (slashes, dots, empty) before doing any DB work.
|
|
113
|
+
const req = new Request(`${ISSUER}/admin/vault-admin-token/has..dots`, {
|
|
114
|
+
headers: { cookie },
|
|
115
|
+
});
|
|
116
|
+
const res = await handleVaultAdminToken(req, "has..dots", {
|
|
117
|
+
db: harness.db,
|
|
118
|
+
issuer: ISSUER,
|
|
119
|
+
knownVaultNames: known("has..dots"),
|
|
120
|
+
});
|
|
121
|
+
expect(res.status).toBe(400);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("200 mints a JWT carrying vault:<name>:admin", async () => {
|
|
125
|
+
const { cookie, userId } = await withSession();
|
|
126
|
+
const req = new Request(`${ISSUER}/admin/vault-admin-token/work`, { headers: { cookie } });
|
|
127
|
+
const res = await handleVaultAdminToken(req, "work", {
|
|
128
|
+
db: harness.db,
|
|
129
|
+
issuer: ISSUER,
|
|
130
|
+
knownVaultNames: known("work", "default"),
|
|
131
|
+
});
|
|
132
|
+
expect(res.status).toBe(200);
|
|
133
|
+
expect(res.headers.get("cache-control")).toBe("no-store");
|
|
134
|
+
|
|
135
|
+
const body = (await res.json()) as { token: string; expires_at: string; scopes: string[] };
|
|
136
|
+
expect(body.scopes).toEqual(["vault:work:admin"]);
|
|
137
|
+
expect(body.token.length).toBeGreaterThan(20);
|
|
138
|
+
|
|
139
|
+
const expMs = new Date(body.expires_at).getTime();
|
|
140
|
+
const skew = expMs - Date.now();
|
|
141
|
+
expect(skew).toBeGreaterThan((VAULT_ADMIN_TOKEN_TTL_SECONDS - 30) * 1000);
|
|
142
|
+
expect(skew).toBeLessThan((VAULT_ADMIN_TOKEN_TTL_SECONDS + 30) * 1000);
|
|
143
|
+
|
|
144
|
+
const validated = await validateAccessToken(harness.db, body.token, ISSUER);
|
|
145
|
+
expect(validated.payload.sub).toBe(userId);
|
|
146
|
+
expect(validated.payload.iss).toBe(ISSUER);
|
|
147
|
+
// Per-vault audience: vault validates `aud === "vault.<name>"` against
|
|
148
|
+
// its own URL-bound config (parachute-vault src/auth.ts ~line 167).
|
|
149
|
+
// A constant `aud: "hub"` here would be rejected by every vault.
|
|
150
|
+
expect(validated.payload.aud).toBe("vault.work");
|
|
151
|
+
const scopeClaim = (validated.payload as { scope?: string }).scope ?? "";
|
|
152
|
+
expect(scopeClaim.split(/\s+/)).toContain("vault:work:admin");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("audience is per-vault — different vaults get different aud claims", async () => {
|
|
156
|
+
// Regression for PR #173 follow-up: a single shared audience constant
|
|
157
|
+
// meant a token minted for vault `boulder` carried `aud: "hub"` and
|
|
158
|
+
// got rejected by vault's strict-equality check against
|
|
159
|
+
// `vault.boulder`. Mint twice and confirm each token is bound to its
|
|
160
|
+
// own vault.
|
|
161
|
+
const { cookie } = await withSession();
|
|
162
|
+
const namesSet = known("default", "boulder");
|
|
163
|
+
|
|
164
|
+
const reqDefault = new Request(`${ISSUER}/admin/vault-admin-token/default`, {
|
|
165
|
+
headers: { cookie },
|
|
166
|
+
});
|
|
167
|
+
const resDefault = await handleVaultAdminToken(reqDefault, "default", {
|
|
168
|
+
db: harness.db,
|
|
169
|
+
issuer: ISSUER,
|
|
170
|
+
knownVaultNames: namesSet,
|
|
171
|
+
});
|
|
172
|
+
expect(resDefault.status).toBe(200);
|
|
173
|
+
const bodyDefault = (await resDefault.json()) as { token: string };
|
|
174
|
+
const validatedDefault = await validateAccessToken(harness.db, bodyDefault.token, ISSUER);
|
|
175
|
+
expect(validatedDefault.payload.aud).toBe("vault.default");
|
|
176
|
+
|
|
177
|
+
const reqBoulder = new Request(`${ISSUER}/admin/vault-admin-token/boulder`, {
|
|
178
|
+
headers: { cookie },
|
|
179
|
+
});
|
|
180
|
+
const resBoulder = await handleVaultAdminToken(reqBoulder, "boulder", {
|
|
181
|
+
db: harness.db,
|
|
182
|
+
issuer: ISSUER,
|
|
183
|
+
knownVaultNames: namesSet,
|
|
184
|
+
});
|
|
185
|
+
expect(resBoulder.status).toBe(200);
|
|
186
|
+
const bodyBoulder = (await resBoulder.json()) as { token: string };
|
|
187
|
+
const validatedBoulder = await validateAccessToken(harness.db, bodyBoulder.token, ISSUER);
|
|
188
|
+
expect(validatedBoulder.payload.aud).toBe("vault.boulder");
|
|
189
|
+
});
|
|
190
|
+
});
|