cvx-kit 0.0.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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/_virtual/_rolldown/runtime.mjs +1 -0
  4. package/dist/auth.d.mts +353 -0
  5. package/dist/auth.mjs +2 -0
  6. package/dist/auth.mjs.map +1 -0
  7. package/dist/components/approvals/_generated/api.d.mts +47 -0
  8. package/dist/components/approvals/_generated/api.mjs +2 -0
  9. package/dist/components/approvals/_generated/api.mjs.map +1 -0
  10. package/dist/components/approvals/_generated/component.d.mts +297 -0
  11. package/dist/components/approvals/_generated/component.mjs +1 -0
  12. package/dist/components/approvals/_generated/dataModel.d.mts +41 -0
  13. package/dist/components/approvals/_generated/dataModel.mjs +1 -0
  14. package/dist/components/approvals/_generated/server.d.mts +116 -0
  15. package/dist/components/approvals/_generated/server.mjs +2 -0
  16. package/dist/components/approvals/_generated/server.mjs.map +1 -0
  17. package/dist/components/approvals/audit.d.mts +40 -0
  18. package/dist/components/approvals/audit.mjs +2 -0
  19. package/dist/components/approvals/audit.mjs.map +1 -0
  20. package/dist/components/approvals/client.d.mts +98 -0
  21. package/dist/components/approvals/client.mjs +2 -0
  22. package/dist/components/approvals/client.mjs.map +1 -0
  23. package/dist/components/approvals/constants.d.mts +31 -0
  24. package/dist/components/approvals/constants.mjs +2 -0
  25. package/dist/components/approvals/constants.mjs.map +1 -0
  26. package/dist/components/approvals/convex.config.d.mts +5 -0
  27. package/dist/components/approvals/convex.config.mjs +2 -0
  28. package/dist/components/approvals/convex.config.mjs.map +1 -0
  29. package/dist/components/approvals/decisions.d.mts +61 -0
  30. package/dist/components/approvals/decisions.mjs +2 -0
  31. package/dist/components/approvals/decisions.mjs.map +1 -0
  32. package/dist/components/approvals/functions.d.mts +9 -0
  33. package/dist/components/approvals/functions.mjs +2 -0
  34. package/dist/components/approvals/functions.mjs.map +1 -0
  35. package/dist/components/approvals/requests.d.mts +323 -0
  36. package/dist/components/approvals/requests.mjs +2 -0
  37. package/dist/components/approvals/requests.mjs.map +1 -0
  38. package/dist/components/approvals/schema.d.mts +376 -0
  39. package/dist/components/approvals/schema.mjs +2 -0
  40. package/dist/components/approvals/schema.mjs.map +1 -0
  41. package/dist/components/approvals/validators.d.mts +302 -0
  42. package/dist/components/approvals/validators.mjs +2 -0
  43. package/dist/components/approvals/validators.mjs.map +1 -0
  44. package/dist/components/approvals/workflow.d.mts +11 -0
  45. package/dist/components/approvals/workflow.mjs +2 -0
  46. package/dist/components/approvals/workflow.mjs.map +1 -0
  47. package/dist/components/approvals/workflow_steps.d.mts +83 -0
  48. package/dist/components/approvals/workflow_steps.mjs +2 -0
  49. package/dist/components/approvals/workflow_steps.mjs.map +1 -0
  50. package/dist/components/foundation/_generated/api.d.mts +40 -0
  51. package/dist/components/foundation/_generated/api.mjs +2 -0
  52. package/dist/components/foundation/_generated/api.mjs.map +1 -0
  53. package/dist/components/foundation/_generated/component.d.mts +21 -0
  54. package/dist/components/foundation/_generated/component.mjs +1 -0
  55. package/dist/components/foundation/_generated/dataModel.d.mts +41 -0
  56. package/dist/components/foundation/_generated/dataModel.mjs +1 -0
  57. package/dist/components/foundation/_generated/server.d.mts +116 -0
  58. package/dist/components/foundation/_generated/server.mjs +2 -0
  59. package/dist/components/foundation/_generated/server.mjs.map +1 -0
  60. package/dist/components/foundation/client.d.mts +93 -0
  61. package/dist/components/foundation/client.mjs +2 -0
  62. package/dist/components/foundation/client.mjs.map +1 -0
  63. package/dist/components/foundation/convex.config.d.mts +5 -0
  64. package/dist/components/foundation/convex.config.mjs +2 -0
  65. package/dist/components/foundation/convex.config.mjs.map +1 -0
  66. package/dist/components/foundation/functions.d.mts +8 -0
  67. package/dist/components/foundation/functions.mjs +2 -0
  68. package/dist/components/foundation/functions.mjs.map +1 -0
  69. package/dist/components/foundation/modules/command/command.d.mts +47 -0
  70. package/dist/components/foundation/modules/command/command.mjs +2 -0
  71. package/dist/components/foundation/modules/command/command.mjs.map +1 -0
  72. package/dist/components/foundation/modules/observability/observability.d.mts +31 -0
  73. package/dist/components/foundation/modules/observability/observability.mjs +2 -0
  74. package/dist/components/foundation/modules/observability/observability.mjs.map +1 -0
  75. package/dist/components/foundation/modules/query/query.d.mts +24 -0
  76. package/dist/components/foundation/modules/query/query.mjs +2 -0
  77. package/dist/components/foundation/modules/query/query.mjs.map +1 -0
  78. package/dist/components/foundation/result.d.mts +27 -0
  79. package/dist/components/foundation/result.mjs +2 -0
  80. package/dist/components/foundation/result.mjs.map +1 -0
  81. package/dist/components/foundation/schema.d.mts +6 -0
  82. package/dist/components/foundation/schema.mjs +2 -0
  83. package/dist/components/foundation/schema.mjs.map +1 -0
  84. package/dist/components/foundation/telemetry.d.mts +9 -0
  85. package/dist/components/foundation/telemetry.mjs +2 -0
  86. package/dist/components/foundation/telemetry.mjs.map +1 -0
  87. package/dist/errors.d.mts +24 -0
  88. package/dist/errors.mjs +2 -0
  89. package/dist/errors.mjs.map +1 -0
  90. package/dist/index.d.mts +6 -0
  91. package/dist/index.mjs +1 -0
  92. package/dist/triggers.d.mts +27 -0
  93. package/dist/triggers.mjs +2 -0
  94. package/dist/triggers.mjs.map +1 -0
  95. package/dist/zod-table.d.mts +124 -0
  96. package/dist/zod-table.mjs +2 -0
  97. package/dist/zod-table.mjs.map +1 -0
  98. package/package.json +108 -0
  99. package/src/auth.ts +360 -0
  100. package/src/components/approvals/_generated/api.ts +69 -0
  101. package/src/components/approvals/_generated/component.ts +335 -0
  102. package/src/components/approvals/_generated/dataModel.ts +60 -0
  103. package/src/components/approvals/_generated/server.ts +156 -0
  104. package/src/components/approvals/audit.ts +148 -0
  105. package/src/components/approvals/client.ts +321 -0
  106. package/src/components/approvals/constants.ts +51 -0
  107. package/src/components/approvals/convex.config.ts +10 -0
  108. package/src/components/approvals/decisions.ts +376 -0
  109. package/src/components/approvals/functions.ts +27 -0
  110. package/src/components/approvals/requests.ts +240 -0
  111. package/src/components/approvals/schema.ts +23 -0
  112. package/src/components/approvals/validators.ts +272 -0
  113. package/src/components/approvals/workflow.ts +204 -0
  114. package/src/components/approvals/workflow_steps.ts +189 -0
  115. package/src/components/foundation/_generated/api.ts +62 -0
  116. package/src/components/foundation/_generated/component.ts +29 -0
  117. package/src/components/foundation/_generated/dataModel.ts +60 -0
  118. package/src/components/foundation/_generated/server.ts +156 -0
  119. package/src/components/foundation/client.ts +190 -0
  120. package/src/components/foundation/convex.config.ts +3 -0
  121. package/src/components/foundation/functions.ts +10 -0
  122. package/src/components/foundation/modules/command/__tests__/command.test.ts +106 -0
  123. package/src/components/foundation/modules/command/command.ts +154 -0
  124. package/src/components/foundation/modules/observability/__tests__/observability.test.ts +79 -0
  125. package/src/components/foundation/modules/observability/observability.ts +95 -0
  126. package/src/components/foundation/modules/query/__tests__/query.test.ts +25 -0
  127. package/src/components/foundation/modules/query/query.ts +47 -0
  128. package/src/components/foundation/result.ts +77 -0
  129. package/src/components/foundation/schema.ts +4 -0
  130. package/src/components/foundation/telemetry.ts +23 -0
  131. package/src/errors.ts +25 -0
  132. package/src/index.ts +41 -0
  133. package/src/test.ts +36 -0
  134. package/src/triggers.ts +90 -0
  135. package/src/zod-table.ts +197 -0
@@ -0,0 +1,154 @@
1
+ export type Parseable<Output> = Readonly<{ parse: (value: unknown) => Output }>
2
+
3
+ export type CommandRegistry = Readonly<Record<string, CommandDefinition>>
4
+
5
+ export type CommandInput<
6
+ Registry extends CommandRegistry,
7
+ Key extends Operation<Registry>,
8
+ > = Parsed<Registry[Key]['command']>
9
+
10
+ export type CommandResult<
11
+ Registry extends CommandRegistry,
12
+ Key extends Operation<Registry>,
13
+ > = Parsed<Registry[Key]['result']>
14
+
15
+ export type CommandExecution<
16
+ Context,
17
+ Registry extends CommandRegistry,
18
+ Key extends Operation<Registry>,
19
+ > = Readonly<{
20
+ context: Context
21
+ operation: Key
22
+ definition: Registry[Key]
23
+ command: CommandInput<Registry, Key>
24
+ parseResult: (value: unknown) => CommandResult<Registry, Key>
25
+ run: () => Promise<CommandResult<Registry, Key>>
26
+ }>
27
+
28
+ type CommandDefinition = Readonly<{
29
+ command: Parseable<unknown>
30
+ result: Parseable<unknown>
31
+ }>
32
+
33
+ type Operation<Registry extends CommandRegistry> = Extract<
34
+ keyof Registry,
35
+ string
36
+ >
37
+ type Parsed<Schema> = Schema extends Parseable<infer Output> ? Output : never
38
+ type MaybePromise<Value> = Value | Promise<Value>
39
+ type Execute<Context, Registry extends CommandRegistry> = <
40
+ Key extends Operation<Registry>,
41
+ >(
42
+ execution: CommandExecution<Context, Registry, Key>,
43
+ ) => Promise<CommandResult<Registry, Key>>
44
+
45
+ type FixedExecutor<
46
+ Context,
47
+ Registry extends CommandRegistry,
48
+ Key extends Operation<Registry>,
49
+ > = Readonly<{
50
+ operation: Key
51
+ handler: (
52
+ context: Context,
53
+ command: CommandInput<Registry, Key>,
54
+ ) => MaybePromise<CommandResult<Registry, Key>>
55
+ }>
56
+
57
+ type DynamicExecutor<
58
+ Context,
59
+ Registry extends CommandRegistry,
60
+ Dispatcher,
61
+ Key extends Operation<Registry>,
62
+ > = Readonly<{
63
+ dispatcher: Parseable<Dispatcher>
64
+ select: (dispatcher: Dispatcher) => Key
65
+ handler: (
66
+ context: Context,
67
+ command: CommandInput<Registry, Key>,
68
+ ) => MaybePromise<CommandResult<Registry, Key>>
69
+ }>
70
+
71
+ class CommandConfigurationError extends Error {
72
+ readonly code = 'COMMAND_OPERATION_NOT_CONFIGURED'
73
+ readonly name = 'CommandConfigurationError'
74
+
75
+ constructor() {
76
+ super('The selected command operation is not configured')
77
+ }
78
+ }
79
+
80
+ /** Generic command kernel. Host policy is injected by the owning domain. */
81
+ export class Command<Context, const Registry extends CommandRegistry> {
82
+ readonly #operations: Registry
83
+ readonly #execute: Execute<Context, Registry>
84
+
85
+ constructor(configuration: {
86
+ readonly operations: Registry
87
+ readonly execute: Execute<Context, Registry>
88
+ }) {
89
+ this.#operations = configuration.operations
90
+ this.#execute = configuration.execute
91
+ }
92
+
93
+ exec<const Key extends Operation<Registry>>(
94
+ executor: FixedExecutor<Context, Registry, Key>,
95
+ ): (
96
+ context: Context,
97
+ command: CommandInput<Registry, Key>,
98
+ ) => Promise<CommandResult<Registry, Key>>
99
+ exec<Dispatcher, const Key extends Operation<Registry>>(
100
+ executor: DynamicExecutor<Context, Registry, Dispatcher, Key>,
101
+ ): (
102
+ context: Context,
103
+ command: Dispatcher,
104
+ ) => Promise<CommandResult<Registry, Key>>
105
+ exec(
106
+ executor:
107
+ | FixedExecutor<Context, Registry, Operation<Registry>>
108
+ | DynamicExecutor<Context, Registry, unknown, Operation<Registry>>,
109
+ ) {
110
+ return async (context: Context, value: unknown) => {
111
+ const selected =
112
+ 'operation' in executor
113
+ ? { operation: executor.operation, value }
114
+ : (() => {
115
+ const dispatcher = executor.dispatcher.parse(value)
116
+ return {
117
+ operation: executor.select(dispatcher),
118
+ value: dispatcher,
119
+ }
120
+ })()
121
+ if (
122
+ !Object.prototype.hasOwnProperty.call(
123
+ this.#operations,
124
+ selected.operation,
125
+ )
126
+ ) {
127
+ throw new CommandConfigurationError()
128
+ }
129
+ const operation = selected.operation as Operation<Registry>
130
+ const definition = this.#operations[operation]
131
+ const command = definition.command.parse(selected.value) as CommandInput<
132
+ Registry,
133
+ Operation<Registry>
134
+ >
135
+ const parseResult = (result: unknown) =>
136
+ definition.result.parse(result) as CommandResult<
137
+ Registry,
138
+ Operation<Registry>
139
+ >
140
+ const handler = executor.handler as (
141
+ context: Context,
142
+ command: CommandInput<Registry, Operation<Registry>>,
143
+ ) => MaybePromise<CommandResult<Registry, Operation<Registry>>>
144
+ return this.#execute({
145
+ context,
146
+ operation,
147
+ definition,
148
+ command,
149
+ parseResult,
150
+ run: async () => parseResult(await handler(context, command)),
151
+ })
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it, vi } from 'vite-plus/test'
2
+
3
+ import { Observability } from '../observability'
4
+
5
+ describe('Foundation Observability', () => {
6
+ it('emits bounded semantic metadata without payloads', async () => {
7
+ const emit = vi.fn()
8
+ const observability = new Observability({
9
+ enabled: true,
10
+ clock: tickingClock(),
11
+ classifyError: () => ({
12
+ outcome: 'failed',
13
+ errorCode: 'UNEXPECTED_ERROR',
14
+ }),
15
+ emit,
16
+ })
17
+
18
+ await expect(
19
+ observability.observe(
20
+ { operation: 'assignments.claim', classification: 'writer' },
21
+ () => 'claimed',
22
+ ),
23
+ ).resolves.toBe('claimed')
24
+ expect(emit).toHaveBeenCalledWith({
25
+ operation: 'assignments.claim',
26
+ classification: 'writer',
27
+ outcome: 'completed',
28
+ durationMs: 5,
29
+ })
30
+ })
31
+
32
+ it('classifies denial and preserves the original failure', async () => {
33
+ const emit = vi.fn()
34
+ const original = new Error('sensitive reason')
35
+ const observability = new Observability({
36
+ enabled: true,
37
+ clock: tickingClock(),
38
+ classifyError: () => ({ outcome: 'denied', errorCode: 'FORBIDDEN' }),
39
+ emit,
40
+ })
41
+
42
+ await expect(
43
+ observability.observe(
44
+ { operation: 'assignments.assign', classification: 'admin' },
45
+ () => {
46
+ throw original
47
+ },
48
+ ),
49
+ ).rejects.toBe(original)
50
+ expect(JSON.stringify(emit.mock.calls)).not.toContain('sensitive reason')
51
+ })
52
+
53
+ it('drops invalid semantic fields and contains emitter failures', async () => {
54
+ const emit = vi.fn(() => {
55
+ throw new Error('telemetry unavailable')
56
+ })
57
+ const observability = new Observability({
58
+ enabled: true,
59
+ classifyError: () => ({
60
+ outcome: 'failed',
61
+ errorCode: 'UNEXPECTED_ERROR',
62
+ }),
63
+ emit,
64
+ })
65
+
66
+ await expect(
67
+ observability.observe(
68
+ { operation: 'contains secret whitespace', classification: 'writer' },
69
+ () => 'unchanged',
70
+ ),
71
+ ).resolves.toBe('unchanged')
72
+ expect(emit).not.toHaveBeenCalled()
73
+ })
74
+ })
75
+
76
+ function tickingClock(): () => number {
77
+ let now = 10
78
+ return () => (now += 5)
79
+ }
@@ -0,0 +1,95 @@
1
+ export type CommandObservation = Readonly<{
2
+ operation: string
3
+ classification: string
4
+ outcome: 'completed' | 'denied' | 'failed'
5
+ errorCode?: string
6
+ durationMs: number
7
+ }>
8
+
9
+ export type ObservabilityOptions = Readonly<{
10
+ enabled?: boolean | (() => boolean)
11
+ classifyError: (error: unknown) => Readonly<{
12
+ outcome: 'denied' | 'failed'
13
+ errorCode: string
14
+ }>
15
+ emit?: (observation: CommandObservation) => void
16
+ clock?: () => number
17
+ }>
18
+
19
+ const identifier = /^[A-Za-z][A-Za-z0-9_.-]{0,159}$/
20
+ const errorCode = /^[A-Z][A-Z0-9_]{0,95}$/
21
+
22
+ /** Payload-free command telemetry that cannot change command behavior. */
23
+ export class Observability {
24
+ readonly #options: ObservabilityOptions
25
+
26
+ constructor(options: ObservabilityOptions) {
27
+ this.#options = options
28
+ }
29
+
30
+ async observe<Result>(
31
+ activation: Readonly<{ operation: string; classification: string }>,
32
+ execute: () => Result | Promise<Result>,
33
+ ): Promise<Result> {
34
+ const started = this.#now()
35
+ try {
36
+ const result = await execute()
37
+ this.#emit({
38
+ ...activation,
39
+ outcome: 'completed',
40
+ durationMs: this.#duration(started),
41
+ })
42
+ return result
43
+ } catch (error) {
44
+ try {
45
+ this.#emit({
46
+ ...activation,
47
+ ...this.#options.classifyError(error),
48
+ durationMs: this.#duration(started),
49
+ })
50
+ } catch {
51
+ // Preserve the original command failure when telemetry fails.
52
+ }
53
+ throw error
54
+ }
55
+ }
56
+
57
+ #emit(observation: CommandObservation): void {
58
+ try {
59
+ const enabled =
60
+ typeof this.#options.enabled === 'function'
61
+ ? this.#options.enabled()
62
+ : this.#options.enabled
63
+ if (
64
+ enabled !== true ||
65
+ !identifier.test(observation.operation) ||
66
+ !identifier.test(observation.classification) ||
67
+ (observation.errorCode !== undefined &&
68
+ !errorCode.test(observation.errorCode))
69
+ ) {
70
+ return
71
+ }
72
+ if (this.#options.emit) {
73
+ this.#options.emit(Object.freeze(observation))
74
+ return
75
+ }
76
+ console.info(
77
+ JSON.stringify({ event: 'command.execution', ...observation }),
78
+ )
79
+ } catch {
80
+ // Observability is intentionally behaviorally inert.
81
+ }
82
+ }
83
+
84
+ #now(): number {
85
+ try {
86
+ return this.#options.clock?.() ?? Date.now()
87
+ } catch {
88
+ return Date.now()
89
+ }
90
+ }
91
+
92
+ #duration(started: number): number {
93
+ return Math.max(0, Math.min(600_000, this.#now() - started))
94
+ }
95
+ }
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vite-plus/test'
2
+
3
+ import { Query } from '../query'
4
+
5
+ describe('Foundation Query', () => {
6
+ it('runs injected policy before its handler', async () => {
7
+ const order: string[] = []
8
+ const query = new Query({
9
+ defaults: { classification: 'authenticated' as const },
10
+ execute: async (execution) => {
11
+ order.push(`policy:${execution.metadata.classification}`)
12
+ return execution.run()
13
+ },
14
+ })
15
+ const execute = query.exec({
16
+ handler: (_context: unknown, id: string) => {
17
+ order.push('handler')
18
+ return id
19
+ },
20
+ })
21
+
22
+ await expect(execute({}, 'assignment-1')).resolves.toBe('assignment-1')
23
+ expect(order).toEqual(['policy:authenticated', 'handler'])
24
+ })
25
+ })
@@ -0,0 +1,47 @@
1
+ export type QueryExecution<Context, Metadata, Result> = Readonly<{
2
+ context: Context
3
+ metadata: Metadata
4
+ run: () => Promise<Result>
5
+ }>
6
+
7
+ type MaybePromise<Value> = Value | Promise<Value>
8
+
9
+ /** Generic query kernel. Authorization remains an injected host policy. */
10
+ export class Query<Context, Defaults extends object> {
11
+ readonly #defaults: Defaults
12
+ readonly #execute: <Metadata extends object, Result>(
13
+ execution: QueryExecution<Context, Defaults & Metadata, Result>,
14
+ ) => Promise<Result>
15
+
16
+ constructor(configuration: {
17
+ readonly defaults: Defaults
18
+ readonly execute: <Metadata extends object, Result>(
19
+ execution: QueryExecution<Context, Defaults & Metadata, Result>,
20
+ ) => Promise<Result>
21
+ }) {
22
+ this.#defaults = configuration.defaults
23
+ this.#execute = configuration.execute
24
+ }
25
+
26
+ exec<
27
+ Arguments extends readonly unknown[],
28
+ Result,
29
+ Metadata extends object = object,
30
+ >(executor: {
31
+ readonly metadata?: Metadata
32
+ readonly handler: (
33
+ context: Context,
34
+ ...args: Arguments
35
+ ) => MaybePromise<Result>
36
+ }): (context: Context, ...args: Arguments) => Promise<Result> {
37
+ return (context, ...args) =>
38
+ this.#execute({
39
+ context,
40
+ metadata: {
41
+ ...this.#defaults,
42
+ ...(executor.metadata ?? ({} as Metadata)),
43
+ },
44
+ run: async () => executor.handler(context, ...args),
45
+ })
46
+ }
47
+ }
@@ -0,0 +1,77 @@
1
+ import type { TransactionMetrics } from 'convex/server'
2
+
3
+ export type TransactionMetricsContext = Readonly<{
4
+ meta: Readonly<{
5
+ getTransactionMetrics: () => Promise<TransactionMetrics>
6
+ }>
7
+ }>
8
+
9
+ export type Result<Value, Failure> =
10
+ | Readonly<{ ok: true; value: Value }>
11
+ | Readonly<{ ok: false; error: Failure }>
12
+
13
+ export type ResultBoundary<Failure> = Readonly<{
14
+ ok: <Value>(value: Value) => Result<Value, Failure>
15
+ handledError: <Value = never>(failure: Failure) => Result<Value, Failure>
16
+ dataOf: (error: unknown) => Failure | undefined
17
+ }>
18
+
19
+ const metricNames = [
20
+ 'bytesRead',
21
+ 'bytesWritten',
22
+ 'databaseQueries',
23
+ 'documentsRead',
24
+ 'documentsWritten',
25
+ 'functionsScheduled',
26
+ 'scheduledFunctionArgsBytes',
27
+ ] as const satisfies readonly (keyof TransactionMetrics)[]
28
+
29
+ const effectMetricNames = [
30
+ 'bytesWritten',
31
+ 'documentsWritten',
32
+ 'functionsScheduled',
33
+ 'scheduledFunctionArgsBytes',
34
+ ] as const satisfies readonly (keyof TransactionMetrics)[]
35
+
36
+ /** Return handled failures only before effects; otherwise rethrow for rollback. */
37
+ export async function executeResultBoundary<Value, Failure>(
38
+ ctx: TransactionMetricsContext,
39
+ operation: () => Promise<Value>,
40
+ boundary: ResultBoundary<Failure>,
41
+ ): Promise<Result<Value, Failure>> {
42
+ try {
43
+ return boundary.ok(await operation())
44
+ } catch (error) {
45
+ const handled = boundary.dataOf(error)
46
+ if (handled === undefined) throw error
47
+ try {
48
+ if (hasEffects(await ctx.meta.getTransactionMetrics())) throw error
49
+ } catch {
50
+ throw error
51
+ }
52
+ return boundary.handledError(handled)
53
+ }
54
+ }
55
+
56
+ export function projectResult<Value, Failure>(
57
+ result: Result<Value, Failure>,
58
+ projectFailure: (failure: Failure) => never,
59
+ ): Value {
60
+ return result.ok ? result.value : projectFailure(result.error)
61
+ }
62
+
63
+ function hasEffects(metrics: TransactionMetrics): boolean {
64
+ for (const name of metricNames) {
65
+ const metric = metrics[name]
66
+ if (
67
+ !metric ||
68
+ !Number.isFinite(metric.used) ||
69
+ metric.used < 0 ||
70
+ !Number.isFinite(metric.remaining) ||
71
+ metric.remaining < 0
72
+ ) {
73
+ throw new Error('Unreadable transaction metrics')
74
+ }
75
+ }
76
+ return effectMetricNames.some((name) => metrics[name].used > 0)
77
+ }
@@ -0,0 +1,4 @@
1
+ import { defineSchema } from 'convex/server'
2
+
3
+ /** Foundation is a capability-free in-process kernel and owns no business data. */
4
+ export default defineSchema({})
@@ -0,0 +1,23 @@
1
+ type SemanticValue = string | number | boolean
2
+
3
+ const semanticToken = /^[A-Za-z][A-Za-z0-9_.-]{0,159}$/
4
+
5
+ /** Emit bounded semantic fields only; free-form payloads and identifiers drop. */
6
+ export function emitSemanticEvent(
7
+ event: string,
8
+ fields: Readonly<Record<string, SemanticValue | undefined>> = {},
9
+ ): void {
10
+ try {
11
+ if (!semanticToken.test(event)) return
12
+ const safe: Record<string, SemanticValue> = { event }
13
+ for (const [key, value] of Object.entries(fields)) {
14
+ if (!semanticToken.test(key) || value === undefined) continue
15
+ if (typeof value === 'string' && !semanticToken.test(value)) continue
16
+ if (typeof value === 'number' && !Number.isFinite(value)) continue
17
+ safe[key] = value
18
+ }
19
+ console.info(JSON.stringify(safe))
20
+ } catch {
21
+ // Telemetry must never change command behavior.
22
+ }
23
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,25 @@
1
+ /** Structured application error carried across Convex function boundaries. */
2
+ export class KitError extends Error {
3
+ readonly code: string
4
+
5
+ constructor(input: { code: string; message?: string }) {
6
+ super(input.message ?? input.code)
7
+ this.name = 'KitError'
8
+ this.code = input.code
9
+ }
10
+ }
11
+
12
+ /**
13
+ * Injection seam for host error policy. Hosts with their own error taxonomy
14
+ * (e.g. an App.errors singleton) adapt it to this shape; everyone else uses
15
+ * the default factory below.
16
+ */
17
+ export type ErrorFactory = {
18
+ throw(input: { code: string; message?: string }): never
19
+ }
20
+
21
+ export const defaultErrors: ErrorFactory = {
22
+ throw(input) {
23
+ throw new KitError(input)
24
+ },
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ export {
2
+ defaultErrors,
3
+ KitError,
4
+ type ErrorFactory,
5
+ } from './errors'
6
+ export {
7
+ jsonSafeZid,
8
+ TIMESTAMP_FIELDS,
9
+ zodTable,
10
+ zodVariantTable,
11
+ type TableBoundaryOptions,
12
+ } from './zod-table'
13
+ export {
14
+ createAuthFunctions,
15
+ createInclude,
16
+ defaultRoleMap,
17
+ type Actor,
18
+ type AuthBundle,
19
+ type AuthFunctionsConfig,
20
+ type DefaultRole,
21
+ type Include,
22
+ type IncludedQuery,
23
+ } from './auth'
24
+ export {
25
+ appendOnly,
26
+ createTriggers,
27
+ noDelete,
28
+ timestamps,
29
+ Triggers,
30
+ type Change,
31
+ type Trigger,
32
+ } from './triggers'
33
+ export {
34
+ Foundation,
35
+ type ApplicationCommand,
36
+ type AuditedOperation,
37
+ type AuditedRegistry,
38
+ type AuditEntryInput,
39
+ type AuditWriter,
40
+ type FoundationOptions,
41
+ } from './components/foundation/client'
package/src/test.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Test helpers for consumers, following the official component template:
3
+ * register the kit's components on a convex-test instance so host-app tests
4
+ * run against them in-memory. Requires a vitest-compatible runner
5
+ * (import.meta.glob), which is why this export ships as source.
6
+ */
7
+ import approvalsSchema from './components/approvals/schema'
8
+ import foundationSchema from './components/foundation/schema'
9
+
10
+ const foundationModules = import.meta.glob(
11
+ './components/foundation/**/!(*.test).ts',
12
+ )
13
+ const approvalsModules = import.meta.glob(
14
+ './components/approvals/**/!(*.test).ts',
15
+ )
16
+
17
+ type RegistersComponents = {
18
+ registerComponent: (
19
+ name: string,
20
+ schema: unknown,
21
+ modules: Record<string, () => Promise<unknown>>,
22
+ ) => void
23
+ }
24
+
25
+ /** Registers the foundation component under its default install name. */
26
+ export function registerFoundation(
27
+ t: RegistersComponents,
28
+ name = 'foundation',
29
+ ) {
30
+ t.registerComponent(name, foundationSchema, foundationModules)
31
+ }
32
+
33
+ /** Registers the approvals component under its default install name. */
34
+ export function registerApprovals(t: RegistersComponents, name = 'approvals') {
35
+ t.registerComponent(name, approvalsSchema, approvalsModules)
36
+ }