cvx-kit 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -23,6 +23,23 @@ bun add cvx-kit
23
23
  target ES2025, minified, deps external, publint-verified
24
24
  - `bun run build:codegen` — re-run `convex codegen` for both components, then build
25
25
 
26
+ ## Documentation
27
+
28
+ Full docs ship with the package — `node_modules/cvx-kit/docs/` when installed,
29
+ authored in [`src/docs/`](./src/docs/):
30
+
31
+ - [`architecture.md`](./src/docs/architecture.md) — the layered application shape the kit encodes
32
+ - [`conventions.md`](./src/docs/conventions.md) — the doctrine: folder structure, file anatomy, naming grammar, import rules
33
+ - [`zod-table.md`](./src/docs/zod-table.md) — one zod shape per entity, masked into every boundary
34
+ - [`auth.md`](./src/docs/auth.md) — auth-aware function constructors from injected policy
35
+ - [`commands.md`](./src/docs/commands.md) — Foundation, the audited command protocol, observability
36
+ - [`triggers.md`](./src/docs/triggers.md) — trigger registry, timestamps/append-only/no-delete
37
+ - [`approvals.md`](./src/docs/approvals.md) — declarative approval workflows
38
+ - [`tenancy.md`](./src/docs/tenancy.md) — row-level security (roles) with opt-in multi-tenant isolation
39
+ - [`maintainability.md`](./src/docs/maintainability.md) — keeping a Convex app maintainable long-term
40
+ - [`migration.md`](./src/docs/migration.md) — restructuring an existing raw Convex project into this architecture
41
+ - [`llms.md`](./src/docs/llms.md) — condensed reference to paste into an LLM/agent context
42
+
26
43
  ## Modules
27
44
 
28
45
  ### `cvx-kit/zod-table`
package/dist/auth.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ErrorFactory } from "./errors.mjs";
2
- import { ActionBuilder, DocumentByInfo, GenericActionCtx, GenericDataModel, GenericMutationCtx, GenericQueryCtx, GenericTableInfo, IndexNames, IndexRange, IndexRangeBuilder, MutationBuilder, NamedIndex, Query, QueryBuilder, QueryInitializer, UserIdentity } from "convex/server";
2
+ import { RLSConfig, Rules } from "./tenancy.mjs";
3
+ import { ActionBuilder, DocumentByInfo, GenericActionCtx, GenericDataModel, GenericMutationCtx, GenericQueryCtx, GenericTableInfo, IndexNames, IndexRange, IndexRangeBuilder, MutationBuilder, NamedIndex, Query, QueryBuilder, QueryInitializer, TableNamesInDataModel, UserIdentity } from "convex/server";
3
4
  import { Triggers } from "convex-helpers/server/triggers";
4
5
  //#region src/auth.d.ts
5
6
  /** The default vocabulary; every app can supply its own via the Role generic. */
@@ -20,7 +21,38 @@ type AuthBundle<Role extends string> = Readonly<{
20
21
  }>;
21
22
  role: Role;
22
23
  actor: Actor<Role>;
24
+ /** The resolved tenant — security.tenancy.resolve, or the organization id. */
25
+ tenant: string;
23
26
  }>;
27
+ /**
28
+ * Row-level security configuration. Tenant isolation and role-level rules
29
+ * compose (AND) onto the same wrapped database: queries get a wrapped
30
+ * reader, mutations get triggers first, then a wrapped writer.
31
+ */
32
+ type SecurityConfig<DataModel extends GenericDataModel, Role extends string> = {
33
+ /**
34
+ * Tenant isolation. `tables` is THE registry: every listed table gets
35
+ * read/insert/modify gated on row.tenant === ctx.tenant. Pair with
36
+ * tenantTable (schema) and tenantOwnership (triggers) on the same list.
37
+ */
38
+ tenancy?: {
39
+ tables: readonly TableNamesInDataModel<DataModel>[];
40
+ /** Maps the auth bundle to the tenant; defaults to org.organizationId. */
41
+ resolve?: (bundle: Omit<AuthBundle<Role>, 'tenant'>) => string;
42
+ };
43
+ /**
44
+ * Role-level (or any policy) rules derived from the authenticated bundle,
45
+ * composed AND-wise with tenant isolation. E.g. gate `modify` on a table
46
+ * to admin roles.
47
+ */
48
+ rules?: (bundle: AuthBundle<Role>) => Rules<unknown, DataModel>;
49
+ /**
50
+ * Policy for tables with no rule. Defaults to 'deny' when tenancy is
51
+ * configured (unlisted tables are unreachable — fail closed), 'allow'
52
+ * otherwise.
53
+ */
54
+ defaultPolicy?: RLSConfig['defaultPolicy'];
55
+ };
24
56
  type AnyAuthContext<DataModel extends GenericDataModel> = GenericQueryCtx<DataModel> | GenericMutationCtx<DataModel> | GenericActionCtx<DataModel>;
25
57
  type AuthFunctionsConfig<DataModel extends GenericDataModel, Role extends string> = {
26
58
  /** The consumer's generated function builders (from ./_generated/server). */
@@ -64,6 +96,12 @@ type AuthFunctionsConfig<DataModel extends GenericDataModel, Role extends string
64
96
  wrapDB?: (ctx: GenericMutationCtx<DataModel>) => GenericMutationCtx<DataModel>;
65
97
  /** Upper bound for include() reads. */
66
98
  maxIncludedQueryRows?: number;
99
+ /**
100
+ * Row-level security: tenant isolation (from a table registry) and
101
+ * role-level rules, applied structurally inside every auth*, role*, and
102
+ * admin* constructor. system* constructors stay unwrapped (trusted).
103
+ */
104
+ security?: SecurityConfig<DataModel, Role>;
67
105
  };
68
106
  /** WorkOS-style default: `member` writes; reader/writer/admin pass through. */
69
107
  declare function defaultRoleMap(roleSlug?: string): DefaultRole | null;
@@ -113,6 +151,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
113
151
  organizationId: string;
114
152
  role: Role;
115
153
  }>;
154
+ tenant: string;
116
155
  include: Include;
117
156
  }, {}, GenericQueryCtx<DataModel>, "public", object>;
118
157
  roleMutation: (...allowed: readonly Role[]) => import("convex-helpers/server/zod4").CustomBuilder<"mutation", {}, {
@@ -139,6 +178,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
139
178
  organizationId: string;
140
179
  role: Role;
141
180
  }>;
181
+ tenant: string;
142
182
  include: Include;
143
183
  }, {}, GenericMutationCtx<DataModel>, "public", object>;
144
184
  roleAction: (...allowed: readonly Role[]) => import("convex-helpers/server/zod4").CustomBuilder<"action", {}, {
@@ -148,7 +188,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
148
188
  scheduler: import("convex/server").Scheduler;
149
189
  auth: import("convex/server").Auth;
150
190
  storage: import("convex/server").StorageActionWriter;
151
- vectorSearch<TableName extends import("convex/server").TableNamesInDataModel<DataModel>, IndexName extends import("convex/server").VectorIndexNames<import("convex/server").NamedTableInfo<DataModel, TableName>>>(tableName: TableName, indexName: IndexName, query: {
191
+ vectorSearch<TableName extends TableNamesInDataModel<DataModel>, IndexName extends import("convex/server").VectorIndexNames<import("convex/server").NamedTableInfo<DataModel, TableName>>>(tableName: TableName, indexName: IndexName, query: {
152
192
  vector: number[];
153
193
  limit?: number;
154
194
  filter?: ((q: import("convex/server").VectorFilterBuilder<DocumentByInfo<import("convex/server").NamedTableInfo<DataModel, TableName>>, import("convex/server").NamedVectorIndex<import("convex/server").NamedTableInfo<DataModel, TableName>, IndexName>>) => import("convex/server").FilterExpression<boolean>) | undefined;
@@ -171,6 +211,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
171
211
  organizationId: string;
172
212
  role: Role;
173
213
  }>;
214
+ tenant: string;
174
215
  }, {}, GenericActionCtx<DataModel>, "public", object>;
175
216
  authQuery: import("convex-helpers/server/zod4").CustomBuilder<"query", {}, {
176
217
  db: import("convex/server").GenericDatabaseReader<DataModel>;
@@ -194,6 +235,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
194
235
  organizationId: string;
195
236
  role: Role;
196
237
  }>;
238
+ tenant: string;
197
239
  include: Include;
198
240
  }, {}, GenericQueryCtx<DataModel>, "public", object>;
199
241
  authMutation: import("convex-helpers/server/zod4").CustomBuilder<"mutation", {}, {
@@ -220,6 +262,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
220
262
  organizationId: string;
221
263
  role: Role;
222
264
  }>;
265
+ tenant: string;
223
266
  include: Include;
224
267
  }, {}, GenericMutationCtx<DataModel>, "public", object>;
225
268
  authAction: import("convex-helpers/server/zod4").CustomBuilder<"action", {}, {
@@ -229,7 +272,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
229
272
  scheduler: import("convex/server").Scheduler;
230
273
  auth: import("convex/server").Auth;
231
274
  storage: import("convex/server").StorageActionWriter;
232
- vectorSearch<TableName extends import("convex/server").TableNamesInDataModel<DataModel>, IndexName extends import("convex/server").VectorIndexNames<import("convex/server").NamedTableInfo<DataModel, TableName>>>(tableName: TableName, indexName: IndexName, query: {
275
+ vectorSearch<TableName extends TableNamesInDataModel<DataModel>, IndexName extends import("convex/server").VectorIndexNames<import("convex/server").NamedTableInfo<DataModel, TableName>>>(tableName: TableName, indexName: IndexName, query: {
233
276
  vector: number[];
234
277
  limit?: number;
235
278
  filter?: ((q: import("convex/server").VectorFilterBuilder<DocumentByInfo<import("convex/server").NamedTableInfo<DataModel, TableName>>, import("convex/server").NamedVectorIndex<import("convex/server").NamedTableInfo<DataModel, TableName>, IndexName>>) => import("convex/server").FilterExpression<boolean>) | undefined;
@@ -252,6 +295,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
252
295
  organizationId: string;
253
296
  role: Role;
254
297
  }>;
298
+ tenant: string;
255
299
  }, {}, GenericActionCtx<DataModel>, "public", object>;
256
300
  adminQuery: import("convex-helpers/server/zod4").CustomBuilder<"query", {}, {
257
301
  db: import("convex/server").GenericDatabaseReader<DataModel>;
@@ -275,6 +319,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
275
319
  organizationId: string;
276
320
  role: Role;
277
321
  }>;
322
+ tenant: string;
278
323
  include: Include;
279
324
  }, {}, GenericQueryCtx<DataModel>, "public", object>;
280
325
  adminMutation: import("convex-helpers/server/zod4").CustomBuilder<"mutation", {}, {
@@ -301,6 +346,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
301
346
  organizationId: string;
302
347
  role: Role;
303
348
  }>;
349
+ tenant: string;
304
350
  include: Include;
305
351
  }, {}, GenericMutationCtx<DataModel>, "public", object>;
306
352
  adminAction: import("convex-helpers/server/zod4").CustomBuilder<"action", {}, {
@@ -310,7 +356,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
310
356
  scheduler: import("convex/server").Scheduler;
311
357
  auth: import("convex/server").Auth;
312
358
  storage: import("convex/server").StorageActionWriter;
313
- vectorSearch<TableName extends import("convex/server").TableNamesInDataModel<DataModel>, IndexName extends import("convex/server").VectorIndexNames<import("convex/server").NamedTableInfo<DataModel, TableName>>>(tableName: TableName, indexName: IndexName, query: {
359
+ vectorSearch<TableName extends TableNamesInDataModel<DataModel>, IndexName extends import("convex/server").VectorIndexNames<import("convex/server").NamedTableInfo<DataModel, TableName>>>(tableName: TableName, indexName: IndexName, query: {
314
360
  vector: number[];
315
361
  limit?: number;
316
362
  filter?: ((q: import("convex/server").VectorFilterBuilder<DocumentByInfo<import("convex/server").NamedTableInfo<DataModel, TableName>>, import("convex/server").NamedVectorIndex<import("convex/server").NamedTableInfo<DataModel, TableName>, IndexName>>) => import("convex/server").FilterExpression<boolean>) | undefined;
@@ -333,6 +379,7 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
333
379
  organizationId: string;
334
380
  role: Role;
335
381
  }>;
382
+ tenant: string;
336
383
  }, {}, GenericActionCtx<DataModel>, "public", object>;
337
384
  systemMutation: import("convex-helpers/server/zod4").CustomBuilder<"mutation", {}, {
338
385
  db: import("convex/server").GenericDatabaseWriter<DataModel>;
@@ -349,5 +396,5 @@ declare function createAuthFunctions<DataModel extends GenericDataModel, const R
349
396
  systemAction: import("convex-helpers/server/zod4").CustomBuilder<"action", {}, GenericActionCtx<DataModel>, {}, GenericActionCtx<DataModel>, "internal", object>;
350
397
  };
351
398
  //#endregion
352
- export { Actor, AuthBundle, AuthFunctionsConfig, DefaultRole, Include, IncludedQuery, createAuthFunctions, createInclude, defaultRoleMap };
399
+ export { Actor, AuthBundle, AuthFunctionsConfig, DefaultRole, Include, IncludedQuery, SecurityConfig, createAuthFunctions, createInclude, defaultRoleMap };
353
400
  //# sourceMappingURL=auth.d.mts.map
package/dist/auth.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{defaultErrors as e}from"./errors.mjs";import{zCustomAction as t,zCustomMutation as n,zCustomQuery as r}from"convex-helpers/server/zod4";function i(e){let t=e?.trim().toLowerCase();return t===`member`?`writer`:t===`reader`||t===`writer`||t===`admin`?t:null}function a(t){let n=t?.errors??e,r=t?.maxRows??100;function i(e,t){return{when:(n,r)=>t||n==null?i(e,t):i(e,r(e,n)),matching:(n,r,a=!0)=>t||!a?i(e,t):i(e,e.withIndex(n,r)),otherwise:n=>t??n(e),resolve:()=>t??e.fullTableScan(),execute:async(i,a)=>{if(!Number.isSafeInteger(i)||i<1||i>r)return n.throw({code:`INVALID_REQUEST_BOUNDARY`,message:`A bounded query limit must be from 1 to ${r}`});let o=await(t??e.fullTableScan()).take(i);return a?a(o):o}}}return e=>i(e)}function o(i){let o=i.errors??e,s=i.triggers?e=>i.triggers.wrapDB(e):i.wrapDB??(e=>e),c=a({errors:o,maxRows:i.maxIncludedQueryRows});async function l(e){let t=await e.auth.getUserIdentity(),n=t?await i.getAuthUser(e):null;if(!t||!n)return o.throw({code:`UNAUTHENTICATED`});let r=t.organization,a=r&&typeof r==`object`&&`organizationId`in r&&typeof r.organizationId==`string`?r.organizationId:typeof t.org_id==`string`?t.org_id:null;if(!a)return o.throw({code:`UNAUTHENTICATED`});let s=r&&typeof r==`object`&&`role`in r&&typeof r.role==`string`?r.role:typeof t.role==`string`?t.role:void 0,c=i.mapRole(s);return c?Object.freeze({identity:t,user:Object.freeze({id:n.id}),org:Object.freeze({organizationId:a,role:c}),role:c,actor:Object.freeze({userId:n.id,organizationId:a,role:c})}):o.throw({code:`FORBIDDEN`})}async function u(e,t){if(!i.verifyMembership)return null;try{let n=await i.verifyMembership({userId:e,organizationId:t});if(n&&n.organizationId===t)return n}catch{}return o.throw({code:`FORBIDDEN`})}function d(e,t){if(!t.includes(e))return o.throw({code:`FORBIDDEN`})}async function f(e){return{ctx:{...e,include:c,...await l(e)},args:{}}}async function p(e){return{ctx:{...s(e),include:c,...await l(e)},args:{}}}async function m(e){let t=await l(e),n=await u(t.user.id,t.org.organizationId);if(!n)return{ctx:{...e,...t},args:{}};let r=i.mapRole(n.roleSlug);return r?{ctx:{...e,...t,org:Object.freeze({organizationId:n.organizationId,role:r}),role:r,actor:Object.freeze({userId:t.user.id,organizationId:n.organizationId,role:r})},args:{}}:o.throw({code:`FORBIDDEN`})}function h(...e){return r(i.query,{args:{},input:async t=>{let n=await f(t);return d(n.ctx.actor.role,e),n}})}function g(...e){return n(i.mutation,{args:{},input:async t=>{let n=await p(t);return d(n.ctx.actor.role,e),n}})}function _(...e){return t(i.action,{args:{},input:async t=>{let n=await m(t);return d(n.ctx.actor.role,e),n}})}return{include:c,authenticatedUser:l,roleQuery:h,roleMutation:g,roleAction:_,authQuery:r(i.query,{args:{},input:f}),authMutation:n(i.mutation,{args:{},input:p}),authAction:t(i.action,{args:{},input:m}),adminQuery:h(...i.adminRoles),adminMutation:g(...i.adminRoles),adminAction:_(...i.adminRoles),systemMutation:n(i.internalMutation,{args:{},input:async e=>({ctx:{...s(e),include:c},args:{}})}),systemAction:t(i.internalAction,{args:{},input:async e=>({ctx:e,args:{}})})}}export{o as createAuthFunctions,a as createInclude,i as defaultRoleMap};
1
+ import{defaultErrors as e}from"./errors.mjs";import{composeRules as t,createTenantRules as n,wrapDatabaseReader as r,wrapDatabaseWriter as i}from"./tenancy.mjs";import{zCustomAction as a,zCustomMutation as o,zCustomQuery as s}from"convex-helpers/server/zod4";function c(e){let t=e?.trim().toLowerCase();return t===`member`?`writer`:t===`reader`||t===`writer`||t===`admin`?t:null}function l(t){let n=t?.errors??e,r=t?.maxRows??100;function i(e,t){return{when:(n,r)=>t||n==null?i(e,t):i(e,r(e,n)),matching:(n,r,a=!0)=>t||!a?i(e,t):i(e,e.withIndex(n,r)),otherwise:n=>t??n(e),resolve:()=>t??e.fullTableScan(),execute:async(i,a)=>{if(!Number.isSafeInteger(i)||i<1||i>r)return n.throw({code:`INVALID_REQUEST_BOUNDARY`,message:`A bounded query limit must be from 1 to ${r}`});let o=await(t??e.fullTableScan()).take(i);return a?a(o):o}}}return e=>i(e)}function u(c){let u=c.errors??e,d=c.triggers?e=>c.triggers.wrapDB(e):c.wrapDB??(e=>e),f=l({errors:u,maxRows:c.maxIncludedQueryRows});async function p(e){let t=await e.auth.getUserIdentity(),n=t?await c.getAuthUser(e):null;if(!t||!n)return u.throw({code:`UNAUTHENTICATED`});let r=t.organization,i=r&&typeof r==`object`&&`organizationId`in r&&typeof r.organizationId==`string`?r.organizationId:typeof t.org_id==`string`?t.org_id:null;if(!i)return u.throw({code:`UNAUTHENTICATED`});let a=r&&typeof r==`object`&&`role`in r&&typeof r.role==`string`?r.role:typeof t.role==`string`?t.role:void 0,o=c.mapRole(a);if(!o)return u.throw({code:`FORBIDDEN`});let s={identity:t,user:Object.freeze({id:n.id}),org:Object.freeze({organizationId:i,role:o}),role:o,actor:Object.freeze({userId:n.id,organizationId:i,role:o})};return Object.freeze({...s,tenant:c.security?.tenancy?.resolve?.(s)??i})}let m=c.security?{defaultPolicy:c.security.defaultPolicy??(c.security.tenancy?`deny`:`allow`)}:void 0;function h(e){let r=c.security;if(!r)return;let i=[];return r.tenancy&&i.push(n(e.tenant,r.tenancy.tables)),r.rules&&i.push(r.rules(e)),i.length===1?i[0]:t(...i)}function g(e,t){let n=h(t);return n?{...e,db:r({},e.db,n,m)}:e}function _(e,t){let n=h(t);return n?{...e,db:i({},e.db,n,m)}:e}async function v(e,t){if(!c.verifyMembership)return null;try{let n=await c.verifyMembership({userId:e,organizationId:t});if(n&&n.organizationId===t)return n}catch{}return u.throw({code:`FORBIDDEN`})}function y(e,t){if(!t.includes(e))return u.throw({code:`FORBIDDEN`})}async function b(e){let t=await p(e);return{ctx:{...g(e,t),include:f,...t},args:{}}}async function x(e){let t=await p(e);return{ctx:{..._(d(e),t),include:f,...t},args:{}}}async function S(e){let t=await p(e),n=await v(t.user.id,t.org.organizationId);if(!n)return{ctx:{...e,...t},args:{}};let r=c.mapRole(n.roleSlug);if(!r)return u.throw({code:`FORBIDDEN`});let i={identity:t.identity,user:t.user,org:Object.freeze({organizationId:n.organizationId,role:r}),role:r,actor:Object.freeze({userId:t.user.id,organizationId:n.organizationId,role:r})};return{ctx:{...e,...i,tenant:c.security?.tenancy?.resolve?.(i)??n.organizationId},args:{}}}function C(...e){return s(c.query,{args:{},input:async t=>{let n=await b(t);return y(n.ctx.actor.role,e),n}})}function w(...e){return o(c.mutation,{args:{},input:async t=>{let n=await x(t);return y(n.ctx.actor.role,e),n}})}function T(...e){return a(c.action,{args:{},input:async t=>{let n=await S(t);return y(n.ctx.actor.role,e),n}})}return{include:f,authenticatedUser:p,roleQuery:C,roleMutation:w,roleAction:T,authQuery:s(c.query,{args:{},input:b}),authMutation:o(c.mutation,{args:{},input:x}),authAction:a(c.action,{args:{},input:S}),adminQuery:C(...c.adminRoles),adminMutation:w(...c.adminRoles),adminAction:T(...c.adminRoles),systemMutation:o(c.internalMutation,{args:{},input:async e=>({ctx:{...d(e),include:f},args:{}})}),systemAction:a(c.internalAction,{args:{},input:async e=>({ctx:e,args:{}})})}}export{u as createAuthFunctions,l as createInclude,c as defaultRoleMap};
2
2
  //# sourceMappingURL=auth.mjs.map
package/dist/auth.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"auth.mjs","names":[],"sources":["../src/auth.ts"],"sourcesContent":["import {\n\tzCustomAction,\n\tzCustomMutation,\n\tzCustomQuery,\n} from 'convex-helpers/server/zod4'\nimport type {\n\tActionBuilder,\n\tDocumentByInfo,\n\tGenericActionCtx,\n\tGenericDataModel,\n\tGenericMutationCtx,\n\tGenericQueryCtx,\n\tGenericTableInfo,\n\tIndexNames,\n\tIndexRange,\n\tIndexRangeBuilder,\n\tMutationBuilder,\n\tNamedIndex,\n\tQuery,\n\tQueryBuilder,\n\tQueryInitializer,\n\tUserIdentity,\n} from 'convex/server'\nimport type { Triggers } from 'convex-helpers/server/triggers'\nimport { defaultErrors, type ErrorFactory } from './errors'\n\n/** The default vocabulary; every app can supply its own via the Role generic. */\nexport type DefaultRole = 'reader' | 'writer' | 'admin'\n\nexport type Actor<Role extends string> = Readonly<{\n\tuserId: string\n\torganizationId: string\n\trole: Role\n}>\n\nexport type AuthBundle<Role extends string> = Readonly<{\n\tidentity: UserIdentity\n\tuser: Readonly<{ id: string }>\n\torg: Readonly<{ organizationId: string; role: Role }>\n\trole: Role\n\tactor: Actor<Role>\n}>\n\ntype AnyAuthContext<DataModel extends GenericDataModel> =\n\t| GenericQueryCtx<DataModel>\n\t| GenericMutationCtx<DataModel>\n\t| GenericActionCtx<DataModel>\n\nexport type AuthFunctionsConfig<\n\tDataModel extends GenericDataModel,\n\tRole extends string,\n> = {\n\t/** The consumer's generated function builders (from ./_generated/server). */\n\tquery: QueryBuilder<DataModel, 'public'>\n\tmutation: MutationBuilder<DataModel, 'public'>\n\taction: ActionBuilder<DataModel, 'public'>\n\tinternalMutation: MutationBuilder<DataModel, 'internal'>\n\tinternalAction: ActionBuilder<DataModel, 'internal'>\n\t/** Resolves the synchronized principal, e.g. (ctx) => authKit.getAuthUser(ctx). */\n\tgetAuthUser: (\n\t\tctx: AnyAuthContext<DataModel>,\n\t) => Promise<{ id: string } | null>\n\t/**\n\t * Maps an identity role slug onto the app's role vocabulary. Returning\n\t * null rejects the caller with FORBIDDEN — unknown roles never pass.\n\t */\n\tmapRole: (roleSlug?: string) => Role | null\n\t/** Roles allowed through the admin* constructors. */\n\tadminRoles: readonly NoInfer<Role>[]\n\t/**\n\t * Live membership check for actions (queries/mutations trust the JWT).\n\t * Authentication fails closed with FORBIDDEN when verification errors.\n\t * Omit to let actions trust the JWT like queries do.\n\t */\n\tverifyMembership?: (input: {\n\t\tuserId: string\n\t\torganizationId: string\n\t}) => Promise<{ organizationId: string; roleSlug: string } | null>\n\t/** Host error policy; defaults to throwing KitError. */\n\terrors?: ErrorFactory\n\t/**\n\t * Application trigger registry. When set, every authMutation,\n\t * roleMutation, and systemMutation write runs through triggers.wrapDB —\n\t * evaluations registered on tables fire structurally.\n\t */\n\ttriggers?: Triggers<DataModel>\n\t/** Lower-level alternative to `triggers`: a custom ctx wrapper. */\n\twrapDB?: (\n\t\tctx: GenericMutationCtx<DataModel>,\n\t) => GenericMutationCtx<DataModel>\n\t/** Upper bound for include() reads. */\n\tmaxIncludedQueryRows?: number\n}\n\n/** WorkOS-style default: `member` writes; reader/writer/admin pass through. */\nexport function defaultRoleMap(roleSlug?: string): DefaultRole | null {\n\tconst normalized = roleSlug?.trim().toLowerCase()\n\tif (normalized === 'member') return 'writer'\n\treturn normalized === 'reader' ||\n\t\tnormalized === 'writer' ||\n\t\tnormalized === 'admin'\n\t\t? normalized\n\t\t: null\n}\n\nexport type IncludedQuery<Table extends GenericTableInfo> = {\n\twhen<Value>(\n\t\tvalue: Value | null | undefined,\n\t\tselect: (query: QueryInitializer<Table>, value: Value) => Query<Table>,\n\t): IncludedQuery<Table>\n\tmatching<IndexName extends IndexNames<Table>>(\n\t\tindexName: IndexName,\n\t\tindexRange?: (\n\t\t\tquery: IndexRangeBuilder<\n\t\t\t\tDocumentByInfo<Table>,\n\t\t\t\tNamedIndex<Table, IndexName>\n\t\t\t>,\n\t\t) => IndexRange,\n\t\tshouldMatch?: boolean,\n\t): IncludedQuery<Table>\n\totherwise(\n\t\tselect: (query: QueryInitializer<Table>) => Query<Table>,\n\t): Query<Table>\n\tresolve(): Query<Table>\n\texecute<Result = Awaited<ReturnType<Query<Table>['take']>>>(\n\t\tlimit: number,\n\t\ttransform?: (\n\t\t\tdata: Awaited<ReturnType<Query<Table>['take']>>,\n\t\t) => Result | Promise<Result>,\n\t): Promise<Result>\n}\n\nexport type Include = <Table extends GenericTableInfo>(\n\tquery: QueryInitializer<Table>,\n) => IncludedQuery<Table>\n\n/** Builds an include() that selects the first matching indexed query and bounds the read. */\nexport function createInclude(options?: {\n\terrors?: ErrorFactory\n\tmaxRows?: number\n}): Include {\n\tconst errors = options?.errors ?? defaultErrors\n\tconst maxRows = options?.maxRows ?? 100\n\n\tfunction attach<Table extends GenericTableInfo>(\n\t\tquery: QueryInitializer<Table>,\n\t\tselected?: Query<Table>,\n\t): IncludedQuery<Table> {\n\t\treturn {\n\t\t\twhen: (value, select) =>\n\t\t\t\tselected || value === undefined || value === null\n\t\t\t\t\t? attach(query, selected)\n\t\t\t\t\t: attach(query, select(query, value)),\n\t\t\tmatching: (indexName, indexRange, shouldMatch = true) =>\n\t\t\t\tselected || !shouldMatch\n\t\t\t\t\t? attach(query, selected)\n\t\t\t\t\t: attach(query, query.withIndex(indexName, indexRange)),\n\t\t\totherwise: (select) => selected ?? select(query),\n\t\t\tresolve: () => selected ?? query.fullTableScan(),\n\t\t\texecute: async (limit, transform) => {\n\t\t\t\tif (!Number.isSafeInteger(limit) || limit < 1 || limit > maxRows) {\n\t\t\t\t\treturn errors.throw({\n\t\t\t\t\t\tcode: 'INVALID_REQUEST_BOUNDARY',\n\t\t\t\t\t\tmessage: `A bounded query limit must be from 1 to ${maxRows}`,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tconst data = await (selected ?? query.fullTableScan()).take(limit)\n\t\t\t\treturn transform ? transform(data) : (data as never)\n\t\t\t},\n\t\t}\n\t}\n\n\treturn (query) => attach(query)\n}\n\n/**\n * Builds the auth-aware function constructors from the host's injected\n * policy: identity resolution, the app's own role vocabulary, live\n * membership verification for actions, trigger wrapping for mutations,\n * and errors. Role-based access is first-class — every constructor family\n * has a role-gated variant (roleQuery/roleMutation/roleAction), and the\n * admin* constructors are just that gate applied to config.adminRoles.\n */\nexport function createAuthFunctions<\n\tDataModel extends GenericDataModel,\n\tconst Role extends string = DefaultRole,\n>(config: AuthFunctionsConfig<DataModel, Role>) {\n\tconst errors = config.errors ?? defaultErrors\n\tconst wrapDB = config.triggers\n\t\t? (ctx: GenericMutationCtx<DataModel>) =>\n\t\t\t\t(config.triggers as Triggers<DataModel>).wrapDB(ctx)\n\t\t: (config.wrapDB ?? ((ctx) => ctx))\n\tconst include = createInclude({\n\t\terrors,\n\t\tmaxRows: config.maxIncludedQueryRows,\n\t})\n\n\tasync function authenticatedUser(\n\t\tctx: AnyAuthContext<DataModel>,\n\t): Promise<AuthBundle<Role>> {\n\t\tconst identity = await ctx.auth.getUserIdentity()\n\t\tconst user = identity ? await config.getAuthUser(ctx) : null\n\t\tif (!identity || !user) return errors.throw({ code: 'UNAUTHENTICATED' })\n\t\tconst organization = identity.organization\n\t\tconst organizationId =\n\t\t\torganization &&\n\t\t\ttypeof organization === 'object' &&\n\t\t\t'organizationId' in organization &&\n\t\t\ttypeof organization.organizationId === 'string'\n\t\t\t\t? organization.organizationId\n\t\t\t\t: typeof identity.org_id === 'string'\n\t\t\t\t\t? identity.org_id\n\t\t\t\t\t: null\n\t\tif (!organizationId) return errors.throw({ code: 'UNAUTHENTICATED' })\n\t\tconst organizationRole =\n\t\t\torganization &&\n\t\t\ttypeof organization === 'object' &&\n\t\t\t'role' in organization &&\n\t\t\ttypeof organization.role === 'string'\n\t\t\t\t? organization.role\n\t\t\t\t: typeof identity.role === 'string'\n\t\t\t\t\t? identity.role\n\t\t\t\t\t: undefined\n\t\tconst role = config.mapRole(organizationRole)\n\t\tif (!role) return errors.throw({ code: 'FORBIDDEN' })\n\t\treturn Object.freeze({\n\t\t\tidentity,\n\t\t\tuser: Object.freeze({ id: user.id }),\n\t\t\torg: Object.freeze({ organizationId, role }),\n\t\t\trole,\n\t\t\tactor: Object.freeze({ userId: user.id, organizationId, role }),\n\t\t})\n\t}\n\n\tasync function verifiedMembership(userId: string, organizationId: string) {\n\t\tif (!config.verifyMembership) return null\n\t\ttry {\n\t\t\tconst membership = await config.verifyMembership({\n\t\t\t\tuserId,\n\t\t\t\torganizationId,\n\t\t\t})\n\t\t\tif (membership && membership.organizationId === organizationId) {\n\t\t\t\treturn membership\n\t\t\t}\n\t\t} catch {\n\t\t\t// Authentication fails closed when membership cannot be verified.\n\t\t}\n\t\treturn errors.throw({ code: 'FORBIDDEN' })\n\t}\n\n\tfunction requireRole(role: Role, allowed: readonly Role[]) {\n\t\tif (!allowed.includes(role)) return errors.throw({ code: 'FORBIDDEN' })\n\t}\n\n\tasync function queryInput(ctx: GenericQueryCtx<DataModel>) {\n\t\treturn {\n\t\t\tctx: { ...ctx, include, ...(await authenticatedUser(ctx)) },\n\t\t\targs: {},\n\t\t}\n\t}\n\n\tasync function mutationInput(ctx: GenericMutationCtx<DataModel>) {\n\t\treturn {\n\t\t\tctx: { ...wrapDB(ctx), include, ...(await authenticatedUser(ctx)) },\n\t\t\targs: {},\n\t\t}\n\t}\n\n\tasync function actionInput(ctx: GenericActionCtx<DataModel>) {\n\t\tconst authenticated = await authenticatedUser(ctx)\n\t\tconst membership = await verifiedMembership(\n\t\t\tauthenticated.user.id,\n\t\t\tauthenticated.org.organizationId,\n\t\t)\n\t\tif (!membership) return { ctx: { ...ctx, ...authenticated }, args: {} }\n\t\tconst role = config.mapRole(membership.roleSlug)\n\t\tif (!role) return errors.throw({ code: 'FORBIDDEN' })\n\t\treturn {\n\t\t\tctx: {\n\t\t\t\t...ctx,\n\t\t\t\t...authenticated,\n\t\t\t\torg: Object.freeze({\n\t\t\t\t\torganizationId: membership.organizationId,\n\t\t\t\t\trole,\n\t\t\t\t}),\n\t\t\t\trole,\n\t\t\t\tactor: Object.freeze({\n\t\t\t\t\tuserId: authenticated.user.id,\n\t\t\t\t\torganizationId: membership.organizationId,\n\t\t\t\t\trole,\n\t\t\t\t}),\n\t\t\t},\n\t\t\targs: {},\n\t\t}\n\t}\n\n\t/** Query constructor allowing only the given roles. */\n\tfunction roleQuery(...allowed: readonly Role[]) {\n\t\treturn zCustomQuery(config.query, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => {\n\t\t\t\tconst authenticated = await queryInput(ctx)\n\t\t\t\trequireRole(authenticated.ctx.actor.role, allowed)\n\t\t\t\treturn authenticated\n\t\t\t},\n\t\t})\n\t}\n\n\t/** Mutation constructor allowing only the given roles. */\n\tfunction roleMutation(...allowed: readonly Role[]) {\n\t\treturn zCustomMutation(config.mutation, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => {\n\t\t\t\tconst authenticated = await mutationInput(ctx)\n\t\t\t\trequireRole(authenticated.ctx.actor.role, allowed)\n\t\t\t\treturn authenticated\n\t\t\t},\n\t\t})\n\t}\n\n\t/** Action constructor allowing only the given roles (post live verification). */\n\tfunction roleAction(...allowed: readonly Role[]) {\n\t\treturn zCustomAction(config.action, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => {\n\t\t\t\tconst authenticated = await actionInput(ctx)\n\t\t\t\trequireRole(authenticated.ctx.actor.role, allowed)\n\t\t\t\treturn authenticated\n\t\t\t},\n\t\t})\n\t}\n\n\treturn {\n\t\tinclude,\n\t\tauthenticatedUser,\n\t\troleQuery,\n\t\troleMutation,\n\t\troleAction,\n\t\tauthQuery: zCustomQuery(config.query, { args: {}, input: queryInput }),\n\t\tauthMutation: zCustomMutation(config.mutation, {\n\t\t\targs: {},\n\t\t\tinput: mutationInput,\n\t\t}),\n\t\tauthAction: zCustomAction(config.action, {\n\t\t\targs: {},\n\t\t\tinput: actionInput,\n\t\t}),\n\t\tadminQuery: roleQuery(...config.adminRoles),\n\t\tadminMutation: roleMutation(...config.adminRoles),\n\t\tadminAction: roleAction(...config.adminRoles),\n\t\tsystemMutation: zCustomMutation(config.internalMutation, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => ({ ctx: { ...wrapDB(ctx), include }, args: {} }),\n\t\t}),\n\t\tsystemAction: zCustomAction(config.internalAction, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => ({ ctx, args: {} }),\n\t\t}),\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth.mjs","names":[],"sources":["../src/auth.ts"],"sourcesContent":["import {\n\tzCustomAction,\n\tzCustomMutation,\n\tzCustomQuery,\n} from 'convex-helpers/server/zod4'\nimport type {\n\tActionBuilder,\n\tDocumentByInfo,\n\tGenericActionCtx,\n\tGenericDataModel,\n\tGenericMutationCtx,\n\tGenericQueryCtx,\n\tGenericTableInfo,\n\tIndexNames,\n\tIndexRange,\n\tIndexRangeBuilder,\n\tMutationBuilder,\n\tNamedIndex,\n\tQuery,\n\tQueryBuilder,\n\tQueryInitializer,\n\tUserIdentity,\n} from 'convex/server'\nimport type { Triggers } from 'convex-helpers/server/triggers'\nimport type { TableNamesInDataModel } from 'convex/server'\nimport { defaultErrors, type ErrorFactory } from './errors'\nimport {\n\tcomposeRules,\n\tcreateTenantRules,\n\twrapDatabaseReader,\n\twrapDatabaseWriter,\n\ttype RLSConfig,\n\ttype Rules,\n} from './tenancy'\n\n/** The default vocabulary; every app can supply its own via the Role generic. */\nexport type DefaultRole = 'reader' | 'writer' | 'admin'\n\nexport type Actor<Role extends string> = Readonly<{\n\tuserId: string\n\torganizationId: string\n\trole: Role\n}>\n\nexport type AuthBundle<Role extends string> = Readonly<{\n\tidentity: UserIdentity\n\tuser: Readonly<{ id: string }>\n\torg: Readonly<{ organizationId: string; role: Role }>\n\trole: Role\n\tactor: Actor<Role>\n\t/** The resolved tenant — security.tenancy.resolve, or the organization id. */\n\ttenant: string\n}>\n\n/**\n * Row-level security configuration. Tenant isolation and role-level rules\n * compose (AND) onto the same wrapped database: queries get a wrapped\n * reader, mutations get triggers first, then a wrapped writer.\n */\nexport type SecurityConfig<\n\tDataModel extends GenericDataModel,\n\tRole extends string,\n> = {\n\t/**\n\t * Tenant isolation. `tables` is THE registry: every listed table gets\n\t * read/insert/modify gated on row.tenant === ctx.tenant. Pair with\n\t * tenantTable (schema) and tenantOwnership (triggers) on the same list.\n\t */\n\ttenancy?: {\n\t\ttables: readonly TableNamesInDataModel<DataModel>[]\n\t\t/** Maps the auth bundle to the tenant; defaults to org.organizationId. */\n\t\tresolve?: (bundle: Omit<AuthBundle<Role>, 'tenant'>) => string\n\t}\n\t/**\n\t * Role-level (or any policy) rules derived from the authenticated bundle,\n\t * composed AND-wise with tenant isolation. E.g. gate `modify` on a table\n\t * to admin roles.\n\t */\n\trules?: (bundle: AuthBundle<Role>) => Rules<unknown, DataModel>\n\t/**\n\t * Policy for tables with no rule. Defaults to 'deny' when tenancy is\n\t * configured (unlisted tables are unreachable — fail closed), 'allow'\n\t * otherwise.\n\t */\n\tdefaultPolicy?: RLSConfig['defaultPolicy']\n}\n\ntype AnyAuthContext<DataModel extends GenericDataModel> =\n\t| GenericQueryCtx<DataModel>\n\t| GenericMutationCtx<DataModel>\n\t| GenericActionCtx<DataModel>\n\nexport type AuthFunctionsConfig<\n\tDataModel extends GenericDataModel,\n\tRole extends string,\n> = {\n\t/** The consumer's generated function builders (from ./_generated/server). */\n\tquery: QueryBuilder<DataModel, 'public'>\n\tmutation: MutationBuilder<DataModel, 'public'>\n\taction: ActionBuilder<DataModel, 'public'>\n\tinternalMutation: MutationBuilder<DataModel, 'internal'>\n\tinternalAction: ActionBuilder<DataModel, 'internal'>\n\t/** Resolves the synchronized principal, e.g. (ctx) => authKit.getAuthUser(ctx). */\n\tgetAuthUser: (\n\t\tctx: AnyAuthContext<DataModel>,\n\t) => Promise<{ id: string } | null>\n\t/**\n\t * Maps an identity role slug onto the app's role vocabulary. Returning\n\t * null rejects the caller with FORBIDDEN — unknown roles never pass.\n\t */\n\tmapRole: (roleSlug?: string) => Role | null\n\t/** Roles allowed through the admin* constructors. */\n\tadminRoles: readonly NoInfer<Role>[]\n\t/**\n\t * Live membership check for actions (queries/mutations trust the JWT).\n\t * Authentication fails closed with FORBIDDEN when verification errors.\n\t * Omit to let actions trust the JWT like queries do.\n\t */\n\tverifyMembership?: (input: {\n\t\tuserId: string\n\t\torganizationId: string\n\t}) => Promise<{ organizationId: string; roleSlug: string } | null>\n\t/** Host error policy; defaults to throwing KitError. */\n\terrors?: ErrorFactory\n\t/**\n\t * Application trigger registry. When set, every authMutation,\n\t * roleMutation, and systemMutation write runs through triggers.wrapDB —\n\t * evaluations registered on tables fire structurally.\n\t */\n\ttriggers?: Triggers<DataModel>\n\t/** Lower-level alternative to `triggers`: a custom ctx wrapper. */\n\twrapDB?: (\n\t\tctx: GenericMutationCtx<DataModel>,\n\t) => GenericMutationCtx<DataModel>\n\t/** Upper bound for include() reads. */\n\tmaxIncludedQueryRows?: number\n\t/**\n\t * Row-level security: tenant isolation (from a table registry) and\n\t * role-level rules, applied structurally inside every auth*, role*, and\n\t * admin* constructor. system* constructors stay unwrapped (trusted).\n\t */\n\tsecurity?: SecurityConfig<DataModel, Role>\n}\n\n/** WorkOS-style default: `member` writes; reader/writer/admin pass through. */\nexport function defaultRoleMap(roleSlug?: string): DefaultRole | null {\n\tconst normalized = roleSlug?.trim().toLowerCase()\n\tif (normalized === 'member') return 'writer'\n\treturn normalized === 'reader' ||\n\t\tnormalized === 'writer' ||\n\t\tnormalized === 'admin'\n\t\t? normalized\n\t\t: null\n}\n\nexport type IncludedQuery<Table extends GenericTableInfo> = {\n\twhen<Value>(\n\t\tvalue: Value | null | undefined,\n\t\tselect: (query: QueryInitializer<Table>, value: Value) => Query<Table>,\n\t): IncludedQuery<Table>\n\tmatching<IndexName extends IndexNames<Table>>(\n\t\tindexName: IndexName,\n\t\tindexRange?: (\n\t\t\tquery: IndexRangeBuilder<\n\t\t\t\tDocumentByInfo<Table>,\n\t\t\t\tNamedIndex<Table, IndexName>\n\t\t\t>,\n\t\t) => IndexRange,\n\t\tshouldMatch?: boolean,\n\t): IncludedQuery<Table>\n\totherwise(\n\t\tselect: (query: QueryInitializer<Table>) => Query<Table>,\n\t): Query<Table>\n\tresolve(): Query<Table>\n\texecute<Result = Awaited<ReturnType<Query<Table>['take']>>>(\n\t\tlimit: number,\n\t\ttransform?: (\n\t\t\tdata: Awaited<ReturnType<Query<Table>['take']>>,\n\t\t) => Result | Promise<Result>,\n\t): Promise<Result>\n}\n\nexport type Include = <Table extends GenericTableInfo>(\n\tquery: QueryInitializer<Table>,\n) => IncludedQuery<Table>\n\n/** Builds an include() that selects the first matching indexed query and bounds the read. */\nexport function createInclude(options?: {\n\terrors?: ErrorFactory\n\tmaxRows?: number\n}): Include {\n\tconst errors = options?.errors ?? defaultErrors\n\tconst maxRows = options?.maxRows ?? 100\n\n\tfunction attach<Table extends GenericTableInfo>(\n\t\tquery: QueryInitializer<Table>,\n\t\tselected?: Query<Table>,\n\t): IncludedQuery<Table> {\n\t\treturn {\n\t\t\twhen: (value, select) =>\n\t\t\t\tselected || value === undefined || value === null\n\t\t\t\t\t? attach(query, selected)\n\t\t\t\t\t: attach(query, select(query, value)),\n\t\t\tmatching: (indexName, indexRange, shouldMatch = true) =>\n\t\t\t\tselected || !shouldMatch\n\t\t\t\t\t? attach(query, selected)\n\t\t\t\t\t: attach(query, query.withIndex(indexName, indexRange)),\n\t\t\totherwise: (select) => selected ?? select(query),\n\t\t\tresolve: () => selected ?? query.fullTableScan(),\n\t\t\texecute: async (limit, transform) => {\n\t\t\t\tif (!Number.isSafeInteger(limit) || limit < 1 || limit > maxRows) {\n\t\t\t\t\treturn errors.throw({\n\t\t\t\t\t\tcode: 'INVALID_REQUEST_BOUNDARY',\n\t\t\t\t\t\tmessage: `A bounded query limit must be from 1 to ${maxRows}`,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tconst data = await (selected ?? query.fullTableScan()).take(limit)\n\t\t\t\treturn transform ? transform(data) : (data as never)\n\t\t\t},\n\t\t}\n\t}\n\n\treturn (query) => attach(query)\n}\n\n/**\n * Builds the auth-aware function constructors from the host's injected\n * policy: identity resolution, the app's own role vocabulary, live\n * membership verification for actions, trigger wrapping for mutations,\n * and errors. Role-based access is first-class — every constructor family\n * has a role-gated variant (roleQuery/roleMutation/roleAction), and the\n * admin* constructors are just that gate applied to config.adminRoles.\n */\nexport function createAuthFunctions<\n\tDataModel extends GenericDataModel,\n\tconst Role extends string = DefaultRole,\n>(config: AuthFunctionsConfig<DataModel, Role>) {\n\tconst errors = config.errors ?? defaultErrors\n\tconst wrapDB = config.triggers\n\t\t? (ctx: GenericMutationCtx<DataModel>) =>\n\t\t\t\t(config.triggers as Triggers<DataModel>).wrapDB(ctx)\n\t\t: (config.wrapDB ?? ((ctx) => ctx))\n\tconst include = createInclude({\n\t\terrors,\n\t\tmaxRows: config.maxIncludedQueryRows,\n\t})\n\n\tasync function authenticatedUser(\n\t\tctx: AnyAuthContext<DataModel>,\n\t): Promise<AuthBundle<Role>> {\n\t\tconst identity = await ctx.auth.getUserIdentity()\n\t\tconst user = identity ? await config.getAuthUser(ctx) : null\n\t\tif (!identity || !user) return errors.throw({ code: 'UNAUTHENTICATED' })\n\t\tconst organization = identity.organization\n\t\tconst organizationId =\n\t\t\torganization &&\n\t\t\ttypeof organization === 'object' &&\n\t\t\t'organizationId' in organization &&\n\t\t\ttypeof organization.organizationId === 'string'\n\t\t\t\t? organization.organizationId\n\t\t\t\t: typeof identity.org_id === 'string'\n\t\t\t\t\t? identity.org_id\n\t\t\t\t\t: null\n\t\tif (!organizationId) return errors.throw({ code: 'UNAUTHENTICATED' })\n\t\tconst organizationRole =\n\t\t\torganization &&\n\t\t\ttypeof organization === 'object' &&\n\t\t\t'role' in organization &&\n\t\t\ttypeof organization.role === 'string'\n\t\t\t\t? organization.role\n\t\t\t\t: typeof identity.role === 'string'\n\t\t\t\t\t? identity.role\n\t\t\t\t\t: undefined\n\t\tconst role = config.mapRole(organizationRole)\n\t\tif (!role) return errors.throw({ code: 'FORBIDDEN' })\n\t\tconst base = {\n\t\t\tidentity,\n\t\t\tuser: Object.freeze({ id: user.id }),\n\t\t\torg: Object.freeze({ organizationId, role }),\n\t\t\trole,\n\t\t\tactor: Object.freeze({ userId: user.id, organizationId, role }),\n\t\t}\n\t\treturn Object.freeze({\n\t\t\t...base,\n\t\t\ttenant: config.security?.tenancy?.resolve?.(base) ?? organizationId,\n\t\t})\n\t}\n\n\tconst securityConfig: RLSConfig | undefined = config.security\n\t\t? {\n\t\t\t\tdefaultPolicy:\n\t\t\t\t\tconfig.security.defaultPolicy ??\n\t\t\t\t\t(config.security.tenancy ? 'deny' : 'allow'),\n\t\t\t}\n\t\t: undefined\n\n\tfunction securityRules(\n\t\tbundle: AuthBundle<Role>,\n\t): Rules<unknown, DataModel> | undefined {\n\t\tconst security = config.security\n\t\tif (!security) return undefined\n\t\tconst sets: Rules<unknown, DataModel>[] = []\n\t\tif (security.tenancy) {\n\t\t\tsets.push(\n\t\t\t\tcreateTenantRules<DataModel>(bundle.tenant, security.tenancy.tables),\n\t\t\t)\n\t\t}\n\t\tif (security.rules) sets.push(security.rules(bundle))\n\t\treturn sets.length === 1 ? sets[0] : composeRules(...sets)\n\t}\n\n\tfunction secureReader(\n\t\tctx: GenericQueryCtx<DataModel>,\n\t\tbundle: AuthBundle<Role>,\n\t): GenericQueryCtx<DataModel> {\n\t\tconst rules = securityRules(bundle)\n\t\tif (!rules) return ctx\n\t\treturn {\n\t\t\t...ctx,\n\t\t\tdb: wrapDatabaseReader({}, ctx.db, rules as never, securityConfig),\n\t\t}\n\t}\n\n\tfunction secureWriter(\n\t\tctx: GenericMutationCtx<DataModel>,\n\t\tbundle: AuthBundle<Role>,\n\t): GenericMutationCtx<DataModel> {\n\t\tconst rules = securityRules(bundle)\n\t\tif (!rules) return ctx\n\t\t// Triggers wrap first (ctx already trigger-wrapped by the caller),\n\t\t// then row-level security wraps the triggered db — order matters.\n\t\treturn {\n\t\t\t...ctx,\n\t\t\tdb: wrapDatabaseWriter({}, ctx.db, rules as never, securityConfig),\n\t\t}\n\t}\n\n\tasync function verifiedMembership(userId: string, organizationId: string) {\n\t\tif (!config.verifyMembership) return null\n\t\ttry {\n\t\t\tconst membership = await config.verifyMembership({\n\t\t\t\tuserId,\n\t\t\t\torganizationId,\n\t\t\t})\n\t\t\tif (membership && membership.organizationId === organizationId) {\n\t\t\t\treturn membership\n\t\t\t}\n\t\t} catch {\n\t\t\t// Authentication fails closed when membership cannot be verified.\n\t\t}\n\t\treturn errors.throw({ code: 'FORBIDDEN' })\n\t}\n\n\tfunction requireRole(role: Role, allowed: readonly Role[]) {\n\t\tif (!allowed.includes(role)) return errors.throw({ code: 'FORBIDDEN' })\n\t}\n\n\tasync function queryInput(ctx: GenericQueryCtx<DataModel>) {\n\t\tconst bundle = await authenticatedUser(ctx)\n\t\treturn {\n\t\t\tctx: { ...secureReader(ctx, bundle), include, ...bundle },\n\t\t\targs: {},\n\t\t}\n\t}\n\n\tasync function mutationInput(ctx: GenericMutationCtx<DataModel>) {\n\t\tconst bundle = await authenticatedUser(ctx)\n\t\treturn {\n\t\t\tctx: { ...secureWriter(wrapDB(ctx), bundle), include, ...bundle },\n\t\t\targs: {},\n\t\t}\n\t}\n\n\tasync function actionInput(ctx: GenericActionCtx<DataModel>) {\n\t\tconst authenticated = await authenticatedUser(ctx)\n\t\tconst membership = await verifiedMembership(\n\t\t\tauthenticated.user.id,\n\t\t\tauthenticated.org.organizationId,\n\t\t)\n\t\tif (!membership) return { ctx: { ...ctx, ...authenticated }, args: {} }\n\t\tconst role = config.mapRole(membership.roleSlug)\n\t\tif (!role) return errors.throw({ code: 'FORBIDDEN' })\n\t\tconst verified = {\n\t\t\tidentity: authenticated.identity,\n\t\t\tuser: authenticated.user,\n\t\t\torg: Object.freeze({\n\t\t\t\torganizationId: membership.organizationId,\n\t\t\t\trole,\n\t\t\t}),\n\t\t\trole,\n\t\t\tactor: Object.freeze({\n\t\t\t\tuserId: authenticated.user.id,\n\t\t\t\torganizationId: membership.organizationId,\n\t\t\t\trole,\n\t\t\t}),\n\t\t}\n\t\treturn {\n\t\t\tctx: {\n\t\t\t\t...ctx,\n\t\t\t\t...verified,\n\t\t\t\t// Tenant re-derives from the live-verified organization.\n\t\t\t\ttenant:\n\t\t\t\t\tconfig.security?.tenancy?.resolve?.(verified) ??\n\t\t\t\t\tmembership.organizationId,\n\t\t\t},\n\t\t\targs: {},\n\t\t}\n\t}\n\n\t/** Query constructor allowing only the given roles. */\n\tfunction roleQuery(...allowed: readonly Role[]) {\n\t\treturn zCustomQuery(config.query, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => {\n\t\t\t\tconst authenticated = await queryInput(ctx)\n\t\t\t\trequireRole(authenticated.ctx.actor.role, allowed)\n\t\t\t\treturn authenticated\n\t\t\t},\n\t\t})\n\t}\n\n\t/** Mutation constructor allowing only the given roles. */\n\tfunction roleMutation(...allowed: readonly Role[]) {\n\t\treturn zCustomMutation(config.mutation, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => {\n\t\t\t\tconst authenticated = await mutationInput(ctx)\n\t\t\t\trequireRole(authenticated.ctx.actor.role, allowed)\n\t\t\t\treturn authenticated\n\t\t\t},\n\t\t})\n\t}\n\n\t/** Action constructor allowing only the given roles (post live verification). */\n\tfunction roleAction(...allowed: readonly Role[]) {\n\t\treturn zCustomAction(config.action, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => {\n\t\t\t\tconst authenticated = await actionInput(ctx)\n\t\t\t\trequireRole(authenticated.ctx.actor.role, allowed)\n\t\t\t\treturn authenticated\n\t\t\t},\n\t\t})\n\t}\n\n\treturn {\n\t\tinclude,\n\t\tauthenticatedUser,\n\t\troleQuery,\n\t\troleMutation,\n\t\troleAction,\n\t\tauthQuery: zCustomQuery(config.query, { args: {}, input: queryInput }),\n\t\tauthMutation: zCustomMutation(config.mutation, {\n\t\t\targs: {},\n\t\t\tinput: mutationInput,\n\t\t}),\n\t\tauthAction: zCustomAction(config.action, {\n\t\t\targs: {},\n\t\t\tinput: actionInput,\n\t\t}),\n\t\tadminQuery: roleQuery(...config.adminRoles),\n\t\tadminMutation: roleMutation(...config.adminRoles),\n\t\tadminAction: roleAction(...config.adminRoles),\n\t\tsystemMutation: zCustomMutation(config.internalMutation, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => ({ ctx: { ...wrapDB(ctx), include }, args: {} }),\n\t\t}),\n\t\tsystemAction: zCustomAction(config.internalAction, {\n\t\t\targs: {},\n\t\t\tinput: async (ctx) => ({ ctx, args: {} }),\n\t\t}),\n\t}\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{components as e}from"./_generated/api.mjs";import{mutation as t,query as n}from"./_generated/server.mjs";import{APPROVAL_AUDIT_RETENTION_CATEGORY as r,MAX_APPROVAL_AUDIT_RETENTION_DAYS as i}from"./constants.mjs";import{AuditLog as a}from"convex-audit-log";import{v as o}from"convex/values";const s=new a(e.auditLog,{samplingEnabled:!1}),c=n({args:{resourceType:o.string(),resourceRef:o.string(),limit:o.optional(o.number())},returns:o.array(o.object({action:o.string(),actorRef:o.optional(o.string()),correlationKey:o.string(),transition:o.string()})),handler:async(e,t)=>{let n=Math.max(1,Math.min(Math.trunc(t.limit??100),100)),r=await s.queryByResource(e,{resourceType:t.resourceType,resourceId:t.resourceRef,limit:n}),i=new Map;for(let e of r){let t=p(e);t&&i.set(t.correlationKey,t)}return[...i.values()]}}),l=t({args:{olderThanDays:o.number(),batchSize:o.number()},returns:o.number(),handler:(e,t)=>{let n=f(t.olderThanDays,1,i,`olderThanDays`),a=f(t.batchSize,1,100,`batchSize`);return s.cleanup(e,{olderThanDays:n,batchSize:a,retentionCategory:r})}});function u(e){return{action:e.action,...e.actorRef===void 0?{}:{actorId:e.actorRef},resourceType:e.resourceType,resourceId:e.resourceRef,severity:e.severity??`info`,tags:[`approvals`],retentionCategory:r,metadata:{correlationKey:`${e.runId}:${e.transition}`,runId:e.runId,transition:e.transition}}}async function d(e,t){await s.log(e,u(t))}function f(e,t,n,r){if(!Number.isInteger(e)||e<t||e>n)throw Error(`${r} must be an integer between ${t} and ${n}`);return e}function p(e){if(!e||typeof e!=`object`)return null;let t=e,n=t.metadata;if(!n||typeof n!=`object`)return null;let r=n;return typeof t.action!=`string`||typeof r.correlationKey!=`string`||typeof r.transition!=`string`?null:{action:t.action,...typeof t.actorId==`string`?{actorRef:t.actorId}:{},correlationKey:r.correlationKey,transition:r.transition}}export{s as approvalAudit,u as approvalAuditEvent,l as cleanup,c as history,d as logApprovalTransition};
1
+ import{components as e}from"./_generated/api.mjs";import{mutation as t,query as n}from"./_generated/server.mjs";import{APPROVAL_AUDIT_RETENTION_CATEGORY as r,MAX_APPROVAL_AUDIT_RETENTION_DAYS as i}from"./constants.mjs";import{v as a}from"convex/values";import{AuditLog as o}from"convex-audit-log";const s=new o(e.auditLog,{samplingEnabled:!1}),c=n({args:{resourceType:a.string(),resourceRef:a.string(),limit:a.optional(a.number())},returns:a.array(a.object({action:a.string(),actorRef:a.optional(a.string()),correlationKey:a.string(),transition:a.string()})),handler:async(e,t)=>{let n=Math.max(1,Math.min(Math.trunc(t.limit??100),100)),r=await s.queryByResource(e,{resourceType:t.resourceType,resourceId:t.resourceRef,limit:n}),i=new Map;for(let e of r){let t=p(e);t&&i.set(t.correlationKey,t)}return[...i.values()]}}),l=t({args:{olderThanDays:a.number(),batchSize:a.number()},returns:a.number(),handler:(e,t)=>{let n=f(t.olderThanDays,1,i,`olderThanDays`),a=f(t.batchSize,1,100,`batchSize`);return s.cleanup(e,{olderThanDays:n,batchSize:a,retentionCategory:r})}});function u(e){return{action:e.action,...e.actorRef===void 0?{}:{actorId:e.actorRef},resourceType:e.resourceType,resourceId:e.resourceRef,severity:e.severity??`info`,tags:[`approvals`],retentionCategory:r,metadata:{correlationKey:`${e.runId}:${e.transition}`,runId:e.runId,transition:e.transition}}}async function d(e,t){await s.log(e,u(t))}function f(e,t,n,r){if(!Number.isInteger(e)||e<t||e>n)throw Error(`${r} must be an integer between ${t} and ${n}`);return e}function p(e){if(!e||typeof e!=`object`)return null;let t=e,n=t.metadata;if(!n||typeof n!=`object`)return null;let r=n;return typeof t.action!=`string`||typeof r.correlationKey!=`string`||typeof r.transition!=`string`?null:{action:t.action,...typeof t.actorId==`string`?{actorRef:t.actorId}:{},correlationKey:r.correlationKey,transition:r.transition}}export{s as approvalAudit,u as approvalAuditEvent,l as cleanup,c as history,d as logApprovalTransition};
2
2
  //# sourceMappingURL=audit.mjs.map
@@ -5,7 +5,7 @@ import { Result, ResultBoundary, TransactionMetricsContext, executeResultBoundar
5
5
  import { emitSemanticEvent } from "./telemetry.mjs";
6
6
  import _default from "./convex.config.mjs";
7
7
  declare namespace client_d_exports {
8
- export { ApplicationCommand, AuditEntryInput, AuditWriter, AuditedOperation, AuditedRegistry, CommandExecution, CommandInput, CommandObservation, CommandRegistry, CommandResult, Foundation, FoundationOptions, Observability, ObservabilityOptions, Parseable, Query, QueryExecution, Result, ResultBoundary, TransactionMetricsContext, _default as default, emitSemanticEvent, executeResultBoundary, projectResult };
8
+ export { ApplicationCommand, AuditEntryInput, AuditWriter, AuditedOperation, AuditedRegistry, CommandDefaults, CommandExecution, CommandInput, CommandObservation, CommandRegistry, CommandResult, Foundation, FoundationOptions, Observability, ObservabilityOptions, Parseable, PermissionChecker, Query, QueryExecution, Result, ResultBoundary, TransactionMetricsContext, _default as default, emitSemanticEvent, executeResultBoundary, projectResult };
9
9
  }
10
10
  /** Shape handed to the injected audit writer; classification comes from the operation. */
11
11
  type AuditEntryInput = {
@@ -29,11 +29,38 @@ type AuditedOperation = Readonly<{
29
29
  parse: (value: unknown) => unknown;
30
30
  }>;
31
31
  classification: string;
32
+ /**
33
+ * Permission slug required to execute this operation. Checked through the
34
+ * Foundation's injected checkPermission BEFORE guards and the handler;
35
+ * declaring a permission without injecting a checker fails closed.
36
+ */
37
+ permission?: string;
38
+ /**
39
+ * Per-operation precondition, after the permission check and the registry
40
+ * default guard, before the handler. Throw to deny — nothing has run yet.
41
+ */
42
+ guard?: (context: never, command: never) => MaybePromise<void>;
43
+ /**
44
+ * Allowlist of aggregate types this operation's audit may reference. When
45
+ * declared, an audit entry whose aggregate.type is not listed throws —
46
+ * the audit vocabulary is enforced, not advisory.
47
+ */
48
+ aggregates?: readonly string[];
32
49
  audit: (resolution: Readonly<{
33
50
  command: never;
34
51
  result: never;
35
52
  }>, context: never) => MaybePromise<Omit<AuditEntryInput, 'classification'> | null>;
36
53
  }>;
54
+ /** Registry-wide defaults applied to every operation of one Command. */
55
+ type CommandDefaults = Readonly<{
56
+ /** Runs before every operation's own guard. Throw to deny. */
57
+ guard?: (context: never) => MaybePromise<void>;
58
+ }>;
59
+ /** Injected permission policy: throw to deny. Host semantics, kit ordering. */
60
+ type PermissionChecker = (context: never, input: Readonly<{
61
+ permission: string;
62
+ operation: string;
63
+ }>) => MaybePromise<void>;
37
64
  type AuditedRegistry = Readonly<Record<string, AuditedOperation>>;
38
65
  type FoundationOptions = Readonly<{
39
66
  observability: ObservabilityOptions & Readonly<{
@@ -43,6 +70,12 @@ type FoundationOptions = Readonly<{
43
70
  */
44
71
  writeAudit?: AuditWriter;
45
72
  }>;
73
+ /**
74
+ * Permission policy for operations that declare a `permission`. Runs
75
+ * before guards and the handler; throw to deny. An operation with a
76
+ * permission but no injected checker fails closed.
77
+ */
78
+ checkPermission?: PermissionChecker;
46
79
  }>;
47
80
  type FoundationComponentApi = Readonly<{
48
81
  functions: Readonly<{
@@ -57,10 +90,14 @@ type FoundationComponentApi = Readonly<{
57
90
  */
58
91
  declare class BoundCommand<Context, const Operations extends AuditedRegistry> {
59
92
  #private;
93
+ /** Declared aggregate types per operation — introspectable for tests. */
94
+ readonly aggregates: Readonly<{ [Key in OperationKey<Operations>]: Operations[Key]['aggregates']; }>;
60
95
  static operation<const Definition extends AuditedOperation>(definition: Definition): Definition;
61
96
  constructor(operations: Operations, deps: {
62
97
  observability: Observability;
63
98
  writeAudit: AuditWriter;
99
+ checkPermission?: PermissionChecker;
100
+ defaults?: CommandDefaults;
64
101
  });
65
102
  exec<const Key extends OperationKey<Operations>>(executor: {
66
103
  operation: Key;
@@ -73,7 +110,7 @@ declare class BoundCommand<Context, const Operations extends AuditedRegistry> {
73
110
  }
74
111
  type ApplicationCommand<Context, Operations extends AuditedRegistry> = BoundCommand<Context, Operations>;
75
112
  type CommandConstructor = {
76
- new <Context, const Operations extends AuditedRegistry>(operations: Operations): BoundCommand<Context, Operations>;
113
+ new <Context, const Operations extends AuditedRegistry>(operations: Operations, defaults?: CommandDefaults): BoundCommand<Context, Operations>;
77
114
  operation: (typeof BoundCommand)['operation'];
78
115
  };
79
116
  /**
@@ -89,5 +126,5 @@ declare class Foundation<Component extends FoundationComponentApi = FoundationCo
89
126
  constructor(component: Component, options: FoundationOptions);
90
127
  }
91
128
  //#endregion
92
- export { ApplicationCommand, AuditEntryInput, AuditWriter, AuditedOperation, AuditedRegistry, type CommandExecution, type CommandInput, type CommandObservation, type CommandRegistry, type CommandResult, Foundation, FoundationOptions, Observability, type ObservabilityOptions, type Parseable, Query, type QueryExecution, type Result, type ResultBoundary, type TransactionMetricsContext, client_d_exports, _default as default, emitSemanticEvent, executeResultBoundary, projectResult };
129
+ export { ApplicationCommand, AuditEntryInput, AuditWriter, AuditedOperation, AuditedRegistry, CommandDefaults, type CommandExecution, type CommandInput, type CommandObservation, type CommandRegistry, type CommandResult, Foundation, FoundationOptions, Observability, type ObservabilityOptions, type Parseable, PermissionChecker, Query, type QueryExecution, type Result, type ResultBoundary, type TransactionMetricsContext, client_d_exports, _default as default, emitSemanticEvent, executeResultBoundary, projectResult };
93
130
  //# sourceMappingURL=client.d.mts.map
@@ -1,2 +1,2 @@
1
- import{Command as e}from"./modules/command/command.mjs";import{Observability as t}from"./modules/observability/observability.mjs";import{Query as n}from"./modules/query/query.mjs";import{executeResultBoundary as r,projectResult as i}from"./result.mjs";import{emitSemanticEvent as a}from"./telemetry.mjs";import o from"./convex.config.mjs";var s=class{#e;#t;#n;static operation(e){return e}constructor(t,n){this.#t=n.observability,this.#n=n.writeAudit,this.#e=new e({operations:t,execute:e=>this.#r(e)})}exec(e){return this.#e.exec(e)}async#r(e){return this.#t.observe({operation:e.operation,classification:e.definition.classification},async()=>{let t=await e.run(),n=await e.definition.audit({command:e.command,result:t},e.context);return n&&await this.#n(e.context,{...n,classification:e.definition.classification}),t})}},c=class{status;Command;Query=n;observability;constructor(e,n){this.status=e.functions.status;let r=new t(n.observability);this.observability=r;let i=n.observability.writeAudit??(()=>void 0);this.Command=class extends s{constructor(e){super(e,{observability:r,writeAudit:i})}}}};export{c as Foundation,t as Observability,n as Query,o as default,a as emitSemanticEvent,r as executeResultBoundary,i as projectResult};
1
+ import{Command as e}from"./modules/command/command.mjs";import{Observability as t}from"./modules/observability/observability.mjs";import{Query as n}from"./modules/query/query.mjs";import{executeResultBoundary as r,projectResult as i}from"./result.mjs";import{emitSemanticEvent as a}from"./telemetry.mjs";import o from"./convex.config.mjs";var s=class{#e;#t;#n;#r;#i;aggregates;static operation(e){return e}constructor(t,n){this.#t=n.observability,this.#n=n.writeAudit,this.#r=n.checkPermission,this.#i=n.defaults??{},this.aggregates=Object.freeze(Object.fromEntries(Object.entries(t).map(([e,t])=>[e,t.aggregates]))),this.#e=new e({operations:t,execute:e=>this.#a(e)})}exec(e){return this.#e.exec(e)}async#a(e){return this.#t.observe({operation:e.operation,classification:e.definition.classification},async()=>{let t=e.definition.permission;if(t!==void 0){if(!this.#r)throw new l(e.operation);await this.#r(e.context,{permission:t,operation:e.operation})}await this.#i.guard?.(e.context),await e.definition.guard?.(e.context,e.command);let n=await e.run(),r=await e.definition.audit({command:e.command,result:n},e.context);if(r){let t=e.definition.aggregates;if(t&&!t.includes(r.aggregate.type))throw new c(e.operation,r.aggregate.type);await this.#n(e.context,{...r,classification:e.definition.classification})}return n})}},c=class extends Error{code=`COMMAND_AGGREGATE_NOT_DECLARED`;name=`CommandAggregateError`;constructor(e,t){super(`Operation "${e}" audited aggregate type "${t}" outside its declared aggregates`)}},l=class extends Error{code=`COMMAND_PERMISSION_NOT_CONFIGURED`;name=`CommandPermissionError`;constructor(e){super(`Operation "${e}" declares a permission but the Foundation has no checkPermission`)}},u=class{status;Command;Query=n;observability;constructor(e,n){this.status=e.functions.status;let r=new t(n.observability);this.observability=r;let i=n.observability.writeAudit??(()=>void 0),a=n.checkPermission;this.Command=class extends s{constructor(e,t){super(e,{observability:r,writeAudit:i,checkPermission:a,defaults:t})}}}};export{u as Foundation,t as Observability,n as Query,o as default,a as emitSemanticEvent,r as executeResultBoundary,i as projectResult};
2
2
  //# sourceMappingURL=client.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":["#kernel","#observability","#writeAudit","CommandKernelClass","#execute"],"sources":["../../../src/components/foundation/client.ts"],"sourcesContent":["import {\n\tCommand as CommandKernelClass,\n\ttype CommandExecution,\n\ttype CommandInput,\n\ttype CommandRegistry,\n\ttype CommandResult,\n} from './modules/command/command'\nimport {\n\tObservability,\n\ttype ObservabilityOptions,\n} from './modules/observability/observability'\nimport { Query } from './modules/query/query'\n\n/** Shape handed to the injected audit writer; classification comes from the operation. */\nexport type AuditEntryInput = {\n\toperation: string\n\tactorId: string\n\taggregate: { type: string; id: string }\n\tmetadata?: Record<string, unknown>\n\tclassification: string\n}\n\nexport type AuditWriter = (\n\tcontext: never,\n\tentry: AuditEntryInput,\n) => Promise<unknown> | unknown\n\ntype MaybePromise<Value> = Value | Promise<Value>\ntype OperationKey<Operations extends CommandRegistry> = Extract<\n\tkeyof Operations,\n\tstring\n>\n\nexport type AuditedOperation = Readonly<{\n\tcommand: Readonly<{ parse: (value: unknown) => unknown }>\n\tresult: Readonly<{ parse: (value: unknown) => unknown }>\n\tclassification: string\n\taudit: (\n\t\tresolution: Readonly<{ command: never; result: never }>,\n\t\tcontext: never,\n\t) => MaybePromise<Omit<AuditEntryInput, 'classification'> | null>\n}>\n\nexport type AuditedRegistry = Readonly<Record<string, AuditedOperation>>\n\nexport type FoundationOptions = Readonly<{\n\tobservability: ObservabilityOptions &\n\t\tReadonly<{\n\t\t\t/**\n\t\t\t * In-transaction audit writer used by Command. Operations that\n\t\t\t * return an audit record get it written here automatically.\n\t\t\t */\n\t\t\twriteAudit?: AuditWriter\n\t\t}>\n}>\n\ntype FoundationComponentApi = Readonly<{\n\tfunctions: Readonly<{ status: unknown }>\n}>\n\n/**\n * Application command protocol bound to a Foundation instance:\n * validate, observe, execute, and audit. `classification` and `audit()`\n * are mandatory per operation — auditing is type-enforced, not opt-in.\n * Obtain it by destructuring the Foundation: `const { Command } = new Foundation(...)`.\n */\nclass BoundCommand<Context, const Operations extends AuditedRegistry> {\n\treadonly #kernel: CommandKernelClass<Context, Operations>\n\treadonly #observability: Observability\n\treadonly #writeAudit: AuditWriter\n\n\tstatic operation<const Definition extends AuditedOperation>(\n\t\tdefinition: Definition,\n\t): Definition {\n\t\treturn definition\n\t}\n\n\tconstructor(\n\t\toperations: Operations,\n\t\tdeps: { observability: Observability; writeAudit: AuditWriter },\n\t) {\n\t\tthis.#observability = deps.observability\n\t\tthis.#writeAudit = deps.writeAudit\n\t\tthis.#kernel = new CommandKernelClass<Context, Operations>({\n\t\t\toperations,\n\t\t\texecute: (execution) => this.#execute(execution),\n\t\t})\n\t}\n\n\texec<const Key extends OperationKey<Operations>>(executor: {\n\t\toperation: Key\n\t\thandler: (\n\t\t\tcontext: Context,\n\t\t\tcommand: CommandInput<Operations, Key>,\n\t\t) => MaybePromise<CommandResult<Operations, Key>>\n\t}) {\n\t\treturn this.#kernel.exec(executor)\n\t}\n\n\tasync #execute<Key extends OperationKey<Operations>>(\n\t\texecution: CommandExecution<Context, Operations, Key>,\n\t): Promise<CommandResult<Operations, Key>> {\n\t\treturn this.#observability.observe(\n\t\t\t{\n\t\t\t\toperation: execution.operation,\n\t\t\t\tclassification: execution.definition.classification,\n\t\t\t},\n\t\t\tasync () => {\n\t\t\t\tconst result = await execution.run()\n\t\t\t\tconst audit = await execution.definition.audit(\n\t\t\t\t\t{ command: execution.command, result } as never,\n\t\t\t\t\texecution.context as never,\n\t\t\t\t)\n\t\t\t\tif (audit) {\n\t\t\t\t\tawait this.#writeAudit(execution.context as never, {\n\t\t\t\t\t\t...audit,\n\t\t\t\t\t\tclassification: execution.definition.classification,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn result\n\t\t\t},\n\t\t)\n\t}\n}\n\nexport type ApplicationCommand<\n\tContext,\n\tOperations extends AuditedRegistry,\n> = BoundCommand<Context, Operations>\n\ntype CommandConstructor = {\n\tnew <Context, const Operations extends AuditedRegistry>(\n\t\toperations: Operations,\n\t): BoundCommand<Context, Operations>\n\toperation: (typeof BoundCommand)['operation']\n}\n\n/**\n * Host facade for a component that owns no application execution capability.\n * Declared once; the sole source of the command protocol, query kernel, and\n * observability: `const { Command, Query, observability } = new Foundation(...)`.\n */\nexport class Foundation<\n\tComponent extends FoundationComponentApi = FoundationComponentApi,\n> {\n\treadonly status: Component['functions']['status']\n\treadonly Command: CommandConstructor\n\treadonly Query = Query\n\treadonly observability: Observability\n\n\tconstructor(component: Component, options: FoundationOptions) {\n\t\tthis.status = component.functions.status\n\t\tconst observability = new Observability(options.observability)\n\t\tthis.observability = observability\n\t\tconst writeAudit = options.observability.writeAudit ?? (() => undefined)\n\t\tthis.Command = class <\n\t\t\tContext,\n\t\t\tconst Operations extends AuditedRegistry,\n\t\t> extends BoundCommand<Context, Operations> {\n\t\t\tconstructor(operations: Operations) {\n\t\t\t\tsuper(operations, { observability, writeAudit })\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport type {\n\tCommandExecution,\n\tCommandInput,\n\tCommandRegistry,\n\tCommandResult,\n\tParseable,\n} from './modules/command/command'\nexport { Observability } from './modules/observability/observability'\nexport type {\n\tCommandObservation,\n\tObservabilityOptions,\n} from './modules/observability/observability'\nexport { Query } from './modules/query/query'\nexport type { QueryExecution } from './modules/query/query'\nexport { executeResultBoundary, projectResult } from './result'\nexport type {\n\tResult,\n\tResultBoundary,\n\tTransactionMetricsContext,\n} from './result'\nexport { emitSemanticEvent } from './telemetry'\n\n// Component definition as default export: `import foundation from \"cvx-kit/components/foundation\"` → app.use(foundation)\nexport { default } from \"./convex.config\"\n"],"mappings":"mVAkEA,IAAM,EAAN,KAAsE,CACrE,GACA,GACA,GAEA,OAAO,UACN,EACa,CACb,OAAO,CACR,CAEA,YACC,EACA,EACC,CACD,KAAKC,GAAiB,EAAK,cAC3B,KAAKC,GAAc,EAAK,WACxB,KAAKF,GAAU,IAAIG,EAAwC,CAC1D,aACA,QAAU,GAAc,KAAKC,GAAS,CAAS,CAChD,CAAC,CACF,CAEA,KAAiD,EAM9C,CACF,OAAO,KAAKJ,GAAQ,KAAK,CAAQ,CAClC,CAEA,KAAMI,GACL,EAC0C,CAC1C,OAAO,KAAKH,GAAe,QAC1B,CACC,UAAW,EAAU,UACrB,eAAgB,EAAU,WAAW,cACtC,EACA,SAAY,CACX,IAAM,EAAS,MAAM,EAAU,IAAI,EAC7B,EAAQ,MAAM,EAAU,WAAW,MACxC,CAAE,QAAS,EAAU,QAAS,QAAO,EACrC,EAAU,OACX,EAOA,OANI,GACH,MAAM,KAAKC,GAAY,EAAU,QAAkB,CAClD,GAAG,EACH,eAAgB,EAAU,WAAW,cACtC,CAAC,EAEK,CACR,CACD,CACD,CACD,EAmBa,EAAb,KAEE,CACD,OACA,QACA,MAAiB,EACjB,cAEA,YAAY,EAAsB,EAA4B,CAC7D,KAAK,OAAS,EAAU,UAAU,OAClC,IAAM,EAAgB,IAAI,EAAc,EAAQ,aAAa,EAC7D,KAAK,cAAgB,EACrB,IAAM,EAAa,EAAQ,cAAc,iBAAqB,IAAA,IAC9D,KAAK,QAAU,cAGL,CAAkC,CAC3C,YAAY,EAAwB,CACnC,MAAM,EAAY,CAAE,gBAAe,YAAW,CAAC,CAChD,CACD,CACD,CACD"}
1
+ {"version":3,"file":"client.mjs","names":["#kernel","#observability","#writeAudit","#checkPermission","#defaults","CommandKernelClass","#execute"],"sources":["../../../src/components/foundation/client.ts"],"sourcesContent":["import {\n\tCommand as CommandKernelClass,\n\ttype CommandExecution,\n\ttype CommandInput,\n\ttype CommandRegistry,\n\ttype CommandResult,\n} from './modules/command/command'\nimport {\n\tObservability,\n\ttype ObservabilityOptions,\n} from './modules/observability/observability'\nimport { Query } from './modules/query/query'\n\n/** Shape handed to the injected audit writer; classification comes from the operation. */\nexport type AuditEntryInput = {\n\toperation: string\n\tactorId: string\n\taggregate: { type: string; id: string }\n\tmetadata?: Record<string, unknown>\n\tclassification: string\n}\n\nexport type AuditWriter = (\n\tcontext: never,\n\tentry: AuditEntryInput,\n) => Promise<unknown> | unknown\n\ntype MaybePromise<Value> = Value | Promise<Value>\ntype OperationKey<Operations extends CommandRegistry> = Extract<\n\tkeyof Operations,\n\tstring\n>\n\nexport type AuditedOperation = Readonly<{\n\tcommand: Readonly<{ parse: (value: unknown) => unknown }>\n\tresult: Readonly<{ parse: (value: unknown) => unknown }>\n\tclassification: string\n\t/**\n\t * Permission slug required to execute this operation. Checked through the\n\t * Foundation's injected checkPermission BEFORE guards and the handler;\n\t * declaring a permission without injecting a checker fails closed.\n\t */\n\tpermission?: string\n\t/**\n\t * Per-operation precondition, after the permission check and the registry\n\t * default guard, before the handler. Throw to deny — nothing has run yet.\n\t */\n\tguard?: (context: never, command: never) => MaybePromise<void>\n\t/**\n\t * Allowlist of aggregate types this operation's audit may reference. When\n\t * declared, an audit entry whose aggregate.type is not listed throws —\n\t * the audit vocabulary is enforced, not advisory.\n\t */\n\taggregates?: readonly string[]\n\taudit: (\n\t\tresolution: Readonly<{ command: never; result: never }>,\n\t\tcontext: never,\n\t) => MaybePromise<Omit<AuditEntryInput, 'classification'> | null>\n}>\n\n/** Registry-wide defaults applied to every operation of one Command. */\nexport type CommandDefaults = Readonly<{\n\t/** Runs before every operation's own guard. Throw to deny. */\n\tguard?: (context: never) => MaybePromise<void>\n}>\n\n/** Injected permission policy: throw to deny. Host semantics, kit ordering. */\nexport type PermissionChecker = (\n\tcontext: never,\n\tinput: Readonly<{ permission: string; operation: string }>,\n) => MaybePromise<void>\n\nexport type AuditedRegistry = Readonly<Record<string, AuditedOperation>>\n\nexport type FoundationOptions = Readonly<{\n\tobservability: ObservabilityOptions &\n\t\tReadonly<{\n\t\t\t/**\n\t\t\t * In-transaction audit writer used by Command. Operations that\n\t\t\t * return an audit record get it written here automatically.\n\t\t\t */\n\t\t\twriteAudit?: AuditWriter\n\t\t}>\n\t/**\n\t * Permission policy for operations that declare a `permission`. Runs\n\t * before guards and the handler; throw to deny. An operation with a\n\t * permission but no injected checker fails closed.\n\t */\n\tcheckPermission?: PermissionChecker\n}>\n\ntype FoundationComponentApi = Readonly<{\n\tfunctions: Readonly<{ status: unknown }>\n}>\n\n/**\n * Application command protocol bound to a Foundation instance:\n * validate, observe, execute, and audit. `classification` and `audit()`\n * are mandatory per operation — auditing is type-enforced, not opt-in.\n * Obtain it by destructuring the Foundation: `const { Command } = new Foundation(...)`.\n */\nclass BoundCommand<Context, const Operations extends AuditedRegistry> {\n\treadonly #kernel: CommandKernelClass<Context, Operations>\n\treadonly #observability: Observability\n\treadonly #writeAudit: AuditWriter\n\treadonly #checkPermission: PermissionChecker | undefined\n\treadonly #defaults: CommandDefaults\n\n\t/** Declared aggregate types per operation — introspectable for tests. */\n\treadonly aggregates: Readonly<{\n\t\t[Key in OperationKey<Operations>]: Operations[Key]['aggregates']\n\t}>\n\n\tstatic operation<const Definition extends AuditedOperation>(\n\t\tdefinition: Definition,\n\t): Definition {\n\t\treturn definition\n\t}\n\n\tconstructor(\n\t\toperations: Operations,\n\t\tdeps: {\n\t\t\tobservability: Observability\n\t\t\twriteAudit: AuditWriter\n\t\t\tcheckPermission?: PermissionChecker\n\t\t\tdefaults?: CommandDefaults\n\t\t},\n\t) {\n\t\tthis.#observability = deps.observability\n\t\tthis.#writeAudit = deps.writeAudit\n\t\tthis.#checkPermission = deps.checkPermission\n\t\tthis.#defaults = deps.defaults ?? {}\n\t\tthis.aggregates = Object.freeze(\n\t\t\tObject.fromEntries(\n\t\t\t\tObject.entries(operations).map(([operation, definition]) => [\n\t\t\t\t\toperation,\n\t\t\t\t\tdefinition.aggregates,\n\t\t\t\t]),\n\t\t\t),\n\t\t) as this['aggregates']\n\t\tthis.#kernel = new CommandKernelClass<Context, Operations>({\n\t\t\toperations,\n\t\t\texecute: (execution) => this.#execute(execution),\n\t\t})\n\t}\n\n\texec<const Key extends OperationKey<Operations>>(executor: {\n\t\toperation: Key\n\t\thandler: (\n\t\t\tcontext: Context,\n\t\t\tcommand: CommandInput<Operations, Key>,\n\t\t) => MaybePromise<CommandResult<Operations, Key>>\n\t}) {\n\t\treturn this.#kernel.exec(executor)\n\t}\n\n\tasync #execute<Key extends OperationKey<Operations>>(\n\t\texecution: CommandExecution<Context, Operations, Key>,\n\t): Promise<CommandResult<Operations, Key>> {\n\t\treturn this.#observability.observe(\n\t\t\t{\n\t\t\t\toperation: execution.operation,\n\t\t\t\tclassification: execution.definition.classification,\n\t\t\t},\n\t\t\tasync () => {\n\t\t\t\t// Guards run before the handler: permission → default → operation.\n\t\t\t\tconst permission = execution.definition.permission\n\t\t\t\tif (permission !== undefined) {\n\t\t\t\t\tif (!this.#checkPermission) {\n\t\t\t\t\t\tthrow new CommandPermissionError(execution.operation)\n\t\t\t\t\t}\n\t\t\t\t\tawait this.#checkPermission(execution.context as never, {\n\t\t\t\t\t\tpermission,\n\t\t\t\t\t\toperation: execution.operation,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tawait this.#defaults.guard?.(execution.context as never)\n\t\t\t\tawait execution.definition.guard?.(\n\t\t\t\t\texecution.context as never,\n\t\t\t\t\texecution.command as never,\n\t\t\t\t)\n\t\t\t\tconst result = await execution.run()\n\t\t\t\tconst audit = await execution.definition.audit(\n\t\t\t\t\t{ command: execution.command, result } as never,\n\t\t\t\t\texecution.context as never,\n\t\t\t\t)\n\t\t\t\tif (audit) {\n\t\t\t\t\tconst allowed = execution.definition.aggregates\n\t\t\t\t\tif (allowed && !allowed.includes(audit.aggregate.type)) {\n\t\t\t\t\t\tthrow new CommandAggregateError(\n\t\t\t\t\t\t\texecution.operation,\n\t\t\t\t\t\t\taudit.aggregate.type,\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\tawait this.#writeAudit(execution.context as never, {\n\t\t\t\t\t\t...audit,\n\t\t\t\t\t\tclassification: execution.definition.classification,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn result\n\t\t\t},\n\t\t)\n\t}\n}\n\n/** The audit referenced an aggregate type outside the operation's allowlist. */\nclass CommandAggregateError extends Error {\n\treadonly code = 'COMMAND_AGGREGATE_NOT_DECLARED'\n\treadonly name = 'CommandAggregateError'\n\n\tconstructor(operation: string, aggregateType: string) {\n\t\tsuper(\n\t\t\t`Operation \"${operation}\" audited aggregate type \"${aggregateType}\" outside its declared aggregates`,\n\t\t)\n\t}\n}\n\n/** Fails closed: an operation declared a permission but no checker exists. */\nclass CommandPermissionError extends Error {\n\treadonly code = 'COMMAND_PERMISSION_NOT_CONFIGURED'\n\treadonly name = 'CommandPermissionError'\n\n\tconstructor(operation: string) {\n\t\tsuper(\n\t\t\t`Operation \"${operation}\" declares a permission but the Foundation has no checkPermission`,\n\t\t)\n\t}\n}\n\nexport type ApplicationCommand<\n\tContext,\n\tOperations extends AuditedRegistry,\n> = BoundCommand<Context, Operations>\n\ntype CommandConstructor = {\n\tnew <Context, const Operations extends AuditedRegistry>(\n\t\toperations: Operations,\n\t\tdefaults?: CommandDefaults,\n\t): BoundCommand<Context, Operations>\n\toperation: (typeof BoundCommand)['operation']\n}\n\n/**\n * Host facade for a component that owns no application execution capability.\n * Declared once; the sole source of the command protocol, query kernel, and\n * observability: `const { Command, Query, observability } = new Foundation(...)`.\n */\nexport class Foundation<\n\tComponent extends FoundationComponentApi = FoundationComponentApi,\n> {\n\treadonly status: Component['functions']['status']\n\treadonly Command: CommandConstructor\n\treadonly Query = Query\n\treadonly observability: Observability\n\n\tconstructor(component: Component, options: FoundationOptions) {\n\t\tthis.status = component.functions.status\n\t\tconst observability = new Observability(options.observability)\n\t\tthis.observability = observability\n\t\tconst writeAudit = options.observability.writeAudit ?? (() => undefined)\n\t\tconst checkPermission = options.checkPermission\n\t\tthis.Command = class <\n\t\t\tContext,\n\t\t\tconst Operations extends AuditedRegistry,\n\t\t> extends BoundCommand<Context, Operations> {\n\t\t\tconstructor(operations: Operations, defaults?: CommandDefaults) {\n\t\t\t\tsuper(operations, {\n\t\t\t\t\tobservability,\n\t\t\t\t\twriteAudit,\n\t\t\t\t\tcheckPermission,\n\t\t\t\t\tdefaults,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport type {\n\tCommandExecution,\n\tCommandInput,\n\tCommandRegistry,\n\tCommandResult,\n\tParseable,\n} from './modules/command/command'\nexport { Observability } from './modules/observability/observability'\nexport type {\n\tCommandObservation,\n\tObservabilityOptions,\n} from './modules/observability/observability'\nexport { Query } from './modules/query/query'\nexport type { QueryExecution } from './modules/query/query'\nexport { executeResultBoundary, projectResult } from './result'\nexport type {\n\tResult,\n\tResultBoundary,\n\tTransactionMetricsContext,\n} from './result'\nexport { emitSemanticEvent } from './telemetry'\n\n// Component definition as default export: `import foundation from \"cvx-kit/components/foundation\"` → app.use(foundation)\nexport { default } from \"./convex.config\"\n"],"mappings":"mVAqGA,IAAM,EAAN,KAAsE,CACrE,GACA,GACA,GACA,GACA,GAGA,WAIA,OAAO,UACN,EACa,CACb,OAAO,CACR,CAEA,YACC,EACA,EAMC,CACD,KAAKC,GAAiB,EAAK,cAC3B,KAAKC,GAAc,EAAK,WACxB,KAAKC,GAAmB,EAAK,gBAC7B,KAAKC,GAAY,EAAK,UAAY,CAAC,EACnC,KAAK,WAAa,OAAO,OACxB,OAAO,YACN,OAAO,QAAQ,CAAU,CAAC,CAAC,KAAK,CAAC,EAAW,KAAgB,CAC3D,EACA,EAAW,UACZ,CAAC,CACF,CACD,EACA,KAAKJ,GAAU,IAAIK,EAAwC,CAC1D,aACA,QAAU,GAAc,KAAKC,GAAS,CAAS,CAChD,CAAC,CACF,CAEA,KAAiD,EAM9C,CACF,OAAO,KAAKN,GAAQ,KAAK,CAAQ,CAClC,CAEA,KAAMM,GACL,EAC0C,CAC1C,OAAO,KAAKL,GAAe,QAC1B,CACC,UAAW,EAAU,UACrB,eAAgB,EAAU,WAAW,cACtC,EACA,SAAY,CAEX,IAAM,EAAa,EAAU,WAAW,WACxC,GAAI,IAAe,IAAA,GAAW,CAC7B,GAAI,CAAC,KAAKE,GACT,MAAM,IAAI,EAAuB,EAAU,SAAS,EAErD,MAAM,KAAKA,GAAiB,EAAU,QAAkB,CACvD,aACA,UAAW,EAAU,SACtB,CAAC,CACF,CACA,MAAM,KAAKC,GAAU,QAAQ,EAAU,OAAgB,EACvD,MAAM,EAAU,WAAW,QAC1B,EAAU,QACV,EAAU,OACX,EACA,IAAM,EAAS,MAAM,EAAU,IAAI,EAC7B,EAAQ,MAAM,EAAU,WAAW,MACxC,CAAE,QAAS,EAAU,QAAS,QAAO,EACrC,EAAU,OACX,EACA,GAAI,EAAO,CACV,IAAM,EAAU,EAAU,WAAW,WACrC,GAAI,GAAW,CAAC,EAAQ,SAAS,EAAM,UAAU,IAAI,EACpD,MAAM,IAAI,EACT,EAAU,UACV,EAAM,UAAU,IACjB,EAED,MAAM,KAAKF,GAAY,EAAU,QAAkB,CAClD,GAAG,EACH,eAAgB,EAAU,WAAW,cACtC,CAAC,CACF,CACA,OAAO,CACR,CACD,CACD,CACD,EAGM,EAAN,cAAoC,KAAM,CACzC,KAAgB,iCAChB,KAAgB,wBAEhB,YAAY,EAAmB,EAAuB,CACrD,MACC,cAAc,EAAU,4BAA4B,EAAc,kCACnE,CACD,CACD,EAGM,EAAN,cAAqC,KAAM,CAC1C,KAAgB,oCAChB,KAAgB,yBAEhB,YAAY,EAAmB,CAC9B,MACC,cAAc,EAAU,kEACzB,CACD,CACD,EAoBa,EAAb,KAEE,CACD,OACA,QACA,MAAiB,EACjB,cAEA,YAAY,EAAsB,EAA4B,CAC7D,KAAK,OAAS,EAAU,UAAU,OAClC,IAAM,EAAgB,IAAI,EAAc,EAAQ,aAAa,EAC7D,KAAK,cAAgB,EACrB,IAAM,EAAa,EAAQ,cAAc,iBAAqB,IAAA,IACxD,EAAkB,EAAQ,gBAChC,KAAK,QAAU,cAGL,CAAkC,CAC3C,YAAY,EAAwB,EAA4B,CAC/D,MAAM,EAAY,CACjB,gBACA,aACA,kBACA,UACD,CAAC,CACF,CACD,CACD,CACD"}
package/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ErrorFactory, KitError, defaultErrors } from "./errors.mjs";
2
- import { Actor, AuthBundle, AuthFunctionsConfig, DefaultRole, Include, IncludedQuery, createAuthFunctions, createInclude, defaultRoleMap } from "./auth.mjs";
3
- import { ApplicationCommand, AuditEntryInput, AuditWriter, AuditedOperation, AuditedRegistry, Foundation, FoundationOptions } from "./components/foundation/client.mjs";
4
- import { TIMESTAMP_FIELDS, TableBoundaryOptions, jsonSafeZid, zodTable, zodVariantTable } from "./zod-table.mjs";
5
- import { Change, Trigger, Triggers, appendOnly, createTriggers, noDelete, timestamps } from "./triggers.mjs";
6
- export { type Actor, type ApplicationCommand, type AuditEntryInput, type AuditWriter, type AuditedOperation, type AuditedRegistry, type AuthBundle, type AuthFunctionsConfig, type Change, type DefaultRole, type ErrorFactory, Foundation, type FoundationOptions, type Include, type IncludedQuery, KitError, TIMESTAMP_FIELDS, type TableBoundaryOptions, type Trigger, Triggers, appendOnly, createAuthFunctions, createInclude, createTriggers, defaultErrors, defaultRoleMap, jsonSafeZid, noDelete, timestamps, zodTable, zodVariantTable };
2
+ import { RLSConfig, Rules, TENANT_FIELD, assertTenantOwned, composeRules, createTenantRules, requireTenantReference } from "./tenancy.mjs";
3
+ import { Actor, AuthBundle, AuthFunctionsConfig, DefaultRole, Include, IncludedQuery, SecurityConfig, createAuthFunctions, createInclude, defaultRoleMap } from "./auth.mjs";
4
+ import { ApplicationCommand, AuditEntryInput, AuditWriter, AuditedOperation, AuditedRegistry, CommandDefaults, Foundation, FoundationOptions, PermissionChecker } from "./components/foundation/client.mjs";
5
+ import { TIMESTAMP_FIELDS, TableBoundaryOptions, createModule, jsonSafeZid, tenantTable, zodTable, zodVariantTable } from "./zod-table.mjs";
6
+ import { Change, Trigger, Triggers, appendOnly, createTriggers, noDelete, tenantOwnership, timestamps } from "./triggers.mjs";
7
+ export { type Actor, type ApplicationCommand, type AuditEntryInput, type AuditWriter, type AuditedOperation, type AuditedRegistry, type AuthBundle, type AuthFunctionsConfig, type Change, type CommandDefaults, type DefaultRole, type ErrorFactory, Foundation, type FoundationOptions, type Include, type IncludedQuery, KitError, type PermissionChecker, type RLSConfig, type Rules, type SecurityConfig, TENANT_FIELD, TIMESTAMP_FIELDS, type TableBoundaryOptions, type Trigger, Triggers, appendOnly, assertTenantOwned, composeRules, createAuthFunctions, createInclude, createModule, createTenantRules, createTriggers, defaultErrors, defaultRoleMap, jsonSafeZid, noDelete, requireTenantReference, tenantOwnership, tenantTable, timestamps, zodTable, zodVariantTable };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{KitError as e,defaultErrors as t}from"./errors.mjs";import{createAuthFunctions as n,createInclude as r,defaultRoleMap as i}from"./auth.mjs";import{TIMESTAMP_FIELDS as a,jsonSafeZid as o,zodTable as s,zodVariantTable as c}from"./zod-table.mjs";import{Triggers as l,appendOnly as u,createTriggers as d,noDelete as f,timestamps as p}from"./triggers.mjs";import{Foundation as m}from"./components/foundation/client.mjs";export{m as Foundation,e as KitError,a as TIMESTAMP_FIELDS,l as Triggers,u as appendOnly,n as createAuthFunctions,r as createInclude,d as createTriggers,t as defaultErrors,i as defaultRoleMap,o as jsonSafeZid,f as noDelete,p as timestamps,s as zodTable,c as zodVariantTable};
1
+ import{KitError as e,defaultErrors as t}from"./errors.mjs";import{TENANT_FIELD as n,assertTenantOwned as r,composeRules as i,createTenantRules as a,requireTenantReference as o}from"./tenancy.mjs";import{createAuthFunctions as s,createInclude as c,defaultRoleMap as l}from"./auth.mjs";import{TIMESTAMP_FIELDS as u,createModule as d,jsonSafeZid as f,tenantTable as p,zodTable as m,zodVariantTable as h}from"./zod-table.mjs";import{Triggers as g,appendOnly as _,createTriggers as v,noDelete as y,tenantOwnership as b,timestamps as x}from"./triggers.mjs";import{Foundation as S}from"./components/foundation/client.mjs";export{S as Foundation,e as KitError,n as TENANT_FIELD,u as TIMESTAMP_FIELDS,g as Triggers,_ as appendOnly,r as assertTenantOwned,i as composeRules,s as createAuthFunctions,c as createInclude,d as createModule,a as createTenantRules,v as createTriggers,t as defaultErrors,l as defaultRoleMap,f as jsonSafeZid,y as noDelete,o as requireTenantReference,b as tenantOwnership,p as tenantTable,x as timestamps,m as zodTable,h as zodVariantTable};
@@ -0,0 +1,34 @@
1
+ import { ErrorFactory } from "./errors.mjs";
2
+ import { RLSConfig, Rules, wrapDatabaseReader, wrapDatabaseWriter } from "convex-helpers/server/rowLevelSecurity";
3
+ import { GenericDataModel, TableNamesInDataModel } from "convex/server";
4
+ //#region src/tenancy.d.ts
5
+ /** The server-owned tenancy field stamped on every tenant table row. */
6
+ declare const TENANT_FIELD: 'tenant';
7
+ /**
8
+ * Deny-oriented row-level security rules generated mechanically from a table
9
+ * registry: read/insert/modify pass only when the row's tenant matches.
10
+ * Registering a table here is the entire onboarding — there is no second
11
+ * per-table step to forget.
12
+ */
13
+ declare function createTenantRules<DataModel extends GenericDataModel>(tenant: string, tables: readonly TableNamesInDataModel<DataModel>[]): Rules<unknown, DataModel>;
14
+ /**
15
+ * AND-composes rule sets: a row passes an operation only when every rule set
16
+ * that defines a rule for it passes. Lets tenant isolation and role-level
17
+ * rules coexist on the same wrapped database.
18
+ */
19
+ declare function composeRules<Ctx, DataModel extends GenericDataModel>(...sets: readonly Rules<Ctx, DataModel>[]): Rules<Ctx, DataModel>;
20
+ /**
21
+ * Loads a client-referenced document and verifies tenant ownership without
22
+ * disclosing existence: a missing row and a foreign row fail identically
23
+ * with REFERENCE_NOT_FOUND. Use for every id argument a client supplies.
24
+ */
25
+ declare function requireTenantReference<Document extends {
26
+ [TENANT_FIELD]?: unknown;
27
+ }>(tenant: string, load: () => Promise<Document | null>, errors?: ErrorFactory): Promise<Document>;
28
+ /** Asserts a loaded document belongs to the tenant; use on internal paths. */
29
+ declare function assertTenantOwned<Document extends {
30
+ [TENANT_FIELD]?: unknown;
31
+ }>(tenant: string, document: Document, errors?: ErrorFactory): Document;
32
+ //#endregion
33
+ export { type RLSConfig, type Rules, TENANT_FIELD, assertTenantOwned, composeRules, createTenantRules, requireTenantReference, wrapDatabaseReader, wrapDatabaseWriter };
34
+ //# sourceMappingURL=tenancy.d.mts.map
@@ -0,0 +1,2 @@
1
+ import{defaultErrors as e}from"./errors.mjs";import{wrapDatabaseReader as t,wrapDatabaseWriter as n}from"convex-helpers/server/rowLevelSecurity";const r=`tenant`;function i(e,t){let n=async(t,n)=>n[r]===e;return Object.fromEntries(t.map(e=>[e,{read:n,insert:n,modify:n}]))}function a(...e){let t=new Set(e.flatMap(e=>Object.keys(e))),n={};for(let r of t){let t={};for(let n of[`read`,`insert`,`modify`]){let i=e.map(e=>e[r]).map(e=>e?.[n]).filter(e=>typeof e==`function`);i.length!==0&&(t[n]=async(e,t)=>{for(let n of i)if(!await n(e,t))return!1;return!0})}n[r]=t}return n}async function o(t,n,r=e){let i=await n();return!i||i.tenant!==t?r.throw({code:`REFERENCE_NOT_FOUND`,message:`The referenced row is unavailable`}):i}function s(t,n,r=e){return n.tenant===t?n:r.throw({code:`CROSS_TENANT_REFERENCE`,message:`The document belongs to another tenant`})}export{r as TENANT_FIELD,s as assertTenantOwned,a as composeRules,i as createTenantRules,o as requireTenantReference,t as wrapDatabaseReader,n as wrapDatabaseWriter};
2
+ //# sourceMappingURL=tenancy.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenancy.mjs","names":[],"sources":["../src/tenancy.ts"],"sourcesContent":["import {\n\twrapDatabaseReader,\n\twrapDatabaseWriter,\n\ttype RLSConfig,\n\ttype Rules,\n} from 'convex-helpers/server/rowLevelSecurity'\nimport type {\n\tGenericDataModel,\n\tTableNamesInDataModel,\n} from 'convex/server'\nimport { defaultErrors, type ErrorFactory } from './errors'\n\nexport { wrapDatabaseReader, wrapDatabaseWriter }\nexport type { RLSConfig, Rules }\n\n/** The server-owned tenancy field stamped on every tenant table row. */\nexport const TENANT_FIELD = 'tenant' as const\n\ntype TenantRow = { [TENANT_FIELD]?: unknown }\n\n/**\n * Deny-oriented row-level security rules generated mechanically from a table\n * registry: read/insert/modify pass only when the row's tenant matches.\n * Registering a table here is the entire onboarding — there is no second\n * per-table step to forget.\n */\nexport function createTenantRules<DataModel extends GenericDataModel>(\n\ttenant: string,\n\ttables: readonly TableNamesInDataModel<DataModel>[],\n): Rules<unknown, DataModel> {\n\tconst owned = async (_ctx: unknown, doc: TenantRow) =>\n\t\tdoc[TENANT_FIELD] === tenant\n\treturn Object.fromEntries(\n\t\ttables.map((table) => [\n\t\t\ttable,\n\t\t\t{ read: owned, insert: owned, modify: owned },\n\t\t]),\n\t) as unknown as Rules<unknown, DataModel>\n}\n\n/**\n * AND-composes rule sets: a row passes an operation only when every rule set\n * that defines a rule for it passes. Lets tenant isolation and role-level\n * rules coexist on the same wrapped database.\n */\nexport function composeRules<Ctx, DataModel extends GenericDataModel>(\n\t...sets: readonly Rules<Ctx, DataModel>[]\n): Rules<Ctx, DataModel> {\n\tconst tables = new Set(sets.flatMap((set) => Object.keys(set)))\n\tconst composed: Record<string, Record<string, unknown>> = {}\n\tfor (const table of tables) {\n\t\tconst operations: Record<string, unknown> = {}\n\t\tfor (const operation of ['read', 'insert', 'modify'] as const) {\n\t\t\tconst rules = sets\n\t\t\t\t.map((set) => (set as Record<string, Record<string, unknown>>)[table])\n\t\t\t\t.map((tableRules) => tableRules?.[operation])\n\t\t\t\t.filter((rule): rule is (ctx: Ctx, doc: never) => Promise<boolean> =>\n\t\t\t\t\ttypeof rule === 'function',\n\t\t\t\t)\n\t\t\tif (rules.length === 0) continue\n\t\t\toperations[operation] = async (ctx: Ctx, doc: never) => {\n\t\t\t\tfor (const rule of rules) {\n\t\t\t\t\tif (!(await rule(ctx, doc))) return false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\tcomposed[table] = operations\n\t}\n\treturn composed as Rules<Ctx, DataModel>\n}\n\n/**\n * Loads a client-referenced document and verifies tenant ownership without\n * disclosing existence: a missing row and a foreign row fail identically\n * with REFERENCE_NOT_FOUND. Use for every id argument a client supplies.\n */\nexport async function requireTenantReference<\n\tDocument extends { [TENANT_FIELD]?: unknown },\n>(\n\ttenant: string,\n\tload: () => Promise<Document | null>,\n\terrors: ErrorFactory = defaultErrors,\n): Promise<Document> {\n\tconst document = await load()\n\tif (!document || document[TENANT_FIELD] !== tenant) {\n\t\treturn errors.throw({\n\t\t\tcode: 'REFERENCE_NOT_FOUND',\n\t\t\tmessage: 'The referenced row is unavailable',\n\t\t})\n\t}\n\treturn document\n}\n\n/** Asserts a loaded document belongs to the tenant; use on internal paths. */\nexport function assertTenantOwned<\n\tDocument extends { [TENANT_FIELD]?: unknown },\n>(\n\ttenant: string,\n\tdocument: Document,\n\terrors: ErrorFactory = defaultErrors,\n): Document {\n\tif (document[TENANT_FIELD] !== tenant) {\n\t\treturn errors.throw({\n\t\t\tcode: 'CROSS_TENANT_REFERENCE',\n\t\t\tmessage: 'The document belongs to another tenant',\n\t\t})\n\t}\n\treturn document\n}\n"],"mappings":"iJAgBA,MAAa,EAAe,SAU5B,SAAgB,EACf,EACA,EAC4B,CAC5B,IAAM,EAAQ,MAAO,EAAe,IACnC,EAAI,KAAkB,EACvB,OAAO,OAAO,YACb,EAAO,IAAK,GAAU,CACrB,EACA,CAAE,KAAM,EAAO,OAAQ,EAAO,OAAQ,CAAM,CAC7C,CAAC,CACF,CACD,CAOA,SAAgB,EACf,GAAG,EACqB,CACxB,IAAM,EAAS,IAAI,IAAI,EAAK,QAAS,GAAQ,OAAO,KAAK,CAAG,CAAC,CAAC,EACxD,EAAoD,CAAC,EAC3D,IAAK,IAAM,KAAS,EAAQ,CAC3B,IAAM,EAAsC,CAAC,EAC7C,IAAK,IAAM,IAAa,CAAC,OAAQ,SAAU,QAAQ,EAAY,CAC9D,IAAM,EAAQ,EACZ,IAAK,GAAS,EAAgD,EAAM,CAAC,CACrE,IAAK,GAAe,IAAa,EAAU,CAAC,CAC5C,OAAQ,GACR,OAAO,GAAS,UACjB,EACG,EAAM,SAAW,IACrB,EAAW,GAAa,MAAO,EAAU,IAAe,CACvD,IAAK,IAAM,KAAQ,EAClB,GAAI,CAAE,MAAM,EAAK,EAAK,CAAG,EAAI,MAAO,GAErC,MAAO,EACR,EACD,CACA,EAAS,GAAS,CACnB,CACA,OAAO,CACR,CAOA,eAAsB,EAGrB,EACA,EACA,EAAuB,EACH,CACpB,IAAM,EAAW,MAAM,EAAK,EAO5B,MANI,CAAC,GAAY,EAAA,SAA2B,EACpC,EAAO,MAAM,CACnB,KAAM,sBACN,QAAS,mCACV,CAAC,EAEK,CACR,CAGA,SAAgB,EAGf,EACA,EACA,EAAuB,EACZ,CAOX,OANI,EAAA,SAA2B,EAMxB,EALC,EAAO,MAAM,CACnB,KAAM,yBACN,QAAS,wCACV,CAAC,CAGH"}
@@ -15,6 +15,14 @@ declare function createTriggers<DataModel extends GenericDataModel>(): Triggers<
15
15
  declare function appendOnly<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
16
16
  /** Forbids hard deletes on the given tables (soft-delete discipline). */
17
17
  declare function noDelete<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
18
+ /**
19
+ * Post-image tenant invariant for tenantTable rows. Row-level security
20
+ * authorizes a modification against the PRE-image, so ownership must also be
21
+ * enforced against the post-image: inserts must carry a tenant, and updates
22
+ * may never reassign it. Register for every tenant table (same registry that
23
+ * drives createTenantRules).
24
+ */
25
+ declare function tenantOwnership<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
18
26
  /**
19
27
  * Maintains zod-table's opinionated lifecycle timestamps: createdAt and
20
28
  * updatedAt on insert, updatedAt on every update. archivedAt stays under
@@ -23,5 +31,5 @@ declare function noDelete<DataModel extends GenericDataModel>(triggers: Triggers
23
31
  */
24
32
  declare function timestamps<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
25
33
  //#endregion
26
- export { type Change, type Trigger, Triggers, appendOnly, createTriggers, noDelete, timestamps };
34
+ export { type Change, type Trigger, Triggers, appendOnly, createTriggers, noDelete, tenantOwnership, timestamps };
27
35
  //# sourceMappingURL=triggers.d.mts.map
package/dist/triggers.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{Triggers as e}from"convex-helpers/server/triggers";function t(){return new e}function n(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation!==`insert`)throw Error(`${n} is append-only`)})}function r(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`delete`)throw Error(`${n} rows cannot be deleted`)})}function i(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`delete`)return;let n=Date.now();if(t.operation===`insert`){t.newDoc.createdAt===void 0&&await e.innerDb.patch(t.id,{createdAt:n,updatedAt:n});return}let r=t.oldDoc;t.newDoc.updatedAt===r.updatedAt&&await e.innerDb.patch(t.id,{updatedAt:n})})}export{e as Triggers,n as appendOnly,t as createTriggers,r as noDelete,i as timestamps};
1
+ import{Triggers as e}from"convex-helpers/server/triggers";function t(){return new e}function n(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation!==`insert`)throw Error(`${n} is append-only`)})}function r(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`delete`)throw Error(`${n} rows cannot be deleted`)})}function i(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`insert`){let e=t.newDoc;if(typeof e.tenant!=`string`||e.tenant.length===0)throw Error(`${n} rows require a tenant`);return}if(t.operation===`update`){let e=t.oldDoc,r=t.newDoc;if(e.tenant!==r.tenant)throw Error(`${n} tenant ownership cannot be reassigned`)}})}function a(e,...t){for(let n of t)e.register(n,async(e,t)=>{if(t.operation===`delete`)return;let n=Date.now();if(t.operation===`insert`){t.newDoc.createdAt===void 0&&await e.innerDb.patch(t.id,{createdAt:n,updatedAt:n});return}let r=t.oldDoc;t.newDoc.updatedAt===r.updatedAt&&await e.innerDb.patch(t.id,{updatedAt:n})})}export{e as Triggers,n as appendOnly,t as createTriggers,r as noDelete,i as tenantOwnership,a as timestamps};
2
2
  //# sourceMappingURL=triggers.mjs.map