@fabrika/app 0.3.3 → 0.4.0

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 (114) hide show
  1. package/README.md +78 -90
  2. package/package.json +68 -6
  3. package/skill/SKILL.md +319 -0
  4. package/skill/references/app-setup.md +364 -0
  5. package/skill/references/dev.md +67 -0
  6. package/skill/references/endpoint.md +627 -0
  7. package/skill/references/errors.md +108 -0
  8. package/skill/references/files.md +335 -0
  9. package/skill/references/jobs.md +169 -0
  10. package/skill/references/public-intake.md +1495 -0
  11. package/skill/references/spa.md +741 -0
  12. package/skill/references/start.md +179 -0
  13. package/skill/references/table-and-migration.md +264 -0
  14. package/skill/references/tests.md +305 -0
  15. package/skill/references/webhooks-links-connectors.md +190 -0
  16. package/src/agent-skill.ts +77 -0
  17. package/src/build.ts +53 -0
  18. package/src/bun.ts +302 -3
  19. package/src/client/caller.ts +24 -0
  20. package/src/client/contract.ts +309 -0
  21. package/src/client/files.ts +135 -0
  22. package/src/client/index.ts +19 -0
  23. package/src/client/rpc.ts +47 -0
  24. package/src/client/share.ts +14 -0
  25. package/src/{client.ts → client/transport.ts} +58 -61
  26. package/src/cloudflare.ts +139 -3
  27. package/src/contract/artifact-schema.ts +317 -0
  28. package/src/contract/attachment.ts +74 -0
  29. package/src/contract/build.ts +1936 -0
  30. package/src/contract/compile.ts +143 -0
  31. package/src/contract/connect.ts +170 -0
  32. package/src/contract/context.ts +437 -0
  33. package/src/contract/declaration.ts +310 -0
  34. package/src/contract/defaults.ts +156 -0
  35. package/src/contract/define.ts +567 -0
  36. package/src/contract/file-schemas.ts +25 -0
  37. package/src/contract/group.ts +263 -0
  38. package/src/contract/handled.ts +50 -0
  39. package/src/contract/index.ts +34 -0
  40. package/src/contract/kit.ts +607 -0
  41. package/src/contract/platform.ts +113 -0
  42. package/src/contract/problems.ts +30 -0
  43. package/src/contract/schema-from-shape.ts +152 -0
  44. package/src/contract/schema-shape.ts +407 -0
  45. package/src/contract/snapshot-source.ts +168 -0
  46. package/src/contract/vocabulary.ts +50 -0
  47. package/src/disk-assets.ts +25 -0
  48. package/src/fetch.ts +10 -0
  49. package/src/files/archive.ts +292 -0
  50. package/src/files/clamscan.ts +253 -0
  51. package/src/files/collections.ts +669 -0
  52. package/src/files/handle.ts +77 -0
  53. package/src/files/scan.ts +308 -0
  54. package/src/files/sniff.ts +70 -0
  55. package/src/files/treatment.ts +48 -0
  56. package/src/from-row.ts +53 -0
  57. package/src/implement/anonymous.ts +102 -0
  58. package/src/implement/attachment.ts +143 -0
  59. package/src/implement/boot.ts +808 -0
  60. package/src/implement/bounded.ts +72 -0
  61. package/src/implement/browser-config.ts +54 -0
  62. package/src/implement/capability.ts +439 -0
  63. package/src/implement/challenge.ts +115 -0
  64. package/src/implement/connect.ts +386 -0
  65. package/src/implement/dispatch.ts +1085 -0
  66. package/src/implement/environment.ts +47 -0
  67. package/src/implement/failures.ts +116 -0
  68. package/src/implement/faults.ts +46 -0
  69. package/src/implement/form-admission.ts +120 -0
  70. package/src/implement/headers.ts +17 -0
  71. package/src/implement/implement.ts +81 -0
  72. package/src/implement/index.ts +60 -0
  73. package/src/implement/invocation.ts +346 -0
  74. package/src/implement/membership.ts +485 -0
  75. package/src/implement/principal.ts +99 -0
  76. package/src/implement/project.ts +76 -0
  77. package/src/implement/serve.ts +142 -0
  78. package/src/implement/types.ts +215 -0
  79. package/src/implement/webhook.ts +108 -0
  80. package/src/implement/workload.ts +160 -0
  81. package/src/implement/xlsx.ts +275 -0
  82. package/src/index.ts +12 -13
  83. package/src/jobs/report.ts +134 -0
  84. package/src/jobs/runner.ts +749 -0
  85. package/src/jobs/schedule.ts +129 -0
  86. package/src/jobs/types.ts +44 -0
  87. package/src/react/caller.tsx +60 -0
  88. package/src/react/errors.ts +82 -0
  89. package/src/react/hooks.ts +239 -0
  90. package/src/react/index.ts +31 -0
  91. package/src/react/provider.tsx +165 -0
  92. package/src/react/store.ts +215 -0
  93. package/src/react/types.ts +40 -0
  94. package/src/rpc/dispatcher.ts +4 -19
  95. package/src/service/define.ts +121 -0
  96. package/src/service/index.ts +21 -0
  97. package/src/service/serve.ts +99 -0
  98. package/src/testing/caller.ts +123 -0
  99. package/src/testing/dev.ts +73 -0
  100. package/src/testing/endpoint-access.ts +806 -0
  101. package/src/testing/index.ts +23 -0
  102. package/src/testing/isolation.ts +25 -0
  103. package/src/testing/kit.ts +552 -0
  104. package/src/testing/membership.ts +747 -0
  105. package/src/testing/policy-isolation.ts +827 -0
  106. package/src/testing/preload.ts +6 -0
  107. package/src/testing/proxy.ts +87 -0
  108. package/src/testing/runner.ts +61 -0
  109. package/src/testing/status.ts +19 -0
  110. package/src/undumpable.ts +42 -0
  111. package/src/app.ts +0 -76
  112. package/src/middleware.ts +0 -38
  113. package/src/router.ts +0 -149
  114. package/src/testing.ts +0 -119
@@ -0,0 +1,407 @@
1
+ // Reads a zod 4 schema of the contract subset into the `SchemaShape` the artifact carries. The subset is the one
2
+ // `@fabrika/check` accepts (`packages/check/CLAUDE.md`); a form outside it is a problem, never dropped silently.
3
+
4
+ import type { ArrayShape, NumberShape, SchemaShape, StringShape } from '@fabrika/auth-core'
5
+ import { z } from 'zod/v4'
6
+ import { attachmentOf } from './attachment'
7
+ import type { Problems } from './problems'
8
+
9
+ const SUBSET = 'allowed: the supported z subset of packages/check/CLAUDE.md'
10
+
11
+ type StringFormat = NonNullable<StringShape['format']>
12
+
13
+ const STRING_FORMATS: ReadonlySet<string> = new Set<StringFormat>(['uuid', 'email', 'url', 'datetime', 'date', 'time', 'duration'])
14
+
15
+ function isStringFormat(format: string): format is StringFormat {
16
+ return STRING_FORMATS.has(format)
17
+ }
18
+
19
+ /** A schema's shape, or `null` after recording why it has none. */
20
+ export function describeSchema(schema: z.core.$ZodType, where: string, problems: Problems): SchemaShape | null {
21
+ if (attachmentOf(schema) !== null) {
22
+ problems.add(where, "an attachment() is a procedure's whole output, never part of a schema or an input (allowed: output: attachment({ … }))")
23
+ return null
24
+ }
25
+ if (schema instanceof z.core.$ZodString || schema instanceof z.core.$ZodStringFormat) {
26
+ return describeString(schema, where, problems)
27
+ }
28
+ if (schema instanceof z.core.$ZodNumber) {
29
+ return describeNumber(schema, where, problems)
30
+ }
31
+ if (!hasNoChecks(schema, where, problems)) {
32
+ return null
33
+ }
34
+ if (schema instanceof z.core.$ZodBoolean) {
35
+ if (schema._zod.def.coerce === true) {
36
+ problems.add(where, `a coercing schema is outside the contract subset (${SUBSET})`)
37
+ return null
38
+ }
39
+ return { type: 'boolean' }
40
+ }
41
+ if (schema instanceof z.core.$ZodNull) {
42
+ return { type: 'null' }
43
+ }
44
+ if (schema instanceof z.core.$ZodArray) {
45
+ return describeArray(schema, where, problems)
46
+ }
47
+ if (schema instanceof z.core.$ZodLiteral) {
48
+ return describeLiteral(schema._zod.def.values, where, problems)
49
+ }
50
+ if (schema instanceof z.core.$ZodEnum) {
51
+ return describeEnum(Object.values(schema._zod.def.entries), where, problems)
52
+ }
53
+ if (schema instanceof z.core.$ZodObject) {
54
+ return describeObject(schema, where, problems)
55
+ }
56
+ if (schema instanceof z.core.$ZodTuple) {
57
+ const items = schema._zod.def.items.map((item, index) => describeSchema(item, `${where}[${index}]`, problems))
58
+ const rest = schema._zod.def.rest === null ? null : describeSchema(schema._zod.def.rest, `${where}[…]`, problems)
59
+ return allPresent(items) && (schema._zod.def.rest === null || rest !== null) ? { type: 'tuple', items, rest } : null
60
+ }
61
+ if (schema instanceof z.core.$ZodRecord) {
62
+ const key = describeSchema(schema._zod.def.keyType, `${where} key`, problems)
63
+ const value = describeSchema(schema._zod.def.valueType, `${where} value`, problems)
64
+ return key === null || value === null ? null : { type: 'record', key, value }
65
+ }
66
+ if (schema instanceof z.core.$ZodDiscriminatedUnion && !hasRequiredDiscriminator(schema, where, problems)) {
67
+ return null
68
+ }
69
+ // A discriminated union admits what the plain union of its options admits, once no discriminator has a default.
70
+ if (schema instanceof z.core.$ZodUnion) {
71
+ const options = schema._zod.def.options.map((option, index) => describeSchema(option, `${where} option ${index}`, problems))
72
+ return allPresent(options) ? { type: 'union', options } : null
73
+ }
74
+ // A default makes the value optional to the caller; the framework fills it in from the live schema.
75
+ if (schema instanceof z.core.$ZodDefault && !admitsItsDefault(schema, where, problems)) {
76
+ return null
77
+ }
78
+ if (schema instanceof z.core.$ZodOptional || schema instanceof z.core.$ZodDefault) {
79
+ const inner = describeSchema(schema._zod.def.innerType, where, problems)
80
+ return inner === null ? null : { type: 'optional', inner }
81
+ }
82
+ if (schema instanceof z.core.$ZodNullable) {
83
+ const inner = describeSchema(schema._zod.def.innerType, where, problems)
84
+ return inner === null ? null : { type: 'nullable', inner }
85
+ }
86
+ problems.add(where, `a '${schema._zod.def.type}' schema is outside the contract subset (${SUBSET})`)
87
+ return null
88
+ }
89
+
90
+ function describeObject(schema: z.core.$ZodObject, where: string, problems: Problems): SchemaShape | null {
91
+ if (!(schema._zod.def.catchall instanceof z.core.$ZodNever)) {
92
+ problems.add(where, 'the object admits undeclared keys (allowed: z.strictObject({ … }))')
93
+ return null
94
+ }
95
+ const fields: Record<string, SchemaShape> = {}
96
+ let complete = true
97
+ for (const [name, field] of Object.entries(schema._zod.def.shape)) {
98
+ const shape = describeSchema(field, `${where}.${name}`, problems)
99
+ if (shape === null) {
100
+ complete = false
101
+ } else {
102
+ fields[name] = shape
103
+ }
104
+ }
105
+ return complete ? { type: 'object', fields } : null
106
+ }
107
+
108
+ function describeLiteral(values: readonly unknown[], where: string, problems: Problems): SchemaShape | null {
109
+ const shapes: SchemaShape[] = []
110
+ for (const value of values) {
111
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null) {
112
+ shapes.push({ type: 'literal', value })
113
+ } else {
114
+ problems.add(where, `the literal ${String(value)} is not JSON (allowed: a string, number, boolean or null)`)
115
+ return null
116
+ }
117
+ }
118
+ const [only] = shapes
119
+ return shapes.length === 1 && only !== undefined ? only : { type: 'union', options: shapes }
120
+ }
121
+
122
+ function describeEnum(values: readonly unknown[], where: string, problems: Problems): SchemaShape | null {
123
+ const strings: string[] = []
124
+ for (const value of values) {
125
+ if (typeof value !== 'string') {
126
+ problems.add(where, `the enum value ${String(value)} is not a string (allowed: z.enum(['a', 'b']))`)
127
+ return null
128
+ }
129
+ strings.push(value)
130
+ }
131
+ return { type: 'enum', values: strings }
132
+ }
133
+
134
+ function describeString(schema: z.core.$ZodString | z.core.$ZodStringFormat, where: string, problems: Problems): StringShape | null {
135
+ if (schema._zod.def.coerce === true) {
136
+ problems.add(where, `a coercing schema is outside the contract subset (${SUBSET})`)
137
+ return null
138
+ }
139
+ const shape: { -readonly [K in keyof StringShape]: StringShape[K] } = {
140
+ type: 'string',
141
+ format: null,
142
+ minLength: null,
143
+ maxLength: null,
144
+ pattern: null,
145
+ startsWith: null,
146
+ endsWith: null,
147
+ includes: null,
148
+ }
149
+ // `z.uuid()` is itself the format check that `z.string().uuid()` adds to its checks.
150
+ const checks: readonly z.core.$ZodCheck<string>[] = schema instanceof z.core.$ZodCheckStringFormat
151
+ ? [schema, ...(schema._zod.def.checks ?? [])]
152
+ : (schema._zod.def.checks ?? [])
153
+ let complete = true
154
+ const setOnce = (field: 'pattern' | 'startsWith' | 'endsWith' | 'includes', value: string, method: string): void => {
155
+ if (shape[field] === null) {
156
+ shape[field] = value
157
+ } else {
158
+ problems.add(where, `the string calls .${method}() twice (allowed: one .${method}() per string)`)
159
+ complete = false
160
+ }
161
+ }
162
+ for (const check of checks) {
163
+ if (isLengthCheck(check)) {
164
+ const bounds = lengthBounds(check, { min: shape.minLength, max: shape.maxLength }, where, problems)
165
+ complete &&= bounds !== null
166
+ shape.minLength = bounds?.min ?? shape.minLength
167
+ shape.maxLength = bounds?.max ?? shape.maxLength
168
+ } else if (check instanceof z.core.$ZodCheckRegex) {
169
+ if (check._zod.def.pattern.flags !== '') {
170
+ problems.add(where, `the pattern /${check._zod.def.pattern.source}/ has flags (allowed: a pattern without flags)`)
171
+ complete = false
172
+ }
173
+ setOnce('pattern', check._zod.def.pattern.source, 'regex')
174
+ } else if (check instanceof z.core.$ZodCheckStartsWith) {
175
+ setOnce('startsWith', check._zod.def.prefix, 'startsWith')
176
+ } else if (check instanceof z.core.$ZodCheckEndsWith) {
177
+ setOnce('endsWith', check._zod.def.suffix, 'endsWith')
178
+ } else if (check instanceof z.core.$ZodCheckIncludes) {
179
+ if (check._zod.def.position !== undefined) {
180
+ problems.add(where, `.includes() with a position is outside the contract subset (allowed: .includes('text'))`)
181
+ complete = false
182
+ }
183
+ setOnce('includes', check._zod.def.includes, 'includes')
184
+ } else if (check instanceof z.core.$ZodCheckStringFormat) {
185
+ const format = check._zod.def.format
186
+ if (!isStringFormat(format) || !hasDefaultOptions(check)) {
187
+ problems.add(where, `the string format '${format}' with these options is outside the contract subset (${SUBSET})`)
188
+ complete = false
189
+ } else if (shape.format !== null) {
190
+ problems.add(where, `the string has two formats, ${shape.format} and ${format} (allowed: one format per string)`)
191
+ complete = false
192
+ } else {
193
+ shape.format = format
194
+ }
195
+ } else {
196
+ problems.add(where, `a '${check._zod.def.check}' check is outside the contract subset (${SUBSET})`)
197
+ complete = false
198
+ }
199
+ }
200
+ return complete && isOrderFree(shape, where, problems) && isSatisfiable(shape.minLength, shape.maxLength, where, problems) ? shape : null
201
+ }
202
+
203
+ /**
204
+ * The artifact keeps a string's checks unordered, so they must not depend on their order. `z.url()` rewrites the
205
+ * value it accepts (`HTTPS://A.CO` → `https://a.co/`), so a check before it and after it see different strings;
206
+ * `startsWith`, `endsWith` and `includes` overlap with what a format admits, so they stay on a plain string.
207
+ */
208
+ function isOrderFree(shape: StringShape, where: string, problems: Problems): boolean {
209
+ const affixed = shape.startsWith !== null || shape.endsWith !== null || shape.includes !== null
210
+ if (shape.format === 'url' && (affixed || shape.pattern !== null || shape.minLength !== null || shape.maxLength !== null)) {
211
+ problems.add(where, 'z.url() rewrites the value, so another string check depends on its order (allowed: z.url() with no other check)')
212
+ return false
213
+ }
214
+ if (shape.format !== null && affixed) {
215
+ problems.add(where, `.startsWith(), .endsWith() and .includes() do not combine with the format '${shape.format}' (allowed: one or the other)`)
216
+ return false
217
+ }
218
+ return true
219
+ }
220
+
221
+ /**
222
+ * zod returns a default without parsing it, so a default its own schema refuses reaches a handler, or leaves an
223
+ * endpoint as output, outside the shape the artifact records. A default the schema admits is a value a caller could
224
+ * have sent, so the shape stays faithful without naming it.
225
+ */
226
+ function admitsItsDefault(schema: z.core.$ZodDefault, where: string, problems: Problems): boolean {
227
+ if (z.safeParse(schema._zod.def.innerType, schema._zod.def.defaultValue).success) {
228
+ return true
229
+ }
230
+ problems.add(where, 'the default is a value its own schema refuses, and zod returns it unchecked (allowed: a default the schema admits)')
231
+ return false
232
+ }
233
+
234
+ /**
235
+ * A discriminator with a default lets a value without it match an option, which the artifact's union cannot say. The
236
+ * default may sit under `.nullable()` or `.optional()`, so every wrapper is looked through.
237
+ */
238
+ function hasRequiredDiscriminator(schema: z.core.$ZodDiscriminatedUnion, where: string, problems: Problems): boolean {
239
+ const discriminator = schema._zod.def.discriminator
240
+ for (const option of schema._zod.def.options) {
241
+ const field = option instanceof z.core.$ZodObject ? option._zod.def.shape[discriminator] : undefined
242
+ if (field !== undefined && hasDefault(field)) {
243
+ problems.add(where, `the discriminator '${discriminator}' has a default (allowed: a required literal in every option)`)
244
+ return false
245
+ }
246
+ }
247
+ return true
248
+ }
249
+
250
+ function hasDefault(schema: z.core.$ZodType): boolean {
251
+ let wrapper = schema
252
+ for (;;) {
253
+ if (wrapper instanceof z.core.$ZodDefault) return true
254
+ if (!(wrapper instanceof z.core.$ZodOptional || wrapper instanceof z.core.$ZodNullable)) return false
255
+ wrapper = wrapper._zod.def.innerType
256
+ }
257
+ }
258
+
259
+ type LengthCheck = z.core.$ZodCheckMinLength | z.core.$ZodCheckMaxLength | z.core.$ZodCheckLengthEquals
260
+
261
+ function isLengthCheck(check: z.core.$ZodCheck<never>): check is LengthCheck {
262
+ return check instanceof z.core.$ZodCheckMinLength || check instanceof z.core.$ZodCheckMaxLength || check instanceof z.core.$ZodCheckLengthEquals
263
+ }
264
+
265
+ interface Bounds {
266
+ readonly min: number | null
267
+ readonly max: number | null
268
+ }
269
+
270
+ /** The tighter bounds after one length check, or `null` when its value is not a length. */
271
+ function lengthBounds(check: LengthCheck, bounds: Bounds, where: string, problems: Problems): Bounds | null {
272
+ const value = check instanceof z.core.$ZodCheckMinLength
273
+ ? check._zod.def.minimum
274
+ : check instanceof z.core.$ZodCheckMaxLength
275
+ ? check._zod.def.maximum
276
+ : check._zod.def.length
277
+ if (!Number.isSafeInteger(value) || value < 0) {
278
+ problems.add(where, `the length ${value} is not a whole number of at least 0`)
279
+ return null
280
+ }
281
+ const min = check instanceof z.core.$ZodCheckMaxLength ? bounds.min : Math.max(bounds.min ?? 0, value)
282
+ const max = check instanceof z.core.$ZodCheckMinLength ? bounds.max : Math.min(bounds.max ?? value, value)
283
+ return { min, max }
284
+ }
285
+
286
+ function isSatisfiable(min: number | null, max: number | null, where: string, problems: Problems): boolean {
287
+ if (min !== null && max !== null && min > max) {
288
+ problems.add(where, `the minimum ${min} is above the maximum ${max}, so nothing is admitted`)
289
+ return false
290
+ }
291
+ return true
292
+ }
293
+
294
+ /** Options change what a format admits, and the artifact names only the format. */
295
+ function hasDefaultOptions(check: z.core.$ZodCheckStringFormat): boolean {
296
+ if (check instanceof z.core.$ZodUUID) {
297
+ return check._zod.def.version === undefined
298
+ }
299
+ if (check instanceof z.core.$ZodEmail) {
300
+ return check._zod.def.pattern?.source === z.regexes.email.source
301
+ }
302
+ if (check instanceof z.core.$ZodURL) {
303
+ return check._zod.def.hostname === undefined && check._zod.def.protocol === undefined
304
+ }
305
+ if (check instanceof z.core.$ZodISODateTime) {
306
+ return !check._zod.def.offset && !check._zod.def.local && check._zod.def.precision === null
307
+ }
308
+ if (check instanceof z.core.$ZodISOTime) {
309
+ return (check._zod.def.precision ?? null) === null
310
+ }
311
+ return true
312
+ }
313
+
314
+ function describeNumber(schema: z.core.$ZodNumber, where: string, problems: Problems): NumberShape | null {
315
+ if (schema._zod.def.coerce === true) {
316
+ problems.add(where, `a coercing schema is outside the contract subset (${SUBSET})`)
317
+ return null
318
+ }
319
+ const shape: { -readonly [K in keyof NumberShape]: NumberShape[K] } = {
320
+ type: 'number',
321
+ int: false,
322
+ min: null,
323
+ exclusiveMin: false,
324
+ max: null,
325
+ exclusiveMax: false,
326
+ multipleOf: null,
327
+ }
328
+ // `z.int()` is itself the format check that `z.number().int()` adds to its checks.
329
+ const checks: readonly z.core.$ZodCheck<never>[] = schema instanceof z.core.$ZodCheckNumberFormat
330
+ ? [schema, ...(schema._zod.def.checks ?? [])]
331
+ : (schema._zod.def.checks ?? [])
332
+ let complete = true
333
+ for (const check of checks) {
334
+ if (check instanceof z.core.$ZodCheckNumberFormat && check._zod.def.format === 'safeint') {
335
+ shape.int = true
336
+ } else if (
337
+ (check instanceof z.core.$ZodCheckGreaterThan || check instanceof z.core.$ZodCheckLessThan || check instanceof z.core.$ZodCheckMultipleOf)
338
+ && !(typeof check._zod.def.value === 'number' && Number.isFinite(check._zod.def.value))
339
+ ) {
340
+ problems.add(where, `the bound ${String(check._zod.def.value)} is not a finite number`)
341
+ complete = false
342
+ } else if (check instanceof z.core.$ZodCheckGreaterThan && typeof check._zod.def.value === 'number') {
343
+ const value = check._zod.def.value
344
+ const exclusive = !check._zod.def.inclusive
345
+ if (shape.min === null || value > shape.min || (value === shape.min && exclusive)) {
346
+ shape.min = value
347
+ shape.exclusiveMin = exclusive
348
+ }
349
+ } else if (check instanceof z.core.$ZodCheckLessThan && typeof check._zod.def.value === 'number') {
350
+ const value = check._zod.def.value
351
+ const exclusive = !check._zod.def.inclusive
352
+ if (shape.max === null || value < shape.max || (value === shape.max && exclusive)) {
353
+ shape.max = value
354
+ shape.exclusiveMax = exclusive
355
+ }
356
+ } else if (check instanceof z.core.$ZodCheckMultipleOf && typeof check._zod.def.value === 'number' && shape.multipleOf === null) {
357
+ if (check._zod.def.value <= 0) {
358
+ problems.add(where, `.multipleOf(${check._zod.def.value}) is not a positive step`)
359
+ complete = false
360
+ }
361
+ shape.multipleOf = check._zod.def.value
362
+ } else {
363
+ problems.add(where, `a '${check._zod.def.check}' check is outside the contract subset (${SUBSET}; one .multipleOf() per number)`)
364
+ complete = false
365
+ }
366
+ }
367
+ if (shape.min !== null && shape.max !== null && (shape.min > shape.max || (shape.min === shape.max && (shape.exclusiveMin || shape.exclusiveMax)))) {
368
+ problems.add(where, `the bounds ${shape.min} and ${shape.max} admit no number`)
369
+ return null
370
+ }
371
+ return complete ? shape : null
372
+ }
373
+
374
+ function describeArray(schema: z.core.$ZodArray, where: string, problems: Problems): ArrayShape | null {
375
+ const items = describeSchema(schema._zod.def.element, `${where}[]`, problems)
376
+ let minItems: number | null = null
377
+ let maxItems: number | null = null
378
+ let complete = items !== null
379
+ for (const check of schema._zod.def.checks ?? []) {
380
+ if (isLengthCheck(check)) {
381
+ const bounds = lengthBounds(check, { min: minItems, max: maxItems }, where, problems)
382
+ complete &&= bounds !== null
383
+ minItems = bounds?.min ?? minItems
384
+ maxItems = bounds?.max ?? maxItems
385
+ } else {
386
+ problems.add(where, `a '${check._zod.def.check}' check is outside the contract subset (${SUBSET})`)
387
+ complete = false
388
+ }
389
+ }
390
+ return complete && items !== null && isSatisfiable(minItems, maxItems, where, problems) ? { type: 'array', items, minItems, maxItems } : null
391
+ }
392
+
393
+ function hasNoChecks(schema: z.core.$ZodType, where: string, problems: Problems): boolean {
394
+ if (schema instanceof z.core.$ZodArray) {
395
+ return true
396
+ }
397
+ const [check] = schema._zod.def.checks ?? []
398
+ if (check !== undefined) {
399
+ problems.add(where, `a '${check._zod.def.check}' check is outside the contract subset (${SUBSET})`)
400
+ return false
401
+ }
402
+ return true
403
+ }
404
+
405
+ function allPresent<T>(values: readonly (T | null)[]): values is readonly T[] {
406
+ return values.every((value) => value !== null)
407
+ }
@@ -0,0 +1,168 @@
1
+ // The vendored snapshot module `fabrika connect add` and `pull` write: an export's description as zod source in the
2
+ // contract subset, so `@fabrika/check` reads it as a literal and `describeSnapshot` reads back the shapes it came from.
3
+ // The description arrives from another application, so every value is written as a literal the parser cannot misread.
4
+
5
+ import type { ExportDescription, NumberShape, SchemaShape, StringShape } from '@fabrika/auth-core'
6
+ import { ContractError } from './problems'
7
+
8
+ /** The directory, relative to the application root, that holds the vendored snapshots. */
9
+ export const SNAPSHOT_DIRECTORY = 'fabrika/connect'
10
+
11
+ const HEADER = '// @generated by `fabrika connect` from the export description its owner serves. Do not edit: `fabrika connect pull` rewrites it.'
12
+
13
+ export interface SnapshotNames {
14
+ /** Relative to the application root: `fabrika/connect/catalog.products.v1.ts`. */
15
+ readonly file: string
16
+ /** The snapshot const: `catalogProductsV1`. */
17
+ readonly snapshot: string
18
+ /** The digest const the contract's import names: `catalogProductsV1Digest`. */
19
+ readonly digest: string
20
+ }
21
+
22
+ function capitalized(word: string): string {
23
+ return `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`
24
+ }
25
+
26
+ /** The file and the identifiers of the snapshot of `app`'s export `name` at `version`. */
27
+ export function snapshotNames(app: string, name: string, version: number): SnapshotNames {
28
+ const [first = '', ...rest] = app.split('-')
29
+ const appWord = `${first}${rest.map(capitalized).join('')}`
30
+ const snapshot = `${/^[a-z]/.test(appWord) ? appWord : `app${appWord}`}${capitalized(name)}V${version}`
31
+ return { file: `${SNAPSHOT_DIRECTORY}/${app}.${name}.v${version}.ts`, snapshot, digest: `${snapshot}Digest` }
32
+ }
33
+
34
+ /** The snapshot module of `description`. Throws `ContractError` for a shape no zod source in the subset writes. */
35
+ export function snapshotSource(description: ExportDescription): string {
36
+ const names = snapshotNames(description.app, description.export, description.version)
37
+ const procedures = Object.entries(description.procedures).map(([path, procedure]) =>
38
+ [
39
+ `\t\t${key(path)}: {`,
40
+ `\t\t\tinput: ${procedure.input === null ? 'null' : schemaSource(procedure.input, path)},`,
41
+ `\t\t\toutput: ${schemaSource(procedure.output, path)},`,
42
+ '\t\t},',
43
+ ].join('\n')
44
+ )
45
+ return [
46
+ HEADER,
47
+ "import { z } from 'zod/v4'",
48
+ '',
49
+ `export const ${names.digest} = ${literal(description.digest)}`,
50
+ '',
51
+ `export const ${names.snapshot} = {`,
52
+ `\tapp: ${literal(description.app)},`,
53
+ `\texport: ${literal(description.export)},`,
54
+ `\tversion: ${number(description.version)},`,
55
+ `\taudience: ${literal(description.audience)},`,
56
+ `\tdigest: ${names.digest},`,
57
+ '\tprocedures: {',
58
+ ...procedures,
59
+ '\t},',
60
+ '} as const',
61
+ '',
62
+ ].join('\n')
63
+ }
64
+
65
+ /**
66
+ * A single-quoted string literal with the value's own characters. JSON escapes every `"` as `\"` and every `\` as a
67
+ * pair, so turning each `\"` back into `"` touches only a quote; a `'` is then escaped.
68
+ */
69
+ function literal(value: string | number | boolean | null): string {
70
+ if (typeof value !== 'string') {
71
+ return typeof value === 'number' ? number(value) : String(value)
72
+ }
73
+ return `'${JSON.stringify(value).slice(1, -1).replaceAll('\\"', '"').replaceAll("'", "\\'")}'`
74
+ }
75
+
76
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
77
+
78
+ /** `__proto__` as a literal key sets the object's prototype instead of a property, quoted or not. */
79
+ function key(name: string): string {
80
+ if (name === '__proto__') {
81
+ throw new ContractError(['a field or procedure named __proto__ has no object-literal key'])
82
+ }
83
+ return IDENTIFIER.test(name) ? name : literal(name)
84
+ }
85
+
86
+ function number(value: number): string {
87
+ if (!Number.isFinite(value)) {
88
+ throw new ContractError([`the number ${value} has no literal`])
89
+ }
90
+ return Object.is(value, -0) ? '0' : String(value)
91
+ }
92
+
93
+ function schemaSource(shape: SchemaShape, where: string): string {
94
+ switch (shape.type) {
95
+ case 'string':
96
+ return stringSource(shape, where)
97
+ case 'number':
98
+ return numberSource(shape)
99
+ case 'boolean':
100
+ case 'null':
101
+ return `z.${shape.type}()`
102
+ case 'literal':
103
+ return `z.literal(${literal(shape.value)})`
104
+ case 'enum':
105
+ if (shape.values.length === 0) {
106
+ throw new ContractError([`procedure '${where}': an enum without values has no zod source`])
107
+ }
108
+ return `z.enum([${shape.values.map(literal).join(', ')}])`
109
+ case 'array':
110
+ return `z.array(${schemaSource(shape.items, where)})${bound('min', shape.minItems)}${bound('max', shape.maxItems)}`
111
+ case 'tuple': {
112
+ const items = `[${shape.items.map((item) => schemaSource(item, where)).join(', ')}]`
113
+ return shape.rest === null ? `z.tuple(${items})` : `z.tuple(${items}, ${schemaSource(shape.rest, where)})`
114
+ }
115
+ case 'record':
116
+ return `z.record(${schemaSource(shape.key, where)}, ${schemaSource(shape.value, where)})`
117
+ case 'object': {
118
+ const fields = Object.entries(shape.fields).map(([name, field]) => `${key(name)}: ${schemaSource(field, where)}`)
119
+ return `z.strictObject({ ${fields.join(', ')} })`
120
+ }
121
+ case 'optional':
122
+ case 'nullable':
123
+ return `z.${shape.type}(${schemaSource(shape.inner, where)})`
124
+ case 'union':
125
+ return `z.union([${shape.options.map((option) => schemaSource(option, where)).join(', ')}])`
126
+ }
127
+ }
128
+
129
+ function bound(method: string, value: number | null): string {
130
+ return value === null ? '' : `.${method}(${number(value)})`
131
+ }
132
+
133
+ const ISO_FORMATS: ReadonlySet<string> = new Set(['datetime', 'date', 'time', 'duration'])
134
+
135
+ function stringSource(shape: StringShape, where: string): string {
136
+ const base = shape.format === null ? 'z.string()' : ISO_FORMATS.has(shape.format) ? `z.iso.${shape.format}()` : `z.${shape.format}()`
137
+ const affixes = (['startsWith', 'endsWith', 'includes'] as const).map((field) => {
138
+ const value = shape[field]
139
+ return value === null ? '' : `.${field}(${literal(value)})`
140
+ })
141
+ return `${base}${bound('min', shape.minLength)}${bound('max', shape.maxLength)}${patternSource(shape.pattern, where)}${affixes.join('')}`
142
+ }
143
+
144
+ /**
145
+ * A pattern is written as a regular-expression literal, which only `.regex()` takes. `RegExp#source` escapes every `/`
146
+ * and line terminator, so a pattern equal to its own normalized source cannot end the literal early.
147
+ */
148
+ function patternSource(pattern: string | null, where: string): string {
149
+ if (pattern === null) {
150
+ return ''
151
+ }
152
+ let normalized: string
153
+ try {
154
+ normalized = new RegExp(pattern).source
155
+ } catch {
156
+ normalized = ''
157
+ }
158
+ if (normalized !== pattern || pattern === '(?:)') {
159
+ throw new ContractError([`procedure '${where}': the pattern is not one regular-expression literal`])
160
+ }
161
+ return `.regex(/${pattern}/)`
162
+ }
163
+
164
+ function numberSource(shape: NumberShape): string {
165
+ const min = shape.min === null ? '' : `.${shape.exclusiveMin ? 'gt' : 'gte'}(${number(shape.min)})`
166
+ const max = shape.max === null ? '' : `.${shape.exclusiveMax ? 'lt' : 'lte'}(${number(shape.max)})`
167
+ return `${shape.int ? 'z.int()' : 'z.number()'}${min}${max}${bound('multipleOf', shape.multipleOf)}`
168
+ }
@@ -0,0 +1,50 @@
1
+ // `defineVocabulary`: the authorization vocabulary of an application, the first module of its staged kit. It knows
2
+ // nothing else, so the tables, the kit, the groups and the contract can each be typed by it without a cycle.
3
+ // Browser-safe: plain data.
4
+
5
+ import { type AppMembers, type DimensionDef, type Presentation, presentationOf, type RoleDefs, type Vocabulary, type WildcardMessage } from './define'
6
+
7
+ /**
8
+ * The vocabulary as `defineVocabulary` returned it, with its literal names, dimension definitions and the actions of its
9
+ * people (`M`, `never` when it declares none).
10
+ */
11
+ export interface AppVocabulary<A extends string, D extends string, R extends string, Dims, M extends AppMembers<A> = never>
12
+ extends Vocabulary<A, D, R>
13
+ {
14
+ readonly dimensions: Dims & { readonly [K in D]: DimensionDef<A, D, R> }
15
+ readonly members?: M
16
+ }
17
+
18
+ export type AnyAppVocabulary = AppVocabulary<string, string, string, unknown, AppMembers<string>>
19
+
20
+ /**
21
+ * Declares the dimensions, actions and roles, and how IAM names the application (`locale`, `displayName`). `src/vocabulary.ts` exports it; `defineTables`, `defineApp` and the
22
+ * contract take it, so each name is written once.
23
+ */
24
+ export function defineVocabulary<
25
+ const A extends string,
26
+ const D extends string,
27
+ const R extends string,
28
+ const Dims extends { readonly [K in D]: DimensionDef<NoInfer<A>, NoInfer<D>, NoInfer<R>> },
29
+ const M extends AppMembers<NoInfer<A>> = never,
30
+ >(
31
+ vocabulary: Presentation & {
32
+ readonly app: string
33
+ readonly dimensions: Dims & { readonly [K in D]: DimensionDef<NoInfer<A>, NoInfer<D>, NoInfer<R>> }
34
+ readonly actions: { readonly [K in A]: K extends `${string}*${string}` ? WildcardMessage : string }
35
+ readonly roles: RoleDefs<NoInfer<A>, NoInfer<D>, R>
36
+ readonly members?: M
37
+ readonly directory?: 'labels'
38
+ },
39
+ ): AppVocabulary<A, D, R, Dims, M> {
40
+ const people = vocabulary.members
41
+ return {
42
+ app: vocabulary.app,
43
+ dimensions: vocabulary.dimensions,
44
+ actions: vocabulary.actions,
45
+ roles: vocabulary.roles,
46
+ ...presentationOf(vocabulary),
47
+ ...(people === undefined ? {} : { members: people }),
48
+ ...(vocabulary.directory === undefined ? {} : { directory: vocabulary.directory }),
49
+ }
50
+ }
@@ -0,0 +1,25 @@
1
+ // The SPA mounts served from their build directories on disk, as the Bun runtime adapter serves them in a deploy; the
2
+ // test kit serves a built directory the same way.
3
+
4
+ import { rawRequestTarget } from '@fabrika/auth-core'
5
+ import { join, resolve, sep } from 'node:path'
6
+ import type { SpaAssets } from './implement/dispatch.js'
7
+
8
+ /** Serves each SPA mount from its build directory under `root`; a path that names no file gets the SPA's `index.html`. */
9
+ export function diskAssets(root: string): SpaAssets {
10
+ return {
11
+ async serve(mount, request) {
12
+ const dir = resolve(root, mount.dir)
13
+ const pathname = rawRequestTarget(request.url).pathname
14
+ const relative = mount.prefix === '/' ? pathname.slice(1) : pathname.slice(mount.prefix.length + 1)
15
+ // The path is canonical, so every segment decodes and none is `.` or `..`.
16
+ const file = join(dir, ...relative.split('/').filter((segment) => segment !== '').map((segment) => decodeURIComponent(segment)))
17
+ const candidate = file.startsWith(`${dir}${sep}`) ? Bun.file(file) : null
18
+ const served = candidate !== null && (await candidate.exists()) ? candidate : Bun.file(join(dir, 'index.html'))
19
+ if (!(await served.exists())) {
20
+ return new Response('Not found', { status: 404 })
21
+ }
22
+ return new Response(served)
23
+ },
24
+ }
25
+ }
package/src/fetch.ts ADDED
@@ -0,0 +1,10 @@
1
+ // What both runtime adapters serve: a service contract's dispatcher (`serveService`), or any other `{ fetch }`.
2
+
3
+ /** Runtime-neutral background-work capability available while handling a request. */
4
+ export interface RequestExecutionContext {
5
+ waitUntil(promise: Promise<unknown>): void
6
+ }
7
+
8
+ export interface FetchService<Env> {
9
+ fetch(request: Request, env: Env, exec: RequestExecutionContext): Promise<Response>
10
+ }