@caelo-cms/shared 0.10.53 → 0.10.55

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 (76) hide show
  1. package/dist/auto-update.d.ts +5 -2
  2. package/dist/auto-update.d.ts.map +1 -1
  3. package/dist/auto-update.js +13 -6
  4. package/dist/auto-update.js.map +1 -1
  5. package/dist/fonts.d.ts +12 -0
  6. package/dist/fonts.d.ts.map +1 -1
  7. package/dist/fonts.js +37 -1
  8. package/dist/fonts.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/link-refs.d.ts +250 -0
  14. package/dist/link-refs.d.ts.map +1 -0
  15. package/dist/link-refs.js +416 -0
  16. package/dist/link-refs.js.map +1 -0
  17. package/dist/media.d.ts +1 -1
  18. package/dist/nested-module-render.d.ts +4 -1
  19. package/dist/nested-module-render.d.ts.map +1 -1
  20. package/dist/nested-module-render.js +14 -2
  21. package/dist/nested-module-render.js.map +1 -1
  22. package/dist/platform-updates.d.ts +7 -3
  23. package/dist/platform-updates.d.ts.map +1 -1
  24. package/dist/platform-updates.js +13 -2
  25. package/dist/platform-updates.js.map +1 -1
  26. package/dist/preview-compose.d.ts +10 -0
  27. package/dist/preview-compose.d.ts.map +1 -1
  28. package/dist/preview-compose.js +58 -20
  29. package/dist/preview-compose.js.map +1 -1
  30. package/dist/raw-text-escape.d.ts +18 -0
  31. package/dist/raw-text-escape.d.ts.map +1 -0
  32. package/dist/raw-text-escape.js +40 -0
  33. package/dist/raw-text-escape.js.map +1 -0
  34. package/dist/responsive-images.d.ts.map +1 -1
  35. package/dist/responsive-images.js +3 -3
  36. package/dist/responsive-images.js.map +1 -1
  37. package/dist/seo.js +1 -1
  38. package/dist/seo.js.map +1 -1
  39. package/dist/template-balance.d.ts +3 -0
  40. package/dist/template-balance.d.ts.map +1 -0
  41. package/dist/template-balance.js +214 -0
  42. package/dist/template-balance.js.map +1 -0
  43. package/dist/template-engine.d.ts +11 -0
  44. package/dist/template-engine.d.ts.map +1 -1
  45. package/dist/template-engine.js +12 -0
  46. package/dist/template-engine.js.map +1 -1
  47. package/dist/theme-render.d.ts +2 -2
  48. package/dist/theme-render.d.ts.map +1 -1
  49. package/dist/theme-render.js +29 -5
  50. package/dist/theme-render.js.map +1 -1
  51. package/dist/version.d.ts +2 -2
  52. package/dist/version.js +1 -1
  53. package/package.json +2 -2
  54. package/src/auto-update.test.ts +32 -1
  55. package/src/auto-update.ts +13 -6
  56. package/src/fonts.test.ts +37 -0
  57. package/src/fonts.ts +38 -1
  58. package/src/index.ts +2 -0
  59. package/src/link-refs.test.ts +552 -0
  60. package/src/link-refs.ts +569 -0
  61. package/src/nested-module-render.ts +18 -1
  62. package/src/platform-updates.test.ts +5 -0
  63. package/src/platform-updates.ts +13 -2
  64. package/src/preview-compose-deferrals.test.ts +103 -0
  65. package/src/preview-compose.ts +73 -24
  66. package/src/raw-text-escape.ts +44 -0
  67. package/src/responsive-images.test.ts +10 -0
  68. package/src/responsive-images.ts +6 -3
  69. package/src/seo.test.ts +7 -0
  70. package/src/seo.ts +1 -1
  71. package/src/template-balance.test.ts +97 -0
  72. package/src/template-balance.ts +216 -0
  73. package/src/template-engine.ts +23 -0
  74. package/src/theme-render-sanitize.test.ts +40 -0
  75. package/src/theme-render.ts +40 -8
  76. package/src/version.ts +1 -1
@@ -20,11 +20,14 @@
20
20
  * the soak period.
21
21
  *
22
22
  * Rules, in order (maintainer decisions on #696):
23
- * 1. nothing newer, or notices off → none;
23
+ * 1. nothing newer → none (there is no "notices off": security
24
+ * releases are always announced, #862);
24
25
  * 2. off the stable channel, or the policy only notifies → notify;
25
26
  * 3. major → notify (never automatic before 1.0);
26
27
  * 4. release information unreadable → blocked (fails loudly, no guess);
27
- * 5. changes the cloud setup → needs_cloud_shell (never automatic);
28
+ * 5. changes the cloud setup → needs_cloud_shell (never automatic, even
29
+ * with `autoConverge` on: that choice is inert until the converge job
30
+ * runs these releases from Caelo, #819);
28
31
  * 6. destructive database changes → manual (never automatic);
29
32
  * 7. the kind is not switched on (security / patch / minor) → notify;
30
33
  * 8. no upgrader on this install → blocked;
@@ -194,9 +197,6 @@ export function decideAutoUpdate(input: AutoUpdateInput): AutoUpdateDecision {
194
197
  if (kind === null) {
195
198
  return { action: "none", reason: `v${input.installedVersion} is the newest release.` };
196
199
  }
197
- if (policy.mode === "off") {
198
- return { action: "none", reason: "Update notices are switched off in the update policy." };
199
- }
200
200
  const v = release.version;
201
201
  const base = { version: v, kind } as const;
202
202
  if (!isStable(v)) {
@@ -229,10 +229,17 @@ export function decideAutoUpdate(input: AutoUpdateInput): AutoUpdateDecision {
229
229
  }
230
230
  const facts = input.facts;
231
231
  if (facts.infraChange === true) {
232
+ // FIXME(issue #819): once the converge job runs these releases from
233
+ // Caelo, `policy.autoConverge` decides here like autoPatch does below.
234
+ // Until then the Owner's "install automatically" choice is stored but
235
+ // has no effect, and the reason says so.
236
+ const ownerChoice = policy.autoConverge
237
+ ? " Your setting to install these automatically takes effect once Caelo can run them itself."
238
+ : "";
232
239
  return {
233
240
  action: "needs_cloud_shell",
234
241
  ...base,
235
- reason: `v${v} changes this install's cloud setup, which the admin may not do. It is never installed automatically: the Owner runs it once with their own account in Cloud Shell.`,
242
+ reason: `v${v} changes this install's cloud setup, which the admin may not do. It is never installed automatically: the Owner runs it once with their own account in Cloud Shell.${ownerChoice}`,
236
243
  };
237
244
  }
238
245
  if (facts.contractMigrations) {
package/src/fonts.test.ts CHANGED
@@ -10,6 +10,7 @@ import { describe, expect, it } from "bun:test";
10
10
  import {
11
11
  buildFontFaceCss,
12
12
  extractThemeFontRequests,
13
+ fontFaceProblem,
13
14
  fontUnresolvableMarker,
14
15
  googleFontsCssUrl,
15
16
  isSystemFontFamily,
@@ -155,3 +156,39 @@ describe("fontUnresolvableMarker", () => {
155
156
  expect(fontUnresolvableMarker("Poppins")).toBe("theme-font-unresolvable:Poppins");
156
157
  });
157
158
  });
159
+
160
+ describe("buildFontFaceCss descriptor validation", () => {
161
+ const good = {
162
+ family: "Inter",
163
+ style: "normal",
164
+ weight: "400",
165
+ unicodeRange: "U+0000-00FF, U+0131, U+0152-0153",
166
+ publicUrl: "/_assets/fonts/inter/abc123.woff2",
167
+ };
168
+
169
+ it("accepts the descriptor shapes a css2 response carries", () => {
170
+ expect(fontFaceProblem(good)).toBeNull();
171
+ expect(fontFaceProblem({ ...good, style: "italic", weight: "100 900" })).toBeNull();
172
+ expect(fontFaceProblem({ ...good, style: "oblique 0deg 10deg" })).toBeNull();
173
+ expect(fontFaceProblem({ ...good, unicodeRange: null })).toBeNull();
174
+ expect(fontFaceProblem({ ...good, unicodeRange: "U+4??" })).toBeNull();
175
+ });
176
+
177
+ it("refuses descriptors that could close the <style> or add rules, instead of scrubbing them", () => {
178
+ expect(fontFaceProblem({ ...good, style: "normal;}</style><script>" })).toMatch(/font-style/);
179
+ expect(fontFaceProblem({ ...good, weight: "400;}</style>" })).toMatch(/font-weight/);
180
+ expect(fontFaceProblem({ ...good, unicodeRange: "U+0000-00FF;</style>" })).toMatch(
181
+ /unicode-range/,
182
+ );
183
+ expect(fontFaceProblem({ ...good, publicUrl: "/x.woff2)</style>" })).toMatch(/url/);
184
+ expect(() => buildFontFaceCss([{ ...good, weight: "400}</style>" }])).toThrow(
185
+ /invalid font-weight/,
186
+ );
187
+ });
188
+
189
+ it("writes a family name's < as a CSS escape so it cannot spell </style", () => {
190
+ const css = buildFontFaceCss([{ ...good, family: "Evil</style><script>alert(1)</script>" }]);
191
+ expect(css.toLowerCase()).not.toContain("</style");
192
+ expect(css).toContain("Evil\\3c /style>");
193
+ });
194
+ });
package/src/fonts.ts CHANGED
@@ -224,16 +224,53 @@ export function parseFontsCss(cssText: string): ParsedFontFace[] {
224
224
  return faces;
225
225
  }
226
226
 
227
+ const FONT_STYLE_RE = /^(?:normal|italic|oblique(?: +-?\d{1,3}(?:\.\d+)?deg){0,2})$/i;
228
+ const FONT_WEIGHT_RE = /^(?:normal|bold|\d{1,4}(?: +\d{1,4})?)$/i;
229
+ const UNICODE_RANGE_ITEM = "U\\+[0-9A-F?]{1,6}(?:-[0-9A-F]{1,6})?";
230
+ const UNICODE_RANGE_RE = new RegExp(`^${UNICODE_RANGE_ITEM}(?:, *${UNICODE_RANGE_ITEM})*$`, "i");
231
+ /** A same-site asset path: no quotes, parens, whitespace or `<`. */
232
+ const FONT_URL_RE = /^[A-Za-z0-9._~/:%+-]+$/;
233
+
234
+ /**
235
+ * Why `face` cannot be emitted as an `@font-face` rule, or null when it can.
236
+ * The descriptors come from a remote fonts-CSS payload, so each one is held
237
+ * to the grammar a real descriptor has instead of being scrubbed: a value
238
+ * outside it could close the surrounding `<style>` or add rules, and a
239
+ * scrubbed value would ship a silently different font.
240
+ */
241
+ export function fontFaceProblem(face: ResolvedFontFace): string | null {
242
+ if (!FONT_STYLE_RE.test(face.style)) return `font-style ${JSON.stringify(face.style)}`;
243
+ if (!FONT_WEIGHT_RE.test(face.weight)) return `font-weight ${JSON.stringify(face.weight)}`;
244
+ if (face.unicodeRange !== null && !UNICODE_RANGE_RE.test(face.unicodeRange)) {
245
+ return `unicode-range ${JSON.stringify(face.unicodeRange)}`;
246
+ }
247
+ if (!FONT_URL_RE.test(face.publicUrl)) return `url ${JSON.stringify(face.publicUrl)}`;
248
+ return null;
249
+ }
250
+
227
251
  /**
228
252
  * Emit self-hosted @font-face CSS. `font-display: swap` always — text
229
253
  * must render in the fallback while the woff2 loads.
254
+ *
255
+ * Throws on a face {@link fontFaceProblem} refuses; the resolver checks
256
+ * faces first and reports a bad family as unresolved, so reaching the
257
+ * throw means a caller skipped that check.
230
258
  */
231
259
  export function buildFontFaceCss(faces: readonly ResolvedFontFace[]): string {
232
260
  return faces
233
261
  .map((f) => {
262
+ const problem = fontFaceProblem(f);
263
+ if (problem !== null) {
264
+ throw new Error(
265
+ `fonts: refusing the @font-face for ${JSON.stringify(f.family)} — invalid ${problem}. Re-fetch the family or pick another font.`,
266
+ );
267
+ }
268
+ // A CSS string; `<` is written as its CSS escape so no family name
269
+ // can spell `</style` in the surrounding HTML.
270
+ const family = JSON.stringify(f.family).replaceAll("<", "\\3c ");
234
271
  const range = f.unicodeRange !== null ? `unicode-range:${f.unicodeRange};` : "";
235
272
  return (
236
- `@font-face{font-family:${JSON.stringify(f.family)};` +
273
+ `@font-face{font-family:${family};` +
237
274
  `font-style:${f.style};font-weight:${f.weight};font-display:swap;` +
238
275
  `src:url(${f.publicUrl}) format('woff2');${range}}`
239
276
  );
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export * from "./genesis-inventory.js";
25
25
  export * from "./google-models.js";
26
26
  export * from "./image-capabilities.js";
27
27
  export * from "./interactions.js";
28
+ export * from "./link-refs.js";
28
29
  export {
29
30
  type LogContext,
30
31
  type Logger,
@@ -47,6 +48,7 @@ export * from "./preview-scanner.js";
47
48
  export * from "./proposal-status.js";
48
49
  export * from "./provisioning-command.js";
49
50
  export * from "./publish-live-parity.js";
51
+ export * from "./raw-text-escape.js";
50
52
  export * from "./redirect-emit-caddy.js";
51
53
  export * from "./redirect-emit-cloudfront.js";
52
54
  export * from "./redirect-emit-front-door.js";