import { getImpersonationSQL, type SafeSqlFragment } from '@supabase/pg-meta' import { uuidv4 } from './helpers' import type { User } from '@/data/auth/users-infinite-query' import { RoleImpersonationState as ValtioRoleImpersonationState } from '@/state/role-impersonation-state' type PostgrestImpersonationRole = | { type: 'postgrest' role: 'anon' } | { type: 'postgrest' role: 'service_role' } | { type: 'postgrest' role: 'authenticated' userType: 'native' user?: User aal?: 'aal1' | 'aal2' } | { type: 'postgrest' role: 'authenticated' userType: 'external' externalAuth?: { sub: string additionalClaims?: Record } aal?: 'aal1' | 'aal2' } export type PostgrestRole = PostgrestImpersonationRole['role'] type CustomImpersonationRole = { type: 'custom' role: string } export type ImpersonationRole = PostgrestImpersonationRole | CustomImpersonationRole export function getExp1HourFromNow() { return Math.floor((Date.now() + 60 * 60 * 1000) / 1000) } export function getPostgrestClaims(projectRef: string, role: PostgrestImpersonationRole) { const exp = getExp1HourFromNow() const nowTimestamp = Math.floor(Date.now() / 1000) if (role.role === 'authenticated') { // Supabase native auth case if (role.userType === 'native' && role.user) { const user = role.user return { aal: role.aal ?? 'aal1', amr: [{ method: 'password', timestamp: nowTimestamp }], app_metadata: user.raw_app_meta_data, aud: 'authenticated', email: user.email, exp, iat: nowTimestamp, iss: `https://${projectRef}.supabase.co/auth/v1`, phone: user.phone, role: user.role ?? role.role, session_id: uuidv4(), sub: user.id, user_metadata: user.raw_user_meta_data, is_anonymous: user.is_anonymous, } } // External auth case if (role.userType === 'external' && role.externalAuth) { return { aal: role.aal ?? 'aal1', aud: 'authenticated', exp, iat: nowTimestamp, role: 'authenticated', session_id: uuidv4(), sub: role.externalAuth.sub, ...role.externalAuth.additionalClaims, } } } return { iss: 'supabase', ref: projectRef, role: role.role, iat: nowTimestamp, exp, } } export type RoleImpersonationState = Pick export function wrapWithRoleImpersonation( sql: SafeSqlFragment, state?: RoleImpersonationState ): SafeSqlFragment { const { role, claims } = state ?? { role: undefined, claims: undefined } if (role === undefined) return sql const unexpiredClaims = claims !== undefined ? { ...claims, exp: getExp1HourFromNow() } : undefined const impersonationSql = getImpersonationSQL({ role: role, unexpiredClaims, sql }) return impersonationSql } function encodeText(data: string) { return new TextEncoder().encode(data) } function encodeBase64Url(data: ArrayBuffer | Uint8Array | string): string { return btoa( String.fromCharCode(...new Uint8Array(typeof data === 'string' ? encodeText(data) : data)) ) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, '') } function genKey(rawKey: string) { return window.crypto.subtle.importKey( 'raw', encodeText(rawKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify'] ) } async function createToken(jwtPayload: object, key: string) { const headerAndPayload = encodeBase64Url(encodeText(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))) + '.' + encodeBase64Url(encodeText(JSON.stringify(jwtPayload))) const signature = encodeBase64Url( new Uint8Array( await window.crypto.subtle.sign( { name: 'HMAC' }, await genKey(key), encodeText(headerAndPayload) ) ) ) return `${headerAndPayload}.${signature}` } export function getRoleImpersonationJWT( projectRef: string, jwtSecret: string, role: PostgrestImpersonationRole ): Promise { const claims = { ...getPostgrestClaims(projectRef, role), exp: getExp1HourFromNow(), } return createToken(claims, jwtSecret) }