···
1
-
import { NodeOAuthClient, type ClientMetadata } from "@atproto/oauth-client-node";
import { SQL } from "bun";
3
-
import { JoseKey } from "@atproto/jwk-jose";
import { BASE_HOST } from "./constants";
export const db = new SQL(
···
224
-
export const getAllWispDomains = async (did: string) => {
222
+
export const getAllWispDomains = async (did: string): Promise<Array<{ domain: string; rkey: string | null }>> => {
const rows = await db`SELECT domain, rkey FROM domains WHERE did = ${did} ORDER BY created_at ASC`;
···
export const deleteWispDomain = async (domain: string): Promise<void> => {
await db`DELETE FROM domains WHERE domain = ${domain}`;
343
-
// Session timeout configuration (30 days in seconds)
344
-
const SESSION_TIMEOUT = 30 * 24 * 60 * 60; // 2592000 seconds
345
-
// OAuth state timeout (1 hour in seconds)
346
-
const STATE_TIMEOUT = 60 * 60; // 3600 seconds
348
-
const stateStore = {
349
-
async set(key: string, data: any) {
350
-
const expiresAt = Math.floor(Date.now() / 1000) + STATE_TIMEOUT;
352
-
INSERT INTO oauth_states (key, data, created_at, expires_at)
353
-
VALUES (${key}, ${JSON.stringify(data)}, EXTRACT(EPOCH FROM NOW()), ${expiresAt})
354
-
ON CONFLICT (key) DO UPDATE SET data = EXCLUDED.data, expires_at = ${expiresAt}
357
-
async get(key: string) {
358
-
const now = Math.floor(Date.now() / 1000);
359
-
const result = await db`
360
-
SELECT data, expires_at
364
-
if (!result[0]) return undefined;
366
-
// Check if expired
367
-
const expiresAt = Number(result[0].expires_at);
368
-
if (expiresAt && now > expiresAt) {
369
-
await db`DELETE FROM oauth_states WHERE key = ${key}`;
373
-
return JSON.parse(result[0].data);
375
-
async del(key: string) {
376
-
await db`DELETE FROM oauth_states WHERE key = ${key}`;
380
-
const sessionStore = {
381
-
async set(sub: string, data: any) {
382
-
const expiresAt = Math.floor(Date.now() / 1000) + SESSION_TIMEOUT;
384
-
INSERT INTO oauth_sessions (sub, data, updated_at, expires_at)
385
-
VALUES (${sub}, ${JSON.stringify(data)}, EXTRACT(EPOCH FROM NOW()), ${expiresAt})
386
-
ON CONFLICT (sub) DO UPDATE SET
387
-
data = EXCLUDED.data,
388
-
updated_at = EXTRACT(EPOCH FROM NOW()),
389
-
expires_at = ${expiresAt}
392
-
async get(sub: string) {
393
-
const now = Math.floor(Date.now() / 1000);
394
-
const result = await db`
395
-
SELECT data, expires_at
396
-
FROM oauth_sessions
399
-
if (!result[0]) return undefined;
401
-
// Check if expired
402
-
const expiresAt = Number(result[0].expires_at);
403
-
if (expiresAt && now > expiresAt) {
404
-
console.log('[sessionStore] Session expired, deleting', sub);
405
-
await db`DELETE FROM oauth_sessions WHERE sub = ${sub}`;
409
-
return JSON.parse(result[0].data);
411
-
async del(sub: string) {
412
-
await db`DELETE FROM oauth_sessions WHERE sub = ${sub}`;
416
-
export { sessionStore };
418
-
// Cleanup expired sessions and states
419
-
export const cleanupExpiredSessions = async () => {
420
-
const now = Math.floor(Date.now() / 1000);
422
-
const sessionsDeleted = await db`
423
-
DELETE FROM oauth_sessions WHERE expires_at < ${now}
425
-
const statesDeleted = await db`
426
-
DELETE FROM oauth_states WHERE expires_at IS NOT NULL AND expires_at < ${now}
428
-
console.log(`[Cleanup] Deleted ${sessionsDeleted.length} expired sessions and ${statesDeleted.length} expired states`);
429
-
return { sessions: sessionsDeleted.length, states: statesDeleted.length };
431
-
console.error('[Cleanup] Failed to cleanup expired data:', err);
432
-
return { sessions: 0, states: 0 };
436
-
export const createClientMetadata = (config: { domain: `http://${string}` | `https://${string}`, clientName: string }): ClientMetadata => {
437
-
const isLocalDev = process.env.LOCAL_DEV === 'true';
440
-
// Loopback client for local development
441
-
// For loopback, scopes and redirect_uri must be in client_id query string
442
-
const redirectUri = 'http://127.0.0.1:8000/api/auth/callback';
443
-
const scope = 'atproto transition:generic';
444
-
const params = new URLSearchParams();
445
-
params.append('redirect_uri', redirectUri);
446
-
params.append('scope', scope);
449
-
client_id: `http://localhost?${params.toString()}`,
450
-
client_name: config.clientName,
451
-
client_uri: config.domain,
452
-
redirect_uris: [redirectUri],
453
-
grant_types: ['authorization_code', 'refresh_token'],
454
-
response_types: ['code'],
455
-
application_type: 'web',
456
-
token_endpoint_auth_method: 'none',
458
-
dpop_bound_access_tokens: false,
459
-
subject_type: 'public'
463
-
// Production client with private_key_jwt
465
-
client_id: `${config.domain}/client-metadata.json`,
466
-
client_name: config.clientName,
467
-
client_uri: config.domain,
468
-
logo_uri: `${config.domain}/logo.png`,
469
-
tos_uri: `${config.domain}/tos`,
470
-
policy_uri: `${config.domain}/policy`,
471
-
redirect_uris: [`${config.domain}/api/auth/callback`],
472
-
grant_types: ['authorization_code', 'refresh_token'],
473
-
response_types: ['code'],
474
-
application_type: 'web',
475
-
token_endpoint_auth_method: 'private_key_jwt',
476
-
token_endpoint_auth_signing_alg: "ES256",
477
-
scope: "atproto transition:generic",
478
-
dpop_bound_access_tokens: true,
479
-
jwks_uri: `${config.domain}/jwks.json`,
480
-
subject_type: 'public',
481
-
authorization_signed_response_alg: 'ES256'
485
-
const persistKey = async (key: JoseKey) => {
486
-
const priv = key.privateJwk;
488
-
const kid = key.kid ?? crypto.randomUUID();
490
-
INSERT INTO oauth_keys (kid, jwk, created_at)
491
-
VALUES (${kid}, ${JSON.stringify(priv)}, EXTRACT(EPOCH FROM NOW()))
492
-
ON CONFLICT (kid) DO UPDATE SET jwk = EXCLUDED.jwk
496
-
const loadPersistedKeys = async (): Promise<JoseKey[]> => {
497
-
const rows = await db`SELECT kid, jwk, created_at FROM oauth_keys ORDER BY kid`;
498
-
const keys: JoseKey[] = [];
499
-
for (const row of rows) {
501
-
const obj = JSON.parse(row.jwk);
502
-
const key = await JoseKey.fromImportable(obj as any, (obj as any).kid);
505
-
console.error('Could not parse stored JWK', err);
511
-
const ensureKeys = async (): Promise<JoseKey[]> => {
512
-
let keys = await loadPersistedKeys();
513
-
const needed: string[] = [];
514
-
for (let i = 1; i <= 3; i++) {
515
-
const kid = `key${i}`;
516
-
if (!keys.some(k => k.kid === kid)) needed.push(kid);
518
-
for (const kid of needed) {
519
-
const newKey = await JoseKey.generate(['ES256'], kid);
520
-
await persistKey(newKey);
523
-
keys.sort((a, b) => (a.kid ?? '').localeCompare(b.kid ?? ''));
527
-
// Load keys from database every time (stateless - safe for horizontal scaling)
528
-
export const getCurrentKeys = async (): Promise<JoseKey[]> => {
529
-
return await loadPersistedKeys();
532
-
// Key rotation - rotate keys older than 30 days (monthly rotation)
533
-
const KEY_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
535
-
export const rotateKeysIfNeeded = async (): Promise<boolean> => {
536
-
const now = Math.floor(Date.now() / 1000);
537
-
const cutoffTime = now - KEY_MAX_AGE;
540
-
// Find keys older than 30 days
541
-
const oldKeys = await db`
542
-
SELECT kid, created_at FROM oauth_keys
543
-
WHERE created_at IS NOT NULL AND created_at < ${cutoffTime}
544
-
ORDER BY created_at ASC
547
-
if (oldKeys.length === 0) {
548
-
console.log('[KeyRotation] No keys need rotation');
552
-
console.log(`[KeyRotation] Found ${oldKeys.length} key(s) older than 30 days, rotating oldest key`);
554
-
// Rotate the oldest key
555
-
const oldestKey = oldKeys[0];
556
-
const oldKid = oldestKey.kid;
558
-
// Generate new key with same kid
559
-
const newKey = await JoseKey.generate(['ES256'], oldKid);
560
-
await persistKey(newKey);
562
-
console.log(`[KeyRotation] Rotated key ${oldKid}`);
566
-
console.error('[KeyRotation] Failed to rotate keys:', err);
571
-
export const getOAuthClient = async (config: { domain: `http://${string}` | `https://${string}`, clientName: string }) => {
572
-
const keys = await ensureKeys();
574
-
return new NodeOAuthClient({
575
-
clientMetadata: createClientMetadata(config),
export const getCustomDomainsByDid = async (did: string) => {