@fabrika/app 0.8.2 → 0.9.0-cloudflare.2

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 (201) hide show
  1. package/package.json +40 -24
  2. package/skill/SKILL.md +29 -3
  3. package/skill/references/app-setup.md +29 -11
  4. package/skill/references/dev.md +2 -1
  5. package/skill/references/endpoint.md +12 -3
  6. package/skill/references/errors.md +39 -35
  7. package/skill/references/files.md +4 -2
  8. package/skill/references/jobs.md +9 -0
  9. package/skill/references/table-and-migration.md +19 -2
  10. package/skill/references/tests.md +33 -1
  11. package/skill/references/webhooks-links-connectors.md +1 -1
  12. package/src/build-recipe.ts +198 -0
  13. package/src/build.ts +60 -4
  14. package/src/bun.ts +33 -8
  15. package/src/cloudflare/application.ts +352 -0
  16. package/src/cloudflare/bindings.ts +159 -0
  17. package/src/cloudflare/names.ts +16 -0
  18. package/src/cloudflare/ops.ts +246 -0
  19. package/src/cloudflare/r2-files.ts +172 -0
  20. package/src/cloudflare.ts +99 -146
  21. package/src/contract/app-id.ts +5 -0
  22. package/src/contract/artifact-schema.ts +8 -1
  23. package/src/contract/build.ts +68 -11
  24. package/src/contract/compile.ts +1 -0
  25. package/src/contract/context.ts +9 -0
  26. package/src/contract/declaration.ts +2 -0
  27. package/src/contract/define.ts +28 -2
  28. package/src/contract/group.ts +10 -1
  29. package/src/contract/kit.ts +6 -2
  30. package/src/contract/platform.ts +3 -2
  31. package/src/contract/vocabulary.ts +26 -8
  32. package/src/dev/changes.ts +35 -0
  33. package/src/dev/contract-digest.ts +17 -0
  34. package/src/dev/index.ts +11 -0
  35. package/src/dev/launcher.ts +713 -0
  36. package/src/dev/output.ts +72 -0
  37. package/src/dev/wire.ts +136 -0
  38. package/src/disk-assets.ts +8 -3
  39. package/src/files/archive.ts +2 -2
  40. package/src/files/job-names.ts +12 -0
  41. package/src/files/kinds.ts +19 -7
  42. package/src/files/scan.ts +3 -4
  43. package/src/files/streams.ts +3 -1
  44. package/src/implement/anonymous.ts +11 -1
  45. package/src/implement/boot.ts +70 -7
  46. package/src/implement/bounded.ts +21 -2
  47. package/src/implement/capability.ts +18 -3
  48. package/src/implement/dispatch.ts +251 -147
  49. package/src/implement/egress.ts +341 -8
  50. package/src/implement/failures.ts +17 -1
  51. package/src/implement/faults.ts +1 -0
  52. package/src/implement/index.ts +1 -0
  53. package/src/implement/invocation.ts +18 -9
  54. package/src/implement/principal.ts +25 -2
  55. package/src/implement/terminals.ts +93 -0
  56. package/src/implement/workload.ts +24 -13
  57. package/src/jobs/report.ts +5 -1
  58. package/src/jobs/runner.ts +40 -19
  59. package/src/jobs/schedule.ts +7 -1
  60. package/src/jobs/scrub.ts +14 -1
  61. package/src/spa-fallback.ts +12 -0
  62. package/src/testing/endpoint-access.ts +307 -152
  63. package/src/testing/isolation.ts +8 -1
  64. package/src/testing/kit.ts +20 -2
  65. package/src/testing/proxy.ts +5 -5
  66. package/types/src/agent-skill.d.ts +29 -0
  67. package/types/src/build-recipe.d.ts +70 -0
  68. package/types/src/build.d.ts +29 -0
  69. package/types/src/bun.d.ts +66 -0
  70. package/types/src/client/caller.d.ts +19 -0
  71. package/types/src/client/contract.d.ts +131 -0
  72. package/types/src/client/error-reporting.d.ts +66 -0
  73. package/types/src/client/index.d.ts +4 -0
  74. package/types/src/client/rpc.d.ts +14 -0
  75. package/types/src/client/share.d.ts +2 -0
  76. package/types/src/client/transport.d.ts +42 -0
  77. package/types/src/cloudflare/application.d.ts +42 -0
  78. package/types/src/cloudflare/bindings.d.ts +33 -0
  79. package/types/src/cloudflare/names.d.ts +11 -0
  80. package/types/src/cloudflare/ops.d.ts +46 -0
  81. package/types/src/cloudflare/r2-files.d.ts +9 -0
  82. package/types/src/cloudflare.d.ts +42 -0
  83. package/types/src/contract/app-id.d.ts +2 -0
  84. package/types/src/contract/artifact-schema.d.ts +8 -0
  85. package/types/src/contract/attachment.d.ts +41 -0
  86. package/types/src/contract/build.d.ts +12 -0
  87. package/types/src/contract/compile.d.ts +11 -0
  88. package/types/src/contract/connect.d.ts +51 -0
  89. package/types/src/contract/context.d.ts +510 -0
  90. package/types/src/contract/declaration.d.ts +307 -0
  91. package/types/src/contract/defaults.d.ts +208 -0
  92. package/types/src/contract/define.d.ts +376 -0
  93. package/types/src/contract/file.d.ts +44 -0
  94. package/types/src/contract/group.d.ts +282 -0
  95. package/types/src/contract/handled.d.ts +18 -0
  96. package/types/src/contract/index.d.ts +19 -0
  97. package/types/src/contract/kit.d.ts +211 -0
  98. package/types/src/contract/platform.d.ts +9 -0
  99. package/types/src/contract/problems.d.ts +14 -0
  100. package/types/src/contract/redirect.d.ts +25 -0
  101. package/types/src/contract/schema-from-shape.d.ts +10 -0
  102. package/types/src/contract/schema-shape.d.ts +5 -0
  103. package/types/src/contract/snapshot-source.d.ts +15 -0
  104. package/types/src/contract/token.d.ts +58 -0
  105. package/types/src/contract/vocabulary.d.ts +32 -0
  106. package/types/src/contract/webhook-binding.d.ts +24 -0
  107. package/types/src/contract/webhook-scheme.d.ts +23 -0
  108. package/types/src/dev/changes.d.ts +7 -0
  109. package/types/src/dev/contract-digest.d.ts +1 -0
  110. package/types/src/dev/index.d.ts +2 -0
  111. package/types/src/dev/launcher.d.ts +32 -0
  112. package/types/src/dev/output.d.ts +28 -0
  113. package/types/src/dev/wire.d.ts +126 -0
  114. package/types/src/disk-assets.d.ts +6 -0
  115. package/types/src/errors.d.ts +46 -0
  116. package/types/src/fetch.d.ts +7 -0
  117. package/types/src/files/archive.d.ts +26 -0
  118. package/types/src/files/clamscan.d.ts +32 -0
  119. package/types/src/files/handle.d.ts +106 -0
  120. package/types/src/files/job-names.d.ts +9 -0
  121. package/types/src/files/kinds.d.ts +101 -0
  122. package/types/src/files/scan.d.ts +78 -0
  123. package/types/src/files/sniff.d.ts +7 -0
  124. package/types/src/files/streams.d.ts +17 -0
  125. package/types/src/files/treatment.d.ts +18 -0
  126. package/types/src/from-row.d.ts +20 -0
  127. package/types/src/honeypot.d.ts +2 -0
  128. package/types/src/implement/anonymous.d.ts +28 -0
  129. package/types/src/implement/attachment.d.ts +14 -0
  130. package/types/src/implement/boot.d.ts +120 -0
  131. package/types/src/implement/bounded.d.ts +32 -0
  132. package/types/src/implement/browser-config.d.ts +19 -0
  133. package/types/src/implement/capability.d.ts +144 -0
  134. package/types/src/implement/challenge.d.ts +75 -0
  135. package/types/src/implement/client-address.d.ts +11 -0
  136. package/types/src/implement/connect.d.ts +67 -0
  137. package/types/src/implement/crashes.d.ts +31 -0
  138. package/types/src/implement/dispatch.d.ts +221 -0
  139. package/types/src/implement/egress.d.ts +49 -0
  140. package/types/src/implement/environment.d.ts +14 -0
  141. package/types/src/implement/failures.d.ts +39 -0
  142. package/types/src/implement/faults.d.ts +45 -0
  143. package/types/src/implement/headers.d.ts +7 -0
  144. package/types/src/implement/implement.d.ts +58 -0
  145. package/types/src/implement/index.d.ts +25 -0
  146. package/types/src/implement/invocation.d.ts +125 -0
  147. package/types/src/implement/membership.d.ts +201 -0
  148. package/types/src/implement/multipart.d.ts +36 -0
  149. package/types/src/implement/params.d.ts +7 -0
  150. package/types/src/implement/principal.d.ts +57 -0
  151. package/types/src/implement/project.d.ts +11 -0
  152. package/types/src/implement/public.d.ts +61 -0
  153. package/types/src/implement/serve.d.ts +25 -0
  154. package/types/src/implement/terminals.d.ts +33 -0
  155. package/types/src/implement/tokens.d.ts +49 -0
  156. package/types/src/implement/types.d.ts +243 -0
  157. package/types/src/implement/urls.d.ts +15 -0
  158. package/types/src/implement/verifiers.d.ts +19 -0
  159. package/types/src/implement/webhook.d.ts +55 -0
  160. package/types/src/implement/workload.d.ts +56 -0
  161. package/types/src/implement/xlsx.d.ts +6 -0
  162. package/types/src/index.d.ts +12 -0
  163. package/types/src/jobs/exception.d.ts +40 -0
  164. package/types/src/jobs/mail.d.ts +67 -0
  165. package/types/src/jobs/report.d.ts +56 -0
  166. package/types/src/jobs/runner.d.ts +154 -0
  167. package/types/src/jobs/schedule.d.ts +17 -0
  168. package/types/src/jobs/scrub.d.ts +12 -0
  169. package/types/src/jobs/types.d.ts +98 -0
  170. package/types/src/react/caller.d.ts +30 -0
  171. package/types/src/react/errors.d.ts +40 -0
  172. package/types/src/react/hooks.d.ts +110 -0
  173. package/types/src/react/index.d.ts +5 -0
  174. package/types/src/react/provider.d.ts +58 -0
  175. package/types/src/react/store.d.ts +68 -0
  176. package/types/src/react/testing.d.ts +28 -0
  177. package/types/src/react/types.d.ts +33 -0
  178. package/types/src/rpc/builder.d.ts +52 -0
  179. package/types/src/rpc/contract.d.ts +23 -0
  180. package/types/src/rpc/dispatcher.d.ts +11 -0
  181. package/types/src/rpc/types.d.ts +62 -0
  182. package/types/src/service/define.d.ts +42 -0
  183. package/types/src/service/index.d.ts +3 -0
  184. package/types/src/service/serve.d.ts +28 -0
  185. package/types/src/spa-fallback.d.ts +2 -0
  186. package/types/src/standard-schema.d.ts +55 -0
  187. package/types/src/testing/caller.d.ts +95 -0
  188. package/types/src/testing/challenge.d.ts +11 -0
  189. package/types/src/testing/dev.d.ts +97 -0
  190. package/types/src/testing/endpoint-access.d.ts +131 -0
  191. package/types/src/testing/index.d.ts +12 -0
  192. package/types/src/testing/isolation.d.ts +3 -0
  193. package/types/src/testing/kit.d.ts +187 -0
  194. package/types/src/testing/membership.d.ts +132 -0
  195. package/types/src/testing/policy-isolation.d.ts +113 -0
  196. package/types/src/testing/preload.d.ts +1 -0
  197. package/types/src/testing/proxy.d.ts +40 -0
  198. package/types/src/testing/runner.d.ts +8 -0
  199. package/types/src/testing/sample-file.d.ts +5 -0
  200. package/types/src/testing/status.d.ts +4 -0
  201. package/types/src/undumpable.d.ts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/app",
3
- "version": "0.8.2",
3
+ "version": "0.9.0-cloudflare.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,77 +14,93 @@
14
14
  "files": [
15
15
  "src",
16
16
  "skill",
17
- "!src/**/__tests__/**"
17
+ "!src/**/__tests__/**",
18
+ "types"
18
19
  ],
19
20
  "exports": {
20
21
  ".": {
21
22
  "bun": "./src/index.ts",
22
- "types": "./src/index.ts",
23
+ "types": "./types/src/index.d.ts",
23
24
  "default": "./src/index.ts"
24
25
  },
25
26
  "./bun": {
26
27
  "bun": "./src/bun.ts",
27
- "types": "./src/bun.ts",
28
+ "types": "./types/src/bun.d.ts",
28
29
  "default": "./src/bun.ts"
29
30
  },
30
31
  "./cloudflare": {
31
32
  "bun": "./src/cloudflare.ts",
32
- "types": "./src/cloudflare.ts",
33
+ "types": "./types/src/cloudflare.d.ts",
33
34
  "default": "./src/cloudflare.ts"
34
35
  },
35
36
  "./contract": {
36
37
  "bun": "./src/contract/index.ts",
37
- "types": "./src/contract/index.ts",
38
+ "types": "./types/src/contract/index.d.ts",
38
39
  "default": "./src/contract/index.ts"
39
40
  },
40
41
  "./client": {
41
42
  "bun": "./src/client/index.ts",
42
- "types": "./src/client/index.ts",
43
+ "types": "./types/src/client/index.d.ts",
43
44
  "default": "./src/client/index.ts"
44
45
  },
45
46
  "./react": {
46
47
  "bun": "./src/react/index.ts",
47
- "types": "./src/react/index.ts",
48
+ "types": "./types/src/react/index.d.ts",
48
49
  "default": "./src/react/index.ts"
49
50
  },
50
51
  "./react/testing": {
51
52
  "bun": "./src/react/testing.ts",
52
- "types": "./src/react/testing.ts",
53
+ "types": "./types/src/react/testing.d.ts",
53
54
  "default": "./src/react/testing.ts"
54
55
  },
55
56
  "./implement": {
56
57
  "bun": "./src/implement/index.ts",
57
- "types": "./src/implement/index.ts",
58
+ "types": "./types/src/implement/index.d.ts",
58
59
  "default": "./src/implement/index.ts"
59
60
  },
60
61
  "./service": {
61
62
  "bun": "./src/service/index.ts",
62
- "types": "./src/service/index.ts",
63
+ "types": "./types/src/service/index.d.ts",
63
64
  "default": "./src/service/index.ts"
64
65
  },
65
66
  "./build": {
66
67
  "bun": "./src/build.ts",
67
- "types": "./src/build.ts",
68
+ "types": "./types/src/build.d.ts",
68
69
  "default": "./src/build.ts"
69
70
  },
70
71
  "./testing": {
71
72
  "bun": "./src/testing/index.ts",
72
- "types": "./src/testing/index.ts",
73
+ "types": "./types/src/testing/index.d.ts",
73
74
  "default": "./src/testing/index.ts"
74
75
  },
75
76
  "./testing/isolation": {
76
77
  "bun": "./src/testing/isolation.ts",
77
- "types": "./src/testing/isolation.ts",
78
+ "types": "./types/src/testing/isolation.d.ts",
78
79
  "default": "./src/testing/isolation.ts"
79
80
  },
80
81
  "./testing/preload": {
81
82
  "bun": "./src/testing/preload.ts",
82
- "types": "./src/testing/preload.ts",
83
+ "types": "./types/src/testing/preload.d.ts",
83
84
  "default": "./src/testing/preload.ts"
84
85
  },
86
+ "./dev": {
87
+ "bun": "./src/dev/index.ts",
88
+ "types": "./types/src/dev/index.d.ts",
89
+ "default": "./src/dev/index.ts"
90
+ },
91
+ "./dev/wire": {
92
+ "bun": "./src/dev/wire.ts",
93
+ "types": "./types/src/dev/wire.d.ts",
94
+ "default": "./src/dev/wire.ts"
95
+ },
96
+ "./dev/changes": {
97
+ "bun": "./src/dev/changes.ts",
98
+ "types": "./types/src/dev/changes.d.ts",
99
+ "default": "./src/dev/changes.ts"
100
+ },
85
101
  "./agent-skill": {
86
102
  "bun": "./src/agent-skill.ts",
87
- "types": "./src/agent-skill.ts",
103
+ "types": "./types/src/agent-skill.d.ts",
88
104
  "default": "./src/agent-skill.ts"
89
105
  }
90
106
  },
@@ -93,14 +109,14 @@
93
109
  "test": "bun test --path-ignore-patterns='src/react/**' && bun test --isolate src/react"
94
110
  },
95
111
  "dependencies": {
96
- "@fabrika/auth": "0.8.2",
97
- "@fabrika/auth-core": "0.8.2",
98
- "@fabrika/db": "0.8.2",
99
- "@fabrika/email": "0.8.2",
100
- "@fabrika/jwks": "0.8.2",
101
- "@fabrika/platform": "0.8.2",
102
- "@fabrika/platform-node": "0.8.2",
103
- "@fabrika/proxy-core": "0.8.2",
112
+ "@fabrika/auth": "0.9.0-cloudflare.2",
113
+ "@fabrika/auth-core": "0.9.0-cloudflare.2",
114
+ "@fabrika/db": "0.9.0-cloudflare.2",
115
+ "@fabrika/email": "0.9.0-cloudflare.2",
116
+ "@fabrika/jwks": "0.9.0-cloudflare.2",
117
+ "@fabrika/platform": "0.9.0-cloudflare.2",
118
+ "@fabrika/platform-node": "0.9.0-cloudflare.2",
119
+ "@fabrika/proxy-core": "0.9.0-cloudflare.2",
104
120
  "altcha-lib": "2.6.0",
105
121
  "jose": "^5.9.0"
106
122
  },
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fabrika-app
3
- version: 0.8.2
3
+ version: 0.9.0-cloudflare.2
4
4
  description: >
5
5
  How to write and change a Fabrika application: src/vocabulary.ts, db/migrations/, src/db/tables.ts, the
6
6
  slices' endpoints.ts with their handlers, fabrika.contract.ts, jobs, files, webhooks, share links, connectors
@@ -231,6 +231,7 @@ the access check. `references/spa.md` shows the stack.
231
231
  | e-mail (a cron job only) | `mailer: 'installation'` | `ctx.mail.send` |
232
232
  | a file from a public procedure | `file('<kind>')` input, `admission` with `bytesPerHour` | a reference the handler writes into a file column |
233
233
  | an outside HTTPS API | `egress: ['https://api.example.com']` | `fetch` to exactly those origins |
234
+ | any public website (a job) | `egress: 'public'` | `fetch` to public hosts, never the installation |
234
235
  | a signed link (unsubscribe) | `signs: ['<purpose>']` | `ctx.tokens.sign` |
235
236
  | a webhook binding | `binds: '<webhook>'` | `ctx.webhooks('<webhook>').bind` and `.unbind` |
236
237
  | a job run after the commit | `kicks: ['<job>']` | `ctx.kick('<job>')` |
@@ -258,7 +259,7 @@ Which promise the types, the checker, boot, the runtime or the approver keeps, a
258
259
  - No `as` (except `as const`), `any`, `@ts-ignore`, `@ts-expect-error`, `process.env` or `Bun.env`.
259
260
  - Never log an error object, a request, headers or the environment.
260
261
  - Never edit a generated file (`src/db/schema.generated.ts`, the generated tests, `fabrika/entry.generated.ts`,
261
- `fabrika/connect/`) or a committed migration. Regenerate the file; write a new migration.
262
+ `fabrika/migrations.generated.ts`, `fabrika/connect/`) or a committed migration. Regenerate the file; write a new migration.
262
263
 
263
264
  ## Ask the human first
264
265
 
@@ -295,6 +296,30 @@ bunx tsc --noEmit # once, at the end
295
296
  bunx fabrika-check . # once, after tsc; refusals fail; hints print below them
296
297
  ```
297
298
 
299
+ **On Cloudflare** (`fabrika.config.ts` imports `defineApp` from `@fabrika/provider-cloudflare`) the application runs in
300
+ one Durable Object over SQLite. Its `db/migrations/*.sql` are SQLite (`references/table-and-migration.md`), and the
301
+ object applies them when it boots, so there is no `migrate` and no command needs a database URL or `FABRIKA_APP_ID`.
302
+ The tests run on `@fabrika/db/testing-sqlite` (`references/tests.md`), in memory, and never skip. A handler holds the
303
+ object's transaction, so its `limits.timeoutMs` is at most 25 s. No `sql` (`ctx.sql`) and no `scan: true`: the
304
+ checker, the deploy and the boot refuse both. The deploy serves each SPA mount's built `dir` itself; declare no
305
+ `assets` on the Worker (`references/app-setup.md`).
306
+
307
+ The object runs one transaction at a time, so every `await` in a handler that may write holds the whole environment:
308
+ IAM, R2, a `fetch`, a `ctx.connect` call, a binding. One 2 s call caps the environment at half a request a second. A
309
+ query holds no transaction: each of its statements runs on its own, so two of its reads may see different committed
310
+ states. Inside a
311
+ mutation keep I/O to the framework's own: IAM, and the file it writes. A slow outside call goes into a row, the
312
+ mutation calls `ctx.kick('<job>')`, and the job makes the call outside `ctx.transaction` (`references/jobs.md`). The
313
+ checker hints on a mutation that declares `egress` or `connect` (`cloudflare-held-call`), and refuses
314
+ `DEFAULT CURRENT_TIMESTAMP` in a migration (`cloudflare-current-timestamp`).
315
+
316
+ ```bash
317
+ bunx fabrika db new <slug> # a new SQLite migration file
318
+ bunx fabrika db codegen # applies the migrations in memory; writes src/db/schema.generated.ts
319
+ bunx fabrika db test-scaffold # the generated tests, on @fabrika/db/testing-sqlite
320
+ bun test --isolate # no database URL; then tsc and fabrika-check as above
321
+ ```
322
+
298
323
  A generated file changes only through its command (`codegen`, `test-scaffold`); running it is the loop, not an edit.
299
324
  Format and lint only the files you wrote, with the repository's own tools: in the fabrika-platform repository
300
325
  `bunx dprint fmt <files>` and `bunx biome lint <files>`, never `biome check` (its formatter fights dprint), and never a
@@ -326,7 +351,8 @@ e-mails them, a link holder's files, HR's review with its history.
326
351
 
327
352
  ## Not built yet
328
353
 
329
- - The data layer runs on Bun and Postgres only (Zerops). On Cloudflare an application has no tables, jobs or files.
354
+ - On Cloudflare, where the application runs in one Durable Object over its SQLite and its `db/migrations/*.sql` are
355
+ SQLite, `sql` (`ctx.sql`) and a file kind with `scan: true` are refused. Everything else on the road runs there.
330
356
  - Jobs are cron only and app-wide: no `ctx.jobs.enqueue`, no job per scope. A `ctx.kick` starts a run of its job; it
331
357
  carries nothing, and the job finds its work in a queue table with `claim`. A list write is off the road (`sql`).
332
358
  - A read across tables (a join, a filter over a related table, an aggregate over several) is off the road (`sql`).
@@ -19,20 +19,20 @@ the SPA is added with it.
19
19
  "test": "bun test --isolate"
20
20
  },
21
21
  "dependencies": {
22
- "@fabrika/app": "0.8.2",
23
- "@fabrika/db": "0.8.2",
24
- "@fabrika/provider-zerops": "0.8.2",
25
- "@fabrika/proxy-core": "0.8.2",
26
- "@fabrika/ui": "0.8.2",
22
+ "@fabrika/app": "0.9.0-cloudflare.2",
23
+ "@fabrika/db": "0.9.0-cloudflare.2",
24
+ "@fabrika/provider-zerops": "0.9.0-cloudflare.2",
25
+ "@fabrika/proxy-core": "0.9.0-cloudflare.2",
26
+ "@fabrika/ui": "0.9.0-cloudflare.2",
27
27
  "kysely": "0.29.6",
28
28
  "react": "^19.0.0",
29
29
  "react-dom": "^19.0.0",
30
30
  "zod": "^3.25.76"
31
31
  },
32
32
  "devDependencies": {
33
- "@fabrika/check": "0.8.2",
34
- "@fabrika/cli": "0.8.2",
35
- "@fabrika/platform": "0.8.2",
33
+ "@fabrika/check": "0.9.0-cloudflare.2",
34
+ "@fabrika/cli": "0.9.0-cloudflare.2",
35
+ "@fabrika/platform": "0.9.0-cloudflare.2",
36
36
  "@happy-dom/global-registrator": "^20.11.2",
37
37
  "@types/bun": "^1.4.3",
38
38
  "@types/react": "^19.0.0",
@@ -249,6 +249,22 @@ The platform's build runs the tests, writes the entrypoint, runs `fabrika-check`
249
249
  run `fabrika db migrate`, `fabrika db codegen --verify` and `fabrika db test-isolation` before the server starts: a
250
250
  version does not go live until every generated test passes. It writes every `FABRIKA_*` variable; never set one.
251
251
 
252
+ ## The deploy surface (Cloudflare)
253
+
254
+ On Cloudflare `fabrika.config.ts` imports `defineApp`, `Worker` and `iamServiceReference` from
255
+ `@fabrika/provider-cloudflare`, and `resources` returns the application Worker: `main: './fabrika/entry.generated.ts'`,
256
+ `workers_dev: false`, `preview_urls: false`, `routes: []` and `bindings: { IAM: iamServiceReference() }`, with
257
+ `pipeline: { workerDir: '.', build: 'bun run build:web' }` when the application has an SPA. The deploy adds the
258
+ Durable Object, the files bucket, the minute tick, every `FABRIKA_*` variable and the proxy Worker.
259
+
260
+ It also serves the SPA: when the Worker declares no `assets`, the deploy copies the build directory of each SPA and
261
+ link mount (`dir`, a link's `spa`) into `fabrika/assets/` at the mount's prefix (`/admin` into `fabrika/assets/admin/`)
262
+ after `pipeline.build`, and binds it as `ASSETS`. A deploy whose mount directory the build did not write fails and
263
+ names the mount. Do not declare `assets` yourself. A Worker that declares them keeps them, and the object reads each
264
+ request path from that directory as it is (`/admin/app.js` is `admin/app.js` there), so a hand-written `assets` works
265
+ only for a root mount, or for mounts whose directories nest like their prefixes; it needs `binding: 'ASSETS'` and
266
+ `run_worker_first: true`.
267
+
252
268
  ## The SPA (added when the application has one)
253
269
 
254
270
  The SPA is React, built from the screens and hooks of the framework: `@fabrika/app/react` runs the typed client's
@@ -379,8 +395,9 @@ await Bun.write(
379
395
  `src/schemas.ts`, `src/shared/` and its own modules; never a slice, `src/db/` or a Bun or Node module. The plugin loads the
380
396
  contract without its handlers. A row type the screen declares (`Task` above) is checked against the answer where
381
397
  the rows are passed in.
382
- - The last step points the page's assets at the SPA's prefix (`/`): the mount answers `index.html` for every path
383
- under it, so a deep link such as `/tasks/<id>` renders. `define` gives React's production build.
398
+ - The last step points the page's assets at the SPA's prefix (`/`): the mount answers `index.html` for every page
399
+ route under it (a path without an extension, or a navigation), so a deep link such as `/tasks/<id>` renders; a
400
+ missing asset such as an old `chunk-….js` is 404. `define` gives React's production build.
384
401
  - `sourcemap: 'external'` writes each script's map beside it, and the page names none. The platform build uploads the
385
402
  maps to Operations for the release the page reports, so an error's frames show the original source; the mount
386
403
  never serves a `.map` file (404). Keep the maps external: `inline` would ship the source to every visitor.
@@ -397,4 +414,5 @@ await Bun.write(
397
414
 
398
415
  ## Ignored files
399
416
 
400
- `.gitignore`: `node_modules/`, `fabrika.manifest.json`, `/fabrika/entry.generated.ts`, `/dist/`.
417
+ `.gitignore`: `node_modules/`, `fabrika.manifest.json`, `/fabrika/entry.generated.ts`, `/dist/`, and on Cloudflare
418
+ `/fabrika/assets/`, where a deploy stages the SPA's files.
@@ -1,7 +1,8 @@
1
1
  # references/dev.md — previewing the application with `fabrika app dev`
2
2
 
3
3
  `bunx fabrika app dev` serves the application on `http://127.0.0.1:3000/` through its test kit: the proxy decision,
4
- the stand-in IAM, a throwaway schema of `FABRIKA_TEST_POSTGRES_URL`, and each SPA from its build (run
4
+ the stand-in IAM, a throwaway schema of `FABRIKA_TEST_POSTGRES_URL` (on Cloudflare an in-memory SQLite database,
5
+ without a URL), and each SPA from its build (run
5
6
  `bun run build:web` first). Use it to look at the screens, never in place of a test.
6
7
 
7
8
  ## The seed
@@ -421,7 +421,7 @@ export const workloadRows = (
421
421
  ## Raw SQL: `sql: 'read'`
422
422
 
423
423
  Raw SQL is legitimate for a join, a report, an `OR` or `IS NULL` filter, or a bulk write. Declare it on the endpoint;
424
- `ctx.sql` exists only there.
424
+ `ctx.sql` exists only there. It runs on Postgres only: on Cloudflare a site that declares `sql` is refused.
425
425
 
426
426
  ```ts src/reports/endpoints.ts
427
427
  import { attachment } from '@fabrika/app/contract'
@@ -564,7 +564,7 @@ Every procedure takes `admission` (who may send it, how often) and `limits` (how
564
564
  | `admission.origins` | `'same-origin'` (default) or `'listed'` | `'same-origin'`, `'listed'` or `'any'` |
565
565
  | `admission.challenge` | none | `{ provider: 'altcha', secret, action?, cost?, maxAgeMs? }`, or `{ provider: 'turnstile', secret, siteKey, action? }` |
566
566
  | `limits.maxBytes` | 64 KiB, at most 1 MiB; with file fields 64 KiB plus each file at its kind's `maxBytes`, at most 48 MiB | the same |
567
- | `limits.timeoutMs` | 10 s, at most 60 s | the same |
567
+ | `limits.timeoutMs` | 10 s, at most 60 s; at most 25 s on Cloudflare | the same |
568
568
 
569
569
  - A declared `rate` merges into the default; `null` turns one bucket off. Past a bucket the answer is 429.
570
570
  - Every bucket is charged before the body is read and stays charged whatever the handler does. A bucket is a database
@@ -642,7 +642,7 @@ export const app = defineApp(vocabulary, tables, {
642
642
  - A public procedure updates or removes a row only by an id a verified token of its request carries, with
643
643
  `expectVersion`.
644
644
  - Single use and revocation are your state (a row the token names). The keys are the platform's: Control generates
645
- and rotates them, and an old key verifies until its last token expires. Cloudflare has no signed tokens.
645
+ and rotates them, and an old key verifies until its last token expires.
646
646
 
647
647
  ## Outside calls: `egress`
648
648
 
@@ -652,6 +652,15 @@ refused while the site runs (`capability_refused: undeclared_egress`). A public,
652
652
  none. The checker's `cross-app-fetch` accepts an absolute URL only under an origin some site declares. The approver
653
653
  sees every origin. The guard stops mistakes; it is no sandbox.
654
654
 
655
+ On Cloudflare a procedure waits for its outside call inside the object's transaction, so the whole environment waits
656
+ with it (`SKILL.md`). A mutation records a row and kicks a job, which declares the `egress` and calls outside
657
+ `ctx.transaction` (`references/jobs.md`).
658
+
659
+ A cron job that calls hosts nobody knows when the contract is written (the websites its operators enter: a crawler, an
660
+ auditor, a link checker) declares `egress: 'public'` instead of a list (`references/jobs.md`). Only a cron job may: a
661
+ procedure, a mutation and a webhook name their origins. Never keep `fetch` from before boot or open sockets to get past
662
+ the list; that is hidden reach.
663
+
655
664
  ## Links to your own procedures: `ctx.url`
656
665
 
657
666
  `ctx.url('<endpoint id>', { input?, query? })` builds an absolute URL of one of the application's own procedures, on
@@ -8,39 +8,43 @@ architecture, and a refusal's allowed form is the one fix that passes. There is
8
8
 
9
9
  The refusals agents meet most, and their fix:
10
10
 
11
- | Rule | Means | Fix |
12
- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
13
- | `literal` | the shell (contract, vocabulary, tables, groups, schemas) holds a computed value or call | literal objects, `z.*` calls and imported literal consts; move logic into `.handle(…)` |
14
- | `contract` | the contract does not compile (a reserved role, an unknown action, a wrong field) | the message names the field |
15
- | `package-replacement` | `package.json` names the app under `@fabrika/*`, or a dependency is `workspace:*` | another name; a registry range such as `^0.0.4` |
16
- | `generated-test-hash` | a generated test is missing, edited or stale | `bunx fabrika db test-scaffold`, and commit what it writes |
17
- | `endpoint-fixture` | an endpoint's input cannot be sampled, a fixture or seed names no endpoint, or a member, invitation or link field no endpoint that reads, cancels or revokes one | a literal in `src/__tests__/endpoint-access.fixtures.ts` (`references/tests.md`) |
18
- | `raw-sql` | a driver's `sql` (imported from `bun`, Kysely, `postgres`) | the handles, or `sql: 'read'` and `ctx.sql` |
19
- | `forbidden-import` | a driver, an S3 client, `jose`, `node:http`, `child_process` | the handles, a file column with `ctx.files`, a connector |
20
- | `forbidden-global` | `Bun.sql`, `Bun.serve`, `Bun.spawn`; `globalThis`, `window`, `self`, `global` or `Bun` in a server module | the handles; state lives in a table, not in a global; `crypto.randomUUID()` for an id |
21
- | `env-read` | `process.env`, `Bun.env` | `defineDeps(build)`, which receives the application's variables |
22
- | `client-identity` | an input field `userId`, `ownerId`, `createdBy`, `tenantId`, `workspaceId` | the access form's `from`, `ctx.principal.id`, or `createdBy` in the policy |
23
- | `type-assertion` | `as T` (other than `as const`), `<T>x` | fix the type; `fromRow` parses a row into a schema's type |
24
- | `sensitive-log` | a log of an error object, a request, headers or the environment | log a short fixed message |
25
- | `migration-name` | a migration not named `YYYYMMDDHHMMSS_slug.sql` (UTC) | `bunx fabrika db new <slug>` |
26
- | `test-only-import` | `@fabrika/app/testing`, `@fabrika/app/react/testing` or `@fabrika/db/testing` outside a test file | import them only from `*.test.ts` or `__tests__/` |
27
- | `spa-server-import` | the SPA loads a server module | the SPA imports `@fabrika/app/client`, the contract, `src/schemas.ts` and `src/shared/` |
28
- | `shared-import` | a module under `src/shared/` loads a slice, `src/db/`, `@fabrika/*` or a builtin | a shared module imports `zod/v4` and other shared modules only |
29
- | `skipped-test` | `.skip`, `.only`, `.todo` | run the test |
30
- | `file-column-migration` | a file column without `REFERENCES fabrika_files (id)` or without an index it leads | both, in a new migration (`references/table-and-migration.md`) |
31
- | `on-conflict-index` | `insert(…, { onConflict: { columns } })` whose columns are no unique index the migrations create | a `UNIQUE` constraint or a `CREATE UNIQUE INDEX` without `WHERE` on exactly those columns |
32
- | `cross-app-fetch` | a `fetch` of an absolute URL under no origin a site declares in `egress` | `egress: ['https://…']` on the site that calls it (`references/endpoint.md`) |
33
- | `url-endpoint` | `ctx.url('<id>')` names no procedure of the contract | the endpoint id `<mount>.<group>.<procedure>` |
11
+ | Rule | Means | Fix |
12
+ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
13
+ | `literal` | the shell (contract, vocabulary, tables, groups, schemas) holds a computed value or call | literal objects, `z.*` calls and imported literal consts; move logic into `.handle(…)` |
14
+ | `contract` | the contract does not compile (a reserved role, an unknown action, a wrong field) | the message names the field |
15
+ | `package-replacement` | `package.json` names the app under `@fabrika/*`, or a dependency is `workspace:*` | another name; a registry range such as `^0.0.4` |
16
+ | `generated-test-hash` | a generated test is missing, edited or stale | `bunx fabrika db test-scaffold`, and commit what it writes |
17
+ | `endpoint-fixture` | an endpoint's input cannot be sampled, a fixture or seed names no endpoint, a member, invitation or link field no endpoint that reads, cancels or revokes one, a file names no file field or a path outside the application, or a refusal is `validation` | a literal in `src/__tests__/endpoint-access.fixtures.ts` (`references/tests.md`) |
18
+ | `raw-sql` | a driver's `sql` (imported from `bun`, Kysely, `postgres`) | the handles, or `sql: 'read'` and `ctx.sql` |
19
+ | `forbidden-import` | a driver, an S3 client, `jose`, `node:http`, `child_process` | the handles, a file column with `ctx.files`, a connector |
20
+ | `forbidden-global` | `Bun.sql`, `Bun.serve`, `Bun.spawn`; `globalThis`, `window`, `self`, `global` or `Bun` in a server module | the handles; state lives in a table, not in a global; `crypto.randomUUID()` for an id |
21
+ | `env-read` | `process.env`, `Bun.env` | `defineDeps(build)`, which receives the application's variables |
22
+ | `client-identity` | an input field `userId`, `ownerId`, `createdBy`, `tenantId`, `workspaceId` | the access form's `from`, `ctx.principal.id`, or `createdBy` in the policy |
23
+ | `type-assertion` | `as T` (other than `as const`), `<T>x` | fix the type; `fromRow` parses a row into a schema's type |
24
+ | `sensitive-log` | a log of an error object, a request, headers or the environment | log a short fixed message |
25
+ | `migration-name` | a migration not named `YYYYMMDDHHMMSS_slug.sql` (UTC) | `bunx fabrika db new <slug>` |
26
+ | `test-only-import` | `@fabrika/app/testing`, `@fabrika/app/react/testing`, `@fabrika/db/testing` or `@fabrika/db/testing-sqlite` outside a test file | import them only from `*.test.ts` or `__tests__/` |
27
+ | `spa-server-import` | the SPA loads a server module | the SPA imports `@fabrika/app/client`, the contract, `src/schemas.ts` and `src/shared/` |
28
+ | `shared-import` | a module under `src/shared/` loads a slice, `src/db/`, `@fabrika/*` or a builtin | a shared module imports `zod/v4` and other shared modules only |
29
+ | `skipped-test` | `.skip`, `.only`, `.todo` | run the test |
30
+ | `file-column-migration` | a file column without `REFERENCES fabrika_files (id)` or without an index it leads | both, in a new migration (`references/table-and-migration.md`) |
31
+ | `on-conflict-index` | `insert(…, { onConflict: { columns } })` whose columns are no unique index the migrations create | a `UNIQUE` constraint or a `CREATE UNIQUE INDEX` without `WHERE` on exactly those columns |
32
+ | `cross-app-fetch` | a `fetch` of an absolute URL under no origin a site declares in `egress`, or one a job's `egress: 'public'` refuses (an IP address, `localhost`) | `egress: ['https://…']` on the site that calls it (`references/endpoint.md`) |
33
+ | `url-endpoint` | `ctx.url('<id>')` names no procedure of the contract | the endpoint id `<mount>.<group>.<procedure>` |
34
+ | `cloudflare-sql` | on Cloudflare, a site that declares `sql` | the handles (`ctx.db`), without `sql` |
35
+ | `cloudflare-scan` | on Cloudflare, a file kind with `scan: true` | the kind without `scan`, and no public procedure that takes its files |
36
+ | `cloudflare-current-timestamp` | on Cloudflare, `DEFAULT CURRENT_TIMESTAMP` in a migration: SQLite stores `YYYY-MM-DD HH:MM:SS`, which every read of the table refuses | `DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`, in a new migration |
34
37
 
35
38
  The hints:
36
39
 
37
- | Hint | Means | Fix |
38
- | ---------------------- | ------------------------------------------------------------- | ----------------------------------------------------------- |
39
- | `handler-location` | a `.handle(…)` outside a slice's `endpoints.ts` | move the endpoint into `src/<slice>/endpoints.ts` |
40
- | `data-access-location` | `ctx.db` or `ctx.sql` outside `endpoints.ts` and `queries.ts` | pass the handle into a function of the slice's `queries.ts` |
41
- | `logic-data-access` | `logic.ts` reaches data | keep `logic.ts` pure; pass it plain values |
42
- | `cross-slice-queries` | a slice loads another slice's `queries.ts` | move the function, or call that slice's endpoint |
43
- | `sql-undeclared` | `ctx.sql` in a handler whose endpoint declares no `sql` | add `sql: 'read'` (or `'write'`) to that endpoint |
40
+ | Hint | Means | Fix |
41
+ | ---------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
42
+ | `handler-location` | a `.handle(…)` outside a slice's `endpoints.ts` | move the endpoint into `src/<slice>/endpoints.ts` |
43
+ | `data-access-location` | `ctx.db` or `ctx.sql` outside `endpoints.ts` and `queries.ts` | pass the handle into a function of the slice's `queries.ts` |
44
+ | `logic-data-access` | `logic.ts` reaches data | keep `logic.ts` pure; pass it plain values |
45
+ | `cross-slice-queries` | a slice loads another slice's `queries.ts` | move the function, or call that slice's endpoint |
46
+ | `sql-undeclared` | `ctx.sql` in a handler whose endpoint declares no `sql` | add `sql: 'read'` (or `'write'`) to that endpoint |
47
+ | `cloudflare-held-call` | on Cloudflare, a mutation that declares `egress` or `connect` holds the whole environment while it waits | record a row and `ctx.kick('<job>')`; the job makes the call outside `ctx.transaction` |
44
48
 
45
49
  The first run may report only the contract's refusals: the generated-test findings appear once the contract compiles.
46
50
 
@@ -99,11 +103,11 @@ A 500 answers a fixed message; the server log has one line, `fabrika: <endpoint>
99
103
  fixed failure code and, for a framework refusal, its reason and the reason's fixed text, never a value or your error's
100
104
  message. The codes you meet most:
101
105
 
102
- | Code | Means |
103
- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
- | `query_refused` | the data layer refused a statement or an operation: `undeclared_reach`, `sql_read_only`, `unknown_column`, `read_only`, `sql_outside_transaction` …; the reason's text names the rule |
105
- | `capability_refused` | a capability of another kind, dimension or action than the call takes (`wrong_capability`, `wrong_action`, `out_of_reach`), or one kept past its request (`ended`, `foreign_work`) |
106
- | `handler_failed` | your handler threw something else; the log has no more: the Operations issue has the stack, or test the handler |
106
+ | Code | Means |
107
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
108
+ | `query_refused` | the data layer refused a statement or an operation: `undeclared_reach`, `sql_read_only`, `unknown_column`, `read_only`, `sql_outside_transaction`, `sql_dialect` (`ctx.sql` on SQLite, Cloudflare) …; the reason's text names the rule |
109
+ | `capability_refused` | a capability of another kind, dimension or action than the call takes (`wrong_capability`, `wrong_action`, `out_of_reach`), one kept past its request (`ended`, `foreign_work`), or a `fetch` its `egress` refuses (`undeclared_egress`, `refused_egress`, `oversized_egress`) |
110
+ | `handler_failed` | your handler threw something else; the log has no more: the Operations issue has the stack, or test the handler |
107
111
 
108
112
  With an Operations DSN the same 500 is a `RequestFailed` issue in Operations. Its event also carries what your handler
109
113
  threw: the error's type, its message, its stack and its `cause` chain. Read the stack there, not in the log. A crash
@@ -363,7 +363,8 @@ export const erase = app.cron('30 2 * * *', {
363
363
  ## Malware scan
364
364
 
365
365
  `scan: true` on a kind holds every file of it `quarantined` until ClamAV answers clean; on Zerops the platform
366
- installs ClamAV and runs the scan job. A file answer is 409 `file_not_ready` until then and 409 `file_rejected` once
366
+ installs ClamAV and runs the scan job. No scanner runs on Cloudflare: the deploy and the boot refuse a kind with
367
+ `scan: true` there, and so a public procedure with a file field. A file answer is 409 `file_not_ready` until then and 409 `file_rejected` once
367
368
  the file is `rejected`; a list shows each file's `status`, so show it rather than a broken link. In a test, the scan runs with
368
369
  `kit.runJob(FILE_SCAN_JOB)` and a `scanner` passed to `testKit` (`references/tests.md`); the generated tests pass their
369
370
  own seeded files.
@@ -401,7 +402,8 @@ the transaction that deletes the row. The file is never copied; the sweep delete
401
402
  ## Storage
402
403
 
403
404
  On Zerops, declare one `object-storage` service in `fabrika.config.ts`; the platform writes `FABRIKA_APP_STORAGE_*`
404
- from it. Never read those variables, build a storage URL or import an S3 client.
405
+ from it. Never read those variables, build a storage URL or import an S3 client. On Cloudflare the platform binds the
406
+ environment's R2 bucket itself; declare nothing.
405
407
 
406
408
  ```text
407
409
  // in the services of fabrika.config.ts
@@ -144,6 +144,15 @@ export const contract = app.contract((c) => ({
144
144
  - A job may also declare `kicks` (another job; the edges never form a cycle), `signs` (`ctx.tokens.sign`),
145
145
  `egress` (the HTTPS origins its `fetch` calls) and `issues` (`references/endpoint.md`). `ctx.url('<endpoint id>', …)`
146
146
  builds an absolute link to one of the application's procedures, for an e-mail.
147
+ - A job that calls the websites its operators enter (a crawler, an auditor) declares `egress: 'public'`:
148
+ `app.cron('*/5 * * * *', { actions: ['sites.read'], egress: 'public' })`. Its `fetch` reaches any public host over
149
+ HTTPS or HTTP on the default port. It is refused (`capability_refused: refused_egress`) for an IP address, a name of
150
+ one label (`localhost`, an internal service), a name under `.local`, `.internal`, `.workers.dev`, `.zerops.app` and similar, a port,
151
+ the installation's own hosts (IAM, Control, Operations, every application's domain) and, on Bun (Zerops), a name
152
+ that resolves to a private address. Every redirect is checked again: `redirect: 'manual'` gets the redirect, the default
153
+ follows at most 10. A request has 60 s, a response body at most 16 MiB (`oversized_egress`); pass `ctx.signal`, or
154
+ `AbortSignal.any([ctx.signal, AbortSignal.timeout(8_000)])` for a shorter bound per page. The approver reviews it
155
+ closely, as reach to anywhere public. Keep the sites operators enter in a table the job reads.
147
156
  - `ctx.signal` aborts when the attempt ends (its timeout, a lost lease). Call `ctx.signal.throwIfAborted()` before
148
157
  each external effect and pass the signal to `fetch`, so a stale attempt starts nothing after its replacement.
149
158
  - A failed attempt names its failure in `kit.runJob`'s answer and the log: `handler_failed`, a framework refusal
@@ -1,7 +1,7 @@
1
1
  # references/table-and-migration.md — the vocabulary, migrations and table policies
2
2
 
3
- The order is fixed: the vocabulary, then the migration, then `fabrika db migrate` and `fabrika db codegen`, then the
4
- table policies, which take the generated `columns`.
3
+ The order is fixed: the vocabulary, then the migration, then `fabrika db migrate` and `fabrika db codegen` (on
4
+ Cloudflare `codegen` only), then the table policies, which take the generated `columns`.
5
5
 
6
6
  ## The vocabulary
7
7
 
@@ -155,6 +155,23 @@ INSERT INTO priorities (code, label, rank) VALUES ('low', 'Low', 1), ('normal',
155
155
  - Never edit, rename or delete a committed migration; add a new one. Change a schema by expand, switch, contract, and
156
156
  ask before you drop or rename anything.
157
157
 
158
+ ## On Cloudflare: SQLite migrations
159
+
160
+ An application on Cloudflare (its `fabrika.config.ts` imports `defineApp` from `@fabrika/provider-cloudflare`) writes
161
+ its migrations in SQLite. Its Durable Object applies them when it boots, each file once in name order, in a transaction
162
+ of its own, so there is no `fabrika db migrate`; `fabrika db codegen` applies them to an in-memory database and needs
163
+ no URL. The rules above for keys, chains, composite foreign keys, `version` and `created_by` hold unchanged, and the
164
+ database enforces foreign keys.
165
+
166
+ - The declared types codegen reads: `TEXT` (a UUID too), `INTEGER`, `REAL`, `BLOB`; `BOOLEAN`, stored as 0 or 1 and
167
+ read as a boolean; `TIMESTAMP`, stored as UTC ISO text and read as a `Date`; `DATE`, `YYYY-MM-DD` text. Any other
168
+ declared type is refused. A `STRICT` table admits only `INTEGER`, `REAL`, `TEXT` and `BLOB`, so a table with a
169
+ `BOOLEAN`, `TIMESTAMP` or `DATE` column is not `STRICT`.
170
+ - SQLite has no `ENUM`: a column with a fixed set of values is `TEXT` with `CHECK (<column> IN ('a', 'b'))`. The
171
+ generated tests write its first value.
172
+ - No transaction control (`BEGIN`, `COMMIT`, `SAVEPOINT`) and no Postgres syntax (`timestamptz`, `::` casts,
173
+ `GENERATED ALWAYS AS IDENTITY`, `CREATE TYPE`).
174
+
158
175
  ## Database code is a declared deviation
159
176
 
160
177
  A function, trigger, view, rule, policy or `DO` block in a migration is allowed. The migration file declares it, and
@@ -12,7 +12,7 @@ longer writes. Commit them; never edit, format or import them (`generated-test-h
12
12
  `app` or `creates` endpoint refuses a holder of its action in one scope only (403) and answers a global holder. An
13
13
  endpoint of several `capabilities` adds a denied case per capability. A public procedure answers anyone without a
14
14
  credential, or, behind a challenge, refuses a caller without a solved one. A file input is sent as a generated file
15
- of its kind; a `GET` procedure's input goes in its URL.
15
+ of its kind, or as the file the fixtures name (`files`, below); a `GET` procedure's input goes in its URL.
16
16
  - Their kit stands in for IAM, as `testKit` does (below): it creates a scope, assigns or invites a member and issues a
17
17
  link under IAM's rules for the caller's grants. A repeated invitation answers the open one, `resend` restarts its
18
18
  7 days, and an organization's address is refused for an external role (`internal_mailbox`). So the caller of an endpoint with the dimension's `members.manage`
@@ -59,6 +59,35 @@ export const members = { 'api.tasks.assign': ['assignee'] }
59
59
  table of the table that owns it, or a table it contains; the checker refuses a row further down
60
60
  (`endpoint-fixture`). Without it they send a sample, the handler
61
61
  finds no row (404), and the case fails.
62
+ - `appRows['<endpoint id>']` gives the rows of application tables (`scope: { app }`) that the endpoint's handler reads
63
+ for the caller's own scope, by table, with the columns they need: `export const appRows = { 'portal.guide': { guides:
64
+ { active: true } } }`. Each of its leak and own-object cases seeds one row of each before it asks and removes them
65
+ once it is over, with every row that refers to them (a download the handler logged), so a handler that reads the
66
+ newest guide answers 200 instead of 404, and no other endpoint's case finds the row. A foreign key from one given row
67
+ to another holds that row's key. A text column may be given; the key, the version, the creator and a file column may
68
+ not (a file column holds a live, scanned file). Only application tables, and only for an endpoint with a leak or an
69
+ own-object case: the checker refuses a table in a scope, which `rows` and `seeds` address (`endpoint-fixture`).
70
+ - `files['<endpoint id>']` names, by file field, a file of the repository that every case of the endpoint sends there
71
+ instead of the generated one, a path from the application's root:
72
+ `export const files = { 'api.skills.publish': { package: 'src/__tests__/fixtures/skill.zip' } }`. Give one when the
73
+ handler inspects the content (a package it unpacks, a document it parses) and answers 400 to the generated file, which
74
+ has only the leading bytes of its type. Commit the file; keep it small and free of real data. The kit sends it with the
75
+ first type of the field's kind its bytes are, and a file it cannot read, or of none of those types, fails the case. The
76
+ checker refuses a field that holds no file and a path that leaves the application, starts with `.` or has characters
77
+ other than letters, digits, `_`, `-`, `.` and `/` (`endpoint-fixture`).
78
+ - `pins['<endpoint id>']` gives text columns of the row the endpoint addresses (the row of the table that owns its
79
+ dimension) a known value instead of the seed's marker: `export const pins = { 'api.skills.publish': { slug:
80
+ 'published-skill' } }`, when the handler ties the column to the input or to the file in `files` (a package whose
81
+ `SKILL.md` names the skill's slug). The own scope's row holds the value, the other scope's `<value>-other`, which a
82
+ leak case still looks for in the own scope's answer. A pin of a column that places the row, or of no text column,
83
+ fails the case; the checker refuses one of an endpoint without an addressed row (`endpoint-fixture`).
84
+ - `refusals['<endpoint id>']` lists the error types of the 400s the endpoint's handler answers with when its rule ties
85
+ the row to the input and the scope is the other one: `export const refusals = { 'api.skills.publish':
86
+ ['skill_frontmatter'] }`, beside the pin above. A leak case asks from the other scope first, whose pinned value the
87
+ package does not name; that request may answer one of them. The own case passes on 200 or one of them, and the
88
+ denial cases still expect 404 or 403. The answer a leak case checks, the own scope's, still fails on it, because an
89
+ answer that holds nothing proves no isolation: pin the row and give the file so that the own scope succeeds. Any
90
+ other 400 fails the case, and the checker refuses `validation`, an input the schema refuses (`endpoint-fixture`).
62
91
  - The endpoint id is `<mount>.<group>.<endpoint>` (`api.tasks.close`).
63
92
  - The seed of the tests fills every text column with a marker and every other required column with a sample of its
64
93
  type: an enum's first label, `1`, `false`, today. A column with a `DEFAULT` keeps it (`status` is `open`). A column
@@ -196,6 +225,9 @@ if (postgres !== null) {
196
225
  `<label> skipped: set FABRIKA_TEST_POSTGRES_URL to run the Postgres-backed tests`, and the file registers no test.
197
226
  A skip proves nothing. To confirm a file ran, run it alone (`bun test --isolate src/tasks`) and check that the pass
198
227
  count is its number of tests, or set `FABRIKA_TEST_POSTGRES_REQUIRED=1`: a missing URL then fails the file.
228
+ - On Cloudflare import `testDatabase` from `@fabrika/db/testing-sqlite` instead, with the same options: it migrates a
229
+ fresh in-memory SQLite database as the Durable Object does, needs no URL and never answers `null`. The generated tests
230
+ already use it.
199
231
  - `statusOf(call)` answers 200 when the call resolves and the status of the application's refusal otherwise; any other
200
232
  failure (no answer, a bug in the test) is rethrown. Import it beside `testKit`; do not write your own.
201
233
  - `kit.testCaller(grants, { kind })` makes a caller: a grant is a role or an action, in one scope or global. A
@@ -89,7 +89,7 @@ export const integrations = app.group({ callers: ['human'] }, (r) => ({
89
89
  that binds it takes an `app` capability: `access: { action, app: true }`, `binds`, and
90
90
  `ctx.webhooks('<webhook>').bind(ctx.app, account)`.
91
91
  - `limits: { maxBytes?, timeoutMs? }`: the body is 1 MiB by default and at most 48 MiB; the handler runs 10 s by
92
- default, at most 60 s. A webhook may declare `kicks`, `signs`, `egress`, `sql` and `unscoped`, and `ctx.files.put`
92
+ default, at most 60 s (25 s on Cloudflare). A webhook may declare `kicks`, `signs`, `egress`, `sql` and `unscoped`, and `ctx.files.put`
93
93
  stores a file a provider sends (an inbound e-mail's attachment) for a file column: raise `limits.maxBytes` to the
94
94
  mail you accept.
95
95
  - Not covered: a provider that validates its URL with a `GET`, or that needs a custom answer body.