@beechcms/core 0.8.0 → 0.9.0

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 (389) hide show
  1. package/dist/auth/account-email.d.ts +3 -0
  2. package/dist/auth/account-email.d.ts.map +1 -0
  3. package/dist/auth/account-email.js +9 -0
  4. package/dist/auth/password-reset-token.repository.d.ts +13 -2
  5. package/dist/auth/password-reset-token.repository.d.ts.map +1 -1
  6. package/dist/auth/user.repository.d.ts +69 -4
  7. package/dist/auth/user.repository.d.ts.map +1 -1
  8. package/dist/automations-grammar-words.d.ts +2 -0
  9. package/dist/automations-grammar-words.d.ts.map +1 -0
  10. package/dist/automations-grammar-words.js +8 -0
  11. package/dist/automations.repository.interface.d.ts +19 -0
  12. package/dist/automations.repository.interface.d.ts.map +1 -0
  13. package/dist/automations.repository.interface.js +3 -0
  14. package/dist/automations.runner.interface.d.ts +10 -0
  15. package/dist/automations.runner.interface.d.ts.map +1 -0
  16. package/dist/automations.runner.interface.js +3 -0
  17. package/dist/automations.runner.stub.d.ts +5 -0
  18. package/dist/automations.runner.stub.d.ts.map +1 -0
  19. package/dist/automations.runner.stub.js +7 -0
  20. package/dist/automations.types.d.ts +75 -0
  21. package/dist/automations.types.d.ts.map +1 -0
  22. package/dist/automations.types.js +3 -0
  23. package/dist/clock.d.ts +25 -0
  24. package/dist/clock.d.ts.map +1 -0
  25. package/dist/clock.js +11 -0
  26. package/dist/common/canonical-json.d.ts +31 -0
  27. package/dist/common/canonical-json.d.ts.map +1 -0
  28. package/dist/common/canonical-json.js +91 -0
  29. package/dist/content/content-scan.repository.d.ts +1 -1
  30. package/dist/content/content.repository.d.ts +154 -3
  31. package/dist/content/content.repository.d.ts.map +1 -1
  32. package/dist/content/content.repository.js +62 -0
  33. package/dist/content/deletion-ledger.d.ts +39 -0
  34. package/dist/content/deletion-ledger.d.ts.map +1 -0
  35. package/dist/content/deletion-ledger.js +3 -0
  36. package/dist/content/seed-media-purge.repository.d.ts +34 -0
  37. package/dist/content/seed-media-purge.repository.d.ts.map +1 -0
  38. package/dist/content/seed-media-purge.repository.js +3 -0
  39. package/dist/content/seed.repository.d.ts +14 -0
  40. package/dist/content/seed.repository.d.ts.map +1 -1
  41. package/dist/content-scan.repository.d.ts +9 -0
  42. package/dist/content-scan.repository.d.ts.map +1 -0
  43. package/dist/content-scan.repository.js +3 -0
  44. package/dist/content.repository.d.ts +247 -0
  45. package/dist/content.repository.d.ts.map +1 -0
  46. package/dist/content.repository.js +59 -0
  47. package/dist/dashboard-layout/content-view.d.ts +757 -0
  48. package/dist/dashboard-layout/content-view.d.ts.map +1 -0
  49. package/dist/dashboard-layout/content-view.js +285 -0
  50. package/dist/dashboard-layout/content-view.repository.d.ts +39 -0
  51. package/dist/dashboard-layout/content-view.repository.d.ts.map +1 -0
  52. package/dist/dashboard-layout/content-view.repository.js +3 -0
  53. package/dist/dashboard-layout/dashboard-layout.d.ts +5 -0
  54. package/dist/dashboard-layout/dashboard-layout.d.ts.map +1 -1
  55. package/dist/dashboard-layout/dashboard-layout.js +17 -9
  56. package/dist/dashboard-layout/kanban/kanban.d.ts +32 -18
  57. package/dist/dashboard-layout/kanban/kanban.d.ts.map +1 -1
  58. package/dist/dashboard-layout/kanban/kanban.js +45 -21
  59. package/dist/dashboard-layout/seed-layout.d.ts +22 -31
  60. package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
  61. package/dist/dashboard-layout/seed-layout.js +93 -18
  62. package/dist/dashboard-layout/seed-layout.repository.d.ts +1 -5
  63. package/dist/dashboard-layout/seed-layout.repository.d.ts.map +1 -1
  64. package/dist/dashboard-layout.d.ts +154 -0
  65. package/dist/dashboard-layout.d.ts.map +1 -0
  66. package/dist/dashboard-layout.js +186 -0
  67. package/dist/dashboard-layout.repository.d.ts +18 -0
  68. package/dist/dashboard-layout.repository.d.ts.map +1 -0
  69. package/dist/dashboard-layout.repository.js +3 -0
  70. package/dist/dashboard-permissions.d.ts +3 -0
  71. package/dist/dashboard-permissions.d.ts.map +1 -0
  72. package/dist/dashboard-permissions.js +11 -0
  73. package/dist/dashboard-scopes.d.ts +12 -0
  74. package/dist/dashboard-scopes.d.ts.map +1 -0
  75. package/dist/dashboard-scopes.js +30 -0
  76. package/dist/define-seed.d.ts +3 -0
  77. package/dist/define-seed.d.ts.map +1 -0
  78. package/dist/define-seed.js +5 -0
  79. package/dist/demo-data.repository.d.ts +5 -0
  80. package/dist/demo-data.repository.d.ts.map +1 -0
  81. package/dist/demo-data.repository.js +3 -0
  82. package/dist/email.provider.d.ts +32 -0
  83. package/dist/email.provider.d.ts.map +1 -0
  84. package/dist/email.provider.js +4 -0
  85. package/dist/engine/ddl/ddl.d.ts +247 -0
  86. package/dist/engine/ddl/ddl.d.ts.map +1 -0
  87. package/dist/engine/ddl/ddl.js +653 -0
  88. package/dist/engine/ddl/schema-mutator.d.ts +29 -0
  89. package/dist/engine/ddl/schema-mutator.d.ts.map +1 -0
  90. package/dist/engine/ddl/schema-mutator.js +3 -0
  91. package/dist/engine/ddl/seed-ddl-destructive.d.ts +40 -0
  92. package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +1 -0
  93. package/dist/engine/ddl/seed-ddl-destructive.js +149 -0
  94. package/dist/engine/ddl/seed-ddl.d.ts +43 -0
  95. package/dist/engine/ddl/seed-ddl.d.ts.map +1 -0
  96. package/dist/engine/ddl/seed-ddl.js +115 -0
  97. package/dist/engine/ddl/seed-purge-guards.d.ts +7 -0
  98. package/dist/engine/ddl/seed-purge-guards.d.ts.map +1 -0
  99. package/dist/engine/ddl/seed-purge-guards.js +19 -0
  100. package/dist/engine/engine.d.ts +4 -3
  101. package/dist/engine/engine.d.ts.map +1 -1
  102. package/dist/engine/engine.js +4 -3
  103. package/dist/engine/introspection/introspection.d.ts +101 -0
  104. package/dist/engine/introspection/introspection.d.ts.map +1 -0
  105. package/dist/engine/introspection/introspection.js +127 -0
  106. package/dist/engine/introspection/relation-audit.d.ts +46 -0
  107. package/dist/engine/introspection/relation-audit.d.ts.map +1 -0
  108. package/dist/engine/introspection/relation-audit.js +103 -0
  109. package/dist/engine/introspection/schema-fingerprint.d.ts +74 -0
  110. package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -0
  111. package/dist/engine/introspection/schema-fingerprint.js +102 -0
  112. package/dist/engine/introspection.d.ts +101 -0
  113. package/dist/engine/introspection.d.ts.map +1 -0
  114. package/dist/engine/introspection.js +127 -0
  115. package/dist/engine/localization/localization.d.ts +127 -0
  116. package/dist/engine/localization/localization.d.ts.map +1 -0
  117. package/dist/engine/localization/localization.js +212 -0
  118. package/dist/engine/localization.d.ts +127 -0
  119. package/dist/engine/localization.d.ts.map +1 -0
  120. package/dist/engine/localization.js +212 -0
  121. package/dist/engine/privacy/policies.d.ts +58 -0
  122. package/dist/engine/privacy/policies.d.ts.map +1 -0
  123. package/dist/engine/privacy/policies.js +191 -0
  124. package/dist/engine/privacy/privacy.service.d.ts +78 -0
  125. package/dist/engine/privacy/privacy.service.d.ts.map +1 -0
  126. package/dist/engine/privacy/privacy.service.js +138 -0
  127. package/dist/engine/privacy.service.d.ts.map +1 -1
  128. package/dist/engine/query/active-clause.d.ts +19 -0
  129. package/dist/engine/query/active-clause.d.ts.map +1 -0
  130. package/dist/engine/query/active-clause.js +27 -0
  131. package/dist/engine/query/query.d.ts +36 -0
  132. package/dist/engine/query/query.d.ts.map +1 -0
  133. package/dist/engine/query/query.js +393 -0
  134. package/dist/engine/query/relations.d.ts +28 -0
  135. package/dist/engine/query/relations.d.ts.map +1 -0
  136. package/dist/engine/query/relations.js +25 -0
  137. package/dist/engine/query/serialize.d.ts +22 -0
  138. package/dist/engine/query/serialize.d.ts.map +1 -0
  139. package/dist/engine/query/serialize.js +175 -0
  140. package/dist/engine/schema-fingerprint.d.ts +73 -0
  141. package/dist/engine/schema-fingerprint.d.ts.map +1 -0
  142. package/dist/engine/schema-fingerprint.js +97 -0
  143. package/dist/engine/seed-registry.d.ts.map +1 -1
  144. package/dist/engine/seeds/candidate-ids.d.ts +14 -0
  145. package/dist/engine/seeds/candidate-ids.d.ts.map +1 -0
  146. package/dist/engine/seeds/candidate-ids.js +40 -0
  147. package/dist/engine/seeds/define-seed.d.ts +3 -0
  148. package/dist/engine/seeds/define-seed.d.ts.map +1 -0
  149. package/dist/engine/seeds/define-seed.js +5 -0
  150. package/dist/engine/seeds/seed-definition-diff.d.ts +38 -0
  151. package/dist/engine/seeds/seed-definition-diff.d.ts.map +1 -0
  152. package/dist/engine/seeds/seed-definition-diff.js +51 -0
  153. package/dist/engine/seeds/seed-registry.d.ts +88 -0
  154. package/dist/engine/seeds/seed-registry.d.ts.map +1 -0
  155. package/dist/engine/seeds/seed-registry.js +194 -0
  156. package/dist/engine/seeds/seed-types-generator.d.ts +25 -0
  157. package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -0
  158. package/dist/engine/seeds/seed-types-generator.js +116 -0
  159. package/dist/engine/seeds/seed-validation.d.ts +35 -0
  160. package/dist/engine/seeds/seed-validation.d.ts.map +1 -0
  161. package/dist/engine/seeds/seed-validation.js +384 -0
  162. package/dist/engine/seeds/seeds.d.ts +20 -0
  163. package/dist/engine/seeds/seeds.d.ts.map +1 -0
  164. package/dist/engine/seeds/seeds.js +21 -0
  165. package/dist/engine/seeds/tag-migration.d.ts +73 -0
  166. package/dist/engine/seeds/tag-migration.d.ts.map +1 -0
  167. package/dist/engine/seeds/tag-migration.js +212 -0
  168. package/dist/engine/seeds/tag-options.d.ts +15 -0
  169. package/dist/engine/seeds/tag-options.d.ts.map +1 -0
  170. package/dist/engine/seeds/tag-options.js +111 -0
  171. package/dist/engine/seeds/tag-value.d.ts +31 -0
  172. package/dist/engine/seeds/tag-value.d.ts.map +1 -0
  173. package/dist/engine/seeds/tag-value.js +51 -0
  174. package/dist/engine/types.d.ts +59 -7
  175. package/dist/engine/types.d.ts.map +1 -1
  176. package/dist/engine/validation/cache.d.ts.map +1 -1
  177. package/dist/engine/validation/cache.js +20 -4
  178. package/dist/engine/validation/emptiness.d.ts +12 -0
  179. package/dist/engine/validation/emptiness.d.ts.map +1 -0
  180. package/dist/engine/validation/emptiness.js +33 -0
  181. package/dist/engine/validation/index.d.ts +11 -0
  182. package/dist/engine/validation/index.d.ts.map +1 -1
  183. package/dist/engine/validation/index.js +29 -33
  184. package/dist/engine/validation/primitives.d.ts +12 -0
  185. package/dist/engine/validation/primitives.d.ts.map +1 -1
  186. package/dist/engine/validation/primitives.js +22 -0
  187. package/dist/engine/validation/richtext-sanitizer.d.ts +9 -1
  188. package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
  189. package/dist/engine/validation/richtext-sanitizer.js +36 -7
  190. package/dist/engine/validation/schema-builders.d.ts +16 -0
  191. package/dist/engine/validation/schema-builders.d.ts.map +1 -1
  192. package/dist/engine/validation/schema-builders.js +125 -18
  193. package/dist/engine.d.ts +144 -0
  194. package/dist/engine.d.ts.map +1 -0
  195. package/dist/engine.js +741 -0
  196. package/dist/file-types.d.ts +13 -0
  197. package/dist/file-types.d.ts.map +1 -0
  198. package/dist/file-types.js +85 -0
  199. package/dist/hooks.d.ts +29 -0
  200. package/dist/hooks.d.ts.map +1 -0
  201. package/dist/hooks.js +3 -0
  202. package/dist/id-generator.d.ts +26 -0
  203. package/dist/id-generator.d.ts.map +1 -0
  204. package/dist/id-generator.js +12 -0
  205. package/dist/idempotency.repository.d.ts +13 -0
  206. package/dist/idempotency.repository.d.ts.map +1 -0
  207. package/dist/idempotency.repository.js +3 -0
  208. package/dist/index.d.ts +34 -11
  209. package/dist/index.d.ts.map +1 -1
  210. package/dist/index.js +34 -11
  211. package/dist/kanban-position.repository.d.ts +19 -0
  212. package/dist/kanban-position.repository.d.ts.map +1 -0
  213. package/dist/kanban-position.repository.js +3 -0
  214. package/dist/kanban.d.ts +75 -0
  215. package/dist/kanban.d.ts.map +1 -0
  216. package/dist/kanban.js +82 -0
  217. package/dist/layout-permissions.d.ts +3 -0
  218. package/dist/layout-permissions.d.ts.map +1 -0
  219. package/dist/layout-permissions.js +11 -0
  220. package/dist/media/file-types.d.ts +12 -0
  221. package/dist/media/file-types.d.ts.map +1 -1
  222. package/dist/media/file-types.js +24 -4
  223. package/dist/media/image-transformer.d.ts +34 -0
  224. package/dist/media/image-transformer.d.ts.map +1 -0
  225. package/dist/media/image-transformer.js +3 -0
  226. package/dist/media/magic-bytes.d.ts.map +1 -1
  227. package/dist/media/magic-bytes.js +19 -9
  228. package/dist/media/media-transform.d.ts +92 -0
  229. package/dist/media/media-transform.d.ts.map +1 -0
  230. package/dist/media/media-transform.js +197 -0
  231. package/dist/media/media.repository.d.ts +5 -0
  232. package/dist/media/media.repository.d.ts.map +1 -1
  233. package/dist/media.repository.d.ts +61 -0
  234. package/dist/media.repository.d.ts.map +1 -0
  235. package/dist/media.repository.js +3 -0
  236. package/dist/notifications/notification.repository.d.ts +11 -2
  237. package/dist/notifications/notification.repository.d.ts.map +1 -1
  238. package/dist/oauth/authorization-code.repository.d.ts +10 -5
  239. package/dist/oauth/authorization-code.repository.d.ts.map +1 -1
  240. package/dist/oauth/role-guard.d.ts +30 -11
  241. package/dist/oauth/role-guard.d.ts.map +1 -1
  242. package/dist/oauth/role-guard.js +27 -5
  243. package/dist/oauth/token.repository.d.ts +2 -0
  244. package/dist/oauth/token.repository.d.ts.map +1 -1
  245. package/dist/policies.d.ts +10 -0
  246. package/dist/policies.d.ts.map +1 -0
  247. package/dist/policies.js +33 -0
  248. package/dist/queue/queue.interface.d.ts +9 -1
  249. package/dist/queue/queue.interface.d.ts.map +1 -1
  250. package/dist/queue/queue.stub.d.ts +1 -1
  251. package/dist/queue/queue.stub.d.ts.map +1 -1
  252. package/dist/queue/queue.stub.js +2 -3
  253. package/dist/queue.interface.d.ts +39 -0
  254. package/dist/queue.interface.d.ts.map +1 -0
  255. package/dist/queue.interface.js +3 -0
  256. package/dist/queue.stub.d.ts +6 -0
  257. package/dist/queue.stub.d.ts.map +1 -0
  258. package/dist/queue.stub.js +8 -0
  259. package/dist/rate-limit/token-bucket-rate-limiter.d.ts +7 -0
  260. package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -1
  261. package/dist/rate-limit/token-bucket-rate-limiter.js +66 -0
  262. package/dist/rbac/evaluate.d.ts +57 -0
  263. package/dist/rbac/evaluate.d.ts.map +1 -0
  264. package/dist/rbac/evaluate.js +113 -0
  265. package/dist/rbac/invitation.repository.d.ts +84 -0
  266. package/dist/rbac/invitation.repository.d.ts.map +1 -0
  267. package/dist/rbac/invitation.repository.js +3 -0
  268. package/dist/rbac/permissions.d.ts +39 -0
  269. package/dist/rbac/permissions.d.ts.map +1 -0
  270. package/dist/rbac/permissions.js +43 -0
  271. package/dist/rbac/types.d.ts +130 -0
  272. package/dist/rbac/types.d.ts.map +1 -0
  273. package/dist/rbac/types.js +4 -0
  274. package/dist/relations.d.ts +28 -0
  275. package/dist/relations.d.ts.map +1 -0
  276. package/dist/relations.js +25 -0
  277. package/dist/richtext-render.d.ts +11 -0
  278. package/dist/richtext-render.d.ts.map +1 -0
  279. package/dist/richtext-render.js +87 -0
  280. package/dist/richtext.d.ts +11 -0
  281. package/dist/richtext.d.ts.map +1 -0
  282. package/dist/richtext.js +13 -0
  283. package/dist/scheduler.interface.d.ts +4 -0
  284. package/dist/scheduler.interface.d.ts.map +1 -0
  285. package/dist/scheduler.interface.js +3 -0
  286. package/dist/scheduler.stub.d.ts +5 -0
  287. package/dist/scheduler.stub.d.ts.map +1 -0
  288. package/dist/scheduler.stub.js +7 -0
  289. package/dist/schema/canonical.d.ts +15 -0
  290. package/dist/schema/canonical.d.ts.map +1 -0
  291. package/dist/schema/canonical.js +37 -0
  292. package/dist/schema/define.d.ts +42 -0
  293. package/dist/schema/define.d.ts.map +1 -0
  294. package/dist/schema/define.js +56 -0
  295. package/dist/schema/emit.d.ts +14 -0
  296. package/dist/schema/emit.d.ts.map +1 -0
  297. package/dist/schema/emit.js +43 -0
  298. package/dist/schema/index.d.ts +12 -0
  299. package/dist/schema/index.d.ts.map +1 -0
  300. package/dist/schema/index.js +13 -0
  301. package/dist/schema/manifest-seeds.d.ts +12 -0
  302. package/dist/schema/manifest-seeds.d.ts.map +1 -0
  303. package/dist/schema/manifest-seeds.js +48 -0
  304. package/dist/schema/manifest-validation.d.ts +13 -0
  305. package/dist/schema/manifest-validation.d.ts.map +1 -0
  306. package/dist/schema/manifest-validation.js +41 -0
  307. package/dist/schema/manifest.types.d.ts +54 -0
  308. package/dist/schema/manifest.types.d.ts.map +1 -0
  309. package/dist/schema/manifest.types.js +7 -0
  310. package/dist/schema-mutator.d.ts +26 -0
  311. package/dist/schema-mutator.d.ts.map +1 -0
  312. package/dist/schema-mutator.js +3 -0
  313. package/dist/search/search.repository.d.ts +5 -4
  314. package/dist/search/search.repository.d.ts.map +1 -1
  315. package/dist/search/vector-extractor.d.ts.map +1 -1
  316. package/dist/search/vector-extractor.js +23 -2
  317. package/dist/search/vector.repository.d.ts +2 -1
  318. package/dist/search/vector.repository.d.ts.map +1 -1
  319. package/dist/security/time-trap-token.repository.d.ts +5 -3
  320. package/dist/security/time-trap-token.repository.d.ts.map +1 -1
  321. package/dist/seed-ddl-destructive.d.ts +40 -0
  322. package/dist/seed-ddl-destructive.d.ts.map +1 -0
  323. package/dist/seed-ddl-destructive.js +149 -0
  324. package/dist/seed-ddl.d.ts +40 -0
  325. package/dist/seed-ddl.d.ts.map +1 -0
  326. package/dist/seed-ddl.js +100 -0
  327. package/dist/seed-layout.d.ts +145 -0
  328. package/dist/seed-layout.d.ts.map +1 -0
  329. package/dist/seed-layout.js +192 -0
  330. package/dist/seed-layout.repository.d.ts +22 -0
  331. package/dist/seed-layout.repository.d.ts.map +1 -0
  332. package/dist/seed-layout.repository.js +3 -0
  333. package/dist/seed-registry.d.ts +71 -0
  334. package/dist/seed-registry.d.ts.map +1 -0
  335. package/dist/seed-registry.js +143 -0
  336. package/dist/seed-types-generator.d.ts +1 -1
  337. package/dist/seed-types-generator.d.ts.map +1 -1
  338. package/dist/seed-validation.d.ts +23 -0
  339. package/dist/seed-validation.d.ts.map +1 -0
  340. package/dist/seed-validation.js +224 -0
  341. package/dist/seed.repository.d.ts +37 -0
  342. package/dist/seed.repository.d.ts.map +1 -0
  343. package/dist/seed.repository.js +3 -0
  344. package/dist/seeds.d.ts +20 -0
  345. package/dist/seeds.d.ts.map +1 -0
  346. package/dist/seeds.js +21 -0
  347. package/dist/settings/site-settings.repository.d.ts +8 -0
  348. package/dist/settings/site-settings.repository.d.ts.map +1 -1
  349. package/dist/site-settings.repository.d.ts +16 -0
  350. package/dist/site-settings.repository.d.ts.map +1 -0
  351. package/dist/site-settings.repository.js +3 -0
  352. package/dist/slug-utils.d.ts +18 -0
  353. package/dist/slug-utils.d.ts.map +1 -0
  354. package/dist/slug-utils.js +32 -0
  355. package/dist/storage.d.ts +46 -0
  356. package/dist/storage.d.ts.map +1 -0
  357. package/dist/storage.js +3 -0
  358. package/dist/transfer/csv.d.ts +34 -0
  359. package/dist/transfer/csv.d.ts.map +1 -0
  360. package/dist/transfer/csv.js +135 -0
  361. package/dist/transfer/flat-seed.d.ts +13 -0
  362. package/dist/transfer/flat-seed.d.ts.map +1 -0
  363. package/dist/transfer/flat-seed.js +50 -0
  364. package/dist/transfer/index.d.ts +7 -0
  365. package/dist/transfer/index.d.ts.map +1 -0
  366. package/dist/transfer/index.js +8 -0
  367. package/dist/transfer/ndjson.d.ts +25 -0
  368. package/dist/transfer/ndjson.d.ts.map +1 -0
  369. package/dist/transfer/ndjson.js +62 -0
  370. package/dist/transfer/row-mapping.d.ts +36 -0
  371. package/dist/transfer/row-mapping.d.ts.map +1 -0
  372. package/dist/transfer/row-mapping.js +97 -0
  373. package/dist/transfer/transfer.constants.d.ts +20 -0
  374. package/dist/transfer/transfer.constants.d.ts.map +1 -0
  375. package/dist/transfer/transfer.constants.js +21 -0
  376. package/dist/transfer/transfer.types.d.ts +46 -0
  377. package/dist/transfer/transfer.types.d.ts.map +1 -0
  378. package/dist/transfer/transfer.types.js +6 -0
  379. package/dist/types.d.ts +243 -0
  380. package/dist/types.d.ts.map +1 -0
  381. package/dist/types.js +3 -0
  382. package/dist/validation.d.ts +50 -0
  383. package/dist/validation.d.ts.map +1 -0
  384. package/dist/validation.js +660 -0
  385. package/dist/webhook-validation.d.ts +3 -0
  386. package/dist/webhook-validation.d.ts.map +1 -0
  387. package/dist/webhook-validation.js +6 -0
  388. package/package.json +11 -6
  389. package/dist/.tsbuildinfo +0 -1
@@ -25,6 +25,10 @@ export interface FileTypeDefinition {
25
25
  readonly magicBytes?: readonly number[];
26
26
  /** Custom signature matching function for formats with complex, variable, or offset headers. */
27
27
  readonly matchSignature?: (bytes: Uint8Array) => boolean;
28
+ /** Minimum buffer length `matchSignature` needs to decide; defaults to a short generic prefix check. */
29
+ readonly minSignatureLength?: number;
30
+ /** Label for formats sharing one signature (ZIP, OLE2), used when the exact format is ambiguous. */
31
+ readonly signatureFamily?: string;
28
32
  }
29
33
  /**
30
34
  * Checks whether a byte buffer starts with a specified byte prefix.
@@ -81,6 +85,14 @@ export declare function getFileTypeByExtension(ext: string | null | undefined):
81
85
  * @returns The matching FileTypeDefinition, or undefined if unsupported.
82
86
  */
83
87
  export declare function getFileTypeByMime(mime: string | null | undefined): FileTypeDefinition | undefined;
88
+ /**
89
+ * Canonicalizes a declared MIME type to its registered primary form (e.g. 'image/jpg' -> 'image/jpeg').
90
+ * Unrecognized MIME types pass through unchanged (lowercased, parameters stripped).
91
+ *
92
+ * @param mime - The declared MIME string (e.g. 'image/jpg' or 'image/png; charset=utf-8').
93
+ * @returns The canonical primary MIME type, or the normalised input if unrecognized.
94
+ */
95
+ export declare function canonicalizeMime(mime: string): string;
84
96
  /**
85
97
  * Extracts a lowercase file extension from a URL string or query parameters.
86
98
  *
@@ -1 +1 @@
1
- {"version":3,"file":"file-types.d.ts","sourceRoot":"","sources":["../../src/media/file-types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,KAAK,CAAA;AAErD;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;AAE3D;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,qFAAqF;IACrF,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACvC,gGAAgG;IAChG,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAA;CACzD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAMrF;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,kBAAkB,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAKxF;AA6CD;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAgMnE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,WAAW,CAAC,MAAM,CAA4B,CAAA;AAErF;;GAEG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAAmC,CAAA;AAIzF,8CAA8C;AAC9C,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,CAEhD,CAAA;AAED,iDAAiD;AACjD,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAEnD,CAAA;AAED,gDAAgD;AAChD,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAElD,CAAA;AAED,0CAA0C;AAC1C,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAE7C,CAAA;AAED,6CAA6C;AAC7C,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAEhD,CAAA;AAED,4CAA4C;AAC5C,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAE/C,CAAA;AAMD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,kBAAkB,GAAG,SAAS,CAIrG;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,kBAAkB,GAAG,SAAS,CAMjG;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB3D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAO/F;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CASxE"}
1
+ {"version":3,"file":"file-types.d.ts","sourceRoot":"","sources":["../../src/media/file-types.ts"],"names":[],"mappings":"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"}
@@ -61,6 +61,7 @@ const OLE_DOCUMENT_DEFINITION = {
61
61
  isBinary: true,
62
62
  magicBytes: [...OLE_MAGIC],
63
63
  matchSignature: matchOleSignature,
64
+ signatureFamily: 'OLE2 container',
64
65
  };
65
66
  /** Common shared definition for OpenXML Office files. */
66
67
  const OPENXML_DOCUMENT_DEFINITION = {
@@ -68,6 +69,7 @@ const OPENXML_DOCUMENT_DEFINITION = {
68
69
  isBinary: true,
69
70
  magicBytes: [0x50, 0x4B, 0x03, 0x04],
70
71
  matchSignature: matchZipSignature,
72
+ signatureFamily: 'ZIP container',
71
73
  };
72
74
  /**
73
75
  * Single source of truth registry for all supported file types in BeechCMS.
@@ -97,6 +99,7 @@ export const SUPPORTED_FILE_TYPES = {
97
99
  category: 'image',
98
100
  isBinary: true,
99
101
  magicBytes: [0x47, 0x49, 0x46, 0x38],
102
+ minSignatureLength: 6,
100
103
  matchSignature: (bytes) => bytes.length >= 6 &&
101
104
  bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38 &&
102
105
  (bytes[4] === 0x37 || bytes[4] === 0x39) && bytes[5] === 0x61,
@@ -108,6 +111,7 @@ export const SUPPORTED_FILE_TYPES = {
108
111
  category: 'image',
109
112
  isBinary: true,
110
113
  magicBytes: [0x52, 0x49, 0x46, 0x46],
114
+ minSignatureLength: 12,
111
115
  matchSignature: (bytes) => bytes.length >= 12 &&
112
116
  bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 &&
113
117
  bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50,
@@ -119,6 +123,7 @@ export const SUPPORTED_FILE_TYPES = {
119
123
  category: 'image',
120
124
  isBinary: true,
121
125
  magicBytes: [0x66, 0x74, 0x79, 0x70],
126
+ minSignatureLength: 12,
122
127
  matchSignature: (bytes) => {
123
128
  if (bytes.length < 12)
124
129
  return false;
@@ -222,6 +227,7 @@ export const SUPPORTED_FILE_TYPES = {
222
227
  isBinary: true,
223
228
  magicBytes: [0x50, 0x4B, 0x03, 0x04],
224
229
  matchSignature: matchZipSignature,
230
+ signatureFamily: 'ZIP container',
225
231
  },
226
232
  '7z': {
227
233
  extension: '7z',
@@ -238,6 +244,7 @@ export const SUPPORTED_FILE_TYPES = {
238
244
  category: 'archive',
239
245
  isBinary: true,
240
246
  magicBytes: [0x75, 0x73, 0x74, 0x61, 0x72], // 'ustar'
247
+ minSignatureLength: 512,
241
248
  matchSignature: (bytes) => {
242
249
  if (bytes.length < 512)
243
250
  return false;
@@ -297,6 +304,8 @@ export function getFileTypeByExtension(ext) {
297
304
  if (!ext)
298
305
  return undefined;
299
306
  const normalised = ext.replace(/^\./, '').toLowerCase().trim();
307
+ if (!Object.hasOwn(SUPPORTED_FILE_TYPES, normalised))
308
+ return undefined;
300
309
  return SUPPORTED_FILE_TYPES[normalised];
301
310
  }
302
311
  /**
@@ -311,6 +320,17 @@ export function getFileTypeByMime(mime) {
311
320
  const normalised = mime.split(';')[0].trim().toLowerCase();
312
321
  return Object.values(SUPPORTED_FILE_TYPES).find((def) => def.mimeTypes.some((m) => m.toLowerCase() === normalised));
313
322
  }
323
+ /**
324
+ * Canonicalizes a declared MIME type to its registered primary form (e.g. 'image/jpg' -> 'image/jpeg').
325
+ * Unrecognized MIME types pass through unchanged (lowercased, parameters stripped).
326
+ *
327
+ * @param mime - The declared MIME string (e.g. 'image/jpg' or 'image/png; charset=utf-8').
328
+ * @returns The canonical primary MIME type, or the normalised input if unrecognized.
329
+ */
330
+ export function canonicalizeMime(mime) {
331
+ const normalised = mime.split(';')[0].trim().toLowerCase();
332
+ return getFileTypeByMime(normalised)?.primaryMime ?? normalised;
333
+ }
314
334
  /**
315
335
  * Extracts a lowercase file extension from a URL string or query parameters.
316
336
  *
@@ -320,10 +340,10 @@ export function getFileTypeByMime(mime) {
320
340
  export function extensionFromUrl(url) {
321
341
  try {
322
342
  const parsed = new URL(url);
323
- const path = parsed.pathname;
324
- const dot = path.lastIndexOf('.');
325
- if (dot >= 0 && dot < path.length - 1) {
326
- return path.slice(dot + 1).toLowerCase();
343
+ const segment = parsed.pathname.slice(parsed.pathname.lastIndexOf('/') + 1);
344
+ const dot = segment.lastIndexOf('.');
345
+ if (dot >= 0 && dot < segment.length - 1) {
346
+ return segment.slice(dot + 1).toLowerCase();
327
347
  }
328
348
  const param = parsed.searchParams.get('fm') || parsed.searchParams.get('format') || parsed.searchParams.get('ext');
329
349
  if (param && /^[a-z0-9]+$/i.test(param)) {
@@ -0,0 +1,34 @@
1
+ /** Pixel dimensions of an image. */
2
+ export interface MediaDimensions {
3
+ readonly width: number;
4
+ readonly height: number;
5
+ }
6
+ /** Output MIME types the transform path may emit. AVIF is deliberately absent (v1). */
7
+ export type ImageOutputMime = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
8
+ /**
9
+ * A fully resolved transformation. Every number here comes from a pre-registered preset or from
10
+ * MEDIA_QUALITY_VALUES — never from the request.
11
+ */
12
+ export interface ImageTransformSpec {
13
+ readonly width: number;
14
+ /** Present for crop presets only; scale presets derive height from the source aspect ratio. */
15
+ readonly height?: number;
16
+ readonly fit: 'cover' | 'scale-down';
17
+ readonly outputMime: ImageOutputMime;
18
+ readonly quality: number;
19
+ }
20
+ export interface TransformedImage {
21
+ readonly body: ReadableStream<Uint8Array>;
22
+ readonly contentType: string;
23
+ }
24
+ /**
25
+ * Port for edge image transformation. The API injects a Cloudflare Images adapter when the
26
+ * `IMAGES` binding exists and `null` otherwise; handlers never see the binding itself.
27
+ */
28
+ export interface IImageTransformer {
29
+ /** Reads the source's pixel dimensions. Rejects when the stream is not a raster image. */
30
+ probe(source: ReadableStream<Uint8Array>): Promise<MediaDimensions>;
31
+ /** Applies the spec and returns the encoded variant. */
32
+ transform(source: ReadableStream<Uint8Array>, spec: ImageTransformSpec): Promise<TransformedImage>;
33
+ }
34
+ //# sourceMappingURL=image-transformer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-transformer.d.ts","sourceRoot":"","sources":["../../src/media/image-transformer.ts"],"names":[],"mappings":"AAGA,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,uFAAuF;AACvF,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,YAAY,CAAA;AAErF;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAAA;IACpC,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAA;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,CAAA;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,0FAA0F;IAC1F,KAAK,CAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IACnE,wDAAwD;IACxD,SAAS,CAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;CACnG"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -1 +1 @@
1
- {"version":3,"file":"magic-bytes.d.ts","sourceRoot":"","sources":["../../src/media/magic-bytes.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAiBD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,WAAW,GAAG,UAAU,EAChC,YAAY,EAAE,MAAM,GACnB,0BAA0B,CAgD5B"}
1
+ {"version":3,"file":"magic-bytes.d.ts","sourceRoot":"","sources":["../../src/media/magic-bytes.ts"],"names":[],"mappings":"AAWA;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAkBD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,WAAW,GAAG,UAAU,EAChC,YAAY,EAAE,MAAM,GACnB,0BAA0B,CA0D5B"}
@@ -2,18 +2,19 @@
2
2
  // Copyright (c) 2024–2026 Flavio De Musso
3
3
  import { SUPPORTED_FILE_TYPES, getFileTypeByMime, BLOCKED_IMAGE_MIME_TYPES, matchesFileSignature, } from './file-types.js';
4
4
  /**
5
- * Iterates across supported binary definitions to find any matching file signature.
5
+ * Finds the binary formats matching the buffer, one per distinct MIME type.
6
6
  *
7
7
  * @param bytes - The raw byte buffer to analyze.
8
- * @returns The matching FileTypeDefinition if recognized, or undefined.
8
+ * @returns Matching definitions (empty if unrecognized).
9
9
  */
10
- function detectBinaryType(bytes) {
10
+ function detectBinaryTypes(bytes) {
11
+ const matches = new Map();
11
12
  for (const def of Object.values(SUPPORTED_FILE_TYPES)) {
12
- if (def.isBinary && matchesFileSignature(def, bytes)) {
13
- return def;
13
+ if (def.isBinary && !matches.has(def.primaryMime) && matchesFileSignature(def, bytes)) {
14
+ matches.set(def.primaryMime, def);
14
15
  }
15
16
  }
16
- return undefined;
17
+ return [...matches.values()];
17
18
  }
18
19
  /**
19
20
  * Validates that an uploaded file's binary content matches its declared MIME type,
@@ -45,7 +46,7 @@ export function verifyMagicBytes(buffer, declaredMime) {
45
46
  return { valid: true, detectedMime: declaredDef.primaryMime };
46
47
  }
47
48
  // If buffer is smaller than the minimum bytes needed for initial signature inspection (typically 4, or 2-3 for short signatures)
48
- const minInspectionLength = Math.min(4, declaredDef.magicBytes?.length ?? 4);
49
+ const minInspectionLength = declaredDef.minSignatureLength ?? Math.min(4, declaredDef.magicBytes?.length ?? 4);
49
50
  if (bytes.length < minInspectionLength) {
50
51
  return { valid: false, error: 'File buffer too small for signature inspection' };
51
52
  }
@@ -54,13 +55,22 @@ export function verifyMagicBytes(buffer, declaredMime) {
54
55
  return { valid: true, detectedMime: declaredDef.primaryMime };
55
56
  }
56
57
  // Signature mismatch: check if it matches another known binary signature
57
- const detectedDef = detectBinaryType(bytes);
58
- if (detectedDef) {
58
+ const detected = detectBinaryTypes(bytes);
59
+ if (detected.length === 1) {
60
+ const [detectedDef] = detected;
59
61
  return {
60
62
  valid: false,
61
63
  detectedMime: detectedDef.primaryMime,
62
64
  error: `Signature mismatch: file is ${detectedDef.extension.toUpperCase()} but declared as ${declaredMime}`,
63
65
  };
64
66
  }
67
+ if (detected.length > 1) {
68
+ // Shared signature: exact format unknowable
69
+ const family = detected[0].signatureFamily ?? 'shared-signature format';
70
+ return {
71
+ valid: false,
72
+ error: `Signature mismatch: file is a ${family} but declared as ${declaredMime}`,
73
+ };
74
+ }
65
75
  return { valid: false, error: `Unrecognized file signature for declared MIME ${declaredMime}` };
66
76
  }
@@ -0,0 +1,92 @@
1
+ import type { ImageOutputMime, ImageTransformSpec, MediaDimensions } from './image-transformer.js';
2
+ /** Default ceiling, in pixels, on either side of a derived variant. Covers native 32:9 ultrawide panels. */
3
+ export declare const DEFAULT_MEDIA_MAX_DIMENSION = 5120;
4
+ /** MEDIA_MAX_DIMENSION may lower the ceiling freely but raise it only this far. */
5
+ export declare const ABSOLUTE_MAX_MEDIA_DIMENSION = 8192;
6
+ export type MediaOutputFormat = 'original' | 'webp' | 'jpeg';
7
+ export type MediaQuality = 'low' | 'medium' | 'high';
8
+ export declare const MEDIA_OUTPUT_FORMATS: readonly MediaOutputFormat[];
9
+ export declare const MEDIA_QUALITIES: readonly MediaQuality[];
10
+ export declare const DEFAULT_MEDIA_FORMAT: MediaOutputFormat;
11
+ export declare const DEFAULT_MEDIA_QUALITY: MediaQuality;
12
+ /** Encoder quality per enum value. `medium` ≈ 82 per the brief. */
13
+ export declare const MEDIA_QUALITY_VALUES: Readonly<Record<MediaQuality, number>>;
14
+ export type MediaPreset = {
15
+ readonly kind: 'crop';
16
+ readonly width: number;
17
+ readonly height: number;
18
+ } | {
19
+ readonly kind: 'scale';
20
+ readonly width: number;
21
+ };
22
+ export type MediaPresetCatalog = ReadonlyMap<string, MediaPreset>;
23
+ export declare const DEFAULT_MEDIA_SCALE_WIDTHS: readonly [320, 480, 640, 768, 1024, 1280, 1536, 1920, 2560, 3840, 5120];
24
+ export declare const DEFAULT_MEDIA_PRESETS: Readonly<Record<string, MediaPreset>>;
25
+ /** Raster sources the transform path accepts. Everything else — SVG, PDF, video, AVIF, BMP, ICO — is refused. */
26
+ export declare const TRANSFORMABLE_MEDIA_MIME_TYPES: readonly ImageOutputMime[];
27
+ /** Parameters of the discarded free-form contract. Their presence is an error, never ignored. */
28
+ export declare const FORBIDDEN_MEDIA_TRANSFORM_PARAMS: readonly string[];
29
+ export type MediaTransformErrorCode = 'media_param_forbidden' | 'media_param_duplicated' | 'media_preset_required' | 'media_format_invalid' | 'media_quality_invalid' | 'media_preset_unknown' | 'media_not_transformable' | 'media_dimension_exceeded';
30
+ export interface MediaTransformRequest {
31
+ readonly preset: string;
32
+ readonly format: MediaOutputFormat;
33
+ readonly quality: MediaQuality;
34
+ }
35
+ export type MediaTransformQuery = {
36
+ readonly kind: 'none';
37
+ } | {
38
+ readonly kind: 'invalid';
39
+ readonly code: MediaTransformErrorCode;
40
+ } | {
41
+ readonly kind: 'transform';
42
+ readonly request: MediaTransformRequest;
43
+ };
44
+ /** Thrown by buildMediaPresetCatalog on any malformed MEDIA_PRESETS entry. Fail closed, never fall back. */
45
+ export declare class MediaPresetCatalogError extends Error {
46
+ readonly reason: string;
47
+ constructor(reason: string);
48
+ }
49
+ /**
50
+ * Classifies a media query string. Pure: does not consult the catalog (that is env-dependent and
51
+ * resolved by the caller), so an unknown-but-well-formed preset name returns `transform`.
52
+ */
53
+ export declare function parseMediaTransformQuery(params: URLSearchParams): MediaTransformQuery;
54
+ /**
55
+ * Canonical query for a request: fixed order preset → format → quality, defaults omitted.
56
+ * `@beechcms/client` emits exactly this string; the server recomputes it and never trusts the client's.
57
+ */
58
+ export declare function canonicalMediaTransformQuery(request: MediaTransformRequest): string;
59
+ /**
60
+ * Builds the active catalog: DEFAULT_MEDIA_PRESETS merged by name with `overrides`
61
+ * (the parsed MEDIA_PRESETS JSON). An override value of `null` removes a preset.
62
+ * Defaults larger than `maxDimension` are dropped (they could only ever 400); an operator-supplied
63
+ * preset larger than `maxDimension` is an error.
64
+ *
65
+ * @throws {MediaPresetCatalogError} on any malformed override.
66
+ */
67
+ export declare function buildMediaPresetCatalog(overrides: unknown, maxDimension: number): MediaPresetCatalog;
68
+ export declare function isTransformableMime(mime: string | null | undefined): boolean;
69
+ /**
70
+ * Output dimensions of a scale preset (`fit=scale-down`: never upscales). Returns `null` when either
71
+ * side would exceed `maxDimension` — the pathological-aspect-ratio guard of brief §4. Never truncates.
72
+ */
73
+ export declare function deriveScaleOutput(presetWidth: number, source: MediaDimensions, maxDimension: number): MediaDimensions | null;
74
+ /**
75
+ * Identity of a preset's *definition*, so redefining a name never reuses an old variant.
76
+ * A scale preset's validity depends on the effective dimension ceiling (its height derives from the
77
+ * source), so the ceiling is part of its identity; a crop preset is validated at catalog build.
78
+ */
79
+ export declare function mediaPresetSignature(preset: MediaPreset, maxDimension: number): string;
80
+ /** Precondition: `isTransformableMime(sourceMime)` is true. */
81
+ export declare function buildImageTransformSpec(preset: MediaPreset, request: MediaTransformRequest, sourceMime: string): ImageTransformSpec;
82
+ /**
83
+ * Strong ETag of a variant: source identity (key + size — keys are never overwritten) plus the
84
+ * canonical request plus the preset definition (and the ceiling for scale presets). Format: `"mv1-<32 hex>"`.
85
+ */
86
+ export declare function computeMediaVariantEtag(source: {
87
+ readonly key: string;
88
+ readonly size: number;
89
+ }, request: MediaTransformRequest, preset: MediaPreset, maxDimension: number): Promise<string>;
90
+ /** RFC 9110 §13.1.2: `*` matches; otherwise weak comparison against a comma-separated list. */
91
+ export declare function ifNoneMatchMatches(header: string | null | undefined, etag: string | null | undefined): boolean;
92
+ //# sourceMappingURL=media-transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-transform.d.ts","sourceRoot":"","sources":["../../src/media/media-transform.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAElG,4GAA4G;AAC5G,eAAO,MAAM,2BAA2B,OAAO,CAAA;AAC/C,mFAAmF;AACnF,eAAO,MAAM,4BAA4B,OAAO,CAAA;AAEhD,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAA;AAC5D,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEpD,eAAO,MAAM,oBAAoB,EAAE,SAAS,iBAAiB,EAAiC,CAAA;AAC9F,eAAO,MAAM,eAAe,EAAE,SAAS,YAAY,EAA8B,CAAA;AACjF,eAAO,MAAM,oBAAoB,EAAE,iBAA8B,CAAA;AACjE,eAAO,MAAM,qBAAqB,EAAE,YAAuB,CAAA;AAC3D,mEAAmE;AACnE,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAoD,CAAA;AAE5H,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1E;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtD,MAAM,MAAM,kBAAkB,GAAG,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;AAEjE,eAAO,MAAM,0BAA0B,yEAA0E,CAAA;AAEjH,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAOtE,CAAA;AAEF,iHAAiH;AACjH,eAAO,MAAM,8BAA8B,EAAE,SAAS,eAAe,EAA2D,CAAA;AAEhI,iGAAiG;AACjG,eAAO,MAAM,gCAAgC,EAAE,SAAS,MAAM,EAA8C,CAAA;AAE5G,MAAM,MAAM,uBAAuB,GAC/B,uBAAuB,GACvB,wBAAwB,GACxB,uBAAuB,GACvB,sBAAsB,GACtB,uBAAuB,GACvB,sBAAsB,GACtB,yBAAyB,GACzB,0BAA0B,CAAA;AAE9B,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;IAClC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;CAC/B;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAA;CAAE,GACpE;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAA;CAAE,CAAA;AAE3E,4GAA4G;AAC5G,qBAAa,uBAAwB,SAAQ,KAAK;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM;CAIpC;AAOD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,mBAAmB,CAqBrF;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,CAKnF;AAsCD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,kBAAkB,CAoBpG;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAG5E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAM5H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED,+DAA+D;AAC/D,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,GAAG,kBAAkB,CAUnI;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACvD,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,WAAW,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED,+FAA+F;AAC/F,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAO9G"}
@@ -0,0 +1,197 @@
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
+ /**
161
+ * Identity of a preset's *definition*, so redefining a name never reuses an old variant.
162
+ * A scale preset's validity depends on the effective dimension ceiling (its height derives from the
163
+ * source), so the ceiling is part of its identity; a crop preset is validated at catalog build.
164
+ */
165
+ export function mediaPresetSignature(preset, maxDimension) {
166
+ return preset.kind === 'crop' ? `crop:${preset.width}x${preset.height}` : `scale:${preset.width}@${maxDimension}`;
167
+ }
168
+ /** Precondition: `isTransformableMime(sourceMime)` is true. */
169
+ export function buildImageTransformSpec(preset, request, sourceMime) {
170
+ const outputMime = request.format === 'webp'
171
+ ? 'image/webp'
172
+ : request.format === 'jpeg'
173
+ ? 'image/jpeg'
174
+ : canonicalizeMime(sourceMime);
175
+ const quality = MEDIA_QUALITY_VALUES[request.quality];
176
+ return preset.kind === 'crop'
177
+ ? { width: preset.width, height: preset.height, fit: 'cover', outputMime, quality }
178
+ : { width: preset.width, fit: 'scale-down', outputMime, quality };
179
+ }
180
+ /**
181
+ * Strong ETag of a variant: source identity (key + size — keys are never overwritten) plus the
182
+ * canonical request plus the preset definition (and the ceiling for scale presets). Format: `"mv1-<32 hex>"`.
183
+ */
184
+ export async function computeMediaVariantEtag(source, request, preset, maxDimension) {
185
+ const digest = await sha256hex(`${source.key}\n${source.size}\n${canonicalMediaTransformQuery(request)}\n${mediaPresetSignature(preset, maxDimension)}`);
186
+ return `"mv1-${digest.slice(0, 32)}"`;
187
+ }
188
+ /** RFC 9110 §13.1.2: `*` matches; otherwise weak comparison against a comma-separated list. */
189
+ export function ifNoneMatchMatches(header, etag) {
190
+ if (!header || !etag)
191
+ return false;
192
+ const target = etag.replace(/^W\//, '');
193
+ return header.split(',').some((candidate) => {
194
+ const trimmed = candidate.trim();
195
+ return trimmed === '*' || trimmed.replace(/^W\//, '') === target;
196
+ });
197
+ }
@@ -14,6 +14,11 @@ export interface MediaRepository {
14
14
  * Tracks a new file upload in the database.
15
15
  */
16
16
  trackUpload(media: Omit<MediaObject, 'created_at'>): Promise<void>;
17
+ /**
18
+ * Registers a file and counts its bytes toward total storage in one atomic step.
19
+ * Returns false, counting nothing, when the key is already registered.
20
+ */
21
+ registerUpload(media: Omit<MediaObject, 'created_at'>): Promise<boolean>;
17
22
  /**
18
23
  * Retrieves tracking info for a file by its key.
19
24
  */
@@ -1 +1 @@
1
- {"version":3,"file":"media.repository.d.ts","sourceRoot":"","sources":["../../src/media/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"}
1
+ {"version":3,"file":"media.repository.d.ts","sourceRoot":"","sources":["../../src/media/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;;;OAGG;IACH,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEzE;;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,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 {};