diff --git a/public/docs.css b/public/docs.css index 37de1cb..18791a9 100644 --- a/public/docs.css +++ b/public/docs.css @@ -360,7 +360,7 @@ pre code { } .indiko-demo-button::before { - content: ''; + content: ""; position: absolute; top: -4px; left: -4px; @@ -489,7 +489,7 @@ input[type="checkbox"]:checked::after { #startBtn::before, #exchangeBtn::before { - content: ''; + content: ""; position: absolute; top: -4px; left: -4px; diff --git a/scripts/seed-dev.ts b/scripts/seed-dev.ts index ebb0e9a..57b4cd2 100644 --- a/scripts/seed-dev.ts +++ b/scripts/seed-dev.ts @@ -35,15 +35,19 @@ if (reset) { db.run(`DELETE FROM ${table};`); } // Reset autoincrement counters - db.run("DELETE FROM sqlite_sequence WHERE name IN ('users','credentials','sessions','apps','permissions','invites','tokens','invite_roles','invite_uses','authcodes','challenges');"); + db.run( + "DELETE FROM sqlite_sequence WHERE name IN ('users','credentials','sessions','apps','permissions','invites','tokens','invite_roles','invite_uses','authcodes','challenges');", + ); console.log("done."); } const now = Math.floor(Date.now() / 1000); // --- Users --- +// tacy is always the admin (index 0). Look up existing users by username +// so we can seed on top of a live db without --reset. const users = [ - { username: "kieran", name: "Kieran Klukas", email: "kieran@dunkirk.sh", tier: "admin", status: "active" }, + { username: "tacy", name: "Kieran Klukas", email: "kieran@dunkirk.sh", tier: "admin", status: "active" }, { username: "alice", name: "Alice Carter", email: "alice@example.com", tier: "developer", status: "active" }, { username: "bob", name: "Bob Nguyen", email: "bob@example.com", tier: "user", status: "active" }, { username: "charlie", name: "Charlie Park", email: "charlie@example.com", tier: "user", status: "suspended" }, @@ -52,30 +56,46 @@ const users = [ ]; const userIds: number[] = []; +let insertedUsers = 0; for (const u of users) { - const isAdmin = u.tier === "admin" ? 1 : 0; - const res = db - .query( - `INSERT INTO users (username, name, email, tier, status, is_admin, created_at, url) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - ) - .run( - u.username, - u.name, - u.email, - u.tier, - u.status, - isAdmin, - now - Math.floor(Math.random() * 30 * 86400), - `https://${u.username}.example.com`, - ); - userIds.push(Number(res.lastInsertRowid)); + const existing = db + .query("SELECT id FROM users WHERE username = ?") + .get(u.username) as { id: number } | undefined; + + if (existing) { + userIds.push(existing.id); + } else { + const isAdmin = u.tier === "admin" ? 1 : 0; + const res = db + .query( + `INSERT INTO users (username, name, email, tier, status, is_admin, created_at, url) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .run( + u.username, + u.name, + u.email, + u.tier, + u.status, + isAdmin, + now - Math.floor(Math.random() * 30 * 86400), + `https://${u.username}.example.com`, + ); + userIds.push(Number(res.lastInsertRowid)); + insertedUsers++; + } } -console.log(`inserted ${userIds.length} users`); +console.log(`${insertedUsers} new users inserted, ${users.length - insertedUsers} existing`); // --- Credentials (fake passkeys) --- +// Only insert fake creds for newly-created users. Existing users keep their real passkeys. let credCount = 0; for (const [i, uid] of userIds.entries()) { + const existingCreds = db + .query("SELECT COUNT(*) as count FROM credentials WHERE user_id = ?") + .get(uid) as { count: number }; + if (existingCreds.count > 0) continue; + const numCreds = i === 0 ? 3 : i % 2 === 0 ? 2 : 1; for (let j = 0; j < numCreds; j++) { const credId = crypto.getRandomValues(new Uint8Array(32)); @@ -97,11 +117,20 @@ for (const [i, uid] of userIds.entries()) { console.log(`inserted ${credCount} credentials`); // --- Sessions (valid for 24h so you can actually log in) --- -const sessionToken = "dev-session-kieran"; -db.query( - "INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)", -).run(sessionToken, userIds[0], now + 86400); -console.log("inserted 1 dev session (token: dev-session-kieran)"); +const sessionToken = "dev-session-tacy"; +const existingSession = db + .query("SELECT id FROM sessions WHERE token = ?") + .get(sessionToken) as { id: number } | undefined; +if (!existingSession) { + db.query( + "INSERT INTO sessions (token, user_id, expires_at) VALUES (?, ?, ?)", + ).run(sessionToken, userIds[0], now + 86400); + console.log("inserted 1 dev session (token: dev-session-tacy)"); +} else { + // Refresh expiry on existing session + db.query("UPDATE sessions SET expires_at = ? WHERE token = ?").run(now + 86400, sessionToken); + console.log("refreshed existing dev session (token: dev-session-tacy)"); +} // --- Apps (OAuth clients) --- const apps = [ @@ -118,7 +147,8 @@ const apps = [ }, { client_id: "https://blog.dunkirk.sh", - redirect_uris: '["https://blog.dunkirk.sh/auth/callback","https://blog.dunkirk.sh/indieauth"]', + redirect_uris: + '["https://blog.dunkirk.sh/auth/callback","https://blog.dunkirk.sh/indieauth"]', name: "Dunkirk Blog", logo_url: null, description: "IndieWeb blog with Micropub support", @@ -162,7 +192,13 @@ const apps = [ }, ]; +let insertedApps = 0; for (const a of apps) { + const existing = db + .query("SELECT id FROM apps WHERE client_id = ?") + .get(a.client_id) as { id: number } | undefined; + if (existing) continue; + db.query( `INSERT INTO apps (client_id, redirect_uris, name, logo_url, description, is_preregistered, client_secret_hash, available_roles, default_role, first_seen, last_used) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, @@ -179,34 +215,111 @@ for (const a of apps) { now - Math.floor(Math.random() * 90 * 86400), now - Math.floor(Math.random() * 7 * 86400), ); + insertedApps++; } -console.log(`inserted ${apps.length} apps`); +console.log(`${insertedApps} new apps inserted`); // --- Permissions (user -> app links) --- const perms = [ // Kieran has access to everything - { user: 0, client: "https://auth.dunkirk.sh", scopes: '["profile","email","openid"]', role: "admin" }, - { user: 0, client: "https://blog.dunkirk.sh", scopes: '["profile","create","update"]', role: null }, - { user: 0, client: "https://infra.dunkirk.sh", scopes: '["profile","email"]', role: "admin" }, - { user: 0, client: "https://webring.dunkirk.sh", scopes: '["profile"]', role: null }, - { user: 0, client: "https://photos.kieranklukas.com", scopes: '["profile","email","upload"]', role: "admin" }, + { + user: 0, + client: "https://auth.dunkirk.sh", + scopes: '["profile","email","openid"]', + role: "admin", + }, + { + user: 0, + client: "https://blog.dunkirk.sh", + scopes: '["profile","create","update"]', + role: null, + }, + { + user: 0, + client: "https://infra.dunkirk.sh", + scopes: '["profile","email"]', + role: "admin", + }, + { + user: 0, + client: "https://webring.dunkirk.sh", + scopes: '["profile"]', + role: null, + }, + { + user: 0, + client: "https://photos.kieranklukas.com", + scopes: '["profile","email","upload"]', + role: "admin", + }, // Alice has access to a few - { user: 1, client: "https://auth.dunkirk.sh", scopes: '["profile","email"]', role: "editor" }, - { user: 1, client: "https://blog.dunkirk.sh", scopes: '["profile","create"]', role: null }, - { user: 1, client: "https://photos.kieranklukas.com", scopes: '["profile","upload"]', role: "editor" }, + { + user: 1, + client: "https://auth.dunkirk.sh", + scopes: '["profile","email"]', + role: "editor", + }, + { + user: 1, + client: "https://blog.dunkirk.sh", + scopes: '["profile","create"]', + role: null, + }, + { + user: 1, + client: "https://photos.kieranklukas.com", + scopes: '["profile","upload"]', + role: "editor", + }, // Bob has access to 2 - { user: 2, client: "https://blog.dunkirk.sh", scopes: '["profile"]', role: null }, - { user: 2, client: "https://infra.dunkirk.sh", scopes: '["profile","email"]', role: "operator" }, + { + user: 2, + client: "https://blog.dunkirk.sh", + scopes: '["profile"]', + role: null, + }, + { + user: 2, + client: "https://infra.dunkirk.sh", + scopes: '["profile","email"]', + role: "operator", + }, // Dana has access to 3 - { user: 4, client: "https://auth.dunkirk.sh", scopes: '["profile","email"]', role: "viewer" }, - { user: 4, client: "https://photos.kieranklukas.com", scopes: '["profile","upload"]', role: "editor" }, - { user: 4, client: "https://webring.dunkirk.sh", scopes: '["profile"]', role: null }, + { + user: 4, + client: "https://auth.dunkirk.sh", + scopes: '["profile","email"]', + role: "viewer", + }, + { + user: 4, + client: "https://photos.kieranklukas.com", + scopes: '["profile","upload"]', + role: "editor", + }, + { + user: 4, + client: "https://webring.dunkirk.sh", + scopes: '["profile"]', + role: null, + }, // Eve has access to 1 - { user: 5, client: "https://blog.dunkirk.sh", scopes: '["profile","email"]', role: null }, + { + user: 5, + client: "https://blog.dunkirk.sh", + scopes: '["profile","email"]', + role: null, + }, ]; +let insertedPerms = 0; for (const p of perms) { const clientId = p.client; + const existing = db + .query("SELECT id FROM permissions WHERE user_id = ? AND client_id = ?") + .get(userIds[p.user], clientId) as { id: number } | undefined; + if (existing) continue; + db.query( `INSERT INTO permissions (user_id, client_id, scopes, role, granted_at, last_used) VALUES (?, ?, ?, ?, ?, ?)`, @@ -218,36 +331,99 @@ for (const p of perms) { now - Math.floor(Math.random() * 60 * 86400), now - Math.floor(Math.random() * 3 * 86400), ); + insertedPerms++; } -console.log(`inserted ${perms.length} permissions`); +console.log(`${insertedPerms} new permissions inserted`); // --- Tokens (active access tokens) --- const tokens = [ { user: 0, client: "https://auth.dunkirk.sh", scope: "profile email openid" }, - { user: 0, client: "https://blog.dunkirk.sh", scope: "profile create update" }, - { user: 1, client: "https://photos.kieranklukas.com", scope: "profile upload" }, + { + user: 0, + client: "https://blog.dunkirk.sh", + scope: "profile create update", + }, + { + user: 1, + client: "https://photos.kieranklukas.com", + scope: "profile upload", + }, { user: 2, client: "https://infra.dunkirk.sh", scope: "profile email" }, ]; +let insertedTokens = 0; for (const t of tokens) { + // Check if this user+client combo already has an active token + const existing = db + .query("SELECT id FROM tokens WHERE user_id = ? AND client_id = ? AND revoked = 0") + .get(userIds[t.user], t.client) as { id: number } | undefined; + if (existing) continue; + const tokStr = `tok_${crypto.randomUUID().replace(/-/g, "")}`; db.query( `INSERT INTO tokens (token, user_id, client_id, scope, created_at, expires_at, revoked) VALUES (?, ?, ?, ?, ?, ?, 0)`, ).run(tokStr, userIds[t.user], t.client, t.scope, now - 3600, now + 86400); + insertedTokens++; } -console.log(`inserted ${tokens.length} tokens`); +console.log(`${insertedTokens} new tokens inserted`); // --- Invites --- const invites = [ - { code: "invite-alpha-001", createdBy: 0, maxUses: 5, currentUses: 2, expiresAt: now + 7 * 86400, note: "general community invite", message: "Welcome to Indiko!" }, - { code: "invite-beta-002", createdBy: 0, maxUses: 1, currentUses: 1, expiresAt: now - 86400, note: "for Bob", message: null }, - { code: "invite-gamma-003", createdBy: 0, maxUses: 10, currentUses: 0, expiresAt: null, note: "open invite for team", message: "Hey, come join us!" }, - { code: "invite-delta-004", createdBy: 1, maxUses: 3, currentUses: 1, expiresAt: now + 30 * 86400, note: null, message: null }, - { code: "invite-epsilon-005", createdBy: 0, maxUses: 1, currentUses: 0, expiresAt: now + 3 * 86400, note: "for the new dev", message: "Welcome aboard!" }, + { + code: "invite-alpha-001", + createdBy: 0, + maxUses: 5, + currentUses: 2, + expiresAt: now + 7 * 86400, + note: "general community invite", + message: "Welcome to Indiko!", + }, + { + code: "invite-beta-002", + createdBy: 0, + maxUses: 1, + currentUses: 1, + expiresAt: now - 86400, + note: "for Bob", + message: null, + }, + { + code: "invite-gamma-003", + createdBy: 0, + maxUses: 10, + currentUses: 0, + expiresAt: null, + note: "open invite for team", + message: "Hey, come join us!", + }, + { + code: "invite-delta-004", + createdBy: 1, + maxUses: 3, + currentUses: 1, + expiresAt: now + 30 * 86400, + note: null, + message: null, + }, + { + code: "invite-epsilon-005", + createdBy: 0, + maxUses: 1, + currentUses: 0, + expiresAt: now + 3 * 86400, + note: "for the new dev", + message: "Welcome aboard!", + }, ]; +let insertedInvites = 0; for (const inv of invites) { + const existing = db + .query("SELECT id FROM invites WHERE code = ?") + .get(inv.code) as { id: number } | undefined; + if (existing) continue; + db.query( `INSERT INTO invites (code, created_by, used, max_uses, current_uses, expires_at, note, message, created_at) VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?)`, @@ -261,29 +437,48 @@ for (const inv of invites) { inv.message, now - Math.floor(Math.random() * 14 * 86400), ); + insertedInvites++; } -console.log(`inserted ${invites.length} invites`); +console.log(`${insertedInvites} new invites inserted`); -// Mark one invite as used by Bob -const bobInvite = db.query("SELECT id FROM invites WHERE code = 'invite-beta-002'").get() as { id: number }; -db.query( - "UPDATE invites SET used = 1, used_by = ?, used_at = ? WHERE id = ?", -).run(userIds[2], now - 86400, bobInvite.id); -db.query( - "INSERT INTO invite_uses (invite_id, user_id, used_at) VALUES (?, ?, ?)", -).run(bobInvite.id, userIds[2], now - 86400); +// Mark one invite as used by Bob (idempotent) +const bobInvite = db + .query("SELECT id FROM invites WHERE code = 'invite-beta-002'") + .get() as { id: number } | undefined; +if (bobInvite) { + const alreadyUsed = db + .query("SELECT id FROM invite_uses WHERE invite_id = ? AND user_id = ?") + .get(bobInvite.id, userIds[2]) as { id: number } | undefined; + if (!alreadyUsed) { + db.query( + "UPDATE invites SET used = 1, used_by = ?, used_at = ? WHERE id = ?", + ).run(userIds[2], now - 86400, bobInvite.id); + db.query( + "INSERT INTO invite_uses (invite_id, user_id, used_at) VALUES (?, ?, ?)", + ).run(bobInvite.id, userIds[2], now - 86400); + } +} -// Mark one as used by Dana -const danaInvite = db.query("SELECT id FROM invites WHERE code = 'invite-delta-004'").get() as { id: number }; -db.query( - "UPDATE invites SET used = 1, used_by = ?, used_at = ? WHERE id = ?", -).run(userIds[4], now - 2 * 86400, danaInvite.id); -db.query( - "INSERT INTO invite_uses (invite_id, user_id, used_at) VALUES (?, ?, ?)", -).run(danaInvite.id, userIds[4], now - 2 * 86400); +// Mark one as used by Dana (idempotent) +const danaInvite = db + .query("SELECT id FROM invites WHERE code = 'invite-delta-004'") + .get() as { id: number } | undefined; +if (danaInvite) { + const alreadyUsed = db + .query("SELECT id FROM invite_uses WHERE invite_id = ? AND user_id = ?") + .get(danaInvite.id, userIds[4]) as { id: number } | undefined; + if (!alreadyUsed) { + db.query( + "UPDATE invites SET used = 1, used_by = ?, used_at = ? WHERE id = ?", + ).run(userIds[4], now - 2 * 86400, danaInvite.id); + db.query( + "INSERT INTO invite_uses (invite_id, user_id, used_at) VALUES (?, ?, ?)", + ).run(danaInvite.id, userIds[4], now - 2 * 86400); + } +} console.log("marked 2 invites as used"); console.log("\nseed complete!"); -console.log("login token: dev-session-kieran"); -console.log("admin user: kieran"); +console.log("login token: dev-session-tacy"); +console.log("admin user: tacy"); diff --git a/src/client/admin-clients.ts b/src/client/admin-clients.ts index 5921af3..b764c79 100644 --- a/src/client/admin-clients.ts +++ b/src/client/admin-clients.ts @@ -145,15 +145,12 @@ function displayClients(clients: Client[]) {
${client.name}
${client.clientId}
${client.description ? `
${client.description}
` : ""} -
- - ${client.isPreregistered ? "pre-registered" : "auto-registered"} - - first seen ${firstSeenDate} - last used ${lastUsedDate} -
+
first seen ${firstSeenDate} • last used ${lastUsedDate}
-
+
+ + ${client.isPreregistered ? "pre-registered" : "auto-registered"} + ${ client.isPreregistered ? ` @@ -255,16 +252,16 @@ window.toggleClient = async (clientId: string) => { return `
diff --git a/src/client/admin-invites.ts b/src/client/admin-invites.ts index 01077ea..dac8601 100644 --- a/src/client/admin-invites.ts +++ b/src/client/admin-invites.ts @@ -316,40 +316,68 @@ async function loadInvites() { const roleInfo = invite.appRoles.length > 0 - ? `
App roles: ${invite.appRoles - .map((r) => { - const appName = r.name || r.clientId; - return `${appName} (${r.role})`; - }) - .join(", ")}
` + ? `
+
app roles
+
${invite.appRoles + .map((r) => { + const appName = r.name || r.clientId; + return `${appName} • ${r.role}`; + }) + .join("")}
+
` : ""; const usedByInfo = invite.usedBy.length > 0 - ? `
Used by: ${invite.usedBy.map((u) => `${u.username} (${new Date(u.usedAt * 1000).toLocaleDateString()})`).join(", ")}
` + ? `
+
used by
+
${invite.usedBy.map((u) => `${u.username} (${new Date(u.usedAt * 1000).toLocaleDateString()})`).join(", ")}
+
` : ""; const noteInfo = invite.note - ? `
Internal note: ${invite.note}
` + ? `
Internal note: ${invite.note}
` : ""; const messageInfo = invite.message - ? `
Message to invitees: ${invite.message}
` + ? `
Message to invitees: ${invite.message}
` : ""; const isActive = !invite.isExpired && !invite.isFullyUsed; + const statusBadgeClass = isActive ? "badge-active" : "badge-inactive"; + const statusText = isActive ? "active" : status; return `
-
-
${invite.code}
-
Created by ${invite.createdBy} on ${createdDate} • ${status}
-
${expiryInfo}
- ${noteInfo} - ${messageInfo} - ${roleInfo} - ${usedByInfo} -
${invite.inviteUrl}
+
+ ${invite.code} +
+ ${statusText} +
+
+
+
created by
+
${invite.createdBy}
+
+
+
created
+
${createdDate}
+
+
+
expires
+
${expiryInfo}
+
+
+
link
+
${invite.inviteUrl}
+
+
+
+ ${noteInfo} + ${messageInfo} + ${roleInfo} + ${usedByInfo} +
diff --git a/src/html/admin-clients.html b/src/html/admin-clients.html index 8500c6c..2f2e0aa 100644 --- a/src/html/admin-clients.html +++ b/src/html/admin-clients.html @@ -25,147 +25,16 @@ @@ -595,9 +449,8 @@
-
-

oauth clients

- +
+
loading clients...
diff --git a/src/html/admin-invites.html b/src/html/admin-invites.html index b97ff87..b62353e 100644 --- a/src/html/admin-invites.html +++ b/src/html/admin-invites.html @@ -25,146 +25,163 @@ diff --git a/src/html/index.html b/src/html/index.html index ed1c05f..c769e19 100644 --- a/src/html/index.html +++ b/src/html/index.html @@ -42,13 +42,13 @@ } h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; + font-size: var(--text-2xl); + margin-bottom: var(--space-1); } .subtitle { color: var(--paper-dim); - font-size: 1.125rem; + font-size: var(--text-md); font-weight: 300; } diff --git a/src/lib/secrets.ts b/src/lib/secrets.ts index 54e90c2..7277142 100644 --- a/src/lib/secrets.ts +++ b/src/lib/secrets.ts @@ -8,11 +8,11 @@ export function hashSecret(secret: string): string { // Compare a presented secret against a stored sha256 hex digest in constant // time. String equality short-circuits on the first differing byte, which // leaks how much of the secret is correct; compare the raw digests instead. -export function verifySecret(presented: string, storedHashHex: string): boolean { - const presentedHash = crypto - .createHash("sha256") - .update(presented) - .digest(); +export function verifySecret( + presented: string, + storedHashHex: string, +): boolean { + const presentedHash = crypto.createHash("sha256").update(presented).digest(); const storedHash = Buffer.from(storedHashHex, "hex"); diff --git a/src/routes/oauth/token.ts b/src/routes/oauth/token.ts index 7a1f5f2..96570f9 100644 --- a/src/routes/oauth/token.ts +++ b/src/routes/oauth/token.ts @@ -127,9 +127,7 @@ async function handleRefreshTokenGrant( const refreshExpiresAt = now + REFRESH_TOKEN_TTL; const family = tokenData.family ?? crypto.randomUUID(); - db.query( - "UPDATE tokens SET rotated = 1 WHERE id = ?", - ).run(tokenData.id); + db.query("UPDATE tokens SET rotated = 1 WHERE id = ?").run(tokenData.id); db.query( "INSERT INTO tokens (token, user_id, client_id, scope, expires_at, refresh_token, refresh_expires_at, family) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", diff --git a/src/routes/passkeys.ts b/src/routes/passkeys.ts index dfd9b42..e43d3b1 100644 --- a/src/routes/passkeys.ts +++ b/src/routes/passkeys.ts @@ -97,7 +97,7 @@ export async function addPasskeyOptions(req: Request): Promise { // Store challenge const expiresAt = Math.floor(Date.now() / 1000) + 300; // 5 minutes db.query( - "INSERT INTO challenges (challenge, username, type, expires_at) VALUES (?, ?, 'passkey_add', ?)", + "INSERT INTO challenges (challenge, username, type, expires_at) VALUES (?, ?, 'registration', ?)", ).run(options.challenge, user.username, expiresAt); return Response.json(options); @@ -150,7 +150,7 @@ export async function addPasskeyVerify(req: Request): Promise { // Verify challenge exists and is valid const challenge = db .query( - "SELECT challenge, expires_at FROM challenges WHERE challenge = ? AND username = ? AND type = 'passkey_add'", + "SELECT challenge, expires_at FROM challenges WHERE challenge = ? AND username = ? AND type = 'registration'", ) .get(expectedChallenge, user.username) as | { challenge: string; expires_at: number } diff --git a/test/token.test.ts b/test/token.test.ts index 5f2ec3a..608a223 100644 --- a/test/token.test.ts +++ b/test/token.test.ts @@ -145,7 +145,9 @@ describe("token endpoint: authorization_code grant", () => { test("happy path: issues access + refresh tokens, marks code used", async () => { const userId = createUser({ username: "kieran" }); seedApp(); - const code = seedAuthCode(userId, { scopes: ["profile", "offline_access"] }); + const code = seedAuthCode(userId, { + scopes: ["profile", "offline_access"], + }); const res = await token(tokenReq(exchangeBody(code))); expect(res.status).toBe(200); @@ -203,7 +205,9 @@ describe("token endpoint: authorization_code grant", () => { test("refresh token issued when offline_access scope requested", async () => { const userId = createUser({}); seedApp(); - const code = seedAuthCode(userId, { scopes: ["profile", "offline_access"] }); + const code = seedAuthCode(userId, { + scopes: ["profile", "offline_access"], + }); const res = await token(tokenReq(exchangeBody(code))); expect(res.status).toBe(200); @@ -216,7 +220,9 @@ describe("token endpoint: authorization_code grant", () => { describe("token endpoint: refresh_token grant", () => { async function issueTokens(userId: number) { seedApp(); - const code = seedAuthCode(userId, { scopes: ["profile", "offline_access"] }); + const code = seedAuthCode(userId, { + scopes: ["profile", "offline_access"], + }); const res = await token(tokenReq(exchangeBody(code))); return (await res.json()) as { access_token: string; @@ -275,7 +281,9 @@ describe("token endpoint: refresh_token grant", () => { describe("token endpoint: refresh family detection (RFC 9700)", () => { async function issueTokens(userId: number) { seedApp(); - const code = seedAuthCode(userId, { scopes: ["profile", "offline_access"] }); + const code = seedAuthCode(userId, { + scopes: ["profile", "offline_access"], + }); const res = await token(tokenReq(exchangeBody(code))); return (await res.json()) as { access_token: string; @@ -340,9 +348,7 @@ describe("token endpoint: refresh family detection (RFC 9700)", () => { expect(afterRevoke.status).toBe(400); const rows = db - .query( - "SELECT revoked FROM tokens WHERE client_id = ? AND revoked = 1", - ) + .query("SELECT revoked FROM tokens WHERE client_id = ? AND revoked = 1") .all(CLIENT_ID) as Array<{ revoked: number }>; expect(rows.length).toBeGreaterThan(0); });