@407dev/field-types 0.1.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/README.md +34 -0
- package/dist/index.d.ts +226 -0
- package/dist/index.js +1670 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1670 @@
|
|
|
1
|
+
// src/registry.ts
|
|
2
|
+
var registry = /* @__PURE__ */ new Map();
|
|
3
|
+
function registerFieldType(def) {
|
|
4
|
+
registry.set(def.id, def);
|
|
5
|
+
}
|
|
6
|
+
function getFieldType(id) {
|
|
7
|
+
return registry.get(id);
|
|
8
|
+
}
|
|
9
|
+
function hasFieldType(id) {
|
|
10
|
+
return registry.has(id);
|
|
11
|
+
}
|
|
12
|
+
function listFieldTypes() {
|
|
13
|
+
return Array.from(registry.values());
|
|
14
|
+
}
|
|
15
|
+
function listFieldTypeIds() {
|
|
16
|
+
return Array.from(registry.keys());
|
|
17
|
+
}
|
|
18
|
+
function fieldTypeAliases() {
|
|
19
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
20
|
+
for (const def of registry.values()) {
|
|
21
|
+
if (def.aliases) {
|
|
22
|
+
for (const alias of def.aliases) {
|
|
23
|
+
aliases.set(alias, def.id);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return aliases;
|
|
28
|
+
}
|
|
29
|
+
function resolveTypeAlias(name) {
|
|
30
|
+
if (registry.has(name)) {
|
|
31
|
+
return name;
|
|
32
|
+
}
|
|
33
|
+
const aliasMap = fieldTypeAliases();
|
|
34
|
+
return aliasMap.get(name) ?? name;
|
|
35
|
+
}
|
|
36
|
+
function normalizeFieldValue(typeId, value, constraints) {
|
|
37
|
+
const canonicalId = resolveTypeAlias(typeId);
|
|
38
|
+
const typeDef = registry.get(canonicalId) || registry.get(typeId);
|
|
39
|
+
if (!typeDef?.normalize) {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return typeDef.normalize(value, constraints);
|
|
44
|
+
} catch {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function unknownTypeIds(ids) {
|
|
49
|
+
return ids.filter((id) => !registry.has(resolveTypeAlias(id)));
|
|
50
|
+
}
|
|
51
|
+
function validateFieldValue(typeId, value, constraints) {
|
|
52
|
+
const canonicalId = resolveTypeAlias(typeId);
|
|
53
|
+
const typeDef = registry.get(canonicalId) || registry.get(typeId);
|
|
54
|
+
if (!typeDef) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
errors: [
|
|
58
|
+
{
|
|
59
|
+
path: "",
|
|
60
|
+
message: `Unknown field type: '${typeId}'`,
|
|
61
|
+
keyword: "unknown_type"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return typeDef.validate(value, constraints);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return {
|
|
70
|
+
valid: false,
|
|
71
|
+
errors: [
|
|
72
|
+
{
|
|
73
|
+
path: "",
|
|
74
|
+
message: err instanceof Error ? err.message : "Validation exception",
|
|
75
|
+
keyword: "validation_error"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function resetRegistry() {
|
|
82
|
+
registry.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/types/address.ts
|
|
86
|
+
var ALLOWED_ADDRESS_KEYS = /* @__PURE__ */ new Set([
|
|
87
|
+
"line1",
|
|
88
|
+
"line2",
|
|
89
|
+
"city",
|
|
90
|
+
"region",
|
|
91
|
+
"postal",
|
|
92
|
+
"country",
|
|
93
|
+
"lat",
|
|
94
|
+
"lng"
|
|
95
|
+
]);
|
|
96
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
97
|
+
var ISO2_REGEX = /^[A-Za-z]{2}$/;
|
|
98
|
+
var addressType = {
|
|
99
|
+
id: "address",
|
|
100
|
+
defaultValue: null,
|
|
101
|
+
constraintsSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
countryFormat: { type: "string", enum: ["iso2", "free"], default: "iso2" }
|
|
105
|
+
},
|
|
106
|
+
additionalProperties: false
|
|
107
|
+
},
|
|
108
|
+
validate(value, constraints) {
|
|
109
|
+
if (value === null || value === void 0) {
|
|
110
|
+
return { valid: true };
|
|
111
|
+
}
|
|
112
|
+
const errors = [];
|
|
113
|
+
const c = constraints || {};
|
|
114
|
+
const countryFormat = c.countryFormat || "iso2";
|
|
115
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
116
|
+
return {
|
|
117
|
+
valid: false,
|
|
118
|
+
errors: [
|
|
119
|
+
{
|
|
120
|
+
path: "",
|
|
121
|
+
message: `Expected address object, got ${typeof value}`,
|
|
122
|
+
keyword: "type"
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const record = value;
|
|
128
|
+
for (const key of Object.keys(record)) {
|
|
129
|
+
if (FORBIDDEN_KEYS.has(key) || !ALLOWED_ADDRESS_KEYS.has(key)) {
|
|
130
|
+
errors.push({
|
|
131
|
+
path: key,
|
|
132
|
+
message: `Unknown or forbidden property '${key}' in address`,
|
|
133
|
+
keyword: "additional_properties"
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const requiredStringFields = [
|
|
138
|
+
"line1",
|
|
139
|
+
"city",
|
|
140
|
+
"region",
|
|
141
|
+
"postal",
|
|
142
|
+
"country"
|
|
143
|
+
];
|
|
144
|
+
for (const field of requiredStringFields) {
|
|
145
|
+
const val = record[field];
|
|
146
|
+
if (typeof val !== "string" || !val.trim()) {
|
|
147
|
+
errors.push({
|
|
148
|
+
path: field,
|
|
149
|
+
message: `Address field '${field}' is required and must be a non-empty string`,
|
|
150
|
+
keyword: "required"
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (record.line2 !== void 0 && record.line2 !== null && record.line2 !== "") {
|
|
155
|
+
if (typeof record.line2 !== "string") {
|
|
156
|
+
errors.push({
|
|
157
|
+
path: "line2",
|
|
158
|
+
message: "Address line2 must be a string",
|
|
159
|
+
keyword: "type"
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (typeof record.country === "string" && record.country.trim()) {
|
|
164
|
+
if (countryFormat === "iso2" && !ISO2_REGEX.test(record.country.trim())) {
|
|
165
|
+
errors.push({
|
|
166
|
+
path: "country",
|
|
167
|
+
message: `Country '${record.country}' must be a 2-letter ISO-3166-1 alpha-2 code`,
|
|
168
|
+
keyword: "country_format"
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (record.lat !== void 0 && record.lat !== null) {
|
|
173
|
+
if (typeof record.lat !== "number" || !Number.isFinite(record.lat)) {
|
|
174
|
+
errors.push({
|
|
175
|
+
path: "lat",
|
|
176
|
+
message: "Latitude must be a finite number",
|
|
177
|
+
keyword: "type"
|
|
178
|
+
});
|
|
179
|
+
} else if (record.lat < -90 || record.lat > 90) {
|
|
180
|
+
errors.push({
|
|
181
|
+
path: "lat",
|
|
182
|
+
message: `Latitude ${record.lat} is out of bounds (must be between -90 and 90)`,
|
|
183
|
+
keyword: "lat_out_of_bounds"
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (record.lng !== void 0 && record.lng !== null) {
|
|
188
|
+
if (typeof record.lng !== "number" || !Number.isFinite(record.lng)) {
|
|
189
|
+
errors.push({
|
|
190
|
+
path: "lng",
|
|
191
|
+
message: "Longitude must be a finite number",
|
|
192
|
+
keyword: "type"
|
|
193
|
+
});
|
|
194
|
+
} else if (record.lng < -180 || record.lng > 180) {
|
|
195
|
+
errors.push({
|
|
196
|
+
path: "lng",
|
|
197
|
+
message: `Longitude ${record.lng} is out of bounds (must be between -180 and 180)`,
|
|
198
|
+
keyword: "lng_out_of_bounds"
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (errors.length === 0) {
|
|
203
|
+
return { valid: true };
|
|
204
|
+
}
|
|
205
|
+
return { valid: false, errors };
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
var addressListType = {
|
|
209
|
+
id: "address.list",
|
|
210
|
+
aliases: ["addressList"],
|
|
211
|
+
defaultValue: [],
|
|
212
|
+
constraintsSchema: {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
minItems: { type: "integer", minimum: 0 },
|
|
216
|
+
maxItems: { type: "integer", minimum: 0, default: 25 },
|
|
217
|
+
countryFormat: { type: "string", enum: ["iso2", "free"], default: "iso2" }
|
|
218
|
+
},
|
|
219
|
+
additionalProperties: false
|
|
220
|
+
},
|
|
221
|
+
validate(value, constraints) {
|
|
222
|
+
if (value === null || value === void 0) {
|
|
223
|
+
return { valid: true };
|
|
224
|
+
}
|
|
225
|
+
const errors = [];
|
|
226
|
+
const c = constraints || {};
|
|
227
|
+
const maxItems = typeof c.maxItems === "number" ? c.maxItems : 25;
|
|
228
|
+
if (!Array.isArray(value)) {
|
|
229
|
+
return {
|
|
230
|
+
valid: false,
|
|
231
|
+
errors: [
|
|
232
|
+
{
|
|
233
|
+
path: "",
|
|
234
|
+
message: `Expected array for address.list, got ${typeof value}`,
|
|
235
|
+
keyword: "type"
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (typeof c.minItems === "number" && value.length < c.minItems) {
|
|
241
|
+
errors.push({
|
|
242
|
+
path: "",
|
|
243
|
+
message: `Address list item count (${value.length}) is less than minimum (${c.minItems})`,
|
|
244
|
+
keyword: "min_items"
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (value.length > maxItems) {
|
|
248
|
+
errors.push({
|
|
249
|
+
path: "",
|
|
250
|
+
message: `Address list item count (${value.length}) exceeds maximum (${maxItems})`,
|
|
251
|
+
keyword: "max_items"
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
for (let i = 0; i < value.length; i++) {
|
|
255
|
+
const item = value[i];
|
|
256
|
+
const itemResult = addressType.validate(item, constraints);
|
|
257
|
+
if (!itemResult.valid) {
|
|
258
|
+
for (const err of itemResult.errors) {
|
|
259
|
+
const itemPath = err.path ? `[${i}].${err.path}` : `[${i}]`;
|
|
260
|
+
errors.push({
|
|
261
|
+
path: itemPath,
|
|
262
|
+
message: err.message,
|
|
263
|
+
keyword: err.keyword
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (errors.length === 0) {
|
|
269
|
+
return { valid: true };
|
|
270
|
+
}
|
|
271
|
+
return { valid: false, errors };
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/types/geoArea.ts
|
|
276
|
+
var FORBIDDEN_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
277
|
+
function crossProduct(a, b, c) {
|
|
278
|
+
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
279
|
+
}
|
|
280
|
+
var EPSILON = 1e-12;
|
|
281
|
+
function isPointOnSegment(p, a, b) {
|
|
282
|
+
return p[0] >= Math.min(a[0], b[0]) - EPSILON && p[0] <= Math.max(a[0], b[0]) + EPSILON && p[1] >= Math.min(a[1], b[1]) - EPSILON && p[1] <= Math.max(a[1], b[1]) + EPSILON;
|
|
283
|
+
}
|
|
284
|
+
function segmentsIntersect(a, b, c, d) {
|
|
285
|
+
const cp1 = crossProduct(a, b, c);
|
|
286
|
+
const cp2 = crossProduct(a, b, d);
|
|
287
|
+
const cp3 = crossProduct(c, d, a);
|
|
288
|
+
const cp4 = crossProduct(c, d, b);
|
|
289
|
+
if ((cp1 > EPSILON && cp2 < -EPSILON || cp1 < -EPSILON && cp2 > EPSILON) && (cp3 > EPSILON && cp4 < -EPSILON || cp3 < -EPSILON && cp4 > EPSILON)) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
if (Math.abs(cp1) <= EPSILON && isPointOnSegment(c, a, b)) return true;
|
|
293
|
+
if (Math.abs(cp2) <= EPSILON && isPointOnSegment(d, a, b)) return true;
|
|
294
|
+
if (Math.abs(cp3) <= EPSILON && isPointOnSegment(a, c, d)) return true;
|
|
295
|
+
if (Math.abs(cp4) <= EPSILON && isPointOnSegment(b, c, d)) return true;
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
function findRingSelfIntersection(ring) {
|
|
299
|
+
const numSegments = ring.length - 1;
|
|
300
|
+
if (numSegments < 3) return null;
|
|
301
|
+
for (let i = 0; i < numSegments; i++) {
|
|
302
|
+
const a = ring[i];
|
|
303
|
+
const b = ring[i + 1];
|
|
304
|
+
for (let j = i + 1; j < numSegments; j++) {
|
|
305
|
+
if (j === i + 1) continue;
|
|
306
|
+
if (i === 0 && j === numSegments - 1) continue;
|
|
307
|
+
const c = ring[j];
|
|
308
|
+
const d = ring[j + 1];
|
|
309
|
+
if (segmentsIntersect(a, b, c, d)) {
|
|
310
|
+
return [i, j];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
function normalizeRing(rawRing) {
|
|
317
|
+
if (!Array.isArray(rawRing) || rawRing.length === 0) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
const cleaned = [];
|
|
321
|
+
for (const pos of rawRing) {
|
|
322
|
+
if (!Array.isArray(pos) || pos.length < 2) continue;
|
|
323
|
+
const lon = Number(pos[0]);
|
|
324
|
+
const lat = Number(pos[1]);
|
|
325
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
|
|
326
|
+
if (cleaned.length > 0) {
|
|
327
|
+
const prev = cleaned[cleaned.length - 1];
|
|
328
|
+
if (Math.abs(prev[0] - lon) < EPSILON && Math.abs(prev[1] - lat) < EPSILON) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
cleaned.push([lon, lat]);
|
|
333
|
+
}
|
|
334
|
+
if (cleaned.length >= 1) {
|
|
335
|
+
const first = cleaned[0];
|
|
336
|
+
const last = cleaned[cleaned.length - 1];
|
|
337
|
+
if (Math.abs(first[0] - last[0]) >= EPSILON || Math.abs(first[1] - last[1]) >= EPSILON) {
|
|
338
|
+
cleaned.push([first[0], first[1]]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return cleaned;
|
|
342
|
+
}
|
|
343
|
+
function normalizeGeoArea(value) {
|
|
344
|
+
if (!value) {
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
let obj = value;
|
|
348
|
+
if (typeof value === "string") {
|
|
349
|
+
try {
|
|
350
|
+
obj = JSON.parse(value);
|
|
351
|
+
} catch {
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
if (obj.type === "Polygon" && Array.isArray(obj.coordinates)) {
|
|
359
|
+
const newCoords = obj.coordinates.map((ring) => normalizeRing(ring));
|
|
360
|
+
return {
|
|
361
|
+
...obj,
|
|
362
|
+
type: "Polygon",
|
|
363
|
+
coordinates: newCoords
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (obj.type === "MultiPolygon" && Array.isArray(obj.coordinates)) {
|
|
367
|
+
const newCoords = obj.coordinates.map(
|
|
368
|
+
(poly) => Array.isArray(poly) ? poly.map((ring) => normalizeRing(ring)) : []
|
|
369
|
+
);
|
|
370
|
+
return {
|
|
371
|
+
...obj,
|
|
372
|
+
type: "MultiPolygon",
|
|
373
|
+
coordinates: newCoords
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return value;
|
|
377
|
+
}
|
|
378
|
+
var geoAreaType = {
|
|
379
|
+
id: "geo.area",
|
|
380
|
+
aliases: ["geoArea"],
|
|
381
|
+
defaultValue: null,
|
|
382
|
+
constraintsSchema: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {
|
|
385
|
+
maxVertices: { type: "integer", minimum: 4, default: 2e3 },
|
|
386
|
+
maxRings: { type: "integer", minimum: 1, default: 20 }
|
|
387
|
+
},
|
|
388
|
+
additionalProperties: false
|
|
389
|
+
},
|
|
390
|
+
normalize: normalizeGeoArea,
|
|
391
|
+
validate(value, constraints) {
|
|
392
|
+
if (value === null || value === void 0 || value === "") {
|
|
393
|
+
return { valid: true };
|
|
394
|
+
}
|
|
395
|
+
const errors = [];
|
|
396
|
+
const c = constraints || {};
|
|
397
|
+
const maxVertices = typeof c.maxVertices === "number" ? c.maxVertices : 2e3;
|
|
398
|
+
const maxRings = typeof c.maxRings === "number" ? c.maxRings : 20;
|
|
399
|
+
let targetValue = value;
|
|
400
|
+
if (typeof value === "string") {
|
|
401
|
+
try {
|
|
402
|
+
targetValue = JSON.parse(value);
|
|
403
|
+
} catch {
|
|
404
|
+
return {
|
|
405
|
+
valid: false,
|
|
406
|
+
errors: [
|
|
407
|
+
{
|
|
408
|
+
path: "",
|
|
409
|
+
message: "Expected GeoJSON object for geo.area, got invalid JSON string",
|
|
410
|
+
keyword: "type"
|
|
411
|
+
}
|
|
412
|
+
]
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (typeof targetValue !== "object" || targetValue === null || Array.isArray(targetValue)) {
|
|
417
|
+
return {
|
|
418
|
+
valid: false,
|
|
419
|
+
errors: [
|
|
420
|
+
{
|
|
421
|
+
path: "",
|
|
422
|
+
message: `Expected GeoJSON object for geo.area, got ${typeof targetValue}`,
|
|
423
|
+
keyword: "type"
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
const record = targetValue;
|
|
429
|
+
for (const key of Object.keys(record)) {
|
|
430
|
+
if (FORBIDDEN_KEYS2.has(key)) {
|
|
431
|
+
errors.push({
|
|
432
|
+
path: key,
|
|
433
|
+
message: `Forbidden property '${key}' in GeoJSON object`,
|
|
434
|
+
keyword: "forbidden_property"
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (record.type !== "Polygon" && record.type !== "MultiPolygon") {
|
|
439
|
+
errors.push({
|
|
440
|
+
path: "type",
|
|
441
|
+
message: `GeoJSON type must be 'Polygon' or 'MultiPolygon', got '${String(record.type)}'`,
|
|
442
|
+
keyword: "geo_invalid_type"
|
|
443
|
+
});
|
|
444
|
+
return { valid: false, errors };
|
|
445
|
+
}
|
|
446
|
+
if (!Array.isArray(record.coordinates)) {
|
|
447
|
+
errors.push({
|
|
448
|
+
path: "coordinates",
|
|
449
|
+
message: "GeoJSON coordinates property must be an array",
|
|
450
|
+
keyword: "type"
|
|
451
|
+
});
|
|
452
|
+
return { valid: false, errors };
|
|
453
|
+
}
|
|
454
|
+
const rings = [];
|
|
455
|
+
if (record.type === "Polygon") {
|
|
456
|
+
const polyCoords = record.coordinates;
|
|
457
|
+
for (let r = 0; r < polyCoords.length; r++) {
|
|
458
|
+
if (!Array.isArray(polyCoords[r])) {
|
|
459
|
+
errors.push({
|
|
460
|
+
path: `coordinates[${r}]`,
|
|
461
|
+
message: `LinearRing at coordinates[${r}] must be an array of coordinates`,
|
|
462
|
+
keyword: "type"
|
|
463
|
+
});
|
|
464
|
+
} else {
|
|
465
|
+
rings.push({ ring: polyCoords[r], path: `coordinates[${r}]` });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} else {
|
|
469
|
+
const multiCoords = record.coordinates;
|
|
470
|
+
for (let p = 0; p < multiCoords.length; p++) {
|
|
471
|
+
const poly = multiCoords[p];
|
|
472
|
+
if (!Array.isArray(poly)) {
|
|
473
|
+
errors.push({
|
|
474
|
+
path: `coordinates[${p}]`,
|
|
475
|
+
message: `Polygon at coordinates[${p}] must be an array of rings`,
|
|
476
|
+
keyword: "type"
|
|
477
|
+
});
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
for (let r = 0; r < poly.length; r++) {
|
|
481
|
+
if (!Array.isArray(poly[r])) {
|
|
482
|
+
errors.push({
|
|
483
|
+
path: `coordinates[${p}][${r}]`,
|
|
484
|
+
message: `LinearRing at coordinates[${p}][${r}] must be an array of coordinates`,
|
|
485
|
+
keyword: "type"
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
rings.push({ ring: poly[r], path: `coordinates[${p}][${r}]` });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (rings.length > maxRings) {
|
|
494
|
+
errors.push({
|
|
495
|
+
path: "coordinates",
|
|
496
|
+
message: `Total rings count (${rings.length}) exceeds maximum allowed (${maxRings})`,
|
|
497
|
+
keyword: "geo_ring_cap"
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
let totalVertices = 0;
|
|
501
|
+
for (const { ring, path } of rings) {
|
|
502
|
+
totalVertices += ring.length;
|
|
503
|
+
if (ring.length < 4) {
|
|
504
|
+
errors.push({
|
|
505
|
+
path,
|
|
506
|
+
message: `LinearRing at ${path} must contain at least 4 positions (got ${ring.length})`,
|
|
507
|
+
keyword: "geo_ring_too_short"
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
let allPositionsValid = true;
|
|
511
|
+
const validPoints = [];
|
|
512
|
+
for (let p = 0; p < ring.length; p++) {
|
|
513
|
+
const pos = ring[p];
|
|
514
|
+
const posPath = `${path}[${p}]`;
|
|
515
|
+
if (!Array.isArray(pos) || pos.length !== 2) {
|
|
516
|
+
errors.push({
|
|
517
|
+
path: posPath,
|
|
518
|
+
message: `Position at ${posPath} must be a 2-element [longitude, latitude] array (elevation is not supported)`,
|
|
519
|
+
keyword: "geo_position_invalid"
|
|
520
|
+
});
|
|
521
|
+
allPositionsValid = false;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const lon = pos[0];
|
|
525
|
+
const lat = pos[1];
|
|
526
|
+
if (typeof lon !== "number" || typeof lat !== "number" || !Number.isFinite(lon) || !Number.isFinite(lat)) {
|
|
527
|
+
errors.push({
|
|
528
|
+
path: posPath,
|
|
529
|
+
message: `Position at ${posPath} contains non-finite numbers`,
|
|
530
|
+
keyword: "geo_position_invalid"
|
|
531
|
+
});
|
|
532
|
+
allPositionsValid = false;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (lon < -180 || lon > 180 || lat < -90 || lat > 90) {
|
|
536
|
+
errors.push({
|
|
537
|
+
path: posPath,
|
|
538
|
+
message: `Position [${lon}, ${lat}] at ${posPath} is out of bounds (longitude must be in [-180, 180], latitude in [-90, 90])`,
|
|
539
|
+
keyword: "geo_out_of_bounds"
|
|
540
|
+
});
|
|
541
|
+
allPositionsValid = false;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
validPoints.push([lon, lat]);
|
|
545
|
+
}
|
|
546
|
+
if (allPositionsValid && validPoints.length >= 4) {
|
|
547
|
+
const first = validPoints[0];
|
|
548
|
+
const last = validPoints[validPoints.length - 1];
|
|
549
|
+
if (Math.abs(first[0] - last[0]) > EPSILON || Math.abs(first[1] - last[1]) > EPSILON) {
|
|
550
|
+
errors.push({
|
|
551
|
+
path,
|
|
552
|
+
message: `LinearRing at ${path} is not closed (first position [${first[0]}, ${first[1]}] does not match last position [${last[0]}, ${last[1]}])`,
|
|
553
|
+
keyword: "geo_ring_not_closed"
|
|
554
|
+
});
|
|
555
|
+
} else {
|
|
556
|
+
const intersection = findRingSelfIntersection(validPoints);
|
|
557
|
+
if (intersection) {
|
|
558
|
+
errors.push({
|
|
559
|
+
path,
|
|
560
|
+
message: `LinearRing at ${path} self-intersects between segment ${intersection[0]} and segment ${intersection[1]}`,
|
|
561
|
+
keyword: "geo_self_intersection"
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (totalVertices > maxVertices) {
|
|
568
|
+
errors.push({
|
|
569
|
+
path: "coordinates",
|
|
570
|
+
message: `Total vertex count (${totalVertices}) exceeds maximum allowed (${maxVertices})`,
|
|
571
|
+
keyword: "geo_vertex_cap"
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
if (errors.length === 0) {
|
|
575
|
+
return { valid: true };
|
|
576
|
+
}
|
|
577
|
+
return { valid: false, errors };
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/types/image.ts
|
|
582
|
+
var imageType = {
|
|
583
|
+
id: "image",
|
|
584
|
+
aliases: ["img"],
|
|
585
|
+
defaultValue: null,
|
|
586
|
+
renderHint: { tag: "img" },
|
|
587
|
+
constraintsSchema: {
|
|
588
|
+
type: "object",
|
|
589
|
+
properties: {
|
|
590
|
+
maxSizeBytes: { type: "integer", minimum: 0 },
|
|
591
|
+
allowedMimeTypes: {
|
|
592
|
+
type: "array",
|
|
593
|
+
items: { type: "string" }
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
additionalProperties: false
|
|
597
|
+
},
|
|
598
|
+
validate(value) {
|
|
599
|
+
const errors = [];
|
|
600
|
+
if (value === null || value === void 0 || value === "") {
|
|
601
|
+
return { valid: true };
|
|
602
|
+
}
|
|
603
|
+
if (typeof value === "string") {
|
|
604
|
+
return { valid: true };
|
|
605
|
+
}
|
|
606
|
+
if (typeof value === "object" && value !== null) {
|
|
607
|
+
const obj = value;
|
|
608
|
+
if (obj.kind === "asset" && typeof obj.id === "string" && obj.id.trim()) {
|
|
609
|
+
return { valid: true };
|
|
610
|
+
}
|
|
611
|
+
if (obj.kind === "url" && typeof obj.src === "string" && obj.src.trim()) {
|
|
612
|
+
return { valid: true };
|
|
613
|
+
}
|
|
614
|
+
if (typeof obj.id === "string" || typeof obj.url === "string") {
|
|
615
|
+
return { valid: true };
|
|
616
|
+
}
|
|
617
|
+
errors.push({
|
|
618
|
+
path: "",
|
|
619
|
+
message: "Image object must contain an id, url, or valid kind (asset/url)",
|
|
620
|
+
keyword: "image_structure"
|
|
621
|
+
});
|
|
622
|
+
return { valid: false, errors };
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
valid: false,
|
|
626
|
+
errors: [
|
|
627
|
+
{
|
|
628
|
+
path: "",
|
|
629
|
+
message: `Expected image reference (UUID string, URL, or asset object), got ${typeof value}`,
|
|
630
|
+
keyword: "type"
|
|
631
|
+
}
|
|
632
|
+
]
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/types/number.ts
|
|
638
|
+
var numberType = {
|
|
639
|
+
id: "number",
|
|
640
|
+
defaultValue: 0,
|
|
641
|
+
constraintsSchema: {
|
|
642
|
+
type: "object",
|
|
643
|
+
properties: {
|
|
644
|
+
min: { type: "number" },
|
|
645
|
+
max: { type: "number" },
|
|
646
|
+
step: { type: "number", exclusiveMinimum: 0 },
|
|
647
|
+
integer: { type: "boolean" }
|
|
648
|
+
},
|
|
649
|
+
additionalProperties: false
|
|
650
|
+
},
|
|
651
|
+
validate(value, constraints) {
|
|
652
|
+
const errors = [];
|
|
653
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
654
|
+
return {
|
|
655
|
+
valid: false,
|
|
656
|
+
errors: [
|
|
657
|
+
{
|
|
658
|
+
path: "",
|
|
659
|
+
message: `Expected finite number, got ${typeof value === "number" ? "NaN/Infinity" : typeof value}`,
|
|
660
|
+
keyword: "type"
|
|
661
|
+
}
|
|
662
|
+
]
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const c = constraints || {};
|
|
666
|
+
if (c.integer && !Number.isInteger(value)) {
|
|
667
|
+
errors.push({
|
|
668
|
+
path: "",
|
|
669
|
+
message: `Value ${value} must be an integer`,
|
|
670
|
+
keyword: "integer"
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
if (typeof c.min === "number" && value < c.min) {
|
|
674
|
+
errors.push({
|
|
675
|
+
path: "",
|
|
676
|
+
message: `Value ${value} is less than minimum ${c.min}`,
|
|
677
|
+
keyword: "min"
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (typeof c.max === "number" && value > c.max) {
|
|
681
|
+
errors.push({
|
|
682
|
+
path: "",
|
|
683
|
+
message: `Value ${value} exceeds maximum ${c.max}`,
|
|
684
|
+
keyword: "max"
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
if (typeof c.step === "number" && c.step > 0) {
|
|
688
|
+
const base = typeof c.min === "number" ? c.min : 0;
|
|
689
|
+
const diff = Math.abs(value - base);
|
|
690
|
+
const remainder = diff % c.step;
|
|
691
|
+
const isMultiple = remainder < 1e-9 || Math.abs(remainder - c.step) < 1e-9;
|
|
692
|
+
if (!isMultiple) {
|
|
693
|
+
errors.push({
|
|
694
|
+
path: "",
|
|
695
|
+
message: `Value ${value} is not a valid step multiple of ${c.step}`,
|
|
696
|
+
keyword: "step"
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (errors.length === 0) {
|
|
701
|
+
return { valid: true };
|
|
702
|
+
}
|
|
703
|
+
return { valid: false, errors };
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// src/url.ts
|
|
708
|
+
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
709
|
+
function stripUrlControlChars(raw) {
|
|
710
|
+
if (typeof raw !== "string") return "";
|
|
711
|
+
return raw.replace(/[\x00-\x20\x7F-\x9F\u200B-\u200D\uFEFF\s]/g, "");
|
|
712
|
+
}
|
|
713
|
+
function isSafeRelativeOrAnchor(cleaned) {
|
|
714
|
+
if (!cleaned) return false;
|
|
715
|
+
if (cleaned.startsWith("\\")) return false;
|
|
716
|
+
const lower = cleaned.toLowerCase();
|
|
717
|
+
if (lower.startsWith("/%5c") || lower.startsWith("%5c")) return false;
|
|
718
|
+
if (cleaned.startsWith("#")) {
|
|
719
|
+
return !cleaned.includes("\\");
|
|
720
|
+
}
|
|
721
|
+
if (cleaned.startsWith("/")) {
|
|
722
|
+
if (cleaned.length === 1) return true;
|
|
723
|
+
const second = cleaned[1];
|
|
724
|
+
if (second === "/" || second === "\\") return false;
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
function isSafeMediaSrc(raw, allowedHosts) {
|
|
730
|
+
if (typeof raw !== "string") return false;
|
|
731
|
+
const cleaned = stripUrlControlChars(raw);
|
|
732
|
+
if (!cleaned) return false;
|
|
733
|
+
if (UUID_REGEX.test(cleaned)) {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
if (isSafeRelativeOrAnchor(cleaned)) {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
const lower = cleaned.toLowerCase();
|
|
740
|
+
if (lower.startsWith("https://") || lower.startsWith("http://")) {
|
|
741
|
+
if (cleaned.includes("\\") || lower.includes("%5c")) return false;
|
|
742
|
+
try {
|
|
743
|
+
const parsed = new URL(cleaned);
|
|
744
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return false;
|
|
745
|
+
if (parsed.protocol === "http:") {
|
|
746
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
747
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") return true;
|
|
748
|
+
if (!allowedHosts || allowedHosts.length === 0) return false;
|
|
749
|
+
const allowed = allowedHosts.map((h) => h.toLowerCase());
|
|
750
|
+
return allowed.includes(hostname);
|
|
751
|
+
}
|
|
752
|
+
if (allowedHosts && allowedHosts.length > 0) {
|
|
753
|
+
const allowed = allowedHosts.map((h) => h.toLowerCase());
|
|
754
|
+
return allowed.includes(parsed.hostname.toLowerCase());
|
|
755
|
+
}
|
|
756
|
+
return true;
|
|
757
|
+
} catch {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
function isSafeActionHref(raw, allowedHosts) {
|
|
764
|
+
if (typeof raw !== "string") return false;
|
|
765
|
+
const cleaned = stripUrlControlChars(raw);
|
|
766
|
+
if (!cleaned) return false;
|
|
767
|
+
const lower = cleaned.toLowerCase();
|
|
768
|
+
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("blob:") || lower.startsWith("mailto:") || lower.startsWith("tel:") || lower.startsWith("http:")) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
if (isSafeRelativeOrAnchor(cleaned)) {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
if (lower.startsWith("https://")) {
|
|
775
|
+
if (!allowedHosts || allowedHosts.length === 0) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
if (cleaned.includes("\\") || lower.includes("%5c")) return false;
|
|
779
|
+
try {
|
|
780
|
+
const parsed = new URL(cleaned);
|
|
781
|
+
if (parsed.protocol !== "https:") return false;
|
|
782
|
+
const allowed = allowedHosts.map((h) => h.toLowerCase());
|
|
783
|
+
return allowed.includes(parsed.hostname.toLowerCase());
|
|
784
|
+
} catch {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/richtext.ts
|
|
792
|
+
var DEFAULT_ALLOWED_NODES = [
|
|
793
|
+
"doc",
|
|
794
|
+
"paragraph",
|
|
795
|
+
"heading",
|
|
796
|
+
"bulletList",
|
|
797
|
+
"orderedList",
|
|
798
|
+
"listItem",
|
|
799
|
+
"blockquote",
|
|
800
|
+
"codeBlock",
|
|
801
|
+
"hardBreak",
|
|
802
|
+
"horizontalRule",
|
|
803
|
+
"text",
|
|
804
|
+
"image"
|
|
805
|
+
];
|
|
806
|
+
var DEFAULT_ALLOWED_MARKS = ["bold", "italic", "underline", "strike", "code", "link"];
|
|
807
|
+
var FORBIDDEN_PROPERTY_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
808
|
+
function isValidLinkHref(rawHref) {
|
|
809
|
+
if (typeof rawHref !== "string") return false;
|
|
810
|
+
const cleaned = stripUrlControlChars(rawHref);
|
|
811
|
+
if (cleaned.length === 0) return false;
|
|
812
|
+
if (isSafeRelativeOrAnchor(cleaned)) {
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
const lower = cleaned.toLowerCase();
|
|
816
|
+
if (lower.startsWith("https://") || lower.startsWith("mailto:") || lower.startsWith("tel:")) {
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
function isValidImageSrc(rawSrc) {
|
|
822
|
+
if (typeof rawSrc !== "string") return false;
|
|
823
|
+
const cleaned = stripUrlControlChars(rawSrc);
|
|
824
|
+
if (cleaned.length === 0) return false;
|
|
825
|
+
if (isSafeRelativeOrAnchor(cleaned)) return true;
|
|
826
|
+
const lower = cleaned.toLowerCase();
|
|
827
|
+
if (lower.startsWith("https://")) return true;
|
|
828
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(cleaned)) {
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
var ALLOWED_NODE_ATTRS = {
|
|
834
|
+
doc: {},
|
|
835
|
+
paragraph: {},
|
|
836
|
+
heading: {
|
|
837
|
+
level: (v) => typeof v === "number" && Number.isInteger(v) && v >= 1 && v <= 6
|
|
838
|
+
},
|
|
839
|
+
bulletList: {},
|
|
840
|
+
orderedList: {
|
|
841
|
+
order: (v) => typeof v === "number" && Number.isInteger(v),
|
|
842
|
+
start: (v) => typeof v === "number" && Number.isInteger(v)
|
|
843
|
+
},
|
|
844
|
+
listItem: {},
|
|
845
|
+
blockquote: {},
|
|
846
|
+
codeBlock: {
|
|
847
|
+
language: (v) => typeof v === "string" && v.length <= 100,
|
|
848
|
+
params: (v) => typeof v === "string" && v.length <= 100
|
|
849
|
+
},
|
|
850
|
+
hardBreak: {},
|
|
851
|
+
horizontalRule: {},
|
|
852
|
+
text: {},
|
|
853
|
+
image: {
|
|
854
|
+
src: (v) => typeof v === "string" && isValidImageSrc(v),
|
|
855
|
+
alt: (v) => typeof v === "string" && v.length <= 2e3,
|
|
856
|
+
title: (v) => typeof v === "string" && v.length <= 2e3,
|
|
857
|
+
width: (v) => typeof v === "number" && v > 0,
|
|
858
|
+
height: (v) => typeof v === "number" && v > 0
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
var ALLOWED_MARK_ATTRS = {
|
|
862
|
+
bold: {},
|
|
863
|
+
italic: {},
|
|
864
|
+
underline: {},
|
|
865
|
+
strike: {},
|
|
866
|
+
code: {},
|
|
867
|
+
link: {
|
|
868
|
+
href: (v) => typeof v === "string" && isValidLinkHref(v),
|
|
869
|
+
title: (v) => typeof v === "string" && v.length <= 2e3,
|
|
870
|
+
target: (v) => typeof v === "string" && ["_blank", "_self", "_parent", "_top"].includes(v),
|
|
871
|
+
rel: (v) => typeof v === "string" && v.length <= 100
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
function validateProseMirrorDoc(doc, constraints) {
|
|
875
|
+
const errors = [];
|
|
876
|
+
const maxDepth = constraints?.maxDepth ?? 50;
|
|
877
|
+
const maxNodes = constraints?.maxNodes ?? 1e4;
|
|
878
|
+
const allowedNodes = new Set(constraints?.nodes ?? DEFAULT_ALLOWED_NODES);
|
|
879
|
+
const allowedMarks = new Set(constraints?.marks ?? DEFAULT_ALLOWED_MARKS);
|
|
880
|
+
if (doc === null || typeof doc !== "object" || Array.isArray(doc)) {
|
|
881
|
+
return {
|
|
882
|
+
valid: false,
|
|
883
|
+
errors: [{ path: "doc", message: "Document root must be a JSON object", keyword: "type" }]
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
const rootObj = doc;
|
|
887
|
+
if (rootObj.type !== "doc") {
|
|
888
|
+
return {
|
|
889
|
+
valid: false,
|
|
890
|
+
errors: [
|
|
891
|
+
{
|
|
892
|
+
path: "doc",
|
|
893
|
+
message: `Document root type must be 'doc', got '${String(rootObj.type)}'`,
|
|
894
|
+
keyword: "invalid_root_type"
|
|
895
|
+
}
|
|
896
|
+
]
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
let nodeCount = 0;
|
|
900
|
+
const seenObjects = /* @__PURE__ */ new WeakSet();
|
|
901
|
+
function validateNode(node, path, depth) {
|
|
902
|
+
if (depth > maxDepth) {
|
|
903
|
+
errors.push({
|
|
904
|
+
path,
|
|
905
|
+
message: `Maximum nesting depth of ${maxDepth} exceeded`,
|
|
906
|
+
keyword: "maxDepth"
|
|
907
|
+
});
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
nodeCount++;
|
|
911
|
+
if (nodeCount > maxNodes) {
|
|
912
|
+
errors.push({
|
|
913
|
+
path,
|
|
914
|
+
message: `Maximum node budget of ${maxNodes} exceeded`,
|
|
915
|
+
keyword: "maxNodes"
|
|
916
|
+
});
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) {
|
|
920
|
+
errors.push({
|
|
921
|
+
path,
|
|
922
|
+
message: "Node must be a JSON object",
|
|
923
|
+
keyword: "type"
|
|
924
|
+
});
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
if (seenObjects.has(node)) {
|
|
928
|
+
errors.push({
|
|
929
|
+
path,
|
|
930
|
+
message: "Cyclic reference detected in document",
|
|
931
|
+
keyword: "cycle"
|
|
932
|
+
});
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
seenObjects.add(node);
|
|
936
|
+
const obj = node;
|
|
937
|
+
for (const key of Object.keys(obj)) {
|
|
938
|
+
if (FORBIDDEN_PROPERTY_KEYS.has(key)) {
|
|
939
|
+
errors.push({
|
|
940
|
+
path: `${path}/${key}`,
|
|
941
|
+
message: `Forbidden property '${key}' detected`,
|
|
942
|
+
keyword: "security"
|
|
943
|
+
});
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
const nodeType = typeof obj.type === "string" ? obj.type : "";
|
|
948
|
+
if (!nodeType || !allowedNodes.has(nodeType)) {
|
|
949
|
+
errors.push({
|
|
950
|
+
path,
|
|
951
|
+
message: `Unknown or disallowed node type: '${nodeType || String(obj.type)}'`,
|
|
952
|
+
keyword: "disallowed_node_type"
|
|
953
|
+
});
|
|
954
|
+
return false;
|
|
955
|
+
}
|
|
956
|
+
const allowedAttrsForType = ALLOWED_NODE_ATTRS[nodeType];
|
|
957
|
+
if (obj.attrs !== void 0) {
|
|
958
|
+
if (obj.attrs === null || typeof obj.attrs !== "object" || Array.isArray(obj.attrs)) {
|
|
959
|
+
errors.push({
|
|
960
|
+
path: `${path}/attrs`,
|
|
961
|
+
message: "Node attrs must be an object",
|
|
962
|
+
keyword: "type"
|
|
963
|
+
});
|
|
964
|
+
} else {
|
|
965
|
+
const attrsObj = obj.attrs;
|
|
966
|
+
for (const [attrKey, attrVal] of Object.entries(attrsObj)) {
|
|
967
|
+
if (FORBIDDEN_PROPERTY_KEYS.has(attrKey)) {
|
|
968
|
+
errors.push({
|
|
969
|
+
path: `${path}/attrs/${attrKey}`,
|
|
970
|
+
message: `Forbidden attribute key '${attrKey}'`,
|
|
971
|
+
keyword: "security"
|
|
972
|
+
});
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const validator = allowedAttrsForType?.[attrKey];
|
|
976
|
+
if (!validator) {
|
|
977
|
+
errors.push({
|
|
978
|
+
path: `${path}/attrs/${attrKey}`,
|
|
979
|
+
message: `Unknown attribute '${attrKey}' for node type '${nodeType}'`,
|
|
980
|
+
keyword: "unknown_attribute"
|
|
981
|
+
});
|
|
982
|
+
} else if (!validator(attrVal)) {
|
|
983
|
+
errors.push({
|
|
984
|
+
path: `${path}/attrs/${attrKey}`,
|
|
985
|
+
message: `Invalid value for attribute '${attrKey}' on node type '${nodeType}'`,
|
|
986
|
+
keyword: "invalid_attribute_value"
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (nodeType === "text") {
|
|
993
|
+
if (typeof obj.text !== "string") {
|
|
994
|
+
errors.push({
|
|
995
|
+
path: `${path}/text`,
|
|
996
|
+
message: "Text node must have a string text property",
|
|
997
|
+
keyword: "missing_text"
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
if (obj.content !== void 0) {
|
|
1001
|
+
errors.push({
|
|
1002
|
+
path: `${path}/content`,
|
|
1003
|
+
message: "Text node must not have content",
|
|
1004
|
+
keyword: "invalid_content"
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (obj.marks !== void 0) {
|
|
1009
|
+
if (!Array.isArray(obj.marks)) {
|
|
1010
|
+
errors.push({
|
|
1011
|
+
path: `${path}/marks`,
|
|
1012
|
+
message: "Marks must be an array",
|
|
1013
|
+
keyword: "type"
|
|
1014
|
+
});
|
|
1015
|
+
} else {
|
|
1016
|
+
obj.marks.forEach((mark, markIdx) => {
|
|
1017
|
+
const markPath = `${path}/marks/${markIdx}`;
|
|
1018
|
+
if (mark === null || typeof mark !== "object" || Array.isArray(mark)) {
|
|
1019
|
+
errors.push({
|
|
1020
|
+
path: markPath,
|
|
1021
|
+
message: "Mark must be an object",
|
|
1022
|
+
keyword: "type"
|
|
1023
|
+
});
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
const markObj = mark;
|
|
1027
|
+
const markType = typeof markObj.type === "string" ? markObj.type : "";
|
|
1028
|
+
if (!markType || !allowedMarks.has(markType)) {
|
|
1029
|
+
errors.push({
|
|
1030
|
+
path: markPath,
|
|
1031
|
+
message: `Unknown or disallowed mark type: '${markType || String(markObj.type)}'`,
|
|
1032
|
+
keyword: "disallowed_mark_type"
|
|
1033
|
+
});
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
const allowedMarkAttrs = ALLOWED_MARK_ATTRS[markType];
|
|
1037
|
+
if (markObj.attrs !== void 0) {
|
|
1038
|
+
if (markObj.attrs === null || typeof markObj.attrs !== "object" || Array.isArray(markObj.attrs)) {
|
|
1039
|
+
errors.push({
|
|
1040
|
+
path: `${markPath}/attrs`,
|
|
1041
|
+
message: "Mark attrs must be an object",
|
|
1042
|
+
keyword: "type"
|
|
1043
|
+
});
|
|
1044
|
+
} else {
|
|
1045
|
+
const attrsObj = markObj.attrs;
|
|
1046
|
+
for (const [attrKey, attrVal] of Object.entries(attrsObj)) {
|
|
1047
|
+
if (FORBIDDEN_PROPERTY_KEYS.has(attrKey)) {
|
|
1048
|
+
errors.push({
|
|
1049
|
+
path: `${markPath}/attrs/${attrKey}`,
|
|
1050
|
+
message: `Forbidden attribute key '${attrKey}' in mark`,
|
|
1051
|
+
keyword: "security"
|
|
1052
|
+
});
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
const validator = allowedMarkAttrs?.[attrKey];
|
|
1056
|
+
if (!validator) {
|
|
1057
|
+
errors.push({
|
|
1058
|
+
path: `${markPath}/attrs/${attrKey}`,
|
|
1059
|
+
message: `Unknown attribute '${attrKey}' for mark '${markType}'`,
|
|
1060
|
+
keyword: "unknown_attribute"
|
|
1061
|
+
});
|
|
1062
|
+
} else if (!validator(attrVal)) {
|
|
1063
|
+
errors.push({
|
|
1064
|
+
path: `${markPath}/attrs/${attrKey}`,
|
|
1065
|
+
message: `Invalid value for attribute '${attrKey}' in mark '${markType}'`,
|
|
1066
|
+
keyword: "invalid_attribute_value"
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (obj.content !== void 0) {
|
|
1076
|
+
if (!Array.isArray(obj.content)) {
|
|
1077
|
+
errors.push({
|
|
1078
|
+
path: `${path}/content`,
|
|
1079
|
+
message: "Content must be an array of nodes",
|
|
1080
|
+
keyword: "type"
|
|
1081
|
+
});
|
|
1082
|
+
} else {
|
|
1083
|
+
for (let i = 0; i < obj.content.length; i++) {
|
|
1084
|
+
const childPath = `${path}/content/${i}`;
|
|
1085
|
+
const ok = validateNode(obj.content[i], childPath, depth + 1);
|
|
1086
|
+
if (!ok && (nodeCount > maxNodes || depth + 1 > maxDepth)) {
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
return errors.length === 0;
|
|
1093
|
+
}
|
|
1094
|
+
validateNode(doc, "doc", 1);
|
|
1095
|
+
if (errors.length === 0) {
|
|
1096
|
+
return { valid: true };
|
|
1097
|
+
}
|
|
1098
|
+
return { valid: false, errors };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// src/types/richtext.ts
|
|
1102
|
+
var richtextType = {
|
|
1103
|
+
id: "richtext",
|
|
1104
|
+
aliases: ["richText"],
|
|
1105
|
+
defaultValue: {
|
|
1106
|
+
type: "doc",
|
|
1107
|
+
content: []
|
|
1108
|
+
},
|
|
1109
|
+
renderHint: { tag: "div" },
|
|
1110
|
+
constraintsSchema: {
|
|
1111
|
+
type: "object",
|
|
1112
|
+
properties: {
|
|
1113
|
+
nodes: {
|
|
1114
|
+
type: "array",
|
|
1115
|
+
items: { type: "string" }
|
|
1116
|
+
},
|
|
1117
|
+
marks: {
|
|
1118
|
+
type: "array",
|
|
1119
|
+
items: { type: "string" }
|
|
1120
|
+
},
|
|
1121
|
+
maxDepth: { type: "integer", minimum: 1 },
|
|
1122
|
+
maxNodes: { type: "integer", minimum: 1 }
|
|
1123
|
+
},
|
|
1124
|
+
additionalProperties: false
|
|
1125
|
+
},
|
|
1126
|
+
validate(value, constraints) {
|
|
1127
|
+
return validateProseMirrorDoc(value, constraints);
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// src/types/select.ts
|
|
1132
|
+
var selectType = {
|
|
1133
|
+
id: "select",
|
|
1134
|
+
defaultValue: "",
|
|
1135
|
+
constraintsSchema: {
|
|
1136
|
+
type: "object",
|
|
1137
|
+
properties: {
|
|
1138
|
+
options: {
|
|
1139
|
+
type: "array",
|
|
1140
|
+
items: {
|
|
1141
|
+
anyOf: [
|
|
1142
|
+
{ type: "string" },
|
|
1143
|
+
{
|
|
1144
|
+
type: "object",
|
|
1145
|
+
properties: {
|
|
1146
|
+
label: { type: "string" },
|
|
1147
|
+
value: { type: "string" }
|
|
1148
|
+
},
|
|
1149
|
+
required: ["label", "value"],
|
|
1150
|
+
additionalProperties: false
|
|
1151
|
+
}
|
|
1152
|
+
]
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
additionalProperties: false
|
|
1157
|
+
},
|
|
1158
|
+
validate(value, constraints) {
|
|
1159
|
+
const errors = [];
|
|
1160
|
+
if (typeof value !== "string") {
|
|
1161
|
+
return {
|
|
1162
|
+
valid: false,
|
|
1163
|
+
errors: [
|
|
1164
|
+
{
|
|
1165
|
+
path: "",
|
|
1166
|
+
message: `Expected string, got ${typeof value}`,
|
|
1167
|
+
keyword: "type"
|
|
1168
|
+
}
|
|
1169
|
+
]
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
const c = constraints || {};
|
|
1173
|
+
if (Array.isArray(c.options) && c.options.length > 0) {
|
|
1174
|
+
const allowedValues = c.options.map(
|
|
1175
|
+
(opt) => typeof opt === "string" ? opt : opt && typeof opt === "object" ? opt.value : ""
|
|
1176
|
+
);
|
|
1177
|
+
if (value !== "" && !allowedValues.includes(value)) {
|
|
1178
|
+
errors.push({
|
|
1179
|
+
path: "",
|
|
1180
|
+
message: `Value '${value}' is not in allowed options: [${allowedValues.map((v) => JSON.stringify(v)).join(", ")}]`,
|
|
1181
|
+
keyword: "enum"
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (errors.length === 0) {
|
|
1186
|
+
return { valid: true };
|
|
1187
|
+
}
|
|
1188
|
+
return { valid: false, errors };
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
// src/types/streamVideo.ts
|
|
1193
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set(["uid", "durationMs", "thumbnail", "ready"]);
|
|
1194
|
+
var FORBIDDEN_KEYS3 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1195
|
+
var STREAM_UID_REGEX = /^[0-9a-f]{32}$/;
|
|
1196
|
+
var streamVideoType = {
|
|
1197
|
+
id: "media.streamVideo",
|
|
1198
|
+
aliases: ["streamVideo"],
|
|
1199
|
+
defaultValue: null,
|
|
1200
|
+
renderHint: { tag: "video" },
|
|
1201
|
+
constraintsSchema: {
|
|
1202
|
+
type: "object",
|
|
1203
|
+
properties: {
|
|
1204
|
+
maxDurationSeconds: { type: "integer", minimum: 0 },
|
|
1205
|
+
allowedThumbnailHosts: {
|
|
1206
|
+
type: "array",
|
|
1207
|
+
items: { type: "string" }
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
additionalProperties: false
|
|
1211
|
+
},
|
|
1212
|
+
validate(value, constraints) {
|
|
1213
|
+
const errors = [];
|
|
1214
|
+
const c = constraints || {};
|
|
1215
|
+
if (value === null || value === void 0 || value === "") {
|
|
1216
|
+
return { valid: true };
|
|
1217
|
+
}
|
|
1218
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
1219
|
+
return {
|
|
1220
|
+
valid: false,
|
|
1221
|
+
errors: [
|
|
1222
|
+
{
|
|
1223
|
+
path: "",
|
|
1224
|
+
message: `Expected streamVideo object or null, got ${typeof value}`,
|
|
1225
|
+
keyword: "type"
|
|
1226
|
+
}
|
|
1227
|
+
]
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
const obj = value;
|
|
1231
|
+
for (const key of Object.keys(obj)) {
|
|
1232
|
+
if (FORBIDDEN_KEYS3.has(key) || !ALLOWED_KEYS.has(key)) {
|
|
1233
|
+
errors.push({
|
|
1234
|
+
path: key,
|
|
1235
|
+
message: `Unknown or forbidden property '${key}' in streamVideo object`,
|
|
1236
|
+
keyword: "additional_properties"
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (typeof obj.uid !== "string" || !STREAM_UID_REGEX.test(obj.uid.trim())) {
|
|
1241
|
+
errors.push({
|
|
1242
|
+
path: "uid",
|
|
1243
|
+
message: "streamVideo uid is required and must be a 32-character hexadecimal string",
|
|
1244
|
+
keyword: "pattern"
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
if (obj.durationMs !== void 0 && obj.durationMs !== null) {
|
|
1248
|
+
if (typeof obj.durationMs !== "number" || Number.isNaN(obj.durationMs) || obj.durationMs < 0) {
|
|
1249
|
+
errors.push({
|
|
1250
|
+
path: "durationMs",
|
|
1251
|
+
message: "durationMs must be a non-negative number",
|
|
1252
|
+
keyword: "type"
|
|
1253
|
+
});
|
|
1254
|
+
} else if (c.maxDurationSeconds !== void 0 && obj.durationMs > c.maxDurationSeconds * 1e3) {
|
|
1255
|
+
errors.push({
|
|
1256
|
+
path: "durationMs",
|
|
1257
|
+
message: `Video duration (${Math.round(obj.durationMs / 1e3)}s) exceeds max duration of ${c.maxDurationSeconds}s`,
|
|
1258
|
+
keyword: "max_duration"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (obj.thumbnail !== void 0 && obj.thumbnail !== null && obj.thumbnail !== "") {
|
|
1263
|
+
if (typeof obj.thumbnail !== "string") {
|
|
1264
|
+
errors.push({
|
|
1265
|
+
path: "thumbnail",
|
|
1266
|
+
message: "thumbnail must be a string URL",
|
|
1267
|
+
keyword: "type"
|
|
1268
|
+
});
|
|
1269
|
+
} else if (!isSafeMediaSrc(obj.thumbnail, c.allowedThumbnailHosts)) {
|
|
1270
|
+
errors.push({
|
|
1271
|
+
path: "thumbnail",
|
|
1272
|
+
message: `Thumbnail '${obj.thumbnail}' is not a valid or safe media URL`,
|
|
1273
|
+
keyword: "thumbnail_invalid"
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (obj.ready !== void 0 && obj.ready !== null) {
|
|
1278
|
+
if (typeof obj.ready !== "boolean") {
|
|
1279
|
+
errors.push({
|
|
1280
|
+
path: "ready",
|
|
1281
|
+
message: "ready must be a boolean",
|
|
1282
|
+
keyword: "type"
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (errors.length === 0) {
|
|
1287
|
+
return { valid: true };
|
|
1288
|
+
}
|
|
1289
|
+
return { valid: false, errors };
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
// src/types/text.ts
|
|
1294
|
+
var textType = {
|
|
1295
|
+
id: "text",
|
|
1296
|
+
defaultValue: "",
|
|
1297
|
+
constraintsSchema: {
|
|
1298
|
+
type: "object",
|
|
1299
|
+
properties: {
|
|
1300
|
+
minLength: { type: "integer", minimum: 0 },
|
|
1301
|
+
maxLength: { type: "integer", minimum: 0 },
|
|
1302
|
+
pattern: { type: "string" }
|
|
1303
|
+
},
|
|
1304
|
+
additionalProperties: false
|
|
1305
|
+
},
|
|
1306
|
+
validate(value, constraints) {
|
|
1307
|
+
const errors = [];
|
|
1308
|
+
if (typeof value !== "string") {
|
|
1309
|
+
return {
|
|
1310
|
+
valid: false,
|
|
1311
|
+
errors: [
|
|
1312
|
+
{
|
|
1313
|
+
path: "",
|
|
1314
|
+
message: `Expected string, got ${typeof value}`,
|
|
1315
|
+
keyword: "type"
|
|
1316
|
+
}
|
|
1317
|
+
]
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
const c = constraints || {};
|
|
1321
|
+
if (typeof c.minLength === "number" && value.length < c.minLength) {
|
|
1322
|
+
errors.push({
|
|
1323
|
+
path: "",
|
|
1324
|
+
message: `String length ${value.length} is less than minimum ${c.minLength}`,
|
|
1325
|
+
keyword: "minLength"
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
if (typeof c.maxLength === "number" && value.length > c.maxLength) {
|
|
1329
|
+
errors.push({
|
|
1330
|
+
path: "",
|
|
1331
|
+
message: `String length ${value.length} exceeds maximum ${c.maxLength}`,
|
|
1332
|
+
keyword: "maxLength"
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
if (typeof c.pattern === "string") {
|
|
1336
|
+
try {
|
|
1337
|
+
const regex = new RegExp(c.pattern);
|
|
1338
|
+
if (!regex.test(value)) {
|
|
1339
|
+
errors.push({
|
|
1340
|
+
path: "",
|
|
1341
|
+
message: `String does not match required pattern ${c.pattern}`,
|
|
1342
|
+
keyword: "pattern"
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
} catch (_e) {
|
|
1346
|
+
errors.push({
|
|
1347
|
+
path: "",
|
|
1348
|
+
message: `Invalid regex pattern in constraints: ${c.pattern}`,
|
|
1349
|
+
keyword: "pattern_invalid"
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (errors.length === 0) {
|
|
1354
|
+
return { valid: true };
|
|
1355
|
+
}
|
|
1356
|
+
return { valid: false, errors };
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
// src/types/toggle.ts
|
|
1361
|
+
var toggleType = {
|
|
1362
|
+
id: "toggle",
|
|
1363
|
+
defaultValue: false,
|
|
1364
|
+
constraintsSchema: {
|
|
1365
|
+
type: "object",
|
|
1366
|
+
properties: {},
|
|
1367
|
+
additionalProperties: false
|
|
1368
|
+
},
|
|
1369
|
+
validate(value) {
|
|
1370
|
+
if (typeof value !== "boolean") {
|
|
1371
|
+
return {
|
|
1372
|
+
valid: false,
|
|
1373
|
+
errors: [
|
|
1374
|
+
{
|
|
1375
|
+
path: "",
|
|
1376
|
+
message: `Expected boolean, got ${typeof value}`,
|
|
1377
|
+
keyword: "type"
|
|
1378
|
+
}
|
|
1379
|
+
]
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
return { valid: true };
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
// src/types/video.ts
|
|
1387
|
+
var videoType = {
|
|
1388
|
+
id: "video",
|
|
1389
|
+
aliases: ["vid"],
|
|
1390
|
+
defaultValue: null,
|
|
1391
|
+
renderHint: { tag: "video" },
|
|
1392
|
+
constraintsSchema: {
|
|
1393
|
+
type: "object",
|
|
1394
|
+
properties: {
|
|
1395
|
+
maxSizeBytes: { type: "integer", minimum: 0 },
|
|
1396
|
+
allowedMimeTypes: {
|
|
1397
|
+
type: "array",
|
|
1398
|
+
items: { type: "string" }
|
|
1399
|
+
}
|
|
1400
|
+
},
|
|
1401
|
+
additionalProperties: false
|
|
1402
|
+
},
|
|
1403
|
+
validate(value) {
|
|
1404
|
+
const errors = [];
|
|
1405
|
+
if (value === null || value === void 0 || value === "") {
|
|
1406
|
+
return { valid: true };
|
|
1407
|
+
}
|
|
1408
|
+
if (typeof value === "string") {
|
|
1409
|
+
return { valid: true };
|
|
1410
|
+
}
|
|
1411
|
+
if (typeof value === "object" && value !== null) {
|
|
1412
|
+
const obj = value;
|
|
1413
|
+
if (obj.kind === "asset" && typeof obj.id === "string" && obj.id.trim()) {
|
|
1414
|
+
return { valid: true };
|
|
1415
|
+
}
|
|
1416
|
+
if (obj.kind === "url" && typeof obj.src === "string" && obj.src.trim()) {
|
|
1417
|
+
return { valid: true };
|
|
1418
|
+
}
|
|
1419
|
+
if (typeof obj.id === "string" || typeof obj.url === "string") {
|
|
1420
|
+
return { valid: true };
|
|
1421
|
+
}
|
|
1422
|
+
errors.push({
|
|
1423
|
+
path: "",
|
|
1424
|
+
message: "Video object must contain an id, url, or valid kind (asset/url)",
|
|
1425
|
+
keyword: "video_structure"
|
|
1426
|
+
});
|
|
1427
|
+
return { valid: false, errors };
|
|
1428
|
+
}
|
|
1429
|
+
return {
|
|
1430
|
+
valid: false,
|
|
1431
|
+
errors: [
|
|
1432
|
+
{
|
|
1433
|
+
path: "",
|
|
1434
|
+
message: `Expected video reference (UUID string, URL, or asset object), got ${typeof value}`,
|
|
1435
|
+
keyword: "type"
|
|
1436
|
+
}
|
|
1437
|
+
]
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1442
|
+
// src/types/videoList.ts
|
|
1443
|
+
var ALLOWED_ITEM_KEYS = /* @__PURE__ */ new Set(["src", "poster", "caption", "href"]);
|
|
1444
|
+
var FORBIDDEN_KEYS4 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1445
|
+
var videoListType = {
|
|
1446
|
+
id: "media.videoList",
|
|
1447
|
+
aliases: ["videoList"],
|
|
1448
|
+
defaultValue: [],
|
|
1449
|
+
constraintsSchema: {
|
|
1450
|
+
type: "object",
|
|
1451
|
+
properties: {
|
|
1452
|
+
maxItems: { type: "integer", minimum: 0, default: 24 },
|
|
1453
|
+
allowedSrcHosts: {
|
|
1454
|
+
type: "array",
|
|
1455
|
+
items: { type: "string" }
|
|
1456
|
+
},
|
|
1457
|
+
allowedHrefHosts: {
|
|
1458
|
+
type: "array",
|
|
1459
|
+
items: { type: "string" }
|
|
1460
|
+
},
|
|
1461
|
+
maxCaptionLength: { type: "integer", minimum: 0, default: 300 }
|
|
1462
|
+
},
|
|
1463
|
+
additionalProperties: false
|
|
1464
|
+
},
|
|
1465
|
+
validate(value, constraints) {
|
|
1466
|
+
const errors = [];
|
|
1467
|
+
const c = constraints || {};
|
|
1468
|
+
const maxItems = typeof c.maxItems === "number" ? c.maxItems : 24;
|
|
1469
|
+
const maxCaptionLength = typeof c.maxCaptionLength === "number" ? c.maxCaptionLength : 300;
|
|
1470
|
+
if (value === null || value === void 0) {
|
|
1471
|
+
return { valid: true };
|
|
1472
|
+
}
|
|
1473
|
+
if (!Array.isArray(value)) {
|
|
1474
|
+
return {
|
|
1475
|
+
valid: false,
|
|
1476
|
+
errors: [
|
|
1477
|
+
{
|
|
1478
|
+
path: "",
|
|
1479
|
+
message: `Expected array for videoList, got ${typeof value}`,
|
|
1480
|
+
keyword: "type"
|
|
1481
|
+
}
|
|
1482
|
+
]
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
if (value.length > maxItems) {
|
|
1486
|
+
errors.push({
|
|
1487
|
+
path: "",
|
|
1488
|
+
message: `Video list exceeds maximum item count of ${maxItems} (got ${value.length})`,
|
|
1489
|
+
keyword: "max_items"
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
for (let i = 0; i < value.length; i++) {
|
|
1493
|
+
const item = value[i];
|
|
1494
|
+
const itemPath = `videos[${i}]`;
|
|
1495
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
1496
|
+
errors.push({
|
|
1497
|
+
path: itemPath,
|
|
1498
|
+
message: `Expected video item object, got ${item === null ? "null" : Array.isArray(item) ? "array" : typeof item}`,
|
|
1499
|
+
keyword: "type"
|
|
1500
|
+
});
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
const record = item;
|
|
1504
|
+
for (const k of Object.keys(record)) {
|
|
1505
|
+
if (FORBIDDEN_KEYS4.has(k) || !ALLOWED_ITEM_KEYS.has(k)) {
|
|
1506
|
+
errors.push({
|
|
1507
|
+
path: `${itemPath}.${k}`,
|
|
1508
|
+
message: `Unknown or forbidden property '${k}' in video item`,
|
|
1509
|
+
keyword: "additional_properties"
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (typeof record.src === "string") {
|
|
1514
|
+
if (!record.src.trim()) {
|
|
1515
|
+
errors.push({
|
|
1516
|
+
path: `${itemPath}.src`,
|
|
1517
|
+
message: "Video src is required and must be a non-empty string",
|
|
1518
|
+
keyword: "required"
|
|
1519
|
+
});
|
|
1520
|
+
} else if (!isSafeMediaSrc(record.src, c.allowedSrcHosts)) {
|
|
1521
|
+
errors.push({
|
|
1522
|
+
path: `${itemPath}.src`,
|
|
1523
|
+
message: `Video src '${record.src}' is not a valid or safe media URL/reference`,
|
|
1524
|
+
keyword: "video_src_invalid"
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
} else if (typeof record.src === "object" && record.src !== null) {
|
|
1528
|
+
const srcObj = record.src;
|
|
1529
|
+
if (srcObj.kind === "asset") {
|
|
1530
|
+
if (typeof srcObj.id !== "string" || !srcObj.id.trim()) {
|
|
1531
|
+
errors.push({
|
|
1532
|
+
path: `${itemPath}.src`,
|
|
1533
|
+
message: "Video asset reference must include a valid id",
|
|
1534
|
+
keyword: "required"
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
} else if (srcObj.kind === "url") {
|
|
1538
|
+
if (typeof srcObj.src !== "string" || !srcObj.src.trim()) {
|
|
1539
|
+
errors.push({
|
|
1540
|
+
path: `${itemPath}.src`,
|
|
1541
|
+
message: "Video url reference must include a valid src",
|
|
1542
|
+
keyword: "required"
|
|
1543
|
+
});
|
|
1544
|
+
} else if (!isSafeMediaSrc(srcObj.src, c.allowedSrcHosts)) {
|
|
1545
|
+
errors.push({
|
|
1546
|
+
path: `${itemPath}.src`,
|
|
1547
|
+
message: `Video src '${srcObj.src}' is not a valid or safe media URL/reference`,
|
|
1548
|
+
keyword: "video_src_invalid"
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
} else {
|
|
1552
|
+
errors.push({
|
|
1553
|
+
path: `${itemPath}.src`,
|
|
1554
|
+
message: 'Video src object must have kind "asset" or "url"',
|
|
1555
|
+
keyword: "type"
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
} else {
|
|
1559
|
+
errors.push({
|
|
1560
|
+
path: `${itemPath}.src`,
|
|
1561
|
+
message: "Video src is required and must be a string or media reference object",
|
|
1562
|
+
keyword: "required"
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
if (record.poster !== void 0 && record.poster !== null && record.poster !== "") {
|
|
1566
|
+
if (typeof record.poster === "string") {
|
|
1567
|
+
if (!isSafeMediaSrc(record.poster, c.allowedSrcHosts)) {
|
|
1568
|
+
errors.push({
|
|
1569
|
+
path: `${itemPath}.poster`,
|
|
1570
|
+
message: `Video poster '${record.poster}' is not a valid or safe media URL/reference`,
|
|
1571
|
+
keyword: "video_poster_invalid"
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
} else if (typeof record.poster === "object" && record.poster !== null) {
|
|
1575
|
+
const posterObj = record.poster;
|
|
1576
|
+
if (posterObj.kind === "asset") {
|
|
1577
|
+
if (typeof posterObj.id !== "string" || !posterObj.id.trim()) {
|
|
1578
|
+
errors.push({
|
|
1579
|
+
path: `${itemPath}.poster`,
|
|
1580
|
+
message: "Poster asset reference must include a valid id",
|
|
1581
|
+
keyword: "required"
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
} else if (posterObj.kind === "url") {
|
|
1585
|
+
if (typeof posterObj.src !== "string" || !posterObj.src.trim()) {
|
|
1586
|
+
errors.push({
|
|
1587
|
+
path: `${itemPath}.poster`,
|
|
1588
|
+
message: "Poster url reference must include a valid src",
|
|
1589
|
+
keyword: "required"
|
|
1590
|
+
});
|
|
1591
|
+
} else if (!isSafeMediaSrc(posterObj.src, c.allowedSrcHosts)) {
|
|
1592
|
+
errors.push({
|
|
1593
|
+
path: `${itemPath}.poster`,
|
|
1594
|
+
message: `Video poster '${posterObj.src}' is not a valid or safe media URL/reference`,
|
|
1595
|
+
keyword: "video_poster_invalid"
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
} else {
|
|
1599
|
+
errors.push({
|
|
1600
|
+
path: `${itemPath}.poster`,
|
|
1601
|
+
message: 'Video poster object must have kind "asset" or "url"',
|
|
1602
|
+
keyword: "type"
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
} else {
|
|
1606
|
+
errors.push({
|
|
1607
|
+
path: `${itemPath}.poster`,
|
|
1608
|
+
message: "Video poster must be a string or media reference object",
|
|
1609
|
+
keyword: "type"
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
if (record.href !== void 0 && record.href !== null && record.href !== "") {
|
|
1614
|
+
if (typeof record.href !== "string") {
|
|
1615
|
+
errors.push({
|
|
1616
|
+
path: `${itemPath}.href`,
|
|
1617
|
+
message: "Video href must be a string",
|
|
1618
|
+
keyword: "type"
|
|
1619
|
+
});
|
|
1620
|
+
} else if (!isSafeActionHref(record.href, c.allowedHrefHosts)) {
|
|
1621
|
+
errors.push({
|
|
1622
|
+
path: `${itemPath}.href`,
|
|
1623
|
+
message: `Video href '${record.href}' is not allowed or invalid`,
|
|
1624
|
+
keyword: "video_href_not_allowed"
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
if (record.caption !== void 0 && record.caption !== null) {
|
|
1629
|
+
if (typeof record.caption !== "string") {
|
|
1630
|
+
errors.push({
|
|
1631
|
+
path: `${itemPath}.caption`,
|
|
1632
|
+
message: "Video caption must be a string",
|
|
1633
|
+
keyword: "type"
|
|
1634
|
+
});
|
|
1635
|
+
} else if (record.caption.length > maxCaptionLength) {
|
|
1636
|
+
errors.push({
|
|
1637
|
+
path: `${itemPath}.caption`,
|
|
1638
|
+
message: `Video caption length (${record.caption.length}) exceeds maximum ${maxCaptionLength}`,
|
|
1639
|
+
keyword: "max_length"
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (errors.length === 0) {
|
|
1645
|
+
return { valid: true };
|
|
1646
|
+
}
|
|
1647
|
+
return { valid: false, errors };
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
// src/index.ts
|
|
1652
|
+
function registerBaseTypes() {
|
|
1653
|
+
registerFieldType(textType);
|
|
1654
|
+
registerFieldType(numberType);
|
|
1655
|
+
registerFieldType(toggleType);
|
|
1656
|
+
registerFieldType(selectType);
|
|
1657
|
+
registerFieldType(imageType);
|
|
1658
|
+
registerFieldType(richtextType);
|
|
1659
|
+
registerFieldType(videoType);
|
|
1660
|
+
registerFieldType(videoListType);
|
|
1661
|
+
registerFieldType(geoAreaType);
|
|
1662
|
+
registerFieldType(addressType);
|
|
1663
|
+
registerFieldType(addressListType);
|
|
1664
|
+
registerFieldType(streamVideoType);
|
|
1665
|
+
}
|
|
1666
|
+
registerBaseTypes();
|
|
1667
|
+
|
|
1668
|
+
export { DEFAULT_ALLOWED_MARKS, DEFAULT_ALLOWED_NODES, addressListType, addressType, fieldTypeAliases, geoAreaType, getFieldType, hasFieldType, imageType, isSafeActionHref, isSafeMediaSrc, isSafeRelativeOrAnchor, isValidLinkHref, listFieldTypeIds, listFieldTypes, normalizeFieldValue, normalizeGeoArea, numberType, registerBaseTypes, registerFieldType, resetRegistry, resolveTypeAlias, richtextType, selectType, streamVideoType, stripUrlControlChars, textType, toggleType, unknownTypeIds, validateFieldValue, validateProseMirrorDoc, videoListType, videoType };
|
|
1669
|
+
//# sourceMappingURL=index.js.map
|
|
1670
|
+
//# sourceMappingURL=index.js.map
|