@fabrika/app 0.9.0-dev.1 → 0.9.0-dev.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 (134) hide show
  1. package/package.json +27 -26
  2. package/skill/SKILL.md +1 -1
  3. package/skill/references/app-setup.md +8 -8
  4. package/src/dev/changes.ts +5 -43
  5. package/src/dev/contract-digest.ts +17 -0
  6. package/src/dev/index.ts +1 -1
  7. package/src/dev/launcher.ts +69 -22
  8. package/src/dev/wire.ts +2 -1
  9. package/types/src/agent-skill.d.ts +29 -0
  10. package/types/src/build.d.ts +11 -0
  11. package/types/src/bun.d.ts +66 -0
  12. package/types/src/client/caller.d.ts +19 -0
  13. package/types/src/client/contract.d.ts +131 -0
  14. package/types/src/client/error-reporting.d.ts +66 -0
  15. package/types/src/client/index.d.ts +4 -0
  16. package/types/src/client/rpc.d.ts +14 -0
  17. package/types/src/client/share.d.ts +2 -0
  18. package/types/src/client/transport.d.ts +42 -0
  19. package/types/src/cloudflare.d.ts +49 -0
  20. package/types/src/contract/artifact-schema.d.ts +8 -0
  21. package/types/src/contract/attachment.d.ts +41 -0
  22. package/types/src/contract/build.d.ts +12 -0
  23. package/types/src/contract/compile.d.ts +11 -0
  24. package/types/src/contract/connect.d.ts +51 -0
  25. package/types/src/contract/context.d.ts +501 -0
  26. package/types/src/contract/declaration.d.ts +303 -0
  27. package/types/src/contract/defaults.d.ts +208 -0
  28. package/types/src/contract/define.d.ts +359 -0
  29. package/types/src/contract/file.d.ts +44 -0
  30. package/types/src/contract/group.d.ts +274 -0
  31. package/types/src/contract/handled.d.ts +18 -0
  32. package/types/src/contract/index.d.ts +19 -0
  33. package/types/src/contract/kit.d.ts +210 -0
  34. package/types/src/contract/platform.d.ts +9 -0
  35. package/types/src/contract/problems.d.ts +14 -0
  36. package/types/src/contract/redirect.d.ts +25 -0
  37. package/types/src/contract/schema-from-shape.d.ts +10 -0
  38. package/types/src/contract/schema-shape.d.ts +5 -0
  39. package/types/src/contract/snapshot-source.d.ts +15 -0
  40. package/types/src/contract/token.d.ts +58 -0
  41. package/types/src/contract/vocabulary.d.ts +30 -0
  42. package/types/src/contract/webhook-binding.d.ts +24 -0
  43. package/types/src/contract/webhook-scheme.d.ts +23 -0
  44. package/types/src/dev/changes.d.ts +7 -0
  45. package/types/src/dev/contract-digest.d.ts +1 -0
  46. package/types/src/dev/index.d.ts +2 -0
  47. package/types/src/dev/launcher.d.ts +32 -0
  48. package/types/src/dev/output.d.ts +22 -0
  49. package/types/src/dev/wire.d.ts +122 -0
  50. package/types/src/disk-assets.d.ts +3 -0
  51. package/types/src/errors.d.ts +46 -0
  52. package/types/src/fetch.d.ts +7 -0
  53. package/types/src/files/archive.d.ts +26 -0
  54. package/types/src/files/clamscan.d.ts +32 -0
  55. package/types/src/files/handle.d.ts +106 -0
  56. package/types/src/files/kinds.d.ts +105 -0
  57. package/types/src/files/scan.d.ts +80 -0
  58. package/types/src/files/sniff.d.ts +7 -0
  59. package/types/src/files/streams.d.ts +17 -0
  60. package/types/src/files/treatment.d.ts +18 -0
  61. package/types/src/from-row.d.ts +20 -0
  62. package/types/src/honeypot.d.ts +2 -0
  63. package/types/src/implement/anonymous.d.ts +26 -0
  64. package/types/src/implement/attachment.d.ts +14 -0
  65. package/types/src/implement/boot.d.ts +109 -0
  66. package/types/src/implement/bounded.d.ts +22 -0
  67. package/types/src/implement/browser-config.d.ts +19 -0
  68. package/types/src/implement/capability.d.ts +140 -0
  69. package/types/src/implement/challenge.d.ts +75 -0
  70. package/types/src/implement/client-address.d.ts +11 -0
  71. package/types/src/implement/connect.d.ts +67 -0
  72. package/types/src/implement/crashes.d.ts +31 -0
  73. package/types/src/implement/dispatch.d.ts +218 -0
  74. package/types/src/implement/egress.d.ts +14 -0
  75. package/types/src/implement/environment.d.ts +14 -0
  76. package/types/src/implement/failures.d.ts +34 -0
  77. package/types/src/implement/faults.d.ts +44 -0
  78. package/types/src/implement/headers.d.ts +7 -0
  79. package/types/src/implement/implement.d.ts +58 -0
  80. package/types/src/implement/index.d.ts +24 -0
  81. package/types/src/implement/invocation.d.ts +120 -0
  82. package/types/src/implement/membership.d.ts +201 -0
  83. package/types/src/implement/multipart.d.ts +36 -0
  84. package/types/src/implement/params.d.ts +7 -0
  85. package/types/src/implement/principal.d.ts +52 -0
  86. package/types/src/implement/project.d.ts +11 -0
  87. package/types/src/implement/public.d.ts +61 -0
  88. package/types/src/implement/serve.d.ts +25 -0
  89. package/types/src/implement/tokens.d.ts +49 -0
  90. package/types/src/implement/types.d.ts +243 -0
  91. package/types/src/implement/urls.d.ts +15 -0
  92. package/types/src/implement/verifiers.d.ts +19 -0
  93. package/types/src/implement/webhook.d.ts +55 -0
  94. package/types/src/implement/workload.d.ts +56 -0
  95. package/types/src/implement/xlsx.d.ts +6 -0
  96. package/types/src/index.d.ts +12 -0
  97. package/types/src/jobs/exception.d.ts +40 -0
  98. package/types/src/jobs/mail.d.ts +67 -0
  99. package/types/src/jobs/report.d.ts +56 -0
  100. package/types/src/jobs/runner.d.ts +145 -0
  101. package/types/src/jobs/schedule.d.ts +15 -0
  102. package/types/src/jobs/scrub.d.ts +12 -0
  103. package/types/src/jobs/types.d.ts +92 -0
  104. package/types/src/react/caller.d.ts +30 -0
  105. package/types/src/react/errors.d.ts +40 -0
  106. package/types/src/react/hooks.d.ts +110 -0
  107. package/types/src/react/index.d.ts +5 -0
  108. package/types/src/react/provider.d.ts +58 -0
  109. package/types/src/react/store.d.ts +68 -0
  110. package/types/src/react/testing.d.ts +28 -0
  111. package/types/src/react/types.d.ts +33 -0
  112. package/types/src/rpc/builder.d.ts +52 -0
  113. package/types/src/rpc/contract.d.ts +23 -0
  114. package/types/src/rpc/dispatcher.d.ts +11 -0
  115. package/types/src/rpc/types.d.ts +62 -0
  116. package/types/src/service/define.d.ts +42 -0
  117. package/types/src/service/index.d.ts +3 -0
  118. package/types/src/service/serve.d.ts +28 -0
  119. package/types/src/standard-schema.d.ts +55 -0
  120. package/types/src/testing/caller.d.ts +95 -0
  121. package/types/src/testing/challenge.d.ts +11 -0
  122. package/types/src/testing/dev.d.ts +97 -0
  123. package/types/src/testing/endpoint-access.d.ts +99 -0
  124. package/types/src/testing/index.d.ts +12 -0
  125. package/types/src/testing/isolation.d.ts +3 -0
  126. package/types/src/testing/kit.d.ts +182 -0
  127. package/types/src/testing/membership.d.ts +132 -0
  128. package/types/src/testing/policy-isolation.d.ts +113 -0
  129. package/types/src/testing/preload.d.ts +1 -0
  130. package/types/src/testing/proxy.d.ts +40 -0
  131. package/types/src/testing/runner.d.ts +8 -0
  132. package/types/src/testing/sample-file.d.ts +5 -0
  133. package/types/src/testing/status.d.ts +4 -0
  134. 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.9.0-dev.1",
3
+ "version": "0.9.0-dev.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,87 +14,88 @@
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
  },
85
86
  "./dev": {
86
87
  "bun": "./src/dev/index.ts",
87
- "types": "./src/dev/index.ts",
88
+ "types": "./types/src/dev/index.d.ts",
88
89
  "default": "./src/dev/index.ts"
89
90
  },
90
91
  "./dev/wire": {
91
92
  "bun": "./src/dev/wire.ts",
92
- "types": "./src/dev/wire.ts",
93
+ "types": "./types/src/dev/wire.d.ts",
93
94
  "default": "./src/dev/wire.ts"
94
95
  },
95
96
  "./agent-skill": {
96
97
  "bun": "./src/agent-skill.ts",
97
- "types": "./src/agent-skill.ts",
98
+ "types": "./types/src/agent-skill.d.ts",
98
99
  "default": "./src/agent-skill.ts"
99
100
  }
100
101
  },
@@ -103,14 +104,14 @@
103
104
  "test": "bun test --path-ignore-patterns='src/react/**' && bun test --isolate src/react"
104
105
  },
105
106
  "dependencies": {
106
- "@fabrika/auth": "0.9.0-dev.1",
107
- "@fabrika/auth-core": "0.9.0-dev.1",
108
- "@fabrika/db": "0.9.0-dev.1",
109
- "@fabrika/email": "0.9.0-dev.1",
110
- "@fabrika/jwks": "0.9.0-dev.1",
111
- "@fabrika/platform": "0.9.0-dev.1",
112
- "@fabrika/platform-node": "0.9.0-dev.1",
113
- "@fabrika/proxy-core": "0.9.0-dev.1",
107
+ "@fabrika/auth": "0.9.0-dev.2",
108
+ "@fabrika/auth-core": "0.9.0-dev.2",
109
+ "@fabrika/db": "0.9.0-dev.2",
110
+ "@fabrika/email": "0.9.0-dev.2",
111
+ "@fabrika/jwks": "0.9.0-dev.2",
112
+ "@fabrika/platform": "0.9.0-dev.2",
113
+ "@fabrika/platform-node": "0.9.0-dev.2",
114
+ "@fabrika/proxy-core": "0.9.0-dev.2",
114
115
  "altcha-lib": "2.6.0",
115
116
  "jose": "^5.9.0"
116
117
  },
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: fabrika-app
3
- version: 0.9.0-dev.1
3
+ version: 0.9.0-dev.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
@@ -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.9.0-dev.1",
23
- "@fabrika/db": "0.9.0-dev.1",
24
- "@fabrika/provider-zerops": "0.9.0-dev.1",
25
- "@fabrika/proxy-core": "0.9.0-dev.1",
26
- "@fabrika/ui": "0.9.0-dev.1",
22
+ "@fabrika/app": "0.9.0-dev.2",
23
+ "@fabrika/db": "0.9.0-dev.2",
24
+ "@fabrika/provider-zerops": "0.9.0-dev.2",
25
+ "@fabrika/proxy-core": "0.9.0-dev.2",
26
+ "@fabrika/ui": "0.9.0-dev.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.9.0-dev.1",
34
- "@fabrika/cli": "0.9.0-dev.1",
35
- "@fabrika/platform": "0.9.0-dev.1",
33
+ "@fabrika/check": "0.9.0-dev.2",
34
+ "@fabrika/cli": "0.9.0-dev.2",
35
+ "@fabrika/platform": "0.9.0-dev.2",
36
36
  "@happy-dom/global-registrator": "^20.11.2",
37
37
  "@types/bun": "latest",
38
38
  "@types/react": "^19.0.0",
@@ -1,64 +1,26 @@
1
1
  // What a sync changes, read off its paths: whether the build owns one (then the commit needs a bootstrap run), whether
2
- // the migrations changed, and whether a web source did. Pure: the caller reads the files.
2
+ // the migrations changed, and whether a web source did. Whether the contract changed is the digest's to say, not a path's.
3
3
 
4
- import { posix } from 'node:path'
5
4
  import { CONTRACT_FILE } from '../build.js'
6
5
 
7
6
  /** The application's migrations, as `fabrika db migrate` reads them. */
8
7
  export const MIGRATIONS_DIRECTORY = 'db/migrations/'
9
8
 
10
- /** Files whose change the build turns into something else: an install, the contract digest and proxy table, a generated file. */
9
+ /** Files whose change the build turns into something else: an install, or the generated run and build descriptors. */
11
10
  const BUILD_OWNED_FILES: ReadonlySet<string> = new Set(['package.json', 'bun.lock', 'bun.lockb', 'bunfig.toml', 'zerops.yaml', 'zerops.yml'])
12
11
  const BUILD_OWNED_DIRECTORIES = ['fabrika/', 'node_modules/'] as const
13
12
 
14
13
  /** Server code and the database: a change only there leaves the SPA as it is. */
15
14
  const SERVER_DIRECTORIES = ['src/', 'db/'] as const
16
15
 
17
- /** Reads a file at the head the sync brings, or null when it does not exist there. */
18
- export type SourceReader = (path: string) => string | null
19
-
20
- const RELATIVE_IMPORT = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)['"](\.{1,2}\/[^'"]+)['"]/g
21
-
22
- /** The repository paths a relative specifier of `from` may name, in the order Bun resolves a TypeScript import. */
23
- function candidates(from: string, specifier: string): string[] {
24
- const base = posix.normalize(posix.join(posix.dirname(from), specifier))
25
- if (base.startsWith('../')) return []
26
- const withoutJs = base.endsWith('.js') ? base.slice(0, -3) : base
27
- return [base, `${withoutJs}.ts`, `${withoutJs}.tsx`, `${withoutJs}/index.ts`]
28
- }
29
-
30
- /** `fabrika.contract.ts` and every module it reaches by a relative import, read through `read`. */
31
- export function contractModules(read: SourceReader): Set<string> {
32
- const modules = new Set<string>()
33
- const pending = [CONTRACT_FILE]
34
- while (pending.length > 0) {
35
- const path = pending.pop() ?? ''
36
- if (modules.has(path)) continue
37
- const text = read(path)
38
- if (text === null) continue
39
- modules.add(path)
40
- for (const match of text.matchAll(RELATIVE_IMPORT)) {
41
- const resolved = candidates(path, match[1] ?? '').find((candidate) => read(candidate) !== null)
42
- if (resolved !== undefined) pending.push(resolved)
43
- }
44
- }
45
- return modules
46
- }
47
-
48
- /**
49
- * Why the change needs a bootstrap run, in one line, or null when the launcher may apply it. `before` reads the
50
- * container's files, `after` the files at the target: a module the contract imports at either counts.
51
- */
52
- export function bootstrapReason(paths: readonly string[], before: SourceReader, after: SourceReader): string | null {
53
- const contract = new Set([...contractModules(before), ...contractModules(after)])
16
+ /** Why the change needs a bootstrap run, in one line, or null when the launcher may apply it. */
17
+ export function bootstrapReason(paths: readonly string[]): string | null {
54
18
  for (const path of paths) {
55
19
  const why = BUILD_OWNED_FILES.has(path)
56
20
  ? 'the dependencies, the lockfile or the build descriptor'
57
21
  : BUILD_OWNED_DIRECTORIES.some((directory) => path.startsWith(directory))
58
22
  ? 'the generated files or the installed packages'
59
- : contract.has(path)
60
- ? 'the contract or a module it imports (its digest and the proxy table change)'
61
- : path.startsWith('fabrika.') && !path.includes('/')
23
+ : path !== CONTRACT_FILE && path.startsWith('fabrika.') && !path.includes('/')
62
24
  ? 'a fabrika.* file'
63
25
  : null
64
26
  if (why !== null) return `${path} changed, which is ${why}; that needs a bootstrap run: call bind_dev again`
@@ -0,0 +1,17 @@
1
+ // The launcher's child that answers whether a commit changed the contract (ADR-0049). It runs in the application root
2
+ // with no credential in its environment, loads the application module the launcher itself never loads, and prints the
3
+ // digest the application's boot compares with `FABRIKA_CONTRACT_DIGEST`.
4
+
5
+ import { contractDigest } from '@fabrika/auth-core'
6
+ import { join } from 'node:path'
7
+ import { IMPLEMENTATION_FILE } from '../build.js'
8
+ import { compileContract } from '../contract/compile.js'
9
+ import { isContractImplementation } from '../implement/implement.js'
10
+
11
+ const module: unknown = await import(join(process.cwd(), IMPLEMENTATION_FILE))
12
+ const app = typeof module === 'object' && module !== null ? Reflect.get(module, 'default') : undefined
13
+ if (!isContractImplementation(app)) {
14
+ console.error(`${IMPLEMENTATION_FILE} does not default-export serve(contract, { … })`)
15
+ process.exit(1)
16
+ }
17
+ console.log(contractDigest(compileContract(app.contract, app.database?.tables ?? {})))
package/src/dev/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // @fabrika/app/dev — the launcher of a `dev` environment, which the generated dev entrypoint runs (Bun only).
2
2
 
3
- export { bootstrapReason, contractModules, migrationsChanged, webSourceChanged } from './changes.js'
3
+ export { bootstrapReason, migrationsChanged, webSourceChanged } from './changes.js'
4
4
  export {
5
5
  type DevLauncher,
6
6
  type DevLauncherOptions,
@@ -4,14 +4,20 @@
4
4
  // files, which it writes, then it migrates when `db/migrations/` changed, runs the SPA commands when a web source
5
5
  // changed, and respawns the application. A change the build owns is refused with one line: it needs a bootstrap run.
6
6
 
7
- import { DEV_LAUNCHER_CONFIG_FILE, DEV_LAUNCHER_STATE_FILE, type DevLauncherConfig, platformTransportVariables } from '@fabrika/auth-core'
7
+ import {
8
+ DEV_LAUNCHER_CONFIG_FILE,
9
+ DEV_LAUNCHER_STATE_FILE,
10
+ type DevLauncherConfig,
11
+ FABRIKA_CONTRACT_DIGEST,
12
+ platformTransportVariables,
13
+ } from '@fabrika/auth-core'
8
14
  import { applicationRuntimeUrl, dropApplicationDatabase } from '@fabrika/platform-node'
9
- import { existsSync, readFileSync } from 'node:fs'
10
15
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
11
16
  import { dirname, join } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
12
18
  import { z } from 'zod/v4'
13
19
  import { makeProcessUndumpable } from '../undumpable.js'
14
- import { bootstrapReason, migrationsChanged, type SourceReader, webSourceChanged } from './changes.js'
20
+ import { bootstrapReason, migrationsChanged, webSourceChanged } from './changes.js'
15
21
  import { OutputRing, type OutputSink, processOutput, readLines } from './output.js'
16
22
  import {
17
23
  DEV_SOURCE_REPORT_PATH,
@@ -41,6 +47,9 @@ const STOP_GRACE_MS = 3_000
41
47
  const CHECK_TIMEOUT_MS = 10 * 60_000
42
48
  const CRASH_BACKOFF_MS = [1_000, 2_000, 5_000, 10_000] as const
43
49
  const FORWARDED_SIGNALS = ['SIGTERM', 'SIGINT', 'SIGHUP'] as const
50
+ const DIGEST_TIMEOUT_MS = 60_000
51
+ const CONTRACT_DIGEST = /^sha256:[0-9a-f]{64}$/
52
+ const CONTRACT_DIGEST_CHILD = fileURLToPath(new URL('./contract-digest.ts', import.meta.url))
44
53
 
45
54
  const commitSha = z.string().regex(/^[0-9a-f]{40}$/)
46
55
  /** A literal path of unreserved characters, as the provider's readiness probe requests it. */
@@ -318,12 +327,12 @@ class Launcher {
318
327
  // A migration's ALTER would queue behind the live version's locks, and a reset drops the role it connects as.
319
328
  if (this.provisioningUrl !== null && steps.migrate) await this.stopApplication()
320
329
  if (steps.reset) {
321
- failure = await this.resetDatabase()
330
+ failure = await this.timed('the reset', () => this.resetDatabase())
322
331
  this.state = { ...this.state, reset: steps.resetTo }
323
332
  }
324
- if (failure === null && steps.migrate) failure = await this.migrate()
325
- if (failure === null && steps.web) failure = await this.buildSpa()
326
- const served = await this.restartApplication()
333
+ if (failure === null && steps.migrate) failure = await this.timed('the migration', () => this.migrate())
334
+ if (failure === null && steps.web) failure = await this.timed('the SPA build', () => this.buildSpa())
335
+ const served = await this.timed('the restart until healthy', () => this.restartApplication())
327
336
  failure ??= served
328
337
  this.status = failure === null ? 'serving' : 'failed'
329
338
  this.target = null
@@ -332,17 +341,37 @@ class Launcher {
332
341
  await this.report()
333
342
  }
334
343
 
344
+ /** Run one step of a sync and log how long it took: what a slow `apply_changes` is read from. */
345
+ private async timed(step: string, run: () => Promise<string | null>): Promise<string | null> {
346
+ const started = performance.now()
347
+ const failure = await run()
348
+ this.say(`${step} ${failure === null ? 'took' : 'failed after'} ${Math.round(performance.now() - started)} ms`)
349
+ return failure
350
+ }
351
+
335
352
  private async writeSync(sync: DevSync): Promise<SyncOutcome> {
336
353
  if (sync.from !== this.state.head) {
337
354
  return { kind: 'refused', reason: `Control sent changes from ${sync.from}, and this container holds ${this.state.head}; call bind_dev again` }
338
355
  }
339
356
  const paths = sync.files.map((file) => file.path)
340
- const incoming = new Map(sync.files.map((file) => [file.path, file.content]))
341
- const before: SourceReader = (path) => this.readLocal(path)
342
- const after: SourceReader = (path) => incoming.has(path) ? incoming.get(path) ?? null : this.readLocal(path)
343
- const reason = bootstrapReason(paths, before, after)
357
+ const reason = bootstrapReason(paths)
344
358
  if (reason !== null) return { kind: 'refused', reason }
345
- for (const file of sync.files) {
359
+ const previous = await Promise.all(
360
+ sync.files.map(async (file) => ({ path: file.path, content: await readFile(join(this.root, file.path)).catch(() => null) })),
361
+ )
362
+ await this.writeFiles(sync.files)
363
+ const changedContract = await this.contractChange()
364
+ if (changedContract !== null) {
365
+ await this.writeFiles(previous)
366
+ return { kind: 'refused', reason: changedContract }
367
+ }
368
+ this.state = { ...this.state, head: sync.to }
369
+ this.say(`synced ${sync.files.length} files to ${sync.to}`)
370
+ return { kind: 'applied', migrations: migrationsChanged(paths), web: this.config.spaBuild.length > 0 && webSourceChanged(paths) }
371
+ }
372
+
373
+ private async writeFiles(files: readonly { readonly path: string; readonly content: string | Uint8Array | null }[]): Promise<void> {
374
+ for (const file of files) {
346
375
  const path = join(this.root, file.path)
347
376
  if (file.content === null) {
348
377
  await rm(path, { force: true })
@@ -352,19 +381,37 @@ class Launcher {
352
381
  await writeFile(`${path}.fabrika-dev`, file.content)
353
382
  await rename(`${path}.fabrika-dev`, path)
354
383
  }
355
- this.state = { ...this.state, head: sync.to }
356
- this.say(`synced ${sync.files.length} files to ${sync.to}`)
357
- return { kind: 'applied', migrations: migrationsChanged(paths), web: this.config.spaBuild.length > 0 && webSourceChanged(paths) }
358
384
  }
359
385
 
360
- private readLocal(path: string): string | null {
361
- const absolute = join(this.root, path)
362
- if (!existsSync(absolute)) return null
363
- try {
364
- return readFileSync(absolute, 'utf8')
365
- } catch {
386
+ /**
387
+ * Why the files on disk need a bootstrap run because their contract digest is not the one the bootstrap run deployed,
388
+ * or null. A child computes it: it loads the application module, which the launcher never does, with no credential.
389
+ * A digest it cannot compute is no reason: the respawned application then fails to boot and reports why.
390
+ */
391
+ private async contractChange(): Promise<string | null> {
392
+ const deployed = this.options.environment[FABRIKA_CONTRACT_DIGEST] ?? ''
393
+ if (deployed === '') return null
394
+ const started = performance.now()
395
+ const child = Bun.spawn(['bun', CONTRACT_DIGEST_CHILD], {
396
+ cwd: this.root,
397
+ env: { PATH: this.options.environment['PATH'] ?? '', HOME: this.options.environment['HOME'] ?? '' },
398
+ stdin: 'ignore',
399
+ stdout: 'pipe',
400
+ stderr: 'pipe',
401
+ })
402
+ const timer = setTimeout(() => child.kill('SIGKILL'), DIGEST_TIMEOUT_MS)
403
+ const [stdout, stderr] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited])
404
+ clearTimeout(timer)
405
+ const digest = stdout.trim()
406
+ if (child.exitCode !== 0 || !CONTRACT_DIGEST.test(digest)) {
407
+ for (const line of stderr.split('\n').filter((text) => text.trim() !== '').slice(-5)) this.ring.push('stderr', line)
408
+ this.say('the contract digest could not be computed; the application is respawned and reports why', 'stderr')
366
409
  return null
367
410
  }
411
+ this.say(`contract digest computed in ${Math.round(performance.now() - started)} ms`)
412
+ if (digest === deployed) return null
413
+ return `the change alters the contract (its digest is ${digest}, and the bootstrap run deployed ${deployed}): the proxy table and `
414
+ + 'the approval follow the contract, so that needs a bootstrap run: call bind_dev again'
368
415
  }
369
416
 
370
417
  private async persist(): Promise<void> {
@@ -421,7 +468,7 @@ class Launcher {
421
468
  output: result.lines.slice(-CHECK_OUTPUT_LINES).map((line) => line.slice(0, 2_000)),
422
469
  })
423
470
  }
424
- this.checkResults = { request, results }
471
+ this.checkResults = { request, head: this.state.head, results }
425
472
  this.state = { ...this.state, checks: request }
426
473
  this.status = before.status
427
474
  this.message = before.message
package/src/dev/wire.ts CHANGED
@@ -98,7 +98,8 @@ export const devReportSchema = z.object({
98
98
  /** The newest lines of the application's and the launcher's output, oldest first. */
99
99
  log: z.array(devLogLineSchema).max(DEV_REPORT_MAX_LINES),
100
100
  reset: generation.nullable(),
101
- checks: z.object({ request: generation, results: z.array(devCheckResultSchema).max(8) }).strict().nullable(),
101
+ /** The newest check run: the request it answers, the commit whose files it checked, and each check's result. */
102
+ checks: z.object({ request: generation, head: commitSha, results: z.array(devCheckResultSchema).max(8) }).strict().nullable(),
102
103
  }).strict()
103
104
 
104
105
  export type DevReport = z.infer<typeof devReportSchema>
@@ -0,0 +1,29 @@
1
+ export declare const APP_AGENT_SKILL_NAME = "fabrika-app";
2
+ export interface AppAgentSkillReference {
3
+ /** The file's path in the skill directory, `references/<topic>.md`: the path `SKILL.md` names it by. */
4
+ readonly path: string;
5
+ /** The topic `get_app_rules` answers it under: its file name without `.md`. */
6
+ readonly topic: string;
7
+ readonly text: string;
8
+ }
9
+ export interface AppAgentSkill {
10
+ /** The `version` of the frontmatter: the version of the `@fabrika/app` that ships it. */
11
+ readonly version: string;
12
+ /** The whole `SKILL.md`, frontmatter included: what a skill directory holds. */
13
+ readonly text: string;
14
+ /** `SKILL.md` without the frontmatter. */
15
+ readonly body: string;
16
+ /** The reference files, by path. */
17
+ readonly references: readonly AppAgentSkillReference[];
18
+ }
19
+ /** The skill of this package. */
20
+ export declare function readAppAgentSkill(): AppAgentSkill;
21
+ /** The skill in `skillDir`, the `skill/` directory of an installed `@fabrika/app`. */
22
+ export declare function readAppAgentSkillAt(skillDir: string): AppAgentSkill;
23
+ /**
24
+ * The skill of the `@fabrika/app` an application installed: the nearest `node_modules/@fabrika/app` from `appRoot`
25
+ * up, as Bun resolves the package. `null` when the application has not installed it.
26
+ */
27
+ export declare function installedAppAgentSkill(appRoot: string): AppAgentSkill | null;
28
+ /** The `AGENTS.md` of an application repository: the skill's body, then each reference file. */
29
+ export declare function renderAgentsFile(skill: AppAgentSkill): string;
@@ -0,0 +1,11 @@
1
+ /** An application root with this file is a contract application. The paths equal `@fabrika/check`'s layout. */
2
+ export declare const CONTRACT_FILE = "fabrika.contract.ts";
3
+ /** Relative to the application root. */
4
+ export declare const GENERATED_ENTRY_FILE = "fabrika/entry.generated.ts";
5
+ /** The module whose default export is `serve(contract, …)`, relative to the application root. */
6
+ export declare const IMPLEMENTATION_FILE = "src/app.ts";
7
+ export type EntryRuntime = 'bun' | 'cloudflare';
8
+ /** The entrypoint source for one runtime. */
9
+ export declare function generatedEntry(runtime: EntryRuntime): string;
10
+ /** Writes the entrypoint under `root` and returns its path. */
11
+ export declare function writeGeneratedEntry(root: string, runtime: EntryRuntime): Promise<string>;
@@ -0,0 +1,66 @@
1
+ import type { FileObjectStore } from '@fabrika/platform';
2
+ import type { FetchService, RequestExecutionContext } from './fetch.js';
3
+ import { type IamKeySource } from './implement/boot.js';
4
+ import { type CrashEvents } from './implement/crashes.js';
5
+ import { type ContractImplementation } from './implement/implement.js';
6
+ import { type JobRunOptions, type JobRunResult } from './jobs/runner.js';
7
+ export { diskAssets } from './disk-assets.js';
8
+ export { clamscanArguments, type ClamscanOptions, clamscanScanner } from './files/clamscan.js';
9
+ export { markProductionProcess } from '@fabrika/db/production';
10
+ export interface ManagedBunHandlerOptions {
11
+ /** Required sink for rejected background tasks. Do not log raw errors or secret-bearing values. */
12
+ onBackgroundError(error: unknown): void;
13
+ executionContext?: never;
14
+ }
15
+ export interface ExternalBunHandlerOptions {
16
+ /** Existing supervised process context. Its owner remains responsible for draining it. */
17
+ executionContext: RequestExecutionContext;
18
+ onBackgroundError?: never;
19
+ }
20
+ export type BunHandlerOptions = ManagedBunHandlerOptions | ExternalBunHandlerOptions;
21
+ export interface BunHandler {
22
+ fetch(request: Request): Promise<Response>;
23
+ /** Wait until every task registered through `waitUntil()` has settled. */
24
+ drain(): Promise<void>;
25
+ }
26
+ export declare function createBunHandler<Env>(app: FetchService<Env>, env: Env, options: BunHandlerOptions): BunHandler;
27
+ /** IAM's public key set at the issuer's `/.well-known/jwks.json`, for a process with no IAM binding. */
28
+ export declare function issuerJwks(issuer: unknown): IamKeySource;
29
+ export interface ContractServer {
30
+ /** The port the server listens on; `PORT=0` picks a free one. */
31
+ readonly port: number;
32
+ /** Stops accepting requests and waits for the ones in flight. */
33
+ stop(): Promise<void>;
34
+ }
35
+ /**
36
+ * The stored files' object storage (Zerops object storage, path-style), when the platform wrote its variables;
37
+ * a partial set stops the boot. The credentials are never quoted in a message.
38
+ */
39
+ export declare function openFileStorage(framework: Readonly<Record<string, string>>): FileObjectStore | undefined;
40
+ /** The application module, imported only after the environment was captured: the generated entrypoint's `import()`. */
41
+ export type ImplementationLoader = () => Promise<{
42
+ readonly default: ContractImplementation;
43
+ }>;
44
+ export interface ContractServerOptions {
45
+ /** Where the process hears of an uncaught exception or an unhandled rejection; `process` by default. */
46
+ readonly crashes?: CrashEvents;
47
+ /** What posts a report to Operations' ingest; `fetch` by default. */
48
+ readonly operationsFetch?: (request: Request) => Promise<Response>;
49
+ }
50
+ /**
51
+ * Captures the framework's variables and scrubs them from the process, then imports the application, boots it and
52
+ * serves it on `PORT` (default 3000), on every interface: the project's private network is how the proxy reaches
53
+ * it. Called only by the generated entrypoint, with `process.env`. SIGTERM drains. Given the provisioning credential,
54
+ * it is the launcher instead: it never loads the application and re-runs the entrypoint as the runtime role.
55
+ * With an Operations DSN, a startup that throws is reported before it is rethrown, and so is a crash once it serves.
56
+ */
57
+ export declare function serveContract(load: ImplementationLoader, env: Readonly<Record<string, string | undefined>>, options?: ContractServerOptions): Promise<ContractServer>;
58
+ /** The application module at `path`, imported when the loader runs; its default export must be what `serve` returned. */
59
+ export declare function implementationAt(path: string): ImplementationLoader;
60
+ /**
61
+ * One run of the job `name` (`fabrika job run <name>`, the crontab's command): the framework's variables are captured
62
+ * and scrubbed before the application loads, exactly as `serveContract` does, then `boot` binds the job and the run
63
+ * takes its lease. The connection closes when the run ends. Given the provisioning credential, it re-runs the command
64
+ * as the runtime role, as `serveContract` does.
65
+ */
66
+ export declare function runContractJob(load: ImplementationLoader, env: Readonly<Record<string, string | undefined>>, name: string, options?: JobRunOptions): Promise<JobRunResult>;
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod/v4';
2
+ declare const PageCallerSchema: z.ZodObject<{
3
+ label: z.ZodString;
4
+ email: z.ZodString;
5
+ actions: z.ZodObject<{
6
+ /** The actions held as a global grant, so in every value. */
7
+ app: z.ZodArray<z.ZodString>;
8
+ /** Each scope value the person holds something in, with those actions; sorted by dimension, then value. */
9
+ scopes: z.ZodArray<z.ZodObject<{
10
+ type: z.ZodString;
11
+ value: z.ZodString;
12
+ actions: z.ZodArray<z.ZodString>;
13
+ }, z.core.$strict>>;
14
+ }, z.core.$strict>;
15
+ }, z.core.$strict>;
16
+ export type PageCaller = z.infer<typeof PageCallerSchema>;
17
+ /** The caller document as the framework answers it; throws on any other value. */
18
+ export declare function parsePageCaller(value: unknown): PageCaller;
19
+ export {};