@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
@@ -0,0 +1,198 @@
1
+ // The build, init and start of a contract application in a Bun container, as data (ADR-0043): the steps and their
2
+ // order are the platform's, never the repository's. A provider renders the recipe into its own build form, with its own
3
+ // paths, file writes and scheduler. PURE — no network, no filesystem.
4
+ //
5
+ // PLATFORM PROTOCOL, not framework API: the platform renders this with ITS copy for applications of older releases
6
+ // (ADR-0062), so the commands, script names and job names an application must accept change only additively. Never
7
+ // render the application's own copy.
8
+
9
+ import { APPLICATION_SERVER_MAX_HEADER_BYTES, FRAMEWORK_RELEASE_PATTERN } from '@fabrika/auth-core'
10
+ import { CONTRACT_APP_ID_PATTERN } from './contract/app-id.js'
11
+ import { FILE_SCAN_JOB, FILE_SCAN_SCHEDULE, FILE_SWEEP_JOB, FILE_SWEEP_SCHEDULE } from './files/job-names.js'
12
+
13
+ export { CONTRACT_APP_ID_PATTERN } from './contract/app-id.js'
14
+
15
+ export { FILE_SCAN_JOB, FILE_SWEEP_JOB } from './files/job-names.js'
16
+
17
+ /** The TypeScript release of the platform's type check; the application's own `typescript` is not run. */
18
+ export const PLATFORM_TYPESCRIPT_VERSION = '5.9.3'
19
+
20
+ /** The CLI a generated script runs, relative to the application root. */
21
+ const FABRIKA_BIN = './node_modules/.bin/fabrika'
22
+ const MIGRATE_SCRIPT = 'fabrika/migrate.generated.sh'
23
+ const VERIFY_SCRIPT = 'fabrika/verify.generated.sh'
24
+ const START_SCRIPT = 'fabrika/start.generated.sh'
25
+ const SERVE: readonly string[] = ['bun', `--max-http-header-size=${APPLICATION_SERVER_MAX_HEADER_BYTES}`, 'fabrika/entry.generated.ts']
26
+ const MIGRATE: readonly string[] = [FABRIKA_BIN, 'db', 'migrate']
27
+ const PLATFORM_TSCONFIG = 'fabrika/tsconfig.platform.json'
28
+
29
+ /**
30
+ * The compiler options of the platform's type check; the application's own `tsconfig.json` and its `paths` are not read.
31
+ * It covers the SPA under `web/` too: Bun bundles the SPA without checking its types.
32
+ */
33
+ const PLATFORM_TSCONFIG_CONTENTS = JSON.stringify({
34
+ compilerOptions: {
35
+ target: 'ES2022',
36
+ module: 'ESNext',
37
+ moduleResolution: 'bundler',
38
+ lib: ['ES2022', 'DOM', 'DOM.Iterable'],
39
+ jsx: 'react-jsx',
40
+ types: ['bun'],
41
+ strict: true,
42
+ noUncheckedIndexedAccess: true,
43
+ noImplicitOverride: true,
44
+ forceConsistentCasingInFileNames: true,
45
+ skipLibCheck: true,
46
+ resolveJsonModule: true,
47
+ isolatedModules: true,
48
+ noEmit: true,
49
+ },
50
+ include: ['../fabrika.contract.ts', '../src', '../web', 'entry.generated.ts'],
51
+ })
52
+
53
+ const INSTALL = 'bun install --frozen-lockfile --ignore-scripts'
54
+
55
+ /**
56
+ * What `@fabrika/db/testing` prints after the label of a test file it skips without `FABRIKA_TEST_POSTGRES_URL`. Every
57
+ * framework release prints it, so the platform counts it for any application's release.
58
+ */
59
+ const DATABASE_TEST_SKIP_MARKER = ' skipped: set FABRIKA_TEST_POSTGRES_URL to run the Postgres-backed tests'
60
+
61
+ /** The prefix of the line the tests command prints after the test run. */
62
+ const TESTS_REPORT_PREFIX = 'fabrika-tests:'
63
+
64
+ /** One `bun -e "…"` running `script`, which may hold none of the characters a double-quoted shell word expands. */
65
+ const bunEvalCommand = (script: readonly string[]): string => {
66
+ const source = script.join(' ')
67
+ if (/["$`\\!]/.test(source)) throw new Error('a generated bun -e script must not contain a quote, an expansion or a backslash')
68
+ return `bun -e "${source}"`
69
+ }
70
+
71
+ // The build has no database, so every Postgres-backed test file skips and the run still passes. This command runs the
72
+ // tests as before and then says how many files did not run, so a green test step does not read as a tested database.
73
+ const TESTS_COMMAND = bunEvalCommand([
74
+ "const child = Bun.spawn([process.execPath, 'test', '--isolate', '--pass-with-no-tests'], { stdout: 'inherit', stderr: 'pipe' });",
75
+ 'const decoder = new TextDecoder();',
76
+ "let output = '';",
77
+ 'for await (const chunk of child.stderr) { process.stderr.write(chunk); output += decoder.decode(chunk, { stream: true }) }',
78
+ 'const code = await child.exited;',
79
+ `const marker = '${DATABASE_TEST_SKIP_MARKER}';`,
80
+ 'const skipped = output.split(String.fromCharCode(10)).filter((line) => line.includes(marker)).map((line) => line.slice(0, line.indexOf(marker)).trim());',
81
+ `const prefix = '${TESTS_REPORT_PREFIX}';`,
82
+ "console.warn(skipped.length === 0 ? prefix + ' no Postgres-backed test file skipped' : prefix + ' ' + skipped.length + ' Postgres-backed test files did not run, because the build has no database: ' + skipped.join(', '));",
83
+ 'process.exit(code);',
84
+ ])
85
+
86
+ /** A `c.cron` job name, which a crontab line carries unquoted. */
87
+ export const CONTRACT_JOB_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/
88
+ /** Five cron fields of digits and `*`, `/`, `,`, `-` only. */
89
+ export const CRON_SCHEDULE_PATTERN = /^[0-9*/,-]+(?: [0-9*/,-]+){4}$/
90
+
91
+ /** What one contract application's recipe depends on. `contractRecipe` refuses a value that a shell would read as more. */
92
+ export interface ContractRecipeInput {
93
+ readonly appId: string
94
+ /** The `@fabrika/check` release whose reader read the application (ADR-0062). */
95
+ readonly checkRelease: string
96
+ /** The absolute directory the build runs in: the checker reads the source there. */
97
+ readonly sourceRoot: string
98
+ /** The absolute directory the built application runs from: each generated script starts there. */
99
+ readonly runtimeRoot: string
100
+ /** With a database, every container migrates and checks its schema before the server starts. */
101
+ readonly database: boolean
102
+ /** Whether the build uploads its source maps, last and never failing the build. */
103
+ readonly sourceMaps: boolean
104
+ readonly jobs: readonly { readonly name: string; readonly schedule: string }[]
105
+ /** The contract declares a file collection: the sweep of removed files is scheduled. */
106
+ readonly files: boolean
107
+ /** A file collection scans its uploads: the scan pass is scheduled. */
108
+ readonly scan: boolean
109
+ }
110
+
111
+ /** One build step: a command run in the source root, or a generated file written there, line by line. */
112
+ export type ContractBuildStep = { readonly run: string } | { readonly write: string; readonly lines: readonly string[] }
113
+
114
+ /** One check of the build: a command run in the source root that fails the build. */
115
+ export interface ContractCheck {
116
+ readonly name: 'tests' | 'tsc' | 'fabrika-check'
117
+ readonly command: string
118
+ }
119
+
120
+ export interface ContractRecipe {
121
+ /** The steps after the authored SPA commands, in order. */
122
+ readonly build: readonly ContractBuildStep[]
123
+ /** The checks among `build`, in its order. */
124
+ readonly checks: readonly ContractCheck[]
125
+ /** The generated scripts every container runs before `start`, in order; any failure stops the activation. */
126
+ readonly init: readonly string[]
127
+ /** The generated script that starts the server, which only `exec`s it. */
128
+ readonly start: string
129
+ /** The command the start script `exec`s, run from the runtime root. */
130
+ readonly serve: readonly string[]
131
+ /** The command that migrates the application's database, run from the runtime root. */
132
+ readonly migrate: readonly string[]
133
+ /** The jobs to schedule, each on one container; a command runs from the runtime root. */
134
+ readonly jobs: readonly { readonly schedule: string; readonly command: string }[]
135
+ }
136
+
137
+ /**
138
+ * The recipe of one contract application. The application's tests run first: they are authored code, so everything
139
+ * after them is reinstalled, regenerated or checked again. They run `--isolate`, as the application skill runs them: a
140
+ * test file's DOM globals otherwise leak into the next file. `tsc` runs before the checker, as in the skill's loop: the
141
+ * usual quick fix of a type error is a cast or an `any`, which only the checker refuses, so the checker is the last word
142
+ * on the code. The build has no database, so the schema checks run as init scripts, after the migrations and before the
143
+ * readiness window opens.
144
+ */
145
+ export function contractRecipe(input: ContractRecipeInput): ContractRecipe {
146
+ const { appId, checkRelease, sourceRoot, runtimeRoot } = input
147
+ if (!CONTRACT_APP_ID_PATTERN.test(appId)) throw new Error('a contract application id must match [a-z0-9][a-z0-9-]*')
148
+ if (!FRAMEWORK_RELEASE_PATTERN.test(checkRelease)) throw new Error('the checker release must be a release version')
149
+ if (input.jobs.some((job) => !CONTRACT_JOB_NAME_PATTERN.test(job.name) || !CRON_SCHEDULE_PATTERN.test(job.schedule))) {
150
+ throw new Error('a job name or schedule is not one a generated crontab line may carry')
151
+ }
152
+ const script = (lines: readonly string[]): readonly string[] => ['#!/bin/sh', 'set -eu', `cd ${runtimeRoot}`, ...lines]
153
+ const init = input.database
154
+ ? [
155
+ { path: MIGRATE_SCRIPT, lines: script([`FABRIKA_APP_ID=${appId} exec ${MIGRATE.join(' ')}`]) },
156
+ // The committed schema types must match the migrated schema, and every generated isolation test must pass on a
157
+ // throwaway schema.
158
+ {
159
+ path: VERIFY_SCRIPT,
160
+ lines: script([`FABRIKA_APP_ID=${appId} ${FABRIKA_BIN} db codegen --verify`, `FABRIKA_APP_ID=${appId} exec ${FABRIKA_BIN} db test-isolation`]),
161
+ },
162
+ ]
163
+ : []
164
+ const tests: ContractCheck = { name: 'tests', command: TESTS_COMMAND }
165
+ const tsc: ContractCheck = {
166
+ name: 'tsc',
167
+ command: `cd /tmp && bun x --bun -p typescript@${PLATFORM_TYPESCRIPT_VERSION} tsc -p ${sourceRoot}/${PLATFORM_TSCONFIG}`,
168
+ }
169
+ const check: ContractCheck = {
170
+ name: 'fabrika-check',
171
+ command: `cd /tmp && bun x --bun -p @fabrika/check@${checkRelease} fabrika-check ${sourceRoot}`,
172
+ }
173
+ return {
174
+ build: [
175
+ { run: INSTALL },
176
+ { run: tests.command },
177
+ { run: `rm -rf node_modules && ${INSTALL}` },
178
+ { run: `bun -e "await import('@fabrika/app/build').then((build) => build.writeGeneratedEntry('.', 'bun'))"` },
179
+ { write: PLATFORM_TSCONFIG, lines: [PLATFORM_TSCONFIG_CONTENTS] },
180
+ { run: tsc.command },
181
+ { run: check.command },
182
+ ...init.map((generated) => ({ write: generated.path, lines: generated.lines })),
183
+ { write: START_SCRIPT, lines: script([`exec ${SERVE.join(' ')}`]) },
184
+ // After every check, so a map is uploaded only for a build that can deploy; the report line says how it went.
185
+ ...(input.sourceMaps ? [{ run: `cd /tmp && bun x --bun -p @fabrika/check@${checkRelease} fabrika-source-maps ${sourceRoot} || true` }] : []),
186
+ ],
187
+ checks: [tests, tsc, check],
188
+ init: init.map((generated) => generated.path),
189
+ start: START_SCRIPT,
190
+ serve: SERVE,
191
+ migrate: MIGRATE,
192
+ jobs: [
193
+ ...input.jobs.map((job) => ({ schedule: job.schedule, command: `${FABRIKA_BIN} job run ${job.name}` })),
194
+ ...(input.files ? [{ schedule: FILE_SWEEP_SCHEDULE, command: `${FABRIKA_BIN} job run ${FILE_SWEEP_JOB}` }] : []),
195
+ ...(input.scan ? [{ schedule: FILE_SCAN_SCHEDULE, command: `${FABRIKA_BIN} job run ${FILE_SCAN_JOB}` }] : []),
196
+ ],
197
+ }
198
+ }
package/src/build.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // the only module that reads `process.env` and the only caller of `boot`, through the runtime adapter, and it imports
3
3
  // the application only after the adapter took the framework's variables out of the process.
4
4
 
5
- import { mkdir, writeFile } from 'node:fs/promises'
5
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
6
6
  import { dirname, join } from 'node:path'
7
7
 
8
8
  /** An application root with this file is a contract application. The paths equal `@fabrika/check`'s layout. */
@@ -14,8 +14,24 @@ export const GENERATED_ENTRY_FILE = 'fabrika/entry.generated.ts'
14
14
  /** The module whose default export is `serve(contract, …)`, relative to the application root. */
15
15
  export const IMPLEMENTATION_FILE = 'src/app.ts'
16
16
 
17
+ /** The application's migrations as a module, relative to the application root: a Worker reads no file system. */
18
+ export const GENERATED_MIGRATIONS_FILE = 'fabrika/migrations.generated.ts'
19
+
20
+ /** Where an application keeps its migrations, relative to its root; `fabrika db new` writes there. */
21
+ export const MIGRATIONS_DIRECTORY = 'db/migrations'
22
+
17
23
  export type EntryRuntime = 'bun' | 'cloudflare'
18
24
 
25
+ export * from './build-recipe.js'
26
+
27
+ // In a module of their own, which the Worker imports without this one's file system.
28
+ export {
29
+ CLOUDFLARE_APPLICATION_BINDING,
30
+ CLOUDFLARE_APPLICATION_CLASS,
31
+ CLOUDFLARE_ASSETS_BINDING,
32
+ CLOUDFLARE_FILES_BINDING,
33
+ } from './cloudflare/names.js'
34
+
19
35
  const HEADER = '// @generated by `fabrika app build` from src/app.ts. Do not edit: every build rewrites it.'
20
36
 
21
37
  /** The entrypoint source for one runtime. */
@@ -35,19 +51,59 @@ export function generatedEntry(runtime: EntryRuntime): string {
35
51
  return [
36
52
  HEADER,
37
53
  "import { createContractWorker, markProductionProcess } from '@fabrika/app/cloudflare'",
54
+ "import { migrations } from './migrations.generated'",
38
55
  '',
39
- '// The test kits refuse to load from here on; the application is imported only after the framework took its variables.',
56
+ "export { FabrikaApplication } from '@fabrika/app/cloudflare'",
57
+ '',
58
+ '// The test kits refuse to load from here on; the object imports the application only after the framework took its variables.',
40
59
  'markProductionProcess()',
41
- "export default createContractWorker(() => import('../src/app'))",
60
+ "export default createContractWorker(() => import('../src/app'), { migrations })",
42
61
  '',
43
62
  ].join('\n')
44
63
  }
45
64
  }
46
65
 
47
- /** Writes the entrypoint under `root` and returns its path. */
66
+ /** Writes the entrypoint under `root` and returns its path; the Cloudflare one also writes the migrations module it imports. */
48
67
  export async function writeGeneratedEntry(root: string, runtime: EntryRuntime): Promise<string> {
49
68
  const path = join(root, GENERATED_ENTRY_FILE)
50
69
  await mkdir(dirname(path), { recursive: true })
51
70
  await writeFile(path, generatedEntry(runtime))
71
+ if (runtime === 'cloudflare') {
72
+ await writeGeneratedMigrations(root)
73
+ }
74
+ return path
75
+ }
76
+
77
+ /** The application's migrations under `root`, every `.sql` file of `db/migrations` by name; none without the directory. */
78
+ export async function applicationMigrations(root: string): Promise<readonly { readonly name: string; readonly sql: string }[]> {
79
+ const directory = join(root, MIGRATIONS_DIRECTORY)
80
+ let names: string[]
81
+ try {
82
+ names = await readdir(directory)
83
+ } catch (error) {
84
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
85
+ return []
86
+ }
87
+ throw error
88
+ }
89
+ const files = names.filter((name) => name.endsWith('.sql')).sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
90
+ return await Promise.all(files.map(async (name) => ({ name, sql: await readFile(join(directory, name), 'utf8') })))
91
+ }
92
+
93
+ /** The migrations module source: the files' names and texts, verbatim, so their checksums stay the files'. */
94
+ export function generatedMigrations(migrations: readonly { readonly name: string; readonly sql: string }[]): string {
95
+ return [
96
+ `// @generated by \`fabrika app build\` from ${MIGRATIONS_DIRECTORY}. Do not edit: every build rewrites it.`,
97
+ '',
98
+ `export const migrations: readonly { readonly name: string; readonly sql: string }[] = ${JSON.stringify(migrations, null, '\t')}`,
99
+ '',
100
+ ].join('\n')
101
+ }
102
+
103
+ /** Writes the migrations module under `root` and returns its path. */
104
+ export async function writeGeneratedMigrations(root: string): Promise<string> {
105
+ const path = join(root, GENERATED_MIGRATIONS_FILE)
106
+ await mkdir(dirname(path), { recursive: true })
107
+ await writeFile(path, generatedMigrations(await applicationMigrations(root)))
52
108
  return path
53
109
  }
package/src/bun.ts CHANGED
@@ -5,14 +5,16 @@
5
5
  // contract application is served through `serveContract`, from the generated
6
6
  // entrypoint.
7
7
 
8
- import { HttpIamMembership } from '@fabrika/auth'
9
- import type { Database, PostgresOptions } from '@fabrika/db'
8
+ import { HttpIamMembership, HttpIamTerminals } from '@fabrika/auth'
9
+ import type { Database } from '@fabrika/db'
10
10
  import { isScopedDatabase } from '@fabrika/db/framework'
11
+ import type { PostgresOptions } from '@fabrika/db/postgres'
11
12
  import type { EmailProviderRegistry } from '@fabrika/email'
12
13
  import { directoryEmailProvider } from '@fabrika/email/directory'
13
14
  import { parseJwks } from '@fabrika/jwks'
14
15
  import type { FileObjectStore } from '@fabrika/platform'
15
16
  import { applicationRuntimeUrl, S3BlobStore } from '@fabrika/platform-node'
17
+ import { lookup } from 'node:dns/promises'
16
18
  import { diskAssets } from './disk-assets.js'
17
19
  import type { FetchService, RequestExecutionContext } from './fetch.js'
18
20
  import { clamscanScanner } from './files/clamscan.js'
@@ -117,7 +119,7 @@ export interface ContractServer {
117
119
 
118
120
  /**
119
121
  * What the adapter takes from `@fabrika/db` before the application loads, so no application module can stand in for
120
- * it: a replaced `Database.postgres` would receive the database URL.
122
+ * it: a replaced `openPostgres` would receive the database URL.
121
123
  */
122
124
  interface DataLayerOpeners {
123
125
  readonly postgres: (options: PostgresOptions) => Database
@@ -126,8 +128,8 @@ interface DataLayerOpeners {
126
128
 
127
129
  /** Loads `@fabrika/db` before the application: it captures its built-ins and freezes the Kysely compiler at load. */
128
130
  async function loadDataLayer(): Promise<DataLayerOpeners> {
129
- const { Database, applicationSchema } = await import('@fabrika/db')
130
- return { postgres: Database.postgres, applicationSchema }
131
+ const [{ applicationSchema }, { openPostgres }] = await Promise.all([import('@fabrika/db'), import('@fabrika/db/postgres')])
132
+ return { postgres: openPostgres, applicationSchema }
131
133
  }
132
134
 
133
135
  /** How long a stopping process waits for the work its requests left running; a kicked run aborts at once. */
@@ -277,12 +279,22 @@ export function openFileStorage(framework: Readonly<Record<string, string>>): Fi
277
279
  /**
278
280
  * IAM's membership transport, when the platform gives the process `FABRIKA_IAM_RPC_URL` (IAM's public origin) and
279
281
  * `FABRIKA_IAM_RPC_KEY` (this environment's membership transport key); without both, there is none, and `boot` refuses
280
- * a contract that needs one.
282
+ * a contract that needs one. The identifications at a terminal travel on the same transport.
281
283
  */
282
- function httpMembership(framework: Readonly<Record<string, string>>): HttpIamMembership | undefined {
284
+ function httpIamTransport(framework: Readonly<Record<string, string>>): { readonly origin: string; readonly key: string } | undefined {
283
285
  const origin = framework['FABRIKA_IAM_RPC_URL']?.trim() ?? ''
284
286
  const key = framework['FABRIKA_IAM_RPC_KEY']?.trim() ?? ''
285
- return origin === '' || key === '' ? undefined : new HttpIamMembership({ origin, key })
287
+ return origin === '' || key === '' ? undefined : { origin, key }
288
+ }
289
+
290
+ function httpMembership(framework: Readonly<Record<string, string>>): HttpIamMembership | undefined {
291
+ const transport = httpIamTransport(framework)
292
+ return transport === undefined ? undefined : new HttpIamMembership(transport)
293
+ }
294
+
295
+ function httpTerminals(framework: Readonly<Record<string, string>>): HttpIamTerminals | undefined {
296
+ const transport = httpIamTransport(framework)
297
+ return transport === undefined ? undefined : new HttpIamTerminals(transport)
286
298
  }
287
299
 
288
300
  /** IAM's public token exchange, at the issuer every deploy names; `boot` reports a missing issuer itself. */
@@ -381,6 +393,7 @@ async function startContractServer(
381
393
  const iam = issuerJwks(environment.framework['FABRIKA_IAM_ISSUER'])
382
394
  const database = isScopedDatabase(app.database) ? openApplicationDatabase(data, environment.framework) : undefined
383
395
  const membership = httpMembership(environment.framework)
396
+ const terminals = httpTerminals(environment.framework)
384
397
  // Zerops is where a Bun application runs, and a namespace there is one private network (ADR-0022 rule 5).
385
398
  const files = openFileStorage(environment.framework)
386
399
  const workloadTokens = workloadTokensOf(environment.framework)
@@ -391,7 +404,9 @@ async function startContractServer(
391
404
  database,
392
405
  files,
393
406
  membership,
407
+ terminals,
394
408
  workloadTokens,
409
+ resolveHost,
395
410
  requireProxySecret: true,
396
411
  localEmailProviders: LOCAL_EMAIL_PROVIDERS,
397
412
  connections: databaseConnections(environment.framework[DATABASE_CONNECTIONS]) ?? DEFAULT_DATABASE_CONNECTIONS,
@@ -419,6 +434,13 @@ async function startContractServer(
419
434
  return { port: server.port ?? port, stop }
420
435
  }
421
436
 
437
+ /**
438
+ * Every address `hostname` resolves to, as the operating system's resolver answers Bun's own `fetch`: a cron job's public
439
+ * egress refuses a hop whose name resolves to any private one (ADR-0070).
440
+ */
441
+ const resolveHost = async (hostname: string): Promise<readonly string[]> =>
442
+ (await lookup(hostname, { all: true, verbatim: true })).map((entry) => entry.address)
443
+
422
444
  /** The application module at `path`, imported when the loader runs; its default export must be what `serve` returned. */
423
445
  export function implementationAt(path: string): ImplementationLoader {
424
446
  return async () => {
@@ -454,6 +476,7 @@ export async function runContractJob(
454
476
  try {
455
477
  const workloadTokens = workloadTokensOf(environment.framework)
456
478
  const membership = httpMembership(environment.framework)
479
+ const terminals = httpTerminals(environment.framework)
457
480
  const files = openFileStorage(environment.framework)
458
481
  return await boot(app, {
459
482
  environment,
@@ -462,7 +485,9 @@ export async function runContractJob(
462
485
  database,
463
486
  files,
464
487
  membership,
488
+ terminals,
465
489
  workloadTokens,
490
+ resolveHost,
466
491
  requireProxySecret: true,
467
492
  localEmailProviders: LOCAL_EMAIL_PROVIDERS,
468
493
  // ClamAV on PATH, as the provider's build installs it for a `scan: true` file kind; missing, the scan fails closed.