@webmaster-droid/server 0.1.0-alpha.0
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/agent/index.d.ts +37 -0
- package/dist/agent/index.js +9 -0
- package/dist/api-aws/index.d.ts +6 -0
- package/dist/api-aws/index.js +11 -0
- package/dist/chunk-2LAI3MY2.js +620 -0
- package/dist/chunk-5CVLHGGO.js +672 -0
- package/dist/chunk-MLID7STX.js +412 -0
- package/dist/chunk-X6TU47KZ.js +1496 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.js +16 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +32 -0
- package/dist/service-BYwdlvCI.d.ts +47 -0
- package/dist/storage-s3/index.d.ts +49 -0
- package/dist/storage-s3/index.js +6 -0
- package/dist/types-OKJgq7Oo.d.ts +101 -0
- package/package.json +63 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SelectedElementContext, CmsDocument } from '@webmaster-droid/contracts';
|
|
2
|
+
import { C as CmsService } from '../service-BYwdlvCI.js';
|
|
3
|
+
import '../types-OKJgq7Oo.js';
|
|
4
|
+
|
|
5
|
+
interface AgentRunnerInput {
|
|
6
|
+
prompt: string;
|
|
7
|
+
actor?: string;
|
|
8
|
+
includeThinking?: boolean;
|
|
9
|
+
modelId?: string;
|
|
10
|
+
currentPath?: string;
|
|
11
|
+
selectedElement?: SelectedElementContext;
|
|
12
|
+
history?: Array<{
|
|
13
|
+
role: "user" | "assistant";
|
|
14
|
+
text: string;
|
|
15
|
+
}>;
|
|
16
|
+
onThinkingEvent?: (note: string) => void;
|
|
17
|
+
onToolEvent?: (event: {
|
|
18
|
+
tool: string;
|
|
19
|
+
summary: string;
|
|
20
|
+
}) => void;
|
|
21
|
+
}
|
|
22
|
+
interface AgentRunnerResult {
|
|
23
|
+
text: string;
|
|
24
|
+
thinking: string[];
|
|
25
|
+
toolEvents: Array<{
|
|
26
|
+
tool: string;
|
|
27
|
+
summary: string;
|
|
28
|
+
}>;
|
|
29
|
+
updatedDraft: CmsDocument;
|
|
30
|
+
mutationsApplied: boolean;
|
|
31
|
+
}
|
|
32
|
+
declare const STATIC_TOOL_NAMES: readonly ["patch_content", "patch_theme_tokens", "get_page", "get_section", "search_content", "generate_image"];
|
|
33
|
+
type StaticToolName = (typeof STATIC_TOOL_NAMES)[number];
|
|
34
|
+
declare function listStaticToolNames(): StaticToolName[];
|
|
35
|
+
declare function runAgentTurn(service: CmsService, input: AgentRunnerInput): Promise<AgentRunnerResult>;
|
|
36
|
+
|
|
37
|
+
export { type AgentRunnerInput, type AgentRunnerResult, type StaticToolName, listStaticToolNames, runAgentTurn };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
|
|
2
|
+
|
|
3
|
+
declare function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult>;
|
|
4
|
+
declare const streamHandler: (event: unknown, responseStream: unknown, context: unknown) => Promise<void>;
|
|
5
|
+
|
|
6
|
+
export { handler, streamHandler };
|
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
// src/core/patch.ts
|
|
2
|
+
import {
|
|
3
|
+
isEditablePath,
|
|
4
|
+
isHttpsUrl,
|
|
5
|
+
requiresStrictImageValidation
|
|
6
|
+
} from "@webmaster-droid/contracts";
|
|
7
|
+
function cloneDocument(doc) {
|
|
8
|
+
return JSON.parse(JSON.stringify(doc));
|
|
9
|
+
}
|
|
10
|
+
function splitPath(path) {
|
|
11
|
+
return path.replace(/\[(\d+)\]/g, ".$1").split(".").map((segment) => segment.trim()).filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
var RESTRICTED_LINK_ROOTS = [
|
|
14
|
+
["layout", "header", "primaryLinks"],
|
|
15
|
+
["layout", "footer", "navigationLinks"],
|
|
16
|
+
["layout", "footer", "legalLinks"]
|
|
17
|
+
];
|
|
18
|
+
function classifyRestrictedLinkPath(path) {
|
|
19
|
+
const segments = splitPath(path);
|
|
20
|
+
for (const root of RESTRICTED_LINK_ROOTS) {
|
|
21
|
+
const isRootMatch = root.every((segment, index) => segments[index] === segment);
|
|
22
|
+
if (!isRootMatch) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (segments.length === root.length) {
|
|
26
|
+
return "restricted";
|
|
27
|
+
}
|
|
28
|
+
const indexSegment = segments[root.length];
|
|
29
|
+
const leafSegment = segments[root.length + 1];
|
|
30
|
+
const hasValidIndex = /^\d+$/.test(indexSegment ?? "");
|
|
31
|
+
if (hasValidIndex && segments.length === root.length + 2 && (leafSegment === "label" || leafSegment === "href")) {
|
|
32
|
+
return "allowed_leaf";
|
|
33
|
+
}
|
|
34
|
+
return "restricted";
|
|
35
|
+
}
|
|
36
|
+
return "none";
|
|
37
|
+
}
|
|
38
|
+
function normalizeInternalPath(path) {
|
|
39
|
+
const trimmed = path.trim();
|
|
40
|
+
if (!trimmed.startsWith("/")) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const [withoutQuery] = trimmed.split(/[?#]/, 1);
|
|
44
|
+
if (!withoutQuery) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (withoutQuery === "/") {
|
|
48
|
+
return "/";
|
|
49
|
+
}
|
|
50
|
+
const normalized = withoutQuery.replace(/\/+$/, "");
|
|
51
|
+
if (!normalized) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return `${normalized}/`;
|
|
55
|
+
}
|
|
56
|
+
function readByPath(input, path) {
|
|
57
|
+
const segments = splitPath(path);
|
|
58
|
+
let current = input;
|
|
59
|
+
for (const segment of segments) {
|
|
60
|
+
if (Array.isArray(current)) {
|
|
61
|
+
const index = Number(segment);
|
|
62
|
+
if (Number.isNaN(index)) {
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
current = current[index];
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (typeof current !== "object" || current === null) {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
current = current[segment];
|
|
72
|
+
}
|
|
73
|
+
return current;
|
|
74
|
+
}
|
|
75
|
+
function writeByPath(input, path, value) {
|
|
76
|
+
const segments = splitPath(path);
|
|
77
|
+
if (segments.length === 0) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
let current = input;
|
|
81
|
+
for (let idx = 0; idx < segments.length - 1; idx += 1) {
|
|
82
|
+
const segment = segments[idx];
|
|
83
|
+
if (Array.isArray(current)) {
|
|
84
|
+
const index = Number(segment);
|
|
85
|
+
if (Number.isNaN(index)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (current[index] === void 0) {
|
|
89
|
+
const next = segments[idx + 1];
|
|
90
|
+
current[index] = /^\d+$/.test(next) ? [] : {};
|
|
91
|
+
}
|
|
92
|
+
current = current[index];
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (typeof current !== "object" || current === null) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const record = current;
|
|
99
|
+
if (record[segment] === void 0) {
|
|
100
|
+
const next = segments[idx + 1];
|
|
101
|
+
record[segment] = /^\d+$/.test(next) ? [] : {};
|
|
102
|
+
}
|
|
103
|
+
current = record[segment];
|
|
104
|
+
}
|
|
105
|
+
const finalSegment = segments.at(-1);
|
|
106
|
+
if (!finalSegment) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(current)) {
|
|
110
|
+
const index = Number(finalSegment);
|
|
111
|
+
if (Number.isNaN(index)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
current[index] = value;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (typeof current !== "object" || current === null) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
current[finalSegment] = value;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
function validatePatch(patch, source, options) {
|
|
124
|
+
const warnings = [];
|
|
125
|
+
const errors = [];
|
|
126
|
+
const allowedInternalPaths = new Set(options.allowedInternalPaths);
|
|
127
|
+
if (patch.operations.length > options.maxOperationsPerPatch) {
|
|
128
|
+
errors.push(
|
|
129
|
+
`Patch contains ${patch.operations.length} operations; limit is ${options.maxOperationsPerPatch}.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
for (const operation of patch.operations) {
|
|
133
|
+
if (operation.op !== "set") {
|
|
134
|
+
errors.push(`Unsupported operation type: ${operation.op}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!isEditablePath(operation.path)) {
|
|
138
|
+
errors.push(`Path is out of editable scope: ${operation.path}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const linkPathClassification = classifyRestrictedLinkPath(operation.path);
|
|
142
|
+
if (linkPathClassification === "restricted") {
|
|
143
|
+
errors.push(
|
|
144
|
+
`Only link label and href leaf fields are editable for header/footer links: ${operation.path}`
|
|
145
|
+
);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const currentValue = readByPath(source, operation.path);
|
|
149
|
+
if (currentValue === void 0) {
|
|
150
|
+
errors.push(
|
|
151
|
+
`Path does not exist and cannot be created by patch_content: ${operation.path}`
|
|
152
|
+
);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (requiresStrictImageValidation(operation.path) && !isHttpsUrl(operation.value)) {
|
|
156
|
+
errors.push(`Image path requires HTTPS URL: ${operation.path}`);
|
|
157
|
+
}
|
|
158
|
+
if (linkPathClassification === "allowed_leaf" && operation.path.endsWith(".href")) {
|
|
159
|
+
if (typeof operation.value !== "string") {
|
|
160
|
+
errors.push(`Link href must be a string: ${operation.path}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const normalizedHref = normalizeInternalPath(operation.value);
|
|
164
|
+
if (!normalizedHref || !allowedInternalPaths.has(normalizedHref)) {
|
|
165
|
+
errors.push(
|
|
166
|
+
`Link href is outside allowed internal routes: ${operation.path} (${operation.value})`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (typeof currentValue === "number" && typeof operation.value !== "number" && operation.value !== null) {
|
|
171
|
+
warnings.push(
|
|
172
|
+
`Numeric field ${operation.path} received non-number value; value will still be applied.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (typeof currentValue === "boolean" && typeof operation.value !== "boolean" && operation.value !== null) {
|
|
176
|
+
warnings.push(
|
|
177
|
+
`Boolean field ${operation.path} received non-boolean value; value will still be applied.`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (typeof currentValue === "string" && typeof operation.value === "string") {
|
|
181
|
+
const before = currentValue.trim();
|
|
182
|
+
const after = operation.value.trim();
|
|
183
|
+
if (before.length >= 80 && after.length < Math.floor(before.length * 0.85) && before.startsWith(after)) {
|
|
184
|
+
errors.push(
|
|
185
|
+
`Refusing potentially truncated update at ${operation.path}; fetch full section and retry with complete value.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
valid: errors.length === 0,
|
|
192
|
+
warnings,
|
|
193
|
+
errors
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function applyPatch(source, patch, options) {
|
|
197
|
+
const validation = validatePatch(patch, source, options);
|
|
198
|
+
if (!validation.valid) {
|
|
199
|
+
throw new Error(validation.errors.join("\n"));
|
|
200
|
+
}
|
|
201
|
+
const document = cloneDocument(source);
|
|
202
|
+
for (const operation of patch.operations) {
|
|
203
|
+
const ok = writeByPath(document, operation.path, operation.value);
|
|
204
|
+
if (!ok) {
|
|
205
|
+
throw new Error(`Failed to apply operation at path ${operation.path}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
document,
|
|
210
|
+
warnings: validation.warnings
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function applyThemeTokenPatch(source, patch) {
|
|
214
|
+
const document = cloneDocument(source);
|
|
215
|
+
const warnings = [];
|
|
216
|
+
for (const [tokenKey, tokenValue] of Object.entries(
|
|
217
|
+
patch
|
|
218
|
+
)) {
|
|
219
|
+
if (typeof tokenValue !== "string") {
|
|
220
|
+
warnings.push(`Theme token ${tokenKey} ignored because value is not a string.`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (!tokenValue.trim()) {
|
|
224
|
+
warnings.push(`Theme token ${tokenKey} ignored because value is empty.`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
document.themeTokens[tokenKey] = tokenValue;
|
|
228
|
+
}
|
|
229
|
+
return { document, warnings };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/core/service.ts
|
|
233
|
+
import {
|
|
234
|
+
REQUIRED_PUBLISH_CONFIRMATION,
|
|
235
|
+
isEditablePath as isEditablePath2
|
|
236
|
+
} from "@webmaster-droid/contracts";
|
|
237
|
+
var DEFAULT_MAX_OPERATIONS = 25;
|
|
238
|
+
var DEFAULT_ALLOWED_INTERNAL_PATHS = ["/"];
|
|
239
|
+
var DEFAULT_PUBLIC_ASSET_PREFIX = "assets/generated";
|
|
240
|
+
var DEFAULT_GENERATED_IMAGE_CACHE_CONTROL = "public,max-age=31536000,immutable";
|
|
241
|
+
function createId(prefix) {
|
|
242
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
243
|
+
}
|
|
244
|
+
function nowIso() {
|
|
245
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
246
|
+
}
|
|
247
|
+
function normalizeInternalPath2(path) {
|
|
248
|
+
const trimmed = path.trim();
|
|
249
|
+
if (!trimmed.startsWith("/")) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const [withoutQuery] = trimmed.split(/[?#]/, 1);
|
|
253
|
+
if (!withoutQuery) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (withoutQuery === "/") {
|
|
257
|
+
return "/";
|
|
258
|
+
}
|
|
259
|
+
const normalized = withoutQuery.replace(/\/+$/, "");
|
|
260
|
+
if (!normalized) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return `${normalized}/`;
|
|
264
|
+
}
|
|
265
|
+
function normalizeAllowedInternalPaths(paths) {
|
|
266
|
+
const out = /* @__PURE__ */ new Set();
|
|
267
|
+
for (const path of paths) {
|
|
268
|
+
const normalized = normalizeInternalPath2(path);
|
|
269
|
+
if (normalized) {
|
|
270
|
+
out.add(normalized);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return Array.from(out);
|
|
274
|
+
}
|
|
275
|
+
function comparableContentSnapshot(document) {
|
|
276
|
+
return JSON.stringify({
|
|
277
|
+
themeTokens: document.themeTokens,
|
|
278
|
+
layout: document.layout,
|
|
279
|
+
pages: document.pages,
|
|
280
|
+
seo: document.seo
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function normalizePublicAssetBaseUrl(value) {
|
|
284
|
+
const raw = value?.trim();
|
|
285
|
+
if (!raw) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const parsed = new URL(raw);
|
|
290
|
+
if (parsed.protocol !== "https:") {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
294
|
+
} catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function normalizePublicAssetPrefix(value) {
|
|
299
|
+
const normalized = (value ?? DEFAULT_PUBLIC_ASSET_PREFIX).trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
300
|
+
return normalized || DEFAULT_PUBLIC_ASSET_PREFIX;
|
|
301
|
+
}
|
|
302
|
+
function sanitizeTargetPathForKey(value) {
|
|
303
|
+
const normalized = value.replace(/\[(\d+)\]/g, "-$1-").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").toLowerCase();
|
|
304
|
+
return normalized.slice(0, 80) || "image";
|
|
305
|
+
}
|
|
306
|
+
function extensionFromMimeType(contentType) {
|
|
307
|
+
const normalized = contentType.trim().toLowerCase().split(";", 1)[0];
|
|
308
|
+
if (normalized === "image/jpeg") {
|
|
309
|
+
return "jpg";
|
|
310
|
+
}
|
|
311
|
+
if (normalized === "image/png") {
|
|
312
|
+
return "png";
|
|
313
|
+
}
|
|
314
|
+
if (normalized === "image/webp") {
|
|
315
|
+
return "webp";
|
|
316
|
+
}
|
|
317
|
+
if (normalized === "image/gif") {
|
|
318
|
+
return "gif";
|
|
319
|
+
}
|
|
320
|
+
return "png";
|
|
321
|
+
}
|
|
322
|
+
function buildGeneratedAssetKey(targetPath, publicAssetPrefix, contentType, now = /* @__PURE__ */ new Date()) {
|
|
323
|
+
const year = String(now.getUTCFullYear());
|
|
324
|
+
const month = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
325
|
+
const day = String(now.getUTCDate()).padStart(2, "0");
|
|
326
|
+
const timestamp = now.getTime();
|
|
327
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
328
|
+
const safePath = sanitizeTargetPathForKey(targetPath);
|
|
329
|
+
const ext = extensionFromMimeType(contentType);
|
|
330
|
+
return `${publicAssetPrefix}/${year}/${month}/${day}/${safePath}-${timestamp}-${random}.${ext}`;
|
|
331
|
+
}
|
|
332
|
+
var CmsService = class {
|
|
333
|
+
storage;
|
|
334
|
+
modelConfig;
|
|
335
|
+
maxOperationsPerPatch;
|
|
336
|
+
allowedInternalPaths;
|
|
337
|
+
publicAssetBaseUrl;
|
|
338
|
+
publicAssetPrefix;
|
|
339
|
+
constructor(storage, config) {
|
|
340
|
+
this.storage = storage;
|
|
341
|
+
this.modelConfig = config.modelConfig;
|
|
342
|
+
this.maxOperationsPerPatch = config.maxOperationsPerPatch ?? DEFAULT_MAX_OPERATIONS;
|
|
343
|
+
this.allowedInternalPaths = normalizeAllowedInternalPaths(
|
|
344
|
+
config.allowedInternalPaths ?? DEFAULT_ALLOWED_INTERNAL_PATHS
|
|
345
|
+
);
|
|
346
|
+
this.publicAssetBaseUrl = normalizePublicAssetBaseUrl(config.publicAssetBaseUrl);
|
|
347
|
+
this.publicAssetPrefix = normalizePublicAssetPrefix(config.publicAssetPrefix);
|
|
348
|
+
}
|
|
349
|
+
async ensureInitialized(seed) {
|
|
350
|
+
await this.storage.ensureInitialized(seed);
|
|
351
|
+
}
|
|
352
|
+
async getContent(stage) {
|
|
353
|
+
return this.storage.getContent(stage);
|
|
354
|
+
}
|
|
355
|
+
getModelConfig() {
|
|
356
|
+
return this.modelConfig;
|
|
357
|
+
}
|
|
358
|
+
getPublicAssetBaseUrl() {
|
|
359
|
+
return this.publicAssetBaseUrl;
|
|
360
|
+
}
|
|
361
|
+
async saveGeneratedImage(input) {
|
|
362
|
+
const targetPath = input.targetPath.trim();
|
|
363
|
+
if (!targetPath) {
|
|
364
|
+
throw new Error("Generated image target path is required.");
|
|
365
|
+
}
|
|
366
|
+
if (!(input.data instanceof Uint8Array) || input.data.length === 0) {
|
|
367
|
+
throw new Error("Generated image bytes are required.");
|
|
368
|
+
}
|
|
369
|
+
const contentType = input.contentType.trim().toLowerCase().split(";", 1)[0];
|
|
370
|
+
if (!contentType.startsWith("image/")) {
|
|
371
|
+
throw new Error(`Generated content type is not an image: ${input.contentType}`);
|
|
372
|
+
}
|
|
373
|
+
if (!this.publicAssetBaseUrl) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
"CMS public asset base URL is not configured. Set CMS_PUBLIC_BASE_URL to enable generated image URLs."
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const key = buildGeneratedAssetKey(targetPath, this.publicAssetPrefix, contentType);
|
|
379
|
+
await this.storage.putPublicAsset({
|
|
380
|
+
key,
|
|
381
|
+
body: input.data,
|
|
382
|
+
contentType,
|
|
383
|
+
cacheControl: input.cacheControl ?? DEFAULT_GENERATED_IMAGE_CACHE_CONTROL
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
key,
|
|
387
|
+
url: `${this.publicAssetBaseUrl}/${key}`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
async mutateDraft(input) {
|
|
391
|
+
const currentDraft = await this.storage.getContent("draft");
|
|
392
|
+
const checkpoint = await this.storage.createCheckpoint(currentDraft, {
|
|
393
|
+
createdBy: input.actor,
|
|
394
|
+
reason: input.reason
|
|
395
|
+
});
|
|
396
|
+
const { document, warnings } = applyPatch(currentDraft, input.patch, {
|
|
397
|
+
maxOperationsPerPatch: this.maxOperationsPerPatch,
|
|
398
|
+
allowedInternalPaths: this.allowedInternalPaths
|
|
399
|
+
});
|
|
400
|
+
document.meta.updatedAt = nowIso();
|
|
401
|
+
document.meta.updatedBy = input.actor;
|
|
402
|
+
document.meta.contentVersion = createId("draft");
|
|
403
|
+
document.meta.sourceCheckpointId = checkpoint.id;
|
|
404
|
+
await this.storage.saveDraft(document);
|
|
405
|
+
await this.storage.appendEvent(this.createEvent("chat_mutation", input.actor, {
|
|
406
|
+
checkpointId: checkpoint.id,
|
|
407
|
+
reason: input.reason,
|
|
408
|
+
operations: input.patch.operations,
|
|
409
|
+
warnings
|
|
410
|
+
}));
|
|
411
|
+
return {
|
|
412
|
+
document,
|
|
413
|
+
checkpoint,
|
|
414
|
+
warnings
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
async mutateDraftBatch(input) {
|
|
418
|
+
const hasContentPatch = Boolean(input.patch && input.patch.operations.length > 0);
|
|
419
|
+
const hasThemePatch = Boolean(input.themePatch && Object.keys(input.themePatch).length > 0);
|
|
420
|
+
if (!hasContentPatch && !hasThemePatch) {
|
|
421
|
+
throw new Error("No draft mutations provided.");
|
|
422
|
+
}
|
|
423
|
+
const currentDraft = await this.storage.getContent("draft");
|
|
424
|
+
const checkpoint = await this.storage.createCheckpoint(currentDraft, {
|
|
425
|
+
createdBy: input.actor,
|
|
426
|
+
reason: input.reason
|
|
427
|
+
});
|
|
428
|
+
let workingDocument = currentDraft;
|
|
429
|
+
const warnings = [];
|
|
430
|
+
if (hasContentPatch) {
|
|
431
|
+
const contentResult = applyPatch(workingDocument, input.patch, {
|
|
432
|
+
maxOperationsPerPatch: this.maxOperationsPerPatch,
|
|
433
|
+
allowedInternalPaths: this.allowedInternalPaths
|
|
434
|
+
});
|
|
435
|
+
workingDocument = contentResult.document;
|
|
436
|
+
warnings.push(...contentResult.warnings);
|
|
437
|
+
}
|
|
438
|
+
if (hasThemePatch) {
|
|
439
|
+
const themeResult = applyThemeTokenPatch(
|
|
440
|
+
workingDocument,
|
|
441
|
+
input.themePatch
|
|
442
|
+
);
|
|
443
|
+
workingDocument = themeResult.document;
|
|
444
|
+
warnings.push(...themeResult.warnings);
|
|
445
|
+
}
|
|
446
|
+
const document = {
|
|
447
|
+
...workingDocument,
|
|
448
|
+
meta: {
|
|
449
|
+
...workingDocument.meta,
|
|
450
|
+
updatedAt: nowIso(),
|
|
451
|
+
updatedBy: input.actor,
|
|
452
|
+
contentVersion: createId("draft"),
|
|
453
|
+
sourceCheckpointId: checkpoint.id
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
await this.storage.saveDraft(document);
|
|
457
|
+
await this.storage.appendEvent(
|
|
458
|
+
this.createEvent("chat_mutation", input.actor, {
|
|
459
|
+
checkpointId: checkpoint.id,
|
|
460
|
+
reason: input.reason,
|
|
461
|
+
operations: input.patch?.operations,
|
|
462
|
+
themePatch: input.themePatch,
|
|
463
|
+
warnings
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
return {
|
|
467
|
+
document,
|
|
468
|
+
checkpoint,
|
|
469
|
+
warnings
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
async mutateThemeTokens(input) {
|
|
473
|
+
const currentDraft = await this.storage.getContent("draft");
|
|
474
|
+
const checkpoint = await this.storage.createCheckpoint(currentDraft, {
|
|
475
|
+
createdBy: input.actor,
|
|
476
|
+
reason: input.reason
|
|
477
|
+
});
|
|
478
|
+
const { document, warnings } = applyThemeTokenPatch(currentDraft, input.patch);
|
|
479
|
+
document.meta.updatedAt = nowIso();
|
|
480
|
+
document.meta.updatedBy = input.actor;
|
|
481
|
+
document.meta.contentVersion = createId("draft");
|
|
482
|
+
document.meta.sourceCheckpointId = checkpoint.id;
|
|
483
|
+
await this.storage.saveDraft(document);
|
|
484
|
+
await this.storage.appendEvent(this.createEvent("chat_mutation", input.actor, {
|
|
485
|
+
checkpointId: checkpoint.id,
|
|
486
|
+
reason: input.reason,
|
|
487
|
+
themePatch: input.patch,
|
|
488
|
+
warnings
|
|
489
|
+
}));
|
|
490
|
+
return {
|
|
491
|
+
document,
|
|
492
|
+
checkpoint,
|
|
493
|
+
warnings
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
async publishDraft(input, actor) {
|
|
497
|
+
if (input.confirmationText !== REQUIRED_PUBLISH_CONFIRMATION) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`Invalid publish confirmation text. Use exactly: ${REQUIRED_PUBLISH_CONFIRMATION}`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const currentDraft = await this.storage.getContent("draft");
|
|
503
|
+
const version = await this.storage.publishDraft({
|
|
504
|
+
content: currentDraft,
|
|
505
|
+
createdBy: actor
|
|
506
|
+
});
|
|
507
|
+
const publishedLive = {
|
|
508
|
+
...currentDraft,
|
|
509
|
+
meta: {
|
|
510
|
+
...currentDraft.meta,
|
|
511
|
+
updatedAt: nowIso(),
|
|
512
|
+
updatedBy: actor,
|
|
513
|
+
contentVersion: version.id
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
await this.storage.saveLive(publishedLive);
|
|
517
|
+
await this.storage.saveDraft(publishedLive);
|
|
518
|
+
await this.storage.appendEvent(this.createEvent("publish", actor, {
|
|
519
|
+
version
|
|
520
|
+
}));
|
|
521
|
+
return version;
|
|
522
|
+
}
|
|
523
|
+
async rollbackDraft(input, actor) {
|
|
524
|
+
const currentDraft = await this.storage.getContent("draft");
|
|
525
|
+
const targetSnapshot = await this.storage.getSnapshot(input);
|
|
526
|
+
if (!targetSnapshot) {
|
|
527
|
+
throw new Error(`Rollback source not found: ${input.sourceType}/${input.sourceId}`);
|
|
528
|
+
}
|
|
529
|
+
const currentComparable = comparableContentSnapshot(currentDraft);
|
|
530
|
+
const targetComparable = comparableContentSnapshot(targetSnapshot);
|
|
531
|
+
if (currentComparable === targetComparable) {
|
|
532
|
+
await this.storage.appendEvent(this.createEvent("rollback", actor, {
|
|
533
|
+
sourceType: input.sourceType,
|
|
534
|
+
sourceId: input.sourceId,
|
|
535
|
+
newDraftVersion: currentDraft.meta.contentVersion,
|
|
536
|
+
skipped: true,
|
|
537
|
+
reason: "already-at-target"
|
|
538
|
+
}));
|
|
539
|
+
return currentDraft;
|
|
540
|
+
}
|
|
541
|
+
const newDraft = {
|
|
542
|
+
...targetSnapshot,
|
|
543
|
+
meta: {
|
|
544
|
+
...targetSnapshot.meta,
|
|
545
|
+
updatedAt: nowIso(),
|
|
546
|
+
updatedBy: actor,
|
|
547
|
+
contentVersion: createId("draft"),
|
|
548
|
+
sourceCheckpointId: `${input.sourceType}:${input.sourceId}`
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
await this.storage.saveDraft(newDraft);
|
|
552
|
+
await this.storage.appendEvent(this.createEvent("rollback", actor, {
|
|
553
|
+
sourceType: input.sourceType,
|
|
554
|
+
sourceId: input.sourceId,
|
|
555
|
+
newDraftVersion: newDraft.meta.contentVersion
|
|
556
|
+
}));
|
|
557
|
+
return newDraft;
|
|
558
|
+
}
|
|
559
|
+
async listHistory() {
|
|
560
|
+
const [checkpoints, published] = await Promise.all([
|
|
561
|
+
this.storage.listCheckpoints(),
|
|
562
|
+
this.storage.listPublishedVersions()
|
|
563
|
+
]);
|
|
564
|
+
return {
|
|
565
|
+
checkpoints,
|
|
566
|
+
published
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
async deleteCheckpoint(checkpointId) {
|
|
570
|
+
const normalizedId = checkpointId.trim();
|
|
571
|
+
if (!normalizedId) {
|
|
572
|
+
throw new Error("Checkpoint id is required.");
|
|
573
|
+
}
|
|
574
|
+
const deleted = await this.storage.deleteCheckpoint(normalizedId);
|
|
575
|
+
if (!deleted) {
|
|
576
|
+
throw new Error(`Checkpoint not found: ${normalizedId}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
createEvent(type, actor, detail) {
|
|
580
|
+
return {
|
|
581
|
+
id: createId("evt"),
|
|
582
|
+
type,
|
|
583
|
+
actor,
|
|
584
|
+
createdAt: nowIso(),
|
|
585
|
+
detail
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
function createPatchFromAgentOperations(operations) {
|
|
590
|
+
const patchOperations = [];
|
|
591
|
+
for (const operation of operations) {
|
|
592
|
+
if (!isEditablePath2(operation.path)) {
|
|
593
|
+
throw new Error(`Path is out of editable scope: ${operation.path}`);
|
|
594
|
+
}
|
|
595
|
+
patchOperations.push({
|
|
596
|
+
op: "set",
|
|
597
|
+
path: operation.path,
|
|
598
|
+
value: operation.value
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
operations: patchOperations
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function createThemePatchFromAgentOperations(operations) {
|
|
606
|
+
const out = {};
|
|
607
|
+
for (const operation of operations) {
|
|
608
|
+
out[operation.token] = operation.value;
|
|
609
|
+
}
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export {
|
|
614
|
+
validatePatch,
|
|
615
|
+
applyPatch,
|
|
616
|
+
applyThemeTokenPatch,
|
|
617
|
+
CmsService,
|
|
618
|
+
createPatchFromAgentOperations,
|
|
619
|
+
createThemePatchFromAgentOperations
|
|
620
|
+
};
|