import { z } from "zod"; /** * Opaque at the type level via Zod's nominal `.brand()`: a plain `string` is not assignable to * `EkklesiaUserId` without passing through `parseEkklesiaUserId`/`safeParseEkklesiaUserId`. The * brand exists only in the type system — it adds no runtime tag, wrapper, or field, so a value * still serializes (JSON.stringify, string concatenation, equality) as a plain opaque string. * Ekklesia does not assume any particular format (in particular, not UUID): Phase 4A's identity * schema stores Better Auth's default application-generated id, an opaque base62-ish string, not * a database-generated UUID. Only the constraints Ekklesia itself owns are validated: non-empty * after trimming, and a generous upper bound against unreasonably large values. */ export const ekklesiaUserIdSchema = z.string().trim().min(1).max(128).brand<"EkklesiaUserId">(); export type EkklesiaUserId = z.infer; export function parseEkklesiaUserId(input: unknown): EkklesiaUserId { return ekklesiaUserIdSchema.parse(input); } export function safeParseEkklesiaUserId(input: unknown): z.ZodSafeParseResult { return ekklesiaUserIdSchema.safeParse(input); } /** * A managed display device: a TV, signage player, or confidence monitor that the platform paired * and can revoke. * * It is deliberately **not** a user. A device has no email, no password, no session that a person * signs into, and no row in the user table — representing one as a fake user would make every * "is this a person?" question in the platform unanswerable, and would quietly hand a screen in a * hallway whatever a signed-in human is allowed to do. * * A device principal carries only the permissions explicitly granted to that device. It satisfies * no `authenticated` baseline: "unattended screen in the foyer" is not "a person is signed in", * and every route gated only by "someone is logged in" would otherwise open to it (ADR 0036). */ export const ekklesiaDeviceIdSchema = z .string() .regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u) .brand<"EkklesiaDeviceId">(); export type EkklesiaDeviceId = z.infer; export function parseEkklesiaDeviceId(input: unknown): EkklesiaDeviceId { return ekklesiaDeviceIdSchema.parse(input); } export function safeParseEkklesiaDeviceId(input: unknown): z.ZodSafeParseResult { return ekklesiaDeviceIdSchema.safeParse(input); } export interface EkklesiaAnonymousPrincipal { readonly kind: "anonymous"; } export interface EkklesiaAuthenticatedPrincipal { readonly kind: "authenticated"; readonly userId: EkklesiaUserId; } export interface EkklesiaDevicePrincipal { readonly kind: "device"; readonly deviceId: EkklesiaDeviceId; } /** * The three principal classes Ekklesia recognizes. They are deliberately a closed union rather than * "authenticated or not": code that means *a person* must be able to say so, and code that merely * needs *some* caller must not accidentally accept a device. */ export type EkklesiaPrincipal = | EkklesiaAnonymousPrincipal | EkklesiaAuthenticatedPrincipal | EkklesiaDevicePrincipal; export const ANONYMOUS_PRINCIPAL: EkklesiaAnonymousPrincipal = Object.freeze({ kind: "anonymous", }); export function createAuthenticatedPrincipal(userId: unknown): EkklesiaAuthenticatedPrincipal { return Object.freeze({ kind: "authenticated", userId: parseEkklesiaUserId(userId), }); } export function createDevicePrincipal(deviceId: unknown): EkklesiaDevicePrincipal { return Object.freeze({ deviceId: parseEkklesiaDeviceId(deviceId), kind: "device", }); } export function isAnonymousPrincipal( principal: EkklesiaPrincipal, ): principal is EkklesiaAnonymousPrincipal { return principal.kind === "anonymous"; } /** * Whether this principal is a signed-in **person**. * * A device principal deliberately returns `false`. Every existing caller of this predicate means * "a human is authenticated here" — the Dashboard shell, the `authenticated` permission baseline, * the grants API — and each of them would be wrong about a screen on a wall. */ export function isAuthenticatedPrincipal( principal: EkklesiaPrincipal, ): principal is EkklesiaAuthenticatedPrincipal { return principal.kind === "authenticated"; } export function isDevicePrincipal( principal: EkklesiaPrincipal, ): principal is EkklesiaDevicePrincipal { return principal.kind === "device"; }