@ai-matrx/data 0.26.2 → 0.28.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.28.1
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/data/v0.28.0..npm/data/v0.28.1 -- apps/shared/data`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.28.0 — sign-in/session doors and a script connection
10
+
11
+ - `@ai-matrx/data/auth`: `getSession`, `getPerson`, `onSessionChange`, `signInWithPassword`, `signInWithOAuth`,
12
+ `signOut`, `updatePerson` over any supabase-js client; refusals throw `SignInError` with the auth server's own
13
+ message, status and code. `userCredentials(client)` is the session as `@ai-matrx/api`'s `user` credential
14
+ (the `CredentialsPort` shape) — api reads the token, never signs anyone in (design §2.6).
15
+ - `@ai-matrx/data/script`: `createScriptDb()` — a Node script's connection from `process.env`
16
+ (`SCRIPT_URL_VARS`, `SCRIPT_KEY_VARS`; a *SECRET* key is the service role, a publishable key the anonymous
17
+ person), or null when unset. Scripts never open their own client.
18
+
19
+ ## 0.27.1 — a real supabase-js client is the port, no cast
20
+
21
+ - `SupabaseLike.schema(s).from(t)` was typed as a filter builder (`eq`, `range`, …), which a real
22
+ `SupabaseClient` table builder is not, so passing the client itself failed to compile (TS2322; extend's
23
+ `tsc` on associations 0.14.0). `from()` is now `unknown` on the port and read as the new `TableLike`
24
+ (`select`/`insert`/`update`/`delete`) inside the doors — a structural match of supabase-js's builder
25
+ generics costs TS2589. Guard: `doors.types.test.ts` assigns an untyped and a Database-typed
26
+ `SupabaseClient` to the port. **Consumer action:** drop any cast you added.
27
+
28
+ ## 0.27.0 — `@ai-matrx/data/short-link`
29
+
30
+ - `mintShortLink(client, options)`, `resolveShortLinkPath(client, token)` and `shortLinkMinter(client)` —
31
+ the short-link database calls, moved here from `@ai-matrx/kit/short-link` (kit 0.42.0 never talks to the
32
+ database). Same functions (`public.shorten_app_url`, `public.resolve_short_link`), same arguments, same
33
+ answers; now through generated function doors (`src/short-link-db/public.ts`). A transport failure still
34
+ answers `{ ok: false, transportError }` with the database's own message.
35
+
3
36
  ## 0.26.2
4
37
 
5
38
  Automatic changed-only republish (docs/metadata drift since the last tag — see
package/dist/auth.cjs ADDED
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/auth.ts
21
+ var auth_exports = {};
22
+ __export(auth_exports, {
23
+ SignInError: () => SignInError,
24
+ getPerson: () => getPerson,
25
+ getSession: () => getSession,
26
+ onSessionChange: () => onSessionChange,
27
+ signInWithOAuth: () => signInWithOAuth,
28
+ signInWithPassword: () => signInWithPassword,
29
+ signOut: () => signOut,
30
+ updatePerson: () => updatePerson,
31
+ userCredentials: () => userCredentials
32
+ });
33
+ module.exports = __toCommonJS(auth_exports);
34
+
35
+ // src/db/doors.ts
36
+ function credentialsOf(client) {
37
+ return {
38
+ async get() {
39
+ if (!client.auth) return null;
40
+ const { data } = await client.auth.getSession();
41
+ return data.session ? { kind: "user", accessToken: data.session.access_token } : null;
42
+ },
43
+ onChange(cb) {
44
+ if (!client.auth) return () => {
45
+ };
46
+ const { data } = client.auth.onAuthStateChange(() => cb());
47
+ return () => data.subscription.unsubscribe();
48
+ }
49
+ };
50
+ }
51
+ function createDb(options) {
52
+ const { client, organization } = options;
53
+ return {
54
+ client,
55
+ role: "user",
56
+ activeOrganization: () => organization?.() ?? null,
57
+ credentials: credentialsOf(client)
58
+ };
59
+ }
60
+
61
+ // src/auth.ts
62
+ var SignInError = class extends Error {
63
+ constructor(door, message, status, code) {
64
+ super(`${door}: ${message}`);
65
+ this.door = door;
66
+ this.status = status;
67
+ this.code = code;
68
+ this.name = "SignInError";
69
+ }
70
+ door;
71
+ status;
72
+ code;
73
+ };
74
+ function check(door, data, error) {
75
+ if (error) throw new SignInError(door, error.message, error.status, error.code);
76
+ return data;
77
+ }
78
+ async function getSession(client) {
79
+ const { data, error } = await client.auth.getSession();
80
+ return check("getSession", data, error).session;
81
+ }
82
+ async function getPerson(client) {
83
+ const { data, error } = await client.auth.getUser();
84
+ if (error && (error.name === "AuthSessionMissingError" || error.status === 401)) return null;
85
+ return check("getPerson", data, error).user;
86
+ }
87
+ function onSessionChange(client, cb) {
88
+ const { data } = client.auth.onAuthStateChange(cb);
89
+ return () => data.subscription.unsubscribe();
90
+ }
91
+ async function signInWithPassword(client, credentials) {
92
+ const { data, error } = await client.auth.signInWithPassword(credentials);
93
+ const ok = check("signInWithPassword", data, error);
94
+ return { session: ok.session, person: ok.user };
95
+ }
96
+ async function signInWithOAuth(client, options) {
97
+ const { provider, ...rest } = options;
98
+ const { data, error } = await client.auth.signInWithOAuth({ provider, options: rest });
99
+ const ok = check("signInWithOAuth", data, error);
100
+ return { provider: ok.provider, url: ok.url ?? null };
101
+ }
102
+ async function signOut(client, scope = "global") {
103
+ const { error } = await client.auth.signOut({ scope });
104
+ check("signOut", null, error);
105
+ }
106
+ async function updatePerson(client, attributes) {
107
+ const { data, error } = await client.auth.updateUser(attributes);
108
+ return check("updatePerson", data, error).user;
109
+ }
110
+ function userCredentials(client) {
111
+ return createDb({ client }).credentials;
112
+ }
113
+ //# sourceMappingURL=auth.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/auth.ts","../src/db/doors.ts"],"sourcesContent":["/**\n * @ai-matrx/data/auth — sign-in and session doors (design §2.6).\n *\n * data owns Supabase sign-in because it is the same client and the same session the\n * database doors use. A package or app asks these doors instead of reaching into\n * `client.auth.*`; the session leaves for `@ai-matrx/api` only as the `user` credential\n * (`userCredentials(client)` = `createDb({ client }).credentials`, the shape api's\n * `CredentialsPort` takes), so api reads the token and never signs anyone in.\n *\n * const session = await getSession(supabase); // Session | null\n * const person = await getPerson(supabase); // User | null (verified by the server)\n * const stop = onSessionChange(supabase, (event, session) => …);\n * await signInWithPassword(supabase, { email, password }); // throws SignInError\n * const { url } = await signInWithOAuth(supabase, { provider: \"google\", redirectTo });\n * await signOut(supabase);\n * await updatePerson(supabase, { data: { full_name } });\n *\n * Every refusal throws `SignInError` carrying the auth server's own message, status and code.\n */\nimport type {\n AuthChangeEvent,\n Provider,\n Session,\n SupabaseClient,\n User,\n UserAttributes,\n} from \"@supabase/supabase-js\";\n\nimport { createDb, type SupabaseLike, type UserCredentialsPort } from \"./db/doors\";\n\nexport type { AuthChangeEvent, Session, User, UserAttributes };\n\n/** Any supabase-js client (browser, server, route, extension) — only its `auth` is used here. */\nexport type AuthClient = Pick<SupabaseClient<any, any, any>, \"auth\">;\n\n/** A refusal from the auth server — its own message, plus status/code when it sent them. */\nexport class SignInError extends Error {\n constructor(\n readonly door: string,\n message: string,\n readonly status?: number,\n readonly code?: string,\n ) {\n super(`${door}: ${message}`);\n this.name = \"SignInError\";\n }\n}\n\ntype AuthErr = { message: string; status?: number | undefined; code?: string | undefined } | null;\nfunction check<T>(door: string, data: T, error: AuthErr): T {\n if (error) throw new SignInError(door, error.message, error.status, error.code);\n return data;\n}\n\n/** The stored session (no network unless a refresh is due), or null when signed out. */\nexport async function getSession(client: AuthClient): Promise<Session | null> {\n const { data, error } = await client.auth.getSession();\n return check(\"getSession\", data, error).session;\n}\n\n/** The signed-in person, verified by the auth server, or null when signed out. */\nexport async function getPerson(client: AuthClient): Promise<User | null> {\n const { data, error } = await client.auth.getUser();\n if (error && (error.name === \"AuthSessionMissingError\" || error.status === 401)) return null;\n return check(\"getPerson\", data, error).user;\n}\n\n/** Hear every session change (sign-in, refresh, sign-out). Returns the unsubscribe. */\nexport function onSessionChange(\n client: AuthClient,\n cb: (event: AuthChangeEvent, session: Session | null) => void,\n): () => void {\n const { data } = client.auth.onAuthStateChange(cb);\n return () => data.subscription.unsubscribe();\n}\n\nexport async function signInWithPassword(\n client: AuthClient,\n credentials: { email: string; password: string } | { phone: string; password: string },\n): Promise<{ session: Session; person: User }> {\n const { data, error } = await client.auth.signInWithPassword(credentials);\n const ok = check(\"signInWithPassword\", data, error);\n return { session: ok.session as Session, person: ok.user as User };\n}\n\nexport async function signInWithOAuth(\n client: AuthClient,\n options: { provider: Provider; redirectTo?: string; scopes?: string; queryParams?: Record<string, string>; skipBrowserRedirect?: boolean },\n): Promise<{ provider: Provider; url: string | null }> {\n const { provider, ...rest } = options;\n const { data, error } = await client.auth.signInWithOAuth({ provider, options: rest });\n const ok = check(\"signInWithOAuth\", data, error);\n return { provider: ok.provider, url: ok.url ?? null };\n}\n\nexport async function signOut(client: AuthClient, scope: \"global\" | \"local\" | \"others\" = \"global\"): Promise<void> {\n const { error } = await client.auth.signOut({ scope });\n check(\"signOut\", null, error);\n}\n\n/** Change the signed-in person's email, phone, password or metadata. */\nexport async function updatePerson(client: AuthClient, attributes: UserAttributes): Promise<User> {\n const { data, error } = await client.auth.updateUser(attributes);\n return check(\"updatePerson\", data, error).user as User;\n}\n\n/** The session as `@ai-matrx/api`'s `user` credential — hand it to api's `CredentialsPort`. */\nexport function userCredentials(client: SupabaseLike): UserCredentialsPort {\n return createDb({ client }).credentials;\n}\n","/**\n * @ai-matrx/data — THE doors: one way to talk to the database.\n *\n * `createDb` owns the connection (wrapping the app's existing Supabase client\n * during migration — data never opens a second auth client or socket), the\n * session (handed to `@ai-matrx/api` as its `user` credential through\n * `db.credentials`) and the organization binding. Every operation is a\n * standalone function over a DESCRIPTOR that each app generates locally from the\n * ONE database description (`matrx-data generate`), so the rules ride the\n * descriptor's flags — read from the live table registry — never a call site's\n * memory:\n *\n * - organization (the access ladder): an entity insert takes the explicit\n * `organization_id`, else the active organization; a CHILD insert\n * (`org.rule === \"parent\"`) never takes the active organization — the\n * database's `inherit_org_from_parent` fills it from the parent, so adding to\n * a record in another organization works; reads NEVER read the active\n * organization; service doors have no binding (organization is required);\n * - complete reads: `listAll` pages past PostgREST's 1,000-row cap\n * (`readAllRows`) and throws on a short read; `page` is the explicit window;\n * - versioned tables: `update` takes `{ expectedVersion }` (guarded, conflict\n * returned) or `{ overwrite: true }` — explicit either way;\n * - lifecycle: `remove` soft-deletes where the table has `deleted_at`; an\n * archive-only table refuses `remove` (use `archive`); a hard delete exists\n * only where the table has neither;\n * - the door set by registry type (entity/detail all · ledger read/list/insert ·\n * reference/system read/list · restricted service-only · deprecated none),\n * narrowed by `client_read_only` / `client_deletes_refused` — refused BEFORE\n * the wire with a sentence that names the rule;\n * - service role: `createServiceDb` throws in a browser bundle;\n * - a table chosen at runtime: `tableByName(index, \"schema.table\")` over the\n * generated flags index — loosely typed, same rules.\n */\nimport { readAllRows } from \"./read-all-rows\";\nimport { guardedUpdate } from \"./guarded-update\";\n\n// ─── descriptors (what `matrx-data generate` emits) ─────────────────────────\n\n/** Registry type (`platform.entity_types.type`); null = untyped or no registry row. */\nexport type TableType = \"entity\" | \"detail\" | \"ledger\" | \"reference\" | \"system\" | \"restricted\" | \"deprecated\";\n\nexport type OrgRule =\n | { readonly rule: \"binding\" }\n | { readonly rule: \"parent\"; readonly parent: string; readonly via: string }\n | { readonly rule: \"column\" }\n | { readonly rule: \"none\" };\n\n/** Phantom carrier for the generated row types — never present at runtime. */\ndeclare const ROW: unique symbol;\n\nexport interface TableDescriptor<Row = Record<string, unknown>, Insert = Record<string, unknown>, Update = Partial<Insert>> {\n readonly schema: string;\n readonly name: string;\n readonly kind: \"table\" | \"view\" | \"materialized_view\";\n readonly type: TableType | null;\n readonly pk: readonly string[];\n readonly org: OrgRule;\n readonly versioned: boolean;\n readonly softDelete: boolean;\n /** The archive column, when the table archives instead of (or as well as) soft-deleting. */\n readonly archive: \"archived_at\" | \"is_archived\" | null;\n readonly readOnly: boolean;\n readonly deletesRefused: boolean;\n readonly readOnlyColumns: readonly string[];\n readonly listScope: string | null;\n readonly realtime: boolean;\n /**\n * The table has a `created_by` column. A signed-in insert that omits it is stamped with the\n * session's person (row security on tables like `workbench.notes` refuses an insert whose\n * `created_by` is not `auth.uid()`). Optional: descriptors generated before 0.25.0 lack it.\n */\n readonly createdBy?: boolean;\n readonly [ROW]?: { row: Row; insert: Insert; update: Update };\n}\n\nexport interface FunctionDescriptor<Args = Record<string, unknown>, Result = unknown> {\n readonly schema: string;\n readonly name: string;\n readonly roles: readonly (\"anon\" | \"authenticated\" | \"service_role\")[];\n readonly registered: boolean;\n readonly [ROW]?: { args: Args; result: Result };\n}\n\nexport type RowOf<T> = T extends TableDescriptor<infer R, any, any> ? R : never;\nexport type InsertOf<T> = T extends TableDescriptor<any, infer I, any> ? I : never;\nexport type UpdateOf<T> = T extends TableDescriptor<any, any, infer U> ? U : never;\nexport type ArgsOf<F> = F extends FunctionDescriptor<infer A, any> ? A : never;\nexport type ResultOf<F> = F extends FunctionDescriptor<any, infer R> ? R : never;\n\n// ─── the connection ─────────────────────────────────────────────────────────\n\ninterface PostgrestError {\n message: string;\n code?: string;\n details?: string | null;\n hint?: string | null;\n}\ninterface Resp<T> {\n data: T | null;\n error: PostgrestError | null;\n count?: number | null;\n}\n/** The slice of a supabase-js query builder the doors use (structural — any supabase-js 2.x client). */\nexport interface QueryLike extends PromiseLike<Resp<any>> {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n eq(column: string, value: unknown): QueryLike;\n is(column: string, value: null | boolean): QueryLike;\n order(column: string, opts?: { ascending?: boolean }): QueryLike;\n range(from: number, to: number): QueryLike;\n maybeSingle(): PromiseLike<Resp<any>>;\n single(): PromiseLike<Resp<any>>;\n}\n/** The slice of a supabase-js table builder (`client.schema(s).from(t)`) the doors use. */\nexport interface TableLike {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n insert(values: any): QueryLike;\n update(values: any): QueryLike;\n delete(): QueryLike;\n}\n/** The slice of a supabase-js client the doors use (a real `SupabaseClient` is assignable — doors.types.test.ts). */\nexport interface SupabaseLike {\n schema(name: string): {\n /**\n * A table builder (read as `TableLike` inside the doors). Typed `unknown` on purpose: matching\n * supabase-js's deep builder generics structurally costs TS2589 in every consumer.\n */\n from(table: string): unknown;\n rpc(fn: string, args?: Record<string, unknown>, opts?: { count?: \"exact\" | \"planned\" | \"estimated\" }): PromiseLike<Resp<any>>;\n };\n auth?: {\n getSession(): Promise<{ data: { session: { access_token: string; user?: { id: string } | null } | null } }>;\n onAuthStateChange(cb: (event: string, session: unknown) => void): { data: { subscription: { unsubscribe(): void } } };\n };\n}\n\n/** The credential shape `@ai-matrx/api`'s `CredentialsPort` accepts (structural; data owns the session). */\nexport interface UserCredentialsPort {\n get(): Promise<{ kind: \"user\"; accessToken: string } | null>;\n onChange?(cb: () => void): () => void;\n}\n\nexport interface Db {\n readonly client: SupabaseLike;\n readonly role: \"user\" | \"service\";\n /** The active organization — where NEW entity rows are saved. Never a read filter. */\n activeOrganization(): string | null;\n /** The session as `@ai-matrx/api`'s `user` credential (one sign-in feeds both lanes). */\n readonly credentials: UserCredentialsPort;\n}\n\nexport interface CreateDbOptions {\n /** The app's existing Supabase client (browser, RSC, route or the host's chat client). */\n client: SupabaseLike;\n /** Where new entity rows are saved. Read lazily on every insert; never consulted by a read. */\n organization?: () => string | null | undefined;\n}\n\nfunction credentialsOf(client: SupabaseLike): UserCredentialsPort {\n return {\n async get() {\n if (!client.auth) return null;\n const { data } = await client.auth.getSession();\n return data.session ? { kind: \"user\", accessToken: data.session.access_token } : null;\n },\n onChange(cb) {\n if (!client.auth) return () => {};\n const { data } = client.auth.onAuthStateChange(() => cb());\n return () => data.subscription.unsubscribe();\n },\n };\n}\n\n/** The one database connection for a signed-in (or anonymous) person. */\nexport function createDb(options: CreateDbOptions): Db {\n const { client, organization } = options;\n return {\n client,\n role: \"user\",\n activeOrganization: () => organization?.() ?? null,\n credentials: credentialsOf(client),\n };\n}\n\n/**\n * Service-role doors — servers only. No session, no binding: every entity insert\n * names its organization. Throws in a browser bundle.\n */\nexport function createServiceDb(options: { client: SupabaseLike }): Db {\n if (typeof (globalThis as { window?: unknown }).window !== \"undefined\") {\n throw new DoorRefusedError(\"createServiceDb\", \"the service role never runs in a browser\");\n }\n return { client: options.client, role: \"service\", activeOrganization: () => null, credentials: credentialsOf(options.client) };\n}\n\n// ─── refusals ───────────────────────────────────────────────────────────────\n\nexport class DoorRefusedError extends Error {\n constructor(\n readonly door: string,\n readonly reason: string,\n ) {\n super(`${door}: ${reason}`);\n this.name = \"DoorRefusedError\";\n }\n}\n\nexport class DatabaseError extends Error {\n constructor(\n readonly door: string,\n readonly cause: PostgrestError,\n ) {\n super(`${door}: ${cause.message}${cause.code ? ` (${cause.code})` : \"\"}`);\n this.name = \"DatabaseError\";\n }\n}\n\ntype Op = \"read\" | \"insert\" | \"update\" | \"remove\" | \"archive\";\nconst CLIENT_DOORS: Record<TableType, readonly Op[]> = {\n entity: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n detail: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n ledger: [\"read\", \"insert\"],\n reference: [\"read\"],\n system: [\"read\"],\n restricted: [],\n deprecated: [],\n};\n\nconst qualified = (t: { schema: string; name: string }) => `${t.schema}.${t.name}`;\n\n/** Refuse before the wire when the descriptor's rules do not open this door. */\nexport function assertDoor(db: Db, table: TableDescriptor<any, any, any>, op: Op): void {\n const door = `${op} ${qualified(table)}`;\n if (table.type === \"deprecated\") throw new DoorRefusedError(door, \"the table is deprecated — no door opens on it\");\n if (table.kind !== \"table\" && op !== \"read\") throw new DoorRefusedError(door, `a ${table.kind.replace(\"_\", \" \")} is read-only`);\n if (db.role === \"service\") return;\n if (table.type && !CLIENT_DOORS[table.type].includes(op))\n throw new DoorRefusedError(door, `a ${table.type} table opens only ${CLIENT_DOORS[table.type].join(\", \") || \"service\"} doors to a client`);\n if (op !== \"read\" && table.readOnly) throw new DoorRefusedError(door, \"the table is read-only to clients (client_read_only)\");\n if (op === \"remove\" && table.deletesRefused) throw new DoorRefusedError(door, \"clients may not delete from this table (client_deletes_refused)\");\n}\n\nfunction check<T>(door: string, res: Resp<T>): T {\n if (res.error) throw new DatabaseError(door, res.error);\n return res.data as T;\n}\n\nconst from = (db: Db, t: TableDescriptor<any, any, any>) => db.client.schema(t.schema).from(t.name) as TableLike;\n\nfunction byPk(q: QueryLike, t: TableDescriptor<any, any, any>, id: unknown): QueryLike {\n if (t.pk.length === 1) return q.eq(t.pk[0] as string, id);\n const key = id as Record<string, unknown>;\n return t.pk.reduce((acc, col) => acc.eq(col, key[col]), q);\n}\n\n// ─── reads (never the active organization; soft-deleted rows excluded unless asked) ─\n\ntype Col<R> = Extract<keyof R, string>;\n\n/** The typed filter a `Where` callback receives — column names and values checked against the row. */\nexport interface Filter<R> {\n eq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n neq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n like(column: Col<R>, pattern: string): Filter<R>;\n ilike(column: Col<R>, pattern: string): Filter<R>;\n in<K extends Col<R>>(column: K, values: readonly NonNullable<R[K]>[]): Filter<R>;\n is(column: Col<R>, value: null | boolean): Filter<R>;\n contains(column: Col<R>, value: unknown): Filter<R>;\n overlaps(column: Col<R>, value: unknown): Filter<R>;\n textSearch(column: Col<R>, query: string, opts?: { type?: \"plain\" | \"phrase\" | \"websearch\"; config?: string }): Filter<R>;\n /** PostgREST's raw `or` grammar (`\"pinned.eq.true,starred.eq.true\"`). */\n or(filters: string, opts?: { referencedTable?: string }): Filter<R>;\n not(column: Col<R>, operator: string, value: unknown): Filter<R>;\n}\n\n/** A filter callback — the same builder `readAllRows` callers write today, typed to the row. */\nexport type Where<R = any> = (q: Filter<R>) => Filter<R>;\n\nexport interface ReadOptions<C> {\n /** Only these columns (the result is typed to them). Default every column. */\n columns?: readonly C[];\n /** Include soft-deleted rows (default: excluded on tables with `deleted_at`). */\n includeDeleted?: boolean;\n}\n\nconst selectOf = (columns?: readonly string[]) => (columns && columns.length ? columns.join(\",\") : \"*\");\n\nfunction live(q: any, table: TableDescriptor<any, any, any>, includeDeleted?: boolean): any {\n return table.softDelete && !includeDeleted ? q.is(\"deleted_at\", null) : q;\n}\n\nfunction ordered(q: any, table: TableDescriptor<any, any, any>, orderBy?: string, ascending = true): any {\n // The FULL primary key, so paging is stable on composite keys.\n const cols = orderBy ? [orderBy, ...table.pk.filter((c) => c !== orderBy)] : table.pk.length ? table.pk : [\"id\"];\n return cols.reduce((acc: any, c) => acc.order(c, { ascending }), q);\n}\n\n/** One row by primary key (a composite key takes `{ col: value }`), or null. */\nexport async function read<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: ReadOptions<C> = {},\n): Promise<Pick<R, C> | null> {\n assertDoor(db, table, \"read\");\n const q = live(byPk(from(db, table).select(selectOf(opts.columns)), table, id), table, opts.includeDeleted);\n return check(`read ${qualified(table)}`, await q.maybeSingle());\n}\n\n/** EVERY matching row, paged past the 1,000-row cap (ordered by the full key); throws on a short read. */\nexport async function listAll<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n where?: Where<R>,\n opts: ReadOptions<C> & { orderBy?: Col<R>; pageSize?: number; maxRows?: number } = {},\n): Promise<Pick<R, C>[]> {\n assertDoor(db, table, \"read\");\n return readAllRows<Pick<R, C>>(\n ({ from: lo, to: hi }) => {\n let q: any = live(from(db, table).select(selectOf(opts.columns), { count: \"exact\" }), table, opts.includeDeleted);\n if (where) q = where(q);\n // The database's own error (code, details) reaches the caller as a DatabaseError,\n // exactly like every other door — never a re-worded readAllRows message.\n return Promise.resolve(ordered(q, table, opts.orderBy).range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(`listAll ${qualified(table)}`, res.error);\n return res;\n });\n },\n {\n label: `listAll ${qualified(table)}`,\n ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}),\n ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}),\n },\n );\n}\n\n/** One explicit window (`from`..`to`, inclusive) plus the exact total. */\nexport async function page<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n window: ReadOptions<C> & { from: number; to: number; where?: Where<R>; orderBy?: Col<R>; ascending?: boolean },\n): Promise<{ rows: Pick<R, C>[]; total: number | null }> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(selectOf(window.columns), { count: \"exact\" }), table, window.includeDeleted);\n if (window.where) q = window.where(q);\n const res = await ordered(q, table, window.orderBy, window.ascending ?? true).range(window.from, window.to);\n return { rows: check(`page ${qualified(table)}`, res) ?? [], total: res.count ?? null };\n}\n\n/** The exact count of matching rows (no rows transferred). */\nexport async function count<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, where?: Where<R>, opts: { includeDeleted?: boolean } = {}): Promise<number> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(\"*\", { count: \"exact\", head: true }), table, opts.includeDeleted);\n if (where) q = where(q);\n const res = await q;\n check(`count ${qualified(table)}`, res);\n return res.count ?? 0;\n}\n\n// ─── writes ─────────────────────────────────────────────────────────────────\n\nfunction withOrganization(db: Db, table: TableDescriptor<any, any, any>, values: Record<string, unknown>): Record<string, unknown> {\n const door = `insert ${qualified(table)}`;\n const org = table.org;\n if (org.rule === \"parent\" && values[org.via] != null) {\n // A child row: its organization is its parent's. Never the active organization —\n // `inherit_org_from_parent` fills organization_id; an explicit one is checked by\n // `refuse_org_foreign_to_parent`. So adding to a record in another organization works.\n return values;\n }\n if (org.rule === \"binding\" || org.rule === \"parent\") {\n // An entity row, or a row whose optional parent is absent (a note outside any folder),\n // is saved in the named organization, else the active one.\n if (values.organization_id != null) return values;\n const active = db.activeOrganization();\n if (!active) {\n throw new DoorRefusedError(\n door,\n db.role === \"service\" ? \"a service insert names its organization_id\" : \"no organization: pass organization_id or bind createDb({ organization })\",\n );\n }\n return { ...values, organization_id: active };\n }\n return values;\n}\n\n/** Insert one row (or many) and return what the database stored. */\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: I): Promise<R>;\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: readonly I[]): Promise<R[]>;\nexport async function insert(db: Db, table: TableDescriptor<any, any, any>, values: unknown): Promise<unknown> {\n assertDoor(db, table, \"insert\");\n const many = Array.isArray(values);\n let rows = (many ? values : [values]).map((v) => withOrganization(db, table, v as Record<string, unknown>));\n if (table.createdBy && db.role === \"user\" && rows.some((r) => r.created_by == null) && db.client.auth) {\n // Stamp the signed-in person (no session: sent as given, and row security answers).\n const { data } = await db.client.auth.getSession();\n const person = data.session?.user?.id;\n if (person) rows = rows.map((r) => (r.created_by == null ? { ...r, created_by: person } : r));\n }\n const res = await from(db, table).insert(rows).select(\"*\");\n const stored = check(`insert ${qualified(table)}`, res) as unknown[];\n return many ? stored : stored[0];\n}\n\n/** Every write on a versioned table is explicit: `{ expectedVersion }` (guarded) or `{ overwrite: true }`. */\nexport type UpdateGuard = { expectedVersion: number } | { overwrite: true };\n\nexport type WriteResult<R> =\n | { status: \"saved\"; row: R; rebasedFrom?: { expectedVersion: number; currentVersion: number } }\n | { status: \"conflict\"; currentRow: R; currentVersion: number }\n | { status: \"not_found\" };\n\n/** The one guarded write every update-shaped door goes through. */\nasync function writeRow<R>(db: Db, table: TableDescriptor<any, any, any>, id: unknown, values: Record<string, unknown>, door: string, guard?: UpdateGuard): Promise<WriteResult<R>> {\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) {\n throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned — nothing would guard it; pass no guard\");\n }\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard && guard.overwrite)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n if (expected !== undefined) {\n return (await guardedUpdate<any>({\n expectedVersion: expected,\n applyUpdate: ({ expectedVersion, nextVersion }) =>\n byPk(from(db, table).update({ ...values, version: nextVersion }), table, id).eq(\"version\", expectedVersion).select(\"*\").maybeSingle(),\n fetchCurrent: () => byPk(from(db, table).select(\"*\"), table, id).maybeSingle(),\n })) as WriteResult<R>;\n }\n const row = check(door, await byPk(from(db, table).update(values), table, id).select(\"*\").maybeSingle());\n return row ? { status: \"saved\", row } : { status: \"not_found\" };\n}\n\n/**\n * Update one row by primary key. On a versioned table the guard is required:\n * `{ expectedVersion }` (a conflict comes back, never a silent overwrite) or\n * `{ overwrite: true }`; on an unversioned table `expectedVersion` is refused.\n */\nexport async function update<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, patch: U, guard?: UpdateGuard): Promise<WriteResult<R>> {\n assertDoor(db, table, \"update\");\n const door = `update ${qualified(table)}`;\n const values = patch as Record<string, unknown>;\n if (db.role === \"user\") {\n const blocked = table.readOnlyColumns.filter((c) => c in values);\n if (blocked.length) throw new DoorRefusedError(door, `read-only to clients: ${blocked.join(\", \")}`);\n }\n return writeRow<R>(db, table, id, values, door, guard);\n}\n\n/**\n * Remove one row the way the table removes: soft delete (`deleted_at`) where it\n * has one; an archive-only table refuses (use `archive`); a hard delete only where\n * the table has neither. Guarded like `update` on a versioned table.\n */\nexport async function remove<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n guard?: UpdateGuard,\n): Promise<{ status: \"soft_deleted\" | \"deleted\" | \"not_found\" } | { status: \"conflict\"; currentVersion: number }> {\n assertDoor(db, table, \"remove\");\n const door = `remove ${qualified(table)}`;\n if (table.softDelete) {\n const res = await writeRow<R>(db, table, id, { deleted_at: new Date().toISOString() }, door, guard);\n return res.status === \"saved\" ? { status: \"soft_deleted\" } : res.status === \"conflict\" ? { status: \"conflict\", currentVersion: res.currentVersion } : res;\n }\n if (table.archive) throw new DoorRefusedError(door, \"this table archives instead of deleting — use archive()\");\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned\");\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n let q = byPk(from(db, table).delete(), table, id);\n if (expected !== undefined) q = q.eq(\"version\", expected);\n const rows = check(door, await q.select(\"*\")) as unknown[] | null;\n if (rows && rows.length) return { status: \"deleted\" };\n if (expected !== undefined) {\n const current = check(door, await byPk(from(db, table).select(\"version\"), table, id).maybeSingle()) as { version: number } | null;\n if (current) return { status: \"conflict\", currentVersion: current.version };\n }\n return { status: \"not_found\" };\n}\n\n/** Archive (or, with `restore`, bring back) one row by the table's archive column. Guarded like `update`. */\nexport async function archive<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: { restore?: boolean; guard?: UpdateGuard } = {},\n): Promise<WriteResult<R>> {\n assertDoor(db, table, \"archive\");\n const door = `${opts.restore ? \"restore\" : \"archive\"} ${qualified(table)}`;\n let values: Record<string, unknown>;\n if (table.archive === \"archived_at\") values = { archived_at: opts.restore ? null : new Date().toISOString() };\n else if (table.archive === \"is_archived\") values = { is_archived: !opts.restore };\n else if (table.softDelete && opts.restore) values = { deleted_at: null };\n else throw new DoorRefusedError(door, \"the table has no archive column\");\n return writeRow<R>(db, table, id, values, door, opts.guard);\n}\n\n/** Bring back an archived or soft-deleted row. */\nexport const restore = <R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, guard?: UpdateGuard) =>\n archive(db, table, id, guard ? { restore: true, guard } : { restore: true });\n\n// ─── functions ──────────────────────────────────────────────────────────────\n\n/** Call a database function through its generated door (PostgREST chooses an overload by argument names). */\nexport async function call<F extends FunctionDescriptor<any, any>>(db: Db, fn: F, args: ArgsOf<F>): Promise<ResultOf<F>> {\n const door = `call ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n return check(door, await db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>));\n}\n\n/** One ordering column of a set-returning function's rows (PostgREST `order`). */\nexport type CallOrder = string | { column: string; ascending?: boolean; nullsFirst?: boolean };\n\n/**\n * EVERY row a set-returning function answers, paged past the 1,000-row cap by\n * `range` over a stable `order` (name enough columns to make it total — end on a key);\n * throws a `DatabaseError` with the database's own error, or `IncompleteReadError` on a short read.\n */\nexport async function callAll<F extends FunctionDescriptor<any, any>>(\n db: Db,\n fn: F,\n args: ArgsOf<F>,\n opts: { order: readonly CallOrder[]; pageSize?: number; maxRows?: number },\n): Promise<ResultOf<F> extends readonly (infer Row)[] ? Row[] : ResultOf<F>[]> {\n const door = `callAll ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n if (!opts.order.length) throw new DoorRefusedError(door, \"a paged read needs an order (end on a key) — pages over no order can skip or repeat rows\");\n return readAllRows<any>(\n ({ from: lo, to: hi }) => {\n let q: any = db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>, { count: \"exact\" });\n for (const o of opts.order) {\n q = typeof o === \"string\" ? q.order(o) : q.order(o.column, { ascending: o.ascending ?? true, ...(o.nullsFirst !== undefined ? { nullsFirst: o.nullsFirst } : {}) });\n }\n return Promise.resolve(q.range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(door, res.error);\n return res;\n });\n },\n { label: door, ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}), ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}) },\n ) as never;\n}\n\n// ─── a table chosen at runtime ──────────────────────────────────────────────\n\n/** One schema's flags (`<schema>.flags.ts` from `matrx-data generate --flags-index`): \"schema.table\" → descriptor. */\nexport type FlagsIndex = Readonly<Record<string, TableDescriptor>>;\n/** Per-schema loaders (`flags-index.ts`): only the schema a runtime name names is loaded (pay-only). */\nexport type FlagLoaders = Readonly<Record<string, () => Promise<FlagsIndex>>>;\n\n/** The door for a table whose name arrives at runtime — loosely typed, the same rules. */\nexport function tableByName(index: FlagsIndex, name: string): TableDescriptor {\n const t = index[name];\n if (!t) throw new DoorRefusedError(`tableByName ${name}`, \"no such table in the database description (schema-qualify it: schema.table)\");\n return t;\n}\n\n/** `tableByName` over per-schema loaders: loads only the named schema's flags. */\nexport async function loadTableByName(loaders: FlagLoaders, name: string): Promise<TableDescriptor> {\n const schema = name.slice(0, name.indexOf(\".\"));\n const load = schema ? loaders[schema] : undefined;\n if (!load) throw new DoorRefusedError(`tableByName ${name}`, \"no such schema in the database description (schema-qualify it: schema.table)\");\n return tableByName(await load(), name);\n}\n\n// ─── realtime ───────────────────────────────────────────────────────────────\n\n/** The `postgres_changes` filter for a table — only published tables have one (an unpublished table delivers nothing). */\nexport function changesOf(table: TableDescriptor<any, any, any>): { schema: string; table: string } {\n if (!table.realtime) throw new DoorRefusedError(`changesOf ${qualified(table)}`, \"the table is not in the supabase_realtime publication\");\n return { schema: table.schema, table: table.name };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC6JA,SAAS,cAAc,QAA2C;AAChE,SAAO;AAAA,IACL,MAAM,MAAM;AACV,UAAI,CAAC,OAAO,KAAM,QAAO;AACzB,YAAM,EAAE,KAAK,IAAI,MAAM,OAAO,KAAK,WAAW;AAC9C,aAAO,KAAK,UAAU,EAAE,MAAM,QAAQ,aAAa,KAAK,QAAQ,aAAa,IAAI;AAAA,IACnF;AAAA,IACA,SAAS,IAAI;AACX,UAAI,CAAC,OAAO,KAAM,QAAO,MAAM;AAAA,MAAC;AAChC,YAAM,EAAE,KAAK,IAAI,OAAO,KAAK,kBAAkB,MAAM,GAAG,CAAC;AACzD,aAAO,MAAM,KAAK,aAAa,YAAY;AAAA,IAC7C;AAAA,EACF;AACF;AAGO,SAAS,SAAS,SAA8B;AACrD,QAAM,EAAE,QAAQ,aAAa,IAAI;AACjC,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,oBAAoB,MAAM,eAAe,KAAK;AAAA,IAC9C,aAAa,cAAc,MAAM;AAAA,EACnC;AACF;;;ADjJO,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YACW,MACT,SACS,QACA,MACT;AACA,UAAM,GAAG,IAAI,KAAK,OAAO,EAAE;AALlB;AAEA;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EAPW;AAAA,EAEA;AAAA,EACA;AAKb;AAGA,SAAS,MAAS,MAAc,MAAS,OAAmB;AAC1D,MAAI,MAAO,OAAM,IAAI,YAAY,MAAM,MAAM,SAAS,MAAM,QAAQ,MAAM,IAAI;AAC9E,SAAO;AACT;AAGA,eAAsB,WAAW,QAA6C;AAC5E,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,WAAW;AACrD,SAAO,MAAM,cAAc,MAAM,KAAK,EAAE;AAC1C;AAGA,eAAsB,UAAU,QAA0C;AACxE,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,QAAQ;AAClD,MAAI,UAAU,MAAM,SAAS,6BAA6B,MAAM,WAAW,KAAM,QAAO;AACxF,SAAO,MAAM,aAAa,MAAM,KAAK,EAAE;AACzC;AAGO,SAAS,gBACd,QACA,IACY;AACZ,QAAM,EAAE,KAAK,IAAI,OAAO,KAAK,kBAAkB,EAAE;AACjD,SAAO,MAAM,KAAK,aAAa,YAAY;AAC7C;AAEA,eAAsB,mBACpB,QACA,aAC6C;AAC7C,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,mBAAmB,WAAW;AACxE,QAAM,KAAK,MAAM,sBAAsB,MAAM,KAAK;AAClD,SAAO,EAAE,SAAS,GAAG,SAAoB,QAAQ,GAAG,KAAa;AACnE;AAEA,eAAsB,gBACpB,QACA,SACqD;AACrD,QAAM,EAAE,UAAU,GAAG,KAAK,IAAI;AAC9B,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,gBAAgB,EAAE,UAAU,SAAS,KAAK,CAAC;AACrF,QAAM,KAAK,MAAM,mBAAmB,MAAM,KAAK;AAC/C,SAAO,EAAE,UAAU,GAAG,UAAU,KAAK,GAAG,OAAO,KAAK;AACtD;AAEA,eAAsB,QAAQ,QAAoB,QAAuC,UAAyB;AAChH,QAAM,EAAE,MAAM,IAAI,MAAM,OAAO,KAAK,QAAQ,EAAE,MAAM,CAAC;AACrD,QAAM,WAAW,MAAM,KAAK;AAC9B;AAGA,eAAsB,aAAa,QAAoB,YAA2C;AAChG,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,WAAW,UAAU;AAC/D,SAAO,MAAM,gBAAgB,MAAM,KAAK,EAAE;AAC5C;AAGO,SAAS,gBAAgB,QAA2C;AACzE,SAAO,SAAS,EAAE,OAAO,CAAC,EAAE;AAC9B;","names":[]}
@@ -0,0 +1,66 @@
1
+ import { SupabaseClient, User, Session, AuthChangeEvent, Provider, UserAttributes } from '@supabase/supabase-js';
2
+ export { AuthChangeEvent, Session, User, UserAttributes } from '@supabase/supabase-js';
3
+ import { S as SupabaseLike, U as UserCredentialsPort } from './doors-D88CwoLU.cjs';
4
+
5
+ /**
6
+ * @ai-matrx/data/auth — sign-in and session doors (design §2.6).
7
+ *
8
+ * data owns Supabase sign-in because it is the same client and the same session the
9
+ * database doors use. A package or app asks these doors instead of reaching into
10
+ * `client.auth.*`; the session leaves for `@ai-matrx/api` only as the `user` credential
11
+ * (`userCredentials(client)` = `createDb({ client }).credentials`, the shape api's
12
+ * `CredentialsPort` takes), so api reads the token and never signs anyone in.
13
+ *
14
+ * const session = await getSession(supabase); // Session | null
15
+ * const person = await getPerson(supabase); // User | null (verified by the server)
16
+ * const stop = onSessionChange(supabase, (event, session) => …);
17
+ * await signInWithPassword(supabase, { email, password }); // throws SignInError
18
+ * const { url } = await signInWithOAuth(supabase, { provider: "google", redirectTo });
19
+ * await signOut(supabase);
20
+ * await updatePerson(supabase, { data: { full_name } });
21
+ *
22
+ * Every refusal throws `SignInError` carrying the auth server's own message, status and code.
23
+ */
24
+
25
+ /** Any supabase-js client (browser, server, route, extension) — only its `auth` is used here. */
26
+ type AuthClient = Pick<SupabaseClient<any, any, any>, "auth">;
27
+ /** A refusal from the auth server — its own message, plus status/code when it sent them. */
28
+ declare class SignInError extends Error {
29
+ readonly door: string;
30
+ readonly status?: number | undefined;
31
+ readonly code?: string | undefined;
32
+ constructor(door: string, message: string, status?: number | undefined, code?: string | undefined);
33
+ }
34
+ /** The stored session (no network unless a refresh is due), or null when signed out. */
35
+ declare function getSession(client: AuthClient): Promise<Session | null>;
36
+ /** The signed-in person, verified by the auth server, or null when signed out. */
37
+ declare function getPerson(client: AuthClient): Promise<User | null>;
38
+ /** Hear every session change (sign-in, refresh, sign-out). Returns the unsubscribe. */
39
+ declare function onSessionChange(client: AuthClient, cb: (event: AuthChangeEvent, session: Session | null) => void): () => void;
40
+ declare function signInWithPassword(client: AuthClient, credentials: {
41
+ email: string;
42
+ password: string;
43
+ } | {
44
+ phone: string;
45
+ password: string;
46
+ }): Promise<{
47
+ session: Session;
48
+ person: User;
49
+ }>;
50
+ declare function signInWithOAuth(client: AuthClient, options: {
51
+ provider: Provider;
52
+ redirectTo?: string;
53
+ scopes?: string;
54
+ queryParams?: Record<string, string>;
55
+ skipBrowserRedirect?: boolean;
56
+ }): Promise<{
57
+ provider: Provider;
58
+ url: string | null;
59
+ }>;
60
+ declare function signOut(client: AuthClient, scope?: "global" | "local" | "others"): Promise<void>;
61
+ /** Change the signed-in person's email, phone, password or metadata. */
62
+ declare function updatePerson(client: AuthClient, attributes: UserAttributes): Promise<User>;
63
+ /** The session as `@ai-matrx/api`'s `user` credential — hand it to api's `CredentialsPort`. */
64
+ declare function userCredentials(client: SupabaseLike): UserCredentialsPort;
65
+
66
+ export { type AuthClient, SignInError, getPerson, getSession, onSessionChange, signInWithOAuth, signInWithPassword, signOut, updatePerson, userCredentials };
package/dist/auth.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { SupabaseClient, User, Session, AuthChangeEvent, Provider, UserAttributes } from '@supabase/supabase-js';
2
+ export { AuthChangeEvent, Session, User, UserAttributes } from '@supabase/supabase-js';
3
+ import { S as SupabaseLike, U as UserCredentialsPort } from './doors-D88CwoLU.js';
4
+
5
+ /**
6
+ * @ai-matrx/data/auth — sign-in and session doors (design §2.6).
7
+ *
8
+ * data owns Supabase sign-in because it is the same client and the same session the
9
+ * database doors use. A package or app asks these doors instead of reaching into
10
+ * `client.auth.*`; the session leaves for `@ai-matrx/api` only as the `user` credential
11
+ * (`userCredentials(client)` = `createDb({ client }).credentials`, the shape api's
12
+ * `CredentialsPort` takes), so api reads the token and never signs anyone in.
13
+ *
14
+ * const session = await getSession(supabase); // Session | null
15
+ * const person = await getPerson(supabase); // User | null (verified by the server)
16
+ * const stop = onSessionChange(supabase, (event, session) => …);
17
+ * await signInWithPassword(supabase, { email, password }); // throws SignInError
18
+ * const { url } = await signInWithOAuth(supabase, { provider: "google", redirectTo });
19
+ * await signOut(supabase);
20
+ * await updatePerson(supabase, { data: { full_name } });
21
+ *
22
+ * Every refusal throws `SignInError` carrying the auth server's own message, status and code.
23
+ */
24
+
25
+ /** Any supabase-js client (browser, server, route, extension) — only its `auth` is used here. */
26
+ type AuthClient = Pick<SupabaseClient<any, any, any>, "auth">;
27
+ /** A refusal from the auth server — its own message, plus status/code when it sent them. */
28
+ declare class SignInError extends Error {
29
+ readonly door: string;
30
+ readonly status?: number | undefined;
31
+ readonly code?: string | undefined;
32
+ constructor(door: string, message: string, status?: number | undefined, code?: string | undefined);
33
+ }
34
+ /** The stored session (no network unless a refresh is due), or null when signed out. */
35
+ declare function getSession(client: AuthClient): Promise<Session | null>;
36
+ /** The signed-in person, verified by the auth server, or null when signed out. */
37
+ declare function getPerson(client: AuthClient): Promise<User | null>;
38
+ /** Hear every session change (sign-in, refresh, sign-out). Returns the unsubscribe. */
39
+ declare function onSessionChange(client: AuthClient, cb: (event: AuthChangeEvent, session: Session | null) => void): () => void;
40
+ declare function signInWithPassword(client: AuthClient, credentials: {
41
+ email: string;
42
+ password: string;
43
+ } | {
44
+ phone: string;
45
+ password: string;
46
+ }): Promise<{
47
+ session: Session;
48
+ person: User;
49
+ }>;
50
+ declare function signInWithOAuth(client: AuthClient, options: {
51
+ provider: Provider;
52
+ redirectTo?: string;
53
+ scopes?: string;
54
+ queryParams?: Record<string, string>;
55
+ skipBrowserRedirect?: boolean;
56
+ }): Promise<{
57
+ provider: Provider;
58
+ url: string | null;
59
+ }>;
60
+ declare function signOut(client: AuthClient, scope?: "global" | "local" | "others"): Promise<void>;
61
+ /** Change the signed-in person's email, phone, password or metadata. */
62
+ declare function updatePerson(client: AuthClient, attributes: UserAttributes): Promise<User>;
63
+ /** The session as `@ai-matrx/api`'s `user` credential — hand it to api's `CredentialsPort`. */
64
+ declare function userCredentials(client: SupabaseLike): UserCredentialsPort;
65
+
66
+ export { type AuthClient, SignInError, getPerson, getSession, onSessionChange, signInWithOAuth, signInWithPassword, signOut, updatePerson, userCredentials };
package/dist/auth.js ADDED
@@ -0,0 +1,90 @@
1
+ // src/db/doors.ts
2
+ function credentialsOf(client) {
3
+ return {
4
+ async get() {
5
+ if (!client.auth) return null;
6
+ const { data } = await client.auth.getSession();
7
+ return data.session ? { kind: "user", accessToken: data.session.access_token } : null;
8
+ },
9
+ onChange(cb) {
10
+ if (!client.auth) return () => {
11
+ };
12
+ const { data } = client.auth.onAuthStateChange(() => cb());
13
+ return () => data.subscription.unsubscribe();
14
+ }
15
+ };
16
+ }
17
+ function createDb(options) {
18
+ const { client, organization } = options;
19
+ return {
20
+ client,
21
+ role: "user",
22
+ activeOrganization: () => organization?.() ?? null,
23
+ credentials: credentialsOf(client)
24
+ };
25
+ }
26
+
27
+ // src/auth.ts
28
+ var SignInError = class extends Error {
29
+ constructor(door, message, status, code) {
30
+ super(`${door}: ${message}`);
31
+ this.door = door;
32
+ this.status = status;
33
+ this.code = code;
34
+ this.name = "SignInError";
35
+ }
36
+ door;
37
+ status;
38
+ code;
39
+ };
40
+ function check(door, data, error) {
41
+ if (error) throw new SignInError(door, error.message, error.status, error.code);
42
+ return data;
43
+ }
44
+ async function getSession(client) {
45
+ const { data, error } = await client.auth.getSession();
46
+ return check("getSession", data, error).session;
47
+ }
48
+ async function getPerson(client) {
49
+ const { data, error } = await client.auth.getUser();
50
+ if (error && (error.name === "AuthSessionMissingError" || error.status === 401)) return null;
51
+ return check("getPerson", data, error).user;
52
+ }
53
+ function onSessionChange(client, cb) {
54
+ const { data } = client.auth.onAuthStateChange(cb);
55
+ return () => data.subscription.unsubscribe();
56
+ }
57
+ async function signInWithPassword(client, credentials) {
58
+ const { data, error } = await client.auth.signInWithPassword(credentials);
59
+ const ok = check("signInWithPassword", data, error);
60
+ return { session: ok.session, person: ok.user };
61
+ }
62
+ async function signInWithOAuth(client, options) {
63
+ const { provider, ...rest } = options;
64
+ const { data, error } = await client.auth.signInWithOAuth({ provider, options: rest });
65
+ const ok = check("signInWithOAuth", data, error);
66
+ return { provider: ok.provider, url: ok.url ?? null };
67
+ }
68
+ async function signOut(client, scope = "global") {
69
+ const { error } = await client.auth.signOut({ scope });
70
+ check("signOut", null, error);
71
+ }
72
+ async function updatePerson(client, attributes) {
73
+ const { data, error } = await client.auth.updateUser(attributes);
74
+ return check("updatePerson", data, error).user;
75
+ }
76
+ function userCredentials(client) {
77
+ return createDb({ client }).credentials;
78
+ }
79
+ export {
80
+ SignInError,
81
+ getPerson,
82
+ getSession,
83
+ onSessionChange,
84
+ signInWithOAuth,
85
+ signInWithPassword,
86
+ signOut,
87
+ updatePerson,
88
+ userCredentials
89
+ };
90
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db/doors.ts","../src/auth.ts"],"sourcesContent":["/**\n * @ai-matrx/data — THE doors: one way to talk to the database.\n *\n * `createDb` owns the connection (wrapping the app's existing Supabase client\n * during migration — data never opens a second auth client or socket), the\n * session (handed to `@ai-matrx/api` as its `user` credential through\n * `db.credentials`) and the organization binding. Every operation is a\n * standalone function over a DESCRIPTOR that each app generates locally from the\n * ONE database description (`matrx-data generate`), so the rules ride the\n * descriptor's flags — read from the live table registry — never a call site's\n * memory:\n *\n * - organization (the access ladder): an entity insert takes the explicit\n * `organization_id`, else the active organization; a CHILD insert\n * (`org.rule === \"parent\"`) never takes the active organization — the\n * database's `inherit_org_from_parent` fills it from the parent, so adding to\n * a record in another organization works; reads NEVER read the active\n * organization; service doors have no binding (organization is required);\n * - complete reads: `listAll` pages past PostgREST's 1,000-row cap\n * (`readAllRows`) and throws on a short read; `page` is the explicit window;\n * - versioned tables: `update` takes `{ expectedVersion }` (guarded, conflict\n * returned) or `{ overwrite: true }` — explicit either way;\n * - lifecycle: `remove` soft-deletes where the table has `deleted_at`; an\n * archive-only table refuses `remove` (use `archive`); a hard delete exists\n * only where the table has neither;\n * - the door set by registry type (entity/detail all · ledger read/list/insert ·\n * reference/system read/list · restricted service-only · deprecated none),\n * narrowed by `client_read_only` / `client_deletes_refused` — refused BEFORE\n * the wire with a sentence that names the rule;\n * - service role: `createServiceDb` throws in a browser bundle;\n * - a table chosen at runtime: `tableByName(index, \"schema.table\")` over the\n * generated flags index — loosely typed, same rules.\n */\nimport { readAllRows } from \"./read-all-rows\";\nimport { guardedUpdate } from \"./guarded-update\";\n\n// ─── descriptors (what `matrx-data generate` emits) ─────────────────────────\n\n/** Registry type (`platform.entity_types.type`); null = untyped or no registry row. */\nexport type TableType = \"entity\" | \"detail\" | \"ledger\" | \"reference\" | \"system\" | \"restricted\" | \"deprecated\";\n\nexport type OrgRule =\n | { readonly rule: \"binding\" }\n | { readonly rule: \"parent\"; readonly parent: string; readonly via: string }\n | { readonly rule: \"column\" }\n | { readonly rule: \"none\" };\n\n/** Phantom carrier for the generated row types — never present at runtime. */\ndeclare const ROW: unique symbol;\n\nexport interface TableDescriptor<Row = Record<string, unknown>, Insert = Record<string, unknown>, Update = Partial<Insert>> {\n readonly schema: string;\n readonly name: string;\n readonly kind: \"table\" | \"view\" | \"materialized_view\";\n readonly type: TableType | null;\n readonly pk: readonly string[];\n readonly org: OrgRule;\n readonly versioned: boolean;\n readonly softDelete: boolean;\n /** The archive column, when the table archives instead of (or as well as) soft-deleting. */\n readonly archive: \"archived_at\" | \"is_archived\" | null;\n readonly readOnly: boolean;\n readonly deletesRefused: boolean;\n readonly readOnlyColumns: readonly string[];\n readonly listScope: string | null;\n readonly realtime: boolean;\n /**\n * The table has a `created_by` column. A signed-in insert that omits it is stamped with the\n * session's person (row security on tables like `workbench.notes` refuses an insert whose\n * `created_by` is not `auth.uid()`). Optional: descriptors generated before 0.25.0 lack it.\n */\n readonly createdBy?: boolean;\n readonly [ROW]?: { row: Row; insert: Insert; update: Update };\n}\n\nexport interface FunctionDescriptor<Args = Record<string, unknown>, Result = unknown> {\n readonly schema: string;\n readonly name: string;\n readonly roles: readonly (\"anon\" | \"authenticated\" | \"service_role\")[];\n readonly registered: boolean;\n readonly [ROW]?: { args: Args; result: Result };\n}\n\nexport type RowOf<T> = T extends TableDescriptor<infer R, any, any> ? R : never;\nexport type InsertOf<T> = T extends TableDescriptor<any, infer I, any> ? I : never;\nexport type UpdateOf<T> = T extends TableDescriptor<any, any, infer U> ? U : never;\nexport type ArgsOf<F> = F extends FunctionDescriptor<infer A, any> ? A : never;\nexport type ResultOf<F> = F extends FunctionDescriptor<any, infer R> ? R : never;\n\n// ─── the connection ─────────────────────────────────────────────────────────\n\ninterface PostgrestError {\n message: string;\n code?: string;\n details?: string | null;\n hint?: string | null;\n}\ninterface Resp<T> {\n data: T | null;\n error: PostgrestError | null;\n count?: number | null;\n}\n/** The slice of a supabase-js query builder the doors use (structural — any supabase-js 2.x client). */\nexport interface QueryLike extends PromiseLike<Resp<any>> {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n eq(column: string, value: unknown): QueryLike;\n is(column: string, value: null | boolean): QueryLike;\n order(column: string, opts?: { ascending?: boolean }): QueryLike;\n range(from: number, to: number): QueryLike;\n maybeSingle(): PromiseLike<Resp<any>>;\n single(): PromiseLike<Resp<any>>;\n}\n/** The slice of a supabase-js table builder (`client.schema(s).from(t)`) the doors use. */\nexport interface TableLike {\n select(columns?: string, opts?: { count?: \"exact\" | \"planned\" | \"estimated\"; head?: boolean }): QueryLike;\n insert(values: any): QueryLike;\n update(values: any): QueryLike;\n delete(): QueryLike;\n}\n/** The slice of a supabase-js client the doors use (a real `SupabaseClient` is assignable — doors.types.test.ts). */\nexport interface SupabaseLike {\n schema(name: string): {\n /**\n * A table builder (read as `TableLike` inside the doors). Typed `unknown` on purpose: matching\n * supabase-js's deep builder generics structurally costs TS2589 in every consumer.\n */\n from(table: string): unknown;\n rpc(fn: string, args?: Record<string, unknown>, opts?: { count?: \"exact\" | \"planned\" | \"estimated\" }): PromiseLike<Resp<any>>;\n };\n auth?: {\n getSession(): Promise<{ data: { session: { access_token: string; user?: { id: string } | null } | null } }>;\n onAuthStateChange(cb: (event: string, session: unknown) => void): { data: { subscription: { unsubscribe(): void } } };\n };\n}\n\n/** The credential shape `@ai-matrx/api`'s `CredentialsPort` accepts (structural; data owns the session). */\nexport interface UserCredentialsPort {\n get(): Promise<{ kind: \"user\"; accessToken: string } | null>;\n onChange?(cb: () => void): () => void;\n}\n\nexport interface Db {\n readonly client: SupabaseLike;\n readonly role: \"user\" | \"service\";\n /** The active organization — where NEW entity rows are saved. Never a read filter. */\n activeOrganization(): string | null;\n /** The session as `@ai-matrx/api`'s `user` credential (one sign-in feeds both lanes). */\n readonly credentials: UserCredentialsPort;\n}\n\nexport interface CreateDbOptions {\n /** The app's existing Supabase client (browser, RSC, route or the host's chat client). */\n client: SupabaseLike;\n /** Where new entity rows are saved. Read lazily on every insert; never consulted by a read. */\n organization?: () => string | null | undefined;\n}\n\nfunction credentialsOf(client: SupabaseLike): UserCredentialsPort {\n return {\n async get() {\n if (!client.auth) return null;\n const { data } = await client.auth.getSession();\n return data.session ? { kind: \"user\", accessToken: data.session.access_token } : null;\n },\n onChange(cb) {\n if (!client.auth) return () => {};\n const { data } = client.auth.onAuthStateChange(() => cb());\n return () => data.subscription.unsubscribe();\n },\n };\n}\n\n/** The one database connection for a signed-in (or anonymous) person. */\nexport function createDb(options: CreateDbOptions): Db {\n const { client, organization } = options;\n return {\n client,\n role: \"user\",\n activeOrganization: () => organization?.() ?? null,\n credentials: credentialsOf(client),\n };\n}\n\n/**\n * Service-role doors — servers only. No session, no binding: every entity insert\n * names its organization. Throws in a browser bundle.\n */\nexport function createServiceDb(options: { client: SupabaseLike }): Db {\n if (typeof (globalThis as { window?: unknown }).window !== \"undefined\") {\n throw new DoorRefusedError(\"createServiceDb\", \"the service role never runs in a browser\");\n }\n return { client: options.client, role: \"service\", activeOrganization: () => null, credentials: credentialsOf(options.client) };\n}\n\n// ─── refusals ───────────────────────────────────────────────────────────────\n\nexport class DoorRefusedError extends Error {\n constructor(\n readonly door: string,\n readonly reason: string,\n ) {\n super(`${door}: ${reason}`);\n this.name = \"DoorRefusedError\";\n }\n}\n\nexport class DatabaseError extends Error {\n constructor(\n readonly door: string,\n readonly cause: PostgrestError,\n ) {\n super(`${door}: ${cause.message}${cause.code ? ` (${cause.code})` : \"\"}`);\n this.name = \"DatabaseError\";\n }\n}\n\ntype Op = \"read\" | \"insert\" | \"update\" | \"remove\" | \"archive\";\nconst CLIENT_DOORS: Record<TableType, readonly Op[]> = {\n entity: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n detail: [\"read\", \"insert\", \"update\", \"remove\", \"archive\"],\n ledger: [\"read\", \"insert\"],\n reference: [\"read\"],\n system: [\"read\"],\n restricted: [],\n deprecated: [],\n};\n\nconst qualified = (t: { schema: string; name: string }) => `${t.schema}.${t.name}`;\n\n/** Refuse before the wire when the descriptor's rules do not open this door. */\nexport function assertDoor(db: Db, table: TableDescriptor<any, any, any>, op: Op): void {\n const door = `${op} ${qualified(table)}`;\n if (table.type === \"deprecated\") throw new DoorRefusedError(door, \"the table is deprecated — no door opens on it\");\n if (table.kind !== \"table\" && op !== \"read\") throw new DoorRefusedError(door, `a ${table.kind.replace(\"_\", \" \")} is read-only`);\n if (db.role === \"service\") return;\n if (table.type && !CLIENT_DOORS[table.type].includes(op))\n throw new DoorRefusedError(door, `a ${table.type} table opens only ${CLIENT_DOORS[table.type].join(\", \") || \"service\"} doors to a client`);\n if (op !== \"read\" && table.readOnly) throw new DoorRefusedError(door, \"the table is read-only to clients (client_read_only)\");\n if (op === \"remove\" && table.deletesRefused) throw new DoorRefusedError(door, \"clients may not delete from this table (client_deletes_refused)\");\n}\n\nfunction check<T>(door: string, res: Resp<T>): T {\n if (res.error) throw new DatabaseError(door, res.error);\n return res.data as T;\n}\n\nconst from = (db: Db, t: TableDescriptor<any, any, any>) => db.client.schema(t.schema).from(t.name) as TableLike;\n\nfunction byPk(q: QueryLike, t: TableDescriptor<any, any, any>, id: unknown): QueryLike {\n if (t.pk.length === 1) return q.eq(t.pk[0] as string, id);\n const key = id as Record<string, unknown>;\n return t.pk.reduce((acc, col) => acc.eq(col, key[col]), q);\n}\n\n// ─── reads (never the active organization; soft-deleted rows excluded unless asked) ─\n\ntype Col<R> = Extract<keyof R, string>;\n\n/** The typed filter a `Where` callback receives — column names and values checked against the row. */\nexport interface Filter<R> {\n eq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n neq<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n gte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lt<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n lte<K extends Col<R>>(column: K, value: NonNullable<R[K]>): Filter<R>;\n like(column: Col<R>, pattern: string): Filter<R>;\n ilike(column: Col<R>, pattern: string): Filter<R>;\n in<K extends Col<R>>(column: K, values: readonly NonNullable<R[K]>[]): Filter<R>;\n is(column: Col<R>, value: null | boolean): Filter<R>;\n contains(column: Col<R>, value: unknown): Filter<R>;\n overlaps(column: Col<R>, value: unknown): Filter<R>;\n textSearch(column: Col<R>, query: string, opts?: { type?: \"plain\" | \"phrase\" | \"websearch\"; config?: string }): Filter<R>;\n /** PostgREST's raw `or` grammar (`\"pinned.eq.true,starred.eq.true\"`). */\n or(filters: string, opts?: { referencedTable?: string }): Filter<R>;\n not(column: Col<R>, operator: string, value: unknown): Filter<R>;\n}\n\n/** A filter callback — the same builder `readAllRows` callers write today, typed to the row. */\nexport type Where<R = any> = (q: Filter<R>) => Filter<R>;\n\nexport interface ReadOptions<C> {\n /** Only these columns (the result is typed to them). Default every column. */\n columns?: readonly C[];\n /** Include soft-deleted rows (default: excluded on tables with `deleted_at`). */\n includeDeleted?: boolean;\n}\n\nconst selectOf = (columns?: readonly string[]) => (columns && columns.length ? columns.join(\",\") : \"*\");\n\nfunction live(q: any, table: TableDescriptor<any, any, any>, includeDeleted?: boolean): any {\n return table.softDelete && !includeDeleted ? q.is(\"deleted_at\", null) : q;\n}\n\nfunction ordered(q: any, table: TableDescriptor<any, any, any>, orderBy?: string, ascending = true): any {\n // The FULL primary key, so paging is stable on composite keys.\n const cols = orderBy ? [orderBy, ...table.pk.filter((c) => c !== orderBy)] : table.pk.length ? table.pk : [\"id\"];\n return cols.reduce((acc: any, c) => acc.order(c, { ascending }), q);\n}\n\n/** One row by primary key (a composite key takes `{ col: value }`), or null. */\nexport async function read<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: ReadOptions<C> = {},\n): Promise<Pick<R, C> | null> {\n assertDoor(db, table, \"read\");\n const q = live(byPk(from(db, table).select(selectOf(opts.columns)), table, id), table, opts.includeDeleted);\n return check(`read ${qualified(table)}`, await q.maybeSingle());\n}\n\n/** EVERY matching row, paged past the 1,000-row cap (ordered by the full key); throws on a short read. */\nexport async function listAll<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n where?: Where<R>,\n opts: ReadOptions<C> & { orderBy?: Col<R>; pageSize?: number; maxRows?: number } = {},\n): Promise<Pick<R, C>[]> {\n assertDoor(db, table, \"read\");\n return readAllRows<Pick<R, C>>(\n ({ from: lo, to: hi }) => {\n let q: any = live(from(db, table).select(selectOf(opts.columns), { count: \"exact\" }), table, opts.includeDeleted);\n if (where) q = where(q);\n // The database's own error (code, details) reaches the caller as a DatabaseError,\n // exactly like every other door — never a re-worded readAllRows message.\n return Promise.resolve(ordered(q, table, opts.orderBy).range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(`listAll ${qualified(table)}`, res.error);\n return res;\n });\n },\n {\n label: `listAll ${qualified(table)}`,\n ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}),\n ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}),\n },\n );\n}\n\n/** One explicit window (`from`..`to`, inclusive) plus the exact total. */\nexport async function page<R, I, U, C extends Col<R> = Col<R>>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n window: ReadOptions<C> & { from: number; to: number; where?: Where<R>; orderBy?: Col<R>; ascending?: boolean },\n): Promise<{ rows: Pick<R, C>[]; total: number | null }> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(selectOf(window.columns), { count: \"exact\" }), table, window.includeDeleted);\n if (window.where) q = window.where(q);\n const res = await ordered(q, table, window.orderBy, window.ascending ?? true).range(window.from, window.to);\n return { rows: check(`page ${qualified(table)}`, res) ?? [], total: res.count ?? null };\n}\n\n/** The exact count of matching rows (no rows transferred). */\nexport async function count<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, where?: Where<R>, opts: { includeDeleted?: boolean } = {}): Promise<number> {\n assertDoor(db, table, \"read\");\n let q: any = live(from(db, table).select(\"*\", { count: \"exact\", head: true }), table, opts.includeDeleted);\n if (where) q = where(q);\n const res = await q;\n check(`count ${qualified(table)}`, res);\n return res.count ?? 0;\n}\n\n// ─── writes ─────────────────────────────────────────────────────────────────\n\nfunction withOrganization(db: Db, table: TableDescriptor<any, any, any>, values: Record<string, unknown>): Record<string, unknown> {\n const door = `insert ${qualified(table)}`;\n const org = table.org;\n if (org.rule === \"parent\" && values[org.via] != null) {\n // A child row: its organization is its parent's. Never the active organization —\n // `inherit_org_from_parent` fills organization_id; an explicit one is checked by\n // `refuse_org_foreign_to_parent`. So adding to a record in another organization works.\n return values;\n }\n if (org.rule === \"binding\" || org.rule === \"parent\") {\n // An entity row, or a row whose optional parent is absent (a note outside any folder),\n // is saved in the named organization, else the active one.\n if (values.organization_id != null) return values;\n const active = db.activeOrganization();\n if (!active) {\n throw new DoorRefusedError(\n door,\n db.role === \"service\" ? \"a service insert names its organization_id\" : \"no organization: pass organization_id or bind createDb({ organization })\",\n );\n }\n return { ...values, organization_id: active };\n }\n return values;\n}\n\n/** Insert one row (or many) and return what the database stored. */\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: I): Promise<R>;\nexport async function insert<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, values: readonly I[]): Promise<R[]>;\nexport async function insert(db: Db, table: TableDescriptor<any, any, any>, values: unknown): Promise<unknown> {\n assertDoor(db, table, \"insert\");\n const many = Array.isArray(values);\n let rows = (many ? values : [values]).map((v) => withOrganization(db, table, v as Record<string, unknown>));\n if (table.createdBy && db.role === \"user\" && rows.some((r) => r.created_by == null) && db.client.auth) {\n // Stamp the signed-in person (no session: sent as given, and row security answers).\n const { data } = await db.client.auth.getSession();\n const person = data.session?.user?.id;\n if (person) rows = rows.map((r) => (r.created_by == null ? { ...r, created_by: person } : r));\n }\n const res = await from(db, table).insert(rows).select(\"*\");\n const stored = check(`insert ${qualified(table)}`, res) as unknown[];\n return many ? stored : stored[0];\n}\n\n/** Every write on a versioned table is explicit: `{ expectedVersion }` (guarded) or `{ overwrite: true }`. */\nexport type UpdateGuard = { expectedVersion: number } | { overwrite: true };\n\nexport type WriteResult<R> =\n | { status: \"saved\"; row: R; rebasedFrom?: { expectedVersion: number; currentVersion: number } }\n | { status: \"conflict\"; currentRow: R; currentVersion: number }\n | { status: \"not_found\" };\n\n/** The one guarded write every update-shaped door goes through. */\nasync function writeRow<R>(db: Db, table: TableDescriptor<any, any, any>, id: unknown, values: Record<string, unknown>, door: string, guard?: UpdateGuard): Promise<WriteResult<R>> {\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) {\n throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned — nothing would guard it; pass no guard\");\n }\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard && guard.overwrite)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n if (expected !== undefined) {\n return (await guardedUpdate<any>({\n expectedVersion: expected,\n applyUpdate: ({ expectedVersion, nextVersion }) =>\n byPk(from(db, table).update({ ...values, version: nextVersion }), table, id).eq(\"version\", expectedVersion).select(\"*\").maybeSingle(),\n fetchCurrent: () => byPk(from(db, table).select(\"*\"), table, id).maybeSingle(),\n })) as WriteResult<R>;\n }\n const row = check(door, await byPk(from(db, table).update(values), table, id).select(\"*\").maybeSingle());\n return row ? { status: \"saved\", row } : { status: \"not_found\" };\n}\n\n/**\n * Update one row by primary key. On a versioned table the guard is required:\n * `{ expectedVersion }` (a conflict comes back, never a silent overwrite) or\n * `{ overwrite: true }`; on an unversioned table `expectedVersion` is refused.\n */\nexport async function update<R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, patch: U, guard?: UpdateGuard): Promise<WriteResult<R>> {\n assertDoor(db, table, \"update\");\n const door = `update ${qualified(table)}`;\n const values = patch as Record<string, unknown>;\n if (db.role === \"user\") {\n const blocked = table.readOnlyColumns.filter((c) => c in values);\n if (blocked.length) throw new DoorRefusedError(door, `read-only to clients: ${blocked.join(\", \")}`);\n }\n return writeRow<R>(db, table, id, values, door, guard);\n}\n\n/**\n * Remove one row the way the table removes: soft delete (`deleted_at`) where it\n * has one; an archive-only table refuses (use `archive`); a hard delete only where\n * the table has neither. Guarded like `update` on a versioned table.\n */\nexport async function remove<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n guard?: UpdateGuard,\n): Promise<{ status: \"soft_deleted\" | \"deleted\" | \"not_found\" } | { status: \"conflict\"; currentVersion: number }> {\n assertDoor(db, table, \"remove\");\n const door = `remove ${qualified(table)}`;\n if (table.softDelete) {\n const res = await writeRow<R>(db, table, id, { deleted_at: new Date().toISOString() }, door, guard);\n return res.status === \"saved\" ? { status: \"soft_deleted\" } : res.status === \"conflict\" ? { status: \"conflict\", currentVersion: res.currentVersion } : res;\n }\n if (table.archive) throw new DoorRefusedError(door, \"this table archives instead of deleting — use archive()\");\n const expected = guard && \"expectedVersion\" in guard ? guard.expectedVersion : undefined;\n if (expected !== undefined && !table.versioned) throw new DoorRefusedError(door, \"expectedVersion on a table that is not versioned\");\n if (table.versioned && expected === undefined && !(guard && \"overwrite\" in guard)) {\n throw new DoorRefusedError(door, \"a versioned table takes { expectedVersion } or { overwrite: true }\");\n }\n let q = byPk(from(db, table).delete(), table, id);\n if (expected !== undefined) q = q.eq(\"version\", expected);\n const rows = check(door, await q.select(\"*\")) as unknown[] | null;\n if (rows && rows.length) return { status: \"deleted\" };\n if (expected !== undefined) {\n const current = check(door, await byPk(from(db, table).select(\"version\"), table, id).maybeSingle()) as { version: number } | null;\n if (current) return { status: \"conflict\", currentVersion: current.version };\n }\n return { status: \"not_found\" };\n}\n\n/** Archive (or, with `restore`, bring back) one row by the table's archive column. Guarded like `update`. */\nexport async function archive<R, I, U>(\n db: Db,\n table: TableDescriptor<R, I, U>,\n id: unknown,\n opts: { restore?: boolean; guard?: UpdateGuard } = {},\n): Promise<WriteResult<R>> {\n assertDoor(db, table, \"archive\");\n const door = `${opts.restore ? \"restore\" : \"archive\"} ${qualified(table)}`;\n let values: Record<string, unknown>;\n if (table.archive === \"archived_at\") values = { archived_at: opts.restore ? null : new Date().toISOString() };\n else if (table.archive === \"is_archived\") values = { is_archived: !opts.restore };\n else if (table.softDelete && opts.restore) values = { deleted_at: null };\n else throw new DoorRefusedError(door, \"the table has no archive column\");\n return writeRow<R>(db, table, id, values, door, opts.guard);\n}\n\n/** Bring back an archived or soft-deleted row. */\nexport const restore = <R, I, U>(db: Db, table: TableDescriptor<R, I, U>, id: unknown, guard?: UpdateGuard) =>\n archive(db, table, id, guard ? { restore: true, guard } : { restore: true });\n\n// ─── functions ──────────────────────────────────────────────────────────────\n\n/** Call a database function through its generated door (PostgREST chooses an overload by argument names). */\nexport async function call<F extends FunctionDescriptor<any, any>>(db: Db, fn: F, args: ArgsOf<F>): Promise<ResultOf<F>> {\n const door = `call ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n return check(door, await db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>));\n}\n\n/** One ordering column of a set-returning function's rows (PostgREST `order`). */\nexport type CallOrder = string | { column: string; ascending?: boolean; nullsFirst?: boolean };\n\n/**\n * EVERY row a set-returning function answers, paged past the 1,000-row cap by\n * `range` over a stable `order` (name enough columns to make it total — end on a key);\n * throws a `DatabaseError` with the database's own error, or `IncompleteReadError` on a short read.\n */\nexport async function callAll<F extends FunctionDescriptor<any, any>>(\n db: Db,\n fn: F,\n args: ArgsOf<F>,\n opts: { order: readonly CallOrder[]; pageSize?: number; maxRows?: number },\n): Promise<ResultOf<F> extends readonly (infer Row)[] ? Row[] : ResultOf<F>[]> {\n const door = `callAll ${fn.schema}.${fn.name}`;\n const role = db.role === \"service\" ? \"service_role\" : \"authenticated\";\n if (!fn.roles.includes(role) && !(role === \"authenticated\" && fn.roles.includes(\"anon\"))) {\n throw new DoorRefusedError(door, `not executable by ${role} (executable by: ${fn.roles.join(\", \") || \"nobody\"})`);\n }\n if (!opts.order.length) throw new DoorRefusedError(door, \"a paged read needs an order (end on a key) — pages over no order can skip or repeat rows\");\n return readAllRows<any>(\n ({ from: lo, to: hi }) => {\n let q: any = db.client.schema(fn.schema).rpc(fn.name, args as Record<string, unknown>, { count: \"exact\" });\n for (const o of opts.order) {\n q = typeof o === \"string\" ? q.order(o) : q.order(o.column, { ascending: o.ascending ?? true, ...(o.nullsFirst !== undefined ? { nullsFirst: o.nullsFirst } : {}) });\n }\n return Promise.resolve(q.range(lo, hi)).then((res: Resp<any>) => {\n if (res.error) throw new DatabaseError(door, res.error);\n return res;\n });\n },\n { label: door, ...(opts.pageSize !== undefined ? { pageSize: opts.pageSize } : {}), ...(opts.maxRows !== undefined ? { maxRows: opts.maxRows } : {}) },\n ) as never;\n}\n\n// ─── a table chosen at runtime ──────────────────────────────────────────────\n\n/** One schema's flags (`<schema>.flags.ts` from `matrx-data generate --flags-index`): \"schema.table\" → descriptor. */\nexport type FlagsIndex = Readonly<Record<string, TableDescriptor>>;\n/** Per-schema loaders (`flags-index.ts`): only the schema a runtime name names is loaded (pay-only). */\nexport type FlagLoaders = Readonly<Record<string, () => Promise<FlagsIndex>>>;\n\n/** The door for a table whose name arrives at runtime — loosely typed, the same rules. */\nexport function tableByName(index: FlagsIndex, name: string): TableDescriptor {\n const t = index[name];\n if (!t) throw new DoorRefusedError(`tableByName ${name}`, \"no such table in the database description (schema-qualify it: schema.table)\");\n return t;\n}\n\n/** `tableByName` over per-schema loaders: loads only the named schema's flags. */\nexport async function loadTableByName(loaders: FlagLoaders, name: string): Promise<TableDescriptor> {\n const schema = name.slice(0, name.indexOf(\".\"));\n const load = schema ? loaders[schema] : undefined;\n if (!load) throw new DoorRefusedError(`tableByName ${name}`, \"no such schema in the database description (schema-qualify it: schema.table)\");\n return tableByName(await load(), name);\n}\n\n// ─── realtime ───────────────────────────────────────────────────────────────\n\n/** The `postgres_changes` filter for a table — only published tables have one (an unpublished table delivers nothing). */\nexport function changesOf(table: TableDescriptor<any, any, any>): { schema: string; table: string } {\n if (!table.realtime) throw new DoorRefusedError(`changesOf ${qualified(table)}`, \"the table is not in the supabase_realtime publication\");\n return { schema: table.schema, table: table.name };\n}\n","/**\n * @ai-matrx/data/auth — sign-in and session doors (design §2.6).\n *\n * data owns Supabase sign-in because it is the same client and the same session the\n * database doors use. A package or app asks these doors instead of reaching into\n * `client.auth.*`; the session leaves for `@ai-matrx/api` only as the `user` credential\n * (`userCredentials(client)` = `createDb({ client }).credentials`, the shape api's\n * `CredentialsPort` takes), so api reads the token and never signs anyone in.\n *\n * const session = await getSession(supabase); // Session | null\n * const person = await getPerson(supabase); // User | null (verified by the server)\n * const stop = onSessionChange(supabase, (event, session) => …);\n * await signInWithPassword(supabase, { email, password }); // throws SignInError\n * const { url } = await signInWithOAuth(supabase, { provider: \"google\", redirectTo });\n * await signOut(supabase);\n * await updatePerson(supabase, { data: { full_name } });\n *\n * Every refusal throws `SignInError` carrying the auth server's own message, status and code.\n */\nimport type {\n AuthChangeEvent,\n Provider,\n Session,\n SupabaseClient,\n User,\n UserAttributes,\n} from \"@supabase/supabase-js\";\n\nimport { createDb, type SupabaseLike, type UserCredentialsPort } from \"./db/doors\";\n\nexport type { AuthChangeEvent, Session, User, UserAttributes };\n\n/** Any supabase-js client (browser, server, route, extension) — only its `auth` is used here. */\nexport type AuthClient = Pick<SupabaseClient<any, any, any>, \"auth\">;\n\n/** A refusal from the auth server — its own message, plus status/code when it sent them. */\nexport class SignInError extends Error {\n constructor(\n readonly door: string,\n message: string,\n readonly status?: number,\n readonly code?: string,\n ) {\n super(`${door}: ${message}`);\n this.name = \"SignInError\";\n }\n}\n\ntype AuthErr = { message: string; status?: number | undefined; code?: string | undefined } | null;\nfunction check<T>(door: string, data: T, error: AuthErr): T {\n if (error) throw new SignInError(door, error.message, error.status, error.code);\n return data;\n}\n\n/** The stored session (no network unless a refresh is due), or null when signed out. */\nexport async function getSession(client: AuthClient): Promise<Session | null> {\n const { data, error } = await client.auth.getSession();\n return check(\"getSession\", data, error).session;\n}\n\n/** The signed-in person, verified by the auth server, or null when signed out. */\nexport async function getPerson(client: AuthClient): Promise<User | null> {\n const { data, error } = await client.auth.getUser();\n if (error && (error.name === \"AuthSessionMissingError\" || error.status === 401)) return null;\n return check(\"getPerson\", data, error).user;\n}\n\n/** Hear every session change (sign-in, refresh, sign-out). Returns the unsubscribe. */\nexport function onSessionChange(\n client: AuthClient,\n cb: (event: AuthChangeEvent, session: Session | null) => void,\n): () => void {\n const { data } = client.auth.onAuthStateChange(cb);\n return () => data.subscription.unsubscribe();\n}\n\nexport async function signInWithPassword(\n client: AuthClient,\n credentials: { email: string; password: string } | { phone: string; password: string },\n): Promise<{ session: Session; person: User }> {\n const { data, error } = await client.auth.signInWithPassword(credentials);\n const ok = check(\"signInWithPassword\", data, error);\n return { session: ok.session as Session, person: ok.user as User };\n}\n\nexport async function signInWithOAuth(\n client: AuthClient,\n options: { provider: Provider; redirectTo?: string; scopes?: string; queryParams?: Record<string, string>; skipBrowserRedirect?: boolean },\n): Promise<{ provider: Provider; url: string | null }> {\n const { provider, ...rest } = options;\n const { data, error } = await client.auth.signInWithOAuth({ provider, options: rest });\n const ok = check(\"signInWithOAuth\", data, error);\n return { provider: ok.provider, url: ok.url ?? null };\n}\n\nexport async function signOut(client: AuthClient, scope: \"global\" | \"local\" | \"others\" = \"global\"): Promise<void> {\n const { error } = await client.auth.signOut({ scope });\n check(\"signOut\", null, error);\n}\n\n/** Change the signed-in person's email, phone, password or metadata. */\nexport async function updatePerson(client: AuthClient, attributes: UserAttributes): Promise<User> {\n const { data, error } = await client.auth.updateUser(attributes);\n return check(\"updatePerson\", data, error).user as User;\n}\n\n/** The session as `@ai-matrx/api`'s `user` credential — hand it to api's `CredentialsPort`. */\nexport function userCredentials(client: SupabaseLike): UserCredentialsPort {\n return createDb({ client }).credentials;\n}\n"],"mappings":";AA6JA,SAAS,cAAc,QAA2C;AAChE,SAAO;AAAA,IACL,MAAM,MAAM;AACV,UAAI,CAAC,OAAO,KAAM,QAAO;AACzB,YAAM,EAAE,KAAK,IAAI,MAAM,OAAO,KAAK,WAAW;AAC9C,aAAO,KAAK,UAAU,EAAE,MAAM,QAAQ,aAAa,KAAK,QAAQ,aAAa,IAAI;AAAA,IACnF;AAAA,IACA,SAAS,IAAI;AACX,UAAI,CAAC,OAAO,KAAM,QAAO,MAAM;AAAA,MAAC;AAChC,YAAM,EAAE,KAAK,IAAI,OAAO,KAAK,kBAAkB,MAAM,GAAG,CAAC;AACzD,aAAO,MAAM,KAAK,aAAa,YAAY;AAAA,IAC7C;AAAA,EACF;AACF;AAGO,SAAS,SAAS,SAA8B;AACrD,QAAM,EAAE,QAAQ,aAAa,IAAI;AACjC,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,oBAAoB,MAAM,eAAe,KAAK;AAAA,IAC9C,aAAa,cAAc,MAAM;AAAA,EACnC;AACF;;;ACjJO,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YACW,MACT,SACS,QACA,MACT;AACA,UAAM,GAAG,IAAI,KAAK,OAAO,EAAE;AALlB;AAEA;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EAPW;AAAA,EAEA;AAAA,EACA;AAKb;AAGA,SAAS,MAAS,MAAc,MAAS,OAAmB;AAC1D,MAAI,MAAO,OAAM,IAAI,YAAY,MAAM,MAAM,SAAS,MAAM,QAAQ,MAAM,IAAI;AAC9E,SAAO;AACT;AAGA,eAAsB,WAAW,QAA6C;AAC5E,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,WAAW;AACrD,SAAO,MAAM,cAAc,MAAM,KAAK,EAAE;AAC1C;AAGA,eAAsB,UAAU,QAA0C;AACxE,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,QAAQ;AAClD,MAAI,UAAU,MAAM,SAAS,6BAA6B,MAAM,WAAW,KAAM,QAAO;AACxF,SAAO,MAAM,aAAa,MAAM,KAAK,EAAE;AACzC;AAGO,SAAS,gBACd,QACA,IACY;AACZ,QAAM,EAAE,KAAK,IAAI,OAAO,KAAK,kBAAkB,EAAE;AACjD,SAAO,MAAM,KAAK,aAAa,YAAY;AAC7C;AAEA,eAAsB,mBACpB,QACA,aAC6C;AAC7C,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,mBAAmB,WAAW;AACxE,QAAM,KAAK,MAAM,sBAAsB,MAAM,KAAK;AAClD,SAAO,EAAE,SAAS,GAAG,SAAoB,QAAQ,GAAG,KAAa;AACnE;AAEA,eAAsB,gBACpB,QACA,SACqD;AACrD,QAAM,EAAE,UAAU,GAAG,KAAK,IAAI;AAC9B,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,gBAAgB,EAAE,UAAU,SAAS,KAAK,CAAC;AACrF,QAAM,KAAK,MAAM,mBAAmB,MAAM,KAAK;AAC/C,SAAO,EAAE,UAAU,GAAG,UAAU,KAAK,GAAG,OAAO,KAAK;AACtD;AAEA,eAAsB,QAAQ,QAAoB,QAAuC,UAAyB;AAChH,QAAM,EAAE,MAAM,IAAI,MAAM,OAAO,KAAK,QAAQ,EAAE,MAAM,CAAC;AACrD,QAAM,WAAW,MAAM,KAAK;AAC9B;AAGA,eAAsB,aAAa,QAAoB,YAA2C;AAChG,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK,WAAW,UAAU;AAC/D,SAAO,MAAM,gBAAgB,MAAM,KAAK,EAAE;AAC5C;AAGO,SAAS,gBAAgB,QAA2C;AACzE,SAAO,SAAS,EAAE,OAAO,CAAC,EAAE;AAC9B;","names":[]}