@beechcms/core 0.7.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 (370) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/antivirus/antivirus.interface.d.ts +12 -0
  3. package/dist/antivirus/antivirus.interface.d.ts.map +1 -0
  4. package/dist/antivirus/antivirus.interface.js +3 -0
  5. package/dist/antivirus/noop-antivirus.provider.d.ts +6 -0
  6. package/dist/antivirus/noop-antivirus.provider.d.ts.map +1 -0
  7. package/dist/antivirus/noop-antivirus.provider.js +8 -0
  8. package/dist/antivirus/virustotal-antivirus.provider.d.ts +8 -0
  9. package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -0
  10. package/dist/antivirus/virustotal-antivirus.provider.js +48 -0
  11. package/dist/automations-grammar-words.d.ts +2 -0
  12. package/dist/automations-grammar-words.d.ts.map +1 -0
  13. package/dist/automations-grammar-words.js +8 -0
  14. package/dist/automations.repository.interface.d.ts +19 -0
  15. package/dist/automations.repository.interface.d.ts.map +1 -0
  16. package/dist/automations.repository.interface.js +3 -0
  17. package/dist/automations.runner.interface.d.ts +10 -0
  18. package/dist/automations.runner.interface.d.ts.map +1 -0
  19. package/dist/automations.runner.interface.js +3 -0
  20. package/dist/automations.runner.stub.d.ts +5 -0
  21. package/dist/automations.runner.stub.d.ts.map +1 -0
  22. package/dist/automations.runner.stub.js +7 -0
  23. package/dist/automations.types.d.ts +75 -0
  24. package/dist/automations.types.d.ts.map +1 -0
  25. package/dist/automations.types.js +3 -0
  26. package/dist/clock.d.ts +25 -0
  27. package/dist/clock.d.ts.map +1 -0
  28. package/dist/clock.js +11 -0
  29. package/dist/common/canonical-json.d.ts +30 -0
  30. package/dist/common/canonical-json.d.ts.map +1 -0
  31. package/dist/common/canonical-json.js +89 -0
  32. package/dist/common/hooks.d.ts +3 -0
  33. package/dist/common/hooks.d.ts.map +1 -1
  34. package/dist/content/content.repository.d.ts +2 -2
  35. package/dist/content/content.repository.d.ts.map +1 -1
  36. package/dist/content/deletion-ledger.d.ts +39 -0
  37. package/dist/content/deletion-ledger.d.ts.map +1 -0
  38. package/dist/content/deletion-ledger.js +3 -0
  39. package/dist/content/richtext/richtext-render.d.ts.map +1 -1
  40. package/dist/content/richtext/richtext-render.js +14 -1
  41. package/dist/content/seed.repository.d.ts +22 -0
  42. package/dist/content/seed.repository.d.ts.map +1 -1
  43. package/dist/content-scan.repository.d.ts +9 -0
  44. package/dist/content-scan.repository.d.ts.map +1 -0
  45. package/dist/content-scan.repository.js +3 -0
  46. package/dist/content.repository.d.ts +247 -0
  47. package/dist/content.repository.d.ts.map +1 -0
  48. package/dist/content.repository.js +59 -0
  49. package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
  50. package/dist/dashboard-layout/seed-layout.js +13 -6
  51. package/dist/dashboard-layout.d.ts +154 -0
  52. package/dist/dashboard-layout.d.ts.map +1 -0
  53. package/dist/dashboard-layout.js +186 -0
  54. package/dist/dashboard-layout.repository.d.ts +18 -0
  55. package/dist/dashboard-layout.repository.d.ts.map +1 -0
  56. package/dist/dashboard-layout.repository.js +3 -0
  57. package/dist/dashboard-permissions.d.ts +3 -0
  58. package/dist/dashboard-permissions.d.ts.map +1 -0
  59. package/dist/dashboard-permissions.js +11 -0
  60. package/dist/dashboard-scopes.d.ts +12 -0
  61. package/dist/dashboard-scopes.d.ts.map +1 -0
  62. package/dist/dashboard-scopes.js +30 -0
  63. package/dist/define-seed.d.ts +3 -0
  64. package/dist/define-seed.d.ts.map +1 -0
  65. package/dist/define-seed.js +5 -0
  66. package/dist/demo-data.repository.d.ts +5 -0
  67. package/dist/demo-data.repository.d.ts.map +1 -0
  68. package/dist/demo-data.repository.js +3 -0
  69. package/dist/email.provider.d.ts +32 -0
  70. package/dist/email.provider.d.ts.map +1 -0
  71. package/dist/email.provider.js +4 -0
  72. package/dist/engine/ddl/ddl.d.ts +216 -0
  73. package/dist/engine/ddl/ddl.d.ts.map +1 -0
  74. package/dist/engine/ddl/ddl.js +574 -0
  75. package/dist/engine/ddl/schema-mutator.d.ts +26 -0
  76. package/dist/engine/ddl/schema-mutator.d.ts.map +1 -0
  77. package/dist/engine/ddl/schema-mutator.js +3 -0
  78. package/dist/engine/ddl/seed-ddl-destructive.d.ts +40 -0
  79. package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +1 -0
  80. package/dist/engine/ddl/seed-ddl-destructive.js +149 -0
  81. package/dist/engine/ddl/seed-ddl.d.ts +43 -0
  82. package/dist/engine/ddl/seed-ddl.d.ts.map +1 -0
  83. package/dist/engine/ddl/seed-ddl.js +115 -0
  84. package/dist/engine/ddl.d.ts +15 -0
  85. package/dist/engine/ddl.d.ts.map +1 -1
  86. package/dist/engine/ddl.js +30 -1
  87. package/dist/engine/introspection/introspection.d.ts +101 -0
  88. package/dist/engine/introspection/introspection.d.ts.map +1 -0
  89. package/dist/engine/introspection/introspection.js +127 -0
  90. package/dist/engine/introspection/schema-fingerprint.d.ts +73 -0
  91. package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -0
  92. package/dist/engine/introspection/schema-fingerprint.js +97 -0
  93. package/dist/engine/introspection.d.ts +101 -0
  94. package/dist/engine/introspection.d.ts.map +1 -0
  95. package/dist/engine/introspection.js +127 -0
  96. package/dist/engine/localization/localization.d.ts +127 -0
  97. package/dist/engine/localization/localization.d.ts.map +1 -0
  98. package/dist/engine/localization/localization.js +212 -0
  99. package/dist/engine/localization.d.ts +127 -0
  100. package/dist/engine/localization.d.ts.map +1 -0
  101. package/dist/engine/localization.js +212 -0
  102. package/dist/engine/policies.d.ts.map +1 -1
  103. package/dist/engine/policies.js +5 -0
  104. package/dist/engine/privacy/policies.d.ts +58 -0
  105. package/dist/engine/privacy/policies.d.ts.map +1 -0
  106. package/dist/engine/privacy/policies.js +185 -0
  107. package/dist/engine/privacy/privacy.service.d.ts +65 -0
  108. package/dist/engine/privacy/privacy.service.d.ts.map +1 -0
  109. package/dist/engine/privacy/privacy.service.js +114 -0
  110. package/dist/engine/privacy.service.d.ts.map +1 -1
  111. package/dist/engine/query/query.d.ts +23 -0
  112. package/dist/engine/query/query.d.ts.map +1 -0
  113. package/dist/engine/query/query.js +301 -0
  114. package/dist/engine/query/relations.d.ts +28 -0
  115. package/dist/engine/query/relations.d.ts.map +1 -0
  116. package/dist/engine/query/relations.js +25 -0
  117. package/dist/engine/query/serialize.d.ts +22 -0
  118. package/dist/engine/query/serialize.d.ts.map +1 -0
  119. package/dist/engine/query/serialize.js +175 -0
  120. package/dist/engine/schema-fingerprint.d.ts +73 -0
  121. package/dist/engine/schema-fingerprint.d.ts.map +1 -0
  122. package/dist/engine/schema-fingerprint.js +97 -0
  123. package/dist/engine/seed-registry.d.ts.map +1 -1
  124. package/dist/engine/seed-registry.js +23 -4
  125. package/dist/engine/seed-types-generator.d.ts.map +1 -1
  126. package/dist/engine/seed-types-generator.js +8 -5
  127. package/dist/engine/seed-validation.d.ts +7 -0
  128. package/dist/engine/seed-validation.d.ts.map +1 -1
  129. package/dist/engine/seed-validation.js +59 -1
  130. package/dist/engine/seeds/define-seed.d.ts +3 -0
  131. package/dist/engine/seeds/define-seed.d.ts.map +1 -0
  132. package/dist/engine/seeds/define-seed.js +5 -0
  133. package/dist/engine/seeds/seed-registry.d.ts +71 -0
  134. package/dist/engine/seeds/seed-registry.d.ts.map +1 -0
  135. package/dist/engine/seeds/seed-registry.js +167 -0
  136. package/dist/engine/seeds/seed-types-generator.d.ts +25 -0
  137. package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -0
  138. package/dist/engine/seeds/seed-types-generator.js +117 -0
  139. package/dist/engine/seeds/seed-validation.d.ts +30 -0
  140. package/dist/engine/seeds/seed-validation.d.ts.map +1 -0
  141. package/dist/engine/seeds/seed-validation.js +364 -0
  142. package/dist/engine/seeds/seeds.d.ts +20 -0
  143. package/dist/engine/seeds/seeds.d.ts.map +1 -0
  144. package/dist/engine/seeds/seeds.js +21 -0
  145. package/dist/engine/types.d.ts +7 -0
  146. package/dist/engine/types.d.ts.map +1 -1
  147. package/dist/engine/validation/emptiness.d.ts +12 -0
  148. package/dist/engine/validation/emptiness.d.ts.map +1 -0
  149. package/dist/engine/validation/emptiness.js +33 -0
  150. package/dist/engine/validation/index.d.ts +1 -1
  151. package/dist/engine/validation/index.d.ts.map +1 -1
  152. package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
  153. package/dist/engine/validation/richtext-sanitizer.js +2 -1
  154. package/dist/engine.d.ts +144 -0
  155. package/dist/engine.d.ts.map +1 -0
  156. package/dist/engine.js +741 -0
  157. package/dist/file-types.d.ts +13 -0
  158. package/dist/file-types.d.ts.map +1 -0
  159. package/dist/file-types.js +85 -0
  160. package/dist/hooks.d.ts +29 -0
  161. package/dist/hooks.d.ts.map +1 -0
  162. package/dist/hooks.js +3 -0
  163. package/dist/id-generator.d.ts +26 -0
  164. package/dist/id-generator.d.ts.map +1 -0
  165. package/dist/id-generator.js +12 -0
  166. package/dist/idempotency.repository.d.ts +13 -0
  167. package/dist/idempotency.repository.d.ts.map +1 -0
  168. package/dist/idempotency.repository.js +3 -0
  169. package/dist/index.d.ts +16 -1
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +16 -1
  172. package/dist/kanban-position.repository.d.ts +19 -0
  173. package/dist/kanban-position.repository.d.ts.map +1 -0
  174. package/dist/kanban-position.repository.js +3 -0
  175. package/dist/kanban.d.ts +75 -0
  176. package/dist/kanban.d.ts.map +1 -0
  177. package/dist/kanban.js +82 -0
  178. package/dist/layout-permissions.d.ts +3 -0
  179. package/dist/layout-permissions.d.ts.map +1 -0
  180. package/dist/layout-permissions.js +11 -0
  181. package/dist/media/file-types.d.ts +103 -9
  182. package/dist/media/file-types.d.ts.map +1 -1
  183. package/dist/media/file-types.js +336 -52
  184. package/dist/media/image-transformer.d.ts +34 -0
  185. package/dist/media/image-transformer.d.ts.map +1 -0
  186. package/dist/media/image-transformer.js +3 -0
  187. package/dist/media/magic-bytes.d.ts +21 -0
  188. package/dist/media/magic-bytes.d.ts.map +1 -0
  189. package/dist/media/magic-bytes.js +66 -0
  190. package/dist/media/media-transform.d.ts +88 -0
  191. package/dist/media/media-transform.d.ts.map +1 -0
  192. package/dist/media/media-transform.js +193 -0
  193. package/dist/media.repository.d.ts +61 -0
  194. package/dist/media.repository.d.ts.map +1 -0
  195. package/dist/media.repository.js +3 -0
  196. package/dist/notifications/notification-service.d.ts +1 -1
  197. package/dist/oauth/authorization-code.repository.d.ts +33 -0
  198. package/dist/oauth/authorization-code.repository.d.ts.map +1 -0
  199. package/dist/oauth/authorization-code.repository.js +3 -0
  200. package/dist/oauth/client.repository.d.ts +17 -0
  201. package/dist/oauth/client.repository.d.ts.map +1 -0
  202. package/dist/oauth/client.repository.js +3 -0
  203. package/dist/oauth/consent.repository.d.ts +25 -0
  204. package/dist/oauth/consent.repository.d.ts.map +1 -0
  205. package/dist/oauth/consent.repository.js +3 -0
  206. package/dist/oauth/pkce.d.ts +21 -0
  207. package/dist/oauth/pkce.d.ts.map +1 -0
  208. package/dist/oauth/pkce.js +47 -0
  209. package/dist/oauth/role-guard.d.ts +32 -0
  210. package/dist/oauth/role-guard.d.ts.map +1 -0
  211. package/dist/oauth/role-guard.js +14 -0
  212. package/dist/oauth/scopes.d.ts +27 -0
  213. package/dist/oauth/scopes.d.ts.map +1 -0
  214. package/dist/oauth/scopes.js +41 -0
  215. package/dist/oauth/token.repository.d.ts +46 -0
  216. package/dist/oauth/token.repository.d.ts.map +1 -0
  217. package/dist/oauth/token.repository.js +3 -0
  218. package/dist/observability/activity-logger.d.ts +1 -1
  219. package/dist/observability/activity-logger.d.ts.map +1 -1
  220. package/dist/policies.d.ts +10 -0
  221. package/dist/policies.d.ts.map +1 -0
  222. package/dist/policies.js +33 -0
  223. package/dist/queue/queue.stub.d.ts.map +1 -1
  224. package/dist/queue/scheduler.stub.d.ts.map +1 -1
  225. package/dist/queue.interface.d.ts +39 -0
  226. package/dist/queue.interface.d.ts.map +1 -0
  227. package/dist/queue.interface.js +3 -0
  228. package/dist/queue.stub.d.ts +6 -0
  229. package/dist/queue.stub.d.ts.map +1 -0
  230. package/dist/queue.stub.js +8 -0
  231. package/dist/rate-limit/rate-limiter.d.ts +13 -5
  232. package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
  233. package/dist/rate-limit/token-bucket-rate-limiter.d.ts +20 -0
  234. package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -0
  235. package/dist/rate-limit/token-bucket-rate-limiter.js +63 -0
  236. package/dist/rbac/evaluate.d.ts +57 -0
  237. package/dist/rbac/evaluate.d.ts.map +1 -0
  238. package/dist/rbac/evaluate.js +113 -0
  239. package/dist/rbac/invitation.repository.d.ts +66 -0
  240. package/dist/rbac/invitation.repository.d.ts.map +1 -0
  241. package/dist/rbac/invitation.repository.js +3 -0
  242. package/dist/rbac/permissions.d.ts +39 -0
  243. package/dist/rbac/permissions.d.ts.map +1 -0
  244. package/dist/rbac/permissions.js +43 -0
  245. package/dist/rbac/types.d.ts +118 -0
  246. package/dist/rbac/types.d.ts.map +1 -0
  247. package/dist/rbac/types.js +3 -0
  248. package/dist/relations.d.ts +28 -0
  249. package/dist/relations.d.ts.map +1 -0
  250. package/dist/relations.js +25 -0
  251. package/dist/richtext-render.d.ts +11 -0
  252. package/dist/richtext-render.d.ts.map +1 -0
  253. package/dist/richtext-render.js +87 -0
  254. package/dist/richtext.d.ts +11 -0
  255. package/dist/richtext.d.ts.map +1 -0
  256. package/dist/richtext.js +13 -0
  257. package/dist/scheduler.interface.d.ts +4 -0
  258. package/dist/scheduler.interface.d.ts.map +1 -0
  259. package/dist/scheduler.interface.js +3 -0
  260. package/dist/scheduler.stub.d.ts +5 -0
  261. package/dist/scheduler.stub.d.ts.map +1 -0
  262. package/dist/scheduler.stub.js +7 -0
  263. package/dist/schema/canonical.d.ts +15 -0
  264. package/dist/schema/canonical.d.ts.map +1 -0
  265. package/dist/schema/canonical.js +37 -0
  266. package/dist/schema/define.d.ts +42 -0
  267. package/dist/schema/define.d.ts.map +1 -0
  268. package/dist/schema/define.js +56 -0
  269. package/dist/schema/emit.d.ts +14 -0
  270. package/dist/schema/emit.d.ts.map +1 -0
  271. package/dist/schema/emit.js +43 -0
  272. package/dist/schema/index.d.ts +12 -0
  273. package/dist/schema/index.d.ts.map +1 -0
  274. package/dist/schema/index.js +13 -0
  275. package/dist/schema/manifest-seeds.d.ts +12 -0
  276. package/dist/schema/manifest-seeds.d.ts.map +1 -0
  277. package/dist/schema/manifest-seeds.js +48 -0
  278. package/dist/schema/manifest-validation.d.ts +13 -0
  279. package/dist/schema/manifest-validation.d.ts.map +1 -0
  280. package/dist/schema/manifest-validation.js +41 -0
  281. package/dist/schema/manifest.types.d.ts +54 -0
  282. package/dist/schema/manifest.types.d.ts.map +1 -0
  283. package/dist/schema/manifest.types.js +7 -0
  284. package/dist/schema-mutator.d.ts +26 -0
  285. package/dist/schema-mutator.d.ts.map +1 -0
  286. package/dist/schema-mutator.js +3 -0
  287. package/dist/search/vector-extractor.d.ts +11 -0
  288. package/dist/search/vector-extractor.d.ts.map +1 -0
  289. package/dist/search/vector-extractor.js +25 -0
  290. package/dist/search/vector.repository.d.ts +13 -0
  291. package/dist/search/vector.repository.d.ts.map +1 -0
  292. package/dist/search/vector.repository.js +3 -0
  293. package/dist/security/time-trap-token.repository.d.ts +9 -0
  294. package/dist/security/time-trap-token.repository.d.ts.map +1 -0
  295. package/dist/security/time-trap-token.repository.js +3 -0
  296. package/dist/security/time-trap.d.ts +7 -0
  297. package/dist/security/time-trap.d.ts.map +1 -0
  298. package/dist/security/time-trap.js +49 -0
  299. package/dist/seed-ddl-destructive.d.ts +40 -0
  300. package/dist/seed-ddl-destructive.d.ts.map +1 -0
  301. package/dist/seed-ddl-destructive.js +149 -0
  302. package/dist/seed-ddl.d.ts +40 -0
  303. package/dist/seed-ddl.d.ts.map +1 -0
  304. package/dist/seed-ddl.js +100 -0
  305. package/dist/seed-layout.d.ts +145 -0
  306. package/dist/seed-layout.d.ts.map +1 -0
  307. package/dist/seed-layout.js +192 -0
  308. package/dist/seed-layout.repository.d.ts +22 -0
  309. package/dist/seed-layout.repository.d.ts.map +1 -0
  310. package/dist/seed-layout.repository.js +3 -0
  311. package/dist/seed-registry.d.ts +71 -0
  312. package/dist/seed-registry.d.ts.map +1 -0
  313. package/dist/seed-registry.js +143 -0
  314. package/dist/seed-types-generator.d.ts +1 -1
  315. package/dist/seed-types-generator.d.ts.map +1 -1
  316. package/dist/seed-validation.d.ts +23 -0
  317. package/dist/seed-validation.d.ts.map +1 -0
  318. package/dist/seed-validation.js +224 -0
  319. package/dist/seed.repository.d.ts +37 -0
  320. package/dist/seed.repository.d.ts.map +1 -0
  321. package/dist/seed.repository.js +3 -0
  322. package/dist/seeds.d.ts +20 -0
  323. package/dist/seeds.d.ts.map +1 -0
  324. package/dist/seeds.js +21 -0
  325. package/dist/site-settings.repository.d.ts +16 -0
  326. package/dist/site-settings.repository.d.ts.map +1 -0
  327. package/dist/site-settings.repository.js +3 -0
  328. package/dist/slug-utils.d.ts +18 -0
  329. package/dist/slug-utils.d.ts.map +1 -0
  330. package/dist/slug-utils.js +32 -0
  331. package/dist/storage.d.ts +46 -0
  332. package/dist/storage.d.ts.map +1 -0
  333. package/dist/storage.js +3 -0
  334. package/dist/transfer/csv.d.ts +34 -0
  335. package/dist/transfer/csv.d.ts.map +1 -0
  336. package/dist/transfer/csv.js +135 -0
  337. package/dist/transfer/flat-seed.d.ts +13 -0
  338. package/dist/transfer/flat-seed.d.ts.map +1 -0
  339. package/dist/transfer/flat-seed.js +45 -0
  340. package/dist/transfer/index.d.ts +7 -0
  341. package/dist/transfer/index.d.ts.map +1 -0
  342. package/dist/transfer/index.js +8 -0
  343. package/dist/transfer/ndjson.d.ts +25 -0
  344. package/dist/transfer/ndjson.d.ts.map +1 -0
  345. package/dist/transfer/ndjson.js +62 -0
  346. package/dist/transfer/row-mapping.d.ts +36 -0
  347. package/dist/transfer/row-mapping.d.ts.map +1 -0
  348. package/dist/transfer/row-mapping.js +90 -0
  349. package/dist/transfer/transfer.constants.d.ts +20 -0
  350. package/dist/transfer/transfer.constants.d.ts.map +1 -0
  351. package/dist/transfer/transfer.constants.js +21 -0
  352. package/dist/transfer/transfer.types.d.ts +46 -0
  353. package/dist/transfer/transfer.types.d.ts.map +1 -0
  354. package/dist/transfer/transfer.types.js +6 -0
  355. package/dist/types.d.ts +243 -0
  356. package/dist/types.d.ts.map +1 -0
  357. package/dist/types.js +3 -0
  358. package/dist/validation.d.ts +50 -0
  359. package/dist/validation.d.ts.map +1 -0
  360. package/dist/validation.js +660 -0
  361. package/dist/webhook-crypto.d.ts +2 -0
  362. package/dist/webhook-crypto.d.ts.map +1 -1
  363. package/dist/webhook-crypto.js +2 -2
  364. package/dist/webhook-validation.d.ts +3 -0
  365. package/dist/webhook-validation.d.ts.map +1 -0
  366. package/dist/webhook-validation.js +6 -0
  367. package/dist/webhooks/webhook-crypto.d.ts +2 -0
  368. package/dist/webhooks/webhook-crypto.d.ts.map +1 -1
  369. package/dist/webhooks/webhook-crypto.js +2 -2
  370. package/package.json +23 -15
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { SUPPORTED_FILE_TYPES, getFileTypeByMime, BLOCKED_IMAGE_MIME_TYPES, matchesFileSignature, } from './file-types.js';
4
+ /**
5
+ * Iterates across supported binary definitions to find any matching file signature.
6
+ *
7
+ * @param bytes - The raw byte buffer to analyze.
8
+ * @returns The matching FileTypeDefinition if recognized, or undefined.
9
+ */
10
+ function detectBinaryType(bytes) {
11
+ for (const def of Object.values(SUPPORTED_FILE_TYPES)) {
12
+ if (def.isBinary && matchesFileSignature(def, bytes)) {
13
+ return def;
14
+ }
15
+ }
16
+ return undefined;
17
+ }
18
+ /**
19
+ * Validates that an uploaded file's binary content matches its declared MIME type,
20
+ * protecting against MIME-spoofing, disguised executables, and blocked formats (e.g. SVG).
21
+ *
22
+ * @param buffer - The raw binary buffer (ArrayBuffer or Uint8Array).
23
+ * @param declaredMime - The client-declared MIME type string.
24
+ * @returns A MagicBytesValidationResult indicating validity and details.
25
+ */
26
+ export function verifyMagicBytes(buffer, declaredMime) {
27
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
28
+ if (bytes.length < 2) {
29
+ return { valid: false, error: 'File buffer too small for signature inspection' };
30
+ }
31
+ const normalizedDeclared = declaredMime.split(';')[0].trim().toLowerCase();
32
+ // Unconditionally reject blocked MIME types (e.g. SVG)
33
+ if (BLOCKED_IMAGE_MIME_TYPES.includes(normalizedDeclared)) {
34
+ return {
35
+ valid: false,
36
+ error: `File type '${declaredMime}' is blocked for security reasons`,
37
+ };
38
+ }
39
+ const declaredDef = getFileTypeByMime(normalizedDeclared);
40
+ if (!declaredDef) {
41
+ return { valid: false, error: `Unrecognized file signature for declared MIME ${declaredMime}` };
42
+ }
43
+ // Non-binary types (e.g. text/plain, text/csv, text/markdown, application/json)
44
+ if (!declaredDef.isBinary) {
45
+ return { valid: true, detectedMime: declaredDef.primaryMime };
46
+ }
47
+ // 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
+ if (bytes.length < minInspectionLength) {
50
+ return { valid: false, error: 'File buffer too small for signature inspection' };
51
+ }
52
+ // Binary types: verify signature against declared definition
53
+ if (matchesFileSignature(declaredDef, bytes)) {
54
+ return { valid: true, detectedMime: declaredDef.primaryMime };
55
+ }
56
+ // Signature mismatch: check if it matches another known binary signature
57
+ const detectedDef = detectBinaryType(bytes);
58
+ if (detectedDef) {
59
+ return {
60
+ valid: false,
61
+ detectedMime: detectedDef.primaryMime,
62
+ error: `Signature mismatch: file is ${detectedDef.extension.toUpperCase()} but declared as ${declaredMime}`,
63
+ };
64
+ }
65
+ return { valid: false, error: `Unrecognized file signature for declared MIME ${declaredMime}` };
66
+ }
@@ -0,0 +1,88 @@
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
+ /** Identity of a preset's *definition*, so redefining a name never reuses an old variant. */
75
+ export declare function mediaPresetSignature(preset: MediaPreset): string;
76
+ /** Precondition: `isTransformableMime(sourceMime)` is true. */
77
+ export declare function buildImageTransformSpec(preset: MediaPreset, request: MediaTransformRequest, sourceMime: string): ImageTransformSpec;
78
+ /**
79
+ * Strong ETag of a variant: source identity (key + size — keys are never overwritten) plus the
80
+ * canonical request plus the preset definition. Format: `"mv1-<32 hex>"`.
81
+ */
82
+ export declare function computeMediaVariantEtag(source: {
83
+ readonly key: string;
84
+ readonly size: number;
85
+ }, request: MediaTransformRequest, preset: MediaPreset): Promise<string>;
86
+ /** RFC 9110 §13.1.2: `*` matches; otherwise weak comparison against a comma-separated list. */
87
+ export declare function ifNoneMatchMatches(header: string | null | undefined, etag: string | null | undefined): boolean;
88
+ //# 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":"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"}
@@ -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>;
@@ -0,0 +1,33 @@
1
+ import type { OAuthScope } from './scopes.js';
2
+ export interface NewAuthorizationCode {
3
+ /** SHA-256 hex hash of the code. The plaintext code is never persisted. */
4
+ codeHash: string;
5
+ clientId: string;
6
+ userId: string;
7
+ scope: OAuthScope[];
8
+ redirectUri: string;
9
+ codeChallenge: string;
10
+ /** Always 'S256'; the column CHECK constraint rejects anything else. */
11
+ codeChallengeMethod: 'S256';
12
+ expiresAt: number;
13
+ }
14
+ export interface AuthorizationCodeRecord extends NewAuthorizationCode {
15
+ createdAt: number;
16
+ consumedAt: number | null;
17
+ }
18
+ export interface IOAuthAuthorizationCodeRepository {
19
+ /** Persists a new single-use authorization code. Hash only, never plaintext. */
20
+ save(record: NewAuthorizationCode): Promise<void>;
21
+ /**
22
+ * Returns the code regardless of its consumed state, provided it is unexpired.
23
+ * Callers MUST inspect `consumedAt`: a non-null value means replay, which
24
+ * requires cascade revocation via IOAuthTokenRepository.revokeByAuthorizationCode.
25
+ */
26
+ findByHash(codeHash: string, nowTimestamp: number): Promise<AuthorizationCodeRecord | null>;
27
+ /**
28
+ * Atomically marks the code consumed. Returns true only for the first caller;
29
+ * every subsequent call returns false, which is the replay signal.
30
+ */
31
+ consumeByHash(codeHash: string, nowTimestamp: number): Promise<boolean>;
32
+ }
33
+ //# sourceMappingURL=authorization-code.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorization-code.repository.d.ts","sourceRoot":"","sources":["../../src/oauth/authorization-code.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;IACnB,aAAa,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,mBAAmB,EAAE,MAAM,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,uBAAwB,SAAQ,oBAAoB;IACnE,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,iCAAiC;IAChD,gFAAgF;IAChF,IAAI,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEjD;;;;OAIG;IACH,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,GAAG,IAAI,CAAC,CAAA;IAE3F;;;OAGG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACxE"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { OAuthScope } from './scopes.js';
2
+ export interface OAuthClientRecord {
3
+ clientId: string;
4
+ name: string;
5
+ /** Registered redirect URIs. Loopback entries are matched ignoring the port. */
6
+ redirectUris: string[];
7
+ allowedScopes: OAuthScope[];
8
+ /** True for clients that cannot hold a secret (native/CLI). PKCE is required regardless. */
9
+ isPublic: boolean;
10
+ createdAt: number;
11
+ disabledAt: number | null;
12
+ }
13
+ export interface IOAuthClientRepository {
14
+ /** Returns the client, or null when unknown or disabled. */
15
+ findActiveById(clientId: string): Promise<OAuthClientRecord | null>;
16
+ }
17
+ //# sourceMappingURL=client.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.repository.d.ts","sourceRoot":"","sources":["../../src/oauth/client.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,gFAAgF;IAChF,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,aAAa,EAAE,UAAU,EAAE,CAAA;IAC3B,4FAA4F;IAC5F,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,sBAAsB;IACrC,4DAA4D;IAC5D,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAA;CACpE"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { OAuthScope } from './scopes.js';
2
+ export interface ConsentRecord {
3
+ id: string;
4
+ clientId: string;
5
+ userId: string;
6
+ /** Cumulative set of scopes the user has approved for this client. */
7
+ scopes: OAuthScope[];
8
+ createdAt: number;
9
+ updatedAt: number;
10
+ revokedAt: number | null;
11
+ }
12
+ export interface IOAuthConsentRepository {
13
+ /** Returns the live consent for the pair, or null when absent or revoked. */
14
+ findActive(clientId: string, userId: string): Promise<ConsentRecord | null>;
15
+ /**
16
+ * Records consent for the pair, unioning `scopes` into any existing live grant
17
+ * and reviving a previously revoked row. Idempotent for an unchanged scope set.
18
+ */
19
+ grant(id: string, clientId: string, userId: string, scopes: readonly OAuthScope[], nowTimestamp: number): Promise<void>;
20
+ /** Revokes the consent. False when there was nothing live to revoke. */
21
+ revoke(clientId: string, userId: string, nowTimestamp: number): Promise<boolean>;
22
+ /** Lists all live consents for a user, newest first. */
23
+ listForUser(userId: string): Promise<ConsentRecord[]>;
24
+ }
25
+ //# sourceMappingURL=consent.repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consent.repository.d.ts","sourceRoot":"","sources":["../../src/oauth/consent.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,sEAAsE;IACtE,MAAM,EAAE,UAAU,EAAE,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,6EAA6E;IAC7E,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAE3E;;;OAGG;IACH,KAAK,CACH,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAAA;IAEhB,wEAAwE;IACxE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAEhF,wDAAwD;IACxD,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAAA;CACtD"}
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * PKCE (RFC 7636) verification. `S256` only — `plain` is rejected unconditionally,
3
+ * per OAuth 2.1, so no weak fallback configuration can ever exist.
4
+ */
5
+ /** The only accepted code challenge method. */
6
+ export declare const PKCE_CHALLENGE_METHOD: 'S256';
7
+ export type PkceChallengeMethod = typeof PKCE_CHALLENGE_METHOD;
8
+ /** Minimum / maximum code_verifier length, RFC 7636 §4.1. */
9
+ export declare const PKCE_VERIFIER_MIN_LENGTH = 43;
10
+ export declare const PKCE_VERIFIER_MAX_LENGTH = 128;
11
+ /** Validates the code_verifier character set and length. */
12
+ export declare function isValidCodeVerifier(verifier: string): boolean;
13
+ /** Computes BASE64URL(SHA256(ASCII(verifier))). */
14
+ export declare function deriveCodeChallenge(verifier: string): Promise<string>;
15
+ /**
16
+ * Verifies a code_verifier against a stored code_challenge.
17
+ * Returns false — never throws — on any malformed input, unsupported method, or
18
+ * mismatch, so callers cannot distinguish failure modes from the return value.
19
+ */
20
+ export declare function verifyPkceChallenge(verifier: string, challenge: string, method: string): Promise<boolean>;
21
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/oauth/pkce.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,+CAA+C;AAC/C,eAAO,MAAM,qBAAqB,EAAG,MAAe,CAAA;AAEpD,MAAM,MAAM,mBAAmB,GAAG,OAAO,qBAAqB,CAAA;AAE9D,6DAA6D;AAC7D,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAC1C,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAI3C,4DAA4D;AAC5D,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAM7D;AAQD,mDAAmD;AACnD,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG3E;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAUlB"}
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ /**
4
+ * PKCE (RFC 7636) verification. `S256` only — `plain` is rejected unconditionally,
5
+ * per OAuth 2.1, so no weak fallback configuration can ever exist.
6
+ */
7
+ /** The only accepted code challenge method. */
8
+ export const PKCE_CHALLENGE_METHOD = 'S256';
9
+ /** Minimum / maximum code_verifier length, RFC 7636 §4.1. */
10
+ export const PKCE_VERIFIER_MIN_LENGTH = 43;
11
+ export const PKCE_VERIFIER_MAX_LENGTH = 128;
12
+ const VERIFIER_PATTERN = /^[A-Za-z0-9\-._~]+$/;
13
+ /** Validates the code_verifier character set and length. */
14
+ export function isValidCodeVerifier(verifier) {
15
+ return (verifier.length >= PKCE_VERIFIER_MIN_LENGTH &&
16
+ verifier.length <= PKCE_VERIFIER_MAX_LENGTH &&
17
+ VERIFIER_PATTERN.test(verifier));
18
+ }
19
+ /** Base64url-encodes bytes without padding, per RFC 7636 §A. */
20
+ function base64UrlEncode(bytes) {
21
+ const binary = String.fromCharCode(...new Uint8Array(bytes));
22
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
23
+ }
24
+ /** Computes BASE64URL(SHA256(ASCII(verifier))). */
25
+ export async function deriveCodeChallenge(verifier) {
26
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
27
+ return base64UrlEncode(digest);
28
+ }
29
+ /**
30
+ * Verifies a code_verifier against a stored code_challenge.
31
+ * Returns false — never throws — on any malformed input, unsupported method, or
32
+ * mismatch, so callers cannot distinguish failure modes from the return value.
33
+ */
34
+ export async function verifyPkceChallenge(verifier, challenge, method) {
35
+ if (method !== PKCE_CHALLENGE_METHOD)
36
+ return false;
37
+ if (!isValidCodeVerifier(verifier))
38
+ return false;
39
+ const derived = await deriveCodeChallenge(verifier);
40
+ if (derived.length !== challenge.length)
41
+ return false;
42
+ let mismatch = 0;
43
+ for (let index = 0; index < derived.length; index += 1) {
44
+ mismatch |= derived.charCodeAt(index) ^ challenge.charCodeAt(index);
45
+ }
46
+ return mismatch === 0;
47
+ }