@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.
@@ -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,9 @@
1
+ import {
2
+ listStaticToolNames,
3
+ runAgentTurn
4
+ } from "../chunk-X6TU47KZ.js";
5
+ import "../chunk-2LAI3MY2.js";
6
+ export {
7
+ listStaticToolNames,
8
+ runAgentTurn
9
+ };
@@ -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,11 @@
1
+ import {
2
+ handler,
3
+ streamHandler
4
+ } from "../chunk-5CVLHGGO.js";
5
+ import "../chunk-X6TU47KZ.js";
6
+ import "../chunk-2LAI3MY2.js";
7
+ import "../chunk-MLID7STX.js";
8
+ export {
9
+ handler,
10
+ streamHandler
11
+ };
@@ -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
+ };