@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.
Files changed (91) hide show
  1. package/README.md +19 -17
  2. package/package.json +15 -4
  3. package/src/__tests__/admin-auth.test.ts +197 -0
  4. package/src/__tests__/admin-config.test.ts +281 -0
  5. package/src/__tests__/admin-grants.test.ts +271 -0
  6. package/src/__tests__/admin-handlers.test.ts +530 -0
  7. package/src/__tests__/admin-host-admin-token.test.ts +115 -0
  8. package/src/__tests__/admin-vault-admin-token.test.ts +190 -0
  9. package/src/__tests__/admin-vaults.test.ts +615 -0
  10. package/src/__tests__/auth-codes.test.ts +253 -0
  11. package/src/__tests__/auth.test.ts +1063 -17
  12. package/src/__tests__/cli.test.ts +50 -0
  13. package/src/__tests__/clients.test.ts +264 -0
  14. package/src/__tests__/cloudflare-state.test.ts +167 -7
  15. package/src/__tests__/csrf.test.ts +117 -0
  16. package/src/__tests__/expose-cloudflare.test.ts +232 -37
  17. package/src/__tests__/expose-off-auto.test.ts +15 -9
  18. package/src/__tests__/expose-public-auto.test.ts +153 -0
  19. package/src/__tests__/expose.test.ts +216 -24
  20. package/src/__tests__/grants.test.ts +164 -0
  21. package/src/__tests__/hub-db.test.ts +153 -0
  22. package/src/__tests__/hub-server.test.ts +984 -26
  23. package/src/__tests__/hub.test.ts +56 -49
  24. package/src/__tests__/install.test.ts +327 -3
  25. package/src/__tests__/jwks.test.ts +37 -0
  26. package/src/__tests__/jwt-sign.test.ts +361 -0
  27. package/src/__tests__/lifecycle.test.ts +616 -5
  28. package/src/__tests__/module-manifest.test.ts +183 -0
  29. package/src/__tests__/oauth-handlers.test.ts +3112 -0
  30. package/src/__tests__/oauth-ui.test.ts +253 -0
  31. package/src/__tests__/operator-token.test.ts +140 -0
  32. package/src/__tests__/providers-detect.test.ts +158 -0
  33. package/src/__tests__/scope-explanations.test.ts +108 -0
  34. package/src/__tests__/scope-registry.test.ts +220 -0
  35. package/src/__tests__/services-manifest.test.ts +137 -1
  36. package/src/__tests__/sessions.test.ts +116 -0
  37. package/src/__tests__/setup.test.ts +361 -0
  38. package/src/__tests__/signing-keys.test.ts +153 -0
  39. package/src/__tests__/upgrade.test.ts +541 -0
  40. package/src/__tests__/users.test.ts +154 -0
  41. package/src/__tests__/well-known.test.ts +127 -10
  42. package/src/admin-auth.ts +126 -0
  43. package/src/admin-config-ui.ts +534 -0
  44. package/src/admin-config.ts +226 -0
  45. package/src/admin-grants.ts +160 -0
  46. package/src/admin-handlers.ts +365 -0
  47. package/src/admin-host-admin-token.ts +83 -0
  48. package/src/admin-vault-admin-token.ts +98 -0
  49. package/src/admin-vaults.ts +359 -0
  50. package/src/auth-codes.ts +189 -0
  51. package/src/cli.ts +202 -25
  52. package/src/clients.ts +210 -0
  53. package/src/cloudflare/config.ts +25 -6
  54. package/src/cloudflare/state.ts +108 -28
  55. package/src/commands/auth.ts +851 -19
  56. package/src/commands/expose-cloudflare.ts +85 -45
  57. package/src/commands/expose-interactive.ts +20 -44
  58. package/src/commands/expose-off-auto.ts +27 -11
  59. package/src/commands/expose-public-auto.ts +179 -0
  60. package/src/commands/expose.ts +63 -32
  61. package/src/commands/install.ts +337 -48
  62. package/src/commands/lifecycle.ts +269 -38
  63. package/src/commands/setup.ts +366 -0
  64. package/src/commands/status.ts +4 -1
  65. package/src/commands/upgrade.ts +429 -0
  66. package/src/csrf.ts +101 -0
  67. package/src/grants.ts +142 -0
  68. package/src/help.ts +133 -19
  69. package/src/hub-control.ts +12 -0
  70. package/src/hub-db.ts +164 -0
  71. package/src/hub-server.ts +643 -22
  72. package/src/hub.ts +97 -390
  73. package/src/jwks.ts +41 -0
  74. package/src/jwt-audience.ts +40 -0
  75. package/src/jwt-sign.ts +275 -0
  76. package/src/module-manifest.ts +435 -0
  77. package/src/oauth-handlers.ts +1175 -0
  78. package/src/oauth-ui.ts +582 -0
  79. package/src/operator-token.ts +129 -0
  80. package/src/providers/detect.ts +97 -0
  81. package/src/scope-explanations.ts +137 -0
  82. package/src/scope-registry.ts +158 -0
  83. package/src/service-spec.ts +270 -97
  84. package/src/services-manifest.ts +57 -1
  85. package/src/sessions.ts +115 -0
  86. package/src/signing-keys.ts +120 -0
  87. package/src/users.ts +144 -0
  88. package/src/well-known.ts +62 -26
  89. package/web/ui/dist/assets/index-BKzPDdB0.js +60 -0
  90. package/web/ui/dist/assets/index-Dyk6g7vT.css +1 -0
  91. package/web/ui/dist/index.html +14 -0
@@ -0,0 +1,253 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ AuthCodeExpiredError,
8
+ AuthCodeNotFoundError,
9
+ AuthCodePkceMismatchError,
10
+ AuthCodeRedirectMismatchError,
11
+ AuthCodeUsedError,
12
+ issueAuthCode,
13
+ redeemAuthCode,
14
+ verifyPkce,
15
+ } from "../auth-codes.ts";
16
+ import { registerClient } from "../clients.ts";
17
+ import { hubDbPath, openHubDb } from "../hub-db.ts";
18
+ import { createUser } from "../users.ts";
19
+
20
+ async function makeDb() {
21
+ const configDir = mkdtempSync(join(tmpdir(), "phub-codes-"));
22
+ const db = openHubDb(hubDbPath(configDir));
23
+ const user = await createUser(db, "owner", "pw");
24
+ const reg = registerClient(db, { redirectUris: ["https://example.com/cb"] });
25
+ return {
26
+ db,
27
+ userId: user.id,
28
+ clientId: reg.client.clientId,
29
+ cleanup: () => {
30
+ db.close();
31
+ rmSync(configDir, { recursive: true, force: true });
32
+ },
33
+ };
34
+ }
35
+
36
+ function s256(verifier: string): string {
37
+ return createHash("sha256").update(verifier).digest("base64url");
38
+ }
39
+
40
+ describe("issueAuthCode", () => {
41
+ test("inserts a row with all fields populated", async () => {
42
+ const { db, userId, clientId, cleanup } = await makeDb();
43
+ try {
44
+ const code = issueAuthCode(db, {
45
+ clientId,
46
+ userId,
47
+ redirectUri: "https://example.com/cb",
48
+ scopes: ["vault.read"],
49
+ codeChallenge: s256("verifier"),
50
+ codeChallengeMethod: "S256",
51
+ });
52
+ expect(code.code.length).toBeGreaterThan(20);
53
+ expect(code.scopes).toEqual(["vault.read"]);
54
+ expect(code.usedAt).toBeNull();
55
+ } finally {
56
+ cleanup();
57
+ }
58
+ });
59
+ });
60
+
61
+ describe("redeemAuthCode", () => {
62
+ test("happy path returns the auth code and stamps used_at", async () => {
63
+ const { db, userId, clientId, cleanup } = await makeDb();
64
+ try {
65
+ const verifier = "verifier-string-long-enough";
66
+ const issued = issueAuthCode(db, {
67
+ clientId,
68
+ userId,
69
+ redirectUri: "https://example.com/cb",
70
+ scopes: ["vault.read"],
71
+ codeChallenge: s256(verifier),
72
+ codeChallengeMethod: "S256",
73
+ });
74
+ const redeemed = redeemAuthCode(db, {
75
+ code: issued.code,
76
+ clientId,
77
+ redirectUri: "https://example.com/cb",
78
+ codeVerifier: verifier,
79
+ });
80
+ expect(redeemed.userId).toBe(userId);
81
+ expect(redeemed.usedAt).not.toBeNull();
82
+ } finally {
83
+ cleanup();
84
+ }
85
+ });
86
+
87
+ test("second redeem of the same code throws AuthCodeUsedError", async () => {
88
+ const { db, userId, clientId, cleanup } = await makeDb();
89
+ try {
90
+ const verifier = "v".repeat(43);
91
+ const issued = issueAuthCode(db, {
92
+ clientId,
93
+ userId,
94
+ redirectUri: "https://example.com/cb",
95
+ scopes: [],
96
+ codeChallenge: s256(verifier),
97
+ codeChallengeMethod: "S256",
98
+ });
99
+ redeemAuthCode(db, {
100
+ code: issued.code,
101
+ clientId,
102
+ redirectUri: "https://example.com/cb",
103
+ codeVerifier: verifier,
104
+ });
105
+ expect(() =>
106
+ redeemAuthCode(db, {
107
+ code: issued.code,
108
+ clientId,
109
+ redirectUri: "https://example.com/cb",
110
+ codeVerifier: verifier,
111
+ }),
112
+ ).toThrow(AuthCodeUsedError);
113
+ } finally {
114
+ cleanup();
115
+ }
116
+ });
117
+
118
+ test("expired code throws AuthCodeExpiredError", async () => {
119
+ const { db, userId, clientId, cleanup } = await makeDb();
120
+ try {
121
+ const verifier = "verifier";
122
+ const epoch = new Date("2026-01-01T00:00:00Z");
123
+ const issued = issueAuthCode(db, {
124
+ clientId,
125
+ userId,
126
+ redirectUri: "https://example.com/cb",
127
+ scopes: [],
128
+ codeChallenge: s256(verifier),
129
+ codeChallengeMethod: "S256",
130
+ now: () => epoch,
131
+ });
132
+ const later = new Date(epoch.getTime() + 90_000); // 90s > 60s TTL
133
+ expect(() =>
134
+ redeemAuthCode(db, {
135
+ code: issued.code,
136
+ clientId,
137
+ redirectUri: "https://example.com/cb",
138
+ codeVerifier: verifier,
139
+ now: () => later,
140
+ }),
141
+ ).toThrow(AuthCodeExpiredError);
142
+ } finally {
143
+ cleanup();
144
+ }
145
+ });
146
+
147
+ test("PKCE verifier mismatch throws AuthCodePkceMismatchError", async () => {
148
+ const { db, userId, clientId, cleanup } = await makeDb();
149
+ try {
150
+ const issued = issueAuthCode(db, {
151
+ clientId,
152
+ userId,
153
+ redirectUri: "https://example.com/cb",
154
+ scopes: [],
155
+ codeChallenge: s256("the-real-verifier"),
156
+ codeChallengeMethod: "S256",
157
+ });
158
+ expect(() =>
159
+ redeemAuthCode(db, {
160
+ code: issued.code,
161
+ clientId,
162
+ redirectUri: "https://example.com/cb",
163
+ codeVerifier: "wrong-verifier",
164
+ }),
165
+ ).toThrow(AuthCodePkceMismatchError);
166
+ } finally {
167
+ cleanup();
168
+ }
169
+ });
170
+
171
+ test("redirect_uri mismatch throws AuthCodeRedirectMismatchError", async () => {
172
+ const { db, userId, clientId, cleanup } = await makeDb();
173
+ try {
174
+ const verifier = "v";
175
+ const issued = issueAuthCode(db, {
176
+ clientId,
177
+ userId,
178
+ redirectUri: "https://example.com/cb",
179
+ scopes: [],
180
+ codeChallenge: s256(verifier),
181
+ codeChallengeMethod: "S256",
182
+ });
183
+ expect(() =>
184
+ redeemAuthCode(db, {
185
+ code: issued.code,
186
+ clientId,
187
+ redirectUri: "https://elsewhere.com/cb",
188
+ codeVerifier: verifier,
189
+ }),
190
+ ).toThrow(AuthCodeRedirectMismatchError);
191
+ } finally {
192
+ cleanup();
193
+ }
194
+ });
195
+
196
+ test("client_id mismatch throws AuthCodeNotFoundError (not an info leak)", async () => {
197
+ const { db, userId, clientId, cleanup } = await makeDb();
198
+ try {
199
+ const verifier = "v";
200
+ const issued = issueAuthCode(db, {
201
+ clientId,
202
+ userId,
203
+ redirectUri: "https://example.com/cb",
204
+ scopes: [],
205
+ codeChallenge: s256(verifier),
206
+ codeChallengeMethod: "S256",
207
+ });
208
+ expect(() =>
209
+ redeemAuthCode(db, {
210
+ code: issued.code,
211
+ clientId: "different-client",
212
+ redirectUri: "https://example.com/cb",
213
+ codeVerifier: verifier,
214
+ }),
215
+ ).toThrow(AuthCodeNotFoundError);
216
+ } finally {
217
+ cleanup();
218
+ }
219
+ });
220
+
221
+ test("unknown code throws AuthCodeNotFoundError", async () => {
222
+ const { db, clientId, cleanup } = await makeDb();
223
+ try {
224
+ expect(() =>
225
+ redeemAuthCode(db, {
226
+ code: "no-such-code",
227
+ clientId,
228
+ redirectUri: "https://example.com/cb",
229
+ codeVerifier: "v",
230
+ }),
231
+ ).toThrow(AuthCodeNotFoundError);
232
+ } finally {
233
+ cleanup();
234
+ }
235
+ });
236
+ });
237
+
238
+ describe("verifyPkce", () => {
239
+ test("S256 round-trip verifies", () => {
240
+ const verifier = "test-verifier";
241
+ const challenge = s256(verifier);
242
+ expect(verifyPkce(challenge, "S256", verifier)).toBe(true);
243
+ expect(verifyPkce(challenge, "S256", "wrong")).toBe(false);
244
+ });
245
+
246
+ test("plain method is rejected", () => {
247
+ expect(verifyPkce("anything", "plain", "anything")).toBe(false);
248
+ });
249
+
250
+ test("unknown method is rejected", () => {
251
+ expect(verifyPkce("x", "S512", "x")).toBe(false);
252
+ });
253
+ });