@caelo-cms/shared 0.10.55 → 0.10.58

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 (72) hide show
  1. package/dist/ai-tools.d.ts +19 -0
  2. package/dist/ai-tools.d.ts.map +1 -1
  3. package/dist/ai-tools.js +19 -9
  4. package/dist/ai-tools.js.map +1 -1
  5. package/dist/auto-update.d.ts +15 -4
  6. package/dist/auto-update.d.ts.map +1 -1
  7. package/dist/auto-update.js +19 -14
  8. package/dist/auto-update.js.map +1 -1
  9. package/dist/google-oauth.d.ts +22 -0
  10. package/dist/google-oauth.d.ts.map +1 -0
  11. package/dist/google-oauth.js +25 -0
  12. package/dist/google-oauth.js.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/link-canonicalise.d.ts +109 -0
  18. package/dist/link-canonicalise.d.ts.map +1 -0
  19. package/dist/link-canonicalise.js +471 -0
  20. package/dist/link-canonicalise.js.map +1 -0
  21. package/dist/link-refs.d.ts +63 -8
  22. package/dist/link-refs.d.ts.map +1 -1
  23. package/dist/link-refs.js +80 -14
  24. package/dist/link-refs.js.map +1 -1
  25. package/dist/media.d.ts.map +1 -1
  26. package/dist/media.js +15 -0
  27. package/dist/media.js.map +1 -1
  28. package/dist/nested-module-render.d.ts +17 -0
  29. package/dist/nested-module-render.d.ts.map +1 -1
  30. package/dist/nested-module-render.js +45 -6
  31. package/dist/nested-module-render.js.map +1 -1
  32. package/dist/platform-updates.d.ts +20 -3
  33. package/dist/platform-updates.d.ts.map +1 -1
  34. package/dist/platform-updates.js +24 -1
  35. package/dist/platform-updates.js.map +1 -1
  36. package/dist/preview-compose.d.ts.map +1 -1
  37. package/dist/preview-compose.js +12 -2
  38. package/dist/preview-compose.js.map +1 -1
  39. package/dist/proposal-status.d.ts +1 -1
  40. package/dist/redirect-emit-gcp.d.ts +11 -1
  41. package/dist/redirect-emit-gcp.d.ts.map +1 -1
  42. package/dist/redirect-emit-gcp.js +16 -2
  43. package/dist/redirect-emit-gcp.js.map +1 -1
  44. package/dist/release-manifest.d.ts +14 -4
  45. package/dist/release-manifest.d.ts.map +1 -1
  46. package/dist/release-manifest.js +19 -3
  47. package/dist/release-manifest.js.map +1 -1
  48. package/dist/theme-normalize.d.ts.map +1 -1
  49. package/dist/theme-normalize.js +17 -2
  50. package/dist/theme-normalize.js.map +1 -1
  51. package/dist/version.d.ts +2 -2
  52. package/dist/version.js +1 -1
  53. package/package.json +1 -1
  54. package/src/ai-tools.ts +22 -11
  55. package/src/auto-update.test.ts +47 -8
  56. package/src/auto-update.ts +29 -14
  57. package/src/google-oauth.ts +27 -0
  58. package/src/index.ts +2 -0
  59. package/src/link-canonicalise.test.ts +387 -0
  60. package/src/link-canonicalise.ts +598 -0
  61. package/src/link-refs.test.ts +206 -0
  62. package/src/link-refs.ts +119 -21
  63. package/src/media.ts +16 -0
  64. package/src/nested-module-render.ts +59 -4
  65. package/src/platform-updates.ts +26 -1
  66. package/src/preview-compose.ts +11 -2
  67. package/src/redirect-emit-gcp.ts +16 -2
  68. package/src/redirect-emit.test.ts +6 -4
  69. package/src/release-manifest.ts +39 -7
  70. package/src/theme-normalize-typography-subfields.test.ts +54 -0
  71. package/src/theme-normalize.ts +19 -2
  72. package/src/version.ts +1 -1
@@ -153,20 +153,22 @@ describe("GCP URL-map rules", () => {
153
153
  });
154
154
 
155
155
  it("refuses a plan over the 1,000-predicate quota with the counts, before any change", () => {
156
+ // The base rule routes /api/* and the remote-MCP discovery paths (#806).
157
+ expect(GCP_PUBLIC_BASE_PATHS).toHaveLength(3);
156
158
  const base = [{ paths: [...GCP_PUBLIC_BASE_PATHS] }];
157
159
  const rows = (n: number) => Array.from({ length: n }, (_, i) => row(`/p${i}`, "/x"));
158
- expect(() => assertGcpRedirectBudget(planOf(rows(499)), base)).not.toThrow();
160
+ expect(() => assertGcpRedirectBudget(planOf(rows(498)), base)).not.toThrow();
159
161
  try {
160
- assertGcpRedirectBudget(planOf(rows(500)), base);
162
+ assertGcpRedirectBudget(planOf(rows(499)), base);
161
163
  throw new Error("expected GcpUrlMapBudgetError");
162
164
  } catch (e) {
163
165
  expect(e).toBeInstanceOf(GcpUrlMapBudgetError);
164
166
  expect((e as GcpUrlMapBudgetError).predicatesNeeded).toBe(1001);
165
- expect((e as Error).message).toContain("500 redirect(s) need 1001 paths");
167
+ expect((e as Error).message).toContain("499 redirect(s) need 1001 paths");
166
168
  expect((e as Error).message).toContain("bulk_delete_redirects");
167
169
  }
168
170
  // File-like paths cost one predicate each.
169
- const files = Array.from({ length: 999 }, (_, i) => row(`/p${i}.html`, "/x"));
171
+ const files = Array.from({ length: 997 }, (_, i) => row(`/p${i}.html`, "/x"));
170
172
  expect(() => assertGcpRedirectBudget(planOf(files), base)).not.toThrow();
171
173
  });
172
174
 
@@ -71,12 +71,23 @@ export const SIGSTORE_BUNDLE_MEDIA_TYPE = "application/vnd.dev.sigstore.bundle.v
71
71
  /** Why fetching the artifact failed. */
72
72
  export type ReleaseArtifactErrorCode = "registry" | "artifact-shape" | "blob-digest";
73
73
 
74
+ /** Why {@link fetchReleaseArtifact} failed. */
75
+ export interface ReleaseArtifactError {
76
+ readonly code: ReleaseArtifactErrorCode;
77
+ readonly message: string;
78
+ /**
79
+ * Set on `registry` errors a retry may cure: the registry could not be
80
+ * reached, timed out, rate-limited (429) or failed (5xx). Absent when the
81
+ * registry answered that the manifest, tag or blob does not exist (404 and
82
+ * other 4xx) — retrying that changes nothing, so callers that would
83
+ * otherwise "try again later" must treat it as a verdict.
84
+ */
85
+ readonly transient?: true;
86
+ }
87
+
74
88
  export type ReleaseArtifactResult =
75
89
  | { readonly ok: true; readonly manifestBytes: Uint8Array; readonly bundleBytes: Uint8Array }
76
- | {
77
- readonly ok: false;
78
- readonly error: { readonly code: ReleaseArtifactErrorCode; readonly message: string };
79
- };
90
+ | { readonly ok: false; readonly error: ReleaseArtifactError };
80
91
 
81
92
  /** Where and how {@link fetchReleaseArtifact} reaches the registry. */
82
93
  export interface ReleaseArtifactDeps {
@@ -101,6 +112,22 @@ const fail = (code: ReleaseArtifactErrorCode, message: string): ReleaseArtifactR
101
112
  error: { code, message },
102
113
  });
103
114
 
115
+ /** A `registry` failure a retry may cure (see {@link ReleaseArtifactError.transient}). */
116
+ const failTransient = (message: string): ReleaseArtifactResult => ({
117
+ ok: false,
118
+ error: { code: "registry", message, transient: true },
119
+ });
120
+
121
+ /** Whether an HTTP status says "try again" rather than "it does not exist". */
122
+ function isTransientStatus(status: number): boolean {
123
+ return status >= 500 || status === 429 || status === 408;
124
+ }
125
+
126
+ /** A non-OK registry response as a failure, transient or not by its status. */
127
+ function failStatus(status: number, message: string): ReleaseArtifactResult {
128
+ return isTransientStatus(status) ? failTransient(message) : fail("registry", message);
129
+ }
130
+
104
131
  /**
105
132
  * Fetch the release manifest artifact of `version` and both its layers,
106
133
  * each checked against its content digest. Verifies NO signature: callers
@@ -120,6 +147,11 @@ export async function fetchReleaseArtifact(
120
147
  headers: { Accept: "application/vnd.oci.image.manifest.v1+json" },
121
148
  });
122
149
  if (!res.ok) {
150
+ if (isTransientStatus(res.status)) {
151
+ return failTransient(
152
+ `the registry at ${base} could not serve the release manifest of v${version} (HTTP ${res.status}); try again later.`,
153
+ );
154
+ }
123
155
  return fail(
124
156
  "registry",
125
157
  `no release manifest for v${version} at ${base} (HTTP ${res.status}). Releases before the signed manifest (epic #696) have none; otherwise check the version exists.`,
@@ -131,7 +163,7 @@ export async function fetchReleaseArtifact(
131
163
  }
132
164
  artifact = parsed.data;
133
165
  } catch (e) {
134
- return fail("registry", `registry unreachable: ${e instanceof Error ? e.message : String(e)}`);
166
+ return failTransient(`registry unreachable: ${e instanceof Error ? e.message : String(e)}`);
135
167
  }
136
168
 
137
169
  if (artifact.artifactType !== RELEASE_MANIFEST_ARTIFACT_TYPE) {
@@ -153,7 +185,7 @@ export async function fetchReleaseArtifact(
153
185
 
154
186
  const blob = async (layerDigest: string): Promise<Uint8Array | ReleaseArtifactResult> => {
155
187
  const res = await deps.fetch(`${base}/blobs/${layerDigest}`);
156
- if (!res.ok) return fail("registry", `blob ${layerDigest}: HTTP ${res.status}`);
188
+ if (!res.ok) return failStatus(res.status, `blob ${layerDigest}: HTTP ${res.status}`);
157
189
  const bytes = new Uint8Array(await res.arrayBuffer());
158
190
  const actual = await sha256Digest(bytes);
159
191
  if (actual !== layerDigest) {
@@ -171,7 +203,7 @@ export async function fetchReleaseArtifact(
171
203
  if (!(bundleBytes instanceof Uint8Array)) return bundleBytes;
172
204
  return { ok: true, manifestBytes, bundleBytes };
173
205
  } catch (e) {
174
- return fail("registry", `registry unreachable: ${e instanceof Error ? e.message : String(e)}`);
206
+ return failTransient(`registry unreachable: ${e instanceof Error ? e.message : String(e)}`);
175
207
  }
176
208
  }
177
209
 
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+ /**
3
+ * Regression: saving the Typography tab posts every sub-field of a named
4
+ * style as its own `typography.<style>.<sub>` key, with lineHeight /
5
+ * fontWeight coerced to numbers. Numbers used to skip the composite wrap
6
+ * (so `$value` became a bare `1.6`) and sibling sub-fields overwrote each
7
+ * other on the shared composite path.
8
+ */
9
+ import { describe, expect, it } from "bun:test";
10
+ import { normalizeTokens } from "./theme-normalize.js";
11
+ import { applyDtcgWrites, themeDocument } from "./themes.js";
12
+
13
+ describe("normalizeTokens typography sub-fields", () => {
14
+ it("wraps numeric sub-field values into the composite", () => {
15
+ const r = normalizeTokens({ "typography.body.lineHeight": 1.6 });
16
+ expect(r.set["typography.body"]).toEqual({ lineHeight: 1.6 });
17
+ });
18
+
19
+ it("merges several sub-fields of one style into one composite", () => {
20
+ const r = normalizeTokens({
21
+ fontHeading: "Inter, sans-serif",
22
+ "typography.heading.fontSize": "3rem",
23
+ "typography.heading.fontWeight": 650,
24
+ "typography.heading.lineHeight": 1.08,
25
+ });
26
+ expect(r.set["typography.heading"]).toEqual({
27
+ fontFamily: "Inter, sans-serif",
28
+ fontSize: "3rem",
29
+ fontWeight: 650,
30
+ lineHeight: 1.08,
31
+ });
32
+ expect(r.canonicalPaths).toEqual(["typography.heading"]);
33
+ });
34
+
35
+ it("produces a valid document when applied over an existing theme", () => {
36
+ const current = {
37
+ typography: {
38
+ body: { $type: "typography", $value: { fontFamily: "Inter", fontSize: "1rem" } },
39
+ },
40
+ };
41
+ const r = normalizeTokens({
42
+ "typography.body.fontWeight": 400,
43
+ "typography.body.lineHeight": 1.6,
44
+ });
45
+ const next = applyDtcgWrites(current as never, r.set, r.types);
46
+ expect(themeDocument.safeParse(next).success).toBe(true);
47
+ expect((next.typography as Record<string, { $value: unknown }>).body?.$value).toEqual({
48
+ fontFamily: "Inter",
49
+ fontSize: "1rem",
50
+ fontWeight: 400,
51
+ lineHeight: 1.6,
52
+ });
53
+ });
54
+ });
@@ -284,8 +284,14 @@ export function normalizeTokens(input: Record<string, unknown>): NormalizeResult
284
284
  // resulting document validates. applyDtcgWrites merges this into
285
285
  // any existing typography composite at the path so partial updates
286
286
  // don't erase sibling sub-fields.
287
+ // Numbers wrap too: the admin form coerces `.lineHeight` / `.fontWeight`
288
+ // to numbers, and an unwrapped `1.6` would replace the whole composite.
287
289
  let storedValue: unknown = rawValue;
288
- if (resolved.compositeWrap && typeof rawValue === "string" && !ALIAS_REGEX.test(rawValue)) {
290
+ if (
291
+ resolved.compositeWrap &&
292
+ (typeof rawValue === "number" ||
293
+ (typeof rawValue === "string" && !ALIAS_REGEX.test(rawValue)))
294
+ ) {
289
295
  storedValue = { [resolved.compositeWrap]: rawValue };
290
296
  } else if (
291
297
  resolved.inferredType === "gradient" &&
@@ -303,10 +309,17 @@ export function normalizeTokens(input: Record<string, unknown>): NormalizeResult
303
309
  // rather than us silently inventing a gradient (CLAUDE.md §2).
304
310
  storedValue = gradientObjectToCss(rawValue as Record<string, unknown>) ?? rawValue;
305
311
  }
312
+ // Several sub-field inputs (`typography.body.fontSize` +
313
+ // `typography.body.lineHeight`) collapse onto the same composite
314
+ // path; merge them so the last one doesn't silently drop the others.
315
+ const prior = set[resolved.path];
316
+ if (resolved.compositeWrap && isPlainObject(prior) && isPlainObject(storedValue)) {
317
+ storedValue = { ...prior, ...storedValue };
318
+ }
306
319
  set[resolved.path] = storedValue;
307
320
  types[resolved.path] = resolved.inferredType;
308
321
  if (envelopeDescription !== undefined) descriptions[resolved.path] = envelopeDescription;
309
- paths.push(resolved.path);
322
+ if (!paths.includes(resolved.path)) paths.push(resolved.path);
310
323
  }
311
324
 
312
325
  return { set, types, descriptions, canonicalPaths: paths };
@@ -721,3 +734,7 @@ function similarity(a: string, b: string): number {
721
734
  function tokenise(s: string): string[] {
722
735
  return s.split(/[.\-_]/).filter((p) => p.length > 0);
723
736
  }
737
+
738
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
739
+ return v !== null && typeof v === "object" && !Array.isArray(v);
740
+ }
package/src/version.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * follow standard SemVer.
23
23
  */
24
24
 
25
- export const CAELO_VERSION = "0.10.55";
25
+ export const CAELO_VERSION = "0.10.58";
26
26
 
27
27
  /**
28
28
  * Deprecated alias for back-compat — early P17 work spelled this