@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.
- package/README.md +109 -15
- package/package.json +2 -2
- package/src/__tests__/account-home-ui.test.ts +205 -0
- package/src/__tests__/admin-handlers.test.ts +74 -0
- package/src/__tests__/admin-host-admin-token.test.ts +62 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +44 -0
- package/src/__tests__/admin-vaults.test.ts +70 -4
- package/src/__tests__/api-account.test.ts +191 -1
- package/src/__tests__/api-mint-token.test.ts +682 -3
- package/src/__tests__/api-modules-config.test.ts +16 -10
- package/src/__tests__/api-modules-ops.test.ts +97 -0
- package/src/__tests__/api-modules.test.ts +100 -83
- package/src/__tests__/api-ready.test.ts +135 -0
- package/src/__tests__/api-revoke-token.test.ts +384 -0
- package/src/__tests__/api-users.test.ts +390 -13
- package/src/__tests__/chrome-strip.test.ts +15 -15
- package/src/__tests__/cli.test.ts +7 -5
- package/src/__tests__/cloudflare-detect.test.ts +60 -5
- package/src/__tests__/expose-auth-preflight.test.ts +58 -50
- package/src/__tests__/expose-cloudflare.test.ts +114 -3
- package/src/__tests__/expose-interactive.test.ts +10 -4
- package/src/__tests__/expose-public-auto.test.ts +5 -1
- package/src/__tests__/expose.test.ts +49 -1
- package/src/__tests__/hub-db.test.ts +194 -29
- package/src/__tests__/hub-server.test.ts +322 -33
- package/src/__tests__/hub.test.ts +11 -0
- package/src/__tests__/init.test.ts +827 -0
- package/src/__tests__/lifecycle.test.ts +33 -1
- package/src/__tests__/migrate.test.ts +433 -51
- package/src/__tests__/notes-redirect.test.ts +20 -20
- package/src/__tests__/oauth-handlers.test.ts +1060 -29
- package/src/__tests__/oauth-ui.test.ts +12 -1
- package/src/__tests__/proxy-error-ui.test.ts +212 -0
- package/src/__tests__/proxy-state.test.ts +192 -0
- package/src/__tests__/resource-binding.test.ts +97 -0
- package/src/__tests__/scope-explanations.test.ts +36 -0
- package/src/__tests__/serve.test.ts +9 -9
- package/src/__tests__/services-manifest.test.ts +40 -40
- package/src/__tests__/setup-wizard.test.ts +1114 -66
- package/src/__tests__/setup.test.ts +1 -1
- package/src/__tests__/status.test.ts +39 -0
- package/src/__tests__/users.test.ts +396 -9
- package/src/__tests__/vault-auth-status.test.ts +271 -11
- package/src/__tests__/vault-hub-origin-env.test.ts +126 -0
- package/src/__tests__/well-known.test.ts +9 -9
- package/src/__tests__/wizard.test.ts +372 -0
- package/src/account-home-ui.ts +547 -0
- package/src/admin-handlers.ts +49 -17
- package/src/admin-host-admin-token.ts +25 -0
- package/src/admin-login-ui.ts +4 -4
- package/src/admin-vault-admin-token.ts +17 -0
- package/src/admin-vaults.ts +48 -15
- package/src/api-account.ts +72 -6
- package/src/api-mint-token.ts +132 -24
- package/src/api-modules-ops.ts +52 -16
- package/src/api-modules.ts +31 -14
- package/src/api-ready.ts +102 -0
- package/src/api-revoke-token.ts +107 -21
- package/src/api-users.ts +497 -58
- package/src/bun-link.ts +55 -0
- package/src/chrome-strip.ts +6 -6
- package/src/cli.ts +93 -24
- package/src/cloudflare/config.ts +10 -4
- package/src/cloudflare/detect.ts +73 -6
- package/src/commands/expose-auth-preflight.ts +55 -63
- package/src/commands/expose-cloudflare.ts +114 -10
- package/src/commands/expose-interactive.ts +10 -11
- package/src/commands/expose-public-auto.ts +6 -4
- package/src/commands/expose.ts +8 -0
- package/src/commands/init.ts +563 -0
- package/src/commands/install.ts +41 -23
- package/src/commands/lifecycle.ts +12 -0
- package/src/commands/migrate.ts +293 -41
- package/src/commands/status.ts +10 -1
- package/src/commands/wizard.ts +843 -0
- package/src/env-file.ts +10 -0
- package/src/help.ts +157 -17
- package/src/hub-db.ts +42 -0
- package/src/hub-server.ts +136 -23
- package/src/hub-settings.ts +13 -2
- package/src/hub.ts +16 -9
- package/src/notes-redirect.ts +5 -5
- package/src/oauth-handlers.ts +342 -173
- package/src/oauth-ui.ts +28 -2
- package/src/proxy-error-ui.ts +506 -0
- package/src/proxy-state.ts +131 -0
- package/src/resource-binding.ts +134 -0
- package/src/scope-attenuation.ts +85 -0
- package/src/scope-explanations.ts +94 -5
- package/src/service-spec.ts +39 -18
- package/src/setup-wizard.ts +1173 -117
- package/src/users.ts +307 -29
- package/src/vault/auth-status.ts +152 -25
- package/src/vault-hub-origin-env.ts +100 -0
- package/web/ui/dist/assets/index-2SSK7JbM.js +61 -0
- package/web/ui/dist/assets/index-B28SdMSz.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/vault-tokens-create-interactive.test.ts +0 -183
- package/src/commands/vault-tokens-create-interactive.ts +0 -143
- package/web/ui/dist/assets/index-7DtAXz7y.css +0 -1
- 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
|
|
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;
|
|
164
|
-
//
|
|
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
|
-
|
|
184
|
-
expect(
|
|
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
|
|
199
|
-
//
|
|
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
|
|
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
|
|
222
|
+
// Now re-run migrations — v8 + v10 apply.
|
|
232
223
|
migrate(db);
|
|
233
224
|
const row = db
|
|
234
|
-
.query<{ password_changed: number
|
|
235
|
-
"SELECT password_changed
|
|
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
|
|
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
|
|
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
|
|
266
|
-
"SELECT password_changed
|
|
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
|
-
|
|
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
|
}
|