export type ManagedRole = 'user' | 'manager' | 'admin' | 'super_admin' export type SubscriptionTier = 'free' | 'pro' | 'pro_plus' export type SubscriptionStatus = 'active' | 'canceled' | 'past_due' | 'expired' export type SubscriptionAction = 'create' | 'update' | 'delete' export class AdminPublicError extends Error { constructor( readonly status: number, readonly code: string, ) { super(code) } } const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i const ROLE_VALUES = new Set(['user', 'manager', 'admin', 'super_admin']) const TIER_VALUES = new Set(['free', 'pro', 'pro_plus']) const STATUS_VALUES = new Set(['active', 'canceled', 'past_due', 'expired']) function recordValue(value: unknown): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new AdminPublicError(400, 'invalid_json_body') } return value as Record } function assertOnlyKeys(value: Record, allowed: readonly string[]): void { const allowedSet = new Set(allowed) if (Object.keys(value).some((key) => !allowedSet.has(key))) { throw new AdminPublicError(400, 'unexpected_request_field') } } function requiredString( value: Record, key: string, minimum: number, maximum: number, ): string { const raw = value[key] if (typeof raw !== 'string') throw new AdminPublicError(400, `invalid_${key}`) const normalized = raw.trim() if (normalized.length < minimum || normalized.length > maximum) { throw new AdminPublicError(400, `invalid_${key}`) } return normalized } function optionalString( value: Record, key: string, maximum: number, ): string | undefined { if (!(key in value)) return undefined const raw = value[key] if (typeof raw !== 'string' || raw.length > maximum) { throw new AdminPublicError(400, `invalid_${key}`) } return raw } export function requireUuid(value: string | null | undefined, code: string): string { if (!value || !UUID_PATTERN.test(value)) throw new AdminPublicError(400, code) return value.toLowerCase() } export function requireIdempotencyKey(req: Request): string { return requireUuid(req.headers.get('idempotency-key')?.trim(), 'valid_idempotency_key_required') } export async function readAdminJson(req: Request, maximumBytes = 16 * 1024): Promise> { const contentType = req.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() if (contentType !== 'application/json') throw new AdminPublicError(415, 'application_json_required') const declaredLength = req.headers.get('content-length') if (declaredLength && (!/^\d+$/.test(declaredLength) || Number(declaredLength) > maximumBytes)) { throw new AdminPublicError(413, 'request_too_large') } const text = await req.text() if (new TextEncoder().encode(text).byteLength > maximumBytes) { throw new AdminPublicError(413, 'request_too_large') } try { return recordValue(JSON.parse(text) as unknown) } catch (error) { if (error instanceof AdminPublicError) throw error throw new AdminPublicError(400, 'invalid_json_body') } } export function actorEmail(user: { email?: string | null }): string { const email = user.email?.trim().toLowerCase() ?? '' if (email.length < 3 || email.length > 150 || !email.includes('@')) { throw new AdminPublicError(403, 'admin_identity_unavailable') } return email } export interface RoleChangeRequest { idempotencyKey: string userId: string newRole: ManagedRole memo: string } export function parseRoleChangeRequest( value: unknown, idempotencyKey: string, ): RoleChangeRequest { const body = recordValue(value) assertOnlyKeys(body, ['userId', 'newRole', 'memo']) const newRole = requiredString(body, 'newRole', 3, 20) as ManagedRole if (!ROLE_VALUES.has(newRole)) throw new AdminPublicError(400, 'invalid_newRole') return { idempotencyKey: requireUuid(idempotencyKey, 'valid_idempotency_key_required'), userId: requireUuid(requiredString(body, 'userId', 36, 36), 'invalid_userId'), newRole, memo: requiredString(body, 'memo', 3, 1000), } } export interface SubscriptionMutationRequest { idempotencyKey: string action: SubscriptionAction userId: string tier?: SubscriptionTier status?: SubscriptionStatus currentPeriodEnd?: string overageCredits?: number adminNote?: string memo: string } function optionalDateTime(body: Record): string | undefined { if (!('currentPeriodEnd' in body)) return undefined const raw = body.currentPeriodEnd if (typeof raw !== 'string' || raw.length > 40 || !/^\d{4}-\d{2}-\d{2}T/.test(raw)) { throw new AdminPublicError(400, 'invalid_currentPeriodEnd') } const timestamp = Date.parse(raw) if (!Number.isFinite(timestamp)) throw new AdminPublicError(400, 'invalid_currentPeriodEnd') return new Date(timestamp).toISOString() } export function parseSubscriptionMutationRequest( action: SubscriptionAction, value: unknown, idempotencyKey: string, queryUserId?: string | null, ): SubscriptionMutationRequest { const body = recordValue(value) const allowed = action === 'create' ? ['userId', 'tier', 'status', 'currentPeriodEnd', 'overageCredits', 'adminNote', 'memo'] : action === 'update' ? ['tier', 'status', 'currentPeriodEnd', 'overageCredits', 'adminNote', 'memo'] : ['memo'] assertOnlyKeys(body, allowed) const userId = action === 'create' ? requireUuid(requiredString(body, 'userId', 36, 36), 'invalid_userId') : requireUuid(queryUserId, 'invalid_userId') const result: SubscriptionMutationRequest = { idempotencyKey: requireUuid(idempotencyKey, 'valid_idempotency_key_required'), action, userId, memo: requiredString(body, 'memo', 3, 1000), } if ('tier' in body) { const tier = requiredString(body, 'tier', 3, 20) as SubscriptionTier if (!TIER_VALUES.has(tier)) throw new AdminPublicError(400, 'invalid_tier') result.tier = tier } if (action === 'create' && !result.tier) throw new AdminPublicError(400, 'tier_required') if ('status' in body) { const status = requiredString(body, 'status', 6, 20) as SubscriptionStatus if (!STATUS_VALUES.has(status)) throw new AdminPublicError(400, 'invalid_status') result.status = status } const currentPeriodEnd = optionalDateTime(body) if (currentPeriodEnd) result.currentPeriodEnd = currentPeriodEnd if ('overageCredits' in body) { const credits = body.overageCredits if (!Number.isInteger(credits) || (credits as number) < 0 || (credits as number) > 1_000_000) { throw new AdminPublicError(400, 'invalid_overageCredits') } result.overageCredits = credits as number } const adminNote = optionalString(body, 'adminNote', 2000) if (adminNote !== undefined) result.adminNote = adminNote return result } export function validateQueryKeys(url: URL, allowed: readonly string[]): void { const allowedSet = new Set(allowed) for (const key of url.searchParams.keys()) { if (!allowedSet.has(key)) throw new AdminPublicError(400, 'unexpected_query_parameter') } } export function parsePagination(url: URL): { page: number; limit: number; from: number; to: number } { const pageRaw = url.searchParams.get('page') ?? '1' const limitRaw = url.searchParams.get('limit') ?? '20' if (!/^\d+$/.test(pageRaw) || !/^\d+$/.test(limitRaw)) { throw new AdminPublicError(400, 'invalid_pagination') } const page = Number(pageRaw) const limit = Number(limitRaw) if (!Number.isSafeInteger(page) || page < 1 || page > 1_000_000 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) { throw new AdminPublicError(400, 'invalid_pagination') } const from = (page - 1) * limit return { page, limit, from, to: from + limit - 1 } } export function optionalEnum( value: string | null, allowed: ReadonlySet, code: string, ): T | undefined { if (!value) return undefined if (!allowed.has(value as T)) throw new AdminPublicError(400, code) return value as T } export function managedRoleFilter(value: string | null): ManagedRole | undefined { return optionalEnum(value, ROLE_VALUES, 'invalid_role_filter') } export function subscriptionTierFilter(value: string | null): SubscriptionTier | undefined { return optionalEnum(value, TIER_VALUES, 'invalid_tier_filter') } export function subscriptionStatusFilter(value: string | null): SubscriptionStatus | undefined { return optionalEnum(value, STATUS_VALUES, 'invalid_status_filter') } export function parseSearch(value: string | null): string | undefined { if (value === null || value === '') return undefined const normalized = value.trim() if (!normalized || normalized.length > 100 || Array.from(normalized).some((character) => character.charCodeAt(0) < 32)) { throw new AdminPublicError(400, 'invalid_search') } return normalized } export function parsePositiveInteger(value: string | null, code: string): number | undefined { if (value === null) return undefined if (!/^[1-9]\d{0,9}$/.test(value)) throw new AdminPublicError(400, code) const parsed = Number(value) if (!Number.isSafeInteger(parsed)) throw new AdminPublicError(400, code) return parsed } export function parseIsoDate(value: string | null, code: string): string | undefined { if (value === null || value === '') return undefined if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) throw new AdminPublicError(400, code) const timestamp = Date.parse(`${value}T00:00:00Z`) if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString().slice(0, 10) !== value) { throw new AdminPublicError(400, code) } return value } const SENSITIVE_AUDIT_KEYS = new Set([ 'api_key', 'password', 'payload', 'payload_digest', 'payple_payer_id', 'payple_pay_oid', 'provider_event_id', 'provider_resource_id', 'raw_payload', 'secret', 'token', ]) function sanitizeAuditValue(value: unknown, depth: number): unknown { if (depth > 8) return '[REDACTED]' if (Array.isArray(value)) return value.map((item) => sanitizeAuditValue(item, depth + 1)) if (!value || typeof value !== 'object') return value const sanitized: Record = {} for (const [key, item] of Object.entries(value as Record)) { if (SENSITIVE_AUDIT_KEYS.has(key.toLowerCase())) continue sanitized[key] = sanitizeAuditValue(item, depth + 1) } return sanitized } export function sanitizeAuditRecord(value: Record): Record { return { ...value, before_data: sanitizeAuditValue(value.before_data, 0), after_data: sanitizeAuditValue(value.after_data, 0), } } interface RpcErrorLike { code?: string | null message?: string | null } export function adminRpcError(error: RpcErrorLike): AdminPublicError { const message = error.message ?? '' const mappings: Array<[string, number, string]> = [ ['admin_identity_not_linked', 403, 'admin_identity_not_linked'], ['insufficient_admin_role', 403, 'admin_forbidden'], ['super_admin_required_for_privileged_role', 403, 'super_admin_required'], ['admin_role_required', 403, 'admin_role_required'], ['target_user_not_found', 404, 'user_not_found'], ['subscription_not_found', 404, 'subscription_not_found'], ['subscription_already_exists', 409, 'subscription_already_exists'], ['idempotency_key_reused_with_different_request', 409, 'idempotency_conflict'], ['operation_in_progress', 409, 'operation_in_progress'], ['cannot_demote_last_super_admin', 409, 'last_super_admin_protected'], ['memo_must_be_3_to_1000_characters', 400, 'invalid_memo'], ['invalid_', 400, 'invalid_request'], ['tier_required', 400, 'tier_required'], ] for (const [fragment, status, code] of mappings) { if (message.includes(fragment)) return new AdminPublicError(status, code) } if (error.code === '42501') return new AdminPublicError(403, 'admin_forbidden') if (error.code === 'P0002') return new AdminPublicError(404, 'resource_not_found') if (error.code === '23505' || error.code === '23514' || error.code === '55P03') { return new AdminPublicError(409, 'admin_operation_conflict') } if (error.code === '22023') return new AdminPublicError(400, 'invalid_request') return new AdminPublicError(500, 'admin_operation_failed') } export function validateRoleRpcResult( value: unknown, expected: RoleChangeRequest, ): Record { const result = recordValue(value) if (result.success !== true || result.userId !== expected.userId || result.newRole !== expected.newRole) { throw new AdminPublicError(502, 'invalid_admin_rpc_response') } return { success: true, userId: expected.userId, newRole: expected.newRole } } export function validateSubscriptionRpcResult( value: unknown, expected: SubscriptionMutationRequest, ): Record { const result = recordValue(value) const subscription = recordValue(result.subscription) if (result.success !== true || subscription.user_id !== expected.userId || !TIER_VALUES.has(subscription.tier as SubscriptionTier) || !STATUS_VALUES.has(subscription.status as SubscriptionStatus)) { throw new AdminPublicError(502, 'invalid_admin_rpc_response') } return { success: true, subscription } } function isStatusError(error: unknown): error is { status: number } { return !!error && typeof error === 'object' && 'status' in error && typeof (error as { status?: unknown }).status === 'number' } export function publicAdminError(error: unknown): AdminPublicError { if (error instanceof AdminPublicError) return error if (isStatusError(error)) { if (error.status === 401) return new AdminPublicError(401, 'authentication_required') if (error.status === 403) return new AdminPublicError(403, 'admin_forbidden') } return new AdminPublicError(500, 'internal_error') } export function adminJsonResponse( body: Record, corsHeaders: Record, status = 200, ): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', }, }) } export function adminErrorResponse(error: unknown, corsHeaders: Record): Response { const publicError = publicAdminError(error) return adminJsonResponse({ error: publicError.code }, corsHeaders, publicError.status) }