@fabrika/app 0.3.3 → 0.4.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 (116) hide show
  1. package/README.md +78 -90
  2. package/package.json +77 -6
  3. package/skill/SKILL.md +319 -0
  4. package/skill/references/app-setup.md +370 -0
  5. package/skill/references/dev.md +90 -0
  6. package/skill/references/endpoint.md +640 -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 +746 -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 +308 -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 +331 -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 +1932 -0
  30. package/src/contract/compile.ts +143 -0
  31. package/src/contract/connect.ts +170 -0
  32. package/src/contract/context.ts +444 -0
  33. package/src/contract/declaration.ts +310 -0
  34. package/src/contract/defaults.ts +161 -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 +824 -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 +120 -0
  64. package/src/implement/connect.ts +386 -0
  65. package/src/implement/dispatch.ts +1095 -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 +760 -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 +281 -0
  90. package/src/react/index.ts +34 -0
  91. package/src/react/provider.tsx +179 -0
  92. package/src/react/store.ts +215 -0
  93. package/src/react/testing.ts +144 -0
  94. package/src/react/types.ts +40 -0
  95. package/src/rpc/dispatcher.ts +4 -19
  96. package/src/service/define.ts +121 -0
  97. package/src/service/index.ts +21 -0
  98. package/src/service/serve.ts +99 -0
  99. package/src/testing/caller.ts +123 -0
  100. package/src/testing/challenge.ts +10 -0
  101. package/src/testing/dev.ts +99 -0
  102. package/src/testing/endpoint-access.ts +809 -0
  103. package/src/testing/index.ts +36 -0
  104. package/src/testing/isolation.ts +25 -0
  105. package/src/testing/kit.ts +585 -0
  106. package/src/testing/membership.ts +797 -0
  107. package/src/testing/policy-isolation.ts +827 -0
  108. package/src/testing/preload.ts +6 -0
  109. package/src/testing/proxy.ts +87 -0
  110. package/src/testing/runner.ts +61 -0
  111. package/src/testing/status.ts +19 -0
  112. package/src/undumpable.ts +42 -0
  113. package/src/app.ts +0 -76
  114. package/src/middleware.ts +0 -38
  115. package/src/router.ts +0 -149
  116. package/src/testing.ts +0 -119
@@ -0,0 +1,567 @@
1
+ // `defineContract`: the application's whole access surface as data. Browser-safe: it builds plain objects and
2
+ // holds zod schemas; it validates nothing at runtime, `compileContract` does.
3
+
4
+ import type { BuiltinRoleKey, ChallengeProvider, Locale, MailTemplates, RpcCaller } from '@fabrika/auth-core'
5
+ import type { z } from 'zod/v4'
6
+ import {
7
+ type CronDeclaration,
8
+ type CspDeclaration,
9
+ type ExportAccessDeclaration,
10
+ type ExportDeclaration,
11
+ type FilesDeclaration,
12
+ type FormMountDeclaration,
13
+ type HealthMountDeclaration,
14
+ type ImportDeclaration,
15
+ type LinkAccessDeclaration,
16
+ type LinkMountDeclaration,
17
+ type ProcedureDeclaration,
18
+ type ProcedureTreeDeclaration,
19
+ type ProcedureType,
20
+ type RpcMountDeclaration,
21
+ type SpaAccessDeclaration,
22
+ type SpaMountDeclaration,
23
+ unknownOptions,
24
+ type WebhookMountDeclaration,
25
+ } from './declaration'
26
+
27
+ export type Path = `/${string}`
28
+
29
+ /** Any zod 4 schema. `compileContract` refuses every form outside the contract subset. */
30
+ export type Schema = z.core.$ZodType
31
+
32
+ /**
33
+ * An input object. zod 4's types do not tell `z.object` from `z.strictObject`, so the type refuses only loose
34
+ * objects; `compileContract` and `@fabrika/check` refuse `z.object` too.
35
+ */
36
+ export type InputSchema = z.ZodObject<z.core.$ZodShape, z.core.$ZodObjectConfig>
37
+
38
+ type LooseMessage = 'a loose object admits undeclared keys: declare it with z.strictObject({ … })'
39
+
40
+ /** `S`, unless it is an object that admits undeclared keys (`.loose()`, `.passthrough()`, `.catchall()`). */
41
+ export type NotLoose<S> = S extends z.ZodObject<z.core.$ZodShape, infer Config extends z.core.$ZodObjectConfig>
42
+ ? string extends keyof Config['out'] ? LooseMessage : S
43
+ : S
44
+
45
+ /** Fields a builder sets itself: an options object that carries one does not type-check. */
46
+ type Owned<K extends string> = { readonly [P in K]?: never }
47
+
48
+ export type WildcardMessage = 'an action is exact: a role grants listed actions, and * is not an action'
49
+
50
+ /** What a vocabulary's role of a reserved name must be instead: the refusal, at the role's own line. */
51
+ export type ReservedRoleMessage =
52
+ `${BuiltinRoleKey} is a role IAM defines itself for every application (reserved: ${BuiltinRoleKey}): choose another name`
53
+
54
+ /** A vocabulary's roles: each a `RoleDef`, and none of a name IAM reserves. */
55
+ export type RoleDefs<A extends string, D extends string, R extends string> =
56
+ & { readonly [K in R]: RoleDef<A, D, NoInfer<R>> }
57
+ & (string extends R ? unknown : { readonly [K in NoInfer<R> & BuiltinRoleKey]: ReservedRoleMessage })
58
+
59
+ // ── Vocabulary ──────────────────────────────────────────────────────────────────────────────────
60
+
61
+ export interface DimensionDef<A extends string, D extends string, R extends string> {
62
+ /** Containment: every value of this dimension lies in exactly one value of `in`. */
63
+ readonly in?: D
64
+ /** `creates: <dimension>` mints a value and grants the caller this role in it. */
65
+ readonly creator?: R
66
+ /** The action that manages the dimension's members, and the one that lists them. */
67
+ readonly members?: { readonly manage: A; readonly list: A }
68
+ /**
69
+ * `'labels'`: `ctx.members.list()` of a `members.list` endpoint also answers each member's label, for a people
70
+ * picker. A user's label spells its address, so this widens what members see of each other: a reviewed declaration.
71
+ */
72
+ readonly directory?: 'labels'
73
+ }
74
+
75
+ export interface RoleDef<A extends string, D extends string, R extends string> {
76
+ /** Exact actions only: `'*'` and `'notes.*'` are not actions of this contract. */
77
+ readonly grants: readonly A[]
78
+ /**
79
+ * A membership role, granted in one value of `scope`. `assignableBy` names who may assign it there: a holder of one
80
+ * of its membership roles of `scope` in that value, or of an application-wide role (one with no `member`) globally.
81
+ */
82
+ readonly member?: { readonly scope: D; readonly assignableBy: readonly R[] }
83
+ /**
84
+ * An external principal, a person from outside the organization, may hold this membership role (ADR-0051). Absent,
85
+ * only internal people hold it. Only a role with `member` may declare it; an internal person may hold it too.
86
+ */
87
+ readonly external?: true
88
+ }
89
+
90
+ export interface Vocabulary<A extends string, D extends string, R extends string> extends Presentation, AppPeople<A> {
91
+ readonly app: string
92
+ readonly dimensions: { readonly [K in D]: DimensionDef<A, D, R> }
93
+ readonly actions: { readonly [K in A]: string }
94
+ readonly roles: { readonly [K in R]: RoleDef<A, D, R> }
95
+ }
96
+
97
+ /** The actions that manage and list the application's own people. */
98
+ export interface AppMembers<A extends string> {
99
+ readonly manage: A
100
+ readonly list: A
101
+ }
102
+
103
+ /**
104
+ * The application's own people: every holder of one of its roles, globally or in a value. A holder of `members.manage`,
105
+ * globally, grants and revokes the application's global roles in IAM's console within its own actions; an `app: true`
106
+ * endpoint whose action is `members.list` reads them (`ctx.members.list()`).
107
+ */
108
+ export interface AppPeople<A extends string> {
109
+ readonly members?: AppMembers<A>
110
+ /** `'labels'`: the directory also answers each person's label to a `members.list` holder. A reviewed declaration. */
111
+ readonly directory?: 'labels'
112
+ }
113
+
114
+ /** A vocabulary's people fields, copied one by one: each exists only when declared. */
115
+ export function peopleOf<A extends string>(vocabulary: AppPeople<A>): AppPeople<A> {
116
+ return {
117
+ ...(vocabulary.members === undefined ? {} : { members: { manage: vocabulary.members.manage, list: vocabulary.members.list } }),
118
+ ...(vocabulary.directory === undefined ? {} : { directory: vocabulary.directory }),
119
+ }
120
+ }
121
+
122
+ /** How IAM names the application to a person: on its sign-in, password and invitation pages, and in its e-mails. */
123
+ export interface Presentation {
124
+ /** The language of those pages and e-mails; absent, the installation's default. */
125
+ readonly locale?: Locale
126
+ /** The application's name there, plain text of one short line; absent, a wording without a name. */
127
+ readonly displayName?: string
128
+ /** The application's subject and text of IAM's e-mails, by message and language; absent, IAM's own wording. */
129
+ readonly mail?: MailTemplates
130
+ }
131
+
132
+ /** A vocabulary's presentation fields, copied one by one: each exists only when declared. */
133
+ export function presentationOf(vocabulary: Presentation): Presentation {
134
+ return {
135
+ ...(vocabulary.locale === undefined ? {} : { locale: vocabulary.locale }),
136
+ ...(vocabulary.displayName === undefined ? {} : { displayName: vocabulary.displayName }),
137
+ ...(vocabulary.mail === undefined ? {} : { mail: vocabulary.mail }),
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Top-level dimensions that declare a `creator` role; only they can be the target of `creates`. IAM creates a value
143
+ * with no container (`createScope`); a contained value is a row its container's members insert.
144
+ */
145
+ export type CreatableDimension<Dims> =
146
+ & {
147
+ [K in keyof Dims]: Dims[K] extends { readonly creator: string } ? Dims[K] extends { readonly in: string } ? never : K : never
148
+ }[keyof Dims]
149
+ & string
150
+
151
+ // ── Access requirements ─────────────────────────────────────────────────────────────────────────
152
+
153
+ export interface MemberAccess {
154
+ readonly member: true
155
+ }
156
+ export interface OverAccess<A extends string, D extends string> {
157
+ readonly action: A
158
+ readonly over: D
159
+ }
160
+ export interface InAccess<A extends string, D extends string, F extends string> {
161
+ readonly action: A
162
+ readonly in: D
163
+ readonly from: F
164
+ }
165
+ export interface OnAccess<A extends string, D extends string, F extends string> {
166
+ readonly action: A
167
+ readonly on: D
168
+ readonly from: F
169
+ }
170
+ export interface CreatesAccess<A extends string, D extends string> {
171
+ readonly action: A
172
+ readonly creates: D
173
+ }
174
+ /** The action held as a global grant: data of the whole application (`ctx.db.app`). */
175
+ export interface AppAccess<A extends string> {
176
+ readonly action: A
177
+ readonly app: true
178
+ }
179
+ export interface LinkAccess<A extends string> {
180
+ readonly action: A
181
+ }
182
+ /** An SPA anyone opens, with no session; the page reaches only the anonymous endpoints. */
183
+ export interface PublicAccess {
184
+ readonly public: true
185
+ }
186
+
187
+ /** Without an `input`, TS infers `I` as its whole constraint, not the `null` default; both mean "no input". */
188
+ export type InputOf<I> = [I] extends [InputSchema] ? z.output<I> : Record<never, never>
189
+
190
+ /** Input fields of string type: the only fields a scope value can be read from. */
191
+ export type StringField<In> = { [K in keyof In]-?: In[K] extends string ? K : never }[keyof In] & string
192
+
193
+ export type QueryAccess<A extends string, D extends string, In> =
194
+ | MemberAccess
195
+ | OverAccess<A, D>
196
+ | InAccess<A, D, StringField<In>>
197
+ | OnAccess<A, D, StringField<In>>
198
+ | AppAccess<A>
199
+
200
+ export type MutationAccess<A extends string, D extends string, CD extends string, In> = QueryAccess<A, D, In> | CreatesAccess<A, CD>
201
+
202
+ // ── Endpoints and mounts ────────────────────────────────────────────────────────────────────────
203
+
204
+ export interface Procedure<T extends ProcedureType, I extends InputSchema | null, O extends Schema, Acc> extends ProcedureDeclaration<Schema, Acc> {
205
+ readonly type: T
206
+ readonly input: I | null
207
+ readonly output: O
208
+ }
209
+
210
+ export type ProcedureTree = ProcedureTreeDeclaration<Schema>
211
+
212
+ export type LinkProcedureTree = ProcedureTreeDeclaration<Schema, LinkAccessDeclaration>
213
+
214
+ /** Exports serve queries only: a read-only context, so retrying one is safe. */
215
+ export interface QueryTree {
216
+ readonly [name: string]: (ProcedureDeclaration<Schema, ExportAccessDeclaration> & { readonly type: 'query' }) | QueryTree
217
+ }
218
+
219
+ export interface ProcedureDef<I, O, Acc> extends Owned<'~kind' | 'type'> {
220
+ readonly input?: I & NotLoose<I>
221
+ readonly output: O & NotLoose<O>
222
+ readonly access: Acc
223
+ }
224
+
225
+ export interface RpcMount<C extends readonly [RpcCaller, ...RpcCaller[]], T extends ProcedureTree> extends RpcMountDeclaration<Schema> {
226
+ readonly callers: C
227
+ readonly procedures: T
228
+ }
229
+
230
+ export interface LinkMount<D extends string, G extends string, IB extends string, T extends LinkProcedureTree> extends LinkMountDeclaration<Schema> {
231
+ readonly dimension: D
232
+ readonly grants: readonly G[]
233
+ readonly issuedBy: IB
234
+ readonly procedures: T
235
+ }
236
+
237
+ export interface SpaMount<Acc extends SpaAccessDeclaration> extends SpaMountDeclaration {
238
+ readonly access: Acc
239
+ }
240
+
241
+ export type HealthMount = HealthMountDeclaration
242
+
243
+ export interface FormMount<I extends Schema, T extends string = string> extends FormMountDeclaration<Schema> {
244
+ readonly input: I
245
+ readonly then?: T
246
+ }
247
+
248
+ export interface WebhookMount<B extends string, Acts extends string, I extends Schema> extends WebhookMountDeclaration<Schema> {
249
+ readonly boundTo: B
250
+ readonly actions: readonly Acts[]
251
+ readonly input: I
252
+ }
253
+
254
+ /** `L` is which link mounts' holders reach the collection: the typed client gives a link mount's holders only those. */
255
+ /** `link` of `c.files`: every link mount bound to the owner's dimension, none, or the named ones. */
256
+ export type LinkServing = boolean | readonly string[]
257
+
258
+ export interface FileCollection<A extends string, L extends LinkServing = LinkServing> extends FilesDeclaration {
259
+ readonly read: A
260
+ readonly write: A
261
+ readonly delete: A
262
+ readonly link?: L
263
+ }
264
+
265
+ export interface CronJob<Acts extends string, Sc extends string> extends CronDeclaration {
266
+ readonly actions: readonly Acts[]
267
+ readonly scope: Sc
268
+ }
269
+
270
+ export interface Export<T extends QueryTree> extends ExportDeclaration<Schema> {
271
+ readonly procedures: T
272
+ }
273
+
274
+ export type AnyMount =
275
+ | RpcMount<readonly [RpcCaller, ...RpcCaller[]], ProcedureTree>
276
+ | LinkMount<string, string, string, LinkProcedureTree>
277
+ | SpaMount<SpaAccessDeclaration>
278
+ | HealthMount
279
+ | FormMount<Schema>
280
+ | WebhookMount<string, string, Schema>
281
+
282
+ /** What the callback of `defineContract` returns: mounts by name, and the sections `SURFACE_SECTIONS` names. */
283
+ export interface SurfaceSections {
284
+ readonly files?: { readonly [name: string]: FileCollection<string> }
285
+ readonly jobs?: { readonly [name: string]: CronJob<string, string> }
286
+ readonly exports?: { readonly [name: string]: Export<QueryTree> }
287
+ readonly imports?: { readonly [name: string]: ImportDeclaration }
288
+ readonly csp?: CspDeclaration
289
+ }
290
+
291
+ export interface Surface extends SurfaceSections {
292
+ readonly [name: string]: AnyMount | SurfaceSections[keyof SurfaceSections]
293
+ }
294
+
295
+ /** What a form's `then` must be instead: the refusal, at the form's own line. */
296
+ export type FormJobMessage = "then names a job of the contract's jobs section"
297
+
298
+ /** Each form's `then` names a job of the same surface's `jobs` section. */
299
+ export type FormJobs<S> = {
300
+ readonly [K in keyof S]: S[K] extends { readonly kind: 'form'; readonly then?: infer T }
301
+ ? [T] extends [string] ? { readonly then?: (S extends { readonly jobs: infer J } ? keyof J & string : never) | FormJobMessage }
302
+ : unknown
303
+ : unknown
304
+ }
305
+
306
+ /** A form's challenge: the provider and the application variables that hold its secret and its public site key. */
307
+ export interface FormChallengeOption {
308
+ readonly provider: ChallengeProvider
309
+ readonly secret: string
310
+ /** Served by every SPA mount at `<prefix>/__fabrika-config.json`, so the page renders the widget with it. */
311
+ readonly siteKey?: string
312
+ }
313
+
314
+ /** What `c.form(path, options)` takes. */
315
+ export type FormOptions<I, T> =
316
+ & {
317
+ readonly input: I & NotLoose<I>
318
+ readonly appendTo: string
319
+ readonly maxBytes?: number
320
+ readonly rate?: { readonly perClient?: number; readonly perForm?: number }
321
+ readonly honeypot?: string
322
+ readonly challenge?: FormChallengeOption
323
+ /** The cron job each accepted submission kicks, in the background of the constant answer. */
324
+ readonly then?: T
325
+ }
326
+ & Owned<'kind' | 'path'>
327
+
328
+ export interface LinkKit<A extends string> {
329
+ query<const Acc extends LinkAccess<A>, O extends Schema, I extends InputSchema | null = null>(
330
+ def: ProcedureDef<I, O, Acc>,
331
+ ): Procedure<'query', I, O, Acc>
332
+ mutation<const Acc extends LinkAccess<A>, O extends Schema, I extends InputSchema | null = null>(
333
+ def: ProcedureDef<I, O, Acc>,
334
+ ): Procedure<'mutation', I, O, Acc>
335
+ }
336
+
337
+ export interface Kit<A extends string, D extends string, CD extends string> {
338
+ query<const Acc extends QueryAccess<A, D, InputOf<I>>, O extends Schema, I extends InputSchema | null = null>(
339
+ def: ProcedureDef<I, O, Acc>,
340
+ ): Procedure<'query', I, O, Acc>
341
+ mutation<const Acc extends MutationAccess<A, D, CD, InputOf<I>>, O extends Schema, I extends InputSchema | null = null>(
342
+ def: ProcedureDef<I, O, Acc>,
343
+ ): Procedure<'mutation', I, O, Acc>
344
+ /** Procedures served at `POST <prefix>/<name>`, to the callers named once here. There is no public caller. */
345
+ rpc<const C extends readonly [RpcCaller, ...RpcCaller[]], const T extends ProcedureTree>(
346
+ prefix: Path,
347
+ options: { readonly callers: C } & Owned<'kind' | 'prefix' | 'procedures'>,
348
+ procedures: T,
349
+ ): RpcMount<C, T>
350
+ /** A share SPA under `prefix` and its procedures under `<prefix>/api`, for an anonymous key bound to one object. */
351
+ link<const LD extends D, const G extends readonly [A, ...A[]], const IB extends A, const T extends LinkProcedureTree>(
352
+ prefix: Path,
353
+ options: {
354
+ readonly spa: string
355
+ readonly dimension: LD
356
+ readonly grants: G
357
+ readonly issuedBy: IB
358
+ readonly maxDays: number
359
+ readonly procedures: (l: LinkKit<G[number]>) => T
360
+ } & Owned<'kind' | 'prefix'>,
361
+ ): LinkMount<LD, G[number], IB, T>
362
+ /** The SPA under `prefix`: for signed-in people, or for anyone with `access: { public: true }`. */
363
+ spa<const Acc extends MemberAccess | OverAccess<A, D> | AppAccess<A> | PublicAccess>(
364
+ prefix: Path,
365
+ options: {
366
+ readonly dir: string
367
+ readonly access: Acc
368
+ /** Application variables whose values the pages read from `<prefix>/__fabrika-config.json`: public values only. */
369
+ readonly public?: readonly [string, ...string[]]
370
+ } & Owned<'kind' | 'prefix'>,
371
+ ): SpaMount<Acc>
372
+ health(path: Path): HealthMount
373
+ form<I extends InputSchema, const T extends string = never>(path: Path, options: FormOptions<I, T>): FormMount<I, NoInfer<T>>
374
+ webhook<const B extends D | 'app', const Acts extends readonly [A, ...A[]], I extends InputSchema>(
375
+ path: Path,
376
+ options: {
377
+ readonly scheme: 'github-sha256'
378
+ readonly boundTo: B
379
+ readonly actions: Acts
380
+ readonly input: I & NotLoose<I>
381
+ readonly maxBytes?: number
382
+ } & Owned<'kind' | 'path'>,
383
+ ): WebhookMount<B, Acts[number], I>
384
+ files<const L extends boolean | readonly [string, ...string[]] = false>(
385
+ collection: {
386
+ readonly owner: string
387
+ readonly read: A
388
+ readonly write: A
389
+ readonly delete: A
390
+ readonly maxBytes: number
391
+ readonly types: readonly [string, ...string[]]
392
+ readonly maxPerOwner?: number
393
+ /** `true`: every link mount bound to the owner's dimension serves the files; a list: the named link mounts only. */
394
+ readonly link?: L
395
+ /** Each upload is scanned for malware and served only after a clean verdict; until then it answers 409. */
396
+ readonly scan?: boolean
397
+ } & Owned<'kind'>,
398
+ ): FileCollection<A, L>
399
+ cron<const Acts extends readonly [A, ...A[]], const Sc extends D | 'app'>(
400
+ schedule: string,
401
+ options: { readonly actions: Acts; readonly scope: Sc; readonly timeout?: string; readonly retries?: number } & Owned<'kind' | 'schedule'>,
402
+ ): CronJob<Acts[number], Sc>
403
+ export<const T extends QueryTree>(
404
+ definition: {
405
+ readonly version: number
406
+ readonly consumers: readonly [string, ...string[]]
407
+ readonly audience: 'any consumer user'
408
+ readonly procedures: T
409
+ } & Owned<'kind'>,
410
+ ): Export<T>
411
+ }
412
+
413
+ // ── The contract ────────────────────────────────────────────────────────────────────────────────
414
+
415
+ export interface Contract<A extends string, D extends string, R extends string, Dims, S extends Surface> {
416
+ readonly vocabulary: Vocabulary<A, D, R>
417
+ /** The dimension definitions as written, with their literal actions and roles, for the types only. */
418
+ readonly dimensions: Dims
419
+ readonly surface: S
420
+ }
421
+
422
+ export type AnyContract = Contract<string, string, string, unknown, Surface>
423
+
424
+ export type ActionOf<C> = C extends Contract<infer A, string, string, unknown, Surface> ? A : never
425
+ export type DimensionOf<C> = C extends Contract<string, infer D, string, unknown, Surface> ? D : never
426
+ export type RoleOf<C> = C extends Contract<string, string, infer R, unknown, Surface> ? R : never
427
+
428
+ function procedure<T extends ProcedureType, I extends InputSchema | null, O extends Schema, Acc>(
429
+ type: T,
430
+ def: ProcedureDef<I, O, Acc>,
431
+ ): Procedure<T, I, O, Acc> {
432
+ return {
433
+ '~kind': 'procedure',
434
+ type,
435
+ input: def.input ?? null,
436
+ output: def.output,
437
+ access: def.access,
438
+ unknownOptions: unknownOptions(def, 'procedure'),
439
+ }
440
+ }
441
+
442
+ function linkKit<A extends string>(): LinkKit<A> {
443
+ return {
444
+ query: (def) => procedure('query', def),
445
+ mutation: (def) => procedure('mutation', def),
446
+ }
447
+ }
448
+
449
+ /** The builders `defineContract` hands its callback; the staged kit's contract reuses its mounts that take no handler. */
450
+ export function surfaceKit<A extends string, D extends string, CD extends string>(): Kit<A, D, CD> {
451
+ return {
452
+ query: (def) => procedure('query', def),
453
+ mutation: (def) => procedure('mutation', def),
454
+ rpc: (prefix, options, procedures) => ({
455
+ kind: 'rpc',
456
+ prefix,
457
+ callers: options.callers,
458
+ procedures,
459
+ unknownOptions: unknownOptions(options, 'rpc'),
460
+ }),
461
+ link: (prefix, options) => ({
462
+ kind: 'link',
463
+ prefix,
464
+ spa: options.spa,
465
+ dimension: options.dimension,
466
+ grants: options.grants,
467
+ issuedBy: options.issuedBy,
468
+ maxDays: options.maxDays,
469
+ procedures: options.procedures(linkKit()),
470
+ unknownOptions: unknownOptions(options, 'link'),
471
+ }),
472
+ spa: (prefix, options) => ({
473
+ kind: 'spa',
474
+ prefix,
475
+ dir: options.dir,
476
+ access: options.access,
477
+ public: options.public,
478
+ unknownOptions: unknownOptions(options, 'spa'),
479
+ }),
480
+ health: (path) => ({ kind: 'health', path }),
481
+ form: (path, options) => ({
482
+ kind: 'form',
483
+ path,
484
+ input: options.input,
485
+ appendTo: options.appendTo,
486
+ maxBytes: options.maxBytes,
487
+ rate: options.rate,
488
+ honeypot: options.honeypot,
489
+ challenge: options.challenge,
490
+ then: options.then,
491
+ unknownOptions: unknownOptions(options, 'form'),
492
+ }),
493
+ webhook: (path, options) => ({
494
+ kind: 'webhook',
495
+ path,
496
+ scheme: options.scheme,
497
+ boundTo: options.boundTo,
498
+ actions: options.actions,
499
+ input: options.input,
500
+ maxBytes: options.maxBytes,
501
+ unknownOptions: unknownOptions(options, 'webhook'),
502
+ }),
503
+ files: (collection) => ({
504
+ kind: 'files',
505
+ owner: collection.owner,
506
+ read: collection.read,
507
+ write: collection.write,
508
+ delete: collection.delete,
509
+ maxBytes: collection.maxBytes,
510
+ types: collection.types,
511
+ maxPerOwner: collection.maxPerOwner,
512
+ link: collection.link,
513
+ scan: collection.scan,
514
+ unknownOptions: unknownOptions(collection, 'files'),
515
+ }),
516
+ cron: (schedule, options) => ({
517
+ kind: 'cron',
518
+ schedule,
519
+ actions: options.actions,
520
+ scope: options.scope,
521
+ timeout: options.timeout,
522
+ retries: options.retries,
523
+ unknownOptions: unknownOptions(options, 'cron'),
524
+ }),
525
+ export: (definition) => ({
526
+ kind: 'export',
527
+ version: definition.version,
528
+ consumers: definition.consumers,
529
+ audience: definition.audience,
530
+ procedures: definition.procedures,
531
+ unknownOptions: unknownOptions(definition, 'export'),
532
+ }),
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Declares the contract: the authorization vocabulary, then the surface built from `c` — mounts by name, and the
538
+ * `files`, `jobs`, `exports`, `imports` and `csp` sections. The contract is a literal (`@fabrika/check`).
539
+ */
540
+ export function defineContract<
541
+ const A extends string,
542
+ const D extends string,
543
+ const R extends string,
544
+ const Dims extends { readonly [K in D]: DimensionDef<NoInfer<A>, NoInfer<D>, NoInfer<R>> },
545
+ const S extends Surface & FormJobs<S>,
546
+ >(
547
+ vocabulary: Presentation & AppPeople<NoInfer<A>> & {
548
+ readonly app: string
549
+ readonly dimensions: Dims & { readonly [K in D]: DimensionDef<NoInfer<A>, NoInfer<D>, NoInfer<R>> }
550
+ readonly actions: { readonly [K in A]: K extends `${string}*${string}` ? WildcardMessage : string }
551
+ readonly roles: RoleDefs<NoInfer<A>, NoInfer<D>, R>
552
+ },
553
+ surface: (c: Kit<A, D, CreatableDimension<Dims>>) => S,
554
+ ): Contract<A, D, R, Dims, S> {
555
+ return {
556
+ vocabulary: {
557
+ app: vocabulary.app,
558
+ dimensions: vocabulary.dimensions,
559
+ actions: vocabulary.actions,
560
+ roles: vocabulary.roles,
561
+ ...presentationOf(vocabulary),
562
+ ...peopleOf(vocabulary),
563
+ },
564
+ dimensions: vocabulary.dimensions,
565
+ surface: surface(surfaceKit()),
566
+ }
567
+ }
@@ -0,0 +1,25 @@
1
+ // The wire schemas of a collection's list endpoint, shared by the files runtime and the browser client. Browser-safe:
2
+ // zod and the contract's limits only. `FILE_LIST_INPUT` and `FILE_LIST_OUTPUT` of the contract describe the same.
3
+
4
+ import { z } from 'zod/v4'
5
+ import { FILE_LIMITS } from './defaults'
6
+
7
+ /** `files.<name>.list`'s input. */
8
+ export const FileListInput = z.strictObject({
9
+ owner: z.string().min(1),
10
+ after: z.uuid().optional(),
11
+ limit: z.number().int().min(1).max(FILE_LIMITS.listPageMax).optional(),
12
+ })
13
+ /** A link holder's `<mount>.files.<name>.list`: no owner, which is the link's object; `FILE_LINK_LIST_INPUT` describes it. */
14
+ export const FileLinkListInput = FileListInput.omit({ owner: true })
15
+ export const FileListOutput = z.strictObject({
16
+ items: z.array(z.strictObject({
17
+ id: z.uuid(),
18
+ name: z.string().min(1).nullable(),
19
+ type: z.string().min(1),
20
+ size: z.number().int().min(0),
21
+ status: z.enum(['live', 'quarantined', 'rejected']),
22
+ createdAt: z.iso.datetime(),
23
+ })).max(FILE_LIMITS.listPageMax),
24
+ next: z.uuid().nullable(),
25
+ })