cvx-kit 0.1.7 → 0.1.10

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.
Files changed (154) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +63 -47
  3. package/dist/agent-tools.d.mts +5 -5
  4. package/dist/agent-tools.mjs.map +1 -1
  5. package/dist/auth.d.mts +39 -289
  6. package/dist/auth.mjs.map +1 -1
  7. package/dist/components/approvals/_generated/api.d.mts +3 -4
  8. package/dist/components/approvals/_generated/api.mjs.map +1 -1
  9. package/dist/components/approvals/_generated/component.d.mts +1 -2
  10. package/dist/components/approvals/_generated/dataModel.d.mts +4 -5
  11. package/dist/components/approvals/_generated/server.d.mts +12 -13
  12. package/dist/components/approvals/_generated/server.mjs.map +1 -1
  13. package/dist/components/approvals/audit.d.mts +8 -8
  14. package/dist/components/approvals/audit.mjs +1 -1
  15. package/dist/components/approvals/audit.mjs.map +1 -1
  16. package/dist/components/approvals/client.d.mts +10 -10
  17. package/dist/components/approvals/client.mjs.map +1 -1
  18. package/dist/components/approvals/constants.d.mts +30 -30
  19. package/dist/components/approvals/constants.mjs.map +1 -1
  20. package/dist/components/approvals/convex.config.mjs.map +1 -1
  21. package/dist/components/approvals/decisions.d.mts +6 -6
  22. package/dist/components/approvals/decisions.mjs.map +1 -1
  23. package/dist/components/approvals/functions.d.mts +3 -3
  24. package/dist/components/approvals/functions.mjs.map +1 -1
  25. package/dist/components/approvals/health.d.mts +2 -2
  26. package/dist/components/approvals/health.mjs.map +1 -1
  27. package/dist/components/approvals/requests.d.mts +8 -8
  28. package/dist/components/approvals/requests.mjs.map +1 -1
  29. package/dist/components/approvals/validators.d.mts +29 -29
  30. package/dist/components/approvals/validators.mjs +1 -1
  31. package/dist/components/approvals/validators.mjs.map +1 -1
  32. package/dist/components/approvals/workflow.d.mts +3 -3
  33. package/dist/components/approvals/workflow.mjs.map +1 -1
  34. package/dist/components/approvals/workflow_steps.d.mts +15 -15
  35. package/dist/components/approvals/workflow_steps.mjs.map +1 -1
  36. package/dist/components/foundation/_generated/api.d.mts +3 -4
  37. package/dist/components/foundation/_generated/api.mjs.map +1 -1
  38. package/dist/components/foundation/_generated/component.d.mts +1 -2
  39. package/dist/components/foundation/_generated/dataModel.d.mts +4 -5
  40. package/dist/components/foundation/_generated/server.d.mts +12 -13
  41. package/dist/components/foundation/_generated/server.mjs.map +1 -1
  42. package/dist/components/foundation/client.d.mts +21 -13
  43. package/dist/components/foundation/client.mjs.map +1 -1
  44. package/dist/components/foundation/convex.config.mjs.map +1 -1
  45. package/dist/components/foundation/functions.d.mts +2 -2
  46. package/dist/components/foundation/functions.mjs.map +1 -1
  47. package/dist/components/foundation/modules/command/command.d.mts +14 -14
  48. package/dist/components/foundation/modules/command/command.mjs.map +1 -1
  49. package/dist/components/foundation/modules/command/lifecycle.d.mts +12 -13
  50. package/dist/components/foundation/modules/command/lifecycle.mjs.map +1 -1
  51. package/dist/components/foundation/modules/observability/observability.d.mts +4 -4
  52. package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
  53. package/dist/components/foundation/modules/query/query.d.mts +5 -5
  54. package/dist/components/foundation/modules/query/query.mjs.map +1 -1
  55. package/dist/components/foundation/operation.d.mts +5 -6
  56. package/dist/components/foundation/operation.mjs.map +1 -1
  57. package/dist/components/foundation/result.d.mts +6 -6
  58. package/dist/components/foundation/result.mjs.map +1 -1
  59. package/dist/components/foundation/telemetry.d.mts +2 -2
  60. package/dist/components/foundation/telemetry.mjs.map +1 -1
  61. package/dist/crud.d.mts +6 -5
  62. package/dist/crud.mjs.map +1 -1
  63. package/dist/errors.d.mts +4 -4
  64. package/dist/errors.mjs.map +1 -1
  65. package/dist/index.d.mts +2 -2
  66. package/dist/middleware.d.mts +2 -3
  67. package/dist/middleware.mjs.map +1 -1
  68. package/dist/modules/contracts/contract.d.mts +12 -13
  69. package/dist/modules/contracts/contract.mjs.map +1 -1
  70. package/dist/modules/contracts/convex.d.mts +3 -5
  71. package/dist/modules/contracts/convex.mjs.map +1 -1
  72. package/dist/modules/contracts/errors.d.mts +8 -9
  73. package/dist/modules/contracts/errors.mjs +1 -1
  74. package/dist/modules/contracts/errors.mjs.map +1 -1
  75. package/dist/modules/contracts/exposure.d.mts +13 -14
  76. package/dist/modules/contracts/exposure.mjs +1 -1
  77. package/dist/modules/contracts/exposure.mjs.map +1 -1
  78. package/dist/modules/contracts/idempotency.d.mts +15 -16
  79. package/dist/modules/contracts/idempotency.mjs +1 -1
  80. package/dist/modules/contracts/idempotency.mjs.map +1 -1
  81. package/dist/modules/contracts/standard.d.mts +2 -3
  82. package/dist/modules/contracts/standard.mjs.map +1 -1
  83. package/dist/modules/contracts/workflow.d.mts +26 -27
  84. package/dist/modules/contracts/workflow.mjs.map +1 -1
  85. package/dist/modules/effect/api-runtime.d.mts +20 -11
  86. package/dist/modules/effect/api-runtime.mjs +1 -1
  87. package/dist/modules/effect/api-runtime.mjs.map +1 -1
  88. package/dist/modules/effect/api-zod.d.mts +5 -6
  89. package/dist/modules/effect/api-zod.mjs.map +1 -1
  90. package/dist/modules/effect/api.d.mts +6 -7
  91. package/dist/modules/effect/api.mjs.map +1 -1
  92. package/dist/modules/effect/command.d.mts +10 -7
  93. package/dist/modules/effect/command.mjs +1 -1
  94. package/dist/modules/effect/command.mjs.map +1 -1
  95. package/dist/modules/effect/crud.d.mts +7 -8
  96. package/dist/modules/effect/crud.mjs.map +1 -1
  97. package/dist/modules/effect/errors.d.mts +1 -2
  98. package/dist/modules/effect/errors.mjs.map +1 -1
  99. package/dist/modules/effect/foundation.d.mts +3 -4
  100. package/dist/modules/effect/foundation.mjs.map +1 -1
  101. package/dist/modules/effect/idempotency.d.mts +2 -3
  102. package/dist/modules/effect/idempotency.mjs.map +1 -1
  103. package/dist/modules/effect/normalize.d.mts +2 -3
  104. package/dist/modules/effect/normalize.mjs.map +1 -1
  105. package/dist/modules/effect/operation.d.mts +17 -18
  106. package/dist/modules/effect/operation.mjs.map +1 -1
  107. package/dist/modules/effect/query.d.mts +8 -5
  108. package/dist/modules/effect/query.mjs +1 -1
  109. package/dist/modules/effect/query.mjs.map +1 -1
  110. package/dist/modules/effect/schema.d.mts +8 -9
  111. package/dist/modules/effect/schema.mjs.map +1 -1
  112. package/dist/modules/effect/workflow.d.mts +5 -6
  113. package/dist/modules/effect/workflow.mjs.map +1 -1
  114. package/dist/oxlint/architecture.d.mts +6 -7
  115. package/dist/oxlint/architecture.mjs +1 -1
  116. package/dist/oxlint/architecture.mjs.map +1 -1
  117. package/dist/oxlint/ast.d.mts +22 -23
  118. package/dist/oxlint/ast.mjs +1 -1
  119. package/dist/oxlint/ast.mjs.map +1 -1
  120. package/dist/oxlint/ownership.d.mts +2 -3
  121. package/dist/oxlint/ownership.mjs +1 -1
  122. package/dist/oxlint/ownership.mjs.map +1 -1
  123. package/dist/oxlint.d.mts +4 -4
  124. package/dist/oxlint.mjs +1 -1
  125. package/dist/oxlint.mjs.map +1 -1
  126. package/dist/state-machine.d.mts +1 -2
  127. package/dist/state-machine.mjs.map +1 -1
  128. package/dist/tenancy.d.mts +6 -6
  129. package/dist/tenancy.mjs.map +1 -1
  130. package/dist/triggers.d.mts +6 -6
  131. package/dist/triggers.mjs.map +1 -1
  132. package/dist/webhooks.d.mts +5 -6
  133. package/dist/webhooks.mjs.map +1 -1
  134. package/dist/zod-table.d.mts +10 -10
  135. package/dist/zod-table.mjs.map +1 -1
  136. package/docs/architecture.md +3 -3
  137. package/docs/auth.md +10 -4
  138. package/docs/command-context.md +150 -0
  139. package/docs/commands-legacy.md +511 -0
  140. package/docs/commands.md +113 -451
  141. package/docs/conventions.md +15 -5
  142. package/docs/crud.md +4 -0
  143. package/docs/domain-operations.md +1 -1
  144. package/docs/effect.md +14 -3
  145. package/docs/llms.md +69 -45
  146. package/docs/migration.md +1 -1
  147. package/docs/oxlint.md +1 -1
  148. package/docs/upgrading.md +18 -1
  149. package/docs/zod-table.md +1 -1
  150. package/package.json +21 -15
  151. package/scripts/test-packed-auth.mjs +68 -0
  152. package/scripts/test-packed-effect.mjs +4 -1
  153. package/scripts/test-packed-ids.mjs +2 -2
  154. package/dist/node_modules/@standard-schema/spec/dist/index.d.mts +0 -79
@@ -1 +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 { GenericDataModel, TableNamesInDataModel } 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\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 rules: Rules<unknown, DataModel> = {}\n\tfor (const table of tables) {\n\t\trules[table] = {\n\t\t\tread: async (_context, document) => document[TENANT_FIELD] === tenant,\n\t\t\tinsert: async (_context, document) => document[TENANT_FIELD] === tenant,\n\t\t\tmodify: async (_context, document) => document[TENANT_FIELD] === tenant,\n\t\t}\n\t}\n\treturn rules\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\t// SAFETY: Rules keys are table names from DataModel; values come only from the supplied rule sets.\n\tconst tables = new Set(sets.flatMap((set) => Object.keys(set))) as Set<\n\t\tTableNamesInDataModel<DataModel>\n\t>\n\tconst composed: Rules<Ctx, DataModel> = {}\n\tfor (const table of tables) {\n\t\tconst reads = sets.flatMap((set) => set[table]?.read ?? [])\n\t\tconst inserts = sets.flatMap((set) => set[table]?.insert ?? [])\n\t\tconst modifies = sets.flatMap((set) => set[table]?.modify ?? [])\n\t\tconst rules: NonNullable<Rules<Ctx, DataModel>[typeof table]> = {}\n\t\tif (reads.length)\n\t\t\trules.read = async (context, document) => {\n\t\t\t\tfor (const rule of reads) if (!(await rule(context, document))) return false\n\t\t\t\treturn true\n\t\t\t}\n\t\tif (inserts.length)\n\t\t\trules.insert = async (context, document) => {\n\t\t\t\tfor (const rule of inserts) if (!(await rule(context, document))) return false\n\t\t\t\treturn true\n\t\t\t}\n\t\tif (modifies.length)\n\t\t\trules.modify = async (context, document) => {\n\t\t\t\tfor (const rule of modifies) if (!(await rule(context, document))) return false\n\t\t\t\treturn true\n\t\t\t}\n\t\tcomposed[table] = rules\n\t}\n\treturn composed\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<Document extends { [TENANT_FIELD]?: unknown }>(\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<Document extends { [TENANT_FIELD]?: unknown }>(\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":"iJAaA,MAAa,EAAe,SAQ5B,SAAgB,EACf,EACA,EAC4B,CAC5B,IAAM,EAAmC,CAAC,EAC1C,IAAK,IAAM,KAAS,EACnB,EAAM,GAAS,CACd,KAAM,MAAO,EAAU,IAAa,EAAS,KAAkB,EAC/D,OAAQ,MAAO,EAAU,IAAa,EAAS,KAAkB,EACjE,OAAQ,MAAO,EAAU,IAAa,EAAS,KAAkB,CAClE,EAED,OAAO,CACR,CAOA,SAAgB,EACf,GAAG,EACqB,CAExB,IAAM,EAAS,IAAI,IAAI,EAAK,QAAS,GAAQ,OAAO,KAAK,CAAG,CAAC,CAAC,EAGxD,EAAkC,CAAC,EACzC,IAAK,IAAM,KAAS,EAAQ,CAC3B,IAAM,EAAQ,EAAK,QAAS,GAAQ,EAAI,EAAM,EAAE,MAAQ,CAAC,CAAC,EACpD,EAAU,EAAK,QAAS,GAAQ,EAAI,EAAM,EAAE,QAAU,CAAC,CAAC,EACxD,EAAW,EAAK,QAAS,GAAQ,EAAI,EAAM,EAAE,QAAU,CAAC,CAAC,EACzD,EAA0D,CAAC,EAC7D,EAAM,SACT,EAAM,KAAO,MAAO,EAAS,IAAa,CACzC,IAAK,IAAM,KAAQ,EAAO,GAAI,CAAE,MAAM,EAAK,EAAS,CAAQ,EAAI,MAAO,GACvE,MAAO,EACR,GACG,EAAQ,SACX,EAAM,OAAS,MAAO,EAAS,IAAa,CAC3C,IAAK,IAAM,KAAQ,EAAS,GAAI,CAAE,MAAM,EAAK,EAAS,CAAQ,EAAI,MAAO,GACzE,MAAO,EACR,GACG,EAAS,SACZ,EAAM,OAAS,MAAO,EAAS,IAAa,CAC3C,IAAK,IAAM,KAAQ,EAAU,GAAI,CAAE,MAAM,EAAK,EAAS,CAAQ,EAAI,MAAO,GAC1E,MAAO,EACR,GACD,EAAS,GAAS,CACnB,CACA,OAAO,CACR,CAOA,eAAsB,EACrB,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,EACf,EACA,EACA,EAAuB,EACZ,CAOX,OANI,EAAA,SAA2B,EAMxB,EALC,EAAO,MAAM,CACnB,KAAM,yBACN,QAAS,wCACV,CAAC,CAGH"}
1
+ {"version":3,"file":"tenancy.mjs","names":["TENANT_FIELD","createTenantRules","rules","table","tables","document","tenant","composeRules","sets","set","composed","reads","inserts","modifies","rule","context","requireTenantReference","defaultErrors","load","\"tenant\"","errors","assertTenantOwned","wrapDatabaseReader","wrapDatabaseWriter"],"sources":["../src/tenancy.ts"],"sourcesContent":["import {\n\twrapDatabaseReader,\n\twrapDatabaseWriter,\n\ttype RLSConfig,\n\ttype Rules,\n} from 'convex-helpers/server/rowLevelSecurity'\nimport type { GenericDataModel, TableNamesInDataModel } 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\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 rules: Rules<unknown, DataModel> = {}\n\tfor (const table of tables) {\n\t\trules[table] = {\n\t\t\tread: async (_context, document) => document[TENANT_FIELD] === tenant,\n\t\t\tinsert: async (_context, document) => document[TENANT_FIELD] === tenant,\n\t\t\tmodify: async (_context, document) => document[TENANT_FIELD] === tenant,\n\t\t}\n\t}\n\treturn rules\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\t// SAFETY: Rules keys are table names from DataModel; values come only from the supplied rule sets.\n\tconst tables = new Set(sets.flatMap((set) => Object.keys(set))) as Set<\n\t\tTableNamesInDataModel<DataModel>\n\t>\n\tconst composed: Rules<Ctx, DataModel> = {}\n\tfor (const table of tables) {\n\t\tconst reads = sets.flatMap((set) => set[table]?.read ?? [])\n\t\tconst inserts = sets.flatMap((set) => set[table]?.insert ?? [])\n\t\tconst modifies = sets.flatMap((set) => set[table]?.modify ?? [])\n\t\tconst rules: NonNullable<Rules<Ctx, DataModel>[typeof table]> = {}\n\t\tif (reads.length)\n\t\t\trules.read = async (context, document) => {\n\t\t\t\tfor (const rule of reads) if (!(await rule(context, document))) return false\n\t\t\t\treturn true\n\t\t\t}\n\t\tif (inserts.length)\n\t\t\trules.insert = async (context, document) => {\n\t\t\t\tfor (const rule of inserts) if (!(await rule(context, document))) return false\n\t\t\t\treturn true\n\t\t\t}\n\t\tif (modifies.length)\n\t\t\trules.modify = async (context, document) => {\n\t\t\t\tfor (const rule of modifies) if (!(await rule(context, document))) return false\n\t\t\t\treturn true\n\t\t\t}\n\t\tcomposed[table] = rules\n\t}\n\treturn composed\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<Document extends { [TENANT_FIELD]?: unknown }>(\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<Document extends { [TENANT_FIELD]?: unknown }>(\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":"iJAaA,MAAaA,EAAe,SAQ5B,SAAgBC,EACf,EACA,EAC4B,CAC5B,IAAMC,EAAmC,CAAC,EAC1C,IAAK,IAAMC,KAASC,EACnB,EAAMD,GAAS,CACd,KAAM,MAAO,EAAU,IAAaE,EAASL,KAAkBM,EAC/D,OAAQ,MAAO,EAAU,IAAaD,EAASL,KAAkBM,EACjE,OAAQ,MAAO,EAAU,IAAaD,EAASL,KAAkBM,CAClE,EAED,OAAOJ,CACR,CAOA,SAAgBK,EACf,GAAGC,EACqB,CAExB,IAAMJ,EAAS,IAAI,IAAII,EAAK,QAAS,GAAQ,OAAO,KAAKC,CAAG,CAAC,CAAC,EAGxDC,EAAkC,CAAC,EACzC,IAAK,IAAMP,KAASC,EAAQ,CAC3B,IAAMO,EAAQH,EAAK,QAAS,GAAQC,EAAIN,EAAM,EAAE,MAAQ,CAAC,CAAC,EACpDS,EAAUJ,EAAK,QAAS,GAAQC,EAAIN,EAAM,EAAE,QAAU,CAAC,CAAC,EACxDU,EAAWL,EAAK,QAAS,GAAQC,EAAIN,EAAM,EAAE,QAAU,CAAC,CAAC,EACzDD,EAA0D,CAAC,EAC7DS,EAAM,SACT,EAAM,KAAO,MAAO,EAAS,IAAa,CACzC,IAAK,IAAMG,KAAQH,EAAO,GAAI,CAAE,MAAMG,EAAKC,EAASV,CAAQ,EAAI,MAAO,GACvE,MAAO,EACR,GACGO,EAAQ,SACX,EAAM,OAAS,MAAO,EAAS,IAAa,CAC3C,IAAK,IAAME,KAAQF,EAAS,GAAI,CAAE,MAAME,EAAKC,EAASV,CAAQ,EAAI,MAAO,GACzE,MAAO,EACR,GACGQ,EAAS,SACZ,EAAM,OAAS,MAAO,EAAS,IAAa,CAC3C,IAAK,IAAMC,KAAQD,EAAU,GAAI,CAAE,MAAMC,EAAKC,EAASV,CAAQ,EAAI,MAAO,GAC1E,MAAO,EACR,GACD,EAASF,GAASD,CACnB,CACA,OAAOQ,CACR,CAOA,eAAsBM,EACrB,EACA,EACA,EAAuBC,EACH,CACpB,IAAMZ,EAAW,MAAMa,EAAK,EAO5B,MANI,CAACb,GAAYA,EAAAc,SAA2Bb,EACpCc,EAAO,MAAM,CACnB,KAAM,sBACN,QAAS,mCACV,CAAC,EAEKf,CACR,CAGA,SAAgBgB,EACf,EACA,EACA,EAAuBJ,EACZ,CAOX,OANIZ,EAAAc,SAA2Bb,EAMxBD,EALCe,EAAO,MAAM,CACnB,KAAM,yBACN,QAAS,wCACV,CAAC,CAGH"}
@@ -7,14 +7,14 @@ import { Change, Trigger, Triggers } from "convex-helpers/server/triggers";
7
7
  * systemMutation runs writes through it — trigger enforcement becomes
8
8
  * structural, not a convention.
9
9
  */
10
- declare function createTriggers<DataModel extends GenericDataModel>(): Triggers<DataModel>;
10
+ export declare function createTriggers<DataModel extends GenericDataModel>(): Triggers<DataModel>;
11
11
  /**
12
12
  * Marks tables as append-only evidence tables: inserts pass, every update or
13
13
  * delete throws. The ontology pattern for votes/history/audit-style tables.
14
14
  */
15
- declare function appendOnly<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
15
+ export 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
- declare function noDelete<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
17
+ export declare function noDelete<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
18
18
  /**
19
19
  * Post-image tenant invariant for tenantTable rows. Row-level security
20
20
  * authorizes a modification against the PRE-image, so ownership must also be
@@ -22,14 +22,14 @@ declare function noDelete<DataModel extends GenericDataModel>(triggers: Triggers
22
22
  * may never reassign it. Register for every tenant table (same registry that
23
23
  * drives createTenantRules).
24
24
  */
25
- declare function tenantOwnership<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
25
+ export declare function tenantOwnership<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
26
26
  /**
27
27
  * Maintains zod-table's opinionated lifecycle timestamps: createdAt and
28
28
  * updatedAt on insert, updatedAt on every update. archivedAt stays under
29
29
  * application control. Register once per table; writes made through the
30
30
  * auth constructors keep these fields correct automatically.
31
31
  */
32
- declare function timestamps<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
32
+ export declare function timestamps<DataModel extends GenericDataModel>(triggers: Triggers<DataModel>, ...tables: readonly TableNamesInDataModel<DataModel>[]): void;
33
33
  //#endregion
34
- export { type Change, type Trigger, Triggers, appendOnly, createTriggers, noDelete, tenantOwnership, timestamps };
34
+ export { type Change, type Trigger, Triggers };
35
35
  //# sourceMappingURL=triggers.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"triggers.mjs","names":[],"sources":["../src/triggers.ts"],"sourcesContent":["import { z } from 'zod'\nimport { Triggers, type Change, type Trigger } from 'convex-helpers/server/triggers'\nimport type { GenericDataModel, TableNamesInDataModel } from 'convex/server'\n\nexport { Triggers }\nexport type { Change, Trigger }\n\n/**\n * One application-wide mutation interceptor registry. Pass it to\n * createAuthFunctions({ triggers }) so every authMutation/roleMutation/\n * systemMutation runs writes through it — trigger enforcement becomes\n * structural, not a convention.\n */\nexport function createTriggers<DataModel extends GenericDataModel>(): Triggers<DataModel> {\n\treturn new Triggers<DataModel>()\n}\n\n/**\n * Marks tables as append-only evidence tables: inserts pass, every update or\n * delete throws. The ontology pattern for votes/history/audit-style tables.\n */\nexport function appendOnly<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation !== 'insert') {\n\t\t\t\tthrow new Error(`${table} is append-only`)\n\t\t\t}\n\t\t})\n\t}\n}\n\n/** Forbids hard deletes on the given tables (soft-delete discipline). */\nexport function noDelete<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation === 'delete') {\n\t\t\t\tthrow new Error(`${table} rows cannot be deleted`)\n\t\t\t}\n\t\t})\n\t}\n}\n\n/**\n * Post-image tenant invariant for tenantTable rows. Row-level security\n * authorizes a modification against the PRE-image, so ownership must also be\n * enforced against the post-image: inserts must carry a tenant, and updates\n * may never reassign it. Register for every tenant table (same registry that\n * drives createTenantRules).\n */\nexport function tenantOwnership<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation === 'insert') {\n\t\t\t\tconst ownership = tenantDocument.safeParse(change.newDoc)\n\t\t\t\tif (!ownership.success) {\n\t\t\t\t\tthrow new Error(`${table} rows require a tenant`)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (change.operation === 'update') {\n\t\t\t\tconst previous = change.oldDoc\n\t\t\t\tconst current = change.newDoc\n\t\t\t\tif (previous.tenant !== current.tenant) {\n\t\t\t\t\tthrow new Error(`${table} tenant ownership cannot be reassigned`)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n/**\n * Maintains zod-table's opinionated lifecycle timestamps: createdAt and\n * updatedAt on insert, updatedAt on every update. archivedAt stays under\n * application control. Register once per table; writes made through the\n * auth constructors keep these fields correct automatically.\n */\nexport function timestamps<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (ctx, change) => {\n\t\t\tif (change.operation === 'delete') return\n\t\t\tconst now = Date.now()\n\t\t\tif (change.operation === 'insert') {\n\t\t\t\tconst document = change.newDoc\n\t\t\t\tif (document.createdAt === undefined) {\n\t\t\t\t\t// SAFETY: registration requires zod-table lifecycle fields; Convex validates this numeric timestamp patch.\n\t\t\t\t\tawait ctx.innerDb.patch(change.id, {\n\t\t\t\t\t\tcreatedAt: now,\n\t\t\t\t\t\tupdatedAt: now,\n\t\t\t\t\t} as never)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst previous = change.oldDoc\n\t\t\tconst current = change.newDoc\n\t\t\t// A write that already moved updatedAt (including our own patch)\n\t\t\t// is left alone — this is the recursion guard.\n\t\t\tif (current.updatedAt === previous.updatedAt) {\n\t\t\t\t// SAFETY: registration requires zod-table lifecycle fields; Convex validates this numeric timestamp patch.\n\t\t\t\tawait ctx.innerDb.patch(change.id, { updatedAt: now } as never)\n\t\t\t}\n\t\t})\n\t}\n}\n\nconst tenantDocument = z.object({ tenant: z.string().min(1) })\n"],"mappings":"kFAaA,SAAgB,GAA0E,CACzF,OAAO,IAAI,CACZ,CAMA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAM,IAAW,CAChD,GAAI,EAAO,YAAc,SACxB,MAAU,MAAM,GAAG,EAAM,gBAAgB,CAE3C,CAAC,CAEH,CAGA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAM,IAAW,CAChD,GAAI,EAAO,YAAc,SACxB,MAAU,MAAM,GAAG,EAAM,wBAAwB,CAEnD,CAAC,CAEH,CASA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAM,IAAW,CAChD,GAAI,EAAO,YAAc,SAAU,CAElC,GAAI,CADc,EAAe,UAAU,EAAO,MACrC,CAAC,CAAC,QACd,MAAU,MAAM,GAAG,EAAM,uBAAuB,EAEjD,MACD,CACA,GAAI,EAAO,YAAc,SAAU,CAClC,IAAM,EAAW,EAAO,OAClB,EAAU,EAAO,OACvB,GAAI,EAAS,SAAW,EAAQ,OAC/B,MAAU,MAAM,GAAG,EAAM,uCAAuC,CAElE,CACD,CAAC,CAEH,CAQA,SAAgB,EACf,EACA,GAAG,EACF,CACD,IAAK,IAAM,KAAS,EACnB,EAAS,SAAS,EAAO,MAAO,EAAK,IAAW,CAC/C,GAAI,EAAO,YAAc,SAAU,OACnC,IAAM,EAAM,KAAK,IAAI,EACrB,GAAI,EAAO,YAAc,SAAU,CACjB,EAAO,OACX,YAAc,IAAA,IAE1B,MAAM,EAAI,QAAQ,MAAM,EAAO,GAAI,CAClC,UAAW,EACX,UAAW,CACZ,CAAU,EAEX,MACD,CACA,IAAM,EAAW,EAAO,OACR,EAAO,OAGX,YAAc,EAAS,WAElC,MAAM,EAAI,QAAQ,MAAM,EAAO,GAAI,CAAE,UAAW,CAAI,CAAU,CAEhE,CAAC,CAEH,CAEA,MAAM,EAAiB,EAAE,OAAO,CAAE,OAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAE,CAAC"}
1
+ {"version":3,"file":"triggers.mjs","names":["createTriggers","Triggers","appendOnly","tables","table","triggers","change","noDelete","tenantOwnership","tenantDocument","previous","current","timestamps","now","ctx","z"],"sources":["../src/triggers.ts"],"sourcesContent":["import { z } from 'zod'\nimport { Triggers, type Change, type Trigger } from 'convex-helpers/server/triggers'\nimport type { GenericDataModel, TableNamesInDataModel } from 'convex/server'\n\nexport { Triggers }\nexport type { Change, Trigger }\n\n/**\n * One application-wide mutation interceptor registry. Pass it to\n * createAuthFunctions({ triggers }) so every authMutation/roleMutation/\n * systemMutation runs writes through it — trigger enforcement becomes\n * structural, not a convention.\n */\nexport function createTriggers<DataModel extends GenericDataModel>(): Triggers<DataModel> {\n\treturn new Triggers<DataModel>()\n}\n\n/**\n * Marks tables as append-only evidence tables: inserts pass, every update or\n * delete throws. The ontology pattern for votes/history/audit-style tables.\n */\nexport function appendOnly<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation !== 'insert') {\n\t\t\t\tthrow new Error(`${table} is append-only`)\n\t\t\t}\n\t\t})\n\t}\n}\n\n/** Forbids hard deletes on the given tables (soft-delete discipline). */\nexport function noDelete<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation === 'delete') {\n\t\t\t\tthrow new Error(`${table} rows cannot be deleted`)\n\t\t\t}\n\t\t})\n\t}\n}\n\n/**\n * Post-image tenant invariant for tenantTable rows. Row-level security\n * authorizes a modification against the PRE-image, so ownership must also be\n * enforced against the post-image: inserts must carry a tenant, and updates\n * may never reassign it. Register for every tenant table (same registry that\n * drives createTenantRules).\n */\nexport function tenantOwnership<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (_ctx, change) => {\n\t\t\tif (change.operation === 'insert') {\n\t\t\t\tconst ownership = tenantDocument.safeParse(change.newDoc)\n\t\t\t\tif (!ownership.success) {\n\t\t\t\t\tthrow new Error(`${table} rows require a tenant`)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (change.operation === 'update') {\n\t\t\t\tconst previous = change.oldDoc\n\t\t\t\tconst current = change.newDoc\n\t\t\t\tif (previous.tenant !== current.tenant) {\n\t\t\t\t\tthrow new Error(`${table} tenant ownership cannot be reassigned`)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n/**\n * Maintains zod-table's opinionated lifecycle timestamps: createdAt and\n * updatedAt on insert, updatedAt on every update. archivedAt stays under\n * application control. Register once per table; writes made through the\n * auth constructors keep these fields correct automatically.\n */\nexport function timestamps<DataModel extends GenericDataModel>(\n\ttriggers: Triggers<DataModel>,\n\t...tables: readonly TableNamesInDataModel<DataModel>[]\n) {\n\tfor (const table of tables) {\n\t\ttriggers.register(table, async (ctx, change) => {\n\t\t\tif (change.operation === 'delete') return\n\t\t\tconst now = Date.now()\n\t\t\tif (change.operation === 'insert') {\n\t\t\t\tconst document = change.newDoc\n\t\t\t\tif (document.createdAt === undefined) {\n\t\t\t\t\t// SAFETY: registration requires zod-table lifecycle fields; Convex validates this numeric timestamp patch.\n\t\t\t\t\tawait ctx.innerDb.patch(change.id, {\n\t\t\t\t\t\tcreatedAt: now,\n\t\t\t\t\t\tupdatedAt: now,\n\t\t\t\t\t} as never)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst previous = change.oldDoc\n\t\t\tconst current = change.newDoc\n\t\t\t// A write that already moved updatedAt (including our own patch)\n\t\t\t// is left alone — this is the recursion guard.\n\t\t\tif (current.updatedAt === previous.updatedAt) {\n\t\t\t\t// SAFETY: registration requires zod-table lifecycle fields; Convex validates this numeric timestamp patch.\n\t\t\t\tawait ctx.innerDb.patch(change.id, { updatedAt: now } as never)\n\t\t\t}\n\t\t})\n\t}\n}\n\nconst tenantDocument = z.object({ tenant: z.string().min(1) })\n"],"mappings":"kFAaA,SAAgBA,GAA0E,CACzF,OAAO,IAAIC,CACZ,CAMA,SAAgBC,EACf,EACA,GAAGC,EACF,CACD,IAAK,IAAMC,KAASD,EACnBE,EAAS,SAASD,EAAO,MAAO,EAAM,IAAW,CAChD,GAAIE,EAAO,YAAc,SACxB,MAAU,MAAM,GAAGF,EAAM,gBAAgB,CAE3C,CAAC,CAEH,CAGA,SAAgBG,EACf,EACA,GAAGJ,EACF,CACD,IAAK,IAAMC,KAASD,EACnBE,EAAS,SAASD,EAAO,MAAO,EAAM,IAAW,CAChD,GAAIE,EAAO,YAAc,SACxB,MAAU,MAAM,GAAGF,EAAM,wBAAwB,CAEnD,CAAC,CAEH,CASA,SAAgBI,EACf,EACA,GAAGL,EACF,CACD,IAAK,IAAMC,KAASD,EACnBE,EAAS,SAASD,EAAO,MAAO,EAAM,IAAW,CAChD,GAAIE,EAAO,YAAc,SAAU,CAElC,GAAI,CADcG,EAAe,UAAUH,EAAO,MACrC,CAAC,CAAC,QACd,MAAU,MAAM,GAAGF,EAAM,uBAAuB,EAEjD,MACD,CACA,GAAIE,EAAO,YAAc,SAAU,CAClC,IAAMI,EAAWJ,EAAO,OAClBK,EAAUL,EAAO,OACvB,GAAII,EAAS,SAAWC,EAAQ,OAC/B,MAAU,MAAM,GAAGP,EAAM,uCAAuC,CAElE,CACD,CAAC,CAEH,CAQA,SAAgBQ,EACf,EACA,GAAGT,EACF,CACD,IAAK,IAAMC,KAASD,EACnBE,EAAS,SAASD,EAAO,MAAO,EAAK,IAAW,CAC/C,GAAIE,EAAO,YAAc,SAAU,OACnC,IAAMO,EAAM,KAAK,IAAI,EACrB,GAAIP,EAAO,YAAc,SAAU,CACjBA,EAAO,OACX,YAAc,IAAA,IAE1B,MAAMQ,EAAI,QAAQ,MAAMR,EAAO,GAAI,CAClC,UAAWO,EACX,UAAWA,CACZ,CAAU,EAEX,MACD,CACA,IAAMH,EAAWJ,EAAO,OACRA,EAAO,OAGX,YAAcI,EAAS,WAElC,MAAMI,EAAI,QAAQ,MAAMR,EAAO,GAAI,CAAE,UAAWO,CAAI,CAAU,CAEhE,CAAC,CAEH,CAEA,MAAMJ,EAAiBM,EAAE,OAAO,CAAE,OAAQA,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAE,CAAC"}
@@ -13,7 +13,7 @@ import { z } from "zod";
13
13
  * longer than the provider's redelivery horizon (a cleanup cron over
14
14
  * `receivedAt` — see docs/webhooks.md).
15
15
  */
16
- declare function webhookEventsTable(): TableBoundary<"webhookEvents", {
16
+ export declare function webhookEventsTable(): TableBoundary<"webhookEvents", {
17
17
  eventKey: z.ZodString;
18
18
  source: z.ZodString;
19
19
  receivedAt: z.ZodNumber;
@@ -25,14 +25,14 @@ type DedupContext = Pick<GenericMutationCtx<GenericDataModel>, 'db'>;
25
25
  * mutation (transactional), never in the action. Returns duplicate: true
26
26
  * when the natural key was already recorded; the caller then no-ops.
27
27
  */
28
- declare function recordWebhookEvent(ctx: DedupContext, input: {
28
+ export declare function recordWebhookEvent(ctx: DedupContext, input: {
29
29
  key: string;
30
30
  source?: string;
31
31
  table?: string;
32
32
  }): Promise<{
33
33
  duplicate: boolean;
34
34
  }>;
35
- type WebhookBoundaryConfig = {
35
+ export type WebhookBoundaryConfig = {
36
36
  /**
37
37
  * Verifies the provider signature over the RAW body bytes — the helper
38
38
  * hands it `request.text()` BEFORE any parsing. Implementations must use
@@ -60,16 +60,15 @@ type WebhookBoundaryConfig = {
60
60
  * handler: httpAction((ctx, request) =>
61
61
  * boundary.handle(ctx, request, internal.domain.vendor.functions.applyEvent)) })
62
62
  */
63
- declare function createWebhookBoundary(config: WebhookBoundaryConfig): {
63
+ export declare function createWebhookBoundary(config: WebhookBoundaryConfig): {
64
64
  handle: <Result>(ctx: {
65
65
  runMutation: (target: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>, args: WebhookDelivery) => Promise<Result>;
66
66
  }, request: Request, target: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>) => Promise<Response>;
67
67
  };
68
- type WebhookDelivery = {
68
+ export type WebhookDelivery = {
69
69
  eventKey: string;
70
70
  payload: string;
71
71
  source: string;
72
72
  };
73
73
  //#endregion
74
- export { WebhookBoundaryConfig, WebhookDelivery, createWebhookBoundary, recordWebhookEvent, webhookEventsTable };
75
74
  //# sourceMappingURL=webhooks.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.mjs","names":[],"sources":["../src/webhooks.ts"],"sourcesContent":["import type { GenericMutationCtx, GenericDataModel, FunctionReference } from 'convex/server'\nimport { z } from 'zod'\nimport { defaultErrors, type ErrorFactory } from './errors'\nimport { zodTable } from './zod-table'\n\n/**\n * The host-owned dedup table (the kit gains no tables). Register it in the\n * schema with the by_eventKey index:\n *\n * webhookEvents: webhookEventsTable().table.index('by_eventKey', ['eventKey'])\n *\n * Rows are host-owned data: prune them on a retention window comfortably\n * longer than the provider's redelivery horizon (a cleanup cron over\n * `receivedAt` — see docs/webhooks.md).\n */\nexport function webhookEventsTable() {\n\treturn zodTable('webhookEvents', () => ({\n\t\teventKey: z.string(),\n\t\tsource: z.string(),\n\t\treceivedAt: z.number(),\n\t}))\n}\n\ntype DedupContext = Pick<GenericMutationCtx<GenericDataModel>, 'db'>\n\n/**\n * Transactional insert-if-absent dedup guard. Call this FIRST inside the\n * internal mutation the boundary delegates to — dedup must live in the\n * mutation (transactional), never in the action. Returns duplicate: true\n * when the natural key was already recorded; the caller then no-ops.\n */\nexport async function recordWebhookEvent(\n\tctx: DedupContext,\n\tinput: { key: string; source?: string; table?: string },\n): Promise<{ duplicate: boolean }> {\n\tconst table = input.table ?? 'webhookEvents'\n\tconst existing = await ctx.db\n\t\t.query(table)\n\t\t.withIndex('by_eventKey', (builder) => builder.eq('eventKey', input.key))\n\t\t.first()\n\tif (existing) return { duplicate: true }\n\tawait ctx.db.insert(table, {\n\t\teventKey: input.key,\n\t\tsource: input.source ?? 'unknown',\n\t\treceivedAt: Date.now(),\n\t})\n\treturn { duplicate: false }\n}\n\nexport type WebhookBoundaryConfig = {\n\t/**\n\t * Verifies the provider signature over the RAW body bytes — the helper\n\t * hands it `request.text()` BEFORE any parsing. Implementations must use\n\t * a constant-time comparison (e.g. crypto.subtle HMAC verify) and read\n\t * the secret from a Convex environment variable. Return false or throw\n\t * to reject.\n\t */\n\tverify: (raw: string, request: Request) => boolean | Promise<boolean>\n\t/**\n\t * Derives the natural dedup key — the `${event}:${id}:${updatedAt}`\n\t * pattern. Receives the raw body and the request; parse inside if needed.\n\t */\n\teventKey: (raw: string, request: Request) => string | Promise<string>\n\tsource?: string\n\terrors?: ErrorFactory\n}\n\n/**\n * The sanctioned webhook pattern as code: verify (raw body, fail closed) →\n * derive natural key → delegate to a host internal mutation whose FIRST act\n * is recordWebhookEvent (transactional dedup). Use inside the host's\n * httpAction:\n *\n * const boundary = createWebhookBoundary({ verify, eventKey })\n * http.route({ path: '/webhooks/vendor', method: 'POST',\n * handler: httpAction((ctx, request) =>\n * boundary.handle(ctx, request, internal.domain.vendor.functions.applyEvent)) })\n */\nexport function createWebhookBoundary(config: WebhookBoundaryConfig) {\n\tconst errors = config.errors ?? defaultErrors\n\n\tasync function handle<Result>(\n\t\tctx: {\n\t\t\trunMutation: (\n\t\t\t\ttarget: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>,\n\t\t\t\targs: WebhookDelivery,\n\t\t\t) => Promise<Result>\n\t\t},\n\t\trequest: Request,\n\t\ttarget: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>,\n\t): Promise<Response> {\n\t\tconst raw = await request.text()\n\t\tlet verified = false\n\t\ttry {\n\t\t\tverified = await config.verify(raw, request)\n\t\t} catch {\n\t\t\tverified = false\n\t\t}\n\t\tif (!verified) {\n\t\t\terrors.throw({\n\t\t\t\tcode: 'WEBHOOK_SIGNATURE_INVALID',\n\t\t\t\tmessage: 'Webhook signature verification failed',\n\t\t\t})\n\t\t}\n\t\tconst key = await config.eventKey(raw, request)\n\t\tawait ctx.runMutation(target, {\n\t\t\teventKey: key,\n\t\t\tpayload: raw,\n\t\t\tsource: config.source ?? 'unknown',\n\t\t})\n\t\treturn new Response(null, { status: 200 })\n\t}\n\n\treturn { handle }\n}\n\nexport type WebhookDelivery = { eventKey: string; payload: string; source: string }\n"],"mappings":"gHAeA,SAAgB,GAAqB,CACpC,OAAO,EAAS,qBAAwB,CACvC,SAAU,EAAE,OAAO,EACnB,OAAQ,EAAE,OAAO,EACjB,WAAY,EAAE,OAAO,CACtB,EAAE,CACH,CAUA,eAAsB,EACrB,EACA,EACkC,CAClC,IAAM,EAAQ,EAAM,OAAS,gBAW7B,OANI,MAJmB,EAAI,GACzB,MAAM,CAAK,CAAC,CACZ,UAAU,cAAgB,GAAY,EAAQ,GAAG,WAAY,EAAM,GAAG,CAAC,CAAC,CACxE,MAAM,EACa,CAAE,UAAW,EAAK,GACvC,MAAM,EAAI,GAAG,OAAO,EAAO,CAC1B,SAAU,EAAM,IAChB,OAAQ,EAAM,QAAU,UACxB,WAAY,KAAK,IAAI,CACtB,CAAC,EACM,CAAE,UAAW,EAAM,EAC3B,CA+BA,SAAgB,EAAsB,EAA+B,CACpE,IAAM,EAAS,EAAO,QAAU,EAEhC,eAAe,EACd,EAMA,EACA,EACoB,CACpB,IAAM,EAAM,MAAM,EAAQ,KAAK,EAC3B,EAAW,GACf,GAAI,CACH,EAAW,MAAM,EAAO,OAAO,EAAK,CAAO,CAC5C,MAAQ,CACP,EAAW,EACZ,CACK,GACJ,EAAO,MAAM,CACZ,KAAM,4BACN,QAAS,uCACV,CAAC,EAEF,IAAM,EAAM,MAAM,EAAO,SAAS,EAAK,CAAO,EAM9C,OALA,MAAM,EAAI,YAAY,EAAQ,CAC7B,SAAU,EACV,QAAS,EACT,OAAQ,EAAO,QAAU,SAC1B,CAAC,EACM,IAAI,SAAS,KAAM,CAAE,OAAQ,GAAI,CAAC,CAC1C,CAEA,MAAO,CAAE,QAAO,CACjB"}
1
+ {"version":3,"file":"webhooks.mjs","names":["webhookEventsTable","zodTable","z","recordWebhookEvent","table","input","ctx","builder","createWebhookBoundary","errors","config","defaultErrors","handle","raw","request","verified","key","target"],"sources":["../src/webhooks.ts"],"sourcesContent":["import type { GenericMutationCtx, GenericDataModel, FunctionReference } from 'convex/server'\nimport { z } from 'zod'\nimport { defaultErrors, type ErrorFactory } from './errors'\nimport { zodTable } from './zod-table'\n\n/**\n * The host-owned dedup table (the kit gains no tables). Register it in the\n * schema with the by_eventKey index:\n *\n * webhookEvents: webhookEventsTable().table.index('by_eventKey', ['eventKey'])\n *\n * Rows are host-owned data: prune them on a retention window comfortably\n * longer than the provider's redelivery horizon (a cleanup cron over\n * `receivedAt` — see docs/webhooks.md).\n */\nexport function webhookEventsTable() {\n\treturn zodTable('webhookEvents', () => ({\n\t\teventKey: z.string(),\n\t\tsource: z.string(),\n\t\treceivedAt: z.number(),\n\t}))\n}\n\ntype DedupContext = Pick<GenericMutationCtx<GenericDataModel>, 'db'>\n\n/**\n * Transactional insert-if-absent dedup guard. Call this FIRST inside the\n * internal mutation the boundary delegates to — dedup must live in the\n * mutation (transactional), never in the action. Returns duplicate: true\n * when the natural key was already recorded; the caller then no-ops.\n */\nexport async function recordWebhookEvent(\n\tctx: DedupContext,\n\tinput: { key: string; source?: string; table?: string },\n): Promise<{ duplicate: boolean }> {\n\tconst table = input.table ?? 'webhookEvents'\n\tconst existing = await ctx.db\n\t\t.query(table)\n\t\t.withIndex('by_eventKey', (builder) => builder.eq('eventKey', input.key))\n\t\t.first()\n\tif (existing) return { duplicate: true }\n\tawait ctx.db.insert(table, {\n\t\teventKey: input.key,\n\t\tsource: input.source ?? 'unknown',\n\t\treceivedAt: Date.now(),\n\t})\n\treturn { duplicate: false }\n}\n\nexport type WebhookBoundaryConfig = {\n\t/**\n\t * Verifies the provider signature over the RAW body bytes — the helper\n\t * hands it `request.text()` BEFORE any parsing. Implementations must use\n\t * a constant-time comparison (e.g. crypto.subtle HMAC verify) and read\n\t * the secret from a Convex environment variable. Return false or throw\n\t * to reject.\n\t */\n\tverify: (raw: string, request: Request) => boolean | Promise<boolean>\n\t/**\n\t * Derives the natural dedup key — the `${event}:${id}:${updatedAt}`\n\t * pattern. Receives the raw body and the request; parse inside if needed.\n\t */\n\teventKey: (raw: string, request: Request) => string | Promise<string>\n\tsource?: string\n\terrors?: ErrorFactory\n}\n\n/**\n * The sanctioned webhook pattern as code: verify (raw body, fail closed) →\n * derive natural key → delegate to a host internal mutation whose FIRST act\n * is recordWebhookEvent (transactional dedup). Use inside the host's\n * httpAction:\n *\n * const boundary = createWebhookBoundary({ verify, eventKey })\n * http.route({ path: '/webhooks/vendor', method: 'POST',\n * handler: httpAction((ctx, request) =>\n * boundary.handle(ctx, request, internal.domain.vendor.functions.applyEvent)) })\n */\nexport function createWebhookBoundary(config: WebhookBoundaryConfig) {\n\tconst errors = config.errors ?? defaultErrors\n\n\tasync function handle<Result>(\n\t\tctx: {\n\t\t\trunMutation: (\n\t\t\t\ttarget: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>,\n\t\t\t\targs: WebhookDelivery,\n\t\t\t) => Promise<Result>\n\t\t},\n\t\trequest: Request,\n\t\ttarget: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>,\n\t): Promise<Response> {\n\t\tconst raw = await request.text()\n\t\tlet verified = false\n\t\ttry {\n\t\t\tverified = await config.verify(raw, request)\n\t\t} catch {\n\t\t\tverified = false\n\t\t}\n\t\tif (!verified) {\n\t\t\terrors.throw({\n\t\t\t\tcode: 'WEBHOOK_SIGNATURE_INVALID',\n\t\t\t\tmessage: 'Webhook signature verification failed',\n\t\t\t})\n\t\t}\n\t\tconst key = await config.eventKey(raw, request)\n\t\tawait ctx.runMutation(target, {\n\t\t\teventKey: key,\n\t\t\tpayload: raw,\n\t\t\tsource: config.source ?? 'unknown',\n\t\t})\n\t\treturn new Response(null, { status: 200 })\n\t}\n\n\treturn { handle }\n}\n\nexport type WebhookDelivery = { eventKey: string; payload: string; source: string }\n"],"mappings":"gHAeA,SAAgBA,GAAqB,CACpC,OAAOC,EAAS,qBAAwB,CACvC,SAAUC,EAAE,OAAO,EACnB,OAAQA,EAAE,OAAO,EACjB,WAAYA,EAAE,OAAO,CACtB,EAAE,CACH,CAUA,eAAsBC,EACrB,EACA,EACkC,CAClC,IAAMC,EAAQC,EAAM,OAAS,gBAW7B,OANI,MAJmBC,EAAI,GACzB,MAAMF,CAAK,CAAC,CACZ,UAAU,cAAgB,GAAYG,EAAQ,GAAG,WAAYF,EAAM,GAAG,CAAC,CAAC,CACxE,MAAM,EACa,CAAE,UAAW,EAAK,GACvC,MAAMC,EAAI,GAAG,OAAOF,EAAO,CAC1B,SAAUC,EAAM,IAChB,OAAQA,EAAM,QAAU,UACxB,WAAY,KAAK,IAAI,CACtB,CAAC,EACM,CAAE,UAAW,EAAM,EAC3B,CA+BA,SAAgBG,EAAsB,EAA+B,CACpE,IAAMC,EAASC,EAAO,QAAUC,EAEhC,eAAeC,EACd,EAMA,EACA,EACoB,CACpB,IAAMC,EAAM,MAAMC,EAAQ,KAAK,EAC3BC,EAAW,GACf,GAAI,CACH,EAAW,MAAML,EAAO,OAAOG,EAAKC,CAAO,CAC5C,MAAQ,CACP,EAAW,EACZ,CACKC,GACJN,EAAO,MAAM,CACZ,KAAM,4BACN,QAAS,uCACV,CAAC,EAEF,IAAMO,EAAM,MAAMN,EAAO,SAASG,EAAKC,CAAO,EAM9C,OALA,MAAMR,EAAI,YAAYW,EAAQ,CAC7B,SAAUD,EACV,QAASH,EACT,OAAQH,EAAO,QAAU,SAC1B,CAAC,EACM,IAAI,SAAS,KAAM,CAAE,OAAQ,GAAI,CAAC,CAC1C,CAEA,MAAO,CAAE,QAAO,CACjB"}
@@ -16,8 +16,8 @@ declare const timestampFields: {
16
16
  archivedAt: z.ZodOptional<z.ZodNumber>;
17
17
  };
18
18
  type TimestampFields = typeof timestampFields;
19
- declare const TIMESTAMP_FIELDS: readonly ['createdAt', 'updatedAt', 'archivedAt'];
20
- type TableBoundaryOptions<Fields extends FieldSchemas, ServerFields extends readonly FieldKey<Fields>[], CommandFields extends readonly FieldKey<Fields>[], PublicFields extends readonly FieldKey<Fields & TimestampFields>[]> = {
19
+ export declare const TIMESTAMP_FIELDS: readonly ['createdAt', 'updatedAt', 'archivedAt'];
20
+ export type TableBoundaryOptions<Fields extends FieldSchemas, ServerFields extends readonly FieldKey<Fields>[], CommandFields extends readonly FieldKey<Fields>[], PublicFields extends readonly FieldKey<Fields & TimestampFields>[]> = {
21
21
  serverFields?: ServerFields;
22
22
  commandFields?: CommandFields;
23
23
  publicFields?: PublicFields;
@@ -26,7 +26,7 @@ type StrictObject<Fields extends FieldSchemas> = z.ZodObject<Fields, z.core.$str
26
26
  type WriteFields<Fields extends FieldSchemas, ServerFields extends readonly FieldKey<Fields>[]> = Omit<Fields & TimestampFields, ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number]>;
27
27
  type PartialFields<Fields extends FieldSchemas> = { [Key in keyof Fields]: z.ZodOptional<Fields[Key]>; };
28
28
  /** Named schema relationships keep generated declarations bounded and consumers fully typed. */
29
- type TableBoundary<Table extends string, Fields extends FieldSchemas, ServerFields extends readonly FieldKey<Fields>[], CommandFields extends readonly FieldKey<Fields>[], PublicFields extends readonly FieldKey<Fields & TimestampFields>[]> = {
29
+ export type TableBoundary<Table extends string, Fields extends FieldSchemas, ServerFields extends readonly FieldKey<Fields>[], CommandFields extends readonly FieldKey<Fields>[], PublicFields extends readonly FieldKey<Fields & TimestampFields>[]> = {
30
30
  tableName: Table;
31
31
  storage: StrictObject<Fields & TimestampFields>;
32
32
  schema: ReturnType<typeof documentSchema<Fields & TimestampFields, Table>>;
@@ -52,7 +52,7 @@ type TableBoundary<Table extends string, Fields extends FieldSchemas, ServerFiel
52
52
  };
53
53
  };
54
54
  /** Defines one table and derives its storage, document, write, and DTO boundaries. */
55
- declare function zodTable<Table extends string, Fields extends FieldSchemas, const ServerFields extends readonly FieldKey<Fields>[] = readonly [], const CommandFields extends readonly FieldKey<Fields>[] = readonly [], const PublicFields extends readonly FieldKey<Fields & TimestampFields>[] = readonly []>(tableName: Table, fields: (id: typeof zid$1) => Fields, options?: TableBoundaryOptions<Fields, ServerFields, CommandFields, PublicFields>): TableBoundary<Table, Fields, ServerFields, CommandFields, PublicFields>;
55
+ export declare function zodTable<Table extends string, Fields extends FieldSchemas, const ServerFields extends readonly FieldKey<Fields>[] = readonly [], const CommandFields extends readonly FieldKey<Fields>[] = readonly [], const PublicFields extends readonly FieldKey<Fields & TimestampFields>[] = readonly []>(tableName: Table, fields: (id: typeof zid$1) => Fields, options?: TableBoundaryOptions<Fields, ServerFields, CommandFields, PublicFields>): TableBoundary<Table, Fields, ServerFields, CommandFields, PublicFields>;
56
56
  declare const tenantFields: {
57
57
  tenant: z.ZodString;
58
58
  };
@@ -65,7 +65,7 @@ type TenantFields = typeof tenantFields;
65
65
  * trigger forbids reassigning it. Expose it via publicFields only when a DTO
66
66
  * genuinely needs it.
67
67
  */
68
- declare function tenantTable<Table extends string, Fields extends FieldSchemas, const ServerFields extends readonly FieldKey<Fields>[] = readonly [], const CommandFields extends readonly FieldKey<Fields>[] = readonly [], const PublicFields extends readonly FieldKey<Fields & TenantFields & TimestampFields>[] = readonly []>(tableName: Table, fields: (id: typeof zid$1) => Fields, options?: TableBoundaryOptions<Fields & TenantFields, readonly (ServerFields[number] | 'tenant')[], CommandFields, PublicFields>): TableBoundary<Table, Fields & {
68
+ export declare function tenantTable<Table extends string, Fields extends FieldSchemas, const ServerFields extends readonly FieldKey<Fields>[] = readonly [], const CommandFields extends readonly FieldKey<Fields>[] = readonly [], const PublicFields extends readonly FieldKey<Fields & TenantFields & TimestampFields>[] = readonly []>(tableName: Table, fields: (id: typeof zid$1) => Fields, options?: TableBoundaryOptions<Fields & TenantFields, readonly (ServerFields[number] | 'tenant')[], CommandFields, PublicFields>): TableBoundary<Table, Fields & {
69
69
  tenant: z.ZodString;
70
70
  }, readonly ("tenant" | ServerFields[number])[], CommandFields, PublicFields>;
71
71
  /**
@@ -73,7 +73,7 @@ declare function tenantTable<Table extends string, Fields extends FieldSchemas,
73
73
  * duplicate table names across modules — the module-registry combinator for
74
74
  * domain/table.ts: `defineSchema(createModule(catalogTables, salesTables))`.
75
75
  */
76
- declare function createModule<const Maps extends readonly Record<string, TableDefinition<any>>[]>(...maps: Maps): UnionToIntersection<Maps[number]>;
76
+ export declare function createModule<const Maps extends readonly Record<string, TableDefinition<any>>[]>(...maps: Maps): UnionToIntersection<Maps[number]>;
77
77
  type UnionToIntersection<Union> = (Union extends unknown ? (member: Union) => void : never) extends ((member: infer Intersection) => void) ? Intersection : never;
78
78
  /**
79
79
  * Cursor-pagination boundary schemas bridged from Convex's own validators:
@@ -84,7 +84,7 @@ type UnionToIntersection<Union> = (Union extends unknown ? (member: Union) => vo
84
84
  * Note: under row-level security, pages may be SHORTER than numItems when
85
85
  * rules reject rows mid-page; cursors remain correct.
86
86
  */
87
- declare function paginated<Dto extends z.ZodType>(dto: Dto): {
87
+ export declare function paginated<Dto extends z.ZodType>(dto: Dto): {
88
88
  args: {
89
89
  paginationOpts: z.ZodObject<{
90
90
  numItems: z.ZodNumber;
@@ -107,7 +107,7 @@ declare function paginated<Dto extends z.ZodType>(dto: Dto): {
107
107
  }, z.core.$strip>;
108
108
  };
109
109
  /** Preserves discriminated-union storage while retaining the same table owner. */
110
- declare function zodVariantTable<Table extends string, Schema extends z.ZodType>(tableName: Table, storage: Schema): {
110
+ export declare function zodVariantTable<Table extends string, Schema extends z.ZodType>(tableName: Table, storage: Schema): {
111
111
  tableName: Table;
112
112
  storage: Schema;
113
113
  insertSchema: Schema;
@@ -117,7 +117,7 @@ declare function zodVariantTable<Table extends string, Schema extends z.ZodType>
117
117
  * A zid that presents as a plain string in generated JSON schemas so
118
118
  * LLM-tool inputs stay primitive, while keeping the Id type at compile time.
119
119
  */
120
- declare function jsonSafeZid<Table extends string>(tableName: Table): z.ZodType<GenericId<Table>, string>;
120
+ export declare function jsonSafeZid<Table extends string>(tableName: Table): z.ZodType<GenericId<Table>, string>;
121
121
  type ZodObjectOmit<Schema extends z.ZodObject<any, any>, OmitMask extends z.util.Mask<keyof Schema['shape']>> = Schema extends z.ZodObject<infer ObjectFields, infer Config> ? z.ZodObject<z.util.Flatten<Omit<ObjectFields, Extract<keyof ObjectFields, keyof OmitMask>>>, Config> : never;
122
122
  declare function documentSchema<Fields extends FieldSchemas, Table extends string>(storage: StrictObject<Fields>, tableName: Table): z.ZodObject<(keyof Fields & ("_creationTime" | "_id") extends never ? Fields & {
123
123
  _id: import("convex-helpers/server/zod4").Zid<Table>;
@@ -127,5 +127,5 @@ declare function documentSchema<Fields extends FieldSchemas, Table extends strin
127
127
  _creationTime: z.ZodNumber;
128
128
  }) extends (infer T) ? { [k in keyof T]: T[k]; } : never, z.core.$strict>;
129
129
  //#endregion
130
- export { TIMESTAMP_FIELDS, TableBoundary, TableBoundaryOptions, convexToZod, createModule, jsonSafeZid, paginated, tenantTable, zid, zodTable, zodToConvex, zodVariantTable };
130
+ export { convexToZod, zid, zodToConvex };
131
131
  //# sourceMappingURL=zod-table.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"zod-table.mjs","names":["zid","zodToConvex","convexToZod"],"sources":["../src/zod-table.ts"],"sourcesContent":["import {\n\tconvexToZod,\n\tzid,\n\tzodToConvex,\n\ttype ConvexValidatorFromZod,\n} from 'convex-helpers/server/zod4'\nimport { defineTable, paginationOptsValidator, type TableDefinition } from 'convex/server'\nimport type { GenericId } from 'convex/values'\nimport { z } from 'zod'\n\n// Use this entry point when deriving validators from kit-owned schemas so\n// the producer and converter consult the same convex-helpers ID registry.\nexport { zid, zodToConvex, convexToZod } from 'convex-helpers/server/zod4'\n\ntype FieldSchemas = Record<string, z.ZodType>\ntype FieldKey<Fields extends FieldSchemas> = Extract<keyof Fields, string>\n\n/**\n * Opinionated lifecycle timestamps — every table has them, server-owned,\n * always excluded from insert/command boundaries. Maintained by the\n * `timestamps` trigger helper (see cvx-kit/triggers).\n */\nconst timestampFields = {\n\tcreatedAt: z.number().optional(),\n\tupdatedAt: z.number().optional(),\n\tarchivedAt: z.number().optional(),\n} satisfies FieldSchemas\n\ntype TimestampFields = typeof timestampFields\n\nexport const TIMESTAMP_FIELDS = ['createdAt', 'updatedAt', 'archivedAt'] as const\n\nexport type TableBoundaryOptions<\n\tFields extends FieldSchemas,\n\tServerFields extends readonly FieldKey<Fields>[],\n\tCommandFields extends readonly FieldKey<Fields>[],\n\tPublicFields extends readonly FieldKey<Fields & TimestampFields>[],\n> = {\n\tserverFields?: ServerFields\n\tcommandFields?: CommandFields\n\tpublicFields?: PublicFields\n}\n\ntype StrictObject<Fields extends FieldSchemas> = z.ZodObject<Fields, z.core.$strict>\ntype WriteFields<\n\tFields extends FieldSchemas,\n\tServerFields extends readonly FieldKey<Fields>[],\n> = Omit<Fields & TimestampFields, ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number]>\ntype PartialFields<Fields extends FieldSchemas> = {\n\t[Key in keyof Fields]: z.ZodOptional<Fields[Key]>\n}\n\n/** Named schema relationships keep generated declarations bounded and consumers fully typed. */\nexport type TableBoundary<\n\tTable extends string,\n\tFields extends FieldSchemas,\n\tServerFields extends readonly FieldKey<Fields>[],\n\tCommandFields extends readonly FieldKey<Fields>[],\n\tPublicFields extends readonly FieldKey<Fields & TimestampFields>[],\n> = {\n\ttableName: Table\n\tstorage: StrictObject<Fields & TimestampFields>\n\tschema: ReturnType<typeof documentSchema<Fields & TimestampFields, Table>>\n\tinsertSchema: StrictObject<WriteFields<Fields, ServerFields>>\n\tupdateSchema: StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>\n\tcommandInput: StrictObject<Pick<Fields & TimestampFields, CommandFields[number]>>\n\tpublicDto: StrictObject<Pick<Fields & TimestampFields, PublicFields[number]>>\n\ttoPublicDto: (\n\t\trow: z.input<StrictObject<Fields & TimestampFields>>,\n\t) => z.output<StrictObject<Pick<Fields & TimestampFields, PublicFields[number]>>>\n\ttable: TableDefinition<ConvexValidatorFromZod<StrictObject<Fields & TimestampFields>, 'required'>>\n\tinsert(): StrictObject<WriteFields<Fields, ServerFields>>\n\tinsert<Mask extends z.util.Mask<keyof WriteFields<Fields, ServerFields>>>(\n\t\tomit: Mask,\n\t): ZodObjectOmit<StrictObject<WriteFields<Fields, ServerFields>>, Mask>\n\tupdate(): StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>\n\tupdate<Mask extends z.util.Mask<keyof WriteFields<Fields, ServerFields>>>(\n\t\tomit: Mask,\n\t): ZodObjectOmit<StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>, Mask>\n\ttools: {\n\t\tinsert: StrictObject<Pick<Fields & TimestampFields, CommandFields[number]>>\n\t\tupdate: StrictObject<{\n\t\t\tdata: StrictObject<PartialFields<Pick<Fields & TimestampFields, CommandFields[number]>>>\n\t\t\tid: z.ZodType<GenericId<Table>, string>\n\t\t}>\n\t\tid: StrictObject<{ id: z.ZodType<GenericId<Table>, string> }>\n\t}\n}\n\n/** Defines one table and derives its storage, document, write, and DTO boundaries. */\nexport function zodTable<\n\tTable extends string,\n\tFields extends FieldSchemas,\n\tconst ServerFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst CommandFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst PublicFields extends readonly FieldKey<Fields & TimestampFields>[] = readonly [],\n>(\n\ttableName: Table,\n\tfields: (id: typeof zid) => Fields,\n\toptions: TableBoundaryOptions<Fields, ServerFields, CommandFields, PublicFields> = {},\n): TableBoundary<Table, Fields, ServerFields, CommandFields, PublicFields> {\n\tconst columns: Fields & TimestampFields = { ...fields(zid), ...timestampFields }\n\tconst storage: z.ZodObject<Fields & TimestampFields, z.core.$strict> = z.object(columns).strict()\n\tconst schema = documentSchema(storage, tableName)\n\tconst omittedKeys: readonly (ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number])[] = [\n\t\t...TIMESTAMP_FIELDS,\n\t\t...(options.serverFields ?? []),\n\t]\n\tconst commandKeys: readonly CommandFields[number][] = options.commandFields ?? []\n\tconst publicKeys: readonly PublicFields[number][] = options.publicFields ?? []\n\tconst insertSchema: z.ZodObject<WriteFields<Fields, ServerFields>, z.core.$strict> = z\n\t\t.object(omitFields(columns, omittedKeys))\n\t\t.strict()\n\tconst updateSchema = insertSchema.partial()\n\tconst commandInput: z.ZodObject<\n\t\tPick<Fields & TimestampFields, CommandFields[number]>,\n\t\tz.core.$strict\n\t> = z.object(pickFields(columns, commandKeys)).strict()\n\tconst publicDto: z.ZodObject<\n\t\tPick<Fields & TimestampFields, PublicFields[number]>,\n\t\tz.core.$strict\n\t> = z.object(pickFields(columns, publicKeys)).strict()\n\tconst toPublicDto = (row: z.input<typeof storage>) =>\n\t\tpublicDto.parse(\n\t\t\tObject.fromEntries(\n\t\t\t\tObject.keys(publicDto.shape).map((key) => {\n\t\t\t\t\t// SAFETY: publicDto contains only keys selected from the storage fields.\n\t\t\t\t\treturn [key, row[key as keyof typeof row]]\n\t\t\t\t}),\n\t\t\t),\n\t\t)\n\n\ttype InsertSchema = typeof insertSchema\n\ttype UpdateSchema = typeof updateSchema\n\ttype InsertMask = z.util.Mask<keyof InsertSchema['shape']>\n\ttype UpdateMask = z.util.Mask<keyof UpdateSchema['shape']>\n\n\tfunction insert(): InsertSchema\n\tfunction insert<OmitMask extends InsertMask>(\n\t\tomit: OmitMask,\n\t): ZodObjectOmit<InsertSchema, OmitMask>\n\tfunction insert(omit?: InsertMask) {\n\t\t// SAFETY: InsertMask contains only insertSchema keys; the overload retains Zod's exact mask relationship.\n\t\treturn omit ? insertSchema.omit(omit as never) : insertSchema\n\t}\n\n\tfunction update(): UpdateSchema\n\tfunction update<OmitMask extends UpdateMask>(\n\t\tomit: OmitMask,\n\t): ZodObjectOmit<UpdateSchema, OmitMask>\n\tfunction update(omit?: UpdateMask) {\n\t\t// SAFETY: UpdateMask contains only updateSchema keys; the overload retains Zod's exact mask relationship.\n\t\treturn omit ? updateSchema.omit(omit as never) : updateSchema\n\t}\n\n\tconst table: TableDefinition<ConvexValidatorFromZod<typeof storage, 'required'>> = defineTable(\n\t\tzodToConvex(storage),\n\t)\n\treturn {\n\t\ttableName,\n\t\tschema,\n\t\tstorage,\n\t\tinsertSchema,\n\t\tupdateSchema,\n\t\tcommandInput,\n\t\tpublicDto,\n\t\ttoPublicDto,\n\t\ttable,\n\t\tinsert,\n\t\tupdate,\n\t\ttools: {\n\t\t\t// jsonSafeZid (not zid): tool masks feed generated JSON schemas, so\n\t\t\t// ids must present as plain strings while keeping the Id<...> type.\n\t\t\tinsert: commandInput,\n\t\t\tupdate: z.object({ data: commandInput.partial(), id: jsonSafeZid(tableName) }).strict(),\n\t\t\tid: z.object({ id: jsonSafeZid(tableName) }).strict(),\n\t\t},\n\t}\n}\n\nconst tenantFields = {\n\ttenant: z.string().min(1),\n} satisfies FieldSchemas\n\ntype TenantFields = typeof tenantFields\n\n/**\n * A zodTable whose rows are tenant-owned: injects the server-owned `tenant`\n * field (the tenancy boundary — see cvx-kit/tenancy). Like timestamps, it is\n * excluded from insert/update/command boundaries; handlers stamp it from\n * ctx.tenant, row-level security matches on it, and the tenantOwnership\n * trigger forbids reassigning it. Expose it via publicFields only when a DTO\n * genuinely needs it.\n */\nexport function tenantTable<\n\tTable extends string,\n\tFields extends FieldSchemas,\n\tconst ServerFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst CommandFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst PublicFields extends readonly FieldKey<Fields & TenantFields & TimestampFields>[] =\n\t\treadonly [],\n>(\n\ttableName: Table,\n\tfields: (id: typeof zid) => Fields,\n\toptions: TableBoundaryOptions<\n\t\tFields & TenantFields,\n\t\treadonly (ServerFields[number] | 'tenant')[],\n\t\tCommandFields,\n\t\tPublicFields\n\t> = {},\n) {\n\treturn zodTable<\n\t\tTable,\n\t\tFields & TenantFields,\n\t\treadonly (ServerFields[number] | 'tenant')[],\n\t\tCommandFields,\n\t\tPublicFields\n\t>(tableName, (id) => ({ ...fields(id), ...tenantFields }), {\n\t\t...options,\n\t\tserverFields: [...(options.serverFields ?? []), 'tenant'],\n\t})\n}\n\n/**\n * Merges per-module table maps into the application schema map, rejecting\n * duplicate table names across modules — the module-registry combinator for\n * domain/table.ts: `defineSchema(createModule(catalogTables, salesTables))`.\n */\nexport function createModule<const Maps extends readonly Record<string, TableDefinition<any>>[]>(\n\t...maps: Maps\n): UnionToIntersection<Maps[number]> {\n\tconst combined: Record<string, TableDefinition<any>> = {}\n\tfor (const map of maps) {\n\t\tfor (const [tableName, definition] of Object.entries(map)) {\n\t\t\tif (tableName in combined) {\n\t\t\t\tthrow new Error(`Table \"${tableName}\" is declared by more than one module`)\n\t\t\t}\n\t\t\tcombined[tableName] = definition\n\t\t}\n\t}\n\t// SAFETY: every input entry is copied, and duplicate keys are rejected above.\n\treturn combined as UnionToIntersection<Maps[number]>\n}\n\ntype UnionToIntersection<Union> = (\n\tUnion extends unknown ? (member: Union) => void : never\n) extends (member: infer Intersection) => void\n\t? Intersection\n\t: never\n\n/**\n * Cursor-pagination boundary schemas bridged from Convex's own validators:\n * `args.paginationOpts` for function args, `result` for the page shape.\n * Pair with `include(...).paginate(opts)` (cvx-kit/auth). The result object\n * is deliberately non-strict — Convex may attach engine fields (splitCursor,\n * pageStatus); the page items themselves are validated against the dto.\n * Note: under row-level security, pages may be SHORTER than numItems when\n * rules reject rows mid-page; cursors remain correct.\n */\nexport function paginated<Dto extends z.ZodType>(dto: Dto) {\n\treturn {\n\t\targs: { paginationOpts: convexToZod(paginationOptsValidator) },\n\t\tresult: z.object({\n\t\t\tpage: z.array(dto),\n\t\t\tisDone: z.boolean(),\n\t\t\tcontinueCursor: z.string(),\n\t\t\tsplitCursor: z.string().nullable().optional(),\n\t\t\tpageStatus: z.enum(['SplitRecommended', 'SplitRequired']).nullable().optional(),\n\t\t}),\n\t}\n}\n\n/** Preserves discriminated-union storage while retaining the same table owner. */\nexport function zodVariantTable<Table extends string, Schema extends z.ZodType>(\n\ttableName: Table,\n\tstorage: Schema,\n) {\n\treturn {\n\t\ttableName,\n\t\tstorage,\n\t\tinsertSchema: storage,\n\t\ttable: defineTable(zodToConvex(storage)),\n\t}\n}\n\n/**\n * A zid that presents as a plain string in generated JSON schemas so\n * LLM-tool inputs stay primitive, while keeping the Id type at compile time.\n */\nexport function jsonSafeZid<Table extends string>(\n\ttableName: Table,\n): z.ZodType<GenericId<Table>, string> {\n\tconst id = zid(tableName)\n\treturn z\n\t\t.string()\n\t\t.refine((value): value is GenericId<Table> => id.safeParse(value).success)\n\t\t.describe(`Convex document id for table \"${tableName}\"; validated against the table by Convex`)\n}\n\ntype ZodObjectOmit<\n\tSchema extends z.ZodObject<any, any>,\n\tOmitMask extends z.util.Mask<keyof Schema['shape']>,\n> =\n\tSchema extends z.ZodObject<infer ObjectFields, infer Config>\n\t\t? z.ZodObject<\n\t\t\t\tz.util.Flatten<Omit<ObjectFields, Extract<keyof ObjectFields, keyof OmitMask>>>,\n\t\t\t\tConfig\n\t\t\t>\n\t\t: never\n\nfunction documentSchema<Fields extends FieldSchemas, Table extends string>(\n\tstorage: StrictObject<Fields>,\n\ttableName: Table,\n) {\n\treturn storage.extend({ _id: zid(tableName), _creationTime: z.number() })\n}\n\nfunction pickFields<Fields extends FieldSchemas, const Keys extends readonly FieldKey<Fields>[]>(\n\tfields: Fields,\n\tkeys: Keys,\n) {\n\t// SAFETY: each entry uses a requested key and its original validator, preserving Pick.\n\treturn Object.fromEntries(keys.map((key) => [key, fields[key]])) as Pick<Fields, Keys[number]>\n}\n\nfunction omitFields<Fields extends FieldSchemas, const Keys extends readonly FieldKey<Fields>[]>(\n\tfields: Fields,\n\tkeys: Keys,\n) {\n\tconst omitted = new Set<string>(keys)\n\t// SAFETY: filtering removes exactly the supplied keys and keeps every remaining validator.\n\treturn Object.fromEntries(Object.entries(fields).filter(([key]) => !omitted.has(key))) as Omit<\n\t\tFields,\n\t\tKeys[number]\n\t>\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"zod-table.mjs","names":["zid","zodToConvex","convexToZod","convexToZod$1","zid$1","zodToConvex$1","timestampFields","z","TIMESTAMP_FIELDS","zodTable","columns","fields","storage","schema","documentSchema","tableName","omittedKeys","options","commandKeys","publicKeys","insertSchema","omitFields","updateSchema","commandInput","pickFields","publicDto","toPublicDto","key","row","insert","omit","update","defineTable","jsonSafeZid","tenantFields","tenantTable","id","createModule","maps","combined","map","definition","paginated","paginationOptsValidator","dto","zodVariantTable","value","keys","omitted","convexToZod","zid","zodToConvex"],"sources":["../src/zod-table.ts"],"sourcesContent":["import {\n\tconvexToZod,\n\tzid,\n\tzodToConvex,\n\ttype ConvexValidatorFromZod,\n} from 'convex-helpers/server/zod4'\nimport { defineTable, paginationOptsValidator, type TableDefinition } from 'convex/server'\nimport type { GenericId } from 'convex/values'\nimport { z } from 'zod'\n\n// Use this entry point when deriving validators from kit-owned schemas so\n// the producer and converter consult the same convex-helpers ID registry.\nexport { zid, zodToConvex, convexToZod } from 'convex-helpers/server/zod4'\n\ntype FieldSchemas = Record<string, z.ZodType>\ntype FieldKey<Fields extends FieldSchemas> = Extract<keyof Fields, string>\n\n/**\n * Opinionated lifecycle timestamps — every table has them, server-owned,\n * always excluded from insert/command boundaries. Maintained by the\n * `timestamps` trigger helper (see cvx-kit/triggers).\n */\nconst timestampFields = {\n\tcreatedAt: z.number().optional(),\n\tupdatedAt: z.number().optional(),\n\tarchivedAt: z.number().optional(),\n} satisfies FieldSchemas\n\ntype TimestampFields = typeof timestampFields\n\nexport const TIMESTAMP_FIELDS = ['createdAt', 'updatedAt', 'archivedAt'] as const\n\nexport type TableBoundaryOptions<\n\tFields extends FieldSchemas,\n\tServerFields extends readonly FieldKey<Fields>[],\n\tCommandFields extends readonly FieldKey<Fields>[],\n\tPublicFields extends readonly FieldKey<Fields & TimestampFields>[],\n> = {\n\tserverFields?: ServerFields\n\tcommandFields?: CommandFields\n\tpublicFields?: PublicFields\n}\n\ntype StrictObject<Fields extends FieldSchemas> = z.ZodObject<Fields, z.core.$strict>\ntype WriteFields<\n\tFields extends FieldSchemas,\n\tServerFields extends readonly FieldKey<Fields>[],\n> = Omit<Fields & TimestampFields, ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number]>\ntype PartialFields<Fields extends FieldSchemas> = {\n\t[Key in keyof Fields]: z.ZodOptional<Fields[Key]>\n}\n\n/** Named schema relationships keep generated declarations bounded and consumers fully typed. */\nexport type TableBoundary<\n\tTable extends string,\n\tFields extends FieldSchemas,\n\tServerFields extends readonly FieldKey<Fields>[],\n\tCommandFields extends readonly FieldKey<Fields>[],\n\tPublicFields extends readonly FieldKey<Fields & TimestampFields>[],\n> = {\n\ttableName: Table\n\tstorage: StrictObject<Fields & TimestampFields>\n\tschema: ReturnType<typeof documentSchema<Fields & TimestampFields, Table>>\n\tinsertSchema: StrictObject<WriteFields<Fields, ServerFields>>\n\tupdateSchema: StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>\n\tcommandInput: StrictObject<Pick<Fields & TimestampFields, CommandFields[number]>>\n\tpublicDto: StrictObject<Pick<Fields & TimestampFields, PublicFields[number]>>\n\ttoPublicDto: (\n\t\trow: z.input<StrictObject<Fields & TimestampFields>>,\n\t) => z.output<StrictObject<Pick<Fields & TimestampFields, PublicFields[number]>>>\n\ttable: TableDefinition<ConvexValidatorFromZod<StrictObject<Fields & TimestampFields>, 'required'>>\n\tinsert(): StrictObject<WriteFields<Fields, ServerFields>>\n\tinsert<Mask extends z.util.Mask<keyof WriteFields<Fields, ServerFields>>>(\n\t\tomit: Mask,\n\t): ZodObjectOmit<StrictObject<WriteFields<Fields, ServerFields>>, Mask>\n\tupdate(): StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>\n\tupdate<Mask extends z.util.Mask<keyof WriteFields<Fields, ServerFields>>>(\n\t\tomit: Mask,\n\t): ZodObjectOmit<StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>, Mask>\n\ttools: {\n\t\tinsert: StrictObject<Pick<Fields & TimestampFields, CommandFields[number]>>\n\t\tupdate: StrictObject<{\n\t\t\tdata: StrictObject<PartialFields<Pick<Fields & TimestampFields, CommandFields[number]>>>\n\t\t\tid: z.ZodType<GenericId<Table>, string>\n\t\t}>\n\t\tid: StrictObject<{ id: z.ZodType<GenericId<Table>, string> }>\n\t}\n}\n\n/** Defines one table and derives its storage, document, write, and DTO boundaries. */\nexport function zodTable<\n\tTable extends string,\n\tFields extends FieldSchemas,\n\tconst ServerFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst CommandFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst PublicFields extends readonly FieldKey<Fields & TimestampFields>[] = readonly [],\n>(\n\ttableName: Table,\n\tfields: (id: typeof zid) => Fields,\n\toptions: TableBoundaryOptions<Fields, ServerFields, CommandFields, PublicFields> = {},\n): TableBoundary<Table, Fields, ServerFields, CommandFields, PublicFields> {\n\tconst columns: Fields & TimestampFields = { ...fields(zid), ...timestampFields }\n\tconst storage: z.ZodObject<Fields & TimestampFields, z.core.$strict> = z.object(columns).strict()\n\tconst schema = documentSchema(storage, tableName)\n\tconst omittedKeys: readonly (ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number])[] = [\n\t\t...TIMESTAMP_FIELDS,\n\t\t...(options.serverFields ?? []),\n\t]\n\tconst commandKeys: readonly CommandFields[number][] = options.commandFields ?? []\n\tconst publicKeys: readonly PublicFields[number][] = options.publicFields ?? []\n\tconst insertSchema: z.ZodObject<WriteFields<Fields, ServerFields>, z.core.$strict> = z\n\t\t.object(omitFields(columns, omittedKeys))\n\t\t.strict()\n\tconst updateSchema = insertSchema.partial()\n\tconst commandInput: z.ZodObject<\n\t\tPick<Fields & TimestampFields, CommandFields[number]>,\n\t\tz.core.$strict\n\t> = z.object(pickFields(columns, commandKeys)).strict()\n\tconst publicDto: z.ZodObject<\n\t\tPick<Fields & TimestampFields, PublicFields[number]>,\n\t\tz.core.$strict\n\t> = z.object(pickFields(columns, publicKeys)).strict()\n\tconst toPublicDto = (row: z.input<typeof storage>) =>\n\t\tpublicDto.parse(\n\t\t\tObject.fromEntries(\n\t\t\t\tObject.keys(publicDto.shape).map((key) => {\n\t\t\t\t\t// SAFETY: publicDto contains only keys selected from the storage fields.\n\t\t\t\t\treturn [key, row[key as keyof typeof row]]\n\t\t\t\t}),\n\t\t\t),\n\t\t)\n\n\ttype InsertSchema = typeof insertSchema\n\ttype UpdateSchema = typeof updateSchema\n\ttype InsertMask = z.util.Mask<keyof InsertSchema['shape']>\n\ttype UpdateMask = z.util.Mask<keyof UpdateSchema['shape']>\n\n\tfunction insert(): InsertSchema\n\tfunction insert<OmitMask extends InsertMask>(\n\t\tomit: OmitMask,\n\t): ZodObjectOmit<InsertSchema, OmitMask>\n\tfunction insert(omit?: InsertMask) {\n\t\t// SAFETY: InsertMask contains only insertSchema keys; the overload retains Zod's exact mask relationship.\n\t\treturn omit ? insertSchema.omit(omit as never) : insertSchema\n\t}\n\n\tfunction update(): UpdateSchema\n\tfunction update<OmitMask extends UpdateMask>(\n\t\tomit: OmitMask,\n\t): ZodObjectOmit<UpdateSchema, OmitMask>\n\tfunction update(omit?: UpdateMask) {\n\t\t// SAFETY: UpdateMask contains only updateSchema keys; the overload retains Zod's exact mask relationship.\n\t\treturn omit ? updateSchema.omit(omit as never) : updateSchema\n\t}\n\n\tconst table: TableDefinition<ConvexValidatorFromZod<typeof storage, 'required'>> = defineTable(\n\t\tzodToConvex(storage),\n\t)\n\treturn {\n\t\ttableName,\n\t\tschema,\n\t\tstorage,\n\t\tinsertSchema,\n\t\tupdateSchema,\n\t\tcommandInput,\n\t\tpublicDto,\n\t\ttoPublicDto,\n\t\ttable,\n\t\tinsert,\n\t\tupdate,\n\t\ttools: {\n\t\t\t// jsonSafeZid (not zid): tool masks feed generated JSON schemas, so\n\t\t\t// ids must present as plain strings while keeping the Id<...> type.\n\t\t\tinsert: commandInput,\n\t\t\tupdate: z.object({ data: commandInput.partial(), id: jsonSafeZid(tableName) }).strict(),\n\t\t\tid: z.object({ id: jsonSafeZid(tableName) }).strict(),\n\t\t},\n\t}\n}\n\nconst tenantFields = {\n\ttenant: z.string().min(1),\n} satisfies FieldSchemas\n\ntype TenantFields = typeof tenantFields\n\n/**\n * A zodTable whose rows are tenant-owned: injects the server-owned `tenant`\n * field (the tenancy boundary — see cvx-kit/tenancy). Like timestamps, it is\n * excluded from insert/update/command boundaries; handlers stamp it from\n * ctx.tenant, row-level security matches on it, and the tenantOwnership\n * trigger forbids reassigning it. Expose it via publicFields only when a DTO\n * genuinely needs it.\n */\nexport function tenantTable<\n\tTable extends string,\n\tFields extends FieldSchemas,\n\tconst ServerFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst CommandFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst PublicFields extends readonly FieldKey<Fields & TenantFields & TimestampFields>[] =\n\t\treadonly [],\n>(\n\ttableName: Table,\n\tfields: (id: typeof zid) => Fields,\n\toptions: TableBoundaryOptions<\n\t\tFields & TenantFields,\n\t\treadonly (ServerFields[number] | 'tenant')[],\n\t\tCommandFields,\n\t\tPublicFields\n\t> = {},\n) {\n\treturn zodTable<\n\t\tTable,\n\t\tFields & TenantFields,\n\t\treadonly (ServerFields[number] | 'tenant')[],\n\t\tCommandFields,\n\t\tPublicFields\n\t>(tableName, (id) => ({ ...fields(id), ...tenantFields }), {\n\t\t...options,\n\t\tserverFields: [...(options.serverFields ?? []), 'tenant'],\n\t})\n}\n\n/**\n * Merges per-module table maps into the application schema map, rejecting\n * duplicate table names across modules — the module-registry combinator for\n * domain/table.ts: `defineSchema(createModule(catalogTables, salesTables))`.\n */\nexport function createModule<const Maps extends readonly Record<string, TableDefinition<any>>[]>(\n\t...maps: Maps\n): UnionToIntersection<Maps[number]> {\n\tconst combined: Record<string, TableDefinition<any>> = {}\n\tfor (const map of maps) {\n\t\tfor (const [tableName, definition] of Object.entries(map)) {\n\t\t\tif (tableName in combined) {\n\t\t\t\tthrow new Error(`Table \"${tableName}\" is declared by more than one module`)\n\t\t\t}\n\t\t\tcombined[tableName] = definition\n\t\t}\n\t}\n\t// SAFETY: every input entry is copied, and duplicate keys are rejected above.\n\treturn combined as UnionToIntersection<Maps[number]>\n}\n\ntype UnionToIntersection<Union> = (\n\tUnion extends unknown ? (member: Union) => void : never\n) extends (member: infer Intersection) => void\n\t? Intersection\n\t: never\n\n/**\n * Cursor-pagination boundary schemas bridged from Convex's own validators:\n * `args.paginationOpts` for function args, `result` for the page shape.\n * Pair with `include(...).paginate(opts)` (cvx-kit/auth). The result object\n * is deliberately non-strict — Convex may attach engine fields (splitCursor,\n * pageStatus); the page items themselves are validated against the dto.\n * Note: under row-level security, pages may be SHORTER than numItems when\n * rules reject rows mid-page; cursors remain correct.\n */\nexport function paginated<Dto extends z.ZodType>(dto: Dto) {\n\treturn {\n\t\targs: { paginationOpts: convexToZod(paginationOptsValidator) },\n\t\tresult: z.object({\n\t\t\tpage: z.array(dto),\n\t\t\tisDone: z.boolean(),\n\t\t\tcontinueCursor: z.string(),\n\t\t\tsplitCursor: z.string().nullable().optional(),\n\t\t\tpageStatus: z.enum(['SplitRecommended', 'SplitRequired']).nullable().optional(),\n\t\t}),\n\t}\n}\n\n/** Preserves discriminated-union storage while retaining the same table owner. */\nexport function zodVariantTable<Table extends string, Schema extends z.ZodType>(\n\ttableName: Table,\n\tstorage: Schema,\n) {\n\treturn {\n\t\ttableName,\n\t\tstorage,\n\t\tinsertSchema: storage,\n\t\ttable: defineTable(zodToConvex(storage)),\n\t}\n}\n\n/**\n * A zid that presents as a plain string in generated JSON schemas so\n * LLM-tool inputs stay primitive, while keeping the Id type at compile time.\n */\nexport function jsonSafeZid<Table extends string>(\n\ttableName: Table,\n): z.ZodType<GenericId<Table>, string> {\n\tconst id = zid(tableName)\n\treturn z\n\t\t.string()\n\t\t.refine((value): value is GenericId<Table> => id.safeParse(value).success)\n\t\t.describe(`Convex document id for table \"${tableName}\"; validated against the table by Convex`)\n}\n\ntype ZodObjectOmit<\n\tSchema extends z.ZodObject<any, any>,\n\tOmitMask extends z.util.Mask<keyof Schema['shape']>,\n> =\n\tSchema extends z.ZodObject<infer ObjectFields, infer Config>\n\t\t? z.ZodObject<\n\t\t\t\tz.util.Flatten<Omit<ObjectFields, Extract<keyof ObjectFields, keyof OmitMask>>>,\n\t\t\t\tConfig\n\t\t\t>\n\t\t: never\n\nfunction documentSchema<Fields extends FieldSchemas, Table extends string>(\n\tstorage: StrictObject<Fields>,\n\ttableName: Table,\n) {\n\treturn storage.extend({ _id: zid(tableName), _creationTime: z.number() })\n}\n\nfunction pickFields<Fields extends FieldSchemas, const Keys extends readonly FieldKey<Fields>[]>(\n\tfields: Fields,\n\tkeys: Keys,\n) {\n\t// SAFETY: each entry uses a requested key and its original validator, preserving Pick.\n\treturn Object.fromEntries(keys.map((key) => [key, fields[key]])) as Pick<Fields, Keys[number]>\n}\n\nfunction omitFields<Fields extends FieldSchemas, const Keys extends readonly FieldKey<Fields>[]>(\n\tfields: Fields,\n\tkeys: Keys,\n) {\n\tconst omitted = new Set<string>(keys)\n\t// SAFETY: filtering removes exactly the supplied keys and keeps every remaining validator.\n\treturn Object.fromEntries(Object.entries(fields).filter(([key]) => !omitted.has(key))) as Omit<\n\t\tFields,\n\t\tKeys[number]\n\t>\n}\n"],"mappings":"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"}
@@ -13,7 +13,7 @@ convex/
13
13
  convex.config.ts # component mounting — each app.use(x) exactly once
14
14
  schema.ts # defineSchema(domainTables) — nothing else
15
15
  functions.ts # createAuthFunctions(...) — the ONLY place raw builders appear
16
- foundation.ts # const { Command, Query, observability } = new Foundation(...)
16
+ foundation.ts # const { Command, Query, observability } = createEffectFoundation(...)
17
17
  approvals.ts # const approvals = new Approvals(components.approvals)
18
18
  audit.ts # writeAuditEntry over convex-audit-log + admin readers
19
19
  triggers.ts # the single Triggers registry + helper registrations
@@ -27,7 +27,7 @@ convex/
27
27
  schema.ts # zodTable declaration (shapes only)
28
28
  table.ts # indexes attached to <zodTable>.table
29
29
  constants.ts # finite vocabularies (UPPER_SNAKE readonly tuples)
30
- commands.ts # operation registry + executors (new Command(...))
30
+ commands.ts # Command({ context, operations }) with handlers inside command({...})
31
31
  queries.ts # domain read logic
32
32
  approval.ts # approvals.define(...) workflow definitions
33
33
  *_functions.ts # internal callbacks (systemMutation/systemAction)
@@ -57,7 +57,7 @@ Every kit surface takes its policy as a parameter:
57
57
 
58
58
  - `createAuthFunctions` receives the generated builders, `getAuthUser`,
59
59
  `mapRole`, `verifyMembership`, `triggers`, `errors`.
60
- - `Foundation` receives `classifyError`, `writeAudit`, the `enabled` flag.
60
+ - `createEffectFoundation` receives `classifyError`, `writeAudit`, the `enabled` flag.
61
61
  - `Approvals` receives only its component reference; host data crosses as
62
62
  opaque validated strings.
63
63
 
package/docs/auth.md CHANGED
@@ -91,12 +91,12 @@ class Actor extends Context.Service<
91
91
  >()('app/Actor') {}
92
92
 
93
93
  export function createEffectAccountApi<DataModel extends GenericDataModel>(
94
- authMutation: ReturnType<typeof createAuthFunctions<DataModel>>['authMutation'],
94
+ baseAuthMutation: ReturnType<typeof createAuthFunctions<DataModel>>['authMutation'],
95
95
  ) {
96
- const effectAuthMutation = effectZodApiBuilder(authMutation, {
96
+ const authMutation = effectZodApiBuilder(baseAuthMutation, {
97
97
  services: (ctx) => Context.make(Actor, ctx.actor),
98
98
  })
99
- const currentActor = effectAuthMutation({
99
+ const currentActor = authMutation({
100
100
  args: {},
101
101
  returns: z.object({ userId: z.string(), organizationId: z.string() }),
102
102
  handler: () =>
@@ -105,7 +105,7 @@ export function createEffectAccountApi<DataModel extends GenericDataModel>(
105
105
  return { userId: actor.userId, organizationId: actor.organizationId }
106
106
  }),
107
107
  })
108
- return { effectAuthMutation, currentActor }
108
+ return { authMutation, currentActor }
109
109
  }
110
110
  ```
111
111
 
@@ -305,3 +305,9 @@ errors: { throw: (input) => { throw App.errors.from(input) } }
305
305
  3. Reads go through `ctx.include(...)` with an explicit limit.
306
306
  4. Use `ctx.actor` as the actor reference in audits and approvals — never
307
307
  re-derive identity in a handler.
308
+
309
+ Optional request context can be added through the existing Effect API adapter's
310
+ `context` callback. Bind selected registries with `commands.withContext(value)`
311
+ and consume `ctx.commands.exec(name, input)` through the application's existing
312
+ function helper names. Keep dependencies local to each feature; explicit execution
313
+ and input-only command handlers remain supported. See [optional context injection](./command-context.md).
@@ -0,0 +1,150 @@
1
+ # Optional command context injection
2
+
3
+ Bind context once at a function boundary when it makes endpoint code clearer.
4
+ `withContext(value)` accepts whatever the registry's resolver accepts: a number,
5
+ a service bundle, a request context, or another bound command group. It returns
6
+ a small handle whose `exec(name, input)` still produces a lazy Effect. Binding
7
+ neither resolves context nor executes a command. Explicit
8
+ `commands.exec(name, input, context)` remains supported.
9
+
10
+ ## Keep existing function names
11
+
12
+ Add context through `effectZodApiBuilder` around the existing custom constructor.
13
+ Export it under your application's ordinary `authMutation`, `adminQuery`,
14
+ `organizationMutation`, or other function name. These are application choices;
15
+ the library does not require a new helper family. The same option works on
16
+ `effectApiBuilder` for native builders. `services` is optional when your Effects
17
+ need no services.
18
+
19
+ The complete example below uses a custom authenticated mutation to make the
20
+ context flow standalone. A kit application can supply
21
+ `createAuthFunctions(...).authMutation` instead. Its authenticated, secured
22
+ context is constructed before the additions callback runs.
23
+
24
+ <!-- packed-effect-example -->
25
+
26
+ ```ts
27
+ import { Effect } from 'effect'
28
+ import { mutationGeneric } from 'convex/server'
29
+ import { zCustomMutation } from 'convex-helpers/server/zod4'
30
+ import { createEffectFoundation, effectZodApiBuilder } from 'cvx-kit/effect'
31
+ import { z } from 'zod'
32
+
33
+ const foundation = createEffectFoundation({
34
+ observability: {
35
+ enabled: false,
36
+ classifyError: () => ({ outcome: 'failed', errorCode: 'COMMAND_FAILED' }),
37
+ },
38
+ // Standalone demonstration only: production applications provide their audit writer.
39
+ writeAudit: () => undefined,
40
+ })
41
+
42
+ const notifications = foundation.Command({
43
+ context: (dependencies: { format: (message: string) => string }) => dependencies,
44
+ operations: ({ command }) => ({
45
+ format: command({
46
+ input: z.string(), result: z.string(), classification: 'business',
47
+ handler: (message, context) => context.format(message),
48
+ audit: () => null,
49
+ }),
50
+ }),
51
+ })
52
+
53
+ const tasks = foundation.Command({
54
+ context: (dependencies: {
55
+ nextNumber: () => number,
56
+ notifications: ReturnType<typeof notifications.withContext>,
57
+ }) => dependencies,
58
+ operations: ({ command }) => ({
59
+ create: command({
60
+ input: z.string(), result: z.string(), classification: 'business',
61
+ handler: (title, context) => context.notifications.exec(
62
+ 'format', `${context.nextNumber()}:${title}`,
63
+ ),
64
+ audit: () => null,
65
+ }),
66
+ }),
67
+ })
68
+
69
+ const baseAuthMutation = zCustomMutation(mutationGeneric, {
70
+ args: {},
71
+ input: async (ctx) => {
72
+ const identity = await ctx.auth.getUserIdentity()
73
+ if (!identity) throw Error('UNAUTHORIZED')
74
+ return { ctx: { actor: identity.subject }, args: {} }
75
+ },
76
+ })
77
+
78
+ // In a real app, keep this composition in the tasks feature's functions module.
79
+ export const authMutation = effectZodApiBuilder(baseAuthMutation, {
80
+ context: (ctx) => {
81
+ const messages = notifications.withContext({
82
+ format: (message) => `${ctx.actor}:${message}`,
83
+ })
84
+ return {
85
+ tasks: tasks.withContext({ nextNumber: () => 42, notifications: messages }),
86
+ }
87
+ },
88
+ })
89
+
90
+ export const createTask = authMutation({
91
+ args: { title: z.string() },
92
+ returns: z.string(),
93
+ handler: (ctx, input) => ctx.tasks.exec('create', input.title),
94
+ })
95
+
96
+ // An input-only command needs no domain dependencies.
97
+ const echo = foundation.Command({
98
+ context: () => undefined,
99
+ operations: ({ command }) => ({
100
+ echo: command({
101
+ input: z.string(), result: z.string(), classification: 'business',
102
+ handler: (input) => input, audit: () => null,
103
+ }),
104
+ }),
105
+ })
106
+ export const echoEffect = echo.exec('echo', 'hello', undefined)
107
+ export const boundEchoEffect = echo.withContext(undefined).exec('echo', 'hello')
108
+ // A program can compose further work; the API adapter owns execution.
109
+ export const composedEcho = Effect.map(boundEchoEffect, (value) => value.length)
110
+ ```
111
+
112
+ Context additions are optional and cannot replace existing fields, including
113
+ `db`, `auth`, or a custom constructor's authenticated actor. Type checking rejects
114
+ known collisions with declared fields. Runtime checking also protects inherited
115
+ properties (such as `toString`) and keys from broadly typed records or erased
116
+ types; those collisions cannot always be determined from the declared context
117
+ type. Additions can
118
+ return a plain record of own properties, or a Promise or Effect of that record.
119
+ Wrap class instances and services in named properties, such as `{ pricingService }`,
120
+ rather than returning an instance as the additions object. The adapter provides services first, then
121
+ runs initialization and the handler in the same invocation scope. Missing
122
+ initializer services are rejected when registering the endpoint, just like
123
+ missing handler services. Initialization failures use the configured error
124
+ projection and release acquired resources.
125
+
126
+ ## Applications with 25–50 entities
127
+
128
+ Organize by feature or workflow rather than making every entity a property of a
129
+ single global context. A tasks helper can bind tasks and notifications; a billing
130
+ helper can bind billing and pricing. Each registry declares its own narrow
131
+ resolver input. Share stable service implementations at module scope, and build
132
+ request-dependent handles within the context callback.
133
+
134
+ Keep shared identity or tenant information small. Inject a business function or
135
+ service only into the features that use it. Pass bound commands only where a
136
+ workflow calls them; avoid mutual command dependencies. Bindings hold references
137
+ and do not initialize the referenced registry. Large dependency graphs are
138
+ application architecture choices, not a required library container.
139
+
140
+ If a feature needs no injected commands, omit `context` additions. If an endpoint
141
+ benefits from explicit execution, pass its context directly. An input-only
142
+ handler can ignore domain context, but permission and audit policies may still
143
+ need the original trusted invocation context. Preserve those policies when
144
+ choosing the registry resolver and binding input.
145
+
146
+ Nested calls return Effects and compose in the current program. Do not call
147
+ `Effect.runPromise` inside command handlers; the function adapter runs the
148
+ complete program once. Command schema checks, permissions, audit, and completion
149
+ remain active for each nested command. Keep database mutation work in a mutation
150
+ boundary so Convex can roll it back if execution or result validation fails.