import { relations, sql } from "drizzle-orm"; import { check, index, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"; import { ekklesiaCoreUser } from "../identity/schema.js"; /** * Core-owned authorization schema: grants (explicit or relationship-derived), platform-owner * status, and an append-only audit log of every security-relevant mutation. Table and index names * are explicit and namespaced so ownership is unambiguous inside the shared public PostgreSQL * schema, exactly like `core/src/identity/schema.ts`. Foreign keys to `ekklesia_core_user` are * safe here (unlike a module referencing another module's tables) because Core owns both sides. */ export const ekklesiaCoreGrant = pgTable( "ekklesia_core_grant", { id: uuid("id").primaryKey().defaultRandom(), userId: text("user_id") .notNull() .references(() => ekklesiaCoreUser.id, { onDelete: "cascade" }), permissionId: text("permission_id").notNull(), scope: text("scope"), sourceKind: text("source_kind").notNull(), sourceRef: text("source_ref"), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), createdByActorType: text("created_by_actor_type").notNull(), createdByActorId: text("created_by_actor_id"), expiresAt: timestamp("expires_at", { withTimezone: true }), revokedAt: timestamp("revoked_at", { withTimezone: true }), revokedByActorType: text("revoked_by_actor_type"), revokedByActorId: text("revoked_by_actor_id"), }, (table) => [ index("ekklesia_core_grant_user_permission_idx").on(table.userId, table.permissionId), check( "ekklesia_core_grant_source_kind_check", sql`${table.sourceKind} in ('explicit', 'relationship')`, ), check( "ekklesia_core_grant_created_actor_type_check", sql`${table.createdByActorType} in ('user', 'system')`, ), check( "ekklesia_core_grant_revoked_actor_type_check", sql`${table.revokedByActorType} is null or ${table.revokedByActorType} in ('user', 'system')`, ), ], ); export const ekklesiaCorePlatformOwner = pgTable( "ekklesia_core_platform_owner", { userId: text("user_id") .primaryKey() .references(() => ekklesiaCoreUser.id, { onDelete: "cascade" }), grantedAt: timestamp("granted_at", { withTimezone: true }).defaultNow().notNull(), grantedByActorType: text("granted_by_actor_type").notNull(), grantedByActorId: text("granted_by_actor_id"), revokedAt: timestamp("revoked_at", { withTimezone: true }), revokedByActorType: text("revoked_by_actor_type"), revokedByActorId: text("revoked_by_actor_id"), }, (table) => [ check( "ekklesia_core_platform_owner_granted_actor_type_check", sql`${table.grantedByActorType} in ('user', 'system')`, ), check( "ekklesia_core_platform_owner_revoked_actor_type_check", sql`${table.revokedByActorType} is null or ${table.revokedByActorType} in ('user', 'system')`, ), ], ); /** * Append-only *by application convention*: Core's authorization store exposes no update or delete * path for these rows. That is a property of this codebase, not a database guarantee — no * PostgreSQL role, trigger, or rule prevents a database administrator, a future migration, or * anything else holding the same credentials from modifying them. `scope` intentionally rejects an * empty (but non-null) string — defense in depth matching the application-layer * `AuthorizationScope` contract, which never produces one — so a coding defect that slipped an * empty scope past that validation fails the whole grant-plus-audit transaction atomically instead * of recording a meaningless audit entry. * * `subject_user_id` is a **retained historical snapshot**, deliberately carrying no foreign key to * the live Better Auth user row. It previously cascaded on user deletion, which meant deleting a * user destroyed exactly the security history describing what that user had been granted and who * granted it — the audit trail disappeared precisely when it mattered most. The value stays a * plain, non-null identifier string; no name, email, or other profile data is ever copied here. */ export const ekklesiaCoreAuthorizationAudit = pgTable( "ekklesia_core_authorization_audit", { id: uuid("id").primaryKey().defaultRandom(), action: text("action").notNull(), actorType: text("actor_type").notNull(), actorId: text("actor_id"), subjectUserId: text("subject_user_id").notNull(), permissionId: text("permission_id"), scope: text("scope"), grantId: uuid("grant_id"), createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), }, (table) => [ index("ekklesia_core_authorization_audit_subject_idx").on(table.subjectUserId), check( "ekklesia_core_authorization_audit_action_check", sql`${table.action} in ('grant.create', 'grant.revoke', 'platform_owner.grant', 'platform_owner.revoke')`, ), check( "ekklesia_core_authorization_audit_actor_type_check", sql`${table.actorType} in ('user', 'system')`, ), check( "ekklesia_core_authorization_audit_scope_check", sql`${table.scope} is null or length(${table.scope}) > 0`, ), ], ); /** * The durable, single-row record meaning "platform-owner bootstrap was successfully consumed." * * Bootstrap's single-use authority previously came from the mere existence of a row in * `ekklesia_core_platform_owner` — which cascades on user deletion, so deleting the bootstrapped * owner's user row silently reopened the one-time operation that establishes the platform's first * administrator. This table exists specifically so that authority survives owner revocation and * user deletion: it has **no foreign key to any user**, and nothing in Core ever deletes from it. * * `id` is pinned by a check constraint to the single literal value `'singleton'`, so the primary * key makes a second row structurally impossible rather than merely unwritten by convention — * concurrent first attempts collide on the primary key inside their transactions. * * `consumed_for_user_id` is an optional historical snapshot of who bootstrap made an owner, in the * same retained-identifier style as the audit subject and carrying no profile data. It is null for * an installation whose consumed state was backfilled by migration, because a backfill cannot know * which of several historical owner rows the original single use actually targeted, and guessing * one would fabricate history. */ export const ekklesiaCorePlatformOwnerBootstrap = pgTable( "ekklesia_core_platform_owner_bootstrap", { id: text("id").primaryKey(), consumedAt: timestamp("consumed_at", { withTimezone: true }).defaultNow().notNull(), consumedForUserId: text("consumed_for_user_id"), }, (table) => [ check("ekklesia_core_platform_owner_bootstrap_singleton_check", sql`${table.id} = 'singleton'`), ], ); /** The only value `ekklesia_core_platform_owner_bootstrap.id` may ever hold. */ export const PLATFORM_OWNER_BOOTSTRAP_SINGLETON_ID = "singleton"; export const ekklesiaCoreGrantRelations = relations(ekklesiaCoreGrant, ({ one }) => ({ user: one(ekklesiaCoreUser, { fields: [ekklesiaCoreGrant.userId], references: [ekklesiaCoreUser.id], }), })); export const ekklesiaCorePlatformOwnerRelations = relations( ekklesiaCorePlatformOwner, ({ one }) => ({ user: one(ekklesiaCoreUser, { fields: [ekklesiaCorePlatformOwner.userId], references: [ekklesiaCoreUser.id], }), }), ); // `ekklesiaCoreAuthorizationAudit` deliberately declares no `relations(...)` to the user table: // its subject is a retained historical identifier that may name a user row that no longer exists, // so modelling it as a live relation would misdescribe the data. /** Explicit map passed to Drizzle, mirroring `identityAdapterSchema`'s explicit-mapping style. */ export const authorizationSchema = { authorizationAudit: ekklesiaCoreAuthorizationAudit, grant: ekklesiaCoreGrant, platformOwner: ekklesiaCorePlatformOwner, platformOwnerBootstrap: ekklesiaCorePlatformOwnerBootstrap, } as const;