feat: expose core identity transport
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ekklesiaUserIdSchema = z.string().trim().min(1).max(128);
|
||||
/**
|
||||
* 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<typeof ekklesiaUserIdSchema>;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ANONYMOUS_PRINCIPAL,
|
||||
createAuthenticatedPrincipal,
|
||||
type EkklesiaPrincipal,
|
||||
type EkklesiaUserId,
|
||||
isAnonymousPrincipal,
|
||||
isAuthenticatedPrincipal,
|
||||
parseEkklesiaUserId,
|
||||
@@ -14,6 +15,25 @@ describe("Ekklesia identity contracts", () => {
|
||||
expect(parseEkklesiaUserId("user_123")).toBe("user_123");
|
||||
});
|
||||
|
||||
it("is opaque at the type level: a plain string is not assignable without parsing", () => {
|
||||
// @ts-expect-error a bare string must not satisfy the branded EkklesiaUserId type.
|
||||
const notAllowed: EkklesiaUserId = "raw-string";
|
||||
expect(notAllowed).toBe("raw-string"); // the brand adds no runtime wrapper or tag
|
||||
});
|
||||
|
||||
it("serializes as a plain opaque string despite the type-level brand", () => {
|
||||
const userId = parseEkklesiaUserId("user_123");
|
||||
|
||||
expect(JSON.stringify({ userId })).toBe('{"userId":"user_123"}');
|
||||
expect(`${userId}`).toBe("user_123");
|
||||
expect(typeof userId).toBe("string");
|
||||
});
|
||||
|
||||
it("does not assume any particular id format such as UUID", () => {
|
||||
expect(() => parseEkklesiaUserId("not-a-uuid-at-all")).not.toThrow();
|
||||
expect(() => parseEkklesiaUserId("2aKp4jPeV1QbXwyz1OH6IcAUfl2n1fc1")).not.toThrow();
|
||||
});
|
||||
|
||||
it("rejects an empty or oversized user id", () => {
|
||||
expect(() => parseEkklesiaUserId("")).toThrow();
|
||||
expect(() => parseEkklesiaUserId(" ")).toThrow();
|
||||
|
||||
Reference in New Issue
Block a user