/** * Authentication integration for postgres.do * * Provides authentication helpers using oauth.do for both * client-side (browser) and server-side (Node.js/Cloudflare Workers) usage. * * The DO-based architecture means each authenticated user can have their own * database, simplifying security by eliminating row-level access control. */ // Re-export from oauth.do based on environment // Users should import from the appropriate subpath for their environment: // - Server: import { ensureLoggedIn } from 'oauth.do/node' // - Browser: import { ensureLoggedIn } from 'oauth.do/browser' import type { Sql, PostgresConfig } from './types' import { createClient } from './client' import { // Re-export shared auth types and utilities type AuthenticatedUser, type AuthTokenValidationResult as TokenValidationResult, extractBearerToken, generateDatabaseId, validateToken as sharedValidateToken, DEFAULT_OAUTH_URL, } from '@dotdo/postgres-shared' // Re-export for backwards compatibility export type { AuthenticatedUser, TokenValidationResult } export { extractBearerToken, generateDatabaseId } export interface AuthenticatedClientOptions extends PostgresConfig { /** OAuth API URL (default: https://oauth.do) */ oauthUrl?: string } export interface AuthenticatedContext { user: AuthenticatedUser token: string } /** * Create an authenticated postgres client for server-side usage * * This function verifies the user's authentication token and returns * a postgres client configured for their specific database. * * @example * ```typescript * import { createAuthenticatedClient } from 'postgres.do' * import { ensureLoggedIn } from 'oauth.do/node' * * export default { * async fetch(request: Request) { * // Verify authentication * const auth = await ensureLoggedIn(request) * * // Create client for this user's database * const sql = createAuthenticatedClient(auth.token, { * url: `https://db.postgres.do/${auth.user.id}` * }) * * const users = await sql`SELECT * FROM users` * return Response.json(users) * } * } * ``` */ export function createAuthenticatedClient( token: string, options: AuthenticatedClientOptions = {} ): Sql { return createClient({ ...options, apiKey: token, }) } /** * Create a postgres client from an authenticated request (server-side) * * This is a convenience function that extracts the Bearer token from * the request Authorization header. * * @example * ```typescript * import { createClientFromRequest } from 'postgres.do' * * export default { * async fetch(request: Request) { * const sql = createClientFromRequest(request, { * url: 'https://db.postgres.do/mydb' * }) * * const users = await sql`SELECT * FROM users` * return Response.json(users) * } * } * ``` */ export function createClientFromRequest( request: Request, options: AuthenticatedClientOptions = {} ): Sql { const authHeader = request.headers.get('Authorization') const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : undefined const clientOptions = { ...options } if (token !== undefined) { clientOptions.apiKey = token } return createClient(clientOptions) } /** * Higher-order function to wrap a request handler with authentication * * @example * ```typescript * import { withAuth } from 'postgres.do' * * export default { * fetch: withAuth(async (request, { user, sql }) => { * // user is guaranteed to be authenticated * // sql is configured for this user's database * const data = await sql`SELECT * FROM my_table` * return Response.json({ user, data }) * }) * } * ``` */ export function withAuth( handler: ( request: Request, context: { user: AuthenticatedUser; sql: Sql; token: string } ) => Promise, options: AuthenticatedClientOptions & { /** Custom function to verify and extract user from token */ verifyToken?: (token: string) => Promise /** URL to get database URL for user (default: uses user.id) */ getDatabaseUrl?: (user: AuthenticatedUser) => string /** Custom unauthorized response */ onUnauthorized?: (request: Request) => Response } = {} ): (request: Request) => Promise { return async (request: Request): Promise => { // Extract token from Authorization header const authHeader = request.headers.get('Authorization') const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null if (!token) { if (options.onUnauthorized) { return options.onUnauthorized(request) } return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }) } // Verify token and get user let user: AuthenticatedUser | null = null if (options.verifyToken) { user = await options.verifyToken(token) } else { // Default: verify with oauth.do using shared validateToken const oauthUrl = options.oauthUrl || DEFAULT_OAUTH_URL const result = await sharedValidateToken(token, { oauthUrl }) if (result.valid && result.user) { user = result.user } } if (!user) { if (options.onUnauthorized) { return options.onUnauthorized(request) } return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }) } // Get database URL for this user const dbUrl = options.getDatabaseUrl ? options.getDatabaseUrl(user) : `https://db.postgres.do/${user.id}` // Create authenticated SQL client const sql = createClient({ ...options, url: dbUrl, apiKey: token, }) try { return await handler(request, { user, sql, token }) } finally { // Clean up connection await sql.end() } } } /** * Extract user ID from a request's authentication token * Useful when you need just the user ID without creating a client */ export async function extractUserId( request: Request, options: { oauthUrl?: string } = {} ): Promise { const token = extractBearerToken(request) if (!token) { return null } const oauthUrl = options.oauthUrl || DEFAULT_OAUTH_URL const result = await sharedValidateToken(token, { oauthUrl }) if (result.valid && result.user) { return result.user.id } return null } /** * Validate a token and return the full result */ export async function validateToken( token: string, options: { oauthUrl?: string } = {} ): Promise { const oauthUrl = options.oauthUrl || DEFAULT_OAUTH_URL return sharedValidateToken(token, { oauthUrl }) } /** * Create a user-scoped database client * The database URL is derived from the user's ID * * @example * ```typescript * import { createUserScopedClient } from 'postgres.do' * * // After validating the user * const sql = createUserScopedClient(user.id, token) * * const data = await sql`SELECT * FROM my_table` * ``` */ export function createUserScopedClient( userId: string, token: string, options: AuthenticatedClientOptions = {} ): Sql { // Generate user-scoped database URL using shared generateDatabaseId const baseUrl = options.url || 'https://db.postgres.do' const userDbUrl = `${baseUrl}/${generateDatabaseId(userId)}` return createClient({ ...options, url: userDbUrl, apiKey: token, }) }