cvx-kit 0.1.6 → 0.1.7

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 (108) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +10 -1
  3. package/dist/agent-tools.d.mts +2 -1
  4. package/dist/agent-tools.mjs +1 -1
  5. package/dist/agent-tools.mjs.map +1 -1
  6. package/dist/components/approvals/audit.mjs +1 -1
  7. package/dist/components/foundation/client.d.mts +8 -7
  8. package/dist/components/foundation/client.mjs +1 -1
  9. package/dist/components/foundation/client.mjs.map +1 -1
  10. package/dist/components/foundation/modules/command/command.d.mts +50 -10
  11. package/dist/components/foundation/modules/command/command.mjs +1 -1
  12. package/dist/components/foundation/modules/command/command.mjs.map +1 -1
  13. package/dist/components/foundation/modules/command/lifecycle.d.mts +81 -0
  14. package/dist/components/foundation/modules/command/lifecycle.mjs +2 -0
  15. package/dist/components/foundation/modules/command/lifecycle.mjs.map +1 -0
  16. package/dist/components/foundation/modules/observability/observability.d.mts +8 -0
  17. package/dist/components/foundation/modules/observability/observability.mjs +1 -1
  18. package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
  19. package/dist/components/foundation/operation.d.mts +6 -6
  20. package/dist/components/foundation/operation.mjs.map +1 -1
  21. package/dist/contracts.d.mts +4 -0
  22. package/dist/contracts.mjs +1 -0
  23. package/dist/crud.d.mts +2 -2
  24. package/dist/crud.mjs.map +1 -1
  25. package/dist/effect.d.mts +12 -0
  26. package/dist/effect.mjs +1 -0
  27. package/dist/errors.d.mts +2 -1
  28. package/dist/errors.mjs +1 -1
  29. package/dist/errors.mjs.map +1 -1
  30. package/dist/idempotency.d.mts +2 -0
  31. package/dist/idempotency.mjs +1 -0
  32. package/dist/modules/contracts/contract.d.mts +70 -0
  33. package/dist/modules/contracts/contract.mjs +2 -0
  34. package/dist/modules/contracts/contract.mjs.map +1 -0
  35. package/dist/modules/contracts/convex.d.mts +22 -0
  36. package/dist/modules/contracts/convex.mjs +2 -0
  37. package/dist/modules/contracts/convex.mjs.map +1 -0
  38. package/dist/modules/contracts/errors.d.mts +44 -0
  39. package/dist/modules/contracts/errors.mjs +2 -0
  40. package/dist/modules/contracts/errors.mjs.map +1 -0
  41. package/dist/modules/contracts/exposure.d.mts +105 -0
  42. package/dist/modules/contracts/exposure.mjs +2 -0
  43. package/dist/modules/contracts/exposure.mjs.map +1 -0
  44. package/dist/modules/contracts/idempotency.d.mts +87 -0
  45. package/dist/modules/contracts/idempotency.mjs +2 -0
  46. package/dist/modules/contracts/idempotency.mjs.map +1 -0
  47. package/dist/modules/contracts/standard.d.mts +16 -0
  48. package/dist/modules/contracts/standard.mjs +2 -0
  49. package/dist/modules/contracts/standard.mjs.map +1 -0
  50. package/dist/modules/contracts/workflow.d.mts +167 -0
  51. package/dist/modules/contracts/workflow.mjs +2 -0
  52. package/dist/modules/contracts/workflow.mjs.map +1 -0
  53. package/dist/modules/effect/agent-tools.d.mts +2 -0
  54. package/dist/modules/effect/agent-tools.mjs +1 -0
  55. package/dist/modules/effect/api-runtime.d.mts +28 -0
  56. package/dist/modules/effect/api-runtime.mjs +2 -0
  57. package/dist/modules/effect/api-runtime.mjs.map +1 -0
  58. package/dist/modules/effect/api-zod.d.mts +35 -0
  59. package/dist/modules/effect/api-zod.mjs +2 -0
  60. package/dist/modules/effect/api-zod.mjs.map +1 -0
  61. package/dist/modules/effect/api.d.mts +21 -0
  62. package/dist/modules/effect/api.mjs +2 -0
  63. package/dist/modules/effect/api.mjs.map +1 -0
  64. package/dist/modules/effect/client.d.mts +11 -0
  65. package/dist/modules/effect/client.mjs +1 -0
  66. package/dist/modules/effect/command.d.mts +59 -0
  67. package/dist/modules/effect/command.mjs +2 -0
  68. package/dist/modules/effect/command.mjs.map +1 -0
  69. package/dist/modules/effect/crud.d.mts +785 -0
  70. package/dist/modules/effect/crud.mjs +2 -0
  71. package/dist/modules/effect/crud.mjs.map +1 -0
  72. package/dist/modules/effect/errors.d.mts +8 -0
  73. package/dist/modules/effect/errors.mjs +2 -0
  74. package/dist/modules/effect/errors.mjs.map +1 -0
  75. package/dist/modules/effect/foundation.d.mts +21 -0
  76. package/dist/modules/effect/foundation.mjs +2 -0
  77. package/dist/modules/effect/foundation.mjs.map +1 -0
  78. package/dist/modules/effect/idempotency.d.mts +31 -0
  79. package/dist/modules/effect/idempotency.mjs +2 -0
  80. package/dist/modules/effect/idempotency.mjs.map +1 -0
  81. package/dist/modules/effect/normalize.d.mts +10 -0
  82. package/dist/modules/effect/normalize.mjs +2 -0
  83. package/dist/modules/effect/normalize.mjs.map +1 -0
  84. package/dist/modules/effect/operation.d.mts +141 -0
  85. package/dist/modules/effect/operation.mjs +2 -0
  86. package/dist/modules/effect/operation.mjs.map +1 -0
  87. package/dist/modules/effect/query.d.mts +65 -0
  88. package/dist/modules/effect/query.mjs +2 -0
  89. package/dist/modules/effect/query.mjs.map +1 -0
  90. package/dist/modules/effect/schema.d.mts +33 -0
  91. package/dist/modules/effect/schema.mjs +2 -0
  92. package/dist/modules/effect/schema.mjs.map +1 -0
  93. package/dist/modules/effect/workflow.d.mts +77 -0
  94. package/dist/modules/effect/workflow.mjs +2 -0
  95. package/dist/modules/effect/workflow.mjs.map +1 -0
  96. package/dist/node_modules/@standard-schema/spec/dist/index.d.mts +79 -0
  97. package/dist/workflow.d.mts +2 -0
  98. package/dist/workflow.mjs +1 -0
  99. package/docs/auth.md +65 -0
  100. package/docs/commands.md +118 -0
  101. package/docs/domain-operations.md +615 -0
  102. package/docs/effect.md +384 -0
  103. package/package.json +33 -1
  104. package/scripts/convex-smoke-env.mjs +33 -0
  105. package/scripts/smoke-packed-components.mjs +9 -2
  106. package/scripts/smoke-packed-effect.mjs +439 -0
  107. package/scripts/test-packed-effect.mjs +457 -0
  108. package/scripts/test-packed-ids.mjs +1 -0
package/docs/effect.md ADDED
@@ -0,0 +1,384 @@
1
+ # Effect domain commands and queries
2
+
3
+ `cvx-kit/effect` adds cohesive command/query declarations and a shared API
4
+ runner. Handlers can return ordinary values, Promises, or Effects. Each
5
+ registry returns a lazy Effect from `exec(operation, input, host)`; the API
6
+ adapter provides request services and runs it once.
7
+
8
+ Install Effect explicitly when opting in:
9
+
10
+ ```sh
11
+ bun add effect@4.0.1
12
+ ```
13
+
14
+ The optional peer range is `>=4.0.1 <5`. Existing imports and Promise-based
15
+ Foundation consumers work without Effect installed. Use the public facade;
16
+ the internal module paths are not supported imports.
17
+
18
+ ## A complete domain-to-API example
19
+
20
+ These four TypeScript sections form one module and are compiled together
21
+ against the packed public package. The example uses Convex's generic builders
22
+ with a concrete schema so it is standalone. In an app, put the schema in
23
+ `convex/schema.ts`, import your generated `query`, `mutation`, `QueryCtx`, and
24
+ `MutationCtx` from `./_generated/server`, and split the remaining code into
25
+ services, domain registries, shared API setup, and endpoint modules.
26
+
27
+ The API obtains the actor from authenticated Convex identity. Caller arguments
28
+ contain only a document key and title. Both services constrain document reads
29
+ and writes by that actor; the application should also enforce its own roles
30
+ and tenant policies. The small permission checker below only requires a
31
+ nonempty trusted actor.
32
+
33
+ ### Contracts and injectable capabilities
34
+
35
+ The reader has no write methods. The mutation provider will add the writer;
36
+ a native query cannot satisfy a command's writer requirement. Expected errors
37
+ are values in the Effect error channel, rather than thrown exceptions.
38
+
39
+ ```ts
40
+ import { Context, Effect } from 'effect'
41
+ import {
42
+ defineSchema,
43
+ defineTable,
44
+ queryGeneric,
45
+ mutationGeneric,
46
+ type DataModelFromSchemaDefinition,
47
+ type QueryBuilder,
48
+ type MutationBuilder,
49
+ type GenericQueryCtx,
50
+ type GenericMutationCtx,
51
+ } from 'convex/server'
52
+ import { ConvexError, v } from 'convex/values'
53
+ import { z } from 'zod'
54
+ import { createEffectFoundation, effectApiBuilder } from 'cvx-kit/effect'
55
+ import type { AuditEntryInput } from 'cvx-kit/components/foundation'
56
+
57
+ export const schema = defineSchema({
58
+ documents: defineTable({ key: v.string(), owner: v.string(), title: v.string() }).index(
59
+ 'by_owner_key',
60
+ ['owner', 'key'],
61
+ ),
62
+ audits: defineTable({ actor: v.string(), operation: v.string(), documentKey: v.string() }),
63
+ })
64
+ type DataModel = DataModelFromSchemaDefinition<typeof schema>
65
+ type QueryCtx = GenericQueryCtx<DataModel>
66
+ type MutationCtx = GenericMutationCtx<DataModel>
67
+ const nativeQuery: QueryBuilder<DataModel, 'public'> = queryGeneric
68
+ const nativeMutation: MutationBuilder<DataModel, 'public'> = mutationGeneric
69
+
70
+ type Host = { actor: string }
71
+ class DocumentMissing {
72
+ readonly _tag = 'DocumentMissing'
73
+ }
74
+ class RenameRejected {
75
+ readonly _tag = 'RenameRejected'
76
+ }
77
+ class AccessDenied {
78
+ readonly _tag = 'AccessDenied'
79
+ }
80
+ class DocumentReader extends Context.Service<
81
+ DocumentReader,
82
+ { title: (key: string, actor: string) => Effect.Effect<string, DocumentMissing> }
83
+ >()('DocumentReader') {}
84
+ class DocumentWriter extends Context.Service<
85
+ DocumentWriter,
86
+ {
87
+ rename: (key: string, title: string, actor: string) => Effect.Effect<void, DocumentMissing>
88
+ audit: (actor: string, entry: AuditEntryInput) => Effect.Effect<void>
89
+ }
90
+ >()('DocumentWriter') {}
91
+ ```
92
+
93
+ ### Cohesive domain declarations
94
+
95
+ `label` is an ordinary async handler, and `preview` uses a generator. `title` uses `Effect.fn` with explicit input
96
+ and host annotations. `rename` returns an `Effect.gen` program, yields the
97
+ query directly, then uses the injected writer. Its pipe converts the query's
98
+ missing-document error into a domain-specific rename rejection before adding
99
+ a span. The audit entry sits beside the command, and the shared audit policy
100
+ uses the same invocation's writer.
101
+
102
+ ```ts
103
+ const foundation = createEffectFoundation({
104
+ observability: {
105
+ enabled: false,
106
+ classifyError: () => ({ outcome: 'failed' as const, errorCode: 'DOCUMENT_OPERATION_FAILED' }),
107
+ },
108
+ checkPermission: (host: Host) =>
109
+ host.actor.length > 0 ? Effect.void : Effect.fail(new AccessDenied()),
110
+ writeAudit: (host: Host, entry: AuditEntryInput) =>
111
+ Effect.gen(function* () {
112
+ const writer = yield* DocumentWriter
113
+ yield* writer.audit(host.actor, entry)
114
+ }),
115
+ })
116
+ const keyInput = z.object({ key: z.string().min(1) })
117
+ const renameInput = keyInput.extend({ title: z.string().trim().min(1) })
118
+ const titleResult = z.string().transform((title) => ({ title }))
119
+
120
+ export const documents = foundation.Query({
121
+ context: (host: Host) => host,
122
+ operations: ({ query }) => ({
123
+ label: query({
124
+ input: keyInput,
125
+ result: z.string(),
126
+ handler: async (input) => `Document ${input.key}`,
127
+ }),
128
+ preview: query({
129
+ input: keyInput,
130
+ result: titleResult,
131
+ permission: 'documents:read',
132
+ handler: (input, host) =>
133
+ Effect.gen(function* () {
134
+ const reader = yield* DocumentReader
135
+ return yield* reader.title(input.key, host.actor)
136
+ }),
137
+ }),
138
+ title: query({
139
+ input: keyInput,
140
+ result: titleResult,
141
+ permission: 'documents:read',
142
+ handler: Effect.fn('documents.title')(function* (
143
+ input: z.output<typeof keyInput>,
144
+ host: Host,
145
+ ) {
146
+ const reader = yield* DocumentReader
147
+ return yield* reader.title(input.key, host.actor)
148
+ }),
149
+ }),
150
+ }),
151
+ })
152
+ export const documentCommands = foundation.Command({
153
+ context: (host: Host) => host,
154
+ operations: ({ command }) => ({
155
+ rename: command({
156
+ input: renameInput,
157
+ result: titleResult,
158
+ classification: 'business',
159
+ permission: 'documents:write',
160
+ handler: (input, host) =>
161
+ Effect.gen(function* () {
162
+ // A nested query stays in the same Effect; no inner runner.
163
+ yield* documents.exec('title', { key: input.key }, host)
164
+ const writer = yield* DocumentWriter
165
+ yield* writer.rename(input.key, input.title, host.actor)
166
+ return input.title
167
+ }).pipe(
168
+ Effect.catchTag('DocumentMissing', () => Effect.fail(new RenameRejected())),
169
+ Effect.withSpan('documents.rename'),
170
+ ),
171
+ guard: (host) => (host.actor.length > 0 ? Effect.void : Effect.fail(new AccessDenied())),
172
+ middleware: ({ next }) => next().pipe(Effect.withSpan('documents.command')),
173
+ audit: ({ command: input, result }, host) => ({
174
+ actorId: host.actor,
175
+ operation: 'documents.rename',
176
+ aggregate: { type: 'document', id: input.key },
177
+ metadata: { title: result.title },
178
+ }),
179
+ }),
180
+ }),
181
+ })
182
+ ```
183
+
184
+ ### Request services and shared API setup
185
+
186
+ Construct the services once in each shared builder provider. Endpoint handlers
187
+ do not call `Context.make`, `Effect.runPromise`, or an extra execution helper.
188
+ The database Promise wrappers below intentionally preserve unexpected database
189
+ failures as defects. For external integrations with expected failures, use
190
+ `Effect.tryPromise` and return your own typed error.
191
+
192
+ `mapError` projects an expected failure to a small `ConvexError` payload. It
193
+ receives `unknown`, so narrow it before projection. Defects, interruption,
194
+ and combined causes do not become expected failures through this hook; the
195
+ adapter preserves those failure causes. The registered function still fails,
196
+ which is required for mutation rollback.
197
+
198
+ ```ts
199
+ function readerFor(ctx: QueryCtx): Context.Context<DocumentReader> {
200
+ return Context.make(DocumentReader, {
201
+ title: (key, actor) =>
202
+ Effect.gen(function* () {
203
+ const document = yield* Effect.promise(() =>
204
+ ctx.db
205
+ .query('documents')
206
+ .withIndex('by_owner_key', (q) => q.eq('owner', actor).eq('key', key))
207
+ .unique(),
208
+ )
209
+ if (!document) return yield* Effect.fail(new DocumentMissing())
210
+ return document.title
211
+ }),
212
+ })
213
+ }
214
+ function writerFor(ctx: MutationCtx): Context.Context<DocumentWriter> {
215
+ return Context.make(DocumentWriter, {
216
+ rename: (key, title, actor) =>
217
+ Effect.gen(function* () {
218
+ const document = yield* Effect.promise(() =>
219
+ ctx.db
220
+ .query('documents')
221
+ .withIndex('by_owner_key', (q) => q.eq('owner', actor).eq('key', key))
222
+ .unique(),
223
+ )
224
+ if (!document) return yield* Effect.fail(new DocumentMissing())
225
+ yield* Effect.promise(() => ctx.db.patch(document._id, { title }))
226
+ }),
227
+ audit: (actor, entry) =>
228
+ Effect.promise(() =>
229
+ ctx.db.insert('audits', {
230
+ actor,
231
+ operation: entry.operation,
232
+ documentKey: entry.aggregate.id,
233
+ }),
234
+ ).pipe(Effect.asVoid),
235
+ })
236
+ }
237
+ function authenticatedHost(ctx: QueryCtx) {
238
+ return Effect.gen(function* () {
239
+ const identity = yield* Effect.promise(() => ctx.auth.getUserIdentity())
240
+ if (!identity) return yield* Effect.fail(new AccessDenied())
241
+ return { actor: identity.tokenIdentifier }
242
+ })
243
+ }
244
+ function projectExpectedError(error: unknown) {
245
+ if (error instanceof DocumentMissing || error instanceof RenameRejected)
246
+ return new ConvexError({ code: 'DOCUMENT_NOT_FOUND' })
247
+ if (error instanceof AccessDenied) return new ConvexError({ code: 'ACCESS_DENIED' })
248
+ return new Error('Document operation failed', { cause: error })
249
+ }
250
+ const queryApi = effectApiBuilder(nativeQuery, {
251
+ services: readerFor,
252
+ mapError: projectExpectedError,
253
+ })
254
+ const mutationApi = effectApiBuilder(nativeMutation, {
255
+ services: (ctx) => Context.merge(readerFor(ctx), writerFor(ctx)),
256
+ mapError: projectExpectedError,
257
+ })
258
+ ```
259
+
260
+ ### Consume the domain operations in the API
261
+
262
+ The Convex callback remains `(ctx, input)`; the domain callback and executor
263
+ use input before context/host. Both endpoints are normal native registrations
264
+ with argument and return validators. Each handler authenticates the request
265
+ and returns the domain Effect. The adapter supplies the services required by
266
+ the complete nested program.
267
+
268
+ ```ts
269
+ export const documentTitle = queryApi({
270
+ args: { key: v.string() },
271
+ returns: v.object({ title: v.string() }),
272
+ handler: (ctx, input) =>
273
+ Effect.gen(function* () {
274
+ const host = yield* authenticatedHost(ctx)
275
+ return yield* documents.exec('title', input, host)
276
+ }),
277
+ })
278
+ export const renameDocument = mutationApi({
279
+ args: { key: v.string(), title: v.string() },
280
+ returns: v.object({ title: v.string() }),
281
+ handler: (ctx, input) =>
282
+ Effect.gen(function* () {
283
+ const host = yield* authenticatedHost(ctx)
284
+ return yield* documentCommands.exec('rename', input, host)
285
+ }),
286
+ })
287
+ ```
288
+
289
+ ## Types and schema boundaries
290
+
291
+ The command accepts `z.input<typeof renameInput>`. Its handler sees
292
+ `z.output<typeof renameInput>`, so the title has already been trimmed. The
293
+ handler returns a string, which is the input of `titleResult`; the registry's
294
+ final result is `{ title: string }`, the output of that transform. Middleware
295
+ `next()` returns the handler result before the final result parser, so wrap
296
+ or transform that value according to the result schema's input contract.
297
+
298
+ TypeScript preserves the selected operation's success, expected error, and
299
+ service requirements. The title query requires `DocumentReader`; rename
300
+ requires `DocumentReader | DocumentWriter` and can fail with `AccessDenied`
301
+ or `RenameRejected`. The API builder checks that its provider supplies those
302
+ services. A reader-only provider cannot register rename, and an incompatible
303
+ handler result cannot pass its declared native return validator.
304
+
305
+ This is server-side Effect composition. Generated Convex references retain
306
+ native argument/result types and visibility; they do not carry Effect's typed
307
+ error or service channels over the transport. Clients receive ordinary Convex
308
+ results or errors.
309
+
310
+ For inferred declarations, put `handler` and `guard` before `middleware`, as
311
+ above. Middleware-first declarations need explicit callback and error/service
312
+ annotations where inference cannot recover the later handler's channels.
313
+ `Effect.fn` accepts typed function arguments; `Effect.gen` accepts a generator
314
+ whose yielded Effects determine its channels. Use `(input, context) =>
315
+ Effect.gen(function* () { ... })` when those values come from the declaration.
316
+ A constructed `Effect.gen` program cannot itself receive handler arguments.
317
+
318
+ ## Hooks, replay, and execution scope
319
+
320
+ A command runs input parsing, observation setup and context resolution, then
321
+ permission checking, preparation, middleware, shared and operation guards,
322
+ handler, final result parsing, audit construction, aggregate checking, audit
323
+ writing, and completion. Observation records the outcome. Query permission
324
+ checking runs before its context resolver, followed by middleware, guards,
325
+ handler, and result parsing. Command audit/completion hooks do not exist on
326
+ queries.
327
+
328
+ Optional command `prepare(context, input)` returns `{ kind: 'execute',
329
+ complete }` or `{ kind: 'replay', result }`. The completion callback receives
330
+ the parsed final result. Permission checking still precedes replay. A replay
331
+ result is parsed with `replayResult` when supplied, otherwise with `result`,
332
+ and skips middleware, guards, handler, audit writing, and completion.
333
+ Middleware receives `operation`, parsed `input` (also `command`), `context`,
334
+ and `next`; it may provide an enriched context to `next({ context })`.
335
+ Each execution allows `next` once. Audit construction sees the original domain
336
+ context, and the audit writer sees the original trusted host.
337
+
338
+ The provider may return a `Context`, a Promise of a Context, or an Effect that
339
+ builds a Context. It runs separately for every native invocation. The shared
340
+ runner opens an Effect scope, evaluates the provider and handler, and awaits
341
+ scope finalizers before returning or throwing. Use `Effect.acquireRelease`
342
+ inside a provider for resources that actually need cleanup. Nested domain
343
+ operations share that runner and scope; do not run them independently inside
344
+ handlers. Cleanup failure remains an endpoint failure, including when it
345
+ combines with a handler failure.
346
+
347
+ ## Convex boundaries and observed support
348
+
349
+ A domain query is a reusable read operation; its name does not choose the
350
+ native function kind. A native Convex query must perform deterministic database
351
+ reads. Native mutations can write, and a failed command, result validator,
352
+ audit writer, completion hook, or awaited cleanup rolls back that mutation's
353
+ writes. Actions are the place for HTTP and other external integrations.
354
+ An action's separate `ctx.runMutation` calls are separate transactions: a
355
+ later failure does not undo an earlier successful mutation.
356
+
357
+ The packed public facade was compiled and executed under npm and Bun with
358
+ Effect 4.0.1 and `convex-helpers` 0.1.123. Legacy packed consumers also ran with
359
+ Effect absent. Local Convex 1.45.0 checks exercised ordinary, generator, and
360
+ named-function query composition; request cleanup; expected and combined
361
+ failures; mutation validation and lifecycle rollback; and an action using
362
+ loopback HTTP with separate mutation transactions. `Effect.withSpan` was
363
+ accepted in those programs; this does not establish exported telemetry,
364
+ background fibers, arbitrary concurrency, or every Effect runtime capability
365
+ in Convex.
366
+
367
+ The standalone packed compiler includes `ESNext.Disposable` in its library
368
+ set for Effect 4 declarations. If your project's TypeScript environment does
369
+ not already provide those globals, include that library as well.
370
+
371
+ ## Adopt one domain at a time
372
+
373
+ Keep existing Promise registries and API endpoints while adding the optional
374
+ facade for a new domain. Move each operation's handler beside its schemas and
375
+ hooks, switch domain callbacks to input-first arguments, and return the new
376
+ registry's lazy Effect to a shared API adapter. A normal async handler works
377
+ in the cohesive declaration without a generator; wrap expected Promise failures
378
+ only when you want a typed error channel.
379
+
380
+ The [command migration guide](./commands.md) shows a complete legacy-to-Effect
381
+ pair and the existing lifecycle contract. The [authentication guide](./auth.md)
382
+ shows how to wrap trusted custom builders with `effectZodApiBuilder`, so auth,
383
+ tenancy, validators, row-level policies, and triggers run through their
384
+ existing builder before Effect request services are provisioned.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cvx-kit",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Reusable Convex application kit: foundation kernels, approvals component, zod table boundaries, WorkOS-backed auth function constructors, and audited command protocol.",
5
5
  "keywords": [
6
6
  "component",
@@ -30,6 +30,22 @@
30
30
  "types": "./dist/zod-table.d.mts",
31
31
  "import": "./dist/zod-table.mjs"
32
32
  },
33
+ "./effect": {
34
+ "types": "./dist/effect.d.mts",
35
+ "import": "./dist/effect.mjs"
36
+ },
37
+ "./contracts": {
38
+ "types": "./dist/contracts.d.mts",
39
+ "import": "./dist/contracts.mjs"
40
+ },
41
+ "./idempotency": {
42
+ "types": "./dist/idempotency.d.mts",
43
+ "import": "./dist/idempotency.mjs"
44
+ },
45
+ "./workflow": {
46
+ "types": "./dist/workflow.d.mts",
47
+ "import": "./dist/workflow.mjs"
48
+ },
33
49
  "./auth": {
34
50
  "types": "./dist/auth.d.mts",
35
51
  "import": "./dist/auth.mjs"
@@ -116,6 +132,8 @@
116
132
  "test:package": "bun run build && vp test run test/package-contents.test.mjs",
117
133
  "test:package:ids": "bun run build && node scripts/test-packed-ids.mjs",
118
134
  "test:package:oxlint": "bun run build && node scripts/test-packed-oxlint.mjs",
135
+ "test:package:effect": "bun run build && node scripts/test-packed-effect.mjs",
136
+ "test:package:smoke:effect": "bun run build && node scripts/smoke-packed-effect.mjs",
119
137
  "test:package:smoke:bun": "bun scripts/smoke-packed-components.mjs bun",
120
138
  "test:package:smoke:npm": "node scripts/smoke-packed-components.mjs npm",
121
139
  "docs:sync": "rm -f docs/*.md && cp src/docs/*.md docs/",
@@ -137,6 +155,7 @@
137
155
  "convex": "1.45.0",
138
156
  "convex-helpers": "0.1.124",
139
157
  "convex-test": "^0.0.56",
158
+ "effect": "4.0.1",
140
159
  "oxlint": "1.79.0",
141
160
  "publint": "^0.3.24",
142
161
  "typescript": "7.0.2",
@@ -146,6 +165,19 @@
146
165
  "peerDependencies": {
147
166
  "convex": "^1.45.0",
148
167
  "convex-helpers": ">=0.1.123 <0.2.0",
168
+ "effect": ">=4.0.1 <5",
149
169
  "zod": "^4.5.4"
170
+ },
171
+ "peerDependenciesMeta": {
172
+ "effect": {
173
+ "optional": true
174
+ }
175
+ },
176
+ "devEngines": {
177
+ "packageManager": {
178
+ "name": "bun",
179
+ "version": "1.4.2",
180
+ "onFail": "download"
181
+ }
150
182
  }
151
183
  }
@@ -0,0 +1,33 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ /** A smoke fixture must never inherit a cloud or self-hosted deployment selection. */
6
+ export function localConvexEnvironment(source = process.env) {
7
+ const env = Object.fromEntries(
8
+ Object.entries(source).filter(
9
+ ([key]) =>
10
+ !key.startsWith('CONVEX_') && key !== 'NEXT_PUBLIC_CONVEX_URL' && key !== 'VITE_CONVEX_URL',
11
+ ),
12
+ )
13
+ return { ...env, CONVEX_AGENT_MODE: 'anonymous' }
14
+ }
15
+
16
+ /** Convex 1.45 can fall back to a legacy home deployment with the same agent name. */
17
+ export function assertIsolatedConvexFixture(
18
+ fixture,
19
+ legacyState = join(homedir(), '.convex', 'anonymous-convex-backend-state', 'anonymous-agent'),
20
+ ) {
21
+ const localConfig = join(fixture, '.convex', 'local', 'default', 'config.json')
22
+ if (!existsSync(localConfig) && existsSync(legacyState)) {
23
+ throw new Error(
24
+ 'Local smoke cannot reuse legacy anonymous-agent state; an isolated fixture is required',
25
+ )
26
+ }
27
+ }
28
+
29
+ export function assertLocalConvexState(fixture) {
30
+ if (!existsSync(join(fixture, '.convex', 'local', 'default', 'config.json'))) {
31
+ throw new Error('Local smoke did not create project-local deployment state')
32
+ }
33
+ }
@@ -2,6 +2,11 @@ import { execFileSync } from 'node:child_process'
2
2
  import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
+ import {
6
+ assertIsolatedConvexFixture,
7
+ assertLocalConvexState,
8
+ localConvexEnvironment,
9
+ } from './convex-smoke-env.mjs'
5
10
 
6
11
  const installer = process.argv[2] ?? 'bun'
7
12
  if (installer !== 'bun' && installer !== 'npm') {
@@ -20,7 +25,7 @@ function run(command, args, cwd = fixture) {
20
25
  cwd,
21
26
  encoding: 'utf8',
22
27
  stdio: ['ignore', 'pipe', 'pipe'],
23
- env: { ...process.env, CONVEX_AGENT_MODE: 'anonymous' },
28
+ env: localConvexEnvironment(),
24
29
  })
25
30
  }
26
31
 
@@ -28,7 +33,7 @@ function runStreaming(command, args, cwd = fixture) {
28
33
  execFileSync(command, args, {
29
34
  cwd,
30
35
  stdio: 'inherit',
31
- env: { ...process.env, CONVEX_AGENT_MODE: 'anonymous' },
36
+ env: localConvexEnvironment(),
32
37
  })
33
38
  }
34
39
 
@@ -247,7 +252,9 @@ export const status = query({
247
252
  runStreaming('npx', ['vp', 'test', 'run', 'packed-test-helper.test.ts']),
248
253
  )
249
254
 
255
+ assertIsolatedConvexFixture(fixture)
250
256
  step('deploy Convex fixture', () => runConvex('dev', '--once', '--typecheck=disable'))
257
+ assertLocalConvexState(fixture)
251
258
  const health = JSON.parse(runConvex('run', 'smoke:health'))
252
259
  if (health.status !== 'ready' || health.schemaVersion !== 1) {
253
260
  throw new Error(`Unexpected approvals health response: ${JSON.stringify(health)}`)