@openparachute/hub 0.5.13 → 0.5.14-rc.10

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 (101) hide show
  1. package/README.md +109 -15
  2. package/package.json +2 -2
  3. package/src/__tests__/account-home-ui.test.ts +205 -0
  4. package/src/__tests__/admin-handlers.test.ts +74 -0
  5. package/src/__tests__/admin-host-admin-token.test.ts +62 -0
  6. package/src/__tests__/admin-vault-admin-token.test.ts +44 -0
  7. package/src/__tests__/admin-vaults.test.ts +70 -4
  8. package/src/__tests__/api-account.test.ts +191 -1
  9. package/src/__tests__/api-mint-token.test.ts +682 -3
  10. package/src/__tests__/api-modules-config.test.ts +16 -10
  11. package/src/__tests__/api-modules-ops.test.ts +97 -0
  12. package/src/__tests__/api-modules.test.ts +100 -83
  13. package/src/__tests__/api-ready.test.ts +135 -0
  14. package/src/__tests__/api-revoke-token.test.ts +384 -0
  15. package/src/__tests__/api-users.test.ts +390 -13
  16. package/src/__tests__/chrome-strip.test.ts +15 -15
  17. package/src/__tests__/cli.test.ts +7 -5
  18. package/src/__tests__/cloudflare-detect.test.ts +60 -5
  19. package/src/__tests__/expose-auth-preflight.test.ts +58 -50
  20. package/src/__tests__/expose-cloudflare.test.ts +114 -3
  21. package/src/__tests__/expose-interactive.test.ts +10 -4
  22. package/src/__tests__/expose-public-auto.test.ts +5 -1
  23. package/src/__tests__/expose.test.ts +49 -1
  24. package/src/__tests__/hub-db.test.ts +194 -29
  25. package/src/__tests__/hub-server.test.ts +322 -33
  26. package/src/__tests__/hub.test.ts +11 -0
  27. package/src/__tests__/init.test.ts +827 -0
  28. package/src/__tests__/lifecycle.test.ts +33 -1
  29. package/src/__tests__/migrate.test.ts +433 -51
  30. package/src/__tests__/notes-redirect.test.ts +20 -20
  31. package/src/__tests__/oauth-handlers.test.ts +1060 -29
  32. package/src/__tests__/oauth-ui.test.ts +12 -1
  33. package/src/__tests__/proxy-error-ui.test.ts +212 -0
  34. package/src/__tests__/proxy-state.test.ts +192 -0
  35. package/src/__tests__/resource-binding.test.ts +97 -0
  36. package/src/__tests__/scope-explanations.test.ts +36 -0
  37. package/src/__tests__/serve.test.ts +9 -9
  38. package/src/__tests__/services-manifest.test.ts +40 -40
  39. package/src/__tests__/setup-wizard.test.ts +1114 -66
  40. package/src/__tests__/setup.test.ts +1 -1
  41. package/src/__tests__/status.test.ts +39 -0
  42. package/src/__tests__/users.test.ts +396 -9
  43. package/src/__tests__/vault-auth-status.test.ts +271 -11
  44. package/src/__tests__/vault-hub-origin-env.test.ts +126 -0
  45. package/src/__tests__/well-known.test.ts +9 -9
  46. package/src/__tests__/wizard.test.ts +372 -0
  47. package/src/account-home-ui.ts +547 -0
  48. package/src/admin-handlers.ts +49 -17
  49. package/src/admin-host-admin-token.ts +25 -0
  50. package/src/admin-login-ui.ts +4 -4
  51. package/src/admin-vault-admin-token.ts +17 -0
  52. package/src/admin-vaults.ts +48 -15
  53. package/src/api-account.ts +72 -6
  54. package/src/api-mint-token.ts +132 -24
  55. package/src/api-modules-ops.ts +52 -16
  56. package/src/api-modules.ts +31 -14
  57. package/src/api-ready.ts +102 -0
  58. package/src/api-revoke-token.ts +107 -21
  59. package/src/api-users.ts +497 -58
  60. package/src/bun-link.ts +55 -0
  61. package/src/chrome-strip.ts +6 -6
  62. package/src/cli.ts +93 -24
  63. package/src/cloudflare/config.ts +10 -4
  64. package/src/cloudflare/detect.ts +73 -6
  65. package/src/commands/expose-auth-preflight.ts +55 -63
  66. package/src/commands/expose-cloudflare.ts +114 -10
  67. package/src/commands/expose-interactive.ts +10 -11
  68. package/src/commands/expose-public-auto.ts +6 -4
  69. package/src/commands/expose.ts +8 -0
  70. package/src/commands/init.ts +563 -0
  71. package/src/commands/install.ts +41 -23
  72. package/src/commands/lifecycle.ts +12 -0
  73. package/src/commands/migrate.ts +293 -41
  74. package/src/commands/status.ts +10 -1
  75. package/src/commands/wizard.ts +843 -0
  76. package/src/env-file.ts +10 -0
  77. package/src/help.ts +157 -17
  78. package/src/hub-db.ts +42 -0
  79. package/src/hub-server.ts +136 -23
  80. package/src/hub-settings.ts +13 -2
  81. package/src/hub.ts +16 -9
  82. package/src/notes-redirect.ts +5 -5
  83. package/src/oauth-handlers.ts +342 -173
  84. package/src/oauth-ui.ts +28 -2
  85. package/src/proxy-error-ui.ts +506 -0
  86. package/src/proxy-state.ts +131 -0
  87. package/src/resource-binding.ts +134 -0
  88. package/src/scope-attenuation.ts +85 -0
  89. package/src/scope-explanations.ts +94 -5
  90. package/src/service-spec.ts +39 -18
  91. package/src/setup-wizard.ts +1173 -117
  92. package/src/users.ts +307 -29
  93. package/src/vault/auth-status.ts +152 -25
  94. package/src/vault-hub-origin-env.ts +100 -0
  95. package/web/ui/dist/assets/index-2SSK7JbM.js +61 -0
  96. package/web/ui/dist/assets/index-B28SdMSz.css +1 -0
  97. package/web/ui/dist/index.html +2 -2
  98. package/src/__tests__/vault-tokens-create-interactive.test.ts +0 -183
  99. package/src/commands/vault-tokens-create-interactive.ts +0 -143
  100. package/web/ui/dist/assets/index-7DtAXz7y.css +0 -1
  101. package/web/ui/dist/assets/index-Dzrbe6EP.js +0 -61
@@ -151,7 +151,7 @@ describe("openHubDb + migrate", () => {
151
151
  }
152
152
  });
153
153
 
154
- test("v8 adds password_changed + assigned_vault columns on a fresh DB", () => {
154
+ test("v8 added password_changed column (still present at v10)", () => {
155
155
  const h = makeHarness();
156
156
  try {
157
157
  const db = openHubDb(h.dbPath);
@@ -160,8 +160,9 @@ describe("openHubDb + migrate", () => {
160
160
  db.query<{ version: number }, []>("SELECT version FROM schema_version").all() ?? []
161
161
  ).map((r) => r.version);
162
162
  expect(versions).toContain(8);
163
- // PRAGMA table_info returns the column shape; we want both new
164
- // columns present with the right defaults / nullability.
163
+ // PRAGMA table_info returns the column shape; password_changed
164
+ // should still be on users at v10 (only assigned_vault was
165
+ // dropped in v10's recreate).
165
166
  interface ColInfo {
166
167
  name: string;
167
168
  type: string;
@@ -180,10 +181,8 @@ describe("openHubDb + migrate", () => {
180
181
  expect(pc?.notnull).toBe(1);
181
182
  // Default literal — SQLite returns it as a string "0".
182
183
  expect(pc?.dflt_value).toBe("0");
183
- const av = byName.get("assigned_vault");
184
- expect(av).toBeDefined();
185
- expect(av?.type).toBe("TEXT");
186
- expect(av?.notnull).toBe(0);
184
+ // v10 dropped assigned_vault — verify the column is gone.
185
+ expect(byName.has("assigned_vault")).toBe(false);
187
186
  } finally {
188
187
  db.close();
189
188
  }
@@ -195,21 +194,13 @@ describe("openHubDb + migrate", () => {
195
194
  test("v8 backfills password_changed=1 for users that pre-date the migration", () => {
196
195
  const h = makeHarness();
197
196
  try {
198
- // Stand up a DB at the v7 state by partially-applying migrations:
199
- // open Database directly, call migrate after stripping the v8 entry
200
- // would be invasive. Instead, drive the same migration shape by hand
201
- // for v1-v7 then insert a row, then call migrate() to apply v8.
202
- // Cleanest path: openHubDb runs everything, but we want a v7 snapshot.
203
- // Approach: open with openHubDb (runs all migrations), drop the v8
204
- // changes, mark v8 unapplied, insert a user with password_changed=0
205
- // (simulating a row from before the backfill), then re-run migrate.
206
- // SQLite doesn't have DROP COLUMN pre-3.35 universally, so we do the
207
- // recreate-and-rename: drop v8's columns by recreating users without
208
- // them, then delete the v8 schema_version row, then call migrate().
197
+ // Stand up a DB at the v7 state by recreating the users table
198
+ // without the v8/v10 columns and re-running migrate().
209
199
  const db = openHubDb(h.dbPath);
210
200
  try {
211
201
  // Build a v7-shape users table and copy the v8-shape rows.
212
202
  db.exec(`
203
+ DROP TABLE IF EXISTS user_vaults;
213
204
  CREATE TABLE users_v7 (
214
205
  id TEXT PRIMARY KEY,
215
206
  username TEXT UNIQUE NOT NULL,
@@ -222,26 +213,26 @@ describe("openHubDb + migrate", () => {
222
213
  DROP TABLE users;
223
214
  ALTER TABLE users_v7 RENAME TO users;
224
215
  `);
225
- db.exec("DELETE FROM schema_version WHERE version = 8");
216
+ db.exec("DELETE FROM schema_version WHERE version IN (8, 10)");
226
217
  // Insert a row that pre-dates v8 (no password_changed column yet).
227
218
  db.prepare(
228
219
  `INSERT INTO users (id, username, password_hash, created_at, updated_at)
229
220
  VALUES (?, ?, ?, ?, ?)`,
230
221
  ).run("legacy-user", "owner", "h", "2026-01-01", "2026-01-01");
231
- // Now re-run migrations — v8 should ALTER the table and backfill.
222
+ // Now re-run migrations — v8 + v10 apply.
232
223
  migrate(db);
233
224
  const row = db
234
- .query<{ password_changed: number; assigned_vault: string | null }, [string]>(
235
- "SELECT password_changed, assigned_vault FROM users WHERE id = ?",
225
+ .query<{ password_changed: number }, [string]>(
226
+ "SELECT password_changed FROM users WHERE id = ?",
236
227
  )
237
228
  .get("legacy-user");
238
229
  expect(row).not.toBeNull();
239
230
  expect(row?.password_changed).toBe(1);
240
- expect(row?.assigned_vault).toBeNull();
241
231
  const versions = (
242
232
  db.query<{ version: number }, []>("SELECT version FROM schema_version").all() ?? []
243
233
  ).map((r) => r.version);
244
234
  expect(versions).toContain(8);
235
+ expect(versions).toContain(10);
245
236
  } finally {
246
237
  db.close();
247
238
  }
@@ -250,24 +241,198 @@ describe("openHubDb + migrate", () => {
250
241
  }
251
242
  });
252
243
 
253
- test("v8 — fresh inserts default password_changed=0 and assigned_vault NULL", () => {
244
+ test("v8 — fresh inserts default password_changed=0 (v10 dropped assigned_vault)", () => {
254
245
  const h = makeHarness();
255
246
  try {
256
247
  const db = openHubDb(h.dbPath);
257
248
  try {
258
- // Insert via the bare-columns SQL (mirrors what a pre-v8 caller
259
- // would emit) to confirm the column DEFAULTs work.
249
+ // Insert via the bare-columns SQL to confirm the column DEFAULTs work.
260
250
  db.prepare(
261
251
  `INSERT INTO users (id, username, password_hash, created_at, updated_at)
262
252
  VALUES (?, ?, ?, ?, ?)`,
263
253
  ).run("u-default", "owner", "h", "2026-01-01", "2026-01-01");
264
254
  const row = db
265
- .query<{ password_changed: number; assigned_vault: string | null }, [string]>(
266
- "SELECT password_changed, assigned_vault FROM users WHERE id = ?",
255
+ .query<{ password_changed: number }, [string]>(
256
+ "SELECT password_changed FROM users WHERE id = ?",
267
257
  )
268
258
  .get("u-default");
269
259
  expect(row?.password_changed).toBe(0);
270
- expect(row?.assigned_vault).toBeNull();
260
+ // user_vaults table is empty for a default insert.
261
+ const vaultCount = db
262
+ .query<{ n: number }, [string]>("SELECT COUNT(*) AS n FROM user_vaults WHERE user_id = ?")
263
+ .get("u-default");
264
+ expect(vaultCount?.n).toBe(0);
265
+ } finally {
266
+ db.close();
267
+ }
268
+ } finally {
269
+ h.cleanup();
270
+ }
271
+ });
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // v10 — user_vaults many-to-many membership (multi-user Phase 2 PR 2)
275
+ // ---------------------------------------------------------------------------
276
+
277
+ test("v10 creates user_vaults table with the expected shape", () => {
278
+ const h = makeHarness();
279
+ try {
280
+ const db = openHubDb(h.dbPath);
281
+ try {
282
+ const versions = (
283
+ db.query<{ version: number }, []>("SELECT version FROM schema_version").all() ?? []
284
+ ).map((r) => r.version);
285
+ expect(versions).toContain(10);
286
+ const tables = (
287
+ db
288
+ .query<{ name: string }, []>(
289
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
290
+ )
291
+ .all() ?? []
292
+ ).map((r) => r.name);
293
+ expect(tables).toContain("user_vaults");
294
+ interface ColInfo {
295
+ name: string;
296
+ type: string;
297
+ notnull: number;
298
+ dflt_value: string | null;
299
+ }
300
+ const cols = db
301
+ .query<ColInfo, []>(
302
+ "SELECT name, type, \"notnull\", dflt_value FROM pragma_table_info('user_vaults')",
303
+ )
304
+ .all();
305
+ const names = cols.map((c) => c.name);
306
+ expect(names).toContain("user_id");
307
+ expect(names).toContain("vault_name");
308
+ expect(names).toContain("role");
309
+ expect(names).toContain("created_at");
310
+ const role = cols.find((c) => c.name === "role");
311
+ expect(role?.notnull).toBe(1);
312
+ // SQLite represents the default literal verbatim — `'write'`.
313
+ expect(role?.dflt_value).toBe("'write'");
314
+ } finally {
315
+ db.close();
316
+ }
317
+ } finally {
318
+ h.cleanup();
319
+ }
320
+ });
321
+
322
+ test("v10 backfills user_vaults from v9 assigned_vault column", () => {
323
+ const h = makeHarness();
324
+ try {
325
+ const db = openHubDb(h.dbPath);
326
+ try {
327
+ // Rebuild a v9-shape users table (with assigned_vault column),
328
+ // mark v10 unapplied, drop user_vaults, populate fixture rows,
329
+ // then re-run migrate to apply v10's backfill.
330
+ db.exec(`
331
+ DROP TABLE IF EXISTS user_vaults;
332
+ CREATE TABLE users_v9 (
333
+ id TEXT PRIMARY KEY,
334
+ username TEXT UNIQUE NOT NULL,
335
+ password_hash TEXT NOT NULL,
336
+ created_at TEXT NOT NULL,
337
+ updated_at TEXT NOT NULL,
338
+ password_changed INTEGER NOT NULL DEFAULT 0,
339
+ assigned_vault TEXT
340
+ );
341
+ INSERT INTO users_v9 (id, username, password_hash, created_at, updated_at, password_changed, assigned_vault)
342
+ VALUES
343
+ ('u-admin', 'admin', 'h', '2026-01-01', '2026-01-01', 1, NULL),
344
+ ('u-alice', 'alice', 'h', '2026-01-02', '2026-01-02', 1, 'personal'),
345
+ ('u-bob', 'bob', 'h', '2026-01-03', '2026-01-03', 1, 'family');
346
+ DROP TABLE users;
347
+ ALTER TABLE users_v9 RENAME TO users;
348
+ `);
349
+ db.exec("DELETE FROM schema_version WHERE version = 10");
350
+ migrate(db);
351
+ // Expect 2 rows in user_vaults (admin had NULL → no row).
352
+ const rows = db
353
+ .query<{ user_id: string; vault_name: string; role: string }, []>(
354
+ "SELECT user_id, vault_name, role FROM user_vaults ORDER BY user_id ASC",
355
+ )
356
+ .all();
357
+ expect(rows.length).toBe(2);
358
+ expect(rows[0]).toMatchObject({
359
+ user_id: "u-alice",
360
+ vault_name: "personal",
361
+ role: "write",
362
+ });
363
+ expect(rows[1]).toMatchObject({ user_id: "u-bob", vault_name: "family", role: "write" });
364
+ // No row for the admin.
365
+ const adminRows = db
366
+ .query<{ n: number }, [string]>("SELECT COUNT(*) AS n FROM user_vaults WHERE user_id = ?")
367
+ .get("u-admin");
368
+ expect(adminRows?.n).toBe(0);
369
+ // assigned_vault column should be gone.
370
+ interface ColInfo {
371
+ name: string;
372
+ }
373
+ const cols = db.query<ColInfo, []>("SELECT name FROM pragma_table_info('users')").all();
374
+ expect(cols.map((c) => c.name)).not.toContain("assigned_vault");
375
+ } finally {
376
+ db.close();
377
+ }
378
+ } finally {
379
+ h.cleanup();
380
+ }
381
+ });
382
+
383
+ test("v10 FK cascade: deleting a user drops their user_vaults rows", () => {
384
+ const h = makeHarness();
385
+ try {
386
+ const db = openHubDb(h.dbPath);
387
+ try {
388
+ const stamp = "2026-05-27T00:00:00.000Z";
389
+ db.prepare(
390
+ "INSERT INTO users (id, username, password_hash, created_at, updated_at, password_changed) VALUES (?, ?, ?, ?, ?, ?)",
391
+ ).run("u1", "alice", "h", stamp, stamp, 1);
392
+ db.prepare(
393
+ "INSERT INTO user_vaults (user_id, vault_name, role, created_at) VALUES (?, ?, ?, ?)",
394
+ ).run("u1", "personal", "write", stamp);
395
+ db.prepare(
396
+ "INSERT INTO user_vaults (user_id, vault_name, role, created_at) VALUES (?, ?, ?, ?)",
397
+ ).run("u1", "family", "write", stamp);
398
+ // sanity
399
+ const before = db
400
+ .query<{ n: number }, [string]>("SELECT COUNT(*) AS n FROM user_vaults WHERE user_id = ?")
401
+ .get("u1");
402
+ expect(before?.n).toBe(2);
403
+ // Delete the user — ON DELETE CASCADE should drop the user_vaults rows.
404
+ db.prepare("DELETE FROM users WHERE id = ?").run("u1");
405
+ const after = db
406
+ .query<{ n: number }, [string]>("SELECT COUNT(*) AS n FROM user_vaults WHERE user_id = ?")
407
+ .get("u1");
408
+ expect(after?.n).toBe(0);
409
+ } finally {
410
+ db.close();
411
+ }
412
+ } finally {
413
+ h.cleanup();
414
+ }
415
+ });
416
+
417
+ test("v10 (user_id, vault_name) PRIMARY KEY blocks duplicate (user, vault) pairs", () => {
418
+ const h = makeHarness();
419
+ try {
420
+ const db = openHubDb(h.dbPath);
421
+ try {
422
+ const stamp = "2026-05-27T00:00:00.000Z";
423
+ db.prepare(
424
+ "INSERT INTO users (id, username, password_hash, created_at, updated_at, password_changed) VALUES (?, ?, ?, ?, ?, ?)",
425
+ ).run("u1", "alice", "h", stamp, stamp, 1);
426
+ db.prepare(
427
+ "INSERT INTO user_vaults (user_id, vault_name, role, created_at) VALUES (?, ?, ?, ?)",
428
+ ).run("u1", "personal", "write", stamp);
429
+ expect(() =>
430
+ db
431
+ .prepare(
432
+ "INSERT INTO user_vaults (user_id, vault_name, role, created_at) VALUES (?, ?, ?, ?)",
433
+ )
434
+ .run("u1", "personal", "write", stamp),
435
+ ).toThrow();
271
436
  } finally {
272
437
  db.close();
273
438
  }