feat: establish core identity foundation
CI / verify (pull_request) Successful in 25s
CI / infrastructure-smoke (pull_request) Successful in 12s
CI / migration-smoke (pull_request) Successful in 22s

Adds Core-owned identity contracts, a Core-owned Better Auth database schema
(ekklesia_core_user/session/account/verification), a first-class Core-owned
migration source that coexists with module-owned migrations under the exact
existing migration system, and an explicit Better Auth construction boundary.
No authentication endpoint or sign-in UI is exposed yet (ADR 0010).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
ekklesia
2026-07-21 16:39:15 -05:00
co-authored by Claude Sonnet 5
parent df0f20d8af
commit 679a5d225c
43 changed files with 4259 additions and 64 deletions
+47
View File
@@ -0,0 +1,47 @@
import { z } from "zod";
export const ekklesiaUserIdSchema = z.string().trim().min(1).max(128);
export type EkklesiaUserId = z.infer<typeof ekklesiaUserIdSchema>;
export function parseEkklesiaUserId(input: unknown): EkklesiaUserId {
return ekklesiaUserIdSchema.parse(input);
}
export function safeParseEkklesiaUserId(input: unknown): z.ZodSafeParseResult<EkklesiaUserId> {
return ekklesiaUserIdSchema.safeParse(input);
}
export interface EkklesiaAnonymousPrincipal {
readonly kind: "anonymous";
}
export interface EkklesiaAuthenticatedPrincipal {
readonly kind: "authenticated";
readonly userId: EkklesiaUserId;
}
export type EkklesiaPrincipal = EkklesiaAnonymousPrincipal | EkklesiaAuthenticatedPrincipal;
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 isAnonymousPrincipal(
principal: EkklesiaPrincipal,
): principal is EkklesiaAnonymousPrincipal {
return principal.kind === "anonymous";
}
export function isAuthenticatedPrincipal(
principal: EkklesiaPrincipal,
): principal is EkklesiaAuthenticatedPrincipal {
return principal.kind === "authenticated";
}
+1
View File
@@ -1,4 +1,5 @@
export * from "./identifiers.js";
export * from "./identity.js";
export * from "./module-lifecycle.js";
export * from "./module-manifest.js";
export * from "./module-migration.js";
+70
View File
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import {
ANONYMOUS_PRINCIPAL,
createAuthenticatedPrincipal,
type EkklesiaPrincipal,
isAnonymousPrincipal,
isAuthenticatedPrincipal,
parseEkklesiaUserId,
safeParseEkklesiaUserId,
} from "../src/index.js";
describe("Ekklesia identity contracts", () => {
it("parses a valid opaque user id", () => {
expect(parseEkklesiaUserId("user_123")).toBe("user_123");
});
it("rejects an empty or oversized user id", () => {
expect(() => parseEkklesiaUserId("")).toThrow();
expect(() => parseEkklesiaUserId(" ")).toThrow();
expect(() => parseEkklesiaUserId("x".repeat(129))).toThrow();
expect(() => parseEkklesiaUserId(42)).toThrow();
expect(() => parseEkklesiaUserId(null)).toThrow();
expect(() => parseEkklesiaUserId(undefined)).toThrow();
});
it("exposes a safe-parse variant that never throws", () => {
expect(safeParseEkklesiaUserId("user_123").success).toBe(true);
const failed = safeParseEkklesiaUserId("");
expect(failed.success).toBe(false);
});
it("returns a frozen, stable anonymous principal singleton", () => {
expect(ANONYMOUS_PRINCIPAL).toEqual({ kind: "anonymous" });
expect(Object.isFrozen(ANONYMOUS_PRINCIPAL)).toBe(true);
expect(isAnonymousPrincipal(ANONYMOUS_PRINCIPAL)).toBe(true);
expect(isAuthenticatedPrincipal(ANONYMOUS_PRINCIPAL)).toBe(false);
});
it("creates a frozen authenticated principal carrying only the opaque user id", () => {
const principal = createAuthenticatedPrincipal("user_123");
expect(principal.kind).toBe("authenticated");
expect(principal.userId).toBe("user_123");
expect(Object.isFrozen(principal)).toBe(true);
expect(isAuthenticatedPrincipal(principal)).toBe(true);
expect(isAnonymousPrincipal(principal)).toBe(false);
expect(Object.keys(principal).sort()).toEqual(["kind", "userId"]);
});
it("rejects an invalid user id when creating an authenticated principal", () => {
expect(() => createAuthenticatedPrincipal("")).toThrow();
expect(() => createAuthenticatedPrincipal(42)).toThrow();
});
it("discriminates principals on kind without any role, permission, or profile fields", () => {
const principals: EkklesiaPrincipal[] = [
ANONYMOUS_PRINCIPAL,
createAuthenticatedPrincipal("user_123"),
];
for (const principal of principals) {
expect(["anonymous", "authenticated"]).toContain(principal.kind);
expect(Object.keys(principal)).not.toContain("email");
expect(Object.keys(principal)).not.toContain("roles");
expect(Object.keys(principal)).not.toContain("permissions");
expect(Object.keys(principal)).not.toContain("organizationId");
expect(Object.keys(principal)).not.toContain("sessionToken");
}
});
});