···6666 // Now try to claim it with testDid1 - should fail
6767 try {
6868 await claimCustomDomain(testDid1, testDomain, hash3)
6969- expect.fail('Should have thrown an error when trying to claim a verified domain')
6969+ expect('Should have thrown an error when trying to claim a verified domain').fail()
7070 } catch (err) {
7171- expect(err.message).toBe('conflict')
7171+ expect((err as Error).message).toBe('conflict')
7272 }
73737474 // Verify the domain is still owned by testDid2 and verified
+1-242
src/lib/db.ts
···11-import { NodeOAuthClient, type ClientMetadata } from "@atproto/oauth-client-node";
21import { SQL } from "bun";
33-import { JoseKey } from "@atproto/jwk-jose";
42import { BASE_HOST } from "./constants";
5364export const db = new SQL(
···221219 return rows[0] ?? null;
222220};
223221224224-export const getAllWispDomains = async (did: string) => {
222222+export const getAllWispDomains = async (did: string): Promise<Array<{ domain: string; rkey: string | null }>> => {
225223 const rows = await db`SELECT domain, rkey FROM domains WHERE did = ${did} ORDER BY created_at ASC`;
226224 return rows;
227225};
···338336339337export const deleteWispDomain = async (domain: string): Promise<void> => {
340338 await db`DELETE FROM domains WHERE domain = ${domain}`;
341341-};
342342-343343-// Session timeout configuration (30 days in seconds)
344344-const SESSION_TIMEOUT = 30 * 24 * 60 * 60; // 2592000 seconds
345345-// OAuth state timeout (1 hour in seconds)
346346-const STATE_TIMEOUT = 60 * 60; // 3600 seconds
347347-348348-const stateStore = {
349349- async set(key: string, data: any) {
350350- const expiresAt = Math.floor(Date.now() / 1000) + STATE_TIMEOUT;
351351- await db`
352352- INSERT INTO oauth_states (key, data, created_at, expires_at)
353353- VALUES (${key}, ${JSON.stringify(data)}, EXTRACT(EPOCH FROM NOW()), ${expiresAt})
354354- ON CONFLICT (key) DO UPDATE SET data = EXCLUDED.data, expires_at = ${expiresAt}
355355- `;
356356- },
357357- async get(key: string) {
358358- const now = Math.floor(Date.now() / 1000);
359359- const result = await db`
360360- SELECT data, expires_at
361361- FROM oauth_states
362362- WHERE key = ${key}
363363- `;
364364- if (!result[0]) return undefined;
365365-366366- // Check if expired
367367- const expiresAt = Number(result[0].expires_at);
368368- if (expiresAt && now > expiresAt) {
369369- await db`DELETE FROM oauth_states WHERE key = ${key}`;
370370- return undefined;
371371- }
372372-373373- return JSON.parse(result[0].data);
374374- },
375375- async del(key: string) {
376376- await db`DELETE FROM oauth_states WHERE key = ${key}`;
377377- }
378378-};
379379-380380-const sessionStore = {
381381- async set(sub: string, data: any) {
382382- const expiresAt = Math.floor(Date.now() / 1000) + SESSION_TIMEOUT;
383383- await db`
384384- INSERT INTO oauth_sessions (sub, data, updated_at, expires_at)
385385- VALUES (${sub}, ${JSON.stringify(data)}, EXTRACT(EPOCH FROM NOW()), ${expiresAt})
386386- ON CONFLICT (sub) DO UPDATE SET
387387- data = EXCLUDED.data,
388388- updated_at = EXTRACT(EPOCH FROM NOW()),
389389- expires_at = ${expiresAt}
390390- `;
391391- },
392392- async get(sub: string) {
393393- const now = Math.floor(Date.now() / 1000);
394394- const result = await db`
395395- SELECT data, expires_at
396396- FROM oauth_sessions
397397- WHERE sub = ${sub}
398398- `;
399399- if (!result[0]) return undefined;
400400-401401- // Check if expired
402402- const expiresAt = Number(result[0].expires_at);
403403- if (expiresAt && now > expiresAt) {
404404- console.log('[sessionStore] Session expired, deleting', sub);
405405- await db`DELETE FROM oauth_sessions WHERE sub = ${sub}`;
406406- return undefined;
407407- }
408408-409409- return JSON.parse(result[0].data);
410410- },
411411- async del(sub: string) {
412412- await db`DELETE FROM oauth_sessions WHERE sub = ${sub}`;
413413- }
414414-};
415415-416416-export { sessionStore };
417417-418418-// Cleanup expired sessions and states
419419-export const cleanupExpiredSessions = async () => {
420420- const now = Math.floor(Date.now() / 1000);
421421- try {
422422- const sessionsDeleted = await db`
423423- DELETE FROM oauth_sessions WHERE expires_at < ${now}
424424- `;
425425- const statesDeleted = await db`
426426- DELETE FROM oauth_states WHERE expires_at IS NOT NULL AND expires_at < ${now}
427427- `;
428428- console.log(`[Cleanup] Deleted ${sessionsDeleted.length} expired sessions and ${statesDeleted.length} expired states`);
429429- return { sessions: sessionsDeleted.length, states: statesDeleted.length };
430430- } catch (err) {
431431- console.error('[Cleanup] Failed to cleanup expired data:', err);
432432- return { sessions: 0, states: 0 };
433433- }
434434-};
435435-436436-export const createClientMetadata = (config: { domain: `http://${string}` | `https://${string}`, clientName: string }): ClientMetadata => {
437437- const isLocalDev = process.env.LOCAL_DEV === 'true';
438438-439439- if (isLocalDev) {
440440- // Loopback client for local development
441441- // For loopback, scopes and redirect_uri must be in client_id query string
442442- const redirectUri = 'http://127.0.0.1:8000/api/auth/callback';
443443- const scope = 'atproto transition:generic';
444444- const params = new URLSearchParams();
445445- params.append('redirect_uri', redirectUri);
446446- params.append('scope', scope);
447447-448448- return {
449449- client_id: `http://localhost?${params.toString()}`,
450450- client_name: config.clientName,
451451- client_uri: config.domain,
452452- redirect_uris: [redirectUri],
453453- grant_types: ['authorization_code', 'refresh_token'],
454454- response_types: ['code'],
455455- application_type: 'web',
456456- token_endpoint_auth_method: 'none',
457457- scope: scope,
458458- dpop_bound_access_tokens: false,
459459- subject_type: 'public'
460460- };
461461- }
462462-463463- // Production client with private_key_jwt
464464- return {
465465- client_id: `${config.domain}/client-metadata.json`,
466466- client_name: config.clientName,
467467- client_uri: config.domain,
468468- logo_uri: `${config.domain}/logo.png`,
469469- tos_uri: `${config.domain}/tos`,
470470- policy_uri: `${config.domain}/policy`,
471471- redirect_uris: [`${config.domain}/api/auth/callback`],
472472- grant_types: ['authorization_code', 'refresh_token'],
473473- response_types: ['code'],
474474- application_type: 'web',
475475- token_endpoint_auth_method: 'private_key_jwt',
476476- token_endpoint_auth_signing_alg: "ES256",
477477- scope: "atproto transition:generic",
478478- dpop_bound_access_tokens: true,
479479- jwks_uri: `${config.domain}/jwks.json`,
480480- subject_type: 'public',
481481- authorization_signed_response_alg: 'ES256'
482482- };
483483-};
484484-485485-const persistKey = async (key: JoseKey) => {
486486- const priv = key.privateJwk;
487487- if (!priv) return;
488488- const kid = key.kid ?? crypto.randomUUID();
489489- await db`
490490- INSERT INTO oauth_keys (kid, jwk, created_at)
491491- VALUES (${kid}, ${JSON.stringify(priv)}, EXTRACT(EPOCH FROM NOW()))
492492- ON CONFLICT (kid) DO UPDATE SET jwk = EXCLUDED.jwk
493493- `;
494494-};
495495-496496-const loadPersistedKeys = async (): Promise<JoseKey[]> => {
497497- const rows = await db`SELECT kid, jwk, created_at FROM oauth_keys ORDER BY kid`;
498498- const keys: JoseKey[] = [];
499499- for (const row of rows) {
500500- try {
501501- const obj = JSON.parse(row.jwk);
502502- const key = await JoseKey.fromImportable(obj as any, (obj as any).kid);
503503- keys.push(key);
504504- } catch (err) {
505505- console.error('Could not parse stored JWK', err);
506506- }
507507- }
508508- return keys;
509509-};
510510-511511-const ensureKeys = async (): Promise<JoseKey[]> => {
512512- let keys = await loadPersistedKeys();
513513- const needed: string[] = [];
514514- for (let i = 1; i <= 3; i++) {
515515- const kid = `key${i}`;
516516- if (!keys.some(k => k.kid === kid)) needed.push(kid);
517517- }
518518- for (const kid of needed) {
519519- const newKey = await JoseKey.generate(['ES256'], kid);
520520- await persistKey(newKey);
521521- keys.push(newKey);
522522- }
523523- keys.sort((a, b) => (a.kid ?? '').localeCompare(b.kid ?? ''));
524524- return keys;
525525-};
526526-527527-// Load keys from database every time (stateless - safe for horizontal scaling)
528528-export const getCurrentKeys = async (): Promise<JoseKey[]> => {
529529- return await loadPersistedKeys();
530530-};
531531-532532-// Key rotation - rotate keys older than 30 days (monthly rotation)
533533-const KEY_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
534534-535535-export const rotateKeysIfNeeded = async (): Promise<boolean> => {
536536- const now = Math.floor(Date.now() / 1000);
537537- const cutoffTime = now - KEY_MAX_AGE;
538538-539539- try {
540540- // Find keys older than 30 days
541541- const oldKeys = await db`
542542- SELECT kid, created_at FROM oauth_keys
543543- WHERE created_at IS NOT NULL AND created_at < ${cutoffTime}
544544- ORDER BY created_at ASC
545545- `;
546546-547547- if (oldKeys.length === 0) {
548548- console.log('[KeyRotation] No keys need rotation');
549549- return false;
550550- }
551551-552552- console.log(`[KeyRotation] Found ${oldKeys.length} key(s) older than 30 days, rotating oldest key`);
553553-554554- // Rotate the oldest key
555555- const oldestKey = oldKeys[0];
556556- const oldKid = oldestKey.kid;
557557-558558- // Generate new key with same kid
559559- const newKey = await JoseKey.generate(['ES256'], oldKid);
560560- await persistKey(newKey);
561561-562562- console.log(`[KeyRotation] Rotated key ${oldKid}`);
563563-564564- return true;
565565- } catch (err) {
566566- console.error('[KeyRotation] Failed to rotate keys:', err);
567567- return false;
568568- }
569569-};
570570-571571-export const getOAuthClient = async (config: { domain: `http://${string}` | `https://${string}`, clientName: string }) => {
572572- const keys = await ensureKeys();
573573-574574- return new NodeOAuthClient({
575575- clientMetadata: createClientMetadata(config),
576576- keyset: keys,
577577- stateStore,
578578- sessionStore
579579- });
580339};
581340582341export const getCustomDomainsByDid = async (did: string) => {
···5151 })
52525353 // Sync sites from PDS to database cache
5454- logger.debug('[Auth] Syncing sites from PDS for', session.did)
5454+ logger.debug('[Auth] Syncing sites from PDS for', session.did as any)
5555 try {
5656 const syncResult = await syncSitesFromPDS(session.did, session)
5757 logger.debug(`[Auth] Sync complete: ${syncResult.synced} sites synced`)
···9292 if (did && typeof did === 'string') {
9393 try {
9494 await client.revoke(did)
9595- logger.debug('[Auth] Revoked OAuth session for', did)
9595+ logger.debug('[Auth] Revoked OAuth session for', did as any)
9696 } catch (err) {
9797 logger.error('[Auth] Failed to revoke session', err)
9898 // Continue with logout even if revoke fails
+1-1
src/routes/domain.ts
···362362 });
363363 } catch (err) {
364364 // Record might not exist in PDS, continue anyway
365365- logger.warn('[Domain] Could not delete wisp domain from PDS', err);
365365+ logger.warn('[Domain] Could not delete wisp domain from PDS', err as any);
366366 }
367367368368 return { success: true };