@ketvietlab/ketjs 0.1.2 → 0.1.3

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 (299) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -2
  3. package/dist/agent/capabilities.d.ts +1 -3
  4. package/dist/agent/capabilities.d.ts.map +1 -1
  5. package/dist/agent/capabilities.js +10 -1
  6. package/dist/agent/capabilities.js.map +1 -1
  7. package/dist/agent/permissions.d.ts +43 -1
  8. package/dist/agent/permissions.d.ts.map +1 -1
  9. package/dist/agent/permissions.js +50 -2
  10. package/dist/agent/permissions.js.map +1 -1
  11. package/dist/cli.js +246 -97
  12. package/dist/cli.js.map +1 -1
  13. package/dist/codegen/dts.d.ts.map +1 -1
  14. package/dist/codegen/dts.js +1 -0
  15. package/dist/codegen/dts.js.map +1 -1
  16. package/dist/data/changeset.d.ts +12 -0
  17. package/dist/data/changeset.d.ts.map +1 -1
  18. package/dist/data/changeset.js +73 -17
  19. package/dist/data/changeset.js.map +1 -1
  20. package/dist/data/expr.d.ts +16 -0
  21. package/dist/data/expr.d.ts.map +1 -1
  22. package/dist/data/expr.js +33 -5
  23. package/dist/data/expr.js.map +1 -1
  24. package/dist/data/fleet.d.ts +41 -1
  25. package/dist/data/fleet.d.ts.map +1 -1
  26. package/dist/data/fleet.js +100 -11
  27. package/dist/data/fleet.js.map +1 -1
  28. package/dist/data/migrate.d.ts +20 -0
  29. package/dist/data/migrate.d.ts.map +1 -1
  30. package/dist/data/migrate.js +86 -3
  31. package/dist/data/migrate.js.map +1 -1
  32. package/dist/data/physical.d.ts +5 -0
  33. package/dist/data/physical.d.ts.map +1 -0
  34. package/dist/data/physical.js +196 -0
  35. package/dist/data/physical.js.map +1 -0
  36. package/dist/data/pool.d.ts +2 -1
  37. package/dist/data/pool.d.ts.map +1 -1
  38. package/dist/data/pool.js +159 -31
  39. package/dist/data/pool.js.map +1 -1
  40. package/dist/data/query.d.ts +10 -1
  41. package/dist/data/query.d.ts.map +1 -1
  42. package/dist/data/query.js +136 -27
  43. package/dist/data/query.js.map +1 -1
  44. package/dist/data/sqlite.d.ts.map +1 -1
  45. package/dist/data/sqlite.js +350 -52
  46. package/dist/data/sqlite.js.map +1 -1
  47. package/dist/data/time.d.ts +13 -0
  48. package/dist/data/time.d.ts.map +1 -1
  49. package/dist/data/time.js +61 -17
  50. package/dist/data/time.js.map +1 -1
  51. package/dist/index.d.ts +43 -22
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +32 -16
  54. package/dist/index.js.map +1 -1
  55. package/dist/kernel/audit.d.ts +27 -0
  56. package/dist/kernel/audit.d.ts.map +1 -0
  57. package/dist/kernel/audit.js +67 -0
  58. package/dist/kernel/audit.js.map +1 -0
  59. package/dist/kernel/classification.d.ts +26 -0
  60. package/dist/kernel/classification.d.ts.map +1 -0
  61. package/dist/kernel/classification.js +73 -0
  62. package/dist/kernel/classification.js.map +1 -0
  63. package/dist/kernel/compose.d.ts +6 -2
  64. package/dist/kernel/compose.d.ts.map +1 -1
  65. package/dist/kernel/compose.js +269 -15
  66. package/dist/kernel/compose.js.map +1 -1
  67. package/dist/kernel/define.d.ts +2 -3
  68. package/dist/kernel/define.d.ts.map +1 -1
  69. package/dist/kernel/define.js +22 -12
  70. package/dist/kernel/define.js.map +1 -1
  71. package/dist/kernel/diff.d.ts.map +1 -1
  72. package/dist/kernel/diff.js +28 -4
  73. package/dist/kernel/diff.js.map +1 -1
  74. package/dist/kernel/graph.js +1 -1
  75. package/dist/kernel/graph.js.map +1 -1
  76. package/dist/kernel/i18n.d.ts +10 -1
  77. package/dist/kernel/i18n.d.ts.map +1 -1
  78. package/dist/kernel/i18n.js +34 -1
  79. package/dist/kernel/i18n.js.map +1 -1
  80. package/dist/kernel/layout.d.ts +97 -0
  81. package/dist/kernel/layout.d.ts.map +1 -1
  82. package/dist/kernel/layout.js +294 -12
  83. package/dist/kernel/layout.js.map +1 -1
  84. package/dist/kernel/menu.d.ts +33 -2
  85. package/dist/kernel/menu.d.ts.map +1 -1
  86. package/dist/kernel/menu.js +99 -7
  87. package/dist/kernel/menu.js.map +1 -1
  88. package/dist/kernel/modules.d.ts +3 -3
  89. package/dist/kernel/modules.d.ts.map +1 -1
  90. package/dist/kernel/modules.js +21 -21
  91. package/dist/kernel/modules.js.map +1 -1
  92. package/dist/kernel/permissions.d.ts +10 -0
  93. package/dist/kernel/permissions.d.ts.map +1 -0
  94. package/dist/kernel/permissions.js +365 -0
  95. package/dist/kernel/permissions.js.map +1 -0
  96. package/dist/kernel/schedule.d.ts +23 -0
  97. package/dist/kernel/schedule.d.ts.map +1 -0
  98. package/dist/kernel/schedule.js +83 -0
  99. package/dist/kernel/schedule.js.map +1 -0
  100. package/dist/kernel/types.d.ts.map +1 -1
  101. package/dist/kernel/types.js +5 -4
  102. package/dist/kernel/types.js.map +1 -1
  103. package/dist/kernel/workspace.d.ts +92 -14
  104. package/dist/kernel/workspace.d.ts.map +1 -1
  105. package/dist/kernel/workspace.js +128 -49
  106. package/dist/kernel/workspace.js.map +1 -1
  107. package/dist/pdf/assets/Inter-Bold.ttf +0 -0
  108. package/dist/pdf/assets/Inter-Regular.ttf +0 -0
  109. package/dist/pdf/assets/Inter-SemiBold.ttf +0 -0
  110. package/dist/pdf/assets/LICENSE.txt +92 -0
  111. package/dist/pdf/font.d.ts +10 -0
  112. package/dist/pdf/font.d.ts.map +1 -0
  113. package/dist/pdf/font.js +83 -0
  114. package/dist/pdf/font.js.map +1 -0
  115. package/dist/pdf/image.d.ts +8 -0
  116. package/dist/pdf/image.d.ts.map +1 -0
  117. package/dist/pdf/image.js +94 -0
  118. package/dist/pdf/image.js.map +1 -0
  119. package/dist/pdf/index.d.ts +15 -0
  120. package/dist/pdf/index.d.ts.map +1 -0
  121. package/dist/pdf/index.js +17 -0
  122. package/dist/pdf/index.js.map +1 -0
  123. package/dist/pdf/markup.d.ts +19 -0
  124. package/dist/pdf/markup.d.ts.map +1 -0
  125. package/dist/pdf/markup.js +146 -0
  126. package/dist/pdf/markup.js.map +1 -0
  127. package/dist/pdf/render.d.ts +14 -0
  128. package/dist/pdf/render.d.ts.map +1 -0
  129. package/dist/pdf/render.js +387 -0
  130. package/dist/pdf/render.js.map +1 -0
  131. package/dist/scaffold/index.js +6 -6
  132. package/dist/scaffold/index.js.map +1 -1
  133. package/dist/scaffold/templates/{app.test.ts.tmpl → deployment.test.ts.tmpl} +3 -3
  134. package/dist/scaffold/templates/ket.workspace.ts.tmpl +4 -6
  135. package/dist/scaffold/templates/module.ts.tmpl +1 -4
  136. package/dist/server/assets.d.ts +23 -0
  137. package/dist/server/assets.d.ts.map +1 -0
  138. package/dist/server/assets.js +97 -0
  139. package/dist/server/assets.js.map +1 -0
  140. package/dist/server/boot.d.ts +157 -38
  141. package/dist/server/boot.d.ts.map +1 -1
  142. package/dist/server/boot.js +360 -138
  143. package/dist/server/boot.js.map +1 -1
  144. package/dist/server/config.d.ts +30 -10
  145. package/dist/server/config.d.ts.map +1 -1
  146. package/dist/server/config.js +73 -17
  147. package/dist/server/config.js.map +1 -1
  148. package/dist/server/ctx.d.ts +10 -0
  149. package/dist/server/ctx.d.ts.map +1 -1
  150. package/dist/server/ctx.js +266 -48
  151. package/dist/server/ctx.js.map +1 -1
  152. package/dist/server/development.d.ts +10 -0
  153. package/dist/server/development.d.ts.map +1 -0
  154. package/dist/server/development.js +15 -0
  155. package/dist/server/development.js.map +1 -0
  156. package/dist/server/fn.d.ts +22 -3
  157. package/dist/server/fn.d.ts.map +1 -1
  158. package/dist/server/fn.js +116 -32
  159. package/dist/server/fn.js.map +1 -1
  160. package/dist/server/form.d.ts +33 -0
  161. package/dist/server/form.d.ts.map +1 -0
  162. package/dist/server/form.js +47 -0
  163. package/dist/server/form.js.map +1 -0
  164. package/dist/server/http.d.ts +55 -3
  165. package/dist/server/http.d.ts.map +1 -1
  166. package/dist/server/http.js +306 -22
  167. package/dist/server/http.js.map +1 -1
  168. package/dist/server/idem.d.ts +3 -2
  169. package/dist/server/idem.d.ts.map +1 -1
  170. package/dist/server/idem.js +12 -4
  171. package/dist/server/idem.js.map +1 -1
  172. package/dist/server/jobs.js +1 -1
  173. package/dist/server/jobs.js.map +1 -1
  174. package/dist/server/log/combinators.d.ts +42 -0
  175. package/dist/server/log/combinators.d.ts.map +1 -0
  176. package/dist/server/log/combinators.js +270 -0
  177. package/dist/server/log/combinators.js.map +1 -0
  178. package/dist/server/log/console.d.ts +12 -0
  179. package/dist/server/log/console.d.ts.map +1 -0
  180. package/dist/server/log/console.js +96 -0
  181. package/dist/server/log/console.js.map +1 -0
  182. package/dist/server/log/file.d.ts +11 -0
  183. package/dist/server/log/file.d.ts.map +1 -0
  184. package/dist/server/log/file.js +61 -0
  185. package/dist/server/log/file.js.map +1 -0
  186. package/dist/server/log/index.d.ts +23 -0
  187. package/dist/server/log/index.d.ts.map +1 -0
  188. package/dist/server/log/index.js +45 -0
  189. package/dist/server/log/index.js.map +1 -0
  190. package/dist/server/log/logger.d.ts +53 -0
  191. package/dist/server/log/logger.d.ts.map +1 -0
  192. package/dist/server/log/logger.js +94 -0
  193. package/dist/server/log/logger.js.map +1 -0
  194. package/dist/server/log/memory.d.ts +11 -0
  195. package/dist/server/log/memory.d.ts.map +1 -0
  196. package/dist/server/log/memory.js +25 -0
  197. package/dist/server/log/memory.js.map +1 -0
  198. package/dist/server/log/types.d.ts +112 -0
  199. package/dist/server/log/types.d.ts.map +1 -0
  200. package/dist/server/log/types.js +58 -0
  201. package/dist/server/log/types.js.map +1 -0
  202. package/dist/server/policy.d.ts +29 -0
  203. package/dist/server/policy.d.ts.map +1 -0
  204. package/dist/server/policy.js +29 -0
  205. package/dist/server/policy.js.map +1 -0
  206. package/dist/server/queue.d.ts +1 -1
  207. package/dist/server/queue.d.ts.map +1 -1
  208. package/dist/server/queue.js.map +1 -1
  209. package/dist/server/ratelimit.d.ts +48 -0
  210. package/dist/server/ratelimit.d.ts.map +1 -0
  211. package/dist/server/ratelimit.js +102 -0
  212. package/dist/server/ratelimit.js.map +1 -0
  213. package/dist/server/respond.d.ts.map +1 -1
  214. package/dist/server/respond.js +16 -1
  215. package/dist/server/respond.js.map +1 -1
  216. package/dist/server/runtime.d.ts +18 -3
  217. package/dist/server/runtime.d.ts.map +1 -1
  218. package/dist/server/runtime.js +32 -2
  219. package/dist/server/runtime.js.map +1 -1
  220. package/dist/server/schedule.d.ts +18 -0
  221. package/dist/server/schedule.d.ts.map +1 -0
  222. package/dist/server/schedule.js +70 -0
  223. package/dist/server/schedule.js.map +1 -0
  224. package/dist/server/sequence.d.ts +42 -0
  225. package/dist/server/sequence.d.ts.map +1 -0
  226. package/dist/server/sequence.js +97 -0
  227. package/dist/server/sequence.js.map +1 -0
  228. package/dist/server/session.d.ts +21 -0
  229. package/dist/server/session.d.ts.map +1 -1
  230. package/dist/server/session.js +84 -10
  231. package/dist/server/session.js.map +1 -1
  232. package/dist/server/sessionstore.d.ts +3 -1
  233. package/dist/server/sessionstore.d.ts.map +1 -1
  234. package/dist/server/sessionstore.js +7 -3
  235. package/dist/server/sessionstore.js.map +1 -1
  236. package/dist/server/storage/index.d.ts +1 -1
  237. package/dist/server/storage/index.d.ts.map +1 -1
  238. package/dist/server/storage/index.js +61 -1
  239. package/dist/server/storage/index.js.map +1 -1
  240. package/dist/server/storage/types.d.ts +8 -0
  241. package/dist/server/storage/types.d.ts.map +1 -1
  242. package/dist/server/storage/types.js +58 -0
  243. package/dist/server/storage/types.js.map +1 -1
  244. package/dist/server/tenants.d.ts +20 -18
  245. package/dist/server/tenants.d.ts.map +1 -1
  246. package/dist/server/tenants.js +133 -84
  247. package/dist/server/tenants.js.map +1 -1
  248. package/dist/server/transport/types.js +2 -2
  249. package/dist/server/transport/types.js.map +1 -1
  250. package/dist/server/worker.d.ts +19 -3
  251. package/dist/server/worker.d.ts.map +1 -1
  252. package/dist/server/worker.js +168 -21
  253. package/dist/server/worker.js.map +1 -1
  254. package/dist/testing.d.ts +26 -10
  255. package/dist/testing.d.ts.map +1 -1
  256. package/dist/testing.js +41 -14
  257. package/dist/testing.js.map +1 -1
  258. package/dist/theme/contracts.d.ts +14 -1
  259. package/dist/theme/contracts.d.ts.map +1 -1
  260. package/dist/theme/contracts.js +35 -6
  261. package/dist/theme/contracts.js.map +1 -1
  262. package/dist/theme/index.d.ts +1 -1
  263. package/dist/theme/index.d.ts.map +1 -1
  264. package/dist/theme/index.js +1 -1
  265. package/dist/theme/index.js.map +1 -1
  266. package/dist/theme/joint-runtime.d.ts +33 -0
  267. package/dist/theme/joint-runtime.d.ts.map +1 -0
  268. package/dist/theme/joint-runtime.js +121 -0
  269. package/dist/theme/joint-runtime.js.map +1 -0
  270. package/dist/theme/joints.d.ts.map +1 -1
  271. package/dist/theme/joints.js +15 -97
  272. package/dist/theme/joints.js.map +1 -1
  273. package/dist/theme/ktl/compile.d.ts +4 -0
  274. package/dist/theme/ktl/compile.d.ts.map +1 -1
  275. package/dist/theme/ktl/compile.js +54 -1
  276. package/dist/theme/ktl/compile.js.map +1 -1
  277. package/dist/theme/ktl/parser.d.ts +4 -0
  278. package/dist/theme/ktl/parser.d.ts.map +1 -1
  279. package/dist/theme/ktl/parser.js +8 -0
  280. package/dist/theme/ktl/parser.js.map +1 -1
  281. package/dist/theme/render.d.ts +8 -0
  282. package/dist/theme/render.d.ts.map +1 -1
  283. package/dist/theme/render.js +88 -118
  284. package/dist/theme/render.js.map +1 -1
  285. package/dist/theme/tokens.d.ts +6 -0
  286. package/dist/theme/tokens.d.ts.map +1 -1
  287. package/dist/theme/tokens.js +6 -0
  288. package/dist/theme/tokens.js.map +1 -1
  289. package/dist/theme/viewmodel.d.ts +24 -0
  290. package/dist/theme/viewmodel.d.ts.map +1 -1
  291. package/dist/theme/viewmodel.js +75 -6
  292. package/dist/theme/viewmodel.js.map +1 -1
  293. package/dist/types.d.ts +304 -56
  294. package/dist/types.d.ts.map +1 -1
  295. package/package.json +6 -2
  296. package/dist/kernel/apps.d.ts +0 -43
  297. package/dist/kernel/apps.d.ts.map +0 -1
  298. package/dist/kernel/apps.js +0 -234
  299. package/dist/kernel/apps.js.map +0 -1
@@ -0,0 +1,73 @@
1
+ // What the deployment's data actually is, read off the manifest.
2
+ //
3
+ // An obligation to export or erase somebody's data cannot be met against a schema
4
+ // nobody has classified, and neither can a question as simple as "which columns
5
+ // would leak if this table went out the door". Both are answered here, from the
6
+ // same composed manifest the migration and the agent descriptor read, so the answer
7
+ // cannot drift from what the deployment actually runs.
8
+ //
9
+ // The inventory deliberately reports what is *not* classified as well. An inventory
10
+ // of only the fields somebody remembered to tag is the one thing worse than no
11
+ // inventory: it looks complete.
12
+ const typeOf = (field) => `${field.base === 'ref' ? `ref:${field.target}` : field.base}${field.optional ? '?' : ''}`;
13
+ export function classificationInventory(manifest) {
14
+ const personal = [];
15
+ const sensitive = [];
16
+ const unclassified = [];
17
+ let fields = 0;
18
+ for (const [model, def] of Object.entries(manifest.models).sort(([a], [b]) => a.localeCompare(b))) {
19
+ let classified = false;
20
+ for (const [field, spec] of Object.entries(def.fields).sort(([a], [b]) => a.localeCompare(b))) {
21
+ fields += 1;
22
+ if (!spec.personal && !spec.sensitive)
23
+ continue;
24
+ classified = true;
25
+ const entry = {
26
+ model,
27
+ field,
28
+ by: spec.by,
29
+ type: typeOf(spec),
30
+ personal: Boolean(spec.personal),
31
+ sensitive: Boolean(spec.sensitive),
32
+ };
33
+ if (entry.personal)
34
+ personal.push(entry);
35
+ if (entry.sensitive)
36
+ sensitive.push(entry);
37
+ }
38
+ if (!classified)
39
+ unclassified.push(model);
40
+ }
41
+ return {
42
+ personal,
43
+ sensitive,
44
+ unclassified,
45
+ counts: {
46
+ models: Object.keys(manifest.models).length,
47
+ fields,
48
+ personal: personal.length,
49
+ sensitive: sensitive.length,
50
+ unclassified: unclassified.length,
51
+ },
52
+ };
53
+ }
54
+ const row = (entry, width) => ` ${`${entry.model}.${entry.field}`.padEnd(width)} ${entry.type.padEnd(18)} by ${entry.by}`;
55
+ export function formatClassification(inventory) {
56
+ const all = [...inventory.personal, ...inventory.sensitive];
57
+ const width = Math.max(24, ...all.map((entry) => `${entry.model}.${entry.field}`.length));
58
+ const lines = [];
59
+ lines.push(`personal data (${inventory.counts.personal} field(s))`);
60
+ lines.push(...(inventory.personal.length ? inventory.personal.map((e) => row(e, width)) : [' none']));
61
+ lines.push('');
62
+ lines.push(`sensitive (${inventory.counts.sensitive} field(s)) — never leaves the system`);
63
+ lines.push(...(inventory.sensitive.length ? inventory.sensitive.map((e) => row(e, width)) : [' none']));
64
+ lines.push('');
65
+ lines.push(`${inventory.counts.unclassified} of ${inventory.counts.models} model(s) classify no field at all`);
66
+ // Named rather than counted, because the list is the work queue.
67
+ if (inventory.unclassified.length) {
68
+ for (const model of inventory.unclassified)
69
+ lines.push(` ${model}`);
70
+ }
71
+ return lines.join('\n');
72
+ }
73
+ //# sourceMappingURL=classification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classification.js","sourceRoot":"","sources":["../../../../../../packages/ketjs/src/kernel/classification.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,10 @@
1
- import type { KetModule, Manifest } from '../types.ts';
1
+ import type { KetModule, Manifest, ModulePermissionsDef } from '../types.ts';
2
+ import type { RoleTemplateDef } from '../types.ts';
2
3
  export declare function compose(modules: KetModule[], opts?: {
3
- appRequires?: string[];
4
+ requiredRegions?: string[];
4
5
  headless?: boolean;
6
+ requirePermissionCoverage?: boolean;
7
+ roleTemplates?: Record<string, RoleTemplateDef>;
8
+ modulePermissionDeclarations?: Record<string, ModulePermissionsDef>;
5
9
  }): Manifest;
6
10
  //# sourceMappingURL=compose.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../../../../packages/ketjs/src/kernel/compose.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAiB,MAAM,aAAa,CAAA;AAOrE,wBAAgB,OAAO,CACrB,OAAO,EAAE,SAAS,EAAE,EACpB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GACxD,QAAQ,CA68BV"}
1
+ {"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../../../../packages/ketjs/src/kernel/compose.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAiB,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAK3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAiDlD,wBAAgB,OAAO,CACrB,OAAO,EAAE,SAAS,EAAE,EACpB,IAAI,GAAE;IACJ,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC/C,4BAA4B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;CAC/D,GACL,QAAQ,CAsqCV"}
@@ -7,7 +7,48 @@
7
7
  import { topoSort } from './graph.js';
8
8
  import { Diagnostics } from './errors.js';
9
9
  import { parseType } from './types.js';
10
+ import { validateSchedule } from './schedule.js';
10
11
  import { ambiguousRoutes, parseRoutePattern } from './routes.js';
12
+ import { tableNameFor } from '../data/migrate.js';
13
+ import { compilePermissionBundles } from './permissions.js';
14
+ const FIELD_KEYS = new Set(['type', 'personal', 'sensitive']);
15
+ /**
16
+ * Accept both field forms.
17
+ *
18
+ * The vocabulary is closed: a key nobody knows is a build error rather than a flag
19
+ * that silently does nothing, which is how a field ends up believed to be protected
20
+ * when it is not.
21
+ */
22
+ function normalizeField(declared) {
23
+ if (typeof declared === 'string') {
24
+ return { ok: true, type: declared, personal: false, sensitive: false };
25
+ }
26
+ if (declared === null || typeof declared !== 'object') {
27
+ return { ok: false, code: 'E_BAD_FIELD', reason: 'a field is a type string or an object with a type' };
28
+ }
29
+ const unknown = Object.keys(declared).filter((k) => !FIELD_KEYS.has(k));
30
+ if (unknown.length) {
31
+ return {
32
+ ok: false,
33
+ code: 'E_BAD_FIELD',
34
+ reason: `unknown field key(s) ${unknown.join(', ')} (accepted: ${[...FIELD_KEYS].join(', ')})`,
35
+ };
36
+ }
37
+ if (typeof declared.type !== 'string') {
38
+ return { ok: false, code: 'E_BAD_FIELD', reason: 'a field object needs a type string' };
39
+ }
40
+ for (const flag of ['personal', 'sensitive']) {
41
+ if (declared[flag] !== undefined && typeof declared[flag] !== 'boolean') {
42
+ return { ok: false, code: 'E_BAD_FIELD', reason: `"${flag}" must be a boolean` };
43
+ }
44
+ }
45
+ return {
46
+ ok: true,
47
+ type: declared.type,
48
+ personal: declared.personal === true,
49
+ sensitive: declared.sensitive === true,
50
+ };
51
+ }
11
52
  const qualify = (mod, name) => `${mod}.${name}`;
12
53
  const jointKey = (mod, name) => `${mod}:${name}`;
13
54
  export function compose(modules, opts = {}) {
@@ -23,9 +64,20 @@ export function compose(modules, opts = {}) {
23
64
  joints: {},
24
65
  fills: [],
25
66
  functions: {},
67
+ permissions: {
68
+ version: 1,
69
+ digest: '',
70
+ coverageRequired: false,
71
+ modules: {},
72
+ bundles: {},
73
+ functions: {},
74
+ exemptions: {},
75
+ roleTemplates: {},
76
+ },
26
77
  jobs: {},
27
78
  views: {},
28
- regions: { required: [...(opts.appRequires ?? [])], provided: {} },
79
+ reports: {},
80
+ regions: { required: [...(opts.requiredRegions ?? [])], provided: {} },
29
81
  islands: {},
30
82
  sections: {},
31
83
  contentTypes: {},
@@ -35,27 +87,81 @@ export function compose(modules, opts = {}) {
35
87
  assets: {},
36
88
  styles: [],
37
89
  routes: {},
90
+ routePrefixes: {},
38
91
  patches: [],
39
92
  messages: {},
40
93
  };
94
+ for (const m of order) {
95
+ for (const raw of m.reserves ?? []) {
96
+ const prefix = raw.trim();
97
+ if (!prefix.startsWith('/') || !prefix.endsWith('/') || prefix.includes('{')) {
98
+ diag.add({
99
+ code: 'E_ROUTE_PREFIX',
100
+ module: m.name,
101
+ message: `"${m.name}" reserves invalid route prefix "${raw}"`,
102
+ hint: 'a reserved prefix is an absolute static path ending in /',
103
+ });
104
+ continue;
105
+ }
106
+ const conflict = Object.entries(manifest.routePrefixes).find(([other]) => prefix.startsWith(other) || other.startsWith(prefix));
107
+ if (conflict) {
108
+ diag.add({
109
+ code: 'E_ROUTE_PREFIX_CLASH',
110
+ module: m.name,
111
+ message: `"${m.name}" and "${conflict[1]}" reserve overlapping prefixes "${prefix}" and "${conflict[0]}"`,
112
+ hint: 'one module must own the whole public namespace',
113
+ });
114
+ continue;
115
+ }
116
+ manifest.routePrefixes[prefix] = m.name;
117
+ }
118
+ }
119
+ const major = (version) => {
120
+ const match = /^(?:\^)?(\d+)(?:\.|$)/.exec(version.trim());
121
+ return match ? Number(match[1]) : null;
122
+ };
123
+ for (const m of order) {
124
+ for (const [dependency, range] of Object.entries(m.compatible ?? {})) {
125
+ if (!m.depends.includes(dependency)) {
126
+ diag.add({
127
+ code: 'E_MODULE_COMPATIBILITY_DEPENDENCY',
128
+ module: m.name,
129
+ message: `"${m.name}" declares compatibility with "${dependency}" without depending on it`,
130
+ hint: `add "${dependency}" to depends`,
131
+ });
132
+ continue;
133
+ }
134
+ const actual = order.find((candidate) => candidate.name === dependency)?.version;
135
+ const wantedMajor = major(range);
136
+ const actualMajor = actual ? major(actual) : null;
137
+ const matches = range.startsWith('^')
138
+ ? wantedMajor != null && actualMajor === wantedMajor
139
+ : actual === range;
140
+ if (!matches) {
141
+ diag.add({
142
+ code: 'E_MODULE_VERSION_SKEW',
143
+ module: m.name,
144
+ message: `"${m.name}" requires "${dependency}" ${range}, but the deployment has ${actual ?? 'nothing'}`,
145
+ hint: 'upgrade the extension and its contract owner together',
146
+ });
147
+ }
148
+ }
149
+ }
41
150
  for (const m of order) {
42
151
  manifest.modules[m.name] = {
43
152
  version: m.version,
44
153
  kind: m.kind,
45
154
  depends: [...m.depends],
46
- app: m.app,
47
155
  title: m.title,
48
156
  summary: m.summary,
49
157
  category: m.category,
50
- install: m.install ?? 'manual',
51
- removable: m.removable !== false,
52
158
  };
53
159
  }
54
160
  // --- the served surface -------------------------------------------------
55
161
  //
56
162
  // Assets, stylesheets and routes are composed for the same reason models are:
57
- // otherwise the app hand-assembles them, which means knowing another module's
58
- // file layout and going on serving it after that module is switched off.
163
+ // otherwise the deployment hand-assembles them, which means knowing another module's
164
+ // file layout and going on serving it after that module leaves the composition.
59
165
  //
60
166
  // `order` is dependency order, so a module that extends another contributes its
61
167
  // stylesheet after it and can override it. That ordering is the point.
@@ -93,6 +199,21 @@ export function compose(modules, opts = {}) {
93
199
  });
94
200
  continue;
95
201
  }
202
+ const reservation = Object.entries(manifest.routePrefixes).find(([prefix]) => path.startsWith(prefix));
203
+ if (reservation) {
204
+ const [prefix, owner] = reservation;
205
+ const contribution = typeof make === 'function' ? null : make.through;
206
+ const allowed = m.name === owner || (contribution === owner && m.depends.includes(owner));
207
+ if (!allowed) {
208
+ diag.add({
209
+ code: 'E_ROUTE_RESERVED',
210
+ module: m.name,
211
+ message: `"${m.name}" claims "${path}", inside the prefix reserved by "${owner}"`,
212
+ hint: `depend on "${owner}" and use its published route factory for "${prefix}"`,
213
+ });
214
+ continue;
215
+ }
216
+ }
96
217
  const taken = manifest.routes[path];
97
218
  if (taken) {
98
219
  diag.add({
@@ -117,13 +238,33 @@ export function compose(modules, opts = {}) {
117
238
  manifest.routes[path] =
118
239
  typeof make === 'function'
119
240
  ? { by: m.name, anonymous: false, make }
120
- : { by: m.name, anonymous: make.anonymous === true, make: make.handler };
241
+ : {
242
+ by: m.name,
243
+ anonymous: make.anonymous === true,
244
+ ...(make.through ? { through: make.through } : {}),
245
+ ...(make.contract ? { contract: make.contract } : {}),
246
+ make: make.handler,
247
+ };
121
248
  }
122
249
  }
123
250
  // --- models -------------------------------------------------------------
251
+ const physicalTables = new Map();
124
252
  for (const m of order) {
125
253
  for (const [modelName, def] of Object.entries(m.models)) {
126
254
  const key = qualify(m.name, modelName);
255
+ const physicalTable = tableNameFor(key);
256
+ const existingPhysicalModel = physicalTables.get(physicalTable);
257
+ if (existingPhysicalModel && existingPhysicalModel !== key) {
258
+ diag.add({
259
+ code: 'E_TABLE_NAME_COLLISION',
260
+ module: m.name,
261
+ message: `models "${existingPhysicalModel}" and "${key}" both map to table "${physicalTable}"`,
262
+ hint: 'rename one model or module so every composed model has a unique physical table',
263
+ });
264
+ }
265
+ else {
266
+ physicalTables.set(physicalTable, key);
267
+ }
127
268
  if (manifest.models[key]) {
128
269
  diag.add({
129
270
  code: 'E_MODEL_DUPLICATE',
@@ -162,7 +303,7 @@ export function compose(modules, opts = {}) {
162
303
  fields['createdAt'] = { base: 'datetime', optional: true, by: '(timestamps)' };
163
304
  fields['updatedAt'] = { base: 'datetime', optional: true, by: '(timestamps)' };
164
305
  }
165
- for (const [fname, tspec] of Object.entries(def.fields ?? {})) {
306
+ for (const [fname, declared] of Object.entries(def.fields ?? {})) {
166
307
  if (def.timestamps && (fname === 'createdAt' || fname === 'updatedAt')) {
167
308
  diag.add({
168
309
  code: 'E_TIMESTAMP_FIELD_RESERVED',
@@ -172,6 +313,14 @@ export function compose(modules, opts = {}) {
172
313
  });
173
314
  continue;
174
315
  }
316
+ // A field is a type string, or an object when it has more to say. The two
317
+ // forms compose to the same thing, so nothing downstream learns the difference.
318
+ const spec = normalizeField(declared);
319
+ if (!spec.ok) {
320
+ diag.add({ code: spec.code, module: m.name, message: `${key}.${fname}: ${spec.reason}` });
321
+ continue;
322
+ }
323
+ const tspec = spec.type;
175
324
  const t = parseType(tspec);
176
325
  if (!t.ok) {
177
326
  diag.add({ code: 'E_BAD_TYPE', module: m.name, message: `${key}.${fname}: ${t.reason}` });
@@ -186,7 +335,14 @@ export function compose(modules, opts = {}) {
186
335
  });
187
336
  continue;
188
337
  }
189
- fields[fname] = { base: t.base, optional: t.optional, target: t.target, by: m.name };
338
+ fields[fname] = {
339
+ base: t.base,
340
+ optional: t.optional,
341
+ target: t.target,
342
+ by: m.name,
343
+ ...(spec.personal ? { personal: true } : {}),
344
+ ...(spec.sensitive ? { sensitive: true } : {}),
345
+ };
190
346
  }
191
347
  const indexes = {};
192
348
  for (const [indexName, index] of Object.entries(def.indexes ?? {})) {
@@ -225,6 +381,7 @@ export function compose(modules, opts = {}) {
225
381
  owner: m.name,
226
382
  scope: def.scope,
227
383
  timestamps: def.timestamps === true,
384
+ append: def.append === true,
228
385
  fields,
229
386
  indexes,
230
387
  };
@@ -252,7 +409,7 @@ export function compose(modules, opts = {}) {
252
409
  });
253
410
  continue;
254
411
  }
255
- for (const [fname, tspec] of Object.entries(addl)) {
412
+ for (const [fname, declared] of Object.entries(addl)) {
256
413
  // Checked ahead of the collision below so the message names the real cause:
257
414
  // these columns are the isolation boundary, not a name somebody took first.
258
415
  if (fname === 'companyId' || fname === 'branchId') {
@@ -274,6 +431,15 @@ export function compose(modules, opts = {}) {
274
431
  });
275
432
  continue;
276
433
  }
434
+ // Extension takes both field forms too. A bridge is exactly the module that
435
+ // adds a phone number to somebody else's partner, so it is exactly the module
436
+ // that has to be able to say the column is personal data.
437
+ const spec = normalizeField(declared);
438
+ if (!spec.ok) {
439
+ diag.add({ code: spec.code, module: m.name, message: `${target}.${fname}: ${spec.reason}` });
440
+ continue;
441
+ }
442
+ const tspec = spec.type;
277
443
  const t = parseType(tspec);
278
444
  if (!t.ok) {
279
445
  diag.add({ code: 'E_BAD_TYPE', module: m.name, message: `${target}.${fname}: ${t.reason}` });
@@ -290,7 +456,14 @@ export function compose(modules, opts = {}) {
290
456
  });
291
457
  continue;
292
458
  }
293
- model.fields[fname] = { base: t.base, optional: t.optional, target: t.target, by: m.name };
459
+ model.fields[fname] = {
460
+ base: t.base,
461
+ optional: t.optional,
462
+ target: t.target,
463
+ by: m.name,
464
+ ...(spec.personal ? { personal: true } : {}),
465
+ ...(spec.sensitive ? { sensitive: true } : {}),
466
+ };
294
467
  }
295
468
  }
296
469
  }
@@ -436,7 +609,7 @@ export function compose(modules, opts = {}) {
436
609
  diag.add({
437
610
  code: 'E_FILL_UNKNOWN_JOINT',
438
611
  module: m.name,
439
- message: `fills joint "${key}", which no installed module publishes`,
612
+ message: `fills joint "${key}", which no composed module publishes`,
440
613
  hint: near.length
441
614
  ? `did you mean "${near[0]}"?`
442
615
  : `published joints: ${Object.keys(manifest.joints).join(', ') || '(none)'}`,
@@ -477,7 +650,7 @@ export function compose(modules, opts = {}) {
477
650
  diag.add({
478
651
  code: 'E_OMIT_UNKNOWN_JOINT',
479
652
  module: m.name,
480
- message: `omits joint "${key}", which no installed module publishes`,
653
+ message: `omits joint "${key}", which no composed module publishes`,
481
654
  hint: `published joints: ${Object.keys(manifest.joints).join(', ') || '(none)'}`,
482
655
  });
483
656
  continue;
@@ -544,6 +717,55 @@ export function compose(modules, opts = {}) {
544
717
  };
545
718
  }
546
719
  }
720
+ manifest.permissions = compilePermissionBundles(order, manifest, {
721
+ requireCoverage: opts.requirePermissionCoverage,
722
+ roleTemplates: opts.roleTemplates,
723
+ moduleDeclarations: opts.modulePermissionDeclarations,
724
+ });
725
+ // --- printable reports ---------------------------------------------------
726
+ for (const m of order) {
727
+ for (const [name, def] of Object.entries(m.reports ?? {})) {
728
+ const id = qualify(m.name, name);
729
+ if (!/^[a-z][a-zA-Z0-9_]*$/.test(name)) {
730
+ diag.add({ code: 'E_REPORT_NAME', module: m.name, message: `invalid report name "${name}"` });
731
+ continue;
732
+ }
733
+ if (!manifest.models[def.target]) {
734
+ diag.add({
735
+ code: 'E_REPORT_UNKNOWN_MODEL',
736
+ module: m.name,
737
+ message: `report "${id}" targets unknown model "${def.target}"`,
738
+ });
739
+ continue;
740
+ }
741
+ const source = manifest.functions[def.source];
742
+ if (!source) {
743
+ diag.add({
744
+ code: 'E_REPORT_UNKNOWN_SOURCE',
745
+ module: m.name,
746
+ message: `report "${id}" uses unknown function "${def.source}"`,
747
+ });
748
+ continue;
749
+ }
750
+ if (source.effects.some((effect) => effect.startsWith('write:') || effect.startsWith('enqueue:'))) {
751
+ diag.add({
752
+ code: 'E_REPORT_SOURCE_WRITES',
753
+ module: m.name,
754
+ message: `report "${id}" source "${def.source}" is not read-only`,
755
+ });
756
+ continue;
757
+ }
758
+ if (!def.template.trim()) {
759
+ diag.add({
760
+ code: 'E_REPORT_TEMPLATE_EMPTY',
761
+ module: m.name,
762
+ message: `report "${id}" has no template`,
763
+ });
764
+ continue;
765
+ }
766
+ manifest.reports[id] = { ...def, by: m.name, id };
767
+ }
768
+ }
547
769
  // --- background jobs -----------------------------------------------------
548
770
  //
549
771
  // Jobs run later and often on another process, but they touch the same data.
@@ -590,6 +812,37 @@ export function compose(modules, opts = {}) {
590
812
  if (!parsed.ok)
591
813
  diag.add({ code: 'E_BAD_TYPE', module: m.name, message: `${key} input ${input}: ${parsed.reason}` });
592
814
  }
815
+ if (def.schedule) {
816
+ try {
817
+ validateSchedule(def.schedule);
818
+ }
819
+ catch (error) {
820
+ diag.add({
821
+ code: 'E_BAD_SCHEDULE',
822
+ module: m.name,
823
+ message: `job "${key}": ${error.message}`,
824
+ hint: error.hint ?? null,
825
+ });
826
+ continue;
827
+ }
828
+ // Nobody is there to supply arguments to a schedule, so a required input is
829
+ // a job that can only ever fail validation at three in the morning.
830
+ const required = Object.entries(def.input ?? {})
831
+ .filter(([, spec]) => {
832
+ const parsed = parseType(spec);
833
+ return parsed.ok && !parsed.optional;
834
+ })
835
+ .map(([name]) => name);
836
+ if (required.length) {
837
+ diag.add({
838
+ code: 'E_SCHEDULED_JOB_INPUT',
839
+ module: m.name,
840
+ message: `scheduled job "${key}" requires input ${required.join(', ')}`,
841
+ hint: 'a schedule enqueues no arguments — make them optional, or drop the schedule',
842
+ });
843
+ continue;
844
+ }
845
+ }
593
846
  for (const effect of def.effects ?? []) {
594
847
  // Enqueue targets are validated after every job has been collected, so a
595
848
  // producer may refer to a job contributed later in dependency order.
@@ -627,6 +880,7 @@ export function compose(modules, opts = {}) {
627
880
  idempotent: true,
628
881
  maxAttempts,
629
882
  timeoutMs,
883
+ ...(def.schedule ? { schedule: def.schedule } : {}),
630
884
  };
631
885
  }
632
886
  }
@@ -934,7 +1188,7 @@ export function compose(modules, opts = {}) {
934
1188
  });
935
1189
  }
936
1190
  }
937
- // --- theme <-> app region contract ---------------------------------------
1191
+ // --- theme <-> deployment region contract --------------------------------
938
1192
  for (const m of order) {
939
1193
  for (const r of m.provides)
940
1194
  (manifest.regions.provided[r] ??= []).push(m.name);
@@ -947,7 +1201,7 @@ export function compose(modules, opts = {}) {
947
1201
  if (!manifest.regions.required.includes(r))
948
1202
  manifest.regions.required.push(r);
949
1203
  }
950
- // A headless app renders nothing, so the region contract does not apply to it.
1204
+ // A headless deployment renders nothing, so the region contract does not apply to it.
951
1205
  // Requirements are still recorded, so adding a theme later checks them.
952
1206
  for (const r of opts.headless ? [] : manifest.regions.required) {
953
1207
  if (!manifest.regions.provided[r]) {