import { base32FromBytes, randomBytes, randomHex } from "@/worker/auth/bytes"; import { sha256Hex } from "@/worker/auth/crypto"; import type { DavScope } from "@/worker/db/types"; export const PAT_PREFIX = "dab_pat_"; export const PAT_RE = /^dab_pat_([0-9a-f]{24,})_([a-z2-7]{52,})$/; export const allDavScopes = [ "dav:files:read", "dav:files:write", "dav:caldav:read", "dav:caldav:write", "dav:carddav:read", "dav:carddav:write", ] as const satisfies readonly DavScope[]; export const patScopePresets: Record = { "files.readonly": ["dav:files:read"], "files.full": ["dav:files:read", "dav:files:write"], "caldav.readonly": ["dav:caldav:read"], "caldav.full": ["dav:caldav:read", "dav:caldav:write"], "carddav.readonly": ["dav:carddav:read"], "carddav.full": ["dav:carddav:read", "dav:carddav:write"], "dav.full": [...allDavScopes], }; export interface ParsedPat { id: string; publicId: string; secret: string; } export interface GeneratedPat { id: string; token: string; tokenDigest: string; } export function parsePat(token: string): ParsedPat | null { const match = PAT_RE.exec(token); if (!match) return null; const [, publicId, secret] = match; return { id: `${PAT_PREFIX}${publicId}`, publicId, secret }; } export async function generatePat(): Promise { const publicId = randomHex(12); const secret = base32FromBytes(randomBytes(32)); const id = `${PAT_PREFIX}${publicId}`; const token = `${id}_${secret}`; return { id, token, tokenDigest: await sha256Hex(token) }; } export async function digestPat(token: string): Promise { if (!parsePat(token)) return null; return await sha256Hex(token); } export function normalizeScopes(input: readonly string[]): DavScope[] | null { const expanded = new Set(); for (const scope of input) { const preset = patScopePresets[scope]; if (preset) { for (const value of preset) expanded.add(value); continue; } if ((allDavScopes as readonly string[]).includes(scope)) { expanded.add(scope as DavScope); continue; } return null; } return [...expanded].sort(); } export function parseBasicAuth(header: string | null): { username: string; password: string } | null { if (!header) return null; const [scheme, encoded, extra] = header.split(/\s+/); if (!scheme || !encoded || extra !== undefined || scheme.toLowerCase() !== "basic") return null; try { const decoded = atob(encoded); const separator = decoded.indexOf(":"); if (separator === -1) return null; return { username: decoded.slice(0, separator), password: decoded.slice(separator + 1), }; } catch { return null; } }