@beechcms/core 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/antivirus/noop-antivirus.provider.d.ts.map +1 -1
  3. package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -1
  4. package/dist/automations-grammar-words.d.ts +2 -0
  5. package/dist/automations-grammar-words.d.ts.map +1 -0
  6. package/dist/automations-grammar-words.js +8 -0
  7. package/dist/automations.repository.interface.d.ts +19 -0
  8. package/dist/automations.repository.interface.d.ts.map +1 -0
  9. package/dist/automations.repository.interface.js +3 -0
  10. package/dist/automations.runner.interface.d.ts +10 -0
  11. package/dist/automations.runner.interface.d.ts.map +1 -0
  12. package/dist/automations.runner.interface.js +3 -0
  13. package/dist/automations.runner.stub.d.ts +5 -0
  14. package/dist/automations.runner.stub.d.ts.map +1 -0
  15. package/dist/automations.runner.stub.js +7 -0
  16. package/dist/automations.types.d.ts +75 -0
  17. package/dist/automations.types.d.ts.map +1 -0
  18. package/dist/automations.types.js +3 -0
  19. package/dist/clock.d.ts +25 -0
  20. package/dist/clock.d.ts.map +1 -0
  21. package/dist/clock.js +11 -0
  22. package/dist/common/canonical-json.d.ts +30 -0
  23. package/dist/common/canonical-json.d.ts.map +1 -0
  24. package/dist/common/canonical-json.js +89 -0
  25. package/dist/content/content.repository.d.ts +2 -2
  26. package/dist/content/content.repository.d.ts.map +1 -1
  27. package/dist/content/deletion-ledger.d.ts +39 -0
  28. package/dist/content/deletion-ledger.d.ts.map +1 -0
  29. package/dist/content/deletion-ledger.js +3 -0
  30. package/dist/content-scan.repository.d.ts +9 -0
  31. package/dist/content-scan.repository.d.ts.map +1 -0
  32. package/dist/content-scan.repository.js +3 -0
  33. package/dist/content.repository.d.ts +247 -0
  34. package/dist/content.repository.d.ts.map +1 -0
  35. package/dist/content.repository.js +59 -0
  36. package/dist/dashboard-layout.d.ts +154 -0
  37. package/dist/dashboard-layout.d.ts.map +1 -0
  38. package/dist/dashboard-layout.js +186 -0
  39. package/dist/dashboard-layout.repository.d.ts +18 -0
  40. package/dist/dashboard-layout.repository.d.ts.map +1 -0
  41. package/dist/dashboard-layout.repository.js +3 -0
  42. package/dist/dashboard-permissions.d.ts +3 -0
  43. package/dist/dashboard-permissions.d.ts.map +1 -0
  44. package/dist/dashboard-permissions.js +11 -0
  45. package/dist/dashboard-scopes.d.ts +12 -0
  46. package/dist/dashboard-scopes.d.ts.map +1 -0
  47. package/dist/dashboard-scopes.js +30 -0
  48. package/dist/define-seed.d.ts +3 -0
  49. package/dist/define-seed.d.ts.map +1 -0
  50. package/dist/define-seed.js +5 -0
  51. package/dist/demo-data.repository.d.ts +5 -0
  52. package/dist/demo-data.repository.d.ts.map +1 -0
  53. package/dist/demo-data.repository.js +3 -0
  54. package/dist/email.provider.d.ts +32 -0
  55. package/dist/email.provider.d.ts.map +1 -0
  56. package/dist/email.provider.js +4 -0
  57. package/dist/engine/ddl/ddl.d.ts +216 -0
  58. package/dist/engine/ddl/ddl.d.ts.map +1 -0
  59. package/dist/engine/ddl/ddl.js +574 -0
  60. package/dist/engine/ddl/schema-mutator.d.ts +26 -0
  61. package/dist/engine/ddl/schema-mutator.d.ts.map +1 -0
  62. package/dist/engine/ddl/schema-mutator.js +3 -0
  63. package/dist/engine/ddl/seed-ddl-destructive.d.ts +40 -0
  64. package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +1 -0
  65. package/dist/engine/ddl/seed-ddl-destructive.js +149 -0
  66. package/dist/engine/ddl/seed-ddl.d.ts +43 -0
  67. package/dist/engine/ddl/seed-ddl.d.ts.map +1 -0
  68. package/dist/engine/ddl/seed-ddl.js +115 -0
  69. package/dist/engine/introspection/introspection.d.ts +101 -0
  70. package/dist/engine/introspection/introspection.d.ts.map +1 -0
  71. package/dist/engine/introspection/introspection.js +127 -0
  72. package/dist/engine/introspection/schema-fingerprint.d.ts +73 -0
  73. package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -0
  74. package/dist/engine/introspection/schema-fingerprint.js +97 -0
  75. package/dist/engine/introspection.d.ts +101 -0
  76. package/dist/engine/introspection.d.ts.map +1 -0
  77. package/dist/engine/introspection.js +127 -0
  78. package/dist/engine/localization/localization.d.ts +127 -0
  79. package/dist/engine/localization/localization.d.ts.map +1 -0
  80. package/dist/engine/localization/localization.js +212 -0
  81. package/dist/engine/localization.d.ts +127 -0
  82. package/dist/engine/localization.d.ts.map +1 -0
  83. package/dist/engine/localization.js +212 -0
  84. package/dist/engine/privacy/policies.d.ts +58 -0
  85. package/dist/engine/privacy/policies.d.ts.map +1 -0
  86. package/dist/engine/privacy/policies.js +185 -0
  87. package/dist/engine/privacy/privacy.service.d.ts +65 -0
  88. package/dist/engine/privacy/privacy.service.d.ts.map +1 -0
  89. package/dist/engine/privacy/privacy.service.js +114 -0
  90. package/dist/engine/privacy.service.d.ts.map +1 -1
  91. package/dist/engine/query/query.d.ts +23 -0
  92. package/dist/engine/query/query.d.ts.map +1 -0
  93. package/dist/engine/query/query.js +301 -0
  94. package/dist/engine/query/relations.d.ts +28 -0
  95. package/dist/engine/query/relations.d.ts.map +1 -0
  96. package/dist/engine/query/relations.js +25 -0
  97. package/dist/engine/query/serialize.d.ts +22 -0
  98. package/dist/engine/query/serialize.d.ts.map +1 -0
  99. package/dist/engine/query/serialize.js +175 -0
  100. package/dist/engine/schema-fingerprint.d.ts +73 -0
  101. package/dist/engine/schema-fingerprint.d.ts.map +1 -0
  102. package/dist/engine/schema-fingerprint.js +97 -0
  103. package/dist/engine/seed-registry.d.ts.map +1 -1
  104. package/dist/engine/seeds/define-seed.d.ts +3 -0
  105. package/dist/engine/seeds/define-seed.d.ts.map +1 -0
  106. package/dist/engine/seeds/define-seed.js +5 -0
  107. package/dist/engine/seeds/seed-registry.d.ts +71 -0
  108. package/dist/engine/seeds/seed-registry.d.ts.map +1 -0
  109. package/dist/engine/seeds/seed-registry.js +167 -0
  110. package/dist/engine/seeds/seed-types-generator.d.ts +25 -0
  111. package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -0
  112. package/dist/engine/seeds/seed-types-generator.js +117 -0
  113. package/dist/engine/seeds/seed-validation.d.ts +30 -0
  114. package/dist/engine/seeds/seed-validation.d.ts.map +1 -0
  115. package/dist/engine/seeds/seed-validation.js +364 -0
  116. package/dist/engine/seeds/seeds.d.ts +20 -0
  117. package/dist/engine/seeds/seeds.d.ts.map +1 -0
  118. package/dist/engine/seeds/seeds.js +21 -0
  119. package/dist/engine/validation/emptiness.d.ts +12 -0
  120. package/dist/engine/validation/emptiness.d.ts.map +1 -0
  121. package/dist/engine/validation/emptiness.js +33 -0
  122. package/dist/engine/validation/index.d.ts +1 -1
  123. package/dist/engine/validation/index.d.ts.map +1 -1
  124. package/dist/engine.d.ts +144 -0
  125. package/dist/engine.d.ts.map +1 -0
  126. package/dist/engine.js +741 -0
  127. package/dist/file-types.d.ts +13 -0
  128. package/dist/file-types.d.ts.map +1 -0
  129. package/dist/file-types.js +85 -0
  130. package/dist/hooks.d.ts +29 -0
  131. package/dist/hooks.d.ts.map +1 -0
  132. package/dist/hooks.js +3 -0
  133. package/dist/id-generator.d.ts +26 -0
  134. package/dist/id-generator.d.ts.map +1 -0
  135. package/dist/id-generator.js +12 -0
  136. package/dist/idempotency.repository.d.ts +13 -0
  137. package/dist/idempotency.repository.d.ts.map +1 -0
  138. package/dist/idempotency.repository.js +3 -0
  139. package/dist/kanban-position.repository.d.ts +19 -0
  140. package/dist/kanban-position.repository.d.ts.map +1 -0
  141. package/dist/kanban-position.repository.js +3 -0
  142. package/dist/kanban.d.ts +75 -0
  143. package/dist/kanban.d.ts.map +1 -0
  144. package/dist/kanban.js +82 -0
  145. package/dist/layout-permissions.d.ts +3 -0
  146. package/dist/layout-permissions.d.ts.map +1 -0
  147. package/dist/layout-permissions.js +11 -0
  148. package/dist/media/image-transformer.d.ts +34 -0
  149. package/dist/media/image-transformer.d.ts.map +1 -0
  150. package/dist/media/image-transformer.js +3 -0
  151. package/dist/media/media-transform.d.ts +88 -0
  152. package/dist/media/media-transform.d.ts.map +1 -0
  153. package/dist/media/media-transform.js +193 -0
  154. package/dist/media.repository.d.ts +61 -0
  155. package/dist/media.repository.d.ts.map +1 -0
  156. package/dist/media.repository.js +3 -0
  157. package/dist/notifications/notification-service.d.ts +1 -1
  158. package/dist/oauth/pkce.d.ts +1 -1
  159. package/dist/oauth/role-guard.d.ts.map +1 -1
  160. package/dist/oauth/scopes.d.ts +1 -1
  161. package/dist/oauth/scopes.d.ts.map +1 -1
  162. package/dist/policies.d.ts +10 -0
  163. package/dist/policies.d.ts.map +1 -0
  164. package/dist/policies.js +33 -0
  165. package/dist/queue/queue.stub.d.ts.map +1 -1
  166. package/dist/queue/scheduler.stub.d.ts.map +1 -1
  167. package/dist/queue.interface.d.ts +39 -0
  168. package/dist/queue.interface.d.ts.map +1 -0
  169. package/dist/queue.interface.js +3 -0
  170. package/dist/queue.stub.d.ts +6 -0
  171. package/dist/queue.stub.d.ts.map +1 -0
  172. package/dist/queue.stub.js +8 -0
  173. package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -1
  174. package/dist/rbac/evaluate.d.ts +57 -0
  175. package/dist/rbac/evaluate.d.ts.map +1 -0
  176. package/dist/rbac/evaluate.js +113 -0
  177. package/dist/rbac/invitation.repository.d.ts +66 -0
  178. package/dist/rbac/invitation.repository.d.ts.map +1 -0
  179. package/dist/rbac/invitation.repository.js +3 -0
  180. package/dist/rbac/permissions.d.ts +39 -0
  181. package/dist/rbac/permissions.d.ts.map +1 -0
  182. package/dist/rbac/permissions.js +43 -0
  183. package/dist/rbac/types.d.ts +118 -0
  184. package/dist/rbac/types.d.ts.map +1 -0
  185. package/dist/rbac/types.js +3 -0
  186. package/dist/relations.d.ts +28 -0
  187. package/dist/relations.d.ts.map +1 -0
  188. package/dist/relations.js +25 -0
  189. package/dist/richtext-render.d.ts +11 -0
  190. package/dist/richtext-render.d.ts.map +1 -0
  191. package/dist/richtext-render.js +87 -0
  192. package/dist/richtext.d.ts +11 -0
  193. package/dist/richtext.d.ts.map +1 -0
  194. package/dist/richtext.js +13 -0
  195. package/dist/scheduler.interface.d.ts +4 -0
  196. package/dist/scheduler.interface.d.ts.map +1 -0
  197. package/dist/scheduler.interface.js +3 -0
  198. package/dist/scheduler.stub.d.ts +5 -0
  199. package/dist/scheduler.stub.d.ts.map +1 -0
  200. package/dist/scheduler.stub.js +7 -0
  201. package/dist/schema/canonical.d.ts +15 -0
  202. package/dist/schema/canonical.d.ts.map +1 -0
  203. package/dist/schema/canonical.js +37 -0
  204. package/dist/schema/define.d.ts +42 -0
  205. package/dist/schema/define.d.ts.map +1 -0
  206. package/dist/schema/define.js +56 -0
  207. package/dist/schema/emit.d.ts +14 -0
  208. package/dist/schema/emit.d.ts.map +1 -0
  209. package/dist/schema/emit.js +43 -0
  210. package/dist/schema/index.d.ts +12 -0
  211. package/dist/schema/index.d.ts.map +1 -0
  212. package/dist/schema/index.js +13 -0
  213. package/dist/schema/manifest-seeds.d.ts +12 -0
  214. package/dist/schema/manifest-seeds.d.ts.map +1 -0
  215. package/dist/schema/manifest-seeds.js +48 -0
  216. package/dist/schema/manifest-validation.d.ts +13 -0
  217. package/dist/schema/manifest-validation.d.ts.map +1 -0
  218. package/dist/schema/manifest-validation.js +41 -0
  219. package/dist/schema/manifest.types.d.ts +54 -0
  220. package/dist/schema/manifest.types.d.ts.map +1 -0
  221. package/dist/schema/manifest.types.js +7 -0
  222. package/dist/schema-mutator.d.ts +26 -0
  223. package/dist/schema-mutator.d.ts.map +1 -0
  224. package/dist/schema-mutator.js +3 -0
  225. package/dist/seed-ddl-destructive.d.ts +40 -0
  226. package/dist/seed-ddl-destructive.d.ts.map +1 -0
  227. package/dist/seed-ddl-destructive.js +149 -0
  228. package/dist/seed-ddl.d.ts +40 -0
  229. package/dist/seed-ddl.d.ts.map +1 -0
  230. package/dist/seed-ddl.js +100 -0
  231. package/dist/seed-layout.d.ts +145 -0
  232. package/dist/seed-layout.d.ts.map +1 -0
  233. package/dist/seed-layout.js +192 -0
  234. package/dist/seed-layout.repository.d.ts +22 -0
  235. package/dist/seed-layout.repository.d.ts.map +1 -0
  236. package/dist/seed-layout.repository.js +3 -0
  237. package/dist/seed-registry.d.ts +71 -0
  238. package/dist/seed-registry.d.ts.map +1 -0
  239. package/dist/seed-registry.js +143 -0
  240. package/dist/seed-types-generator.d.ts +1 -1
  241. package/dist/seed-types-generator.d.ts.map +1 -1
  242. package/dist/seed-validation.d.ts +23 -0
  243. package/dist/seed-validation.d.ts.map +1 -0
  244. package/dist/seed-validation.js +224 -0
  245. package/dist/seed.repository.d.ts +37 -0
  246. package/dist/seed.repository.d.ts.map +1 -0
  247. package/dist/seed.repository.js +3 -0
  248. package/dist/seeds.d.ts +20 -0
  249. package/dist/seeds.d.ts.map +1 -0
  250. package/dist/seeds.js +21 -0
  251. package/dist/site-settings.repository.d.ts +16 -0
  252. package/dist/site-settings.repository.d.ts.map +1 -0
  253. package/dist/site-settings.repository.js +3 -0
  254. package/dist/slug-utils.d.ts +18 -0
  255. package/dist/slug-utils.d.ts.map +1 -0
  256. package/dist/slug-utils.js +32 -0
  257. package/dist/storage.d.ts +46 -0
  258. package/dist/storage.d.ts.map +1 -0
  259. package/dist/storage.js +3 -0
  260. package/dist/transfer/csv.d.ts +34 -0
  261. package/dist/transfer/csv.d.ts.map +1 -0
  262. package/dist/transfer/csv.js +135 -0
  263. package/dist/transfer/flat-seed.d.ts +13 -0
  264. package/dist/transfer/flat-seed.d.ts.map +1 -0
  265. package/dist/transfer/flat-seed.js +45 -0
  266. package/dist/transfer/index.d.ts +7 -0
  267. package/dist/transfer/index.d.ts.map +1 -0
  268. package/dist/transfer/index.js +8 -0
  269. package/dist/transfer/ndjson.d.ts +25 -0
  270. package/dist/transfer/ndjson.d.ts.map +1 -0
  271. package/dist/transfer/ndjson.js +62 -0
  272. package/dist/transfer/row-mapping.d.ts +36 -0
  273. package/dist/transfer/row-mapping.d.ts.map +1 -0
  274. package/dist/transfer/row-mapping.js +90 -0
  275. package/dist/transfer/transfer.constants.d.ts +20 -0
  276. package/dist/transfer/transfer.constants.d.ts.map +1 -0
  277. package/dist/transfer/transfer.constants.js +21 -0
  278. package/dist/transfer/transfer.types.d.ts +46 -0
  279. package/dist/transfer/transfer.types.d.ts.map +1 -0
  280. package/dist/transfer/transfer.types.js +6 -0
  281. package/dist/types.d.ts +243 -0
  282. package/dist/types.d.ts.map +1 -0
  283. package/dist/types.js +3 -0
  284. package/dist/validation.d.ts +50 -0
  285. package/dist/validation.d.ts.map +1 -0
  286. package/dist/validation.js +660 -0
  287. package/dist/webhook-validation.d.ts +3 -0
  288. package/dist/webhook-validation.d.ts.map +1 -0
  289. package/dist/webhook-validation.js +6 -0
  290. package/package.json +4 -4
@@ -0,0 +1,193 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { sha256hex } from '../engine/privacy/policies.js';
4
+ import { canonicalizeMime } from './file-types.js';
5
+ /** Default ceiling, in pixels, on either side of a derived variant. Covers native 32:9 ultrawide panels. */
6
+ export const DEFAULT_MEDIA_MAX_DIMENSION = 5120;
7
+ /** MEDIA_MAX_DIMENSION may lower the ceiling freely but raise it only this far. */
8
+ export const ABSOLUTE_MAX_MEDIA_DIMENSION = 8192;
9
+ export const MEDIA_OUTPUT_FORMATS = ['original', 'webp', 'jpeg'];
10
+ export const MEDIA_QUALITIES = ['low', 'medium', 'high'];
11
+ export const DEFAULT_MEDIA_FORMAT = 'original';
12
+ export const DEFAULT_MEDIA_QUALITY = 'medium';
13
+ /** Encoder quality per enum value. `medium` ≈ 82 per the brief. */
14
+ export const MEDIA_QUALITY_VALUES = Object.freeze({ low: 60, medium: 82, high: 92 });
15
+ export const DEFAULT_MEDIA_SCALE_WIDTHS = [320, 480, 640, 768, 1024, 1280, 1536, 1920, 2560, 3840, 5120];
16
+ export const DEFAULT_MEDIA_PRESETS = Object.freeze({
17
+ thumbnail: { kind: 'crop', width: 200, height: 200 },
18
+ avatar: { kind: 'crop', width: 128, height: 128 },
19
+ card: { kind: 'crop', width: 400, height: 300 },
20
+ 'og-image': { kind: 'crop', width: 1200, height: 630 },
21
+ hero: { kind: 'crop', width: 1920, height: 800 },
22
+ ...Object.fromEntries(DEFAULT_MEDIA_SCALE_WIDTHS.map((width) => [`w-${width}`, { kind: 'scale', width }])),
23
+ });
24
+ /** Raster sources the transform path accepts. Everything else — SVG, PDF, video, AVIF, BMP, ICO — is refused. */
25
+ export const TRANSFORMABLE_MEDIA_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
26
+ /** Parameters of the discarded free-form contract. Their presence is an error, never ignored. */
27
+ export const FORBIDDEN_MEDIA_TRANSFORM_PARAMS = ['w', 'h', 'width', 'height', 'fit', 'q'];
28
+ /** Thrown by buildMediaPresetCatalog on any malformed MEDIA_PRESETS entry. Fail closed, never fall back. */
29
+ export class MediaPresetCatalogError extends Error {
30
+ reason;
31
+ constructor(reason) {
32
+ super(`Invalid media preset catalog: ${reason}`);
33
+ this.reason = reason;
34
+ this.name = 'MediaPresetCatalogError';
35
+ }
36
+ }
37
+ const PRESET_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
38
+ const SCALE_NAME_PATTERN = /^w-([1-9]\d*)$/;
39
+ const invalid = (code) => ({ kind: 'invalid', code });
40
+ /**
41
+ * Classifies a media query string. Pure: does not consult the catalog (that is env-dependent and
42
+ * resolved by the caller), so an unknown-but-well-formed preset name returns `transform`.
43
+ */
44
+ export function parseMediaTransformQuery(params) {
45
+ if (FORBIDDEN_MEDIA_TRANSFORM_PARAMS.some((name) => params.has(name)))
46
+ return invalid('media_param_forbidden');
47
+ const presets = params.getAll('preset');
48
+ const formats = params.getAll('format');
49
+ const qualities = params.getAll('quality');
50
+ if (presets.length > 1 || formats.length > 1 || qualities.length > 1)
51
+ return invalid('media_param_duplicated');
52
+ if (presets.length === 0 && formats.length === 0 && qualities.length === 0)
53
+ return { kind: 'none' };
54
+ const preset = presets[0];
55
+ if (!preset)
56
+ return invalid('media_preset_required');
57
+ const format = formats[0] ?? DEFAULT_MEDIA_FORMAT;
58
+ if (!MEDIA_OUTPUT_FORMATS.includes(format))
59
+ return invalid('media_format_invalid');
60
+ const quality = qualities[0] ?? DEFAULT_MEDIA_QUALITY;
61
+ if (!MEDIA_QUALITIES.includes(quality))
62
+ return invalid('media_quality_invalid');
63
+ if (!PRESET_NAME_PATTERN.test(preset))
64
+ return invalid('media_preset_unknown');
65
+ return { kind: 'transform', request: { preset, format: format, quality: quality } };
66
+ }
67
+ /**
68
+ * Canonical query for a request: fixed order preset → format → quality, defaults omitted.
69
+ * `@beechcms/client` emits exactly this string; the server recomputes it and never trusts the client's.
70
+ */
71
+ export function canonicalMediaTransformQuery(request) {
72
+ let query = `preset=${encodeURIComponent(request.preset)}`;
73
+ if (request.format !== DEFAULT_MEDIA_FORMAT)
74
+ query += `&format=${request.format}`;
75
+ if (request.quality !== DEFAULT_MEDIA_QUALITY)
76
+ query += `&quality=${request.quality}`;
77
+ return query;
78
+ }
79
+ function isPositiveIntegerAtMost(value, max) {
80
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 && value <= max;
81
+ }
82
+ function parsePresetEntry(name, value, maxDimension) {
83
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
84
+ throw new MediaPresetCatalogError(`preset "${name}" must be an object or null`);
85
+ }
86
+ const entry = value;
87
+ const scaleMatch = SCALE_NAME_PATTERN.exec(name);
88
+ if (entry.kind === 'crop') {
89
+ if (Object.keys(entry).some((field) => !['kind', 'width', 'height'].includes(field))) {
90
+ throw new MediaPresetCatalogError(`preset "${name}" has unknown fields`);
91
+ }
92
+ if (scaleMatch)
93
+ throw new MediaPresetCatalogError(`"${name}" is reserved for scale presets`);
94
+ if (!isPositiveIntegerAtMost(entry.width, maxDimension) || !isPositiveIntegerAtMost(entry.height, maxDimension)) {
95
+ throw new MediaPresetCatalogError(`preset "${name}" needs integer width/height in 1..${maxDimension}`);
96
+ }
97
+ return { kind: 'crop', width: entry.width, height: entry.height };
98
+ }
99
+ if (entry.kind === 'scale') {
100
+ if (Object.keys(entry).some((field) => !['kind', 'width'].includes(field))) {
101
+ throw new MediaPresetCatalogError(`preset "${name}" has unknown fields (scale presets never take a height)`);
102
+ }
103
+ if (!isPositiveIntegerAtMost(entry.width, maxDimension)) {
104
+ throw new MediaPresetCatalogError(`preset "${name}" needs an integer width in 1..${maxDimension}`);
105
+ }
106
+ if (name !== `w-${entry.width}`)
107
+ throw new MediaPresetCatalogError(`scale preset "${name}" must be named "w-${entry.width}"`);
108
+ return { kind: 'scale', width: entry.width };
109
+ }
110
+ throw new MediaPresetCatalogError(`preset "${name}" has kind other than "crop" | "scale"`);
111
+ }
112
+ /**
113
+ * Builds the active catalog: DEFAULT_MEDIA_PRESETS merged by name with `overrides`
114
+ * (the parsed MEDIA_PRESETS JSON). An override value of `null` removes a preset.
115
+ * Defaults larger than `maxDimension` are dropped (they could only ever 400); an operator-supplied
116
+ * preset larger than `maxDimension` is an error.
117
+ *
118
+ * @throws {MediaPresetCatalogError} on any malformed override.
119
+ */
120
+ export function buildMediaPresetCatalog(overrides, maxDimension) {
121
+ const catalog = new Map();
122
+ for (const [name, preset] of Object.entries(DEFAULT_MEDIA_PRESETS)) {
123
+ const largest = preset.kind === 'crop' ? Math.max(preset.width, preset.height) : preset.width;
124
+ if (largest <= maxDimension)
125
+ catalog.set(name, preset);
126
+ }
127
+ if (overrides === undefined)
128
+ return catalog;
129
+ if (typeof overrides !== 'object' || overrides === null || Array.isArray(overrides)) {
130
+ throw new MediaPresetCatalogError('MEDIA_PRESETS must be a JSON object keyed by preset name');
131
+ }
132
+ for (const [name, value] of Object.entries(overrides)) {
133
+ if (!PRESET_NAME_PATTERN.test(name))
134
+ throw new MediaPresetCatalogError(`"${name}" is not a valid preset name`);
135
+ if (value === null) {
136
+ catalog.delete(name);
137
+ continue;
138
+ }
139
+ catalog.set(name, parsePresetEntry(name, value, maxDimension));
140
+ }
141
+ return catalog;
142
+ }
143
+ export function isTransformableMime(mime) {
144
+ if (!mime)
145
+ return false;
146
+ return TRANSFORMABLE_MEDIA_MIME_TYPES.includes(canonicalizeMime(mime));
147
+ }
148
+ /**
149
+ * Output dimensions of a scale preset (`fit=scale-down`: never upscales). Returns `null` when either
150
+ * side would exceed `maxDimension` — the pathological-aspect-ratio guard of brief §4. Never truncates.
151
+ */
152
+ export function deriveScaleOutput(presetWidth, source, maxDimension) {
153
+ if (source.width <= 0 || source.height <= 0)
154
+ return null;
155
+ const output = source.width <= presetWidth
156
+ ? { width: source.width, height: source.height }
157
+ : { width: presetWidth, height: Math.max(1, Math.round((source.height * presetWidth) / source.width)) };
158
+ return output.width > maxDimension || output.height > maxDimension ? null : output;
159
+ }
160
+ /** Identity of a preset's *definition*, so redefining a name never reuses an old variant. */
161
+ export function mediaPresetSignature(preset) {
162
+ return preset.kind === 'crop' ? `crop:${preset.width}x${preset.height}` : `scale:${preset.width}`;
163
+ }
164
+ /** Precondition: `isTransformableMime(sourceMime)` is true. */
165
+ export function buildImageTransformSpec(preset, request, sourceMime) {
166
+ const outputMime = request.format === 'webp'
167
+ ? 'image/webp'
168
+ : request.format === 'jpeg'
169
+ ? 'image/jpeg'
170
+ : canonicalizeMime(sourceMime);
171
+ const quality = MEDIA_QUALITY_VALUES[request.quality];
172
+ return preset.kind === 'crop'
173
+ ? { width: preset.width, height: preset.height, fit: 'cover', outputMime, quality }
174
+ : { width: preset.width, fit: 'scale-down', outputMime, quality };
175
+ }
176
+ /**
177
+ * Strong ETag of a variant: source identity (key + size — keys are never overwritten) plus the
178
+ * canonical request plus the preset definition. Format: `"mv1-<32 hex>"`.
179
+ */
180
+ export async function computeMediaVariantEtag(source, request, preset) {
181
+ const digest = await sha256hex(`${source.key}\n${source.size}\n${canonicalMediaTransformQuery(request)}\n${mediaPresetSignature(preset)}`);
182
+ return `"mv1-${digest.slice(0, 32)}"`;
183
+ }
184
+ /** RFC 9110 §13.1.2: `*` matches; otherwise weak comparison against a comma-separated list. */
185
+ export function ifNoneMatchMatches(header, etag) {
186
+ if (!header || !etag)
187
+ return false;
188
+ const target = etag.replace(/^W\//, '');
189
+ return header.split(',').some((candidate) => {
190
+ const trimmed = candidate.trim();
191
+ return trimmed === '*' || trimmed.replace(/^W\//, '') === target;
192
+ });
193
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * MediaRepository - Interface for tracking media assets in the database.
3
+ */
4
+ export interface MediaObject {
5
+ key: string;
6
+ filename: string;
7
+ mime_type: string;
8
+ size_bytes: number;
9
+ uploaded_by: string;
10
+ created_at: number;
11
+ }
12
+ export interface MediaRepository {
13
+ /**
14
+ * Tracks a new file upload in the database.
15
+ */
16
+ trackUpload(media: Omit<MediaObject, 'created_at'>): Promise<void>;
17
+ /**
18
+ * Retrieves tracking info for a file by its key.
19
+ */
20
+ getByKey(key: string): Promise<MediaObject | null>;
21
+ /**
22
+ * Removes tracking info for a file.
23
+ */
24
+ untrack(key: string): Promise<void>;
25
+ /**
26
+ * Lists all tracked media with pagination.
27
+ */
28
+ list(options: {
29
+ limit: number;
30
+ offset: number;
31
+ }): Promise<{
32
+ items: MediaObject[];
33
+ total: number;
34
+ }>;
35
+ /**
36
+ * Gets the total number of tracked media objects.
37
+ */
38
+ count(): Promise<number>;
39
+ }
40
+ /**
41
+ * SystemStatsRepository - Interface for managing global system metrics.
42
+ */
43
+ export interface SystemStatsRepository {
44
+ /**
45
+ * Increments the total storage counter.
46
+ */
47
+ incrementStorage(bytes: number): Promise<void>;
48
+ /**
49
+ * Decrements the total storage counter.
50
+ */
51
+ decrementStorage(bytes: number): Promise<void>;
52
+ /**
53
+ * Sets a specific value for a storage stat (e.g. after a full sync).
54
+ */
55
+ setStorage(bytes: number): Promise<void>;
56
+ /**
57
+ * Gets the current total storage usage in bytes.
58
+ */
59
+ getStorageUsage(): Promise<number>;
60
+ }
61
+ //# sourceMappingURL=media.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.repository.d.ts","sourceRoot":"","sources":["../src/media.repository.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;OAEG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAEnD;;OAEG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpC;;OAEG;IACH,IAAI,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAEnG;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;OAEG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC;;OAEG;IACH,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACpC"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -13,9 +13,9 @@ export declare const createNotificationSchema: z.ZodObject<{
13
13
  title: z.ZodString;
14
14
  message: z.ZodString;
15
15
  type: z.ZodOptional<z.ZodEnum<{
16
- success: "success";
17
16
  error: "error";
18
17
  info: "info";
18
+ success: "success";
19
19
  warning: "warning";
20
20
  }>>;
21
21
  }, z.core.$strip>;
@@ -3,7 +3,7 @@
3
3
  * per OAuth 2.1, so no weak fallback configuration can ever exist.
4
4
  */
5
5
  /** The only accepted code challenge method. */
6
- export declare const PKCE_CHALLENGE_METHOD: "S256";
6
+ export declare const PKCE_CHALLENGE_METHOD: 'S256';
7
7
  export type PkceChallengeMethod = typeof PKCE_CHALLENGE_METHOD;
8
8
  /** Minimum / maximum code_verifier length, RFC 7636 §4.1. */
9
9
  export declare const PKCE_VERIFIER_MIN_LENGTH = 43;
@@ -1 +1 @@
1
- {"version":3,"file":"role-guard.d.ts","sourceRoot":"","sources":["../../src/oauth/role-guard.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,UAAU,EAAE,CAAA;IAC3B,YAAY,EAAE,UAAU,EAAE,CAAA;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,eAAe,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CACzG;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,YAAW,UAAU;IAC5C,SAAS,CACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,eAAe,EAAE,SAAS,UAAU,EAAE,GACrC,OAAO,CAAC,kBAAkB,CAAC;CAG/B"}
1
+ {"version":3,"file":"role-guard.d.ts","sourceRoot":"","sources":["../../src/oauth/role-guard.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,UAAU,EAAE,CAAA;IAC3B,YAAY,EAAE,UAAU,EAAE,CAAA;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,eAAe,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CACzG;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,YAAW,UAAU;IAC5C,SAAS,CACb,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,eAAe,EAAE,SAAS,UAAU,EAAE,GACrC,OAAO,CAAC,kBAAkB,CAAC,CAE7B;CACF"}
@@ -9,7 +9,7 @@
9
9
  * `beech_schema_plan` is deliberately classified as READ: despite the imperative
10
10
  * name it is a dry-run that computes a migration plan and mutates nothing.
11
11
  */
12
- export declare const OAUTH_SCOPES: readonly ["schema:read", "schema:write"];
12
+ export declare const OAUTH_SCOPES: readonly ['schema:read', 'schema:write'];
13
13
  export type OAuthScope = (typeof OAUTH_SCOPES)[number];
14
14
  /** Narrows an arbitrary string to a known scope. */
15
15
  export declare function isOAuthScope(value: string): value is OAuthScope;
@@ -1 +1 @@
1
- {"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../../src/oauth/scopes.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,0CAA2C,CAAA;AAEpE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtD,oDAAoD;AACpD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,UAAU,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,IAAI,CAMjE;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAElE;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAC3B,SAAS,EAAE,SAAS,UAAU,EAAE,EAChC,OAAO,EAAE,SAAS,UAAU,EAAE,GAC7B,OAAO,CAET"}
1
+ {"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../../src/oauth/scopes.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,YAAI,aAAa,EAAE,cAAc,CAAU,CAAA;AAEpE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtD,oDAAoD;AACpD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,UAAU,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,IAAI,CAMjE;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAElE;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAC3B,SAAS,EAAE,SAAS,UAAU,EAAE,EAChC,OAAO,EAAE,SAAS,UAAU,EAAE,GAC7B,OAAO,CAET"}
@@ -0,0 +1,10 @@
1
+ import type { Branch } from './types.js';
2
+ export declare function sha256hex(value: string): Promise<string>;
3
+ export declare function verifyHashField(stored: string, candidate: string): Promise<boolean>;
4
+ /**
5
+ * Risolve le policy di un branch applicando i valori di default.
6
+ * Tutta la logica di accesso ai campi deve passare per questa funzione,
7
+ * mai con inline `branch.policies?.x ?? default`.
8
+ */
9
+ export declare function resolvePolicies(branch: Branch): Required<NonNullable<Branch['policies']>>;
10
+ //# sourceMappingURL=policies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAExC,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK9D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAgBzF"}
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export async function sha256hex(value) {
4
+ const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
5
+ return Array.from(new Uint8Array(buf))
6
+ .map((b) => b.toString(16).padStart(2, '0'))
7
+ .join('');
8
+ }
9
+ export async function verifyHashField(stored, candidate) {
10
+ return stored === (await sha256hex(candidate));
11
+ }
12
+ /**
13
+ * Risolve le policy di un branch applicando i valori di default.
14
+ * Tutta la logica di accesso ai campi deve passare per questa funzione,
15
+ * mai con inline `branch.policies?.x ?? default`.
16
+ */
17
+ export function resolvePolicies(branch) {
18
+ const privacy = branch.policies?.privacy ?? 'plain';
19
+ // Non-plain privacy implies hidden by default: the CMS hashes/encrypts on write,
20
+ // so returning the stored value would leak the digest to readers.
21
+ const defaultVisibility = privacy !== 'plain' ? 'hidden' : 'full';
22
+ // Repeaters live in a single JSON column — never filterable, sortable, or
23
+ // searchable/facetable in v1, regardless of what's set on the branch.
24
+ const isRepeater = branch.type === 'repeater';
25
+ return {
26
+ privacy,
27
+ visibility: branch.policies?.visibility ?? defaultVisibility,
28
+ search: isRepeater ? false : branch.policies?.search ?? true,
29
+ filter: isRepeater ? false : branch.policies?.filter ?? true,
30
+ sort: isRepeater ? false : branch.policies?.sort ?? true,
31
+ public: branch.policies?.public ?? true,
32
+ };
33
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"queue.stub.d.ts","sourceRoot":"","sources":["../../src/queue/queue.stub.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEzD,0EAA0E;AAC1E,qBAAa,gBAAiB,YAAW,aAAa;IAC9C,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;CAI/D"}
1
+ {"version":3,"file":"queue.stub.d.ts","sourceRoot":"","sources":["../../src/queue/queue.stub.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEzD,0EAA0E;AAC1E,qBAAa,gBAAiB,YAAW,aAAa;IAC9C,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAG7D;CACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.stub.d.ts","sourceRoot":"","sources":["../../src/queue/scheduler.stub.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAE1D,qBAAa,aAAc,YAAW,UAAU;IAC9C,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI;CAG5C"}
1
+ {"version":3,"file":"scheduler.stub.d.ts","sourceRoot":"","sources":["../../src/queue/scheduler.stub.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AAE1D,qBAAa,aAAc,YAAW,UAAU;IAC9C,SAAS,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAE1C;CACF"}
@@ -0,0 +1,39 @@
1
+ import type { ContentRepository } from './content.repository.js';
2
+ import type { BeechBucket } from './storage.js';
3
+ import type { IClock } from './clock.js';
4
+ import type { IIdGenerator } from './id-generator.js';
5
+ /**
6
+ * The envelope put on the wire / queue. `name` selects the handler from the
7
+ * JobRegistry; `payload` is the developer-supplied, JSON-serializable body.
8
+ */
9
+ export interface QueueMessage<T = unknown> {
10
+ name: string;
11
+ payload: T;
12
+ }
13
+ /**
14
+ * Execution context handed to every job. Botanical Invariant: jobs receive the
15
+ * engine-mediated repository, NEVER a raw D1Database. `env` is a read-only,
16
+ * stringly-typed view of bindings/secrets for things like fetch targets — it
17
+ * deliberately does not expose `DB`.
18
+ */
19
+ export interface JobContext {
20
+ repository: ContentRepository;
21
+ bucket: BeechBucket;
22
+ clock: IClock;
23
+ idGenerator: IIdGenerator;
24
+ env: Record<string, string | undefined>;
25
+ }
26
+ /** A single background worker. Receives the decoded payload + context. */
27
+ export type JobHandler<T = unknown> = (payload: T, context: JobContext) => Promise<void>;
28
+ /** Name -> handler map, supplied via BeechConfig.jobs. */
29
+ export type JobRegistry = Record<string, JobHandler<any>>;
30
+ /**
31
+ * Producer port. `enqueue` schedules deferred work and returns once the message
32
+ * is accepted by the transport. Like INotificationService, implementations MUST
33
+ * decide their own fire-and-forget vs inline semantics and MUST NOT let a
34
+ * transport failure crash the request that called enqueue.
35
+ */
36
+ export interface IQueueService {
37
+ enqueue<T>(name: string, payload: T): Promise<void>;
38
+ }
39
+ //# sourceMappingURL=queue.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.interface.d.ts","sourceRoot":"","sources":["../src/queue.interface.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD;;;GAGG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,CAAC,CAAA;CACX;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,iBAAiB,CAAA;IAC7B,MAAM,EAAE,WAAW,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,YAAY,CAAA;IACzB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CACxC;AAED,0EAA0E;AAC1E,MAAM,MAAM,UAAU,CAAC,CAAC,GAAG,OAAO,IAAI,CACpC,OAAO,EAAE,CAAC,EACV,OAAO,EAAE,UAAU,KAChB,OAAO,CAAC,IAAI,CAAC,CAAA;AAElB,0DAA0D;AAC1D,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpD"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { IQueueService } from './queue.interface.js';
2
+ /** Safe no-op producer (e.g. unit tests that don't assert on enqueue). */
3
+ export declare class NoOpQueueService implements IQueueService {
4
+ enqueue<T>(_name: string, _payload: T): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=queue.stub.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue.stub.d.ts","sourceRoot":"","sources":["../src/queue.stub.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AAEzD,0EAA0E;AAC1E,qBAAa,gBAAiB,YAAW,aAAa;IAC9C,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAG5D"}
@@ -0,0 +1,8 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ /** Safe no-op producer (e.g. unit tests that don't assert on enqueue). */
4
+ export class NoOpQueueService {
5
+ async enqueue(_name, _payload) {
6
+ // intentionally empty
7
+ }
8
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"token-bucket-rate-limiter.d.ts","sourceRoot":"","sources":["../../src/rate-limit/token-bucket-rate-limiter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAQhD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,qBAAa,sBAAuB,YAAW,YAAY;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAQ;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAQ;gBAE/B,OAAO,CAAC,EAAE,kBAAkB;IAQxC,OAAO,CAAC,mBAAmB;IASrB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAsCvD,KAAK,IAAI,IAAI;CAGd"}
1
+ {"version":3,"file":"token-bucket-rate-limiter.d.ts","sourceRoot":"","sources":["../../src/rate-limit/token-bucket-rate-limiter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAQhD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED,qBAAa,sBAAuB,YAAW,YAAY;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAQ;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAQ;IAE3C,YAAY,OAAO,CAAC,EAAE,kBAAkB,EAMvC;IAED,OAAO,CAAC,mBAAmB;IASrB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAoCtD;IAED,KAAK,IAAI,IAAI,CAEZ;CACF"}
@@ -0,0 +1,57 @@
1
+ import { type Permission, type Scope } from './permissions.js';
2
+ import type { EffectivePermissions, PermissionAssignment, RoleRecord } from './types.js';
3
+ /**
4
+ * Folds a user's active assignments into their effective authority.
5
+ *
6
+ * The model is strictly additive: every assignment can only widen the result, and
7
+ * assignments referencing an unknown role are ignored rather than treated as an error.
8
+ *
9
+ * @param assignments - Active assignments, already decay-filtered by the repository.
10
+ * @param roles - The roles those assignments reference; extra roles are harmless.
11
+ */
12
+ export declare function buildEffectivePermissions(assignments: readonly PermissionAssignment[], roles: readonly RoleRecord[]): EffectivePermissions;
13
+ /**
14
+ * Returns everything the caller may do within one scope: the union of their global
15
+ * permissions and the permissions granted on that specific seed.
16
+ */
17
+ export declare function permissionsForScope(effective: EffectivePermissions, scope: Scope): ReadonlySet<Permission>;
18
+ /**
19
+ * The single authorization question: may this caller perform `permission` on `scope`?
20
+ *
21
+ * A global grant satisfies any scope. A scoped grant satisfies only its own seed and
22
+ * never {@link GLOBAL_SCOPE} — that asymmetry is what stops horizontal escalation.
23
+ */
24
+ export declare function hasPermission(effective: EffectivePermissions, permission: Permission, scope: Scope): boolean;
25
+ /**
26
+ * The anti-escalation rule: an actor may only hand out authority they already hold.
27
+ *
28
+ * A grant is legal only when the actor holds every permission being granted, at the
29
+ * scope it is being granted on. Consequently an actor scoped to seed X can never mint
30
+ * a global assignment, and can never include a permission absent from their own set —
31
+ * regardless of what the role itself contains.
32
+ *
33
+ * @param actor - The granting user's effective authority.
34
+ * @param targetScope - The scope the new assignment would apply to.
35
+ * @param targetPermissions - The permissions carried by the role being assigned.
36
+ */
37
+ export declare function canGrant(actor: EffectivePermissions, targetScope: Scope, targetPermissions: readonly Permission[]): boolean;
38
+ /**
39
+ * Every permission the actor holds at ANY scope — global or seed-scoped — folded into
40
+ * one set.
41
+ *
42
+ * This is deliberately NOT an authorization primitive: it answers "could this actor
43
+ * ever grant X somewhere", not "may this actor do X here". Only {@link hasPermission}
44
+ * and {@link canGrant} answer the latter, and every route-level decision must keep
45
+ * using them.
46
+ */
47
+ export declare function permissionsHeldAnywhere(effective: EffectivePermissions): ReadonlySet<Permission>;
48
+ /**
49
+ * Whether the actor holds `permission` on at least one scope.
50
+ *
51
+ * Backs the coarse route gate for administration endpoints whose scope is not in the
52
+ * URL: the gate keeps out callers with no administrative authority at all, and the
53
+ * slice then makes the exact per-scope decision with {@link hasPermission} /
54
+ * {@link canGrant}. Never use it as the final authorization check.
55
+ */
56
+ export declare function hasPermissionAnywhere(effective: EffectivePermissions, permission: Permission): boolean;
57
+ //# sourceMappingURL=evaluate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../src/rbac/evaluate.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,UAAU,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAExF;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,SAAS,oBAAoB,EAAE,EAC5C,KAAK,EAAE,SAAS,UAAU,EAAE,GAC3B,oBAAoB,CAuBtB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,oBAAoB,EAC/B,KAAK,EAAE,KAAK,GACX,WAAW,CAAC,UAAU,CAAC,CAKzB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,oBAAoB,EAC/B,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,KAAK,GACX,OAAO,CAIT;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,oBAAoB,EAC3B,WAAW,EAAE,KAAK,EAClB,iBAAiB,EAAE,SAAS,UAAU,EAAE,GACvC,OAAO,CAMT;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,oBAAoB,GAAG,WAAW,CAAC,UAAU,CAAC,CAMhG;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,oBAAoB,EAC/B,UAAU,EAAE,UAAU,GACrB,OAAO,CAMT"}
@@ -0,0 +1,113 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { GLOBAL_SCOPE } from './permissions.js';
4
+ /**
5
+ * Folds a user's active assignments into their effective authority.
6
+ *
7
+ * The model is strictly additive: every assignment can only widen the result, and
8
+ * assignments referencing an unknown role are ignored rather than treated as an error.
9
+ *
10
+ * @param assignments - Active assignments, already decay-filtered by the repository.
11
+ * @param roles - The roles those assignments reference; extra roles are harmless.
12
+ */
13
+ export function buildEffectivePermissions(assignments, roles) {
14
+ const rolesById = new Map(roles.map(role => [role.id, role]));
15
+ const global = new Set();
16
+ const byScope = new Map();
17
+ for (const assignment of assignments) {
18
+ const role = rolesById.get(assignment.roleId);
19
+ if (!role)
20
+ continue;
21
+ if (assignment.scope === GLOBAL_SCOPE) {
22
+ for (const permission of role.permissions)
23
+ global.add(permission);
24
+ continue;
25
+ }
26
+ let bucket = byScope.get(assignment.scope);
27
+ if (!bucket) {
28
+ bucket = new Set();
29
+ byScope.set(assignment.scope, bucket);
30
+ }
31
+ for (const permission of role.permissions)
32
+ bucket.add(permission);
33
+ }
34
+ return { global, byScope };
35
+ }
36
+ /**
37
+ * Returns everything the caller may do within one scope: the union of their global
38
+ * permissions and the permissions granted on that specific seed.
39
+ */
40
+ export function permissionsForScope(effective, scope) {
41
+ if (scope === GLOBAL_SCOPE)
42
+ return effective.global;
43
+ const scoped = effective.byScope.get(scope);
44
+ if (!scoped)
45
+ return effective.global;
46
+ return new Set([...effective.global, ...scoped]);
47
+ }
48
+ /**
49
+ * The single authorization question: may this caller perform `permission` on `scope`?
50
+ *
51
+ * A global grant satisfies any scope. A scoped grant satisfies only its own seed and
52
+ * never {@link GLOBAL_SCOPE} — that asymmetry is what stops horizontal escalation.
53
+ */
54
+ export function hasPermission(effective, permission, scope) {
55
+ if (effective.global.has(permission))
56
+ return true;
57
+ if (scope === GLOBAL_SCOPE)
58
+ return false;
59
+ return effective.byScope.get(scope)?.has(permission) === true;
60
+ }
61
+ /**
62
+ * The anti-escalation rule: an actor may only hand out authority they already hold.
63
+ *
64
+ * A grant is legal only when the actor holds every permission being granted, at the
65
+ * scope it is being granted on. Consequently an actor scoped to seed X can never mint
66
+ * a global assignment, and can never include a permission absent from their own set —
67
+ * regardless of what the role itself contains.
68
+ *
69
+ * @param actor - The granting user's effective authority.
70
+ * @param targetScope - The scope the new assignment would apply to.
71
+ * @param targetPermissions - The permissions carried by the role being assigned.
72
+ */
73
+ export function canGrant(actor, targetScope, targetPermissions) {
74
+ if (targetScope === GLOBAL_SCOPE) {
75
+ return targetPermissions.every(permission => actor.global.has(permission));
76
+ }
77
+ const held = permissionsForScope(actor, targetScope);
78
+ return targetPermissions.every(permission => held.has(permission));
79
+ }
80
+ /**
81
+ * Every permission the actor holds at ANY scope — global or seed-scoped — folded into
82
+ * one set.
83
+ *
84
+ * This is deliberately NOT an authorization primitive: it answers "could this actor
85
+ * ever grant X somewhere", not "may this actor do X here". Only {@link hasPermission}
86
+ * and {@link canGrant} answer the latter, and every route-level decision must keep
87
+ * using them.
88
+ */
89
+ export function permissionsHeldAnywhere(effective) {
90
+ const held = new Set(effective.global);
91
+ for (const bucket of effective.byScope.values()) {
92
+ for (const permission of bucket)
93
+ held.add(permission);
94
+ }
95
+ return held;
96
+ }
97
+ /**
98
+ * Whether the actor holds `permission` on at least one scope.
99
+ *
100
+ * Backs the coarse route gate for administration endpoints whose scope is not in the
101
+ * URL: the gate keeps out callers with no administrative authority at all, and the
102
+ * slice then makes the exact per-scope decision with {@link hasPermission} /
103
+ * {@link canGrant}. Never use it as the final authorization check.
104
+ */
105
+ export function hasPermissionAnywhere(effective, permission) {
106
+ if (effective.global.has(permission))
107
+ return true;
108
+ for (const bucket of effective.byScope.values()) {
109
+ if (bucket.has(permission))
110
+ return true;
111
+ }
112
+ return false;
113
+ }