@caelo-cms/shared 0.10.22 → 0.10.23

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 (241) hide show
  1. package/dist/ai-tools.d.ts +290 -214
  2. package/dist/ai-tools.d.ts.map +1 -1
  3. package/dist/ai-tools.js +341 -264
  4. package/dist/ai-tools.js.map +1 -1
  5. package/dist/auth-forms.d.ts.map +1 -1
  6. package/dist/auth-forms.js +4 -1
  7. package/dist/auth-forms.js.map +1 -1
  8. package/dist/base-css.d.ts +24 -0
  9. package/dist/base-css.d.ts.map +1 -0
  10. package/dist/base-css.js +28 -0
  11. package/dist/base-css.js.map +1 -0
  12. package/dist/build-page.d.ts +330 -0
  13. package/dist/build-page.d.ts.map +1 -0
  14. package/dist/build-page.js +282 -0
  15. package/dist/build-page.js.map +1 -0
  16. package/dist/content.d.ts +322 -9
  17. package/dist/content.d.ts.map +1 -1
  18. package/dist/content.js +354 -11
  19. package/dist/content.js.map +1 -1
  20. package/dist/css-gradient-scan.d.ts +14 -0
  21. package/dist/css-gradient-scan.d.ts.map +1 -0
  22. package/dist/css-gradient-scan.js +81 -0
  23. package/dist/css-gradient-scan.js.map +1 -0
  24. package/dist/css-var-scan.d.ts +56 -0
  25. package/dist/css-var-scan.d.ts.map +1 -0
  26. package/dist/css-var-scan.js +97 -0
  27. package/dist/css-var-scan.js.map +1 -0
  28. package/dist/design-manifest.d.ts +36 -0
  29. package/dist/design-manifest.d.ts.map +1 -0
  30. package/dist/design-manifest.js +90 -0
  31. package/dist/design-manifest.js.map +1 -0
  32. package/dist/fonts.d.ts +89 -0
  33. package/dist/fonts.d.ts.map +1 -0
  34. package/dist/fonts.js +241 -0
  35. package/dist/fonts.js.map +1 -0
  36. package/dist/genesis-inventory.d.ts +32 -0
  37. package/dist/genesis-inventory.d.ts.map +1 -0
  38. package/dist/genesis-inventory.js +186 -0
  39. package/dist/genesis-inventory.js.map +1 -0
  40. package/dist/genesis.d.ts +62 -0
  41. package/dist/genesis.d.ts.map +1 -0
  42. package/dist/genesis.js +78 -0
  43. package/dist/genesis.js.map +1 -0
  44. package/dist/i18n.d.ts +44 -1
  45. package/dist/i18n.d.ts.map +1 -1
  46. package/dist/i18n.js +72 -6
  47. package/dist/i18n.js.map +1 -1
  48. package/dist/index.d.ts +27 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +27 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/interactions.d.ts +23 -0
  53. package/dist/interactions.d.ts.map +1 -0
  54. package/dist/interactions.js +44 -0
  55. package/dist/interactions.js.map +1 -0
  56. package/dist/media.d.ts +101 -16
  57. package/dist/media.d.ts.map +1 -1
  58. package/dist/media.js +126 -15
  59. package/dist/media.js.map +1 -1
  60. package/dist/page-log.d.ts +94 -0
  61. package/dist/page-log.d.ts.map +1 -0
  62. package/dist/page-log.js +111 -0
  63. package/dist/page-log.js.map +1 -0
  64. package/dist/preview-compose.d.ts +79 -0
  65. package/dist/preview-compose.d.ts.map +1 -1
  66. package/dist/preview-compose.js +155 -25
  67. package/dist/preview-compose.js.map +1 -1
  68. package/dist/proposal-status.d.ts +40 -0
  69. package/dist/proposal-status.d.ts.map +1 -0
  70. package/dist/proposal-status.js +34 -0
  71. package/dist/proposal-status.js.map +1 -0
  72. package/dist/responsive-images.d.ts +64 -0
  73. package/dist/responsive-images.d.ts.map +1 -0
  74. package/dist/responsive-images.js +98 -0
  75. package/dist/responsive-images.js.map +1 -0
  76. package/dist/safe-keys.d.ts +9 -0
  77. package/dist/safe-keys.d.ts.map +1 -0
  78. package/dist/safe-keys.js +20 -0
  79. package/dist/safe-keys.js.map +1 -0
  80. package/dist/seo.d.ts +8 -0
  81. package/dist/seo.d.ts.map +1 -1
  82. package/dist/seo.js +3 -1
  83. package/dist/seo.js.map +1 -1
  84. package/dist/skills.d.ts +14 -68
  85. package/dist/skills.d.ts.map +1 -1
  86. package/dist/skills.js +19 -113
  87. package/dist/skills.js.map +1 -1
  88. package/dist/strip-cdata.d.ts +7 -0
  89. package/dist/strip-cdata.d.ts.map +1 -0
  90. package/dist/strip-cdata.js +48 -0
  91. package/dist/strip-cdata.js.map +1 -0
  92. package/dist/structured-sets.d.ts +6 -16
  93. package/dist/structured-sets.d.ts.map +1 -1
  94. package/dist/structured-sets.js +5 -16
  95. package/dist/structured-sets.js.map +1 -1
  96. package/dist/subagents.d.ts +105 -3
  97. package/dist/subagents.d.ts.map +1 -1
  98. package/dist/subagents.js +224 -41
  99. package/dist/subagents.js.map +1 -1
  100. package/dist/template-engine.d.ts +85 -0
  101. package/dist/template-engine.d.ts.map +1 -0
  102. package/dist/template-engine.js +403 -0
  103. package/dist/template-engine.js.map +1 -0
  104. package/dist/theme-importers/auto-detect.d.ts +26 -0
  105. package/dist/theme-importers/auto-detect.d.ts.map +1 -0
  106. package/dist/theme-importers/auto-detect.js +42 -0
  107. package/dist/theme-importers/auto-detect.js.map +1 -0
  108. package/dist/theme-importers/css-comments.d.ts +12 -0
  109. package/dist/theme-importers/css-comments.d.ts.map +1 -0
  110. package/dist/theme-importers/css-comments.js +15 -0
  111. package/dist/theme-importers/css-comments.js.map +1 -0
  112. package/dist/theme-importers/dtcg.d.ts +46 -0
  113. package/dist/theme-importers/dtcg.d.ts.map +1 -0
  114. package/dist/theme-importers/dtcg.js +111 -0
  115. package/dist/theme-importers/dtcg.js.map +1 -0
  116. package/dist/theme-importers/loose.d.ts +3 -0
  117. package/dist/theme-importers/loose.d.ts.map +1 -0
  118. package/dist/theme-importers/loose.js +76 -0
  119. package/dist/theme-importers/loose.js.map +1 -0
  120. package/dist/theme-importers/shadcn.d.ts +24 -0
  121. package/dist/theme-importers/shadcn.d.ts.map +1 -0
  122. package/dist/theme-importers/shadcn.js +135 -0
  123. package/dist/theme-importers/shadcn.js.map +1 -0
  124. package/dist/theme-importers/style-dictionary.d.ts +17 -0
  125. package/dist/theme-importers/style-dictionary.d.ts.map +1 -0
  126. package/dist/theme-importers/style-dictionary.js +125 -0
  127. package/dist/theme-importers/style-dictionary.js.map +1 -0
  128. package/dist/theme-importers/tailwind.d.ts +3 -0
  129. package/dist/theme-importers/tailwind.d.ts.map +1 -0
  130. package/dist/theme-importers/tailwind.js +218 -0
  131. package/dist/theme-importers/tailwind.js.map +1 -0
  132. package/dist/theme-literal-binding.d.ts +37 -0
  133. package/dist/theme-literal-binding.d.ts.map +1 -0
  134. package/dist/theme-literal-binding.js +138 -0
  135. package/dist/theme-literal-binding.js.map +1 -0
  136. package/dist/theme-normalize.d.ts +31 -0
  137. package/dist/theme-normalize.d.ts.map +1 -0
  138. package/dist/theme-normalize.js +587 -0
  139. package/dist/theme-normalize.js.map +1 -0
  140. package/dist/theme-ramp.d.ts +55 -0
  141. package/dist/theme-ramp.d.ts.map +1 -0
  142. package/dist/theme-ramp.js +149 -0
  143. package/dist/theme-ramp.js.map +1 -0
  144. package/dist/theme-render.d.ts +105 -0
  145. package/dist/theme-render.d.ts.map +1 -0
  146. package/dist/theme-render.js +441 -0
  147. package/dist/theme-render.js.map +1 -0
  148. package/dist/themes-errors.d.ts +109 -0
  149. package/dist/themes-errors.d.ts.map +1 -0
  150. package/dist/themes-errors.js +170 -0
  151. package/dist/themes-errors.js.map +1 -0
  152. package/dist/themes.d.ts +343 -0
  153. package/dist/themes.d.ts.map +1 -0
  154. package/dist/themes.js +697 -0
  155. package/dist/themes.js.map +1 -0
  156. package/dist/version.d.ts +7 -4
  157. package/dist/version.d.ts.map +1 -1
  158. package/dist/version.js +6 -3
  159. package/dist/version.js.map +1 -1
  160. package/package.json +10 -2
  161. package/src/__tests__/redos-hardening.test.ts +160 -0
  162. package/src/ai-tools-add-module-modes.test.ts +106 -0
  163. package/src/ai-tools-position.test.ts +134 -0
  164. package/src/ai-tools.test.ts +81 -0
  165. package/src/ai-tools.ts +1179 -0
  166. package/src/auth-forms.ts +36 -0
  167. package/src/base-css.ts +30 -0
  168. package/src/build-page.test.ts +228 -0
  169. package/src/build-page.ts +319 -0
  170. package/src/cap-failures.ts +67 -0
  171. package/src/content.test.ts +170 -0
  172. package/src/content.ts +620 -0
  173. package/src/context.ts +43 -0
  174. package/src/css-gradient-scan.ts +88 -0
  175. package/src/css-var-scan.test.ts +96 -0
  176. package/src/css-var-scan.ts +144 -0
  177. package/src/derive-module-type.test.ts +80 -0
  178. package/src/design-manifest.ts +93 -0
  179. package/src/fonts.test.ts +157 -0
  180. package/src/fonts.ts +296 -0
  181. package/src/genesis-inventory.test.ts +86 -0
  182. package/src/genesis-inventory.ts +215 -0
  183. package/src/genesis-sanitize.test.ts +35 -0
  184. package/src/genesis.ts +87 -0
  185. package/src/i18n.test.ts +274 -0
  186. package/src/i18n.ts +269 -0
  187. package/src/index.test.ts +10 -0
  188. package/src/index.ts +59 -0
  189. package/src/interactions.ts +48 -0
  190. package/src/logger.ts +147 -0
  191. package/src/media.test.ts +160 -0
  192. package/src/media.ts +355 -0
  193. package/src/page-log.test.ts +163 -0
  194. package/src/page-log.ts +124 -0
  195. package/src/preview-compose.test.ts +602 -0
  196. package/src/preview-compose.ts +709 -0
  197. package/src/preview-scanner.test.ts +96 -0
  198. package/src/preview-scanner.ts +214 -0
  199. package/src/proposal-status.test.ts +69 -0
  200. package/src/proposal-status.ts +40 -0
  201. package/src/responsive-images.test.ts +104 -0
  202. package/src/responsive-images.ts +151 -0
  203. package/src/result.ts +29 -0
  204. package/src/safe-keys.ts +21 -0
  205. package/src/seo.test.ts +234 -0
  206. package/src/seo.ts +261 -0
  207. package/src/skills.ts +48 -0
  208. package/src/snapshots.test.ts +80 -0
  209. package/src/snapshots.ts +81 -0
  210. package/src/strip-cdata.test.ts +41 -0
  211. package/src/strip-cdata.ts +50 -0
  212. package/src/structured-sets.ts +180 -0
  213. package/src/subagents.test.ts +262 -0
  214. package/src/subagents.ts +432 -0
  215. package/src/template-engine.test.ts +379 -0
  216. package/src/template-engine.ts +520 -0
  217. package/src/theme-gradient.test.ts +92 -0
  218. package/src/theme-importers/__tests__/proto-pollution.test.ts +54 -0
  219. package/src/theme-importers/auto-detect.ts +84 -0
  220. package/src/theme-importers/css-comments.ts +15 -0
  221. package/src/theme-importers/dtcg.ts +106 -0
  222. package/src/theme-importers/loose.ts +76 -0
  223. package/src/theme-importers/shadcn.ts +133 -0
  224. package/src/theme-importers/style-dictionary.ts +125 -0
  225. package/src/theme-importers/tailwind.ts +217 -0
  226. package/src/theme-literal-binding.test.ts +71 -0
  227. package/src/theme-literal-binding.ts +159 -0
  228. package/src/theme-motion.test.ts +115 -0
  229. package/src/theme-normalize-envelope.test.ts +43 -0
  230. package/src/theme-normalize-gradient.test.ts +135 -0
  231. package/src/theme-normalize.ts +661 -0
  232. package/src/theme-ramp.ts +187 -0
  233. package/src/theme-render-sanitize.test.ts +45 -0
  234. package/src/theme-render.test.ts +119 -0
  235. package/src/theme-render.ts +487 -0
  236. package/src/theme-shadow.test.ts +56 -0
  237. package/src/themes-errors.ts +199 -0
  238. package/src/themes.ts +842 -0
  239. package/src/translation.test.ts +160 -0
  240. package/src/translation.ts +295 -0
  241. package/src/version.ts +66 -0
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ export const PROJECT_NAME = "caelo-cms";
4
+
5
+ export * from "./ai-tools.js";
6
+ export * from "./auth-forms.js";
7
+ export * from "./base-css.js";
8
+ export * from "./build-page.js";
9
+ export * from "./cap-failures.js";
10
+ export * from "./content.js";
11
+ export * from "./context.js";
12
+ export * from "./css-gradient-scan.js";
13
+ export * from "./css-var-scan.js";
14
+ export * from "./design-manifest.js";
15
+ export * from "./fonts.js";
16
+ export * from "./genesis.js";
17
+ export * from "./genesis-inventory.js";
18
+ export * from "./i18n.js";
19
+ export * from "./interactions.js";
20
+ export {
21
+ type LogContext,
22
+ type Logger,
23
+ type LoggerOptions,
24
+ type LogLevel,
25
+ makeLogger,
26
+ mintRequestId,
27
+ redact,
28
+ type ServiceName,
29
+ type StructuredLogEntry,
30
+ } from "./logger.js";
31
+ export * from "./media.js";
32
+ export * from "./page-log.js";
33
+ export * from "./preview-compose.js";
34
+ export * from "./preview-scanner.js";
35
+ export * from "./proposal-status.js";
36
+ export * from "./responsive-images.js";
37
+ export * from "./result.js";
38
+ export * from "./safe-keys.js";
39
+ export * from "./seo.js";
40
+ export * from "./skills.js";
41
+ export * from "./snapshots.js";
42
+ export * from "./strip-cdata.js";
43
+ export * from "./structured-sets.js";
44
+ export * from "./subagents.js";
45
+ export * from "./template-engine.js";
46
+ export * from "./theme-importers/auto-detect.js";
47
+ export * from "./theme-importers/dtcg.js";
48
+ export * from "./theme-importers/loose.js";
49
+ export * from "./theme-importers/shadcn.js";
50
+ export * from "./theme-importers/style-dictionary.js";
51
+ export * from "./theme-importers/tailwind.js";
52
+ export * from "./theme-literal-binding.js";
53
+ export * from "./theme-normalize.js";
54
+ export * from "./theme-ramp.js";
55
+ export * from "./theme-render.js";
56
+ export * from "./themes.js";
57
+ export * from "./themes-errors.js";
58
+ export * from "./translation.js";
59
+ export { CAELO_VERSION, CALEO_VERSION, type CaeloVersion, parseVersion } from "./version.js";
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * issue #160 — the vetted interaction baseline (epic #149).
5
+ *
6
+ * A header without a working mobile menu reads as broken, not minimal
7
+ * — and hand-rolled per-module JS gets focus/aria handling wrong more
8
+ * often than right. This module carries the FUNCTIONAL (non-aesthetic)
9
+ * CSS + JS for the built-in nav renderer: hamburger toggle under the
10
+ * mobile breakpoint, `aria-expanded` bookkeeping, escape-to-close.
11
+ * All colors/spacing stay the module/theme's business — these rules
12
+ * only govern show/hide behaviour, so they don't violate the
13
+ * no-global-look re-scope (#151).
14
+ *
15
+ * The composer includes each asset ONCE per page when at least one
16
+ * nav-menu module rendered (natural dedup, same rule as #158).
17
+ */
18
+
19
+ /** Functional-only rules: collapse/expand mechanics, zero aesthetics. */
20
+ export const NAV_FUNCTIONAL_CSS =
21
+ ".caelo-nav-menu .caelo-nav-toggle{display:none;background:none;border:0;cursor:pointer;padding:0.5rem;color:inherit;font:inherit}" +
22
+ ".caelo-nav-menu .caelo-nav-toggle .caelo-nav-bar{display:block;width:1.25em;height:2px;background:currentColor;margin:0.25em 0}" +
23
+ "@media (max-width: 720px){" +
24
+ ".caelo-nav-menu .caelo-nav-toggle{display:inline-block}" +
25
+ ".caelo-nav-menu > ul{display:none}" +
26
+ '.caelo-nav-menu[data-nav-open="true"] > ul{display:block}' +
27
+ "}";
28
+
29
+ /**
30
+ * Toggle behaviour. Runs deferred; idempotent per element via the
31
+ * data attribute; closes on Escape and returns focus to the toggle.
32
+ */
33
+ export const NAV_TOGGLE_JS =
34
+ "(function(){" +
35
+ "document.querySelectorAll('.caelo-nav-menu').forEach(function(nav){" +
36
+ "var btn=nav.querySelector('.caelo-nav-toggle');if(!btn)return;" +
37
+ "btn.addEventListener('click',function(){" +
38
+ "var open=nav.getAttribute('data-nav-open')==='true';" +
39
+ "nav.setAttribute('data-nav-open',String(!open));" +
40
+ "btn.setAttribute('aria-expanded',String(!open));" +
41
+ "});" +
42
+ "nav.addEventListener('keydown',function(e){" +
43
+ "if(e.key==='Escape'&&nav.getAttribute('data-nav-open')==='true'){" +
44
+ "nav.setAttribute('data-nav-open','false');" +
45
+ "btn.setAttribute('aria-expanded','false');btn.focus();" +
46
+ "}});" +
47
+ "});" +
48
+ "})();";
package/src/logger.ts ADDED
@@ -0,0 +1,147 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * P16 — structured logger. JSON-per-line to stderr; the host environment
5
+ * (operator's log aggregator: Loki/Datadog/CloudWatch/etc.) ships from
6
+ * there. Stable shape across every Caelo service so a single
7
+ * `request_id` query reconstructs the full timeline.
8
+ *
9
+ * Redaction pass replaces values for keys matching common-secret regex
10
+ * with `***` so a stray `{password: "..."}` extra never lands in logs.
11
+ *
12
+ * NOT a replacement for `audit_events` — audit captures intent (op,
13
+ * actor, succeeded), structured logs capture trace (request_id,
14
+ * service-to-service hops, latency, errors). Cross-correlate via the
15
+ * shared `request_id` column.
16
+ */
17
+
18
+ export type LogLevel = "debug" | "info" | "warn" | "error";
19
+
20
+ export type ServiceName = "admin" | "gateway" | "orchestrator" | "plugin-host" | "static-gen";
21
+
22
+ export interface LogContext {
23
+ readonly requestId: string;
24
+ readonly actorId?: string;
25
+ readonly actorKind?: "human" | "ai" | "system" | "plugin";
26
+ readonly opName?: string;
27
+ readonly chatSessionId?: string;
28
+ readonly pluginSlug?: string;
29
+ }
30
+
31
+ export interface StructuredLogEntry {
32
+ readonly ts: string;
33
+ readonly level: LogLevel;
34
+ readonly msg: string;
35
+ readonly service: ServiceName;
36
+ readonly env: string;
37
+ readonly ctx: LogContext;
38
+ readonly extra?: Record<string, unknown>;
39
+ }
40
+
41
+ // Word-anchored — without the boundaries `key` would match `monkey`,
42
+ // `token` would match `tokenizer`, `secret` would match `secretary`.
43
+ // Whole-key match is the right semantic for log-field redaction.
44
+ const SECRET_KEY_RE =
45
+ /^(password|passwd|secret|secrets|token|tokens|api[-_]?key|api[-_]?keys|cookie|cookies|authorization|bearer|x[-_]api[-_]?key|csrf[-_]?secret|cookie[-_]?secret)$/i;
46
+
47
+ export function redact(input: unknown): unknown {
48
+ if (input === null || typeof input !== "object") return input;
49
+ if (Array.isArray(input)) return input.map(redact);
50
+ const out: Record<string, unknown> = {};
51
+ for (const [k, v] of Object.entries(input)) {
52
+ if (SECRET_KEY_RE.test(k)) {
53
+ out[k] = "***";
54
+ } else if (v && typeof v === "object") {
55
+ out[k] = redact(v);
56
+ } else {
57
+ out[k] = v;
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ export interface Logger {
64
+ with(ctx: Partial<LogContext>): Logger;
65
+ debug(msg: string, extra?: Record<string, unknown>): void;
66
+ info(msg: string, extra?: Record<string, unknown>): void;
67
+ warn(msg: string, extra?: Record<string, unknown>): void;
68
+ error(msg: string, extra?: Record<string, unknown>): void;
69
+ }
70
+
71
+ export interface LoggerOptions {
72
+ readonly service: ServiceName;
73
+ readonly env?: string;
74
+ /** Defaults to console.error so structured logs flow to stderr. */
75
+ readonly sink?: (entry: StructuredLogEntry) => void;
76
+ /** Minimum level to emit; default 'info' (suppresses 'debug'). */
77
+ readonly minLevel?: LogLevel;
78
+ }
79
+
80
+ const LEVEL_RANK: Record<LogLevel, number> = {
81
+ debug: 10,
82
+ info: 20,
83
+ warn: 30,
84
+ error: 40,
85
+ };
86
+
87
+ export function makeLogger(opts: LoggerOptions): Logger {
88
+ const env = opts.env ?? process.env.CAELO_ENV ?? "dev";
89
+ const minRank = LEVEL_RANK[opts.minLevel ?? "info"];
90
+ const sink =
91
+ opts.sink ??
92
+ ((entry: StructuredLogEntry) => {
93
+ process.stderr.write(`${JSON.stringify(entry)}\n`);
94
+ });
95
+
96
+ function emit(
97
+ ctx: LogContext,
98
+ level: LogLevel,
99
+ msg: string,
100
+ extra?: Record<string, unknown>,
101
+ ): void {
102
+ if (LEVEL_RANK[level] < minRank) return;
103
+ const entry: StructuredLogEntry = {
104
+ ts: new Date().toISOString(),
105
+ level,
106
+ msg,
107
+ service: opts.service,
108
+ env,
109
+ ctx,
110
+ extra: extra ? (redact(extra) as Record<string, unknown>) : undefined,
111
+ };
112
+ sink(entry);
113
+ }
114
+
115
+ function buildLogger(ctx: LogContext): Logger {
116
+ return {
117
+ with(extraCtx) {
118
+ return buildLogger({ ...ctx, ...extraCtx });
119
+ },
120
+ debug(msg, extra) {
121
+ emit(ctx, "debug", msg, extra);
122
+ },
123
+ info(msg, extra) {
124
+ emit(ctx, "info", msg, extra);
125
+ },
126
+ warn(msg, extra) {
127
+ emit(ctx, "warn", msg, extra);
128
+ },
129
+ error(msg, extra) {
130
+ emit(ctx, "error", msg, extra);
131
+ },
132
+ };
133
+ }
134
+
135
+ // Default ctx has a synthetic request_id so a logger created outside
136
+ // of any request boundary still produces correlatable output (visible
137
+ // in the log stream as `synthetic-…`).
138
+ return buildLogger({ requestId: `synthetic-${crypto.randomUUID().slice(0, 8)}` });
139
+ }
140
+
141
+ /**
142
+ * Mint a fresh request_id at a request boundary. Caller threads through
143
+ * ExecutionContext + outbound HTTP headers (`X-Caelo-Request-Id`).
144
+ */
145
+ export function mintRequestId(): string {
146
+ return crypto.randomUUID();
147
+ }
@@ -0,0 +1,160 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import {
5
+ buildMediaUrl,
6
+ buildStorageKey,
7
+ extractMediaRefs,
8
+ MEDIA_ALLOWED_MIMES,
9
+ MEDIA_HARD_LIMIT_BYTES,
10
+ MEDIA_SIZE_CAPS,
11
+ MEDIA_VARIANT_TAGS,
12
+ MEDIA_VARIANT_WIDTHS,
13
+ mediaListInputSchema,
14
+ mediaSetCdnInputSchema,
15
+ mediaUploadInputSchema,
16
+ slugifyMediaName,
17
+ } from "./media.js";
18
+
19
+ describe("media URL helpers", () => {
20
+ it("builds a slug URL: orig is flat, named variants nest under the slug", () => {
21
+ expect(buildMediaUrl("searchviu-logo", "orig")).toBe("/_caelo/media/searchviu-logo");
22
+ expect(buildMediaUrl("searchviu-hero", "webp-800")).toBe(
23
+ "/_caelo/media/searchviu-hero/webp-800",
24
+ );
25
+ });
26
+
27
+ it("extracts slug refs (orig implied when flat) deduped", () => {
28
+ const html = `
29
+ <img src="/_caelo/media/searchviu-hero/webp-800" alt="x" />
30
+ <img src="/_caelo/media/searchviu-hero/webp-800" alt="y" />
31
+ <img src="/_caelo/media/searchviu-logo" />
32
+ `;
33
+ expect(extractMediaRefs(html)).toEqual([
34
+ { ref: "searchviu-hero", isSlug: true, variant: "webp-800" },
35
+ { ref: "searchviu-logo", isSlug: true, variant: "orig" },
36
+ ]);
37
+ });
38
+
39
+ it("still parses the legacy /_caelo/media/<uuid>/<variant> form as an id ref", () => {
40
+ const html = `
41
+ <img src="/_caelo/media/short-slug/webp-800" />
42
+ <img src="/_caelo/media/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/square-800" />
43
+ `;
44
+ expect(extractMediaRefs(html)).toEqual([
45
+ { ref: "short-slug", isSlug: true, variant: "webp-800" },
46
+ { ref: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", isSlug: false, variant: "square-800" },
47
+ ]);
48
+ });
49
+
50
+ it("slugifies a human label: accents stripped, kebab-cased, extension dropped", () => {
51
+ expect(slugifyMediaName("SearchVIU Logo.png")).toBe("searchviu-logo");
52
+ expect(slugifyMediaName("Über uns – Team!")).toBe("uber-uns-team");
53
+ expect(slugifyMediaName(" ")).toBe("image");
54
+ expect(slugifyMediaName("a".repeat(80)).length).toBeLessThanOrEqual(60);
55
+ });
56
+
57
+ it("buildStorageKey is sha-prefixed", () => {
58
+ expect(buildStorageKey("abc123", "webp-800", "webp")).toBe("abc123/webp-800.webp");
59
+ });
60
+ });
61
+
62
+ describe("media size caps + allowlist", () => {
63
+ it("each allowed MIME has a positive size cap below the hard limit", () => {
64
+ for (const m of MEDIA_ALLOWED_MIMES) {
65
+ const cap = MEDIA_SIZE_CAPS[m];
66
+ expect(cap).toBeGreaterThan(0);
67
+ expect(cap).toBeLessThanOrEqual(MEDIA_HARD_LIMIT_BYTES);
68
+ }
69
+ });
70
+
71
+ it("variant widths cover the non-orig tags only", () => {
72
+ for (const t of MEDIA_VARIANT_TAGS) {
73
+ if (t === "orig") continue;
74
+ expect(MEDIA_VARIANT_WIDTHS[t]).toBeGreaterThan(0);
75
+ }
76
+ });
77
+ });
78
+
79
+ describe("media schemas", () => {
80
+ it("mediaUploadInputSchema accepts a minimal valid payload", () => {
81
+ const r = mediaUploadInputSchema.safeParse({
82
+ sha256: "a".repeat(64),
83
+ originalName: "hero.jpg",
84
+ mime: "image/jpeg",
85
+ sizeBytes: 1024,
86
+ width: 1920,
87
+ height: 1080,
88
+ alt: "",
89
+ storageKey: `${"a".repeat(64)}/orig.jpg`,
90
+ variants: [
91
+ {
92
+ variant: "orig",
93
+ format: "jpeg",
94
+ width: 1920,
95
+ height: 1080,
96
+ sizeBytes: 1024,
97
+ storageKey: `${"a".repeat(64)}/orig.jpg`,
98
+ },
99
+ ],
100
+ });
101
+ expect(r.success).toBe(true);
102
+ });
103
+
104
+ it("mediaUploadInputSchema rejects non-hex sha256", () => {
105
+ const r = mediaUploadInputSchema.safeParse({
106
+ sha256: "not-a-sha",
107
+ originalName: "x",
108
+ mime: "image/jpeg",
109
+ sizeBytes: 1,
110
+ width: null,
111
+ height: null,
112
+ storageKey: "x",
113
+ variants: [
114
+ {
115
+ variant: "orig",
116
+ format: "jpeg",
117
+ width: null,
118
+ height: null,
119
+ sizeBytes: 1,
120
+ storageKey: "x",
121
+ },
122
+ ],
123
+ });
124
+ expect(r.success).toBe(false);
125
+ });
126
+
127
+ it("mediaUploadInputSchema rejects unknown MIME", () => {
128
+ const r = mediaUploadInputSchema.safeParse({
129
+ sha256: "a".repeat(64),
130
+ originalName: "x",
131
+ mime: "application/zip",
132
+ sizeBytes: 1,
133
+ width: null,
134
+ height: null,
135
+ storageKey: "x",
136
+ variants: [
137
+ {
138
+ variant: "orig",
139
+ format: "jpeg",
140
+ width: null,
141
+ height: null,
142
+ sizeBytes: 1,
143
+ storageKey: "x",
144
+ },
145
+ ],
146
+ });
147
+ expect(r.success).toBe(false);
148
+ });
149
+
150
+ it("mediaListInputSchema defaults sort=recent and limit=60", () => {
151
+ const r = mediaListInputSchema.parse({});
152
+ expect(r.sort).toBe("recent");
153
+ expect(r.limit).toBe(60);
154
+ expect(r.offset).toBe(0);
155
+ });
156
+
157
+ it("mediaSetCdnInputSchema rejects threshold below 1", () => {
158
+ expect(mediaSetCdnInputSchema.safeParse({ enabled: true, threshold: 0 }).success).toBe(false);
159
+ });
160
+ });