@caelo-cms/shared 0.10.22 → 0.10.24
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.
- package/dist/ai-tools.d.ts +289 -273
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +342 -323
- package/dist/ai-tools.js.map +1 -1
- package/dist/auth-forms.d.ts.map +1 -1
- package/dist/auth-forms.js +4 -1
- package/dist/auth-forms.js.map +1 -1
- package/dist/base-css.d.ts +35 -0
- package/dist/base-css.d.ts.map +1 -0
- package/dist/base-css.js +40 -0
- package/dist/base-css.js.map +1 -0
- package/dist/build-page.d.ts +330 -0
- package/dist/build-page.d.ts.map +1 -0
- package/dist/build-page.js +282 -0
- package/dist/build-page.js.map +1 -0
- package/dist/content.d.ts +322 -9
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +354 -11
- package/dist/content.js.map +1 -1
- package/dist/css-gradient-scan.d.ts +14 -0
- package/dist/css-gradient-scan.d.ts.map +1 -0
- package/dist/css-gradient-scan.js +81 -0
- package/dist/css-gradient-scan.js.map +1 -0
- package/dist/css-var-scan.d.ts +56 -0
- package/dist/css-var-scan.d.ts.map +1 -0
- package/dist/css-var-scan.js +97 -0
- package/dist/css-var-scan.js.map +1 -0
- package/dist/design-draft-shell.d.ts +21 -0
- package/dist/design-draft-shell.d.ts.map +1 -0
- package/dist/design-draft-shell.js +81 -0
- package/dist/design-draft-shell.js.map +1 -0
- package/dist/design-manifest.d.ts +36 -0
- package/dist/design-manifest.d.ts.map +1 -0
- package/dist/design-manifest.js +90 -0
- package/dist/design-manifest.js.map +1 -0
- package/dist/fonts.d.ts +89 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +241 -0
- package/dist/fonts.js.map +1 -0
- package/dist/genesis-inventory.d.ts +32 -0
- package/dist/genesis-inventory.d.ts.map +1 -0
- package/dist/genesis-inventory.js +186 -0
- package/dist/genesis-inventory.js.map +1 -0
- package/dist/genesis.d.ts +102 -0
- package/dist/genesis.d.ts.map +1 -0
- package/dist/genesis.js +145 -0
- package/dist/genesis.js.map +1 -0
- package/dist/i18n.d.ts +29 -36
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +53 -128
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/interactions.d.ts +23 -0
- package/dist/interactions.d.ts.map +1 -0
- package/dist/interactions.js +44 -0
- package/dist/interactions.js.map +1 -0
- package/dist/media.d.ts +101 -16
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +126 -15
- package/dist/media.js.map +1 -1
- package/dist/page-log.d.ts +94 -0
- package/dist/page-log.d.ts.map +1 -0
- package/dist/page-log.js +111 -0
- package/dist/page-log.js.map +1 -0
- package/dist/preview-compose.d.ts +85 -12
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +157 -67
- package/dist/preview-compose.js.map +1 -1
- package/dist/proposal-status.d.ts +40 -0
- package/dist/proposal-status.d.ts.map +1 -0
- package/dist/proposal-status.js +34 -0
- package/dist/proposal-status.js.map +1 -0
- package/dist/responsive-images.d.ts +64 -0
- package/dist/responsive-images.d.ts.map +1 -0
- package/dist/responsive-images.js +98 -0
- package/dist/responsive-images.js.map +1 -0
- package/dist/safe-keys.d.ts +9 -0
- package/dist/safe-keys.d.ts.map +1 -0
- package/dist/safe-keys.js +20 -0
- package/dist/safe-keys.js.map +1 -0
- package/dist/seo.d.ts +11 -16
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +7 -23
- package/dist/seo.js.map +1 -1
- package/dist/skills.d.ts +14 -68
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +19 -113
- package/dist/skills.js.map +1 -1
- package/dist/strip-cdata.d.ts +7 -0
- package/dist/strip-cdata.d.ts.map +1 -0
- package/dist/strip-cdata.js +48 -0
- package/dist/strip-cdata.js.map +1 -0
- package/dist/structured-sets.d.ts +6 -52
- package/dist/structured-sets.d.ts.map +1 -1
- package/dist/structured-sets.js +6 -68
- package/dist/structured-sets.js.map +1 -1
- package/dist/subagents.d.ts +105 -3
- package/dist/subagents.d.ts.map +1 -1
- package/dist/subagents.js +224 -41
- package/dist/subagents.js.map +1 -1
- package/dist/template-engine.d.ts +85 -0
- package/dist/template-engine.d.ts.map +1 -0
- package/dist/template-engine.js +403 -0
- package/dist/template-engine.js.map +1 -0
- package/dist/theme-importers/auto-detect.d.ts +26 -0
- package/dist/theme-importers/auto-detect.d.ts.map +1 -0
- package/dist/theme-importers/auto-detect.js +42 -0
- package/dist/theme-importers/auto-detect.js.map +1 -0
- package/dist/theme-importers/css-comments.d.ts +12 -0
- package/dist/theme-importers/css-comments.d.ts.map +1 -0
- package/dist/theme-importers/css-comments.js +15 -0
- package/dist/theme-importers/css-comments.js.map +1 -0
- package/dist/theme-importers/dtcg.d.ts +46 -0
- package/dist/theme-importers/dtcg.d.ts.map +1 -0
- package/dist/theme-importers/dtcg.js +111 -0
- package/dist/theme-importers/dtcg.js.map +1 -0
- package/dist/theme-importers/loose.d.ts +3 -0
- package/dist/theme-importers/loose.d.ts.map +1 -0
- package/dist/theme-importers/loose.js +76 -0
- package/dist/theme-importers/loose.js.map +1 -0
- package/dist/theme-importers/shadcn.d.ts +24 -0
- package/dist/theme-importers/shadcn.d.ts.map +1 -0
- package/dist/theme-importers/shadcn.js +135 -0
- package/dist/theme-importers/shadcn.js.map +1 -0
- package/dist/theme-importers/style-dictionary.d.ts +17 -0
- package/dist/theme-importers/style-dictionary.d.ts.map +1 -0
- package/dist/theme-importers/style-dictionary.js +125 -0
- package/dist/theme-importers/style-dictionary.js.map +1 -0
- package/dist/theme-importers/tailwind.d.ts +3 -0
- package/dist/theme-importers/tailwind.d.ts.map +1 -0
- package/dist/theme-importers/tailwind.js +218 -0
- package/dist/theme-importers/tailwind.js.map +1 -0
- package/dist/theme-literal-binding.d.ts +37 -0
- package/dist/theme-literal-binding.d.ts.map +1 -0
- package/dist/theme-literal-binding.js +138 -0
- package/dist/theme-literal-binding.js.map +1 -0
- package/dist/theme-normalize.d.ts +31 -0
- package/dist/theme-normalize.d.ts.map +1 -0
- package/dist/theme-normalize.js +587 -0
- package/dist/theme-normalize.js.map +1 -0
- package/dist/theme-ramp.d.ts +55 -0
- package/dist/theme-ramp.d.ts.map +1 -0
- package/dist/theme-ramp.js +149 -0
- package/dist/theme-ramp.js.map +1 -0
- package/dist/theme-render.d.ts +105 -0
- package/dist/theme-render.d.ts.map +1 -0
- package/dist/theme-render.js +441 -0
- package/dist/theme-render.js.map +1 -0
- package/dist/themes-errors.d.ts +109 -0
- package/dist/themes-errors.d.ts.map +1 -0
- package/dist/themes-errors.js +170 -0
- package/dist/themes-errors.js.map +1 -0
- package/dist/themes.d.ts +343 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +697 -0
- package/dist/themes.js.map +1 -0
- package/dist/version.d.ts +7 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +6 -3
- package/dist/version.js.map +1 -1
- package/package.json +10 -2
- package/src/__tests__/redos-hardening.test.ts +160 -0
- package/src/ai-tools-add-module-modes.test.ts +106 -0
- package/src/ai-tools-position.test.ts +134 -0
- package/src/ai-tools.test.ts +81 -0
- package/src/ai-tools.ts +1105 -0
- package/src/auth-forms.ts +36 -0
- package/src/base-css.ts +42 -0
- package/src/build-page.test.ts +228 -0
- package/src/build-page.ts +319 -0
- package/src/cap-failures.ts +67 -0
- package/src/content.test.ts +170 -0
- package/src/content.ts +620 -0
- package/src/context.ts +43 -0
- package/src/css-gradient-scan.ts +88 -0
- package/src/css-var-scan.test.ts +96 -0
- package/src/css-var-scan.ts +144 -0
- package/src/derive-module-type.test.ts +80 -0
- package/src/design-draft-shell.test.ts +85 -0
- package/src/design-draft-shell.ts +109 -0
- package/src/design-manifest.ts +93 -0
- package/src/fonts.test.ts +157 -0
- package/src/fonts.ts +296 -0
- package/src/genesis-inventory.test.ts +86 -0
- package/src/genesis-inventory.ts +215 -0
- package/src/genesis-sanitize.test.ts +35 -0
- package/src/genesis.ts +158 -0
- package/src/i18n.test.ts +58 -0
- package/src/i18n.ts +91 -0
- package/src/index.test.ts +10 -0
- package/src/index.ts +59 -0
- package/src/interactions.ts +48 -0
- package/src/logger.ts +147 -0
- package/src/media.test.ts +160 -0
- package/src/media.ts +355 -0
- package/src/page-log.test.ts +163 -0
- package/src/page-log.ts +124 -0
- package/src/preview-compose.test.ts +637 -0
- package/src/preview-compose.ts +656 -0
- package/src/preview-scanner.test.ts +96 -0
- package/src/preview-scanner.ts +214 -0
- package/src/proposal-status.test.ts +69 -0
- package/src/proposal-status.ts +40 -0
- package/src/responsive-images.test.ts +104 -0
- package/src/responsive-images.ts +151 -0
- package/src/result.ts +29 -0
- package/src/safe-keys.ts +21 -0
- package/src/seo.test.ts +194 -0
- package/src/seo.ts +233 -0
- package/src/skills.ts +48 -0
- package/src/snapshots.test.ts +80 -0
- package/src/snapshots.ts +81 -0
- package/src/strip-cdata.test.ts +41 -0
- package/src/strip-cdata.ts +50 -0
- package/src/structured-sets.ts +114 -0
- package/src/subagents.test.ts +262 -0
- package/src/subagents.ts +432 -0
- package/src/template-engine.test.ts +379 -0
- package/src/template-engine.ts +520 -0
- package/src/theme-gradient.test.ts +92 -0
- package/src/theme-importers/__tests__/proto-pollution.test.ts +54 -0
- package/src/theme-importers/auto-detect.ts +84 -0
- package/src/theme-importers/css-comments.ts +15 -0
- package/src/theme-importers/dtcg.ts +106 -0
- package/src/theme-importers/loose.ts +76 -0
- package/src/theme-importers/shadcn.ts +133 -0
- package/src/theme-importers/style-dictionary.ts +125 -0
- package/src/theme-importers/tailwind.ts +217 -0
- package/src/theme-literal-binding.test.ts +71 -0
- package/src/theme-literal-binding.ts +159 -0
- package/src/theme-motion.test.ts +115 -0
- package/src/theme-normalize-envelope.test.ts +43 -0
- package/src/theme-normalize-gradient.test.ts +135 -0
- package/src/theme-normalize.ts +661 -0
- package/src/theme-ramp.ts +187 -0
- package/src/theme-render-sanitize.test.ts +45 -0
- package/src/theme-render.test.ts +119 -0
- package/src/theme-render.ts +487 -0
- package/src/theme-shadow.test.ts +56 -0
- package/src/themes-errors.ts +199 -0
- package/src/themes.ts +842 -0
- package/src/version.ts +66 -0
- package/dist/translation.d.ts +0 -127
- package/dist/translation.d.ts.map +0 -1
- package/dist/translation.js +0 -208
- package/dist/translation.js.map +0 -1
package/src/result.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discriminated-union Result type. Used at the Query API boundary where errors are
|
|
5
|
+
* expected (validation, RLS denials, unknown operations) and throwing is reserved
|
|
6
|
+
* for genuinely exceptional conditions (lost DB connection, logic bug).
|
|
7
|
+
*
|
|
8
|
+
* Per CLAUDE.md §4: "errors are values where it matters".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type Ok<T> = { readonly ok: true; readonly value: T };
|
|
12
|
+
export type Err<E> = { readonly ok: false; readonly error: E };
|
|
13
|
+
export type Result<T, E> = Ok<T> | Err<E>;
|
|
14
|
+
|
|
15
|
+
export function ok<T>(value: T): Ok<T> {
|
|
16
|
+
return { ok: true, value };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function err<E>(error: E): Err<E> {
|
|
20
|
+
return { ok: false, error };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isOk<T, E>(result: Result<T, E>): result is Ok<T> {
|
|
24
|
+
return result.ok;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isErr<T, E>(result: Result<T, E>): result is Err<E> {
|
|
28
|
+
return !result.ok;
|
|
29
|
+
}
|
package/src/safe-keys.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keys that, when used as a dynamic property name on a plain object,
|
|
5
|
+
* mutate the prototype chain instead of the object's own data — the
|
|
6
|
+
* prototype-pollution vector. Any code that assigns into an object with
|
|
7
|
+
* a key derived from external input (imported CSS variable names, dotted
|
|
8
|
+
* write paths from forms/AI) must reject these first.
|
|
9
|
+
*/
|
|
10
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* True when `key` would pollute the prototype chain if used as a dynamic
|
|
14
|
+
* assignment target (`obj[key] = …`). Callers skip or reject such keys.
|
|
15
|
+
*
|
|
16
|
+
* @param key The candidate property name (e.g. a parsed token name or a
|
|
17
|
+
* path segment).
|
|
18
|
+
*/
|
|
19
|
+
export function isUnsafeKey(key: string): boolean {
|
|
20
|
+
return UNSAFE_KEYS.has(key);
|
|
21
|
+
}
|
package/src/seo.test.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import {
|
|
5
|
+
injectSeoIntoHead,
|
|
6
|
+
renderSeoHead,
|
|
7
|
+
resolveCanonicalUrl,
|
|
8
|
+
seoAutofillInputSchema,
|
|
9
|
+
seoOptimizeInputSchema,
|
|
10
|
+
seoSetInputSchema,
|
|
11
|
+
siteDefaultsSetSeoInputSchema,
|
|
12
|
+
} from "./seo.js";
|
|
13
|
+
|
|
14
|
+
describe("resolveCanonicalUrl", () => {
|
|
15
|
+
it("uses the explicit override when provided", () => {
|
|
16
|
+
expect(
|
|
17
|
+
resolveCanonicalUrl({
|
|
18
|
+
siteBaseUrl: "https://example.com",
|
|
19
|
+
pageSlug: "anything",
|
|
20
|
+
override: "https://canonical.example.com/x",
|
|
21
|
+
}),
|
|
22
|
+
).toBe("https://canonical.example.com/x");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders home as the root path", () => {
|
|
26
|
+
expect(
|
|
27
|
+
resolveCanonicalUrl({
|
|
28
|
+
siteBaseUrl: "https://example.com",
|
|
29
|
+
pageSlug: "home",
|
|
30
|
+
override: null,
|
|
31
|
+
}),
|
|
32
|
+
).toBe("https://example.com/");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("trims trailing slash on the base URL", () => {
|
|
36
|
+
expect(
|
|
37
|
+
resolveCanonicalUrl({
|
|
38
|
+
siteBaseUrl: "https://example.com/",
|
|
39
|
+
pageSlug: "about",
|
|
40
|
+
override: null,
|
|
41
|
+
}),
|
|
42
|
+
).toBe("https://example.com/about/");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("v0.2.85 — pageUrlStyle='no-extension'", () => {
|
|
46
|
+
it("omits the trailing slash for non-home pages", () => {
|
|
47
|
+
expect(
|
|
48
|
+
resolveCanonicalUrl({
|
|
49
|
+
siteBaseUrl: "https://example.com",
|
|
50
|
+
pageSlug: "about",
|
|
51
|
+
override: null,
|
|
52
|
+
pageUrlStyle: "no-extension",
|
|
53
|
+
}),
|
|
54
|
+
).toBe("https://example.com/about");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("keeps the root URL for the home page", () => {
|
|
58
|
+
expect(
|
|
59
|
+
resolveCanonicalUrl({
|
|
60
|
+
siteBaseUrl: "https://example.com",
|
|
61
|
+
pageSlug: "home",
|
|
62
|
+
override: null,
|
|
63
|
+
pageUrlStyle: "no-extension",
|
|
64
|
+
}),
|
|
65
|
+
).toBe("https://example.com/");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("default style preserves pre-v0.2.85 trailing-slash behavior", () => {
|
|
69
|
+
expect(
|
|
70
|
+
resolveCanonicalUrl({
|
|
71
|
+
siteBaseUrl: "https://example.com",
|
|
72
|
+
pageSlug: "about",
|
|
73
|
+
override: null,
|
|
74
|
+
}),
|
|
75
|
+
).toBe("https://example.com/about/");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("renderSeoHead", () => {
|
|
81
|
+
const base = {
|
|
82
|
+
title: "Welcome",
|
|
83
|
+
metaDescription: "A sample description.",
|
|
84
|
+
canonical: "https://example.com/",
|
|
85
|
+
noindex: false,
|
|
86
|
+
ogImageUrl: null,
|
|
87
|
+
organization: {},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
it("emits canonical + og:type + og:url for the simplest valid input", () => {
|
|
91
|
+
const head = renderSeoHead(base);
|
|
92
|
+
expect(head).toContain("<title>Welcome</title>");
|
|
93
|
+
expect(head).toContain('<meta name="description" content="A sample description." />');
|
|
94
|
+
expect(head).toContain('<link rel="canonical" href="https://example.com/" />');
|
|
95
|
+
expect(head).toContain('<meta property="og:type" content="website" />');
|
|
96
|
+
expect(head).toContain('<meta property="og:url" content="https://example.com/" />');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("emits noindex meta only when set", () => {
|
|
100
|
+
expect(renderSeoHead(base)).not.toContain('content="noindex"');
|
|
101
|
+
expect(renderSeoHead({ ...base, noindex: true })).toContain(
|
|
102
|
+
'<meta name="robots" content="noindex" />',
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("emits og:image and twitter summary_large_image when provided", () => {
|
|
107
|
+
const head = renderSeoHead({ ...base, ogImageUrl: "https://example.com/_assets/x/orig.png" });
|
|
108
|
+
expect(head).toContain(
|
|
109
|
+
'<meta property="og:image" content="https://example.com/_assets/x/orig.png" />',
|
|
110
|
+
);
|
|
111
|
+
expect(head).toContain('<meta name="twitter:card" content="summary_large_image" />');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("emits a JSON-LD WebPage block; encodes the < character to avoid script-tag breaks", () => {
|
|
115
|
+
const head = renderSeoHead({
|
|
116
|
+
...base,
|
|
117
|
+
title: "Tag <foo>",
|
|
118
|
+
organization: { name: "Caelo Inc.", url: "https://caelo.example" },
|
|
119
|
+
});
|
|
120
|
+
expect(head).toContain('<script type="application/ld+json">');
|
|
121
|
+
expect(head).not.toContain("<foo>"); // angle bracket inside script ld+json should be escaped
|
|
122
|
+
expect(head).toContain('"publisher"');
|
|
123
|
+
expect(head).toContain('"Caelo Inc."');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("HTML-encodes attribute values to block injection", () => {
|
|
127
|
+
const head = renderSeoHead({
|
|
128
|
+
...base,
|
|
129
|
+
title: 'Quote "test"',
|
|
130
|
+
metaDescription: "<script>alert(1)</script>",
|
|
131
|
+
});
|
|
132
|
+
expect(head).toContain('content="Quote "test""');
|
|
133
|
+
expect(head).toContain("<script>alert(1)</script>");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("injectSeoIntoHead", () => {
|
|
138
|
+
it("strips a layout-supplied <title> and injects the head block before </head>", () => {
|
|
139
|
+
const html = "<html><head><title>OldTitle</title></head><body>x</body></html>";
|
|
140
|
+
const out = injectSeoIntoHead(html, "<title>NewTitle</title>");
|
|
141
|
+
expect(out).not.toContain("OldTitle");
|
|
142
|
+
expect(out).toContain("NewTitle");
|
|
143
|
+
expect(out.indexOf("NewTitle")).toBeLessThan(out.indexOf("</head>"));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("falls back to prepending when the document has no </head>", () => {
|
|
147
|
+
expect(injectSeoIntoHead("<body>x</body>", "<title>X</title>")).toContain("<title>X</title>");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("schemas", () => {
|
|
152
|
+
it("seoAutofillInputSchema rejects empty meta description", () => {
|
|
153
|
+
const r = seoAutofillInputSchema.safeParse({
|
|
154
|
+
pageId: "11111111-1111-4111-8111-111111111111",
|
|
155
|
+
metaDescription: "",
|
|
156
|
+
});
|
|
157
|
+
expect(r.success).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("seoOptimizeInputSchema accepts context near the cap", () => {
|
|
161
|
+
const r = seoOptimizeInputSchema.safeParse({
|
|
162
|
+
pageId: "11111111-1111-4111-8111-111111111111",
|
|
163
|
+
metaDescription: "A description.",
|
|
164
|
+
context: "x".repeat(3500),
|
|
165
|
+
});
|
|
166
|
+
expect(r.success).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("seoOptimizeInputSchema rejects context above the cap", () => {
|
|
170
|
+
const r = seoOptimizeInputSchema.safeParse({
|
|
171
|
+
pageId: "11111111-1111-4111-8111-111111111111",
|
|
172
|
+
metaDescription: "A description.",
|
|
173
|
+
context: "x".repeat(4001),
|
|
174
|
+
});
|
|
175
|
+
expect(r.success).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("seoSetInputSchema rejects priority above 1", () => {
|
|
179
|
+
const r = seoSetInputSchema.safeParse({
|
|
180
|
+
pageId: "11111111-1111-4111-8111-111111111111",
|
|
181
|
+
priority: 1.1,
|
|
182
|
+
});
|
|
183
|
+
expect(r.success).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("siteDefaultsSetSeoInputSchema rejects non-URL siteBaseUrl", () => {
|
|
187
|
+
const r = siteDefaultsSetSeoInputSchema.safeParse({
|
|
188
|
+
siteBaseUrl: "not a url",
|
|
189
|
+
sitemapEnabled: true,
|
|
190
|
+
organizationJson: {},
|
|
191
|
+
});
|
|
192
|
+
expect(r.success).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
});
|
package/src/seo.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase 8 — SEO primitives. Per-page SEO is structured fields only;
|
|
5
|
+
* the renderer projects them into <head> meta + canonical + JSON-LD.
|
|
6
|
+
* Per CLAUDE.md §2 "no raw HTML into <head>" — every Zod schema below
|
|
7
|
+
* is on `.strict()` so the AI cannot smuggle additional keys.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { isHomeSlug } from "./i18n.js";
|
|
12
|
+
|
|
13
|
+
export const CHANGEFREQ_VALUES = [
|
|
14
|
+
"always",
|
|
15
|
+
"hourly",
|
|
16
|
+
"daily",
|
|
17
|
+
"weekly",
|
|
18
|
+
"monthly",
|
|
19
|
+
"yearly",
|
|
20
|
+
"never",
|
|
21
|
+
] as const;
|
|
22
|
+
export type Changefreq = (typeof CHANGEFREQ_VALUES)[number];
|
|
23
|
+
|
|
24
|
+
/** Recommended length caps — server enforces, client trims with feedback. */
|
|
25
|
+
export const SEO_TITLE_RECOMMENDED_MAX = 60;
|
|
26
|
+
export const SEO_DESCRIPTION_RECOMMENDED_MAX = 160;
|
|
27
|
+
|
|
28
|
+
export const SEO_DESCRIPTION_HARD_MAX = 320;
|
|
29
|
+
export const SEO_CANONICAL_MAX = 2048;
|
|
30
|
+
|
|
31
|
+
export const seoSetInputSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
pageId: z.string().uuid(),
|
|
34
|
+
metaDescription: z.string().max(SEO_DESCRIPTION_HARD_MAX).optional(),
|
|
35
|
+
ogImageAssetId: z.string().uuid().nullable().optional(),
|
|
36
|
+
canonicalUrl: z.string().max(SEO_CANONICAL_MAX).nullable().optional(),
|
|
37
|
+
noindex: z.boolean().optional(),
|
|
38
|
+
changefreq: z.enum(CHANGEFREQ_VALUES).optional(),
|
|
39
|
+
priority: z.number().min(0).max(1).optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict();
|
|
42
|
+
export type SeoSetInput = z.infer<typeof seoSetInputSchema>;
|
|
43
|
+
|
|
44
|
+
export const seoAutofillInputSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
pageId: z.string().uuid(),
|
|
47
|
+
metaDescription: z.string().min(1).max(SEO_DESCRIPTION_HARD_MAX),
|
|
48
|
+
ogImageAssetId: z.string().uuid().nullable().optional(),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
export type SeoAutofillInput = z.infer<typeof seoAutofillInputSchema>;
|
|
52
|
+
|
|
53
|
+
export const seoOptimizeInputSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
pageId: z.string().uuid(),
|
|
56
|
+
metaDescription: z.string().min(1).max(SEO_DESCRIPTION_HARD_MAX),
|
|
57
|
+
ogImageAssetId: z.string().uuid().nullable().optional(),
|
|
58
|
+
/** Optional user-supplied context (keyword research, intent shifts) recorded in audit. */
|
|
59
|
+
context: z.string().max(4000).optional(),
|
|
60
|
+
})
|
|
61
|
+
.strict();
|
|
62
|
+
export type SeoOptimizeInput = z.infer<typeof seoOptimizeInputSchema>;
|
|
63
|
+
|
|
64
|
+
export const siteDefaultsSetSeoInputSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
siteBaseUrl: z
|
|
67
|
+
.string()
|
|
68
|
+
.min(1)
|
|
69
|
+
.max(2048)
|
|
70
|
+
.url("siteBaseUrl must be an absolute URL (https://example.com)"),
|
|
71
|
+
sitemapEnabled: z.boolean(),
|
|
72
|
+
organizationJson: z
|
|
73
|
+
.object({
|
|
74
|
+
name: z.string().max(256).optional(),
|
|
75
|
+
url: z.string().max(2048).optional(),
|
|
76
|
+
logo: z.string().max(2048).optional(),
|
|
77
|
+
sameAs: z.array(z.string().max(2048)).max(20).optional(),
|
|
78
|
+
})
|
|
79
|
+
.strict()
|
|
80
|
+
.default({}),
|
|
81
|
+
})
|
|
82
|
+
.strict();
|
|
83
|
+
export type SiteDefaultsSetSeoInput = z.infer<typeof siteDefaultsSetSeoInputSchema>;
|
|
84
|
+
|
|
85
|
+
export interface PageSeoRow {
|
|
86
|
+
pageId: string;
|
|
87
|
+
metaDescription: string;
|
|
88
|
+
ogImageAssetId: string | null;
|
|
89
|
+
canonicalUrl: string | null;
|
|
90
|
+
noindex: boolean;
|
|
91
|
+
changefreq: Changefreq;
|
|
92
|
+
priority: number;
|
|
93
|
+
autofilledAt: string | null;
|
|
94
|
+
optimizedAt: string | null;
|
|
95
|
+
updatedAt: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SiteSeoSettings {
|
|
99
|
+
siteBaseUrl: string;
|
|
100
|
+
sitemapEnabled: boolean;
|
|
101
|
+
organization: {
|
|
102
|
+
name?: string;
|
|
103
|
+
url?: string;
|
|
104
|
+
logo?: string;
|
|
105
|
+
sameAs?: string[];
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the canonical URL for a page. If `pages_seo.canonical_url`
|
|
111
|
+
* is set it wins; otherwise build `<siteBaseUrl>/<slug>` (home pages
|
|
112
|
+
* canonicalize to the bare base URL). URL shaping beyond base + slug
|
|
113
|
+
* becomes a plugin contribution on the URL composition point (#390).
|
|
114
|
+
*/
|
|
115
|
+
export function resolveCanonicalUrl(args: {
|
|
116
|
+
siteBaseUrl: string;
|
|
117
|
+
pageSlug: string;
|
|
118
|
+
override: string | null;
|
|
119
|
+
/**
|
|
120
|
+
* v0.2.85 — page emission style. 'directory' (default) → URLs end
|
|
121
|
+
* in `/<slug>/`; 'no-extension' → URLs end in `/<slug>` (no
|
|
122
|
+
* trailing slash) to match what the bucket actually serves when
|
|
123
|
+
* pages are emitted as bare slugs.
|
|
124
|
+
*/
|
|
125
|
+
pageUrlStyle?: "directory" | "no-extension";
|
|
126
|
+
/**
|
|
127
|
+
* 0184 — explicit homepage designation. When true this page IS the
|
|
128
|
+
* site root and canonicalizes to `<base>/` regardless of its slug.
|
|
129
|
+
* Uses the same predicate (`isHomeSlug`) as the magic-slug fallback
|
|
130
|
+
* so canonical agrees with the emitted output path.
|
|
131
|
+
*/
|
|
132
|
+
isHomePage?: boolean;
|
|
133
|
+
}): string {
|
|
134
|
+
if (args.override && args.override.length > 0) return args.override;
|
|
135
|
+
const base = args.siteBaseUrl.replace(/\/$/, "");
|
|
136
|
+
const isHome = args.isHomePage === true || isHomeSlug(args.pageSlug);
|
|
137
|
+
const cleanSlug = isHome ? "" : args.pageSlug;
|
|
138
|
+
const style = args.pageUrlStyle ?? "directory";
|
|
139
|
+
// Tail = the slug portion of the canonical URL, with the trailing
|
|
140
|
+
// shape dictated by the page-emission style. Home pages always
|
|
141
|
+
// resolve to the base URL (no tail) regardless of style.
|
|
142
|
+
const tail = cleanSlug ? (style === "no-extension" ? cleanSlug : `${cleanSlug}/`) : "";
|
|
143
|
+
return tail ? `${base}/${tail}` : `${base}/`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface SeoMetaInput {
|
|
147
|
+
title: string;
|
|
148
|
+
metaDescription: string;
|
|
149
|
+
canonical: string;
|
|
150
|
+
noindex: boolean;
|
|
151
|
+
ogImageUrl: string | null;
|
|
152
|
+
organization: SiteSeoSettings["organization"];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Render the <head> meta block for a page. Returns the HTML string
|
|
157
|
+
* the renderer injects just before </head>. Order is canonical
|
|
158
|
+
* (W3C-recommended): charset / viewport stay in the layout; meta
|
|
159
|
+
* description + canonical + robots + Open Graph + Twitter card +
|
|
160
|
+
* JSON-LD.
|
|
161
|
+
*
|
|
162
|
+
* No raw HTML escape hatch — every variable is HTML-attribute-encoded.
|
|
163
|
+
*/
|
|
164
|
+
export function renderSeoHead(input: SeoMetaInput): string {
|
|
165
|
+
const enc = (s: string): string =>
|
|
166
|
+
s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
167
|
+
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
lines.push(`<title>${enc(input.title)}</title>`);
|
|
170
|
+
if (input.metaDescription) {
|
|
171
|
+
lines.push(`<meta name="description" content="${enc(input.metaDescription)}" />`);
|
|
172
|
+
}
|
|
173
|
+
lines.push(`<link rel="canonical" href="${enc(input.canonical)}" />`);
|
|
174
|
+
if (input.noindex) {
|
|
175
|
+
lines.push(`<meta name="robots" content="noindex" />`);
|
|
176
|
+
}
|
|
177
|
+
// Open Graph
|
|
178
|
+
lines.push(`<meta property="og:title" content="${enc(input.title)}" />`);
|
|
179
|
+
if (input.metaDescription) {
|
|
180
|
+
lines.push(`<meta property="og:description" content="${enc(input.metaDescription)}" />`);
|
|
181
|
+
}
|
|
182
|
+
lines.push(`<meta property="og:type" content="website" />`);
|
|
183
|
+
lines.push(`<meta property="og:url" content="${enc(input.canonical)}" />`);
|
|
184
|
+
if (input.ogImageUrl) {
|
|
185
|
+
lines.push(`<meta property="og:image" content="${enc(input.ogImageUrl)}" />`);
|
|
186
|
+
}
|
|
187
|
+
// Twitter card
|
|
188
|
+
lines.push(
|
|
189
|
+
`<meta name="twitter:card" content="${input.ogImageUrl ? "summary_large_image" : "summary"}" />`,
|
|
190
|
+
);
|
|
191
|
+
// JSON-LD WebPage block referencing the Organization (when set).
|
|
192
|
+
const ld: Record<string, unknown> = {
|
|
193
|
+
"@context": "https://schema.org",
|
|
194
|
+
"@type": "WebPage",
|
|
195
|
+
name: input.title,
|
|
196
|
+
url: input.canonical,
|
|
197
|
+
};
|
|
198
|
+
if (input.metaDescription) ld.description = input.metaDescription;
|
|
199
|
+
if (input.ogImageUrl) ld.image = input.ogImageUrl;
|
|
200
|
+
if (input.organization.name) {
|
|
201
|
+
ld.publisher = {
|
|
202
|
+
"@type": "Organization",
|
|
203
|
+
name: input.organization.name,
|
|
204
|
+
...(input.organization.url ? { url: input.organization.url } : {}),
|
|
205
|
+
...(input.organization.logo
|
|
206
|
+
? { logo: { "@type": "ImageObject", url: input.organization.logo } }
|
|
207
|
+
: {}),
|
|
208
|
+
...(input.organization.sameAs?.length ? { sameAs: input.organization.sameAs } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
lines.push(
|
|
212
|
+
`<script type="application/ld+json">${JSON.stringify(ld).replace(/</g, "\\u003c")}</script>`,
|
|
213
|
+
);
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Inject `renderSeoHead`'s output just before `</head>`. Replaces any
|
|
219
|
+
* existing `<title>` so the page-level title wins over a layout-
|
|
220
|
+
* supplied default. Pre-existing meta tags from the layout are left
|
|
221
|
+
* alone — authors can still drop e.g. `<meta name="theme-color">` in
|
|
222
|
+
* the layout HTML.
|
|
223
|
+
*/
|
|
224
|
+
export function injectSeoIntoHead(html: string, headBlock: string): string {
|
|
225
|
+
// Strip a layout-supplied <title> if present; we'll re-emit it in
|
|
226
|
+
// the headBlock at the right position.
|
|
227
|
+
const titleStripped = html.replace(/<title\b[^>]*>[\s\S]*?<\/title>/i, "");
|
|
228
|
+
if (titleStripped.includes("</head>")) {
|
|
229
|
+
return titleStripped.replace("</head>", `${headBlock}\n</head>`);
|
|
230
|
+
}
|
|
231
|
+
// No closing head tag — prepend the block to the document.
|
|
232
|
+
return `${headBlock}\n${titleStripped}`;
|
|
233
|
+
}
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill metadata shapes shared across the Query API + chat-runner.
|
|
5
|
+
*
|
|
6
|
+
* Skills use PROGRESSIVE DISCLOSURE (the Anthropic Agent Skills shape): a
|
|
7
|
+
* static `## Skills` index (slug + description) sits in the cached system
|
|
8
|
+
* prompt, and the model pulls a skill's full body into the conversation on
|
|
9
|
+
* demand via the `load_skill` tool (its body then lives in the append-only
|
|
10
|
+
* message history for the rest of the chat).
|
|
11
|
+
*
|
|
12
|
+
* `skillAutoEngagementHints` no longer drives SELECTION — the model
|
|
13
|
+
* self-selects from the descriptions. The hints now only shape how prominently
|
|
14
|
+
* a skill is presented in the index: `alwaysOn` skills get an "always applies"
|
|
15
|
+
* callout, `chipTrigger` skills a "when element chips are attached" callout.
|
|
16
|
+
* (The old keyword/chip/always-on MATCHER + engagement resolver were removed
|
|
17
|
+
* once progressive disclosure replaced them.)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
export const skillAutoEngagementHints = z
|
|
23
|
+
.object({
|
|
24
|
+
/**
|
|
25
|
+
* Lowercase substrings that once boosted the auto-matcher. The matcher is
|
|
26
|
+
* gone (progressive disclosure); retained in the schema so seeded rows that
|
|
27
|
+
* still carry keywords parse under `.strict()`. No runtime effect today.
|
|
28
|
+
*/
|
|
29
|
+
keywords: z.array(z.string().min(1).max(80)).default([]),
|
|
30
|
+
/** Listed under the index's "when element chips are attached" callout. */
|
|
31
|
+
chipTrigger: z.boolean().default(false),
|
|
32
|
+
/** Listed under the index's "always applies" callout (e.g. brand-voice-guard). */
|
|
33
|
+
alwaysOn: z.boolean().default(false),
|
|
34
|
+
})
|
|
35
|
+
.strict();
|
|
36
|
+
export type SkillAutoEngagementHints = z.infer<typeof skillAutoEngagementHints>;
|
|
37
|
+
|
|
38
|
+
/** How a loaded skill came to be engaged this chat. */
|
|
39
|
+
export type EngagementSource = "auto" | "manual" | "pinned";
|
|
40
|
+
|
|
41
|
+
/** A skill the model has loaded (via load_skill) in the current chat. */
|
|
42
|
+
export interface ChatEngagement {
|
|
43
|
+
readonly skillId: string;
|
|
44
|
+
readonly slug: string;
|
|
45
|
+
readonly displayName: string;
|
|
46
|
+
readonly source: EngagementSource;
|
|
47
|
+
readonly rationale: string;
|
|
48
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import {
|
|
5
|
+
moduleImpactInput,
|
|
6
|
+
revertModuleInput,
|
|
7
|
+
revertPageInput,
|
|
8
|
+
revertSiteInput,
|
|
9
|
+
revertTemplateInput,
|
|
10
|
+
snapshotGetInput,
|
|
11
|
+
snapshotsListInput,
|
|
12
|
+
} from "./snapshots.js";
|
|
13
|
+
|
|
14
|
+
const UUID = "11111111-1111-4111-8111-111111111111";
|
|
15
|
+
const UUID_2 = "22222222-2222-4222-8222-222222222222";
|
|
16
|
+
|
|
17
|
+
describe("snapshotsListInput", () => {
|
|
18
|
+
it("defaults limit to 50 when omitted", () => {
|
|
19
|
+
const r = snapshotsListInput.safeParse({});
|
|
20
|
+
expect(r.success).toBe(true);
|
|
21
|
+
if (!r.success) return;
|
|
22
|
+
expect(r.data.limit).toBe(50);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects unknown keys (strict)", () => {
|
|
26
|
+
const r = snapshotsListInput.safeParse({ limit: 10, unexpected: 1 });
|
|
27
|
+
expect(r.success).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("clamps limit between 1 and 200", () => {
|
|
31
|
+
expect(snapshotsListInput.safeParse({ limit: 0 }).success).toBe(false);
|
|
32
|
+
expect(snapshotsListInput.safeParse({ limit: 201 }).success).toBe(false);
|
|
33
|
+
expect(snapshotsListInput.safeParse({ limit: 200 }).success).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("accepts an ISO timestamp for `before`", () => {
|
|
37
|
+
const r = snapshotsListInput.safeParse({ before: "2026-04-26T10:00:00.000Z" });
|
|
38
|
+
expect(r.success).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("revert input schemas", () => {
|
|
43
|
+
it("revertSiteInput requires a uuid snapshotId", () => {
|
|
44
|
+
expect(revertSiteInput.safeParse({ snapshotId: UUID }).success).toBe(true);
|
|
45
|
+
expect(revertSiteInput.safeParse({ snapshotId: "not-a-uuid" }).success).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("revertModuleInput requires both moduleId and snapshotId", () => {
|
|
49
|
+
expect(revertModuleInput.safeParse({ moduleId: UUID, snapshotId: UUID_2 }).success).toBe(true);
|
|
50
|
+
expect(revertModuleInput.safeParse({ moduleId: UUID }).success).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("rejects unknown keys on revertPageInput", () => {
|
|
54
|
+
const r = revertPageInput.safeParse({
|
|
55
|
+
pageId: UUID,
|
|
56
|
+
snapshotId: UUID_2,
|
|
57
|
+
extra: "noop",
|
|
58
|
+
});
|
|
59
|
+
expect(r.success).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("revertTemplateInput rejects an unknown key", () => {
|
|
63
|
+
const r = revertTemplateInput.safeParse({
|
|
64
|
+
templateId: UUID,
|
|
65
|
+
snapshotId: UUID_2,
|
|
66
|
+
extra: 1,
|
|
67
|
+
});
|
|
68
|
+
expect(r.success).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("get / impact inputs", () => {
|
|
73
|
+
it("snapshotGetInput accepts a uuid", () => {
|
|
74
|
+
expect(snapshotGetInput.safeParse({ snapshotId: UUID }).success).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it("moduleImpactInput accepts a uuid and rejects other keys", () => {
|
|
77
|
+
expect(moduleImpactInput.safeParse({ moduleId: UUID }).success).toBe(true);
|
|
78
|
+
expect(moduleImpactInput.safeParse({ moduleId: UUID, foo: 1 }).success).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
package/src/snapshots.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod input schemas for the P4 snapshot ops. Lives in @caelo-cms/shared so that
|
|
5
|
+
* route actions and the ops themselves import from a single source — same
|
|
6
|
+
* pattern as packages/shared/src/content.ts for P3.
|
|
7
|
+
*
|
|
8
|
+
* Every schema is `.strict()` so unknown keys are rejected at the Validator
|
|
9
|
+
* before the handler runs. Snapshot state JSONB is *not* validated by Zod
|
|
10
|
+
* here — it is emitted by trusted ops, never user-supplied (see CMS plan
|
|
11
|
+
* §"Validator rules").
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
export const snapshotsListInput = z
|
|
17
|
+
.object({
|
|
18
|
+
/** ISO timestamp; only snapshots strictly before this are returned. */
|
|
19
|
+
before: z.string().datetime().optional(),
|
|
20
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
21
|
+
/** When set, only snapshots that touched this module are returned. */
|
|
22
|
+
forModuleId: z.string().uuid().optional(),
|
|
23
|
+
/** When set, only snapshots that touched this page (metadata or layout). */
|
|
24
|
+
forPageId: z.string().uuid().optional(),
|
|
25
|
+
/** When set, only snapshots that touched this template (incl. block changes). */
|
|
26
|
+
forTemplateId: z.string().uuid().optional(),
|
|
27
|
+
/** When set, only snapshots whose op_kind matches one of these values. */
|
|
28
|
+
opKinds: z.array(z.string()).optional(),
|
|
29
|
+
/** When true, archived snapshots are also returned. Defaults to false. */
|
|
30
|
+
includeArchived: z.boolean().default(false),
|
|
31
|
+
/** v0.6.0 — only snapshots tagged with this chat's branch are returned.
|
|
32
|
+
* Used by `revert_chat_changes` to enumerate everything one chat did
|
|
33
|
+
* without scanning the full timeline. */
|
|
34
|
+
forChatBranchId: z.string().uuid().optional(),
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
37
|
+
|
|
38
|
+
export const archiveOlderThanInput = z
|
|
39
|
+
.object({
|
|
40
|
+
/** ISO timestamp; snapshots with `created_at < before` get archived_at set. */
|
|
41
|
+
before: z.string().datetime(),
|
|
42
|
+
/** Hard cap to keep one call from updating the whole table by mistake. */
|
|
43
|
+
limit: z.number().int().min(1).max(10_000).default(1000),
|
|
44
|
+
})
|
|
45
|
+
.strict();
|
|
46
|
+
export type ArchiveOlderThanInput = z.infer<typeof archiveOlderThanInput>;
|
|
47
|
+
|
|
48
|
+
export const snapshotGetInput = z.object({ snapshotId: z.string().uuid() }).strict();
|
|
49
|
+
|
|
50
|
+
export const moduleImpactInput = z.object({ moduleId: z.string().uuid() }).strict();
|
|
51
|
+
|
|
52
|
+
export const revertSiteInput = z.object({ snapshotId: z.string().uuid() }).strict();
|
|
53
|
+
|
|
54
|
+
export const revertModuleInput = z
|
|
55
|
+
.object({
|
|
56
|
+
moduleId: z.string().uuid(),
|
|
57
|
+
snapshotId: z.string().uuid(),
|
|
58
|
+
})
|
|
59
|
+
.strict();
|
|
60
|
+
|
|
61
|
+
export const revertTemplateInput = z
|
|
62
|
+
.object({
|
|
63
|
+
templateId: z.string().uuid(),
|
|
64
|
+
snapshotId: z.string().uuid(),
|
|
65
|
+
})
|
|
66
|
+
.strict();
|
|
67
|
+
|
|
68
|
+
export const revertPageInput = z
|
|
69
|
+
.object({
|
|
70
|
+
pageId: z.string().uuid(),
|
|
71
|
+
snapshotId: z.string().uuid(),
|
|
72
|
+
})
|
|
73
|
+
.strict();
|
|
74
|
+
|
|
75
|
+
export type SnapshotsListInput = z.infer<typeof snapshotsListInput>;
|
|
76
|
+
export type SnapshotGetInput = z.infer<typeof snapshotGetInput>;
|
|
77
|
+
export type ModuleImpactInput = z.infer<typeof moduleImpactInput>;
|
|
78
|
+
export type RevertSiteInput = z.infer<typeof revertSiteInput>;
|
|
79
|
+
export type RevertModuleInput = z.infer<typeof revertModuleInput>;
|
|
80
|
+
export type RevertTemplateInput = z.infer<typeof revertTemplateInput>;
|
|
81
|
+
export type RevertPageInput = z.infer<typeof revertPageInput>;
|