cauthy-auth 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cookies.ts","../src/seal.ts","../src/types.ts","../src/cauthy.ts","../src/theme.ts"],"sourcesContent":["// Cookies, by hand: reading the Cookie header and writing Set-Cookie.\n//\n// A session can outgrow one cookie (browsers stop near 4 KB), so a value is\n// written in numbered parts — name.0, name.1 — and read back in order.\n\nconst PART = 3500\n\nexport function readCookies(header: string | null | undefined): Map<string, string> {\n const out = new Map<string, string>()\n for (const pair of (header ?? '').split(';')) {\n const i = pair.indexOf('=')\n if (i > 0) out.set(pair.slice(0, i).trim(), pair.slice(i + 1).trim())\n }\n return out\n}\n\n/** A value written in parts, or null when the cookie is not there. */\nexport function readParts(cookies: Map<string, string>, name: string): string | null {\n let value = ''\n for (let i = 0; cookies.has(`${name}.${i}`); i++) value += cookies.get(`${name}.${i}`)\n return value === '' ? null : value\n}\n\nexport interface CookieAttributes {\n secure: boolean\n maxAge: number // seconds; 0 deletes\n path?: string\n}\n\nfunction attributes(a: CookieAttributes): string {\n // httpOnly: script never reads it. SameSite=Lax: it rides the navigation\n // that comes back from Cauthy, and no cross-site form post.\n return `Path=${a.path ?? '/'}; HttpOnly; SameSite=Lax; Max-Age=${a.maxAge}${a.secure ? '; Secure' : ''}`\n}\n\nexport function writeCookie(name: string, value: string, a: CookieAttributes): string {\n return `${name}=${value}; ${attributes(a)}`\n}\n\n/**\n * The Set-Cookie headers that store a value in parts and delete whatever\n * parts an earlier, longer value left behind.\n */\nexport function writeParts(name: string, value: string, a: CookieAttributes, had: Map<string, string>): string[] {\n const out: string[] = []\n let n = 0\n for (let i = 0; i < value.length; i += PART) out.push(writeCookie(`${name}.${n++}`, value.slice(i, i + PART), a))\n for (let i = n; had.has(`${name}.${i}`); i++) out.push(writeCookie(`${name}.${i}`, '', { ...a, maxAge: 0 }))\n return out\n}\n\nexport function clearParts(name: string, a: CookieAttributes, had: Map<string, string>): string[] {\n const out: string[] = []\n for (let i = 0; had.has(`${name}.${i}`) || i === 0; i++) out.push(writeCookie(`${name}.${i}`, '', { ...a, maxAge: 0 }))\n return out\n}\n","// The session cookie's envelope: encrypted and authenticated with a key only\n// the server has, so the browser carries the session without being able to\n// read or change it.\n\nimport { CompactEncrypt, compactDecrypt } from 'jose'\n\nconst encoder = new TextEncoder()\nconst decoder = new TextDecoder()\n\n/** The 256-bit key a secret stands for. */\nexport async function keyOf(secret: string): Promise<Uint8Array> {\n return new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(secret)))\n}\n\nexport async function seal(value: unknown, key: Uint8Array): Promise<string> {\n return new CompactEncrypt(encoder.encode(JSON.stringify(value)))\n .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })\n .encrypt(key)\n}\n\n/** The value sealed, or null for anything that is not a cookie this key made. */\nexport async function unseal<T>(sealed: string, key: Uint8Array): Promise<T | null> {\n try {\n const { plaintext } = await compactDecrypt(sealed, key, {\n keyManagementAlgorithms: ['dir'],\n contentEncryptionAlgorithms: ['A256GCM'],\n })\n return JSON.parse(decoder.decode(plaintext)) as T\n } catch {\n return null\n }\n}\n","/** How an application is set up to sign its people in with Cauthy. */\nexport interface CauthyOptions {\n /** Cauthy's public address, e.g. `https://id.example.com`. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The application's OAuth client id. Default: `CAUTHY_CLIENT_ID`. */\n clientId?: string\n /** Its secret, when the client is confidential. Default: `CAUTHY_CLIENT_SECRET`. */\n clientSecret?: string\n /**\n * The key the session cookie is encrypted with: any string of 32 characters\n * or more, kept on the server. Default: `CAUTHY_SECRET`.\n */\n secret?: string\n /**\n * The system this application is, as its organizations are filed under in\n * Cauthy (`\"pondari\"`). When set, a session's organizations are that\n * system's. Default: `CAUTHY_SYSTEM`.\n */\n system?: string\n /** Where the routes are mounted. Default: `/api/auth`. */\n basePath?: string\n /**\n * The application's own public origin (`https://app.example.com`), for the\n * redirect URI. Default: the origin of each request, which is right unless\n * a proxy hides it.\n */\n baseURL?: string\n /** Where a person lands after signing in, when no `return_to` was asked. Default: `/`. */\n afterSignIn?: string\n /** Where a person lands after signing out. Default: `/`. */\n afterSignOut?: string\n /** Name of the session cookie. Default: `cauthy_session`. */\n cookieName?: string\n /** `fetch` to reach Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n}\n\n/** The person signed in. Every moment is UTC. */\nexport interface SessionUser {\n id: string\n email: string\n name: string\n emailVerified: boolean\n /**\n * True when the account ran out of time to confirm its address: let it read\n * and refuse its writes.\n */\n restricted: boolean\n /** When an unconfirmed address stops being enough (ISO 8601, UTC), if there is a deadline. */\n emailVerifyBy?: string\n /** The language the person reads in (\"es\", \"en-US\"), when Cauthy knows it. */\n locale?: string\n}\n\n/** A value JSON can carry. What crosses from server to browser is typed as this, never `unknown`. */\nexport type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }\n\n/** An organization the person belongs to, with their role in it. */\nexport interface SessionOrganization {\n id: string\n system: string\n name: string\n kind: string\n externalId?: string\n role: string\n /** What the system keeps about the organization, as it filed it in Cauthy. */\n metadata: Record<string, JsonValue>\n}\n\n/** A signed-in person, as the server sees them. */\nexport interface Session {\n user: SessionUser\n /** The Cauthy tenant the account is of. */\n tenantId: string\n /** The roles the account holds in that tenant. */\n roles: string[]\n /**\n * Cauthy's access token for this person: send it as `Authorization: Bearer`\n * to another service that verifies Cauthy's tokens. Never give it to the browser.\n */\n accessToken: string\n /** When the access token expires and the session is checked with Cauthy again (ISO 8601, UTC). */\n expiresAt: string\n}\n\n/** What the browser is told of a session: who, and their organizations — never a token. */\nexport interface PublicSession {\n user: SessionUser\n tenantId: string\n roles: string[]\n organizations: SessionOrganization[]\n expiresAt: string\n}\n\n/** Why there is no session. */\nexport type SessionStatus =\n | 'authenticated'\n | 'unauthenticated' // nobody is signed in\n | 'expired' // signed in, but the session must be renewed first (see `Cauthy.resolve`)\n\nexport class CauthyConfigError extends Error {\n constructor(message: string) {\n super(`cauthy-auth: ${message}`)\n this.name = 'CauthyConfigError'\n }\n}\n","// Sign in with Cauthy, held by the application's own server.\n//\n// The application mounts one route (`handler`) and asks who is there\n// (`getSession`). Signing in, signing up, second factors, passkeys, recovering\n// a password and the account page are Cauthy's pages: this sends the browser\n// to them and keeps what comes back in an encrypted, httpOnly cookie. There is\n// no database and no screen to build.\n//\n// The session is Cauthy's short-lived access token plus the refresh token that\n// renews it. When the access token expires the session is renewed with Cauthy,\n// which is where an account disabled there, or signed out everywhere, stops\n// being signed in here — within minutes, with nothing to synchronise.\n\nimport { clearParts, readCookies, readParts, writeCookie, writeParts, type CookieAttributes } from './cookies'\nimport { keyOf, seal, unseal } from './seal'\nimport {\n CauthyConfigError,\n type CauthyOptions,\n type JsonValue,\n type PublicSession,\n type Session,\n type SessionOrganization,\n type SessionStatus,\n type SessionUser,\n} from './types'\n\n/** What the session cookie holds. Short names: it rides every request. */\ninterface Stored {\n at: string // access token\n rt: string // refresh token\n exp: number // when the access token expires, epoch seconds\n u: SessionUser\n tid: string\n roles: string[]\n}\n\n/** What the sign-in in progress holds, between leaving for Cauthy and coming back. */\ninterface Flow {\n state: string\n verifier: string\n returnTo: string\n}\n\nconst SESSION_DAYS = 30\nconst FLOW_SECONDS = 600\n/** Renew this long before the access token runs out, so a request never carries one about to. */\nconst SKEW_SECONDS = 30\nconst ORGANIZATIONS_TTL_MS = 60_000\n\n/** What `resolve` answers: who is there, and the cookies that must go back. */\nexport interface Resolved {\n session: Session | null\n status: SessionStatus\n /** `Set-Cookie` values to send with the response. Empty when nothing changed. */\n cookies: string[]\n}\n\nexport interface Cauthy {\n /** Serves the routes under `basePath`: login, callback, session, refresh, logout, account. */\n handler(request: Request): Promise<Response>\n /**\n * Who is signed in, or null.\n *\n * Give it the response's headers and a session that needs renewing is\n * renewed on the spot, its new cookie added to them. Without them an\n * expired session reads as null (see `status`): renewing changes the\n * cookie, and a cookie nobody sends back is a session lost.\n */\n getSession(request: Request, responseHeaders?: Headers): Promise<Session | null>\n /** Whether somebody is signed in, nobody is, or the session must be renewed first. */\n status(request: Request): Promise<SessionStatus>\n /**\n * The session, renewed when it has to be, and the cookies to send back.\n * The caller MUST send `cookies` with its response.\n */\n resolve(request: Request): Promise<Resolved>\n /**\n * For a route only a signed-in person may see: the session, or the response\n * to give instead — a redirect to sign in (or to renew) for a page, a 401\n * for anything else.\n */\n protect(request: Request): Promise<{ session: Session; response?: undefined } | { session?: undefined; response: Response }>\n /** The organizations the person belongs to (of `system`, when set), with their role in each. */\n organizations(session: Session): Promise<SessionOrganization[]>\n /** What the browser may be told of a session. */\n toPublic(session: Session): Promise<PublicSession>\n /** Paths of this application that start a sign-in, a sign-out, or open the account page. */\n urls: { signIn(returnTo?: string): string; signOut(): string; account(returnTo?: string): string; session: string }\n options: Readonly<Required<Pick<CauthyOptions, 'issuer' | 'clientId' | 'basePath' | 'afterSignIn' | 'afterSignOut' | 'cookieName'>>>\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\nfunction b64url(bytes: Uint8Array): string {\n let s = ''\n for (const b of bytes) s += String.fromCharCode(b)\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction random(bytes = 32): string {\n return b64url(crypto.getRandomValues(new Uint8Array(bytes)))\n}\n\n/** A path of this application, and nothing else: never another site, never `//host`. */\nfunction localPath(candidate: string | null | undefined, fallback: string): string {\n if (!candidate || !candidate.startsWith('/') || candidate.startsWith('//') || candidate.includes('\\\\')) return fallback\n return candidate\n}\n\nfunction json(body: unknown, status = 200, headers?: Headers): Response {\n const h = headers ?? new Headers()\n h.set('content-type', 'application/json; charset=utf-8')\n h.set('cache-control', 'no-store')\n return new Response(JSON.stringify(body), { status, headers: h })\n}\n\nfunction redirect(location: string, cookies: string[] = []): Response {\n const h = new Headers({ location, 'cache-control': 'no-store' })\n for (const c of cookies) h.append('set-cookie', c)\n return new Response(null, { status: 302, headers: h })\n}\n\nexport function createCauthy(given: CauthyOptions = {}): Cauthy {\n const issuer = (given.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n const clientId = given.clientId ?? env('CAUTHY_CLIENT_ID') ?? ''\n const clientSecret = given.clientSecret ?? env('CAUTHY_CLIENT_SECRET')\n const secret = given.secret ?? env('CAUTHY_SECRET') ?? ''\n const system = given.system ?? env('CAUTHY_SYSTEM') ?? ''\n if (!/^https?:\\/\\//.test(issuer)) throw new CauthyConfigError('issuer (CAUTHY_ISSUER) must be Cauthy\\'s address, e.g. https://id.example.com')\n if (clientId === '') throw new CauthyConfigError('clientId (CAUTHY_CLIENT_ID) is required')\n if (secret.length < 32) throw new CauthyConfigError('secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie')\n\n const basePath = '/' + (given.basePath ?? '/api/auth').replace(/^\\/+|\\/+$/g, '')\n const afterSignIn = localPath(given.afterSignIn, '/')\n const afterSignOut = localPath(given.afterSignOut, '/')\n const cookieName = given.cookieName ?? 'cauthy_session'\n const flowCookie = `${cookieName}_flow`\n const doFetch: typeof fetch = (...args) => (given.fetch ?? fetch)(...args)\n const key = keyOf(secret)\n\n const originOf = (request: Request) => (given.baseURL ?? new URL(request.url).origin).replace(/\\/+$/, '')\n const attrs = (request: Request, maxAge: number): CookieAttributes => ({ secure: originOf(request).startsWith('https://'), maxAge })\n\n // ── Talking to Cauthy ────────────────────────────────────────────────────\n\n async function token(form: Record<string, string>): Promise<{ ok: true; body: any } | { ok: false; ended: boolean }> {\n const body = new URLSearchParams({ client_id: clientId, ...form })\n if (clientSecret) body.set('client_secret', clientSecret)\n try {\n const res = await doFetch(`${issuer}/v1/oauth/token`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },\n body,\n })\n if (res.ok) return { ok: true, body: await res.json() }\n // 4xx is Cauthy's answer: the grant is no good. Anything else is a\n // failure to ask, and says nothing about the session.\n return { ok: false, ended: res.status >= 400 && res.status < 500 }\n } catch {\n return { ok: false, ended: false }\n }\n }\n\n async function userinfo(accessToken: string): Promise<{ u: SessionUser; tid: string; roles: string[] } | null> {\n try {\n const res = await doFetch(`${issuer}/v1/oauth/userinfo`, { headers: { authorization: `Bearer ${accessToken}` } })\n if (!res.ok) return null\n const b = (await res.json()) as Record<string, any>\n return {\n tid: String(b.tid ?? ''),\n roles: Array.isArray(b.roles) ? b.roles.filter((r: unknown) => typeof r === 'string') : [],\n u: {\n id: String(b.sub ?? ''),\n email: String(b.email ?? ''),\n name: String(b.name ?? ''),\n emailVerified: b.email_verified === true,\n restricted: b.restricted === true,\n ...(typeof b.email_verify_by === 'string' ? { emailVerifyBy: b.email_verify_by } : {}),\n ...(typeof b.locale === 'string' && b.locale !== '' ? { locale: b.locale } : {}),\n },\n }\n } catch {\n return null\n }\n }\n\n /** What Cauthy's answer to a token request becomes in the cookie. */\n async function stored(body: any): Promise<Stored | null> {\n if (typeof body?.access_token !== 'string' || typeof body?.refresh_token !== 'string') return null\n const who = await userinfo(body.access_token)\n if (who === null || who.u.id === '') return null\n const lifetime = typeof body.expires_in === 'number' && body.expires_in > 0 ? body.expires_in : 300\n return { at: body.access_token, rt: body.refresh_token, exp: Math.floor(Date.now() / 1000) + lifetime, ...who }\n }\n\n function toSession(s: Stored): Session {\n return { user: s.u, tenantId: s.tid, roles: s.roles, accessToken: s.at, expiresAt: new Date(s.exp * 1000).toISOString() }\n }\n\n // ── The cookie ───────────────────────────────────────────────────────────\n\n async function read(request: Request): Promise<{ had: Map<string, string>; value: Stored | null; present: boolean }> {\n const had = readCookies(request.headers.get('cookie'))\n const raw = readParts(had, cookieName)\n if (raw === null) return { had, value: null, present: false }\n const value = await unseal<Stored>(raw, await key)\n const sound = value !== null && typeof value.at === 'string' && typeof value.rt === 'string' && typeof value.exp === 'number'\n return { had, value: sound ? value : null, present: true }\n }\n\n async function write(request: Request, s: Stored, had: Map<string, string>): Promise<string[]> {\n return writeParts(cookieName, await seal(s, await key), attrs(request, SESSION_DAYS * 86_400), had)\n }\n\n const fresh = (s: Stored) => s.exp - Date.now() / 1000 > SKEW_SECONDS\n\n // One renewal per refresh token at a time in this process: requests that\n // arrive together share it. (Across processes Cauthy gives a few seconds'\n // grace to the same token, for the same reason.)\n const renewing = new Map<string, Promise<Stored | 'ended' | 'unreachable'>>()\n\n function renew(was: Stored): Promise<Stored | 'ended' | 'unreachable'> {\n let running = renewing.get(was.rt)\n if (running === undefined) {\n const rt = was.rt\n running = (async (): Promise<Stored | 'ended' | 'unreachable'> => {\n const res = await token({ grant_type: 'refresh_token', refresh_token: rt })\n if (!res.ok) return res.ended ? 'ended' : 'unreachable'\n const next = await stored(res.body)\n if (next !== null) return next\n // Renewed, but who it is could not be read just now. The new tokens\n // are kept all the same — the old refresh token is spent, and losing\n // its successor would lose the session — with what was known before.\n if (typeof res.body?.access_token !== 'string' || typeof res.body?.refresh_token !== 'string') return 'unreachable'\n const lifetime = typeof res.body.expires_in === 'number' && res.body.expires_in > 0 ? res.body.expires_in : 300\n return { ...was, at: res.body.access_token, rt: res.body.refresh_token, exp: Math.floor(Date.now() / 1000) + lifetime }\n })()\n renewing.set(rt, running)\n // A renewal that worked is remembered for a moment, for requests still\n // arriving with the old cookie; one that could not ask is tried again\n // by the next request.\n void running.then((result) => {\n if (result === 'unreachable') return renewing.delete(rt)\n const timer = setTimeout(() => renewing.delete(rt), 5_000)\n ;(timer as { unref?: () => void }).unref?.()\n })\n }\n return running\n }\n\n async function resolve(request: Request): Promise<Resolved> {\n const { had, value, present } = await read(request)\n if (value === null) {\n // A cookie this key did not make (a rotated secret, a tampered value) is cleared.\n return { session: null, status: 'unauthenticated', cookies: present ? clearParts(cookieName, attrs(request, 0), had) : [] }\n }\n if (fresh(value)) return { session: toSession(value), status: 'authenticated', cookies: [] }\n\n const renewed = await renew(value)\n if (renewed === 'ended') {\n return { session: null, status: 'unauthenticated', cookies: clearParts(cookieName, attrs(request, 0), had) }\n }\n // Cauthy could not be asked: the cookie stays as it is, and the next\n // request tries again. Nobody is let in on an expired token meanwhile.\n if (renewed === 'unreachable') return { session: null, status: 'expired', cookies: [] }\n return { session: toSession(renewed), status: 'authenticated', cookies: await write(request, renewed, had) }\n }\n\n async function getSession(request: Request, responseHeaders?: Headers): Promise<Session | null> {\n if (responseHeaders === undefined) {\n const { value } = await read(request)\n return value !== null && fresh(value) ? toSession(value) : null\n }\n const r = await resolve(request)\n for (const c of r.cookies) responseHeaders.append('set-cookie', c)\n return r.session\n }\n\n async function status(request: Request): Promise<SessionStatus> {\n const { value } = await read(request)\n if (value === null) return 'unauthenticated'\n return fresh(value) ? 'authenticated' : 'expired'\n }\n\n // ── Organizations ────────────────────────────────────────────────────────\n\n const orgCache = new Map<string, { at: number; value: SessionOrganization[] }>()\n\n async function organizations(session: Session): Promise<SessionOrganization[]> {\n const hit = orgCache.get(session.accessToken)\n if (hit && Date.now() - hit.at < ORGANIZATIONS_TTL_MS) return hit.value\n const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ''}`\n const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } })\n if (!res.ok) throw new Error(`cauthy-auth: reading organizations: ${res.status}`)\n const body = (await res.json()) as { data?: Record<string, any>[] }\n const value = (body.data ?? []).map(\n (o): SessionOrganization => ({\n id: String(o.id),\n system: String(o.system ?? ''),\n name: String(o.name ?? ''),\n kind: String(o.kind ?? ''),\n role: String(o.role ?? ''),\n metadata: (o.metadata as Record<string, JsonValue>) ?? {},\n ...(o.external_id ? { externalId: String(o.external_id) } : {}),\n }),\n )\n if (orgCache.size > 5_000) orgCache.clear()\n orgCache.set(session.accessToken, { at: Date.now(), value })\n return value\n }\n\n async function toPublic(session: Session): Promise<PublicSession> {\n let orgs: SessionOrganization[] = []\n try {\n orgs = await organizations(session)\n } catch {\n // The person is signed in all the same; their organizations come back on the next read.\n }\n return { user: session.user, tenantId: session.tenantId, roles: session.roles, organizations: orgs, expiresAt: session.expiresAt }\n }\n\n // ── Routes ───────────────────────────────────────────────────────────────\n\n const urls = {\n signIn: (returnTo?: string) => `${basePath}/login${returnTo ? `?return_to=${encodeURIComponent(returnTo)}` : ''}`,\n signOut: () => `${basePath}/logout`,\n account: (returnTo?: string) => `${basePath}/account${returnTo ? `?return_to=${encodeURIComponent(returnTo)}` : ''}`,\n session: `${basePath}/session`,\n }\n\n async function login(request: Request, url: URL): Promise<Response> {\n const flow: Flow = { state: random(), verifier: random(48), returnTo: localPath(url.searchParams.get('return_to'), afterSignIn) }\n const challenge = b64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(flow.verifier))))\n const q = new URLSearchParams({\n client_id: clientId,\n redirect_uri: `${originOf(request)}${basePath}/callback`,\n response_type: 'code',\n state: flow.state,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n })\n // What the application may ask of the sign-in page: to ask again, and a language.\n const prompt = url.searchParams.get('prompt')\n if (prompt === 'login' || prompt === 'none') q.set('prompt', prompt)\n const locale = url.searchParams.get('locale') ?? url.searchParams.get('ui_locales')\n if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set('ui_locales', locale)\n return redirect(`${issuer}/v1/oauth/authorize?${q}`, [\n writeCookie(flowCookie, await seal(flow, await key), attrs(request, FLOW_SECONDS)),\n ])\n }\n\n function failed(request: Request, reason: string): Response {\n const h = new Headers({ 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' })\n h.append('set-cookie', writeCookie(flowCookie, '', attrs(request, 0)))\n return new Response(`Sign-in could not be completed (${reason}). Go back and try again.`, { status: 400, headers: h })\n }\n\n async function callback(request: Request, url: URL): Promise<Response> {\n const had = readCookies(request.headers.get('cookie'))\n const sealed = had.get(flowCookie)\n const flow = sealed ? await unseal<Flow>(sealed, await key) : null\n // The answer must be to a question THIS browser asked: without that, a\n // stranger's link could sign a person in to the stranger's account.\n if (flow === null || url.searchParams.get('state') !== flow.state) return failed(request, 'state_mismatch')\n const error = url.searchParams.get('error')\n if (error !== null) {\n // prompt=none and nobody signed in: not an error, just \"nobody\".\n if (error === 'login_required') return redirect(flow.returnTo, [writeCookie(flowCookie, '', attrs(request, 0))])\n return failed(request, error.slice(0, 60))\n }\n const code = url.searchParams.get('code')\n if (!code) return failed(request, 'no_code')\n const res = await token({\n grant_type: 'authorization_code',\n code,\n redirect_uri: `${originOf(request)}${basePath}/callback`,\n code_verifier: flow.verifier,\n })\n const s = res.ok ? await stored(res.body) : null\n if (s === null) return failed(request, 'exchange_failed')\n return redirect(flow.returnTo, [...(await write(request, s, had)), writeCookie(flowCookie, '', attrs(request, 0))])\n }\n\n async function logout(request: Request): Promise<Response> {\n const { had, value } = await read(request)\n if (value !== null) {\n // Best effort: the session is over here whether or not Cauthy heard.\n const body = new URLSearchParams({ client_id: clientId, token: value.rt })\n if (clientSecret) body.set('client_secret', clientSecret)\n await doFetch(`${issuer}/v1/oauth/revoke`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body,\n }).catch(() => undefined)\n orgCache.delete(value.at)\n }\n // Then through Cauthy, which ends its own session in this browser too:\n // otherwise the next \"sign in\" would walk straight back in.\n const q = new URLSearchParams({ client_id: clientId, post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` })\n return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had))\n }\n\n async function handler(request: Request): Promise<Response> {\n const url = new URL(request.url)\n if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response('Not found', { status: 404 })\n const route = url.pathname.slice(basePath.length).replace(/\\/+$/, '') || '/'\n const get = request.method === 'GET' || request.method === 'HEAD'\n\n if (route === '/login' && get) return login(request, url)\n if (route === '/callback' && get) return callback(request, url)\n if (route === '/logout' && (get || request.method === 'POST')) return logout(request)\n\n if (route === '/session' && get) {\n const r = await resolve(request)\n const h = new Headers()\n for (const c of r.cookies) h.append('set-cookie', c)\n return json({ session: r.session ? await toPublic(r.session) : null, status: r.status }, 200, h)\n }\n\n // For a page that found its session expired: renew, and go back to it.\n if (route === '/refresh' && get) {\n const r = await resolve(request)\n const back = localPath(url.searchParams.get('return_to'), afterSignIn)\n if (r.status === 'unauthenticated') return redirect(urls.signIn(back), r.cookies)\n if (r.status === 'expired') return new Response('Sign-in is unavailable right now. Try again in a moment.', { status: 503 })\n return redirect(back, r.cookies)\n }\n\n if (route === '/account' && get) {\n const q = new URLSearchParams({ client_id: clientId })\n const back = localPath(url.searchParams.get('return_to'), '')\n if (back !== '') q.set('return_to', `${originOf(request)}${back}`)\n const locale = url.searchParams.get('locale')\n if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set('ui_locales', locale)\n return redirect(`${issuer}/v1/oauth/account?${q}`)\n }\n return new Response('Not found', { status: 404 })\n }\n\n async function protect(request: Request) {\n const { value } = await read(request)\n if (value !== null && fresh(value)) return { session: toSession(value) }\n const url = new URL(request.url)\n const state = value === null ? 'none' : 'expired'\n // A page is sent to sign in, or to renew and come back. Anything else —\n // a fetch, a form post — is told, and its caller decides.\n const page = request.method === 'GET' && (request.headers.get('sec-fetch-mode') ?? 'navigate') === 'navigate' &&\n (request.headers.get('accept') ?? 'text/html').includes('text/html')\n if (!page) {\n return { response: json({ error: 'unauthenticated', session: state }, 401, new Headers({ 'x-cauthy-session': state })) }\n }\n const back = url.pathname + url.search\n const to = value === null ? urls.signIn(back) : `${basePath}/refresh?return_to=${encodeURIComponent(back)}`\n return { response: redirect(to) }\n }\n\n return {\n handler,\n getSession,\n status,\n resolve,\n protect,\n organizations,\n toPublic,\n urls,\n options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName },\n }\n}\n","// An application's look, for Cauthy's hosted pages.\n//\n// Cauthy's pages — sign-in, sign-up, the account page — can take the colours,\n// corners, typeface and logo of the application that sends its people there,\n// so signing in reads as a page of the application. A theme is a short list of\n// values, which Cauthy checks one by one; it is never CSS.\n//\n// An application built with Stackmyth already has those values, as its\n// `--sm-*` tokens. `themeFromStackmyth` reads them out of its stylesheets.\n\n/** How an application looks on Cauthy's hosted pages. Every value is optional. */\nexport interface CauthyTheme {\n /** The page. Colours are `#rrggbb` or `rgba(r, g, b, a)`. */\n background?: string\n /** The card, and inputs. */\n surface?: string\n /** Headings and what is typed. */\n text?: string\n /** Paragraphs, labels, small print. */\n textMuted?: string\n border?: string\n /** The main button, and what is written on it. */\n primary?: string\n primaryText?: string\n /** Links, and the focus ring. */\n accent?: string\n error?: string\n errorBackground?: string\n success?: string\n successBackground?: string\n /** Corners of cards and inputs, in pixels (0–40). */\n radius?: number\n /** Corners of buttons, in pixels (0–40). */\n buttonRadius?: number\n /** A CSS font-family list. Cauthy loads no font files: put a system font after the application's own. */\n fontFamily?: string\n /** An image shown where \"Cauthy\" is, over https. */\n logoUrl?: string\n}\n\nexport interface StackmythThemeOptions {\n /**\n * The named theme to read, when the stylesheet keeps several under\n * `[data-theme=…]` (`\"vennet\"`). Without it only what applies to every\n * page (`:root`) is read.\n */\n theme?: string\n /** Which of the application's two modes to take. Default: `light`. */\n mode?: 'light' | 'dark'\n logoUrl?: string\n}\n\nexport interface ImportedTheme {\n theme: CauthyTheme\n /** Tokens that were there and could not be carried over, and why. */\n warnings: string[]\n}\n\nconst MAX_RADIUS = 40\n\n// ── Reading custom properties out of CSS ───────────────────────────────────\n\ninterface Declaration {\n name: string\n value: string\n /** The selectors and at-rules it sits inside, outermost first. */\n within: string[]\n}\n\nfunction declarations(css: string): Declaration[] {\n const text = css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n const out: Declaration[] = []\n const within: string[] = []\n let chunk = ''\n let quote = ''\n let depth = 0 // parentheses, so a `;` inside url(…) does not end anything\n const flush = () => {\n const m = /^\\s*(--[\\w-]+)\\s*:\\s*([\\s\\S]+?)\\s*$/.exec(chunk)\n if (m) out.push({ name: m[1], value: m[2].replace(/\\s+/g, ' ').replace(/\\s*!important$/, ''), within: [...within] })\n chunk = ''\n }\n for (const ch of text) {\n if (quote) {\n chunk += ch\n if (ch === quote) quote = ''\n continue\n }\n if (ch === '\"' || ch === \"'\") {\n quote = ch\n chunk += ch\n } else if (ch === '(') {\n depth++\n chunk += ch\n } else if (ch === ')') {\n depth = Math.max(0, depth - 1)\n chunk += ch\n } else if (ch === '{' && depth === 0) {\n within.push(chunk.trim().replace(/\\s+/g, ' '))\n chunk = ''\n } else if (ch === '}' && depth === 0) {\n flush()\n within.pop()\n } else if (ch === ';' && depth === 0) {\n flush()\n } else {\n chunk += ch\n }\n }\n return out\n}\n\nconst DARK = /prefers-color-scheme:\\s*dark|data-(mode|theme|color-scheme)=[\"']?dark|\\.dark\\b/i\nconst LIGHT = /prefers-color-scheme:\\s*light|data-(mode|theme|color-scheme)=[\"']?light|\\.light\\b/i\nconst NAMED = /data-theme=[\"']?([\\w-]+)/i\n\n/** Whether a declaration applies to the page being themed. */\nfunction applies(d: Declaration, o: Required<Pick<StackmythThemeOptions, 'mode'>> & { theme?: string }): boolean {\n for (const scope of d.within) {\n if (o.mode === 'light' && DARK.test(scope)) return false\n if (o.mode === 'dark' && LIGHT.test(scope)) return false\n const named = NAMED.exec(scope)\n if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false\n // Component-level rules (`.btn { --sm-… }`) are not the page's theme.\n if (!scope.startsWith('@') && !/:root|^html\\b|^body\\b|\\[data-|^\\*$|:host/.test(scope)) return false\n }\n return true\n}\n\n// ── Values ─────────────────────────────────────────────────────────────────\n\nfunction resolve(value: string, vars: Map<string, string>, seen = 0): string | null {\n if (seen > 16) return null\n let unresolved = false\n // Innermost var() first: fallbacks may hold another var().\n let out = value\n for (let guard = 0; guard < 32 && /var\\(/.test(out); guard++) {\n out = out.replace(/var\\(\\s*(--[\\w-]+)\\s*(?:,\\s*([^()]*(?:\\([^()]*\\)[^()]*)*))?\\)/, (_m, name: string, fallback?: string) => {\n const known = vars.get(name)\n const next = known !== undefined ? resolve(known, vars, seen + 1) : fallback !== undefined ? resolve(fallback.trim(), vars, seen + 1) : null\n if (next === null) unresolved = true\n return next ?? ''\n })\n if (unresolved) return null\n }\n return /var\\(/.test(out) ? null : out.trim()\n}\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n))\nconst hex2 = (n: number) => Math.round(clamp01(n) * 255).toString(16).padStart(2, '0')\nconst gamma = (c: number) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055)\n\nfunction out(r: number, g: number, b: number, a = 1): string {\n if (a >= 1) return `#${hex2(r)}${hex2(g)}${hex2(b)}`\n return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${Math.round(clamp01(a) * 1000) / 1000})`\n}\n\nconst alpha = (s: string | undefined) => (s === undefined ? 1 : s.endsWith('%') ? parseFloat(s) / 100 : parseFloat(s))\n\nconst KEYWORDS: Record<string, string> = { white: '#ffffff', black: '#000000' }\n\n/** A colour as Cauthy takes it — `#rrggbb` or `rgba(…)` — or null when it is none it can read. */\nexport function toColor(value: string): string | null {\n const v = value.trim().toLowerCase()\n if (KEYWORDS[v]) return KEYWORDS[v]\n if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v)) return v\n let m = /^rgba?\\(\\s*([\\d.]+%?)[\\s,]+([\\d.]+%?)[\\s,]+([\\d.]+%?)(?:\\s*[,/]\\s*([\\d.]+%?))?\\s*\\)$/.exec(v)\n if (m) {\n const ch = (s: string) => (s.endsWith('%') ? parseFloat(s) / 100 : parseFloat(s) / 255)\n return out(ch(m[1]), ch(m[2]), ch(m[3]), alpha(m[4]))\n }\n m = /^hsla?\\(\\s*([\\d.]+)(?:deg)?[\\s,]+([\\d.]+)%[\\s,]+([\\d.]+)%(?:\\s*[,/]\\s*([\\d.]+%?))?\\s*\\)$/.exec(v)\n if (m) {\n const h = ((parseFloat(m[1]) % 360) + 360) % 360\n const s = parseFloat(m[2]) / 100\n const l = parseFloat(m[3]) / 100\n const f = (n: number) => {\n const k = (n + h / 30) % 12\n return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1))\n }\n return out(f(0), f(8), f(4), alpha(m[4]))\n }\n m = /^oklch\\(\\s*([\\d.]+%?)\\s+([\\d.]+%?)\\s+([\\d.]+)(?:deg)?(?:\\s*\\/\\s*([\\d.]+%?))?\\s*\\)$/.exec(v)\n if (m) {\n const L = m[1].endsWith('%') ? parseFloat(m[1]) / 100 : parseFloat(m[1])\n const C = m[2].endsWith('%') ? (parseFloat(m[2]) / 100) * 0.4 : parseFloat(m[2])\n const H = (parseFloat(m[3]) * Math.PI) / 180\n const a = C * Math.cos(H)\n const b = C * Math.sin(H)\n const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3)\n const mm = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3)\n const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3)\n return out(\n gamma(4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s),\n gamma(-1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s),\n gamma(-0.0041960863 * l - 0.7034186147 * mm + 1.707614701 * s),\n alpha(m[4]),\n )\n }\n return null\n}\n\n/** A length in pixels, within what Cauthy takes, or null. */\nfunction toRadius(value: string): number | null {\n const m = /^([\\d.]+)(px|rem|em)?$/.exec(value.trim())\n if (!m) return null\n const px = parseFloat(m[1]) * (m[2] === 'rem' || m[2] === 'em' ? 16 : 1)\n return Number.isFinite(px) ? Math.min(MAX_RADIUS, Math.max(0, Math.round(px))) : null\n}\n\n/** A font list as Cauthy takes it, or null. */\nfunction toFont(value: string): string | null {\n const v = value.trim()\n return /^[A-Za-z0-9 ,'\"_-]{1,200}$/.test(v) && (v.match(/\"/g)?.length ?? 0) % 2 === 0 && (v.match(/'/g)?.length ?? 0) % 2 === 0 ? v : null\n}\n\n// ── Stackmyth ──────────────────────────────────────────────────────────────\n\n/** Which Stackmyth token each value of a theme is read from, first found wins. */\nconst COLORS: [keyof CauthyTheme, string[]][] = [\n ['background', ['--sm-bg-surface', '--sm-surface']],\n ['surface', ['--sm-bg-surface-raised', '--sm-surface-raised']],\n ['text', ['--sm-text-primary', '--sm-surface-text']],\n ['textMuted', ['--sm-text-secondary']],\n ['border', ['--sm-border-default', '--sm-surface-border']],\n ['primary', ['--sm-primary', '--sm-bg-interactive']],\n ['primaryText', ['--sm-primary-text', '--sm-text-inverse']],\n ['accent', ['--sm-text-brand', '--sm-border-focus', '--sm-accent']],\n ['error', ['--sm-error-soft-text', '--sm-error']],\n ['errorBackground', ['--sm-error-soft']],\n ['success', ['--sm-success-soft-text', '--sm-success']],\n ['successBackground', ['--sm-success-soft']],\n]\n\n/**\n * Reads an application's Stackmyth theme out of its stylesheets.\n *\n * Give it the CSS that declares the tokens, in the order the application\n * loads it — Stackmyth's own (`@stackmyth/core/core.vars.css`, or a theme\n * file) and then the application's brand file that re-points them. Tokens\n * that point at the application's own variables are followed. The\n * application's dark mode is left out unless asked for.\n */\nexport function themeFromStackmyth(css: string | string[], options: StackmythThemeOptions = {}): ImportedTheme {\n const o = { mode: options.mode ?? 'light', theme: options.theme }\n const vars = new Map<string, string>()\n for (const sheet of Array.isArray(css) ? css : [css]) {\n for (const d of declarations(sheet)) if (applies(d, o)) vars.set(d.name, d.value)\n }\n const theme: CauthyTheme = {}\n const warnings: string[] = []\n\n const read = <T>(tokens: string[], to: (v: string) => T | null, what: string): T | undefined => {\n for (const token of tokens) {\n const raw = vars.get(token)\n if (raw === undefined) continue\n const value = resolve(raw, vars)\n const got = value === null ? null : to(value)\n if (got !== null) return got\n warnings.push(`${token}: \"${raw}\" is not ${what} Cauthy can take`)\n }\n return undefined\n }\n\n for (const [field, tokens] of COLORS) {\n const c = read(tokens, toColor, 'a colour')\n if (c !== undefined) (theme as Record<string, unknown>)[field] = c\n }\n const radius = read(['--sm-radius-container', '--sm-radius-md'], toRadius, 'a length')\n if (radius !== undefined) theme.radius = radius\n const button = read(['--sm-radius-interactive', '--sm-radius-sm'], toRadius, 'a length')\n if (button !== undefined) theme.buttonRadius = button\n const font = read(['--sm-font-family'], toFont, 'a font list')\n if (font !== undefined) theme.fontFamily = font\n if (options.logoUrl) theme.logoUrl = options.logoUrl\n\n if (vars.size === 0) warnings.push('no custom properties were found: is this the stylesheet that declares the --sm-* tokens?')\n else if (![...vars.keys()].some((k) => k.startsWith('--sm-'))) warnings.push('no --sm-* token was found in these stylesheets')\n return { theme, warnings }\n}\n\n// ── Sending it to Cauthy ───────────────────────────────────────────────────\n\n/** A theme as Cauthy's API names its values. */\nexport function toWire(theme: CauthyTheme): Record<string, string | number> {\n const names: Record<keyof CauthyTheme, string> = {\n background: 'background', surface: 'surface', text: 'text', textMuted: 'text_muted', border: 'border',\n primary: 'primary', primaryText: 'primary_text', accent: 'accent', error: 'error', errorBackground: 'error_background',\n success: 'success', successBackground: 'success_background', radius: 'radius', buttonRadius: 'button_radius',\n fontFamily: 'font_family', logoUrl: 'logo_url',\n }\n const wire: Record<string, string | number> = {}\n for (const [k, v] of Object.entries(theme)) if (v !== undefined && v !== '') wire[names[k as keyof CauthyTheme]] = v as string | number\n return wire\n}\n\nexport interface PushThemeOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The application's client id. Default: `CAUTHY_CLIENT_ID`. */\n clientId?: string\n /** The workspace's slug. Default: `CAUTHY_TENANT`. */\n tenant?: string\n /**\n * A personal access token with `oauth_clients:manage`. Default:\n * `CAUTHY_TOKEN`. It belongs in the deploy pipeline, never in the application.\n */\n token?: string\n theme: CauthyTheme\n fetch?: typeof fetch\n}\n\n/**\n * Gives the application its look on Cauthy's pages. It replaces the theme: a\n * value left out goes back to Cauthy's own. Run it when the brand changes —\n * from a script or the deploy — not on every request.\n */\nexport async function pushTheme(o: PushThemeOptions): Promise<void> {\n const env = (n: string) => (typeof process !== 'undefined' ? process.env?.[n] : undefined)\n const issuer = (o.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n const clientId = o.clientId ?? env('CAUTHY_CLIENT_ID') ?? ''\n const tenant = o.tenant ?? env('CAUTHY_TENANT') ?? ''\n const token = o.token ?? env('CAUTHY_TOKEN') ?? ''\n for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {\n if (v === '') throw new Error(`cauthy-auth: ${name} is required to push a theme`)\n }\n const res = await (o.fetch ?? fetch)(`${issuer}/v1/oauth/clients/${encodeURIComponent(clientId)}/theme`, {\n method: 'PUT',\n headers: { authorization: `Bearer ${token}`, 'x-tenant-key': tenant, 'content-type': 'application/json' },\n body: JSON.stringify(toWire(o.theme)),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { errors?: { message?: string }[] } | null\n throw new Error(`cauthy-auth: Cauthy refused the theme (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`)\n }\n}\n"],"mappings":"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","names":["out","resolve","out","env"]}
1
+ {"version":3,"sources":["../src/cookies.ts","../src/seal.ts","../src/webhook.ts","../src/types.ts","../src/cauthy.ts","../src/theme.ts","../src/permissions.ts"],"sourcesContent":["// Cookies, by hand: reading the Cookie header and writing Set-Cookie.\n//\n// A session can outgrow one cookie (browsers stop near 4 KB), so a value is\n// written in numbered parts — name.0, name.1 — and read back in order.\n\nconst PART = 3500\n\nexport function readCookies(header: string | null | undefined): Map<string, string> {\n const out = new Map<string, string>()\n for (const pair of (header ?? '').split(';')) {\n const i = pair.indexOf('=')\n if (i > 0) out.set(pair.slice(0, i).trim(), pair.slice(i + 1).trim())\n }\n return out\n}\n\n/** A value written in parts, or null when the cookie is not there. */\nexport function readParts(cookies: Map<string, string>, name: string): string | null {\n let value = ''\n for (let i = 0; cookies.has(`${name}.${i}`); i++) value += cookies.get(`${name}.${i}`)\n return value === '' ? null : value\n}\n\nexport interface CookieAttributes {\n secure: boolean\n maxAge: number // seconds; 0 deletes\n path?: string\n}\n\nfunction attributes(a: CookieAttributes): string {\n // httpOnly: script never reads it. SameSite=Lax: it rides the navigation\n // that comes back from Cauthy, and no cross-site form post.\n return `Path=${a.path ?? '/'}; HttpOnly; SameSite=Lax; Max-Age=${a.maxAge}${a.secure ? '; Secure' : ''}`\n}\n\nexport function writeCookie(name: string, value: string, a: CookieAttributes): string {\n return `${name}=${value}; ${attributes(a)}`\n}\n\n/**\n * The Set-Cookie headers that store a value in parts and delete whatever\n * parts an earlier, longer value left behind.\n */\nexport function writeParts(name: string, value: string, a: CookieAttributes, had: Map<string, string>): string[] {\n const out: string[] = []\n let n = 0\n for (let i = 0; i < value.length; i += PART) out.push(writeCookie(`${name}.${n++}`, value.slice(i, i + PART), a))\n for (let i = n; had.has(`${name}.${i}`); i++) out.push(writeCookie(`${name}.${i}`, '', { ...a, maxAge: 0 }))\n return out\n}\n\nexport function clearParts(name: string, a: CookieAttributes, had: Map<string, string>): string[] {\n const out: string[] = []\n for (let i = 0; had.has(`${name}.${i}`) || i === 0; i++) out.push(writeCookie(`${name}.${i}`, '', { ...a, maxAge: 0 }))\n return out\n}\n","// The session cookie's envelope: encrypted and authenticated with a key only\n// the server has, so the browser carries the session without being able to\n// read or change it.\n\nimport { CompactEncrypt, compactDecrypt } from 'jose'\n\nconst encoder = new TextEncoder()\nconst decoder = new TextDecoder()\n\n/** The 256-bit key a secret stands for. */\nexport async function keyOf(secret: string): Promise<Uint8Array> {\n return new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(secret)))\n}\n\nexport async function seal(value: unknown, key: Uint8Array): Promise<string> {\n return new CompactEncrypt(encoder.encode(JSON.stringify(value)))\n .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })\n .encrypt(key)\n}\n\n/** The value sealed, or null for anything that is not a cookie this key made. */\nexport async function unseal<T>(sealed: string, key: Uint8Array): Promise<T | null> {\n try {\n const { plaintext } = await compactDecrypt(sealed, key, {\n keyManagementAlgorithms: ['dir'],\n contentEncryptionAlgorithms: ['A256GCM'],\n })\n return JSON.parse(decoder.decode(plaintext)) as T\n } catch {\n return null\n }\n}\n","// Hearing from Cauthy: verifying a webhook delivery.\n//\n// Cauthy signs every delivery with its own key — the one its tokens are\n// signed with, published at /.well-known/jwks.json — so there is no secret to\n// receive, store or rotate. Verifying is one call:\n//\n// const event = await auth.webhook(request) // throws if it is not Cauthy's\n//\n// What is checked: the signature, against Cauthy's published keys; that the\n// delivery was meant for THIS endpoint (a signature anyone can verify must say\n// who it is for); that it is recent; and that the body is, byte for byte, the\n// one that was signed.\n\nimport { createRemoteJWKSet, jwtVerify, type JWTVerifyGetKey } from 'jose'\nimport type { JsonValue } from './types'\n\n/** Something that happened in Cauthy, as a subscriber is told. Moments are UTC. */\nexport interface CauthyEvent {\n /** This delivery's id: the same event delivered again carries the same one. */\n id: string\n /** What happened: \"organization.member_added\". */\n type: string\n tenantId: string\n createdAt: string\n /** The person it happened to, when it is about one. */\n userId?: string\n /** Who did it. */\n actorId?: string\n /** What the event says: for an organization, its id, system, external_id, role… */\n data: Record<string, JsonValue>\n}\n\nexport class CauthyWebhookError extends Error {\n constructor(readonly reason: string) {\n super(`cauthy-auth: webhook refused: ${reason}`)\n this.name = 'CauthyWebhookError'\n }\n}\n\nexport interface VerifyWebhookOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /**\n * This endpoint's own public address, exactly as it is registered in Cauthy.\n * Default: the request's URL, which is right unless a proxy changes it.\n */\n url?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: Date\n}\n\nconst keys = new Map<string, JWTVerifyGetKey>()\n\nfunction keysOf(issuer: string): JWTVerifyGetKey {\n let set = keys.get(issuer)\n if (set === undefined) {\n set = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 30_000 })\n keys.set(issuer, set)\n }\n return set\n}\n\nconst hex = (s: unknown) => (typeof s === 'string' && s !== '' ? s : undefined)\n\n/**\n * Verifies a delivery and answers the event it carries. Throws\n * `CauthyWebhookError` for anything that is not a delivery Cauthy made for\n * this endpoint just now: answer it 401 and nothing else.\n */\nexport async function verifyWebhook(request: Request, options: VerifyWebhookOptions = {}): Promise<CauthyEvent> {\n const env = typeof process !== 'undefined' ? process.env?.CAUTHY_ISSUER : undefined\n const issuer = (options.issuer ?? env ?? '').replace(/\\/+$/, '')\n if (issuer === '') throw new CauthyWebhookError('issuer (CAUTHY_ISSUER) is not set')\n const signature = request.headers.get('x-cauthy-signature-jwt')\n if (!signature) throw new CauthyWebhookError('no signature')\n\n // The bytes as they arrived: what was signed is their hash, not a re-encoding.\n const raw = new Uint8Array(await request.arrayBuffer())\n const audience = options.url ?? request.url.split('?')[0]\n\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(signature, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience,\n typ: 'cauthy-webhook+jwt',\n requiredClaims: ['exp', 'iat', 'jti', 'body_sha256'],\n currentDate: options.now,\n clockTolerance: 30,\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new CauthyWebhookError(err instanceof Error ? err.message : 'bad signature')\n }\n\n // Not from the future either: a clock that far off is not Cauthy's.\n const now = Math.floor((options.now ?? new Date()).getTime() / 1000)\n if (typeof claims.iat !== 'number' || claims.iat > now + 30) throw new CauthyWebhookError('signed in the future')\n\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', raw))\n let b64 = ''\n for (const b of digest) b64 += String.fromCharCode(b)\n const hash = btoa(b64).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n if (hash !== claims.body_sha256) throw new CauthyWebhookError('the body is not the one that was signed')\n\n let body: Record<string, any>\n try {\n body = JSON.parse(new TextDecoder().decode(raw))\n } catch {\n throw new CauthyWebhookError('the body is not JSON')\n }\n // The envelope and the signature must tell the same story.\n if (body.id !== claims.jti || body.event !== claims.evt || body.tenant_id !== claims.tid) {\n throw new CauthyWebhookError('the body and its signature disagree')\n }\n const inner = (body.data ?? {}) as Record<string, any>\n return {\n id: String(body.id),\n type: String(body.event),\n tenantId: String(body.tenant_id),\n createdAt: String(inner.created_at ?? body.created_at ?? ''),\n ...(hex(inner.user_id) ? { userId: inner.user_id } : {}),\n ...(hex(inner.actor_id) ? { actorId: inner.actor_id } : {}),\n data: (inner.data ?? {}) as Record<string, JsonValue>,\n }\n}\n","/** How an application is set up to sign its people in with Cauthy. */\nexport interface CauthyOptions {\n /** Cauthy's public address, e.g. `https://id.example.com`. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The application's OAuth client id. Default: `CAUTHY_CLIENT_ID`. */\n clientId?: string\n /** Its secret, when the client is confidential. Default: `CAUTHY_CLIENT_SECRET`. */\n clientSecret?: string\n /**\n * The key the session cookie is encrypted with: any string of 32 characters\n * or more, kept on the server. Default: `CAUTHY_SECRET`.\n */\n secret?: string\n /**\n * The system this application is, as its organizations are filed under in\n * Cauthy (`\"pondari\"`). When set, a session's organizations are that\n * system's. Default: `CAUTHY_SYSTEM`.\n */\n system?: string\n /** Where the routes are mounted. Default: `/api/auth`. */\n basePath?: string\n /**\n * The application's own public origin (`https://app.example.com`), for the\n * redirect URI. Default: the origin of each request, which is right unless\n * a proxy hides it.\n */\n baseURL?: string\n /** Where a person lands after signing in, when no `return_to` was asked. Default: `/`. */\n afterSignIn?: string\n /** Where a person lands after signing out. Default: `/`. */\n afterSignOut?: string\n /** Name of the session cookie. Default: `cauthy_session`. */\n cookieName?: string\n /** `fetch` to reach Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n}\n\n/** The person signed in. Every moment is UTC. */\nexport interface SessionUser {\n id: string\n email: string\n name: string\n emailVerified: boolean\n /**\n * True when the account ran out of time to confirm its address: let it read\n * and refuse its writes.\n */\n restricted: boolean\n /** When an unconfirmed address stops being enough (ISO 8601, UTC), if there is a deadline. */\n emailVerifyBy?: string\n /** The language the person reads in (\"es\", \"en-US\"), when Cauthy knows it. */\n locale?: string\n}\n\n/** A value JSON can carry. What crosses from server to browser is typed as this, never `unknown`. */\nexport type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }\n\n/** An organization the person belongs to, with their role in it. */\nexport interface SessionOrganization {\n id: string\n system: string\n name: string\n kind: string\n externalId?: string\n role: string\n /** What the system keeps about the organization, as it filed it in Cauthy. */\n metadata: Record<string, JsonValue>\n /**\n * What the person may do here, as \"resource:action\" entries (\"*\" is any):\n * their role's permissions, what it inherits, and anything given to them\n * apart from it. Absent when the system keeps no catalog of roles in Cauthy.\n * Check with `can(organization, resource, action)`.\n */\n permissions?: string[]\n /** What the person is denied here, whatever else allows it. */\n denied?: string[]\n}\n\n/** A signed-in person, as the server sees them. */\nexport interface Session {\n user: SessionUser\n /** The Cauthy tenant the account is of. */\n tenantId: string\n /** The roles the account holds in that tenant. */\n roles: string[]\n /**\n * Cauthy's access token for this person: send it as `Authorization: Bearer`\n * to another service that verifies Cauthy's tokens. Never give it to the browser.\n */\n accessToken: string\n /** When the access token expires and the session is checked with Cauthy again (ISO 8601, UTC). */\n expiresAt: string\n}\n\n/** What the browser is told of a session: who, and their organizations — never a token. */\nexport interface PublicSession {\n user: SessionUser\n tenantId: string\n roles: string[]\n organizations: SessionOrganization[]\n expiresAt: string\n}\n\n/** Why there is no session. */\nexport type SessionStatus =\n | 'authenticated'\n | 'unauthenticated' // nobody is signed in\n | 'expired' // signed in, but the session must be renewed first (see `Cauthy.resolve`)\n\nexport class CauthyConfigError extends Error {\n constructor(message: string) {\n super(`cauthy-auth: ${message}`)\n this.name = 'CauthyConfigError'\n }\n}\n","// Sign in with Cauthy, held by the application's own server.\n//\n// The application mounts one route (`handler`) and asks who is there\n// (`getSession`). Signing in, signing up, second factors, passkeys, recovering\n// a password and the account page are Cauthy's pages: this sends the browser\n// to them and keeps what comes back in an encrypted, httpOnly cookie. There is\n// no database and no screen to build.\n//\n// The session is Cauthy's short-lived access token plus the refresh token that\n// renews it. When the access token expires the session is renewed with Cauthy,\n// which is where an account disabled there, or signed out everywhere, stops\n// being signed in here — within minutes, with nothing to synchronise.\n\nimport { clearParts, readCookies, readParts, writeCookie, writeParts, type CookieAttributes } from './cookies'\nimport { keyOf, seal, unseal } from './seal'\nimport { verifyWebhook, type CauthyEvent, type VerifyWebhookOptions } from './webhook'\nimport {\n CauthyConfigError,\n type CauthyOptions,\n type JsonValue,\n type PublicSession,\n type Session,\n type SessionOrganization,\n type SessionStatus,\n type SessionUser,\n} from './types'\n\n/** What the session cookie holds. Short names: it rides every request. */\ninterface Stored {\n at: string // access token\n rt: string // refresh token\n exp: number // when the access token expires, epoch seconds\n u: SessionUser\n tid: string\n roles: string[]\n}\n\n/** What the sign-in in progress holds, between leaving for Cauthy and coming back. */\ninterface Flow {\n state: string\n verifier: string\n returnTo: string\n}\n\nconst SESSION_DAYS = 30\nconst FLOW_SECONDS = 600\n/** Renew this long before the access token runs out, so a request never carries one about to. */\nconst SKEW_SECONDS = 30\nconst ORGANIZATIONS_TTL_MS = 60_000\n\n/** What `resolve` answers: who is there, and the cookies that must go back. */\nexport interface Resolved {\n session: Session | null\n status: SessionStatus\n /** `Set-Cookie` values to send with the response. Empty when nothing changed. */\n cookies: string[]\n}\n\nexport interface Cauthy {\n /** Serves the routes under `basePath`: login, callback, session, refresh, logout, account. */\n handler(request: Request): Promise<Response>\n /**\n * Who is signed in, or null.\n *\n * Give it the response's headers and a session that needs renewing is\n * renewed on the spot, its new cookie added to them. Without them an\n * expired session reads as null (see `status`): renewing changes the\n * cookie, and a cookie nobody sends back is a session lost.\n */\n getSession(request: Request, responseHeaders?: Headers): Promise<Session | null>\n /** Whether somebody is signed in, nobody is, or the session must be renewed first. */\n status(request: Request): Promise<SessionStatus>\n /**\n * The session, renewed when it has to be, and the cookies to send back.\n * The caller MUST send `cookies` with its response.\n */\n resolve(request: Request): Promise<Resolved>\n /**\n * For a route only a signed-in person may see: the session, or the response\n * to give instead — a redirect to sign in (or to renew) for a page, a 401\n * for anything else.\n */\n protect(request: Request): Promise<{ session: Session; response?: undefined } | { session?: undefined; response: Response }>\n /** The organizations the person belongs to (of `system`, when set), with their role in each. */\n organizations(session: Session): Promise<SessionOrganization[]>\n /**\n * Verifies a webhook delivery from Cauthy and answers the event it carries;\n * throws `CauthyWebhookError` for anything else. No secret is involved: the\n * delivery is signed with Cauthy's own key.\n */\n webhook(request: Request, options?: Pick<VerifyWebhookOptions, 'jwks' | 'now' | 'url'>): Promise<CauthyEvent>\n /** What the browser may be told of a session. */\n toPublic(session: Session): Promise<PublicSession>\n /** Paths of this application that start a sign-in, a sign-out, or open the account page. */\n urls: { signIn(returnTo?: string): string; signOut(): string; account(returnTo?: string): string; session: string }\n options: Readonly<Required<Pick<CauthyOptions, 'issuer' | 'clientId' | 'basePath' | 'afterSignIn' | 'afterSignOut' | 'cookieName'>>>\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\nfunction b64url(bytes: Uint8Array): string {\n let s = ''\n for (const b of bytes) s += String.fromCharCode(b)\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction random(bytes = 32): string {\n return b64url(crypto.getRandomValues(new Uint8Array(bytes)))\n}\n\n/** A path of this application, and nothing else: never another site, never `//host`. */\nfunction localPath(candidate: string | null | undefined, fallback: string): string {\n if (!candidate || !candidate.startsWith('/') || candidate.startsWith('//') || candidate.includes('\\\\')) return fallback\n return candidate\n}\n\nfunction json(body: unknown, status = 200, headers?: Headers): Response {\n const h = headers ?? new Headers()\n h.set('content-type', 'application/json; charset=utf-8')\n h.set('cache-control', 'no-store')\n return new Response(JSON.stringify(body), { status, headers: h })\n}\n\nfunction redirect(location: string, cookies: string[] = []): Response {\n const h = new Headers({ location, 'cache-control': 'no-store' })\n for (const c of cookies) h.append('set-cookie', c)\n return new Response(null, { status: 302, headers: h })\n}\n\nexport function createCauthy(given: CauthyOptions = {}): Cauthy {\n const issuer = (given.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n const clientId = given.clientId ?? env('CAUTHY_CLIENT_ID') ?? ''\n const clientSecret = given.clientSecret ?? env('CAUTHY_CLIENT_SECRET')\n const secret = given.secret ?? env('CAUTHY_SECRET') ?? ''\n const system = given.system ?? env('CAUTHY_SYSTEM') ?? ''\n if (!/^https?:\\/\\//.test(issuer)) throw new CauthyConfigError('issuer (CAUTHY_ISSUER) must be Cauthy\\'s address, e.g. https://id.example.com')\n if (clientId === '') throw new CauthyConfigError('clientId (CAUTHY_CLIENT_ID) is required')\n if (secret.length < 32) throw new CauthyConfigError('secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie')\n\n const basePath = '/' + (given.basePath ?? '/api/auth').replace(/^\\/+|\\/+$/g, '')\n const afterSignIn = localPath(given.afterSignIn, '/')\n const afterSignOut = localPath(given.afterSignOut, '/')\n const cookieName = given.cookieName ?? 'cauthy_session'\n const flowCookie = `${cookieName}_flow`\n const doFetch: typeof fetch = (...args) => (given.fetch ?? fetch)(...args)\n const key = keyOf(secret)\n\n const originOf = (request: Request) => (given.baseURL ?? new URL(request.url).origin).replace(/\\/+$/, '')\n const attrs = (request: Request, maxAge: number): CookieAttributes => ({ secure: originOf(request).startsWith('https://'), maxAge })\n\n // ── Talking to Cauthy ────────────────────────────────────────────────────\n\n async function token(form: Record<string, string>): Promise<{ ok: true; body: any } | { ok: false; ended: boolean }> {\n const body = new URLSearchParams({ client_id: clientId, ...form })\n if (clientSecret) body.set('client_secret', clientSecret)\n try {\n const res = await doFetch(`${issuer}/v1/oauth/token`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },\n body,\n })\n if (res.ok) return { ok: true, body: await res.json() }\n // 4xx is Cauthy's answer: the grant is no good. Anything else is a\n // failure to ask, and says nothing about the session.\n return { ok: false, ended: res.status >= 400 && res.status < 500 }\n } catch {\n return { ok: false, ended: false }\n }\n }\n\n async function userinfo(accessToken: string): Promise<{ u: SessionUser; tid: string; roles: string[] } | null> {\n try {\n const res = await doFetch(`${issuer}/v1/oauth/userinfo`, { headers: { authorization: `Bearer ${accessToken}` } })\n if (!res.ok) return null\n const b = (await res.json()) as Record<string, any>\n return {\n tid: String(b.tid ?? ''),\n roles: Array.isArray(b.roles) ? b.roles.filter((r: unknown) => typeof r === 'string') : [],\n u: {\n id: String(b.sub ?? ''),\n email: String(b.email ?? ''),\n name: String(b.name ?? ''),\n emailVerified: b.email_verified === true,\n restricted: b.restricted === true,\n ...(typeof b.email_verify_by === 'string' ? { emailVerifyBy: b.email_verify_by } : {}),\n ...(typeof b.locale === 'string' && b.locale !== '' ? { locale: b.locale } : {}),\n },\n }\n } catch {\n return null\n }\n }\n\n /** What Cauthy's answer to a token request becomes in the cookie. */\n async function stored(body: any): Promise<Stored | null> {\n if (typeof body?.access_token !== 'string' || typeof body?.refresh_token !== 'string') return null\n const who = await userinfo(body.access_token)\n if (who === null || who.u.id === '') return null\n const lifetime = typeof body.expires_in === 'number' && body.expires_in > 0 ? body.expires_in : 300\n return { at: body.access_token, rt: body.refresh_token, exp: Math.floor(Date.now() / 1000) + lifetime, ...who }\n }\n\n function toSession(s: Stored): Session {\n return { user: s.u, tenantId: s.tid, roles: s.roles, accessToken: s.at, expiresAt: new Date(s.exp * 1000).toISOString() }\n }\n\n // ── The cookie ───────────────────────────────────────────────────────────\n\n async function read(request: Request): Promise<{ had: Map<string, string>; value: Stored | null; present: boolean }> {\n const had = readCookies(request.headers.get('cookie'))\n const raw = readParts(had, cookieName)\n if (raw === null) return { had, value: null, present: false }\n const value = await unseal<Stored>(raw, await key)\n const sound = value !== null && typeof value.at === 'string' && typeof value.rt === 'string' && typeof value.exp === 'number'\n return { had, value: sound ? value : null, present: true }\n }\n\n async function write(request: Request, s: Stored, had: Map<string, string>): Promise<string[]> {\n return writeParts(cookieName, await seal(s, await key), attrs(request, SESSION_DAYS * 86_400), had)\n }\n\n const fresh = (s: Stored) => s.exp - Date.now() / 1000 > SKEW_SECONDS\n\n // One renewal per refresh token at a time in this process: requests that\n // arrive together share it. (Across processes Cauthy gives a few seconds'\n // grace to the same token, for the same reason.)\n const renewing = new Map<string, Promise<Stored | 'ended' | 'unreachable'>>()\n\n function renew(was: Stored): Promise<Stored | 'ended' | 'unreachable'> {\n let running = renewing.get(was.rt)\n if (running === undefined) {\n const rt = was.rt\n running = (async (): Promise<Stored | 'ended' | 'unreachable'> => {\n const res = await token({ grant_type: 'refresh_token', refresh_token: rt })\n if (!res.ok) return res.ended ? 'ended' : 'unreachable'\n const next = await stored(res.body)\n if (next !== null) return next\n // Renewed, but who it is could not be read just now. The new tokens\n // are kept all the same — the old refresh token is spent, and losing\n // its successor would lose the session — with what was known before.\n if (typeof res.body?.access_token !== 'string' || typeof res.body?.refresh_token !== 'string') return 'unreachable'\n const lifetime = typeof res.body.expires_in === 'number' && res.body.expires_in > 0 ? res.body.expires_in : 300\n return { ...was, at: res.body.access_token, rt: res.body.refresh_token, exp: Math.floor(Date.now() / 1000) + lifetime }\n })()\n renewing.set(rt, running)\n // A renewal that worked is remembered for a moment, for requests still\n // arriving with the old cookie; one that could not ask is tried again\n // by the next request.\n void running.then((result) => {\n if (result === 'unreachable') return renewing.delete(rt)\n const timer = setTimeout(() => renewing.delete(rt), 5_000)\n ;(timer as { unref?: () => void }).unref?.()\n })\n }\n return running\n }\n\n async function resolve(request: Request): Promise<Resolved> {\n const { had, value, present } = await read(request)\n if (value === null) {\n // A cookie this key did not make (a rotated secret, a tampered value) is cleared.\n return { session: null, status: 'unauthenticated', cookies: present ? clearParts(cookieName, attrs(request, 0), had) : [] }\n }\n if (fresh(value)) return { session: toSession(value), status: 'authenticated', cookies: [] }\n\n const renewed = await renew(value)\n if (renewed === 'ended') {\n return { session: null, status: 'unauthenticated', cookies: clearParts(cookieName, attrs(request, 0), had) }\n }\n // Cauthy could not be asked: the cookie stays as it is, and the next\n // request tries again. Nobody is let in on an expired token meanwhile.\n if (renewed === 'unreachable') return { session: null, status: 'expired', cookies: [] }\n return { session: toSession(renewed), status: 'authenticated', cookies: await write(request, renewed, had) }\n }\n\n async function getSession(request: Request, responseHeaders?: Headers): Promise<Session | null> {\n if (responseHeaders === undefined) {\n const { value } = await read(request)\n return value !== null && fresh(value) ? toSession(value) : null\n }\n const r = await resolve(request)\n for (const c of r.cookies) responseHeaders.append('set-cookie', c)\n return r.session\n }\n\n async function status(request: Request): Promise<SessionStatus> {\n const { value } = await read(request)\n if (value === null) return 'unauthenticated'\n return fresh(value) ? 'authenticated' : 'expired'\n }\n\n // ── Organizations ────────────────────────────────────────────────────────\n\n const orgCache = new Map<string, { at: number; value: SessionOrganization[] }>()\n\n async function organizations(session: Session): Promise<SessionOrganization[]> {\n const hit = orgCache.get(session.accessToken)\n if (hit && Date.now() - hit.at < ORGANIZATIONS_TTL_MS) return hit.value\n const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ''}`\n const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } })\n if (!res.ok) throw new Error(`cauthy-auth: reading organizations: ${res.status}`)\n const body = (await res.json()) as { data?: Record<string, any>[] }\n const value = (body.data ?? []).map(\n (o): SessionOrganization => ({\n id: String(o.id),\n system: String(o.system ?? ''),\n name: String(o.name ?? ''),\n kind: String(o.kind ?? ''),\n role: String(o.role ?? ''),\n metadata: (o.metadata as Record<string, JsonValue>) ?? {},\n ...(o.external_id ? { externalId: String(o.external_id) } : {}),\n ...(Array.isArray(o.permissions) ? { permissions: o.permissions.map(String), denied: Array.isArray(o.denied) ? o.denied.map(String) : [] } : {}),\n }),\n )\n if (orgCache.size > 5_000) orgCache.clear()\n orgCache.set(session.accessToken, { at: Date.now(), value })\n return value\n }\n\n async function toPublic(session: Session): Promise<PublicSession> {\n let orgs: SessionOrganization[] = []\n try {\n orgs = await organizations(session)\n } catch {\n // The person is signed in all the same; their organizations come back on the next read.\n }\n return { user: session.user, tenantId: session.tenantId, roles: session.roles, organizations: orgs, expiresAt: session.expiresAt }\n }\n\n // ── Routes ───────────────────────────────────────────────────────────────\n\n const urls = {\n signIn: (returnTo?: string) => `${basePath}/login${returnTo ? `?return_to=${encodeURIComponent(returnTo)}` : ''}`,\n signOut: () => `${basePath}/logout`,\n account: (returnTo?: string) => `${basePath}/account${returnTo ? `?return_to=${encodeURIComponent(returnTo)}` : ''}`,\n session: `${basePath}/session`,\n }\n\n async function login(request: Request, url: URL): Promise<Response> {\n const flow: Flow = { state: random(), verifier: random(48), returnTo: localPath(url.searchParams.get('return_to'), afterSignIn) }\n const challenge = b64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(flow.verifier))))\n const q = new URLSearchParams({\n client_id: clientId,\n redirect_uri: `${originOf(request)}${basePath}/callback`,\n response_type: 'code',\n state: flow.state,\n code_challenge: challenge,\n code_challenge_method: 'S256',\n })\n // What the application may ask of the sign-in page: to ask again, and a language.\n const prompt = url.searchParams.get('prompt')\n if (prompt === 'login' || prompt === 'none') q.set('prompt', prompt)\n const locale = url.searchParams.get('locale') ?? url.searchParams.get('ui_locales')\n if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set('ui_locales', locale)\n return redirect(`${issuer}/v1/oauth/authorize?${q}`, [\n writeCookie(flowCookie, await seal(flow, await key), attrs(request, FLOW_SECONDS)),\n ])\n }\n\n function failed(request: Request, reason: string): Response {\n const h = new Headers({ 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' })\n h.append('set-cookie', writeCookie(flowCookie, '', attrs(request, 0)))\n return new Response(`Sign-in could not be completed (${reason}). Go back and try again.`, { status: 400, headers: h })\n }\n\n async function callback(request: Request, url: URL): Promise<Response> {\n const had = readCookies(request.headers.get('cookie'))\n const sealed = had.get(flowCookie)\n const flow = sealed ? await unseal<Flow>(sealed, await key) : null\n // The answer must be to a question THIS browser asked: without that, a\n // stranger's link could sign a person in to the stranger's account.\n if (flow === null || url.searchParams.get('state') !== flow.state) return failed(request, 'state_mismatch')\n const error = url.searchParams.get('error')\n if (error !== null) {\n // prompt=none and nobody signed in: not an error, just \"nobody\".\n if (error === 'login_required') return redirect(flow.returnTo, [writeCookie(flowCookie, '', attrs(request, 0))])\n return failed(request, error.slice(0, 60))\n }\n const code = url.searchParams.get('code')\n if (!code) return failed(request, 'no_code')\n const res = await token({\n grant_type: 'authorization_code',\n code,\n redirect_uri: `${originOf(request)}${basePath}/callback`,\n code_verifier: flow.verifier,\n })\n const s = res.ok ? await stored(res.body) : null\n if (s === null) return failed(request, 'exchange_failed')\n return redirect(flow.returnTo, [...(await write(request, s, had)), writeCookie(flowCookie, '', attrs(request, 0))])\n }\n\n async function logout(request: Request): Promise<Response> {\n const { had, value } = await read(request)\n if (value !== null) {\n // Best effort: the session is over here whether or not Cauthy heard.\n const body = new URLSearchParams({ client_id: clientId, token: value.rt })\n if (clientSecret) body.set('client_secret', clientSecret)\n await doFetch(`${issuer}/v1/oauth/revoke`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body,\n }).catch(() => undefined)\n orgCache.delete(value.at)\n }\n // Then through Cauthy, which ends its own session in this browser too:\n // otherwise the next \"sign in\" would walk straight back in.\n const q = new URLSearchParams({ client_id: clientId, post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` })\n return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had))\n }\n\n async function handler(request: Request): Promise<Response> {\n const url = new URL(request.url)\n if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response('Not found', { status: 404 })\n const route = url.pathname.slice(basePath.length).replace(/\\/+$/, '') || '/'\n const get = request.method === 'GET' || request.method === 'HEAD'\n\n if (route === '/login' && get) return login(request, url)\n if (route === '/callback' && get) return callback(request, url)\n if (route === '/logout' && (get || request.method === 'POST')) return logout(request)\n\n if (route === '/session' && get) {\n const r = await resolve(request)\n const h = new Headers()\n for (const c of r.cookies) h.append('set-cookie', c)\n return json({ session: r.session ? await toPublic(r.session) : null, status: r.status }, 200, h)\n }\n\n // For a page that found its session expired: renew, and go back to it.\n if (route === '/refresh' && get) {\n const r = await resolve(request)\n const back = localPath(url.searchParams.get('return_to'), afterSignIn)\n if (r.status === 'unauthenticated') return redirect(urls.signIn(back), r.cookies)\n if (r.status === 'expired') return new Response('Sign-in is unavailable right now. Try again in a moment.', { status: 503 })\n return redirect(back, r.cookies)\n }\n\n if (route === '/account' && get) {\n const q = new URLSearchParams({ client_id: clientId })\n const back = localPath(url.searchParams.get('return_to'), '')\n if (back !== '') q.set('return_to', `${originOf(request)}${back}`)\n const locale = url.searchParams.get('locale')\n if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set('ui_locales', locale)\n return redirect(`${issuer}/v1/oauth/account?${q}`)\n }\n return new Response('Not found', { status: 404 })\n }\n\n async function protect(request: Request) {\n const { value } = await read(request)\n if (value !== null && fresh(value)) return { session: toSession(value) }\n const url = new URL(request.url)\n const state = value === null ? 'none' : 'expired'\n // A page is sent to sign in, or to renew and come back. Anything else —\n // a fetch, a form post — is told, and its caller decides.\n const page = request.method === 'GET' && (request.headers.get('sec-fetch-mode') ?? 'navigate') === 'navigate' &&\n (request.headers.get('accept') ?? 'text/html').includes('text/html')\n if (!page) {\n return { response: json({ error: 'unauthenticated', session: state }, 401, new Headers({ 'x-cauthy-session': state })) }\n }\n const back = url.pathname + url.search\n const to = value === null ? urls.signIn(back) : `${basePath}/refresh?return_to=${encodeURIComponent(back)}`\n return { response: redirect(to) }\n }\n\n // The endpoint's public address: the application's origin when it was\n // given (a proxy may hide it from the request), with the request's path.\n const webhook: Cauthy['webhook'] = (request, o = {}) =>\n verifyWebhook(request, { issuer, url: o.url ?? `${originOf(request)}${new URL(request.url).pathname}`, jwks: o.jwks, now: o.now })\n\n return {\n handler,\n webhook,\n getSession,\n status,\n resolve,\n protect,\n organizations,\n toPublic,\n urls,\n options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName },\n }\n}\n","// An application's look, for Cauthy's hosted pages.\n//\n// Cauthy's pages — sign-in, sign-up, the account page — can take the colours,\n// corners, typeface and logo of the application that sends its people there,\n// so signing in reads as a page of the application. A theme is a short list of\n// values, which Cauthy checks one by one; it is never CSS.\n//\n// An application built with Stackmyth already has those values, as its\n// `--sm-*` tokens. `themeFromStackmyth` reads them out of its stylesheets.\n\n/** How an application looks on Cauthy's hosted pages. Every value is optional. */\nexport interface CauthyTheme {\n /** The page. Colours are `#rrggbb` or `rgba(r, g, b, a)`. */\n background?: string\n /** The card, and inputs. */\n surface?: string\n /** Headings and what is typed. */\n text?: string\n /** Paragraphs, labels, small print. */\n textMuted?: string\n border?: string\n /** The main button, and what is written on it. */\n primary?: string\n primaryText?: string\n /** Links, and the focus ring. */\n accent?: string\n error?: string\n errorBackground?: string\n success?: string\n successBackground?: string\n /** Corners of cards and inputs, in pixels (0–40). */\n radius?: number\n /** Corners of buttons, in pixels (0–40). */\n buttonRadius?: number\n /** A CSS font-family list. Cauthy loads no font files: put a system font after the application's own. */\n fontFamily?: string\n /** An image shown where \"Cauthy\" is, over https. */\n logoUrl?: string\n}\n\nexport interface StackmythThemeOptions {\n /**\n * The named theme to read, when the stylesheet keeps several under\n * `[data-theme=…]` (`\"vennet\"`). Without it only what applies to every\n * page (`:root`) is read.\n */\n theme?: string\n /** Which of the application's two modes to take. Default: `light`. */\n mode?: 'light' | 'dark'\n logoUrl?: string\n}\n\nexport interface ImportedTheme {\n theme: CauthyTheme\n /** Tokens that were there and could not be carried over, and why. */\n warnings: string[]\n}\n\nconst MAX_RADIUS = 40\n\n// ── Reading custom properties out of CSS ───────────────────────────────────\n\ninterface Declaration {\n name: string\n value: string\n /** The selectors and at-rules it sits inside, outermost first. */\n within: string[]\n}\n\nfunction declarations(css: string): Declaration[] {\n const text = css.replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n const out: Declaration[] = []\n const within: string[] = []\n let chunk = ''\n let quote = ''\n let depth = 0 // parentheses, so a `;` inside url(…) does not end anything\n const flush = () => {\n const m = /^\\s*(--[\\w-]+)\\s*:\\s*([\\s\\S]+?)\\s*$/.exec(chunk)\n if (m) out.push({ name: m[1], value: m[2].replace(/\\s+/g, ' ').replace(/\\s*!important$/, ''), within: [...within] })\n chunk = ''\n }\n for (const ch of text) {\n if (quote) {\n chunk += ch\n if (ch === quote) quote = ''\n continue\n }\n if (ch === '\"' || ch === \"'\") {\n quote = ch\n chunk += ch\n } else if (ch === '(') {\n depth++\n chunk += ch\n } else if (ch === ')') {\n depth = Math.max(0, depth - 1)\n chunk += ch\n } else if (ch === '{' && depth === 0) {\n within.push(chunk.trim().replace(/\\s+/g, ' '))\n chunk = ''\n } else if (ch === '}' && depth === 0) {\n flush()\n within.pop()\n } else if (ch === ';' && depth === 0) {\n flush()\n } else {\n chunk += ch\n }\n }\n return out\n}\n\nconst DARK = /prefers-color-scheme:\\s*dark|data-(mode|theme|color-scheme)=[\"']?dark|\\.dark\\b/i\nconst LIGHT = /prefers-color-scheme:\\s*light|data-(mode|theme|color-scheme)=[\"']?light|\\.light\\b/i\nconst NAMED = /data-theme=[\"']?([\\w-]+)/i\n\n/** Whether a declaration applies to the page being themed. */\nfunction applies(d: Declaration, o: Required<Pick<StackmythThemeOptions, 'mode'>> & { theme?: string }): boolean {\n for (const scope of d.within) {\n if (o.mode === 'light' && DARK.test(scope)) return false\n if (o.mode === 'dark' && LIGHT.test(scope)) return false\n const named = NAMED.exec(scope)\n if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false\n // Component-level rules (`.btn { --sm-… }`) are not the page's theme.\n if (!scope.startsWith('@') && !/:root|^html\\b|^body\\b|\\[data-|^\\*$|:host/.test(scope)) return false\n }\n return true\n}\n\n// ── Values ─────────────────────────────────────────────────────────────────\n\nfunction resolve(value: string, vars: Map<string, string>, seen = 0): string | null {\n if (seen > 16) return null\n let unresolved = false\n // Innermost var() first: fallbacks may hold another var().\n let out = value\n for (let guard = 0; guard < 32 && /var\\(/.test(out); guard++) {\n out = out.replace(/var\\(\\s*(--[\\w-]+)\\s*(?:,\\s*([^()]*(?:\\([^()]*\\)[^()]*)*))?\\)/, (_m, name: string, fallback?: string) => {\n const known = vars.get(name)\n const next = known !== undefined ? resolve(known, vars, seen + 1) : fallback !== undefined ? resolve(fallback.trim(), vars, seen + 1) : null\n if (next === null) unresolved = true\n return next ?? ''\n })\n if (unresolved) return null\n }\n return /var\\(/.test(out) ? null : out.trim()\n}\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n))\nconst hex2 = (n: number) => Math.round(clamp01(n) * 255).toString(16).padStart(2, '0')\nconst gamma = (c: number) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055)\n\nfunction out(r: number, g: number, b: number, a = 1): string {\n if (a >= 1) return `#${hex2(r)}${hex2(g)}${hex2(b)}`\n return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${Math.round(clamp01(a) * 1000) / 1000})`\n}\n\nconst alpha = (s: string | undefined) => (s === undefined ? 1 : s.endsWith('%') ? parseFloat(s) / 100 : parseFloat(s))\n\nconst KEYWORDS: Record<string, string> = { white: '#ffffff', black: '#000000' }\n\n/** A colour as Cauthy takes it — `#rrggbb` or `rgba(…)` — or null when it is none it can read. */\nexport function toColor(value: string): string | null {\n const v = value.trim().toLowerCase()\n if (KEYWORDS[v]) return KEYWORDS[v]\n if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v)) return v\n let m = /^rgba?\\(\\s*([\\d.]+%?)[\\s,]+([\\d.]+%?)[\\s,]+([\\d.]+%?)(?:\\s*[,/]\\s*([\\d.]+%?))?\\s*\\)$/.exec(v)\n if (m) {\n const ch = (s: string) => (s.endsWith('%') ? parseFloat(s) / 100 : parseFloat(s) / 255)\n return out(ch(m[1]), ch(m[2]), ch(m[3]), alpha(m[4]))\n }\n m = /^hsla?\\(\\s*([\\d.]+)(?:deg)?[\\s,]+([\\d.]+)%[\\s,]+([\\d.]+)%(?:\\s*[,/]\\s*([\\d.]+%?))?\\s*\\)$/.exec(v)\n if (m) {\n const h = ((parseFloat(m[1]) % 360) + 360) % 360\n const s = parseFloat(m[2]) / 100\n const l = parseFloat(m[3]) / 100\n const f = (n: number) => {\n const k = (n + h / 30) % 12\n return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1))\n }\n return out(f(0), f(8), f(4), alpha(m[4]))\n }\n m = /^oklch\\(\\s*([\\d.]+%?)\\s+([\\d.]+%?)\\s+([\\d.]+)(?:deg)?(?:\\s*\\/\\s*([\\d.]+%?))?\\s*\\)$/.exec(v)\n if (m) {\n const L = m[1].endsWith('%') ? parseFloat(m[1]) / 100 : parseFloat(m[1])\n const C = m[2].endsWith('%') ? (parseFloat(m[2]) / 100) * 0.4 : parseFloat(m[2])\n const H = (parseFloat(m[3]) * Math.PI) / 180\n const a = C * Math.cos(H)\n const b = C * Math.sin(H)\n const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3)\n const mm = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3)\n const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3)\n return out(\n gamma(4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s),\n gamma(-1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s),\n gamma(-0.0041960863 * l - 0.7034186147 * mm + 1.707614701 * s),\n alpha(m[4]),\n )\n }\n return null\n}\n\n/** A length in pixels, within what Cauthy takes, or null. */\nfunction toRadius(value: string): number | null {\n const m = /^([\\d.]+)(px|rem|em)?$/.exec(value.trim())\n if (!m) return null\n const px = parseFloat(m[1]) * (m[2] === 'rem' || m[2] === 'em' ? 16 : 1)\n return Number.isFinite(px) ? Math.min(MAX_RADIUS, Math.max(0, Math.round(px))) : null\n}\n\n/** A font list as Cauthy takes it, or null. */\nfunction toFont(value: string): string | null {\n const v = value.trim()\n return /^[A-Za-z0-9 ,'\"_-]{1,200}$/.test(v) && (v.match(/\"/g)?.length ?? 0) % 2 === 0 && (v.match(/'/g)?.length ?? 0) % 2 === 0 ? v : null\n}\n\n// ── Stackmyth ──────────────────────────────────────────────────────────────\n\n/** Which Stackmyth token each value of a theme is read from, first found wins. */\nconst COLORS: [keyof CauthyTheme, string[]][] = [\n ['background', ['--sm-bg-surface', '--sm-surface']],\n ['surface', ['--sm-bg-surface-raised', '--sm-surface-raised']],\n ['text', ['--sm-text-primary', '--sm-surface-text']],\n ['textMuted', ['--sm-text-secondary']],\n ['border', ['--sm-border-default', '--sm-surface-border']],\n ['primary', ['--sm-primary', '--sm-bg-interactive']],\n ['primaryText', ['--sm-primary-text', '--sm-text-inverse']],\n ['accent', ['--sm-text-brand', '--sm-border-focus', '--sm-accent']],\n ['error', ['--sm-error-soft-text', '--sm-error']],\n ['errorBackground', ['--sm-error-soft']],\n ['success', ['--sm-success-soft-text', '--sm-success']],\n ['successBackground', ['--sm-success-soft']],\n]\n\n/**\n * Reads an application's Stackmyth theme out of its stylesheets.\n *\n * Give it the CSS that declares the tokens, in the order the application\n * loads it — Stackmyth's own (`@stackmyth/core/core.vars.css`, or a theme\n * file) and then the application's brand file that re-points them. Tokens\n * that point at the application's own variables are followed. The\n * application's dark mode is left out unless asked for.\n */\nexport function themeFromStackmyth(css: string | string[], options: StackmythThemeOptions = {}): ImportedTheme {\n const o = { mode: options.mode ?? 'light', theme: options.theme }\n const vars = new Map<string, string>()\n for (const sheet of Array.isArray(css) ? css : [css]) {\n for (const d of declarations(sheet)) if (applies(d, o)) vars.set(d.name, d.value)\n }\n const theme: CauthyTheme = {}\n const warnings: string[] = []\n\n const read = <T>(tokens: string[], to: (v: string) => T | null, what: string): T | undefined => {\n for (const token of tokens) {\n const raw = vars.get(token)\n if (raw === undefined) continue\n const value = resolve(raw, vars)\n const got = value === null ? null : to(value)\n if (got !== null) return got\n warnings.push(`${token}: \"${raw}\" is not ${what} Cauthy can take`)\n }\n return undefined\n }\n\n for (const [field, tokens] of COLORS) {\n const c = read(tokens, toColor, 'a colour')\n if (c !== undefined) (theme as Record<string, unknown>)[field] = c\n }\n const radius = read(['--sm-radius-container', '--sm-radius-md'], toRadius, 'a length')\n if (radius !== undefined) theme.radius = radius\n const button = read(['--sm-radius-interactive', '--sm-radius-sm'], toRadius, 'a length')\n if (button !== undefined) theme.buttonRadius = button\n const font = read(['--sm-font-family'], toFont, 'a font list')\n if (font !== undefined) theme.fontFamily = font\n if (options.logoUrl) theme.logoUrl = options.logoUrl\n\n if (vars.size === 0) warnings.push('no custom properties were found: is this the stylesheet that declares the --sm-* tokens?')\n else if (![...vars.keys()].some((k) => k.startsWith('--sm-'))) warnings.push('no --sm-* token was found in these stylesheets')\n return { theme, warnings }\n}\n\n// ── Sending it to Cauthy ───────────────────────────────────────────────────\n\n/** A theme as Cauthy's API names its values. */\nexport function toWire(theme: CauthyTheme): Record<string, string | number> {\n const names: Record<keyof CauthyTheme, string> = {\n background: 'background', surface: 'surface', text: 'text', textMuted: 'text_muted', border: 'border',\n primary: 'primary', primaryText: 'primary_text', accent: 'accent', error: 'error', errorBackground: 'error_background',\n success: 'success', successBackground: 'success_background', radius: 'radius', buttonRadius: 'button_radius',\n fontFamily: 'font_family', logoUrl: 'logo_url',\n }\n const wire: Record<string, string | number> = {}\n for (const [k, v] of Object.entries(theme)) if (v !== undefined && v !== '') wire[names[k as keyof CauthyTheme]] = v as string | number\n return wire\n}\n\nexport interface PushThemeOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The application's client id. Default: `CAUTHY_CLIENT_ID`. */\n clientId?: string\n /** The workspace's slug. Default: `CAUTHY_TENANT`. */\n tenant?: string\n /**\n * A personal access token with `oauth_clients:manage`. Default:\n * `CAUTHY_TOKEN`. It belongs in the deploy pipeline, never in the application.\n */\n token?: string\n theme: CauthyTheme\n fetch?: typeof fetch\n}\n\n/**\n * Gives the application its look on Cauthy's pages. It replaces the theme: a\n * value left out goes back to Cauthy's own. Run it when the brand changes —\n * from a script or the deploy — not on every request.\n */\nexport async function pushTheme(o: PushThemeOptions): Promise<void> {\n const env = (n: string) => (typeof process !== 'undefined' ? process.env?.[n] : undefined)\n const issuer = (o.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n const clientId = o.clientId ?? env('CAUTHY_CLIENT_ID') ?? ''\n const tenant = o.tenant ?? env('CAUTHY_TENANT') ?? ''\n const token = o.token ?? env('CAUTHY_TOKEN') ?? ''\n for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {\n if (v === '') throw new Error(`cauthy-auth: ${name} is required to push a theme`)\n }\n const res = await (o.fetch ?? fetch)(`${issuer}/v1/oauth/clients/${encodeURIComponent(clientId)}/theme`, {\n method: 'PUT',\n headers: { authorization: `Bearer ${token}`, 'x-tenant-key': tenant, 'content-type': 'application/json' },\n body: JSON.stringify(toWire(o.theme)),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { errors?: { message?: string }[] } | null\n throw new Error(`cauthy-auth: Cauthy refused the theme (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`)\n }\n}\n","// What a person may do in an organization.\n//\n// When a system keeps its catalog of roles in Cauthy, each of a person's\n// organizations arrives with what their role — and anything given or denied to\n// them apart from it — lets them do. Checking is a lookup in that list: no\n// call to Cauthy, on the server or in the browser.\n\n/** The part of an organization that says what its member may do. */\nexport interface Permitted {\n /** \"resource:action\" entries the person is allowed; \"*\" is any. Absent when the system keeps no catalog in Cauthy. */\n permissions?: string[]\n /** Entries the person is denied whatever else allows them. */\n denied?: string[]\n}\n\nfunction matches(entry: string, resource: string, action: string): boolean {\n // The resource may itself hold a colon: the action is what follows the last one.\n const i = entry.lastIndexOf(':')\n if (i < 0) return false\n const r = entry.slice(0, i)\n const a = entry.slice(i + 1)\n return (r === '*' || r === resource) && (a === '*' || a === action)\n}\n\n/**\n * Whether the person may do an action on a resource in an organization:\n * allowed by some entry, and denied by none. A denial wins.\n *\n * An organization of a system that keeps no catalog in Cauthy carries no\n * permissions, and the answer is `false`: decide by its `role` instead.\n *\n * if (!can(organization, 'invoicing', 'write')) return forbidden()\n */\nexport function can(organization: Permitted | null | undefined, resource: string, action: string): boolean {\n if (!organization?.permissions) return false\n if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false\n return organization.permissions.some((p) => matches(p, resource, action))\n}\n\n// ── A system's catalog of roles ────────────────────────────────────────────\n\n/** Something a role may do: an action on a resource, in organizations of a kind (\"*\", the default, is every kind). */\nexport interface PolicyPermission {\n kind?: string\n resource: string\n action: string\n}\n\nexport interface PolicyRole {\n role: string\n name?: string\n description?: string\n /** Roles whose permissions this one has as well. */\n inherits?: string[]\n permissions?: PolicyPermission[]\n}\n\nexport interface PushPolicyOptions {\n /** The system whose catalog this is. Default: `CAUTHY_SYSTEM`. */\n system?: string\n roles: PolicyRole[]\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The workspace's slug. Default: `CAUTHY_TENANT`. */\n tenant?: string\n /** A personal access token with `organizations:manage`. Default: `CAUTHY_TOKEN`. */\n token?: string\n fetch?: typeof fetch\n}\n\n/**\n * Gives Cauthy a system's catalog of roles, replacing the one it has. It is\n * the way to load a first catalog from what a system already had; from then on\n * the catalog is Cauthy's, edited in its dashboard, and pushing again would\n * overwrite what was edited there.\n */\nexport async function pushPolicy(o: PushPolicyOptions): Promise<void> {\n const env = (n: string) => (typeof process !== 'undefined' ? process.env?.[n] : undefined)\n const issuer = (o.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n const system = o.system ?? env('CAUTHY_SYSTEM') ?? ''\n const tenant = o.tenant ?? env('CAUTHY_TENANT') ?? ''\n const token = o.token ?? env('CAUTHY_TOKEN') ?? ''\n for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {\n if (v === '') throw new Error(`cauthy-auth: ${name} is required to push a policy`)\n }\n const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {\n method: 'PUT',\n headers: { authorization: `Bearer ${token}`, 'x-tenant-key': tenant, 'content-type': 'application/json' },\n body: JSON.stringify({ roles: o.roles }),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { errors?: { message?: string }[] } | null\n throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`)\n }\n}\n"],"mappings":"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","names":["out","env","resolve","out","env","env"]}
@@ -0,0 +1,52 @@
1
+ /** The part of an organization that says what its member may do. */
2
+ interface Permitted {
3
+ /** "resource:action" entries the person is allowed; "*" is any. Absent when the system keeps no catalog in Cauthy. */
4
+ permissions?: string[];
5
+ /** Entries the person is denied whatever else allows them. */
6
+ denied?: string[];
7
+ }
8
+ /**
9
+ * Whether the person may do an action on a resource in an organization:
10
+ * allowed by some entry, and denied by none. A denial wins.
11
+ *
12
+ * An organization of a system that keeps no catalog in Cauthy carries no
13
+ * permissions, and the answer is `false`: decide by its `role` instead.
14
+ *
15
+ * if (!can(organization, 'invoicing', 'write')) return forbidden()
16
+ */
17
+ declare function can(organization: Permitted | null | undefined, resource: string, action: string): boolean;
18
+ /** Something a role may do: an action on a resource, in organizations of a kind ("*", the default, is every kind). */
19
+ interface PolicyPermission {
20
+ kind?: string;
21
+ resource: string;
22
+ action: string;
23
+ }
24
+ interface PolicyRole {
25
+ role: string;
26
+ name?: string;
27
+ description?: string;
28
+ /** Roles whose permissions this one has as well. */
29
+ inherits?: string[];
30
+ permissions?: PolicyPermission[];
31
+ }
32
+ interface PushPolicyOptions {
33
+ /** The system whose catalog this is. Default: `CAUTHY_SYSTEM`. */
34
+ system?: string;
35
+ roles: PolicyRole[];
36
+ /** Cauthy's address. Default: `CAUTHY_ISSUER`. */
37
+ issuer?: string;
38
+ /** The workspace's slug. Default: `CAUTHY_TENANT`. */
39
+ tenant?: string;
40
+ /** A personal access token with `organizations:manage`. Default: `CAUTHY_TOKEN`. */
41
+ token?: string;
42
+ fetch?: typeof fetch;
43
+ }
44
+ /**
45
+ * Gives Cauthy a system's catalog of roles, replacing the one it has. It is
46
+ * the way to load a first catalog from what a system already had; from then on
47
+ * the catalog is Cauthy's, edited in its dashboard, and pushing again would
48
+ * overwrite what was edited there.
49
+ */
50
+ declare function pushPolicy(o: PushPolicyOptions): Promise<void>;
51
+
52
+ export { type Permitted as P, type PolicyPermission as a, type PolicyRole as b, type PushPolicyOptions as c, can as d, pushPolicy as p };
@@ -0,0 +1,52 @@
1
+ /** The part of an organization that says what its member may do. */
2
+ interface Permitted {
3
+ /** "resource:action" entries the person is allowed; "*" is any. Absent when the system keeps no catalog in Cauthy. */
4
+ permissions?: string[];
5
+ /** Entries the person is denied whatever else allows them. */
6
+ denied?: string[];
7
+ }
8
+ /**
9
+ * Whether the person may do an action on a resource in an organization:
10
+ * allowed by some entry, and denied by none. A denial wins.
11
+ *
12
+ * An organization of a system that keeps no catalog in Cauthy carries no
13
+ * permissions, and the answer is `false`: decide by its `role` instead.
14
+ *
15
+ * if (!can(organization, 'invoicing', 'write')) return forbidden()
16
+ */
17
+ declare function can(organization: Permitted | null | undefined, resource: string, action: string): boolean;
18
+ /** Something a role may do: an action on a resource, in organizations of a kind ("*", the default, is every kind). */
19
+ interface PolicyPermission {
20
+ kind?: string;
21
+ resource: string;
22
+ action: string;
23
+ }
24
+ interface PolicyRole {
25
+ role: string;
26
+ name?: string;
27
+ description?: string;
28
+ /** Roles whose permissions this one has as well. */
29
+ inherits?: string[];
30
+ permissions?: PolicyPermission[];
31
+ }
32
+ interface PushPolicyOptions {
33
+ /** The system whose catalog this is. Default: `CAUTHY_SYSTEM`. */
34
+ system?: string;
35
+ roles: PolicyRole[];
36
+ /** Cauthy's address. Default: `CAUTHY_ISSUER`. */
37
+ issuer?: string;
38
+ /** The workspace's slug. Default: `CAUTHY_TENANT`. */
39
+ tenant?: string;
40
+ /** A personal access token with `organizations:manage`. Default: `CAUTHY_TOKEN`. */
41
+ token?: string;
42
+ fetch?: typeof fetch;
43
+ }
44
+ /**
45
+ * Gives Cauthy a system's catalog of roles, replacing the one it has. It is
46
+ * the way to load a first catalog from what a system already had; from then on
47
+ * the catalog is Cauthy's, edited in its dashboard, and pushing again would
48
+ * overwrite what was edited there.
49
+ */
50
+ declare function pushPolicy(o: PushPolicyOptions): Promise<void>;
51
+
52
+ export { type Permitted as P, type PolicyPermission as a, type PolicyRole as b, type PushPolicyOptions as c, can as d, pushPolicy as p };
package/dist/react.d.mts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { P as PublicSession } from './types-BwCjBn-K.mjs';
4
- export { J as JsonValue, b as SessionOrganization, d as SessionUser } from './types-BwCjBn-K.mjs';
3
+ import { P as PublicSession } from './types-CYmUVWzY.mjs';
4
+ export { J as JsonValue, b as SessionOrganization, d as SessionUser } from './types-CYmUVWzY.mjs';
5
+ export { d as can } from './permissions-B-a3eN0h.mjs';
5
6
 
6
7
  interface SessionContext {
7
8
  /** The signed-in person and their organizations, or null. */
package/dist/react.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { P as PublicSession } from './types-BwCjBn-K.js';
4
- export { J as JsonValue, b as SessionOrganization, d as SessionUser } from './types-BwCjBn-K.js';
3
+ import { P as PublicSession } from './types-CYmUVWzY.js';
4
+ export { J as JsonValue, b as SessionOrganization, d as SessionUser } from './types-CYmUVWzY.js';
5
+ export { d as can } from './permissions-B-a3eN0h.js';
5
6
 
6
7
  interface SessionContext {
7
8
  /** The signed-in person and their organizations, or null. */
package/dist/react.js CHANGED
@@ -21,10 +21,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var react_exports = {};
22
22
  __export(react_exports, {
23
23
  CauthyProvider: () => CauthyProvider,
24
+ can: () => can,
24
25
  useSession: () => useSession
25
26
  });
26
27
  module.exports = __toCommonJS(react_exports);
27
28
  var import_react = require("react");
29
+
30
+ // src/permissions.ts
31
+ function matches(entry, resource, action) {
32
+ const i = entry.lastIndexOf(":");
33
+ if (i < 0) return false;
34
+ const r = entry.slice(0, i);
35
+ const a = entry.slice(i + 1);
36
+ return (r === "*" || r === resource) && (a === "*" || a === action);
37
+ }
38
+ function can(organization, resource, action) {
39
+ if (!organization?.permissions) return false;
40
+ if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
41
+ return organization.permissions.some((p) => matches(p, resource, action));
42
+ }
43
+
44
+ // src/react.tsx
28
45
  var import_jsx_runtime = require("react/jsx-runtime");
29
46
  var Ctx = (0, import_react.createContext)(null);
30
47
  var AHEAD_MS = 6e4;
@@ -102,6 +119,7 @@ function useSession() {
102
119
  // Annotate the CommonJS export names for ESM import in node:
103
120
  0 && (module.exports = {
104
121
  CauthyProvider,
122
+ can,
105
123
  useSession
106
124
  });
107
125
  //# sourceMappingURL=react.js.map
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react.tsx"],"sourcesContent":["// React: who is signed in, in the browser.\n//\n// <CauthyProvider> // once, at the root\n// <App />\n// </CauthyProvider>\n//\n// const { session, status, signIn, signOut } = useSession()\n//\n// The browser never holds a token: it asks the application's own server\n// (`/api/auth/session`), which answers who is signed in and their\n// organizations. The session is kept current by asking again a little before\n// it expires and when the window comes back into view.\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport type { PublicSession } from './types'\n\nexport type { JsonValue, PublicSession, SessionOrganization, SessionUser } from './types'\n\nexport interface SessionContext {\n /** The signed-in person and their organizations, or null. */\n session: PublicSession | null\n status: 'loading' | 'authenticated' | 'unauthenticated'\n /** Sends the browser to sign in, and back to `returnTo` (default: where it is now). */\n signIn(options?: { returnTo?: string; locale?: string; prompt?: 'login' }): void\n /** Signs out of this application and of Cauthy in this browser. */\n signOut(): void\n /** Asks the server again who is signed in. */\n refresh(): Promise<PublicSession | null>\n /** Where the person looks after their account: password, second step, passkeys. */\n accountUrl(returnTo?: string): string\n}\n\nexport interface CauthyProviderProps {\n children: ReactNode\n /** Where the server mounted the routes. Default: `/api/auth`. */\n basePath?: string\n /**\n * The session the server already read while rendering (SSR): the first\n * paint is then right, with no \"loading\". `null` means \"nobody\"; leave it\n * out when the server did not look.\n */\n initialSession?: PublicSession | null\n}\n\nconst Ctx = createContext<SessionContext | null>(null)\n\n/** Ask again this long before the session's token runs out. */\nconst AHEAD_MS = 60_000\n\nexport function CauthyProvider({ children, basePath = '/api/auth', initialSession }: CauthyProviderProps) {\n const base = basePath.replace(/\\/+$/, '')\n const [session, setSession] = useState<PublicSession | null>(initialSession ?? null)\n const [status, setStatus] = useState<SessionContext['status']>(\n initialSession === undefined ? 'loading' : initialSession === null ? 'unauthenticated' : 'authenticated',\n )\n const asking = useRef<Promise<PublicSession | null> | null>(null)\n\n const refresh = useCallback((): Promise<PublicSession | null> => {\n asking.current ??= (async () => {\n try {\n const res = await fetch(`${base}/session`, { credentials: 'same-origin', headers: { accept: 'application/json' } })\n if (!res.ok) throw new Error(String(res.status))\n const body = (await res.json()) as { session: PublicSession | null; status: string }\n // \"expired\" is the server unable to reach Cauthy: what is on screen\n // stays, and the next ask settles it.\n if (body.status !== 'expired') {\n setSession(body.session)\n setStatus(body.session ? 'authenticated' : 'unauthenticated')\n }\n return body.session\n } catch {\n setStatus((s) => (s === 'loading' ? 'unauthenticated' : s))\n return null\n } finally {\n asking.current = null\n }\n })()\n return asking.current\n }, [base])\n\n useEffect(() => {\n if (initialSession === undefined) void refresh()\n }, [initialSession, refresh])\n\n useEffect(() => {\n if (!session) return\n const wait = Math.max(new Date(session.expiresAt).getTime() - Date.now() - AHEAD_MS, 5_000)\n const timer = setTimeout(() => void refresh(), wait)\n return () => clearTimeout(timer)\n }, [session, refresh])\n\n useEffect(() => {\n const onVisible = () => {\n if (document.visibilityState === 'visible') void refresh()\n }\n document.addEventListener('visibilitychange', onVisible)\n return () => document.removeEventListener('visibilitychange', onVisible)\n }, [refresh])\n\n const value = useMemo<SessionContext>(\n () => ({\n session,\n status,\n refresh,\n signIn(options = {}) {\n const q = new URLSearchParams({ return_to: options.returnTo ?? window.location.pathname + window.location.search })\n if (options.locale) q.set('locale', options.locale)\n if (options.prompt) q.set('prompt', options.prompt)\n window.location.assign(`${base}/login?${q}`)\n },\n signOut() {\n window.location.assign(`${base}/logout`)\n },\n accountUrl(returnTo) {\n const back = returnTo ?? (typeof window === 'undefined' ? '/' : window.location.pathname + window.location.search)\n return `${base}/account?return_to=${encodeURIComponent(back)}`\n },\n }),\n [session, status, refresh, base],\n )\n return <Ctx.Provider value={value}>{children}</Ctx.Provider>\n}\n\nexport function useSession(): SessionContext {\n const ctx = useContext(Ctx)\n if (ctx === null) throw new Error('cauthy-auth: useSession needs <CauthyProvider> above it')\n return ctx\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,mBAA6G;AA2GpG;AA5ET,IAAM,UAAM,4BAAqC,IAAI;AAGrD,IAAM,WAAW;AAEV,SAAS,eAAe,EAAE,UAAU,WAAW,aAAa,eAAe,GAAwB;AACxG,QAAM,OAAO,SAAS,QAAQ,QAAQ,EAAE;AACxC,QAAM,CAAC,SAAS,UAAU,QAAI,uBAA+B,kBAAkB,IAAI;AACnF,QAAM,CAAC,QAAQ,SAAS,QAAI;AAAA,IAC1B,mBAAmB,SAAY,YAAY,mBAAmB,OAAO,oBAAoB;AAAA,EAC3F;AACA,QAAM,aAAS,qBAA6C,IAAI;AAEhE,QAAM,cAAU,0BAAY,MAAqC;AAC/D,WAAO,aAAa,YAAY;AAC9B,UAAI;AACF,cAAM,MAAM,MAAM,MAAM,GAAG,IAAI,YAAY,EAAE,aAAa,eAAe,SAAS,EAAE,QAAQ,mBAAmB,EAAE,CAAC;AAClH,YAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,OAAO,IAAI,MAAM,CAAC;AAC/C,cAAM,OAAQ,MAAM,IAAI,KAAK;AAG7B,YAAI,KAAK,WAAW,WAAW;AAC7B,qBAAW,KAAK,OAAO;AACvB,oBAAU,KAAK,UAAU,kBAAkB,iBAAiB;AAAA,QAC9D;AACA,eAAO,KAAK;AAAA,MACd,QAAQ;AACN,kBAAU,CAAC,MAAO,MAAM,YAAY,oBAAoB,CAAE;AAC1D,eAAO;AAAA,MACT,UAAE;AACA,eAAO,UAAU;AAAA,MACnB;AAAA,IACF,GAAG;AACH,WAAO,OAAO;AAAA,EAChB,GAAG,CAAC,IAAI,CAAC;AAET,8BAAU,MAAM;AACd,QAAI,mBAAmB,OAAW,MAAK,QAAQ;AAAA,EACjD,GAAG,CAAC,gBAAgB,OAAO,CAAC;AAE5B,8BAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,KAAK,IAAI,IAAI,KAAK,QAAQ,SAAS,EAAE,QAAQ,IAAI,KAAK,IAAI,IAAI,UAAU,GAAK;AAC1F,UAAM,QAAQ,WAAW,MAAM,KAAK,QAAQ,GAAG,IAAI;AACnD,WAAO,MAAM,aAAa,KAAK;AAAA,EACjC,GAAG,CAAC,SAAS,OAAO,CAAC;AAErB,8BAAU,MAAM;AACd,UAAM,YAAY,MAAM;AACtB,UAAI,SAAS,oBAAoB,UAAW,MAAK,QAAQ;AAAA,IAC3D;AACA,aAAS,iBAAiB,oBAAoB,SAAS;AACvD,WAAO,MAAM,SAAS,oBAAoB,oBAAoB,SAAS;AAAA,EACzE,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,YAAQ;AAAA,IACZ,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,UAAU,CAAC,GAAG;AACnB,cAAM,IAAI,IAAI,gBAAgB,EAAE,WAAW,QAAQ,YAAY,OAAO,SAAS,WAAW,OAAO,SAAS,OAAO,CAAC;AAClH,YAAI,QAAQ,OAAQ,GAAE,IAAI,UAAU,QAAQ,MAAM;AAClD,YAAI,QAAQ,OAAQ,GAAE,IAAI,UAAU,QAAQ,MAAM;AAClD,eAAO,SAAS,OAAO,GAAG,IAAI,UAAU,CAAC,EAAE;AAAA,MAC7C;AAAA,MACA,UAAU;AACR,eAAO,SAAS,OAAO,GAAG,IAAI,SAAS;AAAA,MACzC;AAAA,MACA,WAAW,UAAU;AACnB,cAAM,OAAO,aAAa,OAAO,WAAW,cAAc,MAAM,OAAO,SAAS,WAAW,OAAO,SAAS;AAC3G,eAAO,GAAG,IAAI,sBAAsB,mBAAmB,IAAI,CAAC;AAAA,MAC9D;AAAA,IACF;AAAA,IACA,CAAC,SAAS,QAAQ,SAAS,IAAI;AAAA,EACjC;AACA,SAAO,4CAAC,IAAI,UAAJ,EAAa,OAAe,UAAS;AAC/C;AAEO,SAAS,aAA6B;AAC3C,QAAM,UAAM,yBAAW,GAAG;AAC1B,MAAI,QAAQ,KAAM,OAAM,IAAI,MAAM,yDAAyD;AAC3F,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/react.tsx","../src/permissions.ts"],"sourcesContent":["// React: who is signed in, in the browser.\n//\n// <CauthyProvider> // once, at the root\n// <App />\n// </CauthyProvider>\n//\n// const { session, status, signIn, signOut } = useSession()\n//\n// The browser never holds a token: it asks the application's own server\n// (`/api/auth/session`), which answers who is signed in and their\n// organizations. The session is kept current by asking again a little before\n// it expires and when the window comes back into view.\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport type { PublicSession } from './types'\n\nexport type { JsonValue, PublicSession, SessionOrganization, SessionUser } from './types'\nexport { can } from './permissions'\n\nexport interface SessionContext {\n /** The signed-in person and their organizations, or null. */\n session: PublicSession | null\n status: 'loading' | 'authenticated' | 'unauthenticated'\n /** Sends the browser to sign in, and back to `returnTo` (default: where it is now). */\n signIn(options?: { returnTo?: string; locale?: string; prompt?: 'login' }): void\n /** Signs out of this application and of Cauthy in this browser. */\n signOut(): void\n /** Asks the server again who is signed in. */\n refresh(): Promise<PublicSession | null>\n /** Where the person looks after their account: password, second step, passkeys. */\n accountUrl(returnTo?: string): string\n}\n\nexport interface CauthyProviderProps {\n children: ReactNode\n /** Where the server mounted the routes. Default: `/api/auth`. */\n basePath?: string\n /**\n * The session the server already read while rendering (SSR): the first\n * paint is then right, with no \"loading\". `null` means \"nobody\"; leave it\n * out when the server did not look.\n */\n initialSession?: PublicSession | null\n}\n\nconst Ctx = createContext<SessionContext | null>(null)\n\n/** Ask again this long before the session's token runs out. */\nconst AHEAD_MS = 60_000\n\nexport function CauthyProvider({ children, basePath = '/api/auth', initialSession }: CauthyProviderProps) {\n const base = basePath.replace(/\\/+$/, '')\n const [session, setSession] = useState<PublicSession | null>(initialSession ?? null)\n const [status, setStatus] = useState<SessionContext['status']>(\n initialSession === undefined ? 'loading' : initialSession === null ? 'unauthenticated' : 'authenticated',\n )\n const asking = useRef<Promise<PublicSession | null> | null>(null)\n\n const refresh = useCallback((): Promise<PublicSession | null> => {\n asking.current ??= (async () => {\n try {\n const res = await fetch(`${base}/session`, { credentials: 'same-origin', headers: { accept: 'application/json' } })\n if (!res.ok) throw new Error(String(res.status))\n const body = (await res.json()) as { session: PublicSession | null; status: string }\n // \"expired\" is the server unable to reach Cauthy: what is on screen\n // stays, and the next ask settles it.\n if (body.status !== 'expired') {\n setSession(body.session)\n setStatus(body.session ? 'authenticated' : 'unauthenticated')\n }\n return body.session\n } catch {\n setStatus((s) => (s === 'loading' ? 'unauthenticated' : s))\n return null\n } finally {\n asking.current = null\n }\n })()\n return asking.current\n }, [base])\n\n useEffect(() => {\n if (initialSession === undefined) void refresh()\n }, [initialSession, refresh])\n\n useEffect(() => {\n if (!session) return\n const wait = Math.max(new Date(session.expiresAt).getTime() - Date.now() - AHEAD_MS, 5_000)\n const timer = setTimeout(() => void refresh(), wait)\n return () => clearTimeout(timer)\n }, [session, refresh])\n\n useEffect(() => {\n const onVisible = () => {\n if (document.visibilityState === 'visible') void refresh()\n }\n document.addEventListener('visibilitychange', onVisible)\n return () => document.removeEventListener('visibilitychange', onVisible)\n }, [refresh])\n\n const value = useMemo<SessionContext>(\n () => ({\n session,\n status,\n refresh,\n signIn(options = {}) {\n const q = new URLSearchParams({ return_to: options.returnTo ?? window.location.pathname + window.location.search })\n if (options.locale) q.set('locale', options.locale)\n if (options.prompt) q.set('prompt', options.prompt)\n window.location.assign(`${base}/login?${q}`)\n },\n signOut() {\n window.location.assign(`${base}/logout`)\n },\n accountUrl(returnTo) {\n const back = returnTo ?? (typeof window === 'undefined' ? '/' : window.location.pathname + window.location.search)\n return `${base}/account?return_to=${encodeURIComponent(back)}`\n },\n }),\n [session, status, refresh, base],\n )\n return <Ctx.Provider value={value}>{children}</Ctx.Provider>\n}\n\nexport function useSession(): SessionContext {\n const ctx = useContext(Ctx)\n if (ctx === null) throw new Error('cauthy-auth: useSession needs <CauthyProvider> above it')\n return ctx\n}\n","// What a person may do in an organization.\n//\n// When a system keeps its catalog of roles in Cauthy, each of a person's\n// organizations arrives with what their role — and anything given or denied to\n// them apart from it — lets them do. Checking is a lookup in that list: no\n// call to Cauthy, on the server or in the browser.\n\n/** The part of an organization that says what its member may do. */\nexport interface Permitted {\n /** \"resource:action\" entries the person is allowed; \"*\" is any. Absent when the system keeps no catalog in Cauthy. */\n permissions?: string[]\n /** Entries the person is denied whatever else allows them. */\n denied?: string[]\n}\n\nfunction matches(entry: string, resource: string, action: string): boolean {\n // The resource may itself hold a colon: the action is what follows the last one.\n const i = entry.lastIndexOf(':')\n if (i < 0) return false\n const r = entry.slice(0, i)\n const a = entry.slice(i + 1)\n return (r === '*' || r === resource) && (a === '*' || a === action)\n}\n\n/**\n * Whether the person may do an action on a resource in an organization:\n * allowed by some entry, and denied by none. A denial wins.\n *\n * An organization of a system that keeps no catalog in Cauthy carries no\n * permissions, and the answer is `false`: decide by its `role` instead.\n *\n * if (!can(organization, 'invoicing', 'write')) return forbidden()\n */\nexport function can(organization: Permitted | null | undefined, resource: string, action: string): boolean {\n if (!organization?.permissions) return false\n if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false\n return organization.permissions.some((p) => matches(p, resource, action))\n}\n\n// ── A system's catalog of roles ────────────────────────────────────────────\n\n/** Something a role may do: an action on a resource, in organizations of a kind (\"*\", the default, is every kind). */\nexport interface PolicyPermission {\n kind?: string\n resource: string\n action: string\n}\n\nexport interface PolicyRole {\n role: string\n name?: string\n description?: string\n /** Roles whose permissions this one has as well. */\n inherits?: string[]\n permissions?: PolicyPermission[]\n}\n\nexport interface PushPolicyOptions {\n /** The system whose catalog this is. Default: `CAUTHY_SYSTEM`. */\n system?: string\n roles: PolicyRole[]\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The workspace's slug. Default: `CAUTHY_TENANT`. */\n tenant?: string\n /** A personal access token with `organizations:manage`. Default: `CAUTHY_TOKEN`. */\n token?: string\n fetch?: typeof fetch\n}\n\n/**\n * Gives Cauthy a system's catalog of roles, replacing the one it has. It is\n * the way to load a first catalog from what a system already had; from then on\n * the catalog is Cauthy's, edited in its dashboard, and pushing again would\n * overwrite what was edited there.\n */\nexport async function pushPolicy(o: PushPolicyOptions): Promise<void> {\n const env = (n: string) => (typeof process !== 'undefined' ? process.env?.[n] : undefined)\n const issuer = (o.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n const system = o.system ?? env('CAUTHY_SYSTEM') ?? ''\n const tenant = o.tenant ?? env('CAUTHY_TENANT') ?? ''\n const token = o.token ?? env('CAUTHY_TOKEN') ?? ''\n for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {\n if (v === '') throw new Error(`cauthy-auth: ${name} is required to push a policy`)\n }\n const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {\n method: 'PUT',\n headers: { authorization: `Bearer ${token}`, 'x-tenant-key': tenant, 'content-type': 'application/json' },\n body: JSON.stringify({ roles: o.roles }),\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as { errors?: { message?: string }[] } | null\n throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`)\n }\n}\n"],"mappings":"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","names":[]}