@roottale/cms-core 0.71.0 → 0.72.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/CHANGELOG.md +25 -0
- package/README.md +21 -0
- package/dist/chunk-WNU53PSH.js +197 -0
- package/dist/chunk-WNU53PSH.js.map +1 -0
- package/dist/index.cjs +424 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +130 -1
- package/dist/index.d.ts +130 -1
- package/dist/index.js +151 -1
- package/dist/index.js.map +1 -1
- package/dist/visual-edit-contract-DzlMel7r.d.cts +822 -0
- package/dist/visual-edit-contract-DzlMel7r.d.ts +822 -0
- package/dist/visual-edit.cjs +339 -0
- package/dist/visual-edit.cjs.map +1 -0
- package/dist/visual-edit.d.cts +77 -0
- package/dist/visual-edit.d.ts +77 -0
- package/dist/visual-edit.js +153 -0
- package/dist/visual-edit.js.map +1 -0
- package/package.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @roottale/cms-core
|
|
2
2
|
|
|
3
|
+
## 0.72.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c60835a: 미리보기 메시지 조립·수신 필터·계약 호환성·관리자 origin 목록을 React 없는
|
|
8
|
+
`cms-core/visual-edit`에 추가하고 renderer에서도 공개합니다. 기존 wire 프로토콜과
|
|
9
|
+
브리지 동작을 유지하면서 관리자와 고객 사이트가 같은 규칙을 사용합니다.
|
|
10
|
+
- 4341571: 실제화면 콘텐츠 편집의 안정 필드 주소·draft 렌더 영수증·iframe 메시지 검증 계약과
|
|
11
|
+
`@roottale/cms-renderer-next/visual-edit` bridge를 추가합니다. 브라우저 전용 계약은
|
|
12
|
+
`@roottale/cms-core/visual-edit`로 서버용 토큰 코드와 분리합니다. 기존 사이트 템플릿을
|
|
13
|
+
그대로 렌더하고 저장된 draft의 PC·모바일 결과를 확인할 수 있습니다.
|
|
14
|
+
|
|
15
|
+
`createRevalidateRoute`에 선택적 `contentEditing`과 `CONTENT_EDITING_CACHE_TAG`를
|
|
16
|
+
추가합니다. 활성화한 사이트는 글 발행·수정·삭제와 분류 변경 시 공통 콘텐츠 태그와
|
|
17
|
+
루트 layout, 기존 경로를 함께 갱신하며 일부 실패도 나머지 갱신 후 HTTP 500으로
|
|
18
|
+
알립니다. 기본 동작은 유지하며 연동 문서·Next.js 예제를 제공합니다.
|
|
19
|
+
|
|
20
|
+
- fa50476: visual-edit 경로에 coverageReport를 추가합니다. 사이트가 준비한 렌더 주소·문서·필드 정의·섹션을 계약과 비교하고 미연결 필드와 잘못된 주소·섹션·예외를 보고합니다.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- e97a94f: 공개·미리보기 글에 optional contentEditorResult를 추가해 편집 상태 원본 부재와 해석 실패를 구분합니다. 기존 contentEditor 동작은 유지합니다.
|
|
25
|
+
|
|
26
|
+
커버리지 검사에서 계약 컨테이너의 자식 주소와 flexible_content의 행별 layouts를 지원하고, visualEditorOrigins의 HTTP 허용 범위를 브리지와 같은 loopback으로 제한합니다. 연결 문서와 Next.js 예제를 갱신합니다.
|
|
27
|
+
|
|
3
28
|
## 0.71.0
|
|
4
29
|
|
|
5
30
|
## 0.70.1
|
package/README.md
CHANGED
|
@@ -24,3 +24,24 @@ npm install @roottale/cms-core
|
|
|
24
24
|
## License
|
|
25
25
|
|
|
26
26
|
Proprietary (UNLICENSED). Issued under the RootTale customer contract.
|
|
27
|
+
|
|
28
|
+
## 실제화면 편집 커버리지
|
|
29
|
+
|
|
30
|
+
`@roottale/cms-core/visual-edit`의 `coverageReport(contract, observations, exceptions)`는
|
|
31
|
+
DOM이나 React 없이 동작하는 순수 비교 함수입니다. `contract.visualEditing.templates`와
|
|
32
|
+
다음 관찰값을 받습니다.
|
|
33
|
+
|
|
34
|
+
- `documents`: `{ id, templateKey, fields, fieldDefinitions }` 목록. 필드 정의는 기존 CMS의
|
|
35
|
+
`{ key, name, type, subFields?, layouts? }` 구조를 사용합니다.
|
|
36
|
+
- `addresses`: 렌더에서 수집한 `VisualEditAddress` 목록. 전체 저장 경로와 반복행 `@<UUID>`를 포함합니다.
|
|
37
|
+
- `sections`: 화면에서 조회한 `{ template, key }` 목록.
|
|
38
|
+
- 세 번째 인자 `exceptions`: `{ templateKey, fieldKey, reason }` 목록.
|
|
39
|
+
|
|
40
|
+
결과의 `missingFields`, `invalidAddresses`, `invalidSections`, `invalidExceptions`가 모두
|
|
41
|
+
빈 배열이면 비교를 통과합니다. `invalidAddresses`에는 주소와 실패 이유가 담기며
|
|
42
|
+
`repeaterSegments`는 검증된 반복행 경로 구간 수입니다. 예외는 미연결에만 적용합니다.
|
|
43
|
+
계약이 그룹·반복 컨테이너를 선언하면 그 자식 필드 주소도 허용합니다.
|
|
44
|
+
`flexible_content` 정의에는 `layouts: [{ name, subFields }]`를 전달하며 각 행의
|
|
45
|
+
`__layout`에 해당하는 필드만 검사합니다. 다른 레이아웃 필드·없는 레이아웃은 거부합니다.
|
|
46
|
+
그룹·반복 필드 자체를 가리키는 주소도 허용하고 `itemId`는 생략할 수 있습니다.
|
|
47
|
+
렌더 시나리오·모든 필드를 채우는 fixture·예외 이유·클릭 도달 검사는 사이트에 둡니다.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// src/visual-edit-contract.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var VISUAL_EDIT_PROTOCOL = "roottale:visual-edit";
|
|
4
|
+
var VISUAL_EDIT_PROTOCOL_VERSION = 1;
|
|
5
|
+
var VISUAL_EDIT_ATTRIBUTE = "data-rt-edit";
|
|
6
|
+
var identifier = z.string().min(1).max(160).refine((value) => !/[\u0000-\u001f]/.test(value));
|
|
7
|
+
var safeKey = identifier.refine((value) => !["__proto__", "prototype", "constructor"].includes(value));
|
|
8
|
+
var pathKey = safeKey.refine((value) => !/^\d+$/.test(value), "\uBC18\uBCF5 \uD56D\uBAA9\uC740 \uC21C\uBC88 \uB300\uC2E0 itemId\uB85C \uC120\uD0DD\uD569\uB2C8\uB2E4.");
|
|
9
|
+
var version = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
10
|
+
var session = z.string().uuid();
|
|
11
|
+
var position = z.object({ left: z.number().finite().min(0).max(1e7), top: z.number().finite().min(0).max(1e7) }).strict();
|
|
12
|
+
var pathname = z.string().min(1).max(2e3).refine((value) => value.startsWith("/") && !value.startsWith("//") && !/[\\?#\u0000-\u001f]/.test(value));
|
|
13
|
+
var visualEditAddressSchema = z.object({
|
|
14
|
+
entityId: identifier,
|
|
15
|
+
fieldKey: safeKey,
|
|
16
|
+
itemId: identifier.optional(),
|
|
17
|
+
path: z.array(pathKey).max(16).optional()
|
|
18
|
+
}).strict().refine((address) => {
|
|
19
|
+
const selector = [...address.path ?? []].reverse().find((part) => part.startsWith("@"));
|
|
20
|
+
return !selector || !address.itemId || selector.slice(1) === address.itemId;
|
|
21
|
+
}, "itemId\uB294 \uACBD\uB85C\uC758 \uB9C8\uC9C0\uB9C9 \uBC18\uBCF5 \uD56D\uBAA9 ID\uC640 \uAC19\uC544\uC57C \uD569\uB2C8\uB2E4.");
|
|
22
|
+
var visualPreviewTargetSchema = z.object({
|
|
23
|
+
entityId: identifier,
|
|
24
|
+
draftId: identifier,
|
|
25
|
+
draftVersion: version,
|
|
26
|
+
contractVersion: identifier,
|
|
27
|
+
templateVersion: identifier
|
|
28
|
+
}).strict();
|
|
29
|
+
var visualRenderReceiptSchema = visualPreviewTargetSchema.extend({
|
|
30
|
+
dependencyVersions: z.record(safeKey, identifier).refine((value) => Object.keys(value).length <= 256)
|
|
31
|
+
}).strict();
|
|
32
|
+
var envelope = {
|
|
33
|
+
protocol: z.literal(VISUAL_EDIT_PROTOCOL),
|
|
34
|
+
protocolVersion: z.literal(VISUAL_EDIT_PROTOCOL_VERSION),
|
|
35
|
+
session,
|
|
36
|
+
sequence: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
37
|
+
target: visualPreviewTargetSchema
|
|
38
|
+
};
|
|
39
|
+
var messageSchema = z.discriminatedUnion("type", [
|
|
40
|
+
z.object({ ...envelope, type: z.literal("connect") }).strict(),
|
|
41
|
+
z.object({ ...envelope, type: z.literal("ready") }).strict(),
|
|
42
|
+
z.object({ ...envelope, type: z.literal("select"), address: visualEditAddressSchema, position: position.optional() }).strict(),
|
|
43
|
+
z.object({ ...envelope, type: z.literal("focus"), address: visualEditAddressSchema.optional(), position: position.optional() }).strict(),
|
|
44
|
+
z.object({ ...envelope, type: z.literal("navigate"), pathname }).strict(),
|
|
45
|
+
z.object({ ...envelope, type: z.literal("mode"), mode: z.enum(["select", "navigate"]) }).strict(),
|
|
46
|
+
z.object({ ...envelope, type: z.literal("refresh") }).strict(),
|
|
47
|
+
z.object({ ...envelope, type: z.literal("viewport"), position }).strict(),
|
|
48
|
+
z.object({ ...envelope, type: z.literal("rendered"), receipt: visualRenderReceiptSchema }).strict(),
|
|
49
|
+
z.object({ ...envelope, type: z.literal("error"), code: z.enum(["unavailable", "superseded", "unsupported_contract", "expired"]), message: z.string().max(500).optional() }).strict()
|
|
50
|
+
]);
|
|
51
|
+
function parseVisualEditAddress(value) {
|
|
52
|
+
const result = visualEditAddressSchema.safeParse(value);
|
|
53
|
+
return result.success ? result.data : null;
|
|
54
|
+
}
|
|
55
|
+
function parseVisualEditMessage(value) {
|
|
56
|
+
try {
|
|
57
|
+
if (JSON.stringify(value).length > 64e3) return null;
|
|
58
|
+
const result = messageSchema.safeParse(value);
|
|
59
|
+
if (!result.success) return null;
|
|
60
|
+
const message = result.data;
|
|
61
|
+
if (message.type === "rendered" && !sameVisualPreviewTarget(message.target, message.receipt)) return null;
|
|
62
|
+
if (message.type === "focus" && message.address && message.address.entityId !== message.target.entityId) return null;
|
|
63
|
+
return message;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function sameVisualPreviewTarget(a, b) {
|
|
69
|
+
return a.entityId === b.entityId && a.draftId === b.draftId && a.draftVersion === b.draftVersion && a.contractVersion === b.contractVersion && a.templateVersion === b.templateVersion;
|
|
70
|
+
}
|
|
71
|
+
function sameVisualDependencies(a, b) {
|
|
72
|
+
const keys = Object.keys(a);
|
|
73
|
+
return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && a[key] === b[key]);
|
|
74
|
+
}
|
|
75
|
+
function visualEditAddressKey(address) {
|
|
76
|
+
const path = address.path ?? [];
|
|
77
|
+
const itemId = [...path].reverse().find((part) => part.startsWith("@"))?.slice(1) ?? address.itemId ?? null;
|
|
78
|
+
return JSON.stringify([address.entityId, address.fieldKey, itemId, path]);
|
|
79
|
+
}
|
|
80
|
+
function visualEditAttributes(address, enabled = true) {
|
|
81
|
+
const parsed = enabled ? parseVisualEditAddress(address) : null;
|
|
82
|
+
return parsed ? { [VISUAL_EDIT_ATTRIBUTE]: JSON.stringify(parsed) } : {};
|
|
83
|
+
}
|
|
84
|
+
function isVisualEditOrigin(value) {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(value);
|
|
87
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
88
|
+
return value === url.origin && (url.protocol === "https:" || local && url.protocol === "http:");
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function buildVisualEditPreviewUrl(urlValue, editorOrigin, sessionId) {
|
|
94
|
+
try {
|
|
95
|
+
const url = new URL(urlValue);
|
|
96
|
+
if (url.username || url.password || !isVisualEditOrigin(url.origin) || !isVisualEditOrigin(editorOrigin) || !session.safeParse(sessionId).success) return null;
|
|
97
|
+
url.hash = new URLSearchParams({ "rt-visual-edit": sessionId, origin: editorOrigin }).toString();
|
|
98
|
+
return url.href;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/content-values.ts
|
|
105
|
+
import { z as z2 } from "zod";
|
|
106
|
+
var itemIdSchema = z2.string().uuid();
|
|
107
|
+
var forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
108
|
+
function isContentRecord(value) {
|
|
109
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
110
|
+
const prototype = Object.getPrototypeOf(value);
|
|
111
|
+
return prototype === Object.prototype || prototype === null;
|
|
112
|
+
}
|
|
113
|
+
function validatePath(path) {
|
|
114
|
+
if (!Array.isArray(path) || !path.length || path.some((key) => typeof key !== "string" || !key || forbiddenKeys.has(key) || /^\d+$/u.test(key) || /[\u0000-\u001f]/u.test(key))) {
|
|
115
|
+
throw new Error("Content path must use safe property names and stable item IDs");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function findItemIndex(items, token) {
|
|
119
|
+
if (!token.startsWith("@") || !itemIdSchema.safeParse(token.slice(1)).success) {
|
|
120
|
+
throw new Error("Content repeater path requires @<uuid>");
|
|
121
|
+
}
|
|
122
|
+
const targetId = token.slice(1);
|
|
123
|
+
const seen = /* @__PURE__ */ new Set();
|
|
124
|
+
let selected = -1;
|
|
125
|
+
for (const [index, item] of items.entries()) {
|
|
126
|
+
if (!isContentRecord(item) || !Object.hasOwn(item, "_id")) throw new Error("Content repeater item has no stable ID");
|
|
127
|
+
const parsed = itemIdSchema.safeParse(item._id);
|
|
128
|
+
if (!parsed.success) throw new Error("Content repeater item has an invalid ID");
|
|
129
|
+
if (seen.has(parsed.data)) throw new Error("Content repeater item IDs are ambiguous");
|
|
130
|
+
seen.add(parsed.data);
|
|
131
|
+
if (parsed.data === targetId) selected = index;
|
|
132
|
+
}
|
|
133
|
+
if (selected < 0) throw new Error("Content repeater item was not found");
|
|
134
|
+
return selected;
|
|
135
|
+
}
|
|
136
|
+
function locateContentValue(values, path) {
|
|
137
|
+
validatePath(path);
|
|
138
|
+
if (!isContentRecord(values)) throw new Error("Content values must be a serialized object");
|
|
139
|
+
const frames = [];
|
|
140
|
+
let current = values;
|
|
141
|
+
for (const key of path) {
|
|
142
|
+
if (Array.isArray(current)) {
|
|
143
|
+
const index = findItemIndex(current, key);
|
|
144
|
+
frames.push({ kind: "array", parent: current, index, itemId: key.slice(1) });
|
|
145
|
+
current = current[index];
|
|
146
|
+
} else {
|
|
147
|
+
if (!isContentRecord(current) || key.startsWith("@") || !Object.hasOwn(current, key)) {
|
|
148
|
+
throw new Error("Content property was not found");
|
|
149
|
+
}
|
|
150
|
+
frames.push({ kind: "object", parent: current, key });
|
|
151
|
+
current = current[key];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { value: current, frames };
|
|
155
|
+
}
|
|
156
|
+
function getContentValue(values, path) {
|
|
157
|
+
return locateContentValue(values, path).value;
|
|
158
|
+
}
|
|
159
|
+
function setContentValue(values, path, value) {
|
|
160
|
+
const { frames } = locateContentValue(values, path);
|
|
161
|
+
const leaf = frames[frames.length - 1];
|
|
162
|
+
if (leaf.kind === "object" && leaf.key === "_id" || leaf.kind === "array" && (!isContentRecord(value) || !Object.hasOwn(value, "_id") || value._id !== leaf.itemId)) {
|
|
163
|
+
throw new Error("Content edits must preserve stable item IDs");
|
|
164
|
+
}
|
|
165
|
+
let patched = value;
|
|
166
|
+
for (let index = frames.length - 1; index >= 0; index--) {
|
|
167
|
+
const frame = frames[index];
|
|
168
|
+
if (frame.kind === "array") {
|
|
169
|
+
const items = [...frame.parent];
|
|
170
|
+
items[frame.index] = patched;
|
|
171
|
+
patched = items;
|
|
172
|
+
} else {
|
|
173
|
+
patched = { ...frame.parent, [frame.key]: patched };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return patched;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export {
|
|
180
|
+
VISUAL_EDIT_PROTOCOL,
|
|
181
|
+
VISUAL_EDIT_PROTOCOL_VERSION,
|
|
182
|
+
VISUAL_EDIT_ATTRIBUTE,
|
|
183
|
+
visualEditAddressSchema,
|
|
184
|
+
visualPreviewTargetSchema,
|
|
185
|
+
visualRenderReceiptSchema,
|
|
186
|
+
parseVisualEditAddress,
|
|
187
|
+
parseVisualEditMessage,
|
|
188
|
+
sameVisualPreviewTarget,
|
|
189
|
+
sameVisualDependencies,
|
|
190
|
+
visualEditAddressKey,
|
|
191
|
+
visualEditAttributes,
|
|
192
|
+
isVisualEditOrigin,
|
|
193
|
+
buildVisualEditPreviewUrl,
|
|
194
|
+
getContentValue,
|
|
195
|
+
setContentValue
|
|
196
|
+
};
|
|
197
|
+
//# sourceMappingURL=chunk-WNU53PSH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/visual-edit-contract.ts","../src/content-values.ts"],"sourcesContent":["import { z } from \"zod\";\n\nexport const VISUAL_EDIT_PROTOCOL = \"roottale:visual-edit\";\nexport const VISUAL_EDIT_PROTOCOL_VERSION = 1;\nexport const VISUAL_EDIT_ATTRIBUTE = \"data-rt-edit\";\n\nconst identifier = z.string().min(1).max(160).refine(value => !/[\\u0000-\\u001f]/.test(value));\nconst safeKey = identifier.refine(value => ![\"__proto__\", \"prototype\", \"constructor\"].includes(value));\nconst pathKey = safeKey.refine(value => !/^\\d+$/.test(value), \"반복 항목은 순번 대신 itemId로 선택합니다.\");\nconst version = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);\nconst session = z.string().uuid();\nconst position = z.object({ left: z.number().finite().min(0).max(10_000_000), top: z.number().finite().min(0).max(10_000_000) }).strict();\nconst pathname = z.string().min(1).max(2000).refine(value => value.startsWith(\"/\") && !value.startsWith(\"//\") && !/[\\\\?#\\u0000-\\u001f]/.test(value));\n\n/** 필드 정의의 key와 저장 경로는 구분한다. 반복행의 영속 주소는 _id다. */\nexport const visualEditAddressSchema = z.object({\n entityId: identifier,\n fieldKey: safeKey,\n itemId: identifier.optional(),\n path: z.array(pathKey).max(16).optional(),\n}).strict().refine(address => {\n const selector = [...(address.path ?? [])].reverse().find(part => part.startsWith(\"@\"));\n return !selector || !address.itemId || selector.slice(1) === address.itemId;\n}, \"itemId는 경로의 마지막 반복 항목 ID와 같아야 합니다.\");\n\nexport const visualPreviewTargetSchema = z.object({\n entityId: identifier,\n draftId: identifier,\n draftVersion: version,\n contractVersion: identifier,\n templateVersion: identifier,\n}).strict();\n\nexport const visualRenderReceiptSchema = visualPreviewTargetSchema.extend({\n dependencyVersions: z.record(safeKey, identifier).refine(value => Object.keys(value).length <= 256),\n}).strict();\n\nexport type VisualEditAddress = z.infer<typeof visualEditAddressSchema>;\nexport type VisualPreviewTarget = z.infer<typeof visualPreviewTargetSchema>;\nexport type VisualRenderReceipt = z.infer<typeof visualRenderReceiptSchema>;\nexport type VisualViewportPosition = z.infer<typeof position>;\nexport type VisualEditMode = \"select\" | \"navigate\";\n\nconst envelope = {\n protocol: z.literal(VISUAL_EDIT_PROTOCOL),\n protocolVersion: z.literal(VISUAL_EDIT_PROTOCOL_VERSION),\n session,\n sequence: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),\n target: visualPreviewTargetSchema,\n};\n\nconst messageSchema = z.discriminatedUnion(\"type\", [\n z.object({ ...envelope, type: z.literal(\"connect\") }).strict(),\n z.object({ ...envelope, type: z.literal(\"ready\") }).strict(),\n z.object({ ...envelope, type: z.literal(\"select\"), address: visualEditAddressSchema, position: position.optional() }).strict(),\n z.object({ ...envelope, type: z.literal(\"focus\"), address: visualEditAddressSchema.optional(), position: position.optional() }).strict(),\n z.object({ ...envelope, type: z.literal(\"navigate\"), pathname }).strict(),\n z.object({ ...envelope, type: z.literal(\"mode\"), mode: z.enum([\"select\", \"navigate\"]) }).strict(),\n z.object({ ...envelope, type: z.literal(\"refresh\") }).strict(),\n z.object({ ...envelope, type: z.literal(\"viewport\"), position }).strict(),\n z.object({ ...envelope, type: z.literal(\"rendered\"), receipt: visualRenderReceiptSchema }).strict(),\n z.object({ ...envelope, type: z.literal(\"error\"), code: z.enum([\"unavailable\", \"superseded\", \"unsupported_contract\", \"expired\"]), message: z.string().max(500).optional() }).strict(),\n]);\n\nexport type VisualEditMessage = z.infer<typeof messageSchema>;\nexport type VisualEditPayload = VisualEditMessage extends infer Message\n ? Message extends VisualEditMessage ? Omit<Message, keyof typeof envelope> : never : never;\n\nexport function parseVisualEditAddress(value: unknown): VisualEditAddress | null {\n const result = visualEditAddressSchema.safeParse(value);\n return result.success ? result.data : null;\n}\n\nexport function parseVisualEditMessage(value: unknown): VisualEditMessage | null {\n try {\n if (JSON.stringify(value).length > 64_000) return null;\n const result = messageSchema.safeParse(value);\n if (!result.success) return null;\n const message = result.data;\n if (message.type === \"rendered\" && !sameVisualPreviewTarget(message.target, message.receipt)) return null;\n if (message.type === \"focus\" && message.address && message.address.entityId !== message.target.entityId) return null;\n return message;\n } catch { return null; }\n}\n\nexport function sameVisualPreviewTarget(a: VisualPreviewTarget, b: VisualPreviewTarget): boolean {\n return a.entityId === b.entityId && a.draftId === b.draftId && a.draftVersion === b.draftVersion\n && a.contractVersion === b.contractVersion && a.templateVersion === b.templateVersion;\n}\n\nexport function sameVisualDependencies(a: Record<string, string>, b: Record<string, string>): boolean {\n const keys = Object.keys(a);\n return keys.length === Object.keys(b).length && keys.every(key => Object.hasOwn(b, key) && a[key] === b[key]);\n}\n\nexport function visualEditAddressKey(address: VisualEditAddress): string {\n const path = address.path ?? [];\n // 중첩 반복행은 경로가 정본이다. itemId는 사이트가 생략할 수 있는 보조값이다.\n const itemId = [...path].reverse().find(part => part.startsWith(\"@\"))?.slice(1) ?? address.itemId ?? null;\n return JSON.stringify([address.entityId, address.fieldKey, itemId, path]);\n}\n\n/** 미리보기에서만 사용한다. RSC에서도 호출하는 순수 속성 생성 함수다. */\nexport function visualEditAttributes(address: VisualEditAddress, enabled = true): { \"data-rt-edit\"?: string } {\n const parsed = enabled ? parseVisualEditAddress(address) : null;\n return parsed ? { [VISUAL_EDIT_ATTRIBUTE]: JSON.stringify(parsed) } : {};\n}\n\nexport function isVisualEditOrigin(value: string): boolean {\n try {\n const url = new URL(value);\n const local = [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname);\n return value === url.origin && (url.protocol === \"https:\" || (local && url.protocol === \"http:\"));\n } catch { return false; }\n}\n\n/** 내용·권한은 기존 서버 snapshot 경로가 소유한다. fragment에는 연결 식별자만 싣는다. */\nexport function buildVisualEditPreviewUrl(urlValue: string, editorOrigin: string, sessionId: string): string | null {\n try {\n const url = new URL(urlValue);\n if (url.username || url.password || !isVisualEditOrigin(url.origin) || !isVisualEditOrigin(editorOrigin) || !session.safeParse(sessionId).success) return null;\n url.hash = new URLSearchParams({ \"rt-visual-edit\": sessionId, origin: editorOrigin }).toString();\n return url.href;\n } catch { return null; }\n}\n","import { z } from \"zod\";\n\nconst itemIdSchema = z.string().uuid();\nconst forbiddenKeys = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\n\ntype PathFrame =\n | { readonly kind: \"object\"; readonly parent: Record<string, unknown>; readonly key: string }\n | { readonly kind: \"array\"; readonly parent: readonly unknown[]; readonly index: number; readonly itemId: string };\n\nfunction isContentRecord(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const prototype: unknown = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction validatePath(path: readonly string[]): void {\n if (!Array.isArray(path) || !path.length || path.some((key) => typeof key !== \"string\" || !key\n || forbiddenKeys.has(key) || /^\\d+$/u.test(key) || /[\\u0000-\\u001f]/u.test(key))) {\n throw new Error(\"Content path must use safe property names and stable item IDs\");\n }\n}\n\nfunction findItemIndex(items: readonly unknown[], token: string): number {\n if (!token.startsWith(\"@\") || !itemIdSchema.safeParse(token.slice(1)).success) {\n throw new Error(\"Content repeater path requires @<uuid>\");\n }\n const targetId = token.slice(1);\n const seen = new Set<string>();\n let selected = -1;\n for (const [index, item] of items.entries()) {\n if (!isContentRecord(item) || !Object.hasOwn(item, \"_id\")) throw new Error(\"Content repeater item has no stable ID\");\n const parsed = itemIdSchema.safeParse(item._id);\n if (!parsed.success) throw new Error(\"Content repeater item has an invalid ID\");\n if (seen.has(parsed.data)) throw new Error(\"Content repeater item IDs are ambiguous\");\n seen.add(parsed.data);\n if (parsed.data === targetId) selected = index;\n }\n if (selected < 0) throw new Error(\"Content repeater item was not found\");\n return selected;\n}\n\nfunction locateContentValue(values: Readonly<Record<string, unknown>>, path: readonly string[]): {\n readonly value: unknown; readonly frames: readonly PathFrame[];\n} {\n validatePath(path);\n if (!isContentRecord(values)) throw new Error(\"Content values must be a serialized object\");\n const frames: PathFrame[] = [];\n let current: unknown = values;\n for (const key of path) {\n if (Array.isArray(current)) {\n const index = findItemIndex(current, key);\n frames.push({ kind: \"array\", parent: current, index, itemId: key.slice(1) });\n current = current[index];\n } else {\n if (!isContentRecord(current) || key.startsWith(\"@\") || !Object.hasOwn(current, key)) {\n throw new Error(\"Content property was not found\");\n }\n frames.push({ kind: \"object\", parent: current, key });\n current = current[key];\n }\n }\n return { value: current, frames };\n}\n\n/**\n * Paths start at the serialized ACF root. Each array traversal requires @<uuid>.\n * Unknown properties and repeaters with missing or duplicate IDs throw; no defaults are invented.\n */\nexport function getContentValue(values: Readonly<Record<string, unknown>>, path: readonly string[]): unknown {\n return locateContentValue(values, path).value;\n}\n\n/**\n * Clone only the changed branch and preserve stable IDs. Whole-array replacement is allowed.\n * Leaf permissions and all replacement-value validation belong to the compiled content contract.\n */\nexport function setContentValue(\n values: Readonly<Record<string, unknown>>, path: readonly string[], value: unknown,\n): Record<string, unknown> {\n const { frames } = locateContentValue(values, path);\n const leaf = frames[frames.length - 1]!;\n if ((leaf.kind === \"object\" && leaf.key === \"_id\") || (leaf.kind === \"array\"\n && (!isContentRecord(value) || !Object.hasOwn(value, \"_id\") || value._id !== leaf.itemId))) {\n throw new Error(\"Content edits must preserve stable item IDs\");\n }\n let patched: unknown = value;\n for (let index = frames.length - 1; index >= 0; index--) {\n const frame = frames[index]!;\n if (frame.kind === \"array\") {\n const items = [...frame.parent];\n items[frame.index] = patched;\n patched = items;\n } else {\n patched = { ...frame.parent, [frame.key]: patched };\n }\n }\n return patched as Record<string, unknown>;\n}\n"],"mappings":"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","names":["z"]}
|