cvx-kit 0.0.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +14 -10
  3. package/ROADMAP.md +126 -0
  4. package/dist/agent-tools.d.mts +45 -0
  5. package/dist/agent-tools.mjs +2 -0
  6. package/dist/agent-tools.mjs.map +1 -0
  7. package/dist/auth.d.mts +9 -1
  8. package/dist/auth.mjs +1 -1
  9. package/dist/auth.mjs.map +1 -1
  10. package/dist/components/approvals/_generated/api.d.mts +2 -0
  11. package/dist/components/approvals/_generated/api.mjs.map +1 -1
  12. package/dist/components/approvals/_generated/component.d.mts +7 -0
  13. package/dist/components/approvals/_generated/dataModel.d.mts +1 -1
  14. package/dist/components/approvals/client.d.mts +4 -4
  15. package/dist/components/approvals/client.mjs +1 -1
  16. package/dist/components/approvals/client.mjs.map +1 -1
  17. package/dist/components/approvals/constants.d.mts +8 -2
  18. package/dist/components/approvals/constants.mjs +1 -1
  19. package/dist/components/approvals/constants.mjs.map +1 -1
  20. package/dist/components/approvals/decisions.mjs +1 -1
  21. package/dist/components/approvals/decisions.mjs.map +1 -1
  22. package/dist/components/approvals/health.d.mts +15 -0
  23. package/dist/components/approvals/health.mjs +2 -0
  24. package/dist/components/approvals/health.mjs.map +1 -0
  25. package/dist/components/approvals/requests.mjs +1 -1
  26. package/dist/components/approvals/requests.mjs.map +1 -1
  27. package/dist/components/approvals/schema.js +1 -0
  28. package/dist/components/approvals/validators.d.mts +119 -2
  29. package/dist/components/approvals/validators.mjs +1 -1
  30. package/dist/components/approvals/validators.mjs.map +1 -1
  31. package/dist/components/approvals/workflow_steps.d.mts +2 -25
  32. package/dist/components/approvals/workflow_steps.mjs.map +1 -1
  33. package/dist/components/foundation/_generated/dataModel.d.mts +1 -1
  34. package/dist/components/foundation/client.d.mts +26 -9
  35. package/dist/components/foundation/client.mjs.map +1 -1
  36. package/dist/components/foundation/modules/query/query.d.mts +19 -8
  37. package/dist/components/foundation/modules/query/query.mjs.map +1 -1
  38. package/dist/components/foundation/{schema.mjs → schema.js} +0 -1
  39. package/dist/crud.d.mts +79 -0
  40. package/dist/crud.mjs +2 -0
  41. package/dist/crud.mjs.map +1 -0
  42. package/dist/index.d.mts +9 -4
  43. package/dist/index.mjs +1 -1
  44. package/dist/middleware.d.mts +46 -0
  45. package/dist/middleware.mjs +2 -0
  46. package/dist/middleware.mjs.map +1 -0
  47. package/dist/state-machine.d.mts +18 -0
  48. package/dist/state-machine.mjs +2 -0
  49. package/dist/state-machine.mjs.map +1 -0
  50. package/dist/tenancy.d.mts +1 -1
  51. package/dist/webhooks.d.mts +164 -0
  52. package/dist/webhooks.mjs +2 -0
  53. package/dist/webhooks.mjs.map +1 -0
  54. package/dist/zod-table.d.mts +34 -3
  55. package/dist/zod-table.mjs +1 -1
  56. package/dist/zod-table.mjs.map +1 -1
  57. package/docs/agent-tools.md +41 -0
  58. package/docs/approvals.md +50 -2
  59. package/docs/crud.md +62 -0
  60. package/docs/llms.md +8 -1
  61. package/docs/plans/2026-08-14-001-feat-v0.1.0-feature-set-plan.md +461 -0
  62. package/docs/upgrading.md +175 -0
  63. package/docs/webhooks.md +58 -0
  64. package/package.json +25 -2
  65. package/scripts/convex-config-bridge.mjs +41 -3
  66. package/scripts/extract-changelog.mjs +29 -0
  67. package/scripts/smoke-packed-components.mjs +374 -0
  68. package/src/test.ts +2 -2
  69. package/dist/components/approvals/schema.mjs +0 -2
  70. package/dist/components/approvals/schema.mjs.map +0 -1
  71. package/dist/components/foundation/schema.mjs.map +0 -1
  72. /package/dist/components/approvals/{schema.d.mts → schema.d.ts} +0 -0
  73. /package/dist/components/foundation/{schema.d.mts → schema.d.ts} +0 -0
@@ -0,0 +1,58 @@
1
+ # `cvx-kit/webhooks` — the sanctioned webhook boundary
2
+
3
+ Webhooks are the one boundary that genuinely needs deduplication (Convex's
4
+ client gives exactly-once for its own mutations; external providers redeliver).
5
+ The helper encodes the pattern: **verify over the raw body (fail closed) →
6
+ derive the natural key → delegate to an internal mutation whose first act is
7
+ transactional dedup**.
8
+
9
+ ## Wiring
10
+
11
+ ```ts
12
+ // schema — host-owned dedup table (the kit gains no tables):
13
+ webhookEvents: webhookEventsTable().table.index('by_eventKey', ['eventKey'])
14
+
15
+ // boundary — used inside the host's httpAction:
16
+ const boundary = createWebhookBoundary({
17
+ // RAW body, BEFORE parsing. Constant-time comparison (crypto.subtle HMAC
18
+ // verify), secret from a Convex environment variable — never in code.
19
+ verify: async (raw, request) => verifyVendorSignature(raw, request.headers),
20
+ // The natural-key pattern:
21
+ eventKey: (raw) => {
22
+ const event = JSON.parse(raw)
23
+ return `${event.type}:${event.id}:${event.updatedAt}`
24
+ },
25
+ source: 'vendor',
26
+ })
27
+
28
+ // http.ts — routing only:
29
+ http.route({ path: '/webhooks/vendor', method: 'POST',
30
+ handler: httpAction((ctx, request) =>
31
+ boundary.handle(ctx, request, internal.<module>.functions.applyVendorEvent)) })
32
+
33
+ // the target — a systemMutation; dedup FIRST, transactionally:
34
+ export const applyVendorEvent = systemMutation({
35
+ args: { eventKey: z.string(), payload: z.string(), source: z.string() },
36
+ handler: async (ctx, args) => {
37
+ const { duplicate } = await recordWebhookEvent(ctx, { key: args.eventKey, source: args.source })
38
+ if (duplicate) return null
39
+ const payload = JSON.parse(args.payload) // parse INSIDE the trusted path
40
+ // ...apply, via domain executors
41
+ return null
42
+ },
43
+ })
44
+ ```
45
+
46
+ ## Rules
47
+
48
+ 1. **Dedup lives in the mutation, never the action.** The action can race;
49
+ the mutation's insert-if-absent is transactional.
50
+ 2. **Verification is over raw bytes.** Re-serialized/parsed bodies break or
51
+ weaken HMACs; the helper hands `verify` the raw `request.text()`.
52
+ 3. **Fail closed.** `verify` returning false OR throwing both reject with
53
+ `WEBHOOK_SIGNATURE_INVALID`, before any delegation.
54
+ 4. **Retention is yours.** Dedup rows grow forever unless pruned — run a
55
+ cleanup cron over `receivedAt` with a window comfortably longer than the
56
+ provider's redelivery horizon (days, not hours).
57
+ 5. Providers with signed timestamps (Stripe-style): also enforce a tolerance
58
+ window inside `verify`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cvx-kit",
3
- "version": "0.0.8",
3
+ "version": "0.1.1",
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
  "type": "module",
6
6
  "license": "MIT",
@@ -23,6 +23,26 @@
23
23
  "types": "./dist/tenancy.d.mts",
24
24
  "import": "./dist/tenancy.mjs"
25
25
  },
26
+ "./state-machine": {
27
+ "types": "./dist/state-machine.d.mts",
28
+ "import": "./dist/state-machine.mjs"
29
+ },
30
+ "./crud": {
31
+ "types": "./dist/crud.d.mts",
32
+ "import": "./dist/crud.mjs"
33
+ },
34
+ "./middleware": {
35
+ "types": "./dist/middleware.d.mts",
36
+ "import": "./dist/middleware.mjs"
37
+ },
38
+ "./webhooks": {
39
+ "types": "./dist/webhooks.d.mts",
40
+ "import": "./dist/webhooks.mjs"
41
+ },
42
+ "./agent-tools": {
43
+ "types": "./dist/agent-tools.d.mts",
44
+ "import": "./dist/agent-tools.mjs"
45
+ },
26
46
  "./triggers": {
27
47
  "types": "./dist/triggers.d.mts",
28
48
  "import": "./dist/triggers.mjs"
@@ -70,7 +90,10 @@
70
90
  "scripts": {
71
91
  "build": "vp pack && node scripts/convex-config-bridge.mjs && publint",
72
92
  "typecheck": "tsc --noEmit",
73
- "test": "vp test run",
93
+ "test": "vp test run --exclude test/package-contents.test.mjs",
94
+ "test:package": "bun run build && vp test run test/package-contents.test.mjs",
95
+ "test:package:smoke:bun": "bun scripts/smoke-packed-components.mjs bun",
96
+ "test:package:smoke:npm": "node scripts/smoke-packed-components.mjs npm",
74
97
  "docs:sync": "rm -f docs/*.md && cp src/docs/*.md docs/",
75
98
  "prepublishOnly": "bun run build && bun run typecheck && bun run test && bun run docs:sync",
76
99
  "build:codegen": "convex codegen --component-dir ./src/components/foundation && convex codegen --component-dir ./src/components/approvals && vp pack",
@@ -2,7 +2,13 @@
2
2
  // is literally named `convex.config.js` (see DEFINITION_FILENAME_JS in the
3
3
  // convex CLI). vp pack emits .mjs, so this postbuild step materializes the
4
4
  // compiled definition as convex.config.js in each component's dist directory.
5
- import { readFileSync, writeFileSync } from 'node:fs'
5
+ import {
6
+ readFileSync,
7
+ readdirSync,
8
+ renameSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from 'node:fs'
6
12
  import { dirname, join } from 'node:path'
7
13
  import { fileURLToPath } from 'node:url'
8
14
 
@@ -10,8 +16,40 @@ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
10
16
 
11
17
  for (const component of ['foundation', 'approvals']) {
12
18
  const directory = join(root, 'dist', 'components', component)
13
- const compiled = readFileSync(join(directory, 'convex.config.mjs'), 'utf8')
19
+ const config = readFileSync(join(directory, 'convex.config.mjs'), 'utf8')
14
20
  .replace(/\/\/# sourceMappingURL=.*\n?/, '')
15
- writeFileSync(join(directory, 'convex.config.js'), compiled)
21
+ writeFileSync(join(directory, 'convex.config.js'), config)
22
+
23
+ // Component implementations are discovered from schema.ts/schema.js only.
24
+ // Keeping schema.mjs beside schema.js is also invalid because Convex
25
+ // canonicalizes both extensions to the same module path.
26
+ const schema = readFileSync(join(directory, 'schema.mjs'), 'utf8').replace(
27
+ /\/\/# sourceMappingURL=.*\n?/,
28
+ '',
29
+ )
30
+ writeFileSync(join(directory, 'schema.js'), schema)
31
+ for (const filename of readdirSync(directory)) {
32
+ if (!filename.endsWith('.mjs')) continue
33
+ const path = join(directory, filename)
34
+ const source = readFileSync(path, 'utf8')
35
+ const bridged = source.replaceAll('./schema.mjs', './schema.js')
36
+ if (bridged !== source) writeFileSync(path, bridged)
37
+ }
38
+ renameSync(join(directory, 'schema.d.mts'), join(directory, 'schema.d.ts'))
39
+ const dataModelDeclaration = join(
40
+ directory,
41
+ '_generated',
42
+ 'dataModel.d.mts',
43
+ )
44
+ writeFileSync(
45
+ dataModelDeclaration,
46
+ readFileSync(dataModelDeclaration, 'utf8').replace(
47
+ '../schema.mjs',
48
+ '../schema.js',
49
+ ),
50
+ )
51
+ rmSync(join(directory, 'schema.mjs'))
52
+ rmSync(join(directory, 'schema.mjs.map'))
16
53
  console.log(`bridged dist/components/${component}/convex.config.js`)
54
+ console.log(`bridged dist/components/${component}/schema.js`)
17
55
  }
@@ -0,0 +1,29 @@
1
+ // Extracts one version's section from CHANGELOG.md (Keep a Changelog form).
2
+ // Usage: node scripts/extract-changelog.mjs 0.1.0 [outfile]
3
+ // Used by the release workflow for GitHub Release notes, and runnable
4
+ // locally BEFORE tagging to verify extraction (see the release plan).
5
+ import { readFileSync, writeFileSync } from 'node:fs'
6
+
7
+ const version = process.argv[2]
8
+ if (!version) {
9
+ console.error('usage: extract-changelog.mjs <version> [outfile]')
10
+ process.exit(1)
11
+ }
12
+ const changelog = readFileSync(new URL('../CHANGELOG.md', import.meta.url), 'utf8')
13
+ const pattern = new RegExp(
14
+ String.raw`^## \[${version.replace(/\./g, String.raw`\.`)}\][^\n]*\n([\s\S]*?)(?=^## \[|\Z)`,
15
+ 'm',
16
+ )
17
+ const match = changelog.match(pattern)
18
+ if (!match) {
19
+ console.error(`CHANGELOG.md has no section for version ${version}`)
20
+ process.exit(1)
21
+ }
22
+ const notes = match[1].trim()
23
+ if (!notes) {
24
+ console.error(`CHANGELOG.md section for ${version} is empty`)
25
+ process.exit(1)
26
+ }
27
+ const outfile = process.argv[3]
28
+ if (outfile) writeFileSync(outfile, `${notes}\n`)
29
+ else process.stdout.write(`${notes}\n`)
@@ -0,0 +1,374 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import {
3
+ copyFileSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from 'node:fs'
10
+ import { tmpdir } from 'node:os'
11
+ import { join } from 'node:path'
12
+
13
+ const installer = process.argv[2] ?? 'bun'
14
+ if (installer !== 'bun' && installer !== 'npm') {
15
+ throw new Error('Usage: smoke-packed-components.mjs [bun|npm]')
16
+ }
17
+
18
+ const root = join(import.meta.dirname, '..')
19
+ const temporaryRoot = mkdtempSync(join(tmpdir(), 'cvx-kit-smoke-'))
20
+ const fixture = join(temporaryRoot, 'fixture')
21
+ const packageVersion = JSON.parse(
22
+ readFileSync(join(root, 'package.json'), 'utf8'),
23
+ ).version
24
+ const tarball = join(temporaryRoot, `cvx-kit-${packageVersion}.tgz`)
25
+
26
+ function run(command, args, cwd = fixture) {
27
+ return execFileSync(command, args, {
28
+ cwd,
29
+ encoding: 'utf8',
30
+ stdio: ['ignore', 'pipe', 'pipe'],
31
+ env: { ...process.env, CONVEX_AGENT_MODE: 'anonymous' },
32
+ })
33
+ }
34
+
35
+ function runStreaming(command, args, cwd = fixture) {
36
+ execFileSync(command, args, {
37
+ cwd,
38
+ stdio: 'inherit',
39
+ env: { ...process.env, CONVEX_AGENT_MODE: 'anonymous' },
40
+ })
41
+ }
42
+
43
+ function runConvex(...args) {
44
+ return run('node', [
45
+ join(fixture, 'node_modules', 'convex', 'bin', 'main.js'),
46
+ ...args,
47
+ ])
48
+ }
49
+
50
+ function write(relativePath, contents) {
51
+ const path = join(fixture, relativePath)
52
+ writeFileSync(path, contents)
53
+ }
54
+
55
+ function step(label, callback) {
56
+ console.log(`smoke step: ${label}`)
57
+ return callback()
58
+ }
59
+
60
+ try {
61
+ run(
62
+ 'bun',
63
+ ['pm', 'pack', '--destination', temporaryRoot, '--ignore-scripts'],
64
+ root,
65
+ )
66
+ mkdirSync(join(fixture, 'convex'), { recursive: true })
67
+ write(
68
+ 'package.json',
69
+ JSON.stringify(
70
+ {
71
+ private: true,
72
+ type: 'module',
73
+ dependencies: {
74
+ 'cvx-kit': `file:${tarball}`,
75
+ convex: '1.43.0',
76
+ zod: '4.4.3',
77
+ },
78
+ devDependencies: {
79
+ 'vite-plus': '0.2.8',
80
+ },
81
+ },
82
+ null,
83
+ 2,
84
+ ),
85
+ )
86
+ write(
87
+ 'packed-test-helper.test.ts',
88
+ `import { describe, expect, it } from 'vite-plus/test'
89
+ import { registerApprovals, registerFoundation } from 'cvx-kit/test'
90
+
91
+ describe('packed cvx-kit test helpers', () => {
92
+ it('registers both compiled component bundles and their discoverable schemas', () => {
93
+ const registrations = []
94
+ const testRuntime = {
95
+ registerComponent(name, schema, modules) {
96
+ registrations.push({ name, schema, modules })
97
+ },
98
+ }
99
+
100
+ registerApprovals(testRuntime)
101
+ registerFoundation(testRuntime)
102
+
103
+ expect(registrations.map(({ name }) => name)).toEqual(['approvals', 'foundation'])
104
+ expect(Object.keys(registrations[0].schema.tables).sort()).toEqual([
105
+ 'approvalDecisions',
106
+ 'approvalRuns',
107
+ ])
108
+ expect(Object.keys(registrations[0].modules)).toContain(
109
+ '../dist/components/approvals/health.mjs',
110
+ )
111
+ })
112
+ })
113
+ `,
114
+ )
115
+ write(
116
+ 'convex/convex.config.ts',
117
+ `import { defineApp } from 'convex/server'
118
+ import approvals from 'cvx-kit/components/approvals/convex.config'
119
+ import foundation from 'cvx-kit/components/foundation/convex.config'
120
+
121
+ const app = defineApp()
122
+ app.use(approvals)
123
+ app.use(foundation)
124
+ export default app
125
+ `,
126
+ )
127
+ write(
128
+ 'convex/smoke.ts',
129
+ `import { approvalCallbackArgs } from 'cvx-kit/components/approvals'
130
+ import { createFunctionHandle, paginationOptsValidator } from 'convex/server'
131
+ import { v } from 'convex/values'
132
+ import { components, internal } from './_generated/api'
133
+ import { internalMutation, mutation, query } from './_generated/server'
134
+
135
+ export const applyDecision = internalMutation({
136
+ args: approvalCallbackArgs,
137
+ returns: v.null(),
138
+ handler: (_ctx, input) => {
139
+ if (input.decision?.evidence[0]?.stepKey !== 'releaseDecision') {
140
+ throw new Error('Missing callback decision step key')
141
+ }
142
+ return null
143
+ },
144
+ })
145
+
146
+ export const rejectDecision = internalMutation({
147
+ args: approvalCallbackArgs,
148
+ returns: v.null(),
149
+ handler: () => null,
150
+ })
151
+
152
+ export const health = query({
153
+ args: {},
154
+ returns: v.any(),
155
+ handler: (ctx) => ctx.runQuery(components.approvals.health.check, {}),
156
+ })
157
+
158
+ export const foundationHealth = query({
159
+ args: {},
160
+ returns: v.literal('ready'),
161
+ handler: (ctx) => ctx.runQuery(components.foundation.functions.status, {}),
162
+ })
163
+
164
+ export const start = mutation({
165
+ args: { resourceRef: v.optional(v.string()) },
166
+ returns: v.object({ runId: v.string() }),
167
+ handler: async (ctx, args) => {
168
+ const approvedHandle = await createFunctionHandle(internal.smoke.applyDecision)
169
+ const rejectedHandle = await createFunctionHandle(internal.smoke.rejectDecision)
170
+ return ctx.runMutation(components.approvals.requests.start, {
171
+ scopeRef: 'smoke',
172
+ resourceType: 'release',
173
+ resourceRef: args.resourceRef ?? 'packed-component',
174
+ requester: { actorRef: 'requester', capabilities: [] },
175
+ workflow: {
176
+ schemaVersion: 1,
177
+ compatibilityKey: 'packedSmoke',
178
+ name: 'packedSmoke',
179
+ steps: [
180
+ {
181
+ kind: 'decision',
182
+ key: 'releaseDecision',
183
+ decisions: ['approved', 'rejected'],
184
+ quorum: { kind: 'count', approvals: 1 },
185
+ makerChecker: true,
186
+ },
187
+ {
188
+ kind: 'branch',
189
+ key: 'routeDecision',
190
+ approvedStepKey: 'applyDecision',
191
+ rejectedStepKey: 'rejectDecision',
192
+ },
193
+ {
194
+ kind: 'mutation',
195
+ key: 'applyDecision',
196
+ callback: { kind: 'mutation', handle: approvedHandle, retry: false },
197
+ },
198
+ {
199
+ kind: 'mutation',
200
+ key: 'rejectDecision',
201
+ callback: { kind: 'mutation', handle: rejectedHandle, retry: false },
202
+ },
203
+ ],
204
+ },
205
+ })
206
+ },
207
+ })
208
+
209
+ export const list = query({
210
+ args: {
211
+ paginationOpts: paginationOptsValidator,
212
+ state: v.optional(v.union(v.literal('pending'), v.literal('approved'))),
213
+ },
214
+ returns: v.any(),
215
+ handler: (ctx, args) => ctx.runQuery(components.approvals.requests.list, {
216
+ scopeRef: 'smoke',
217
+ ...args,
218
+ }),
219
+ })
220
+
221
+ export const decide = mutation({
222
+ args: { runId: v.string() },
223
+ returns: v.any(),
224
+ handler: (ctx, args) => ctx.runMutation(components.approvals.decisions.decide, {
225
+ runId: args.runId,
226
+ decision: 'approved',
227
+ actor: { actorRef: 'approver', capabilities: [] },
228
+ compatibilityKey: 'packedSmoke',
229
+ }),
230
+ })
231
+
232
+ export const history = query({
233
+ args: { runId: v.string() },
234
+ returns: v.any(),
235
+ handler: (ctx, args) => ctx.runQuery(components.approvals.decisions.list, args),
236
+ })
237
+
238
+ export const status = query({
239
+ args: { runId: v.string() },
240
+ returns: v.any(),
241
+ handler: (ctx, args) => ctx.runQuery(components.approvals.requests.status, {
242
+ ...args,
243
+ compatibilityKey: 'packedSmoke',
244
+ }),
245
+ })
246
+ `,
247
+ )
248
+
249
+ if (installer === 'bun') {
250
+ copyFileSync(join(root, 'bun.lock'), join(fixture, 'bun.lock'))
251
+ }
252
+ if (installer === 'bun')
253
+ step('install with bun', () =>
254
+ runStreaming('bun', ['install', '--ignore-scripts']),
255
+ )
256
+ else
257
+ step('install with npm', () =>
258
+ runStreaming('npm', ['install', '--ignore-scripts']),
259
+ )
260
+ if (installer === 'bun')
261
+ step('test helper with bun', () =>
262
+ runStreaming('bunx', ['vp', 'test', 'run', 'packed-test-helper.test.ts']),
263
+ )
264
+ else
265
+ step('test helper with npm', () =>
266
+ runStreaming('npx', ['vp', 'test', 'run', 'packed-test-helper.test.ts']),
267
+ )
268
+
269
+ step('deploy Convex fixture', () =>
270
+ runConvex('dev', '--once', '--typecheck=disable'),
271
+ )
272
+ const health = JSON.parse(runConvex('run', 'smoke:health'))
273
+ if (health.status !== 'ready' || health.schemaVersion !== 1) {
274
+ throw new Error(
275
+ `Unexpected approvals health response: ${JSON.stringify(health)}`,
276
+ )
277
+ }
278
+ const foundationHealth = JSON.parse(
279
+ runConvex('run', 'smoke:foundationHealth'),
280
+ )
281
+ if (foundationHealth !== 'ready') {
282
+ throw new Error(
283
+ `Unexpected foundation health response: ${JSON.stringify(foundationHealth)}`,
284
+ )
285
+ }
286
+ const { runId } = JSON.parse(runConvex('run', 'smoke:start'))
287
+ const { runId: secondRunId } = JSON.parse(
288
+ runConvex(
289
+ 'run',
290
+ 'smoke:start',
291
+ JSON.stringify({ resourceRef: 'packed-component-2' }),
292
+ ),
293
+ )
294
+ const firstPage = JSON.parse(
295
+ runConvex(
296
+ 'run',
297
+ 'smoke:list',
298
+ JSON.stringify({
299
+ state: 'pending',
300
+ paginationOpts: { cursor: null, numItems: 1 },
301
+ }),
302
+ ),
303
+ )
304
+ if (firstPage.page.length !== 1 || firstPage.page[0]._id !== secondRunId) {
305
+ throw new Error(`Unexpected first approval page: ${JSON.stringify(firstPage)}`)
306
+ }
307
+ const secondPage = JSON.parse(
308
+ runConvex(
309
+ 'run',
310
+ 'smoke:list',
311
+ JSON.stringify({
312
+ state: 'pending',
313
+ paginationOpts: {
314
+ cursor: firstPage.continueCursor,
315
+ numItems: 10,
316
+ },
317
+ }),
318
+ ),
319
+ )
320
+ if (
321
+ secondPage.page.length !== 1 ||
322
+ secondPage.page[0]._id !== runId ||
323
+ secondPage.page[0]._id === firstPage.page[0]._id
324
+ ) {
325
+ throw new Error(
326
+ `Unexpected continuation approval page: ${JSON.stringify(secondPage)}`,
327
+ )
328
+ }
329
+
330
+ let decision
331
+ let lastError
332
+ for (let attempt = 0; attempt < 10; attempt += 1) {
333
+ try {
334
+ decision = JSON.parse(
335
+ runConvex('run', 'smoke:decide', JSON.stringify({ runId })),
336
+ )
337
+ break
338
+ } catch (error) {
339
+ lastError = error
340
+ const currentStatus = JSON.parse(
341
+ runConvex('run', 'smoke:status', JSON.stringify({ runId })),
342
+ )
343
+ if (currentStatus.state === 'approved') {
344
+ decision = currentStatus
345
+ break
346
+ }
347
+ await new Promise((resolve) => setTimeout(resolve, 500))
348
+ }
349
+ }
350
+ if (!decision) throw lastError
351
+ if (decision.state !== 'approved') {
352
+ throw new Error(`Unexpected decision response: ${JSON.stringify(decision)}`)
353
+ }
354
+ const history = JSON.parse(
355
+ runConvex('run', 'smoke:history', JSON.stringify({ runId })),
356
+ )
357
+ if (history.length !== 1 || history[0].decision !== 'approved') {
358
+ throw new Error(`Unexpected approval history: ${JSON.stringify(history)}`)
359
+ }
360
+ let status
361
+ for (let attempt = 0; attempt < 20; attempt += 1) {
362
+ status = JSON.parse(
363
+ runConvex('run', 'smoke:status', JSON.stringify({ runId })),
364
+ )
365
+ if (status.execution?.type === 'completed') break
366
+ await new Promise((resolve) => setTimeout(resolve, 500))
367
+ }
368
+ if (status?.execution?.type !== 'completed') {
369
+ throw new Error(`Approval callback did not complete: ${JSON.stringify(status)}`)
370
+ }
371
+ console.log(`packed component smoke passed with ${installer}`)
372
+ } finally {
373
+ rmSync(temporaryRoot, { recursive: true, force: true })
374
+ }
package/src/test.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  * registers the COMPILED component modules from dist, so dist is the only
7
7
  * code the package ships.
8
8
  */
9
- import approvalsSchema from '../dist/components/approvals/schema.mjs'
10
- import foundationSchema from '../dist/components/foundation/schema.mjs'
9
+ import approvalsSchema from '../dist/components/approvals/schema.js'
10
+ import foundationSchema from '../dist/components/foundation/schema.js'
11
11
 
12
12
  const foundationModules = import.meta.glob(
13
13
  '../dist/components/foundation/**/*.mjs',
@@ -1,2 +0,0 @@
1
- import{approvalDecisionDocument as e,approvalRunDocument as t}from"./validators.mjs";import{zodToConvex as n}from"convex-helpers/server/zod4";import{defineSchema as r,defineTable as i}from"convex/server";var a=r({approvalRuns:i(n(t)).index(`by_resourceType_and_resourceRef`,[`resourceType`,`resourceRef`]).index(`by_scopeRef_and_state`,[`scopeRef`,`state`]).index(`by_state_and_expiresAt`,[`state`,`expiresAt`]),approvalDecisions:i(n(e)).index(`by_runId_and_stepKey_and_actor_actorRef`,[`runId`,`stepKey`,`actor.actorRef`]).index(`by_runId_and_stepKey_and_decidedAt`,[`runId`,`stepKey`,`decidedAt`]).index(`by_runId_and_decidedAt`,[`runId`,`decidedAt`])});export{a as default};
2
- //# sourceMappingURL=schema.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"schema.mjs","names":[],"sources":["../../../src/components/approvals/schema.ts"],"sourcesContent":["import { zodToConvex } from 'convex-helpers/server/zod4'\nimport { defineSchema, defineTable } from 'convex/server'\n\nimport { approvalDecisionDocument, approvalRunDocument } from './validators'\n\nexport default defineSchema({\n\tapprovalRuns: defineTable(zodToConvex(approvalRunDocument))\n\t\t.index('by_resourceType_and_resourceRef', ['resourceType', 'resourceRef'])\n\t\t.index('by_scopeRef_and_state', ['scopeRef', 'state'])\n\t\t.index('by_state_and_expiresAt', ['state', 'expiresAt']),\n\tapprovalDecisions: defineTable(zodToConvex(approvalDecisionDocument))\n\t\t.index('by_runId_and_stepKey_and_actor_actorRef', [\n\t\t\t'runId',\n\t\t\t'stepKey',\n\t\t\t'actor.actorRef',\n\t\t])\n\t\t.index('by_runId_and_stepKey_and_decidedAt', [\n\t\t\t'runId',\n\t\t\t'stepKey',\n\t\t\t'decidedAt',\n\t\t])\n\t\t.index('by_runId_and_decidedAt', ['runId', 'decidedAt']),\n})\n"],"mappings":"4MAKA,IAAA,EAAe,EAAa,CAC3B,aAAc,EAAY,EAAY,CAAmB,CAAC,CAAC,CACzD,MAAM,kCAAmC,CAAC,eAAgB,aAAa,CAAC,CAAC,CACzE,MAAM,wBAAyB,CAAC,WAAY,OAAO,CAAC,CAAC,CACrD,MAAM,yBAA0B,CAAC,QAAS,WAAW,CAAC,EACxD,kBAAmB,EAAY,EAAY,CAAwB,CAAC,CAAC,CACnE,MAAM,0CAA2C,CACjD,QACA,UACA,gBACD,CAAC,CAAC,CACD,MAAM,qCAAsC,CAC5C,QACA,UACA,WACD,CAAC,CAAC,CACD,MAAM,yBAA0B,CAAC,QAAS,WAAW,CAAC,CACzD,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"schema.mjs","names":[],"sources":["../../../src/components/foundation/schema.ts"],"sourcesContent":["import { defineSchema } from 'convex/server'\n\n/** Foundation is a capability-free in-process kernel and owns no business data. */\nexport default defineSchema({})\n"],"mappings":"6CAGA,IAAA,EAAe,EAAa,CAAC,CAAC"}