@openparachute/hub 0.6.4-rc.2 → 0.6.4-rc.3
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__/users.test.ts +39 -0
- package/src/users.ts +12 -4
package/package.json
CHANGED
|
@@ -304,6 +304,45 @@ describe("deleteUser", () => {
|
|
|
304
304
|
cleanup();
|
|
305
305
|
}
|
|
306
306
|
});
|
|
307
|
+
|
|
308
|
+
test("deletes a user holding an auth_codes row (hub#559 — OAuth-authorize FK regression)", async () => {
|
|
309
|
+
// A user who completed an OAuth authorize has an `auth_codes` row whose
|
|
310
|
+
// NOT-NULL, non-cascading FK to users(id) outlives its 60s TTL. Before the
|
|
311
|
+
// fix, that pinned the FK and `DELETE FROM users` threw
|
|
312
|
+
// SQLITE_CONSTRAINT_FOREIGNKEY → a 500 on the admin "delete user" action.
|
|
313
|
+
const { db, cleanup } = makeDb();
|
|
314
|
+
try {
|
|
315
|
+
const u = await createUser(db, "ag", "ag-strong-passphrase");
|
|
316
|
+
// auth_codes.client_id FKs to clients — seed a minimal client first.
|
|
317
|
+
db.prepare(
|
|
318
|
+
"INSERT INTO clients (client_id, redirect_uris, scopes, registered_at) VALUES (?, ?, ?, ?)",
|
|
319
|
+
).run("client-x", "https://app.example/cb", "vault:default:read", "2026-06-04T00:00:00.000Z");
|
|
320
|
+
db.prepare(
|
|
321
|
+
`INSERT INTO auth_codes
|
|
322
|
+
(code, client_id, user_id, redirect_uri, scopes, code_challenge, code_challenge_method, expires_at, used_at, created_at)
|
|
323
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
324
|
+
).run(
|
|
325
|
+
"dead-code",
|
|
326
|
+
"client-x",
|
|
327
|
+
u.id,
|
|
328
|
+
"https://app.example/cb",
|
|
329
|
+
"vault:default:read",
|
|
330
|
+
"challenge",
|
|
331
|
+
"S256",
|
|
332
|
+
"2026-06-04T00:00:00.000Z", // long-expired
|
|
333
|
+
"2026-06-04T00:00:00.000Z", // already used
|
|
334
|
+
"2026-06-04T00:00:00.000Z",
|
|
335
|
+
);
|
|
336
|
+
expect(deleteUser(db, u.id)).toBe(true);
|
|
337
|
+
expect(getUserById(db, u.id)).toBeNull();
|
|
338
|
+
// The dead auth_code is gone too (hard-deleted with the user).
|
|
339
|
+
expect(db.query("SELECT COUNT(*) c FROM auth_codes WHERE user_id = ?").get(u.id)).toEqual({
|
|
340
|
+
c: 0,
|
|
341
|
+
});
|
|
342
|
+
} finally {
|
|
343
|
+
cleanup();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
307
346
|
});
|
|
308
347
|
|
|
309
348
|
describe("validateUsername", () => {
|
package/src/users.ts
CHANGED
|
@@ -601,8 +601,10 @@ export async function resetUserPassword(
|
|
|
601
601
|
* parent users row. The audit trail survives via the `subject`
|
|
602
602
|
* column we backfill from the username plus the existing
|
|
603
603
|
* `created_at`, `scopes`, `client_id`, `revoked_at` fields.
|
|
604
|
-
* - `sessions.user_id` and `
|
|
605
|
-
* non-cascading FK.
|
|
604
|
+
* - `sessions.user_id`, `grants.user_id`, and `auth_codes.user_id` are
|
|
605
|
+
* NOT NULL with a non-cascading (RESTRICT) FK. All three are deleted
|
|
606
|
+
* before the users row drops — auth_codes are ephemeral OAuth codes
|
|
607
|
+
* (60s TTL, no audit value), so a hard-delete is correct (hub#559).
|
|
606
608
|
* - `user_vaults.user_id` has `ON DELETE CASCADE` (migration v10), so
|
|
607
609
|
* vault assignments are dropped automatically when the parent row
|
|
608
610
|
* goes. No explicit cleanup needed.
|
|
@@ -630,10 +632,16 @@ export function deleteUser(db: Database, userId: string): boolean {
|
|
|
630
632
|
db.prepare(
|
|
631
633
|
"UPDATE tokens SET subject = COALESCE(subject, ?), user_id = NULL WHERE user_id = ?",
|
|
632
634
|
).run(row.username, userId);
|
|
633
|
-
// 2. Drop sessions + grants.
|
|
634
|
-
// leaving rows behind
|
|
635
|
+
// 2. Drop sessions + grants + auth_codes. All have NOT-NULL, non-cascading
|
|
636
|
+
// (RESTRICT) FKs on user_id; leaving rows behind blocks the users delete
|
|
637
|
+
// below with SQLITE_CONSTRAINT_FOREIGNKEY. auth_codes are short-lived
|
|
638
|
+
// (60s TTL) OAuth authorization codes with no audit value — hard-delete,
|
|
639
|
+
// same as sessions. (Omitting this 500'd a real delete of a user who had
|
|
640
|
+
// completed an OAuth authorize: the code row outlived its TTL but still
|
|
641
|
+
// pinned the FK. hub#559.)
|
|
635
642
|
db.prepare("DELETE FROM sessions WHERE user_id = ?").run(userId);
|
|
636
643
|
db.prepare("DELETE FROM grants WHERE user_id = ?").run(userId);
|
|
644
|
+
db.prepare("DELETE FROM auth_codes WHERE user_id = ?").run(userId);
|
|
637
645
|
// 3. Drop the user row itself.
|
|
638
646
|
db.prepare("DELETE FROM users WHERE id = ?").run(userId);
|
|
639
647
|
})();
|