@safe-ugc-ui/validator 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Safe UGC UI Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,385 @@
1
+ import { UGCCard } from '@safe-ugc-ui/types';
2
+
3
+ /**
4
+ * @safe-ugc-ui/validator — Validation Result Types
5
+ *
6
+ * Provides the error and result types used throughout the validation pipeline.
7
+ *
8
+ * Design decisions:
9
+ * - Errors include a `path` for precise location in the card tree.
10
+ * - Errors include a `code` for programmatic handling.
11
+ * - `merge()` combines multiple results, accumulating all errors.
12
+ * - A valid result is simply `{ valid: true, errors: [] }`.
13
+ */
14
+ /**
15
+ * Machine-readable error codes for every validation failure type.
16
+ */
17
+ type ValidationErrorCode = 'INVALID_JSON' | 'MISSING_FIELD' | 'INVALID_TYPE' | 'INVALID_VALUE' | 'UNKNOWN_NODE_TYPE' | 'SCHEMA_ERROR' | 'EXPR_NOT_ALLOWED' | 'REF_NOT_ALLOWED' | 'DYNAMIC_NOT_ALLOWED' | 'FORBIDDEN_STYLE_PROPERTY' | 'STYLE_VALUE_OUT_OF_RANGE' | 'FORBIDDEN_CSS_FUNCTION' | 'INVALID_COLOR' | 'INVALID_LENGTH' | 'FORBIDDEN_OVERFLOW_VALUE' | 'TRANSFORM_SKEW_FORBIDDEN' | 'EXTERNAL_URL' | 'POSITION_FIXED_FORBIDDEN' | 'POSITION_STICKY_FORBIDDEN' | 'POSITION_ABSOLUTE_NOT_IN_STACK' | 'ASSET_PATH_TRAVERSAL' | 'INVALID_ASSET_PATH' | 'PROTOTYPE_POLLUTION' | 'CARD_SIZE_EXCEEDED' | 'TEXT_CONTENT_SIZE_EXCEEDED' | 'STYLE_SIZE_EXCEEDED' | 'NODE_COUNT_EXCEEDED' | 'LOOP_ITERATIONS_EXCEEDED' | 'NESTED_LOOPS_EXCEEDED' | 'OVERFLOW_AUTO_COUNT_EXCEEDED' | 'OVERFLOW_AUTO_NESTED' | 'STACK_NESTING_EXCEEDED' | 'EXPR_TOO_LONG' | 'REF_TOO_LONG' | 'EXPR_TOO_MANY_TOKENS' | 'EXPR_NESTING_TOO_DEEP' | 'EXPR_CONDITION_NESTING_TOO_DEEP' | 'EXPR_REF_DEPTH_EXCEEDED' | 'EXPR_ARRAY_INDEX_EXCEEDED' | 'EXPR_STRING_LITERAL_TOO_LONG' | 'EXPR_FORBIDDEN_TOKEN' | 'EXPR_FUNCTION_CALL' | 'EXPR_INVALID_TOKEN' | 'LOOP_SOURCE_NOT_ARRAY' | 'LOOP_SOURCE_MISSING' | 'STYLE_CIRCULAR_REF' | 'STYLE_REF_NOT_FOUND' | 'INVALID_STYLE_REF' | 'INVALID_STYLE_NAME';
18
+ /**
19
+ * A single validation error with location and diagnostic info.
20
+ */
21
+ interface ValidationError {
22
+ /** Machine-readable error code. */
23
+ code: ValidationErrorCode;
24
+ /** Human-readable error message. */
25
+ message: string;
26
+ /**
27
+ * JSON-pointer-like path to the error location.
28
+ * e.g. `"views.StatusWindow.children[0].style.zIndex"`
29
+ */
30
+ path: string;
31
+ }
32
+ /**
33
+ * The result of running the validation pipeline.
34
+ *
35
+ * - `valid: true` → the card is safe to render.
36
+ * - `valid: false` → the card has errors; do NOT render.
37
+ */
38
+ interface ValidationResult {
39
+ valid: boolean;
40
+ errors: ValidationError[];
41
+ }
42
+ /**
43
+ * Create a single validation error.
44
+ */
45
+ declare function createError(code: ValidationErrorCode, message: string, path: string): ValidationError;
46
+ /**
47
+ * Create a valid result (no errors).
48
+ */
49
+ declare function validResult(): ValidationResult;
50
+ /**
51
+ * Create an invalid result from a list of errors.
52
+ */
53
+ declare function invalidResult(errors: ValidationError[]): ValidationResult;
54
+ /**
55
+ * Wrap errors into a ValidationResult (valid if no errors).
56
+ */
57
+ declare function toResult(errors: ValidationError[]): ValidationResult;
58
+ /**
59
+ * Merge multiple validation results into one.
60
+ * The merged result is valid only if ALL inputs are valid.
61
+ */
62
+ declare function merge(...results: ValidationResult[]): ValidationResult;
63
+
64
+ /**
65
+ * @safe-ugc-ui/validator — Tree Traversal
66
+ *
67
+ * Provides utilities for walking the UGC card tree, tracking:
68
+ * - path: JSON-pointer-like location string
69
+ * - depth: nesting level
70
+ * - parentType: the type of the parent node (for position/overflow rules)
71
+ * - loopDepth: nesting level of for-loops
72
+ * - overflowAutoAncestor: whether an ancestor has overflow:auto
73
+ * - stackDepth: nesting level of Stack components
74
+ *
75
+ * The traversal is generic: it calls a visitor function on each node,
76
+ * allowing different validators to collect different data.
77
+ */
78
+ /**
79
+ * Contextual information available at each node during traversal.
80
+ */
81
+ interface TraversalContext {
82
+ /** JSON-pointer-like path, e.g. "views.Main.children[0].children[1]" */
83
+ path: string;
84
+ /** Nesting depth (root node = 0). */
85
+ depth: number;
86
+ /** The `type` of the immediate parent node, or null for root-level nodes. */
87
+ parentType: string | null;
88
+ /** Current for-loop nesting depth (0 = no loop). */
89
+ loopDepth: number;
90
+ /** True if any ancestor has `overflow: auto` in its style. */
91
+ overflowAutoAncestor: boolean;
92
+ /** Current Stack nesting depth (0 = not inside a Stack). */
93
+ stackDepth: number;
94
+ }
95
+ /**
96
+ * Minimal shape expected for a node during traversal.
97
+ * We use a loose type because traversal runs after schema validation,
98
+ * but the node might have extra or unexpected fields.
99
+ */
100
+ interface TraversableNode {
101
+ type: string;
102
+ children?: TraversableNode[] | ForLoopLike;
103
+ style?: Record<string, unknown>;
104
+ condition?: unknown;
105
+ [key: string]: unknown;
106
+ }
107
+ interface ForLoopLike {
108
+ for: string;
109
+ in: string;
110
+ template: TraversableNode;
111
+ }
112
+ /**
113
+ * A visitor function called for every node in the tree.
114
+ * Return `false` to skip traversing into this node's children.
115
+ */
116
+ type NodeVisitor = (node: TraversableNode, context: TraversalContext) => void | false;
117
+ /**
118
+ * Recursively traverse a single node and its descendants.
119
+ */
120
+ declare function traverseNode(node: TraversableNode, context: TraversalContext, visitor: NodeVisitor): void;
121
+ /**
122
+ * Traverse all nodes in every view of a card.
123
+ *
124
+ * @param views - The `views` object from a UGCCard (mapping view names to root nodes).
125
+ * @param visitor - Called for every node in every view.
126
+ */
127
+ declare function traverseCard(views: Record<string, unknown>, visitor: NodeVisitor): void;
128
+
129
+ /**
130
+ * @safe-ugc-ui/validator — Schema (Structural) Validation
131
+ *
132
+ * Verifies the top-level structure of a UGC card:
133
+ * - Required fields: meta (name, version), views (at least one)
134
+ * - Zod schema parse for the full card structure
135
+ * - Maps Zod parse errors to ValidationError format
136
+ *
137
+ * This is the first step in the validation pipeline (after size check).
138
+ * If structural validation fails, no further checks are performed.
139
+ */
140
+
141
+ /**
142
+ * Validate the structural shape of a card using the Zod schema.
143
+ *
144
+ * @param input - An unknown value (already parsed from JSON).
145
+ * @returns A ValidationResult. If valid, the parsed UGCCard can be accessed
146
+ * from the Zod result; callers should re-parse if they need the typed object.
147
+ */
148
+ declare function validateSchema(input: unknown): ValidationResult;
149
+ /**
150
+ * Parse a card from an unknown input, returning either the typed UGCCard
151
+ * or null if structural validation fails.
152
+ *
153
+ * Callers should first run `validateSchema()` to get user-facing errors.
154
+ * This is a convenience for subsequent pipeline stages that need the typed card.
155
+ */
156
+ declare function parseCard(input: unknown): UGCCard | null;
157
+
158
+ /**
159
+ * @safe-ugc-ui/validator — Node Validator
160
+ *
161
+ * Validates component type-specific requirements for each node in the card tree.
162
+ *
163
+ * For every node encountered during traversal:
164
+ * 1. Rejects unknown node types not in ALL_COMPONENT_TYPES.
165
+ * 2. Checks that required fields exist for each component type.
166
+ * 4. Validates ForLoop structure when children use `for`/`in`/`template`.
167
+ */
168
+
169
+ /**
170
+ * Validate all nodes in every view of a card.
171
+ *
172
+ * Uses `traverseCard()` to visit every node and checks:
173
+ * - Node type is a known component type.
174
+ * - Required fields are present for the given component type.
175
+ * - ForLoop children have valid `for`, `in`, and `template` fields.
176
+ *
177
+ * @param views - The `views` object from a UGCCard.
178
+ * @returns An array of validation errors (empty if all nodes are valid).
179
+ */
180
+ declare function validateNodes(views: Record<string, unknown>): ValidationError[];
181
+
182
+ /**
183
+ * @safe-ugc-ui/validator — Value Type Validation
184
+ *
185
+ * Validates per-property $ref / $expr permission rules based on spec
186
+ * section 4.5 value type table.
187
+ *
188
+ * | Property Type | Literal | $ref | $expr |
189
+ * |-------------------|---------|------|-------|
190
+ * | Image.src | yes | yes | no |
191
+ * | Avatar.src | yes | yes | no |
192
+ * | Icon.name | yes | no | no |
193
+ * | Text.content | yes | yes | yes |
194
+ * | Color properties | yes | yes | yes |
195
+ * | Size properties | yes | yes | yes |
196
+ * | position | yes | no | no |
197
+ * | transform | yes | no | no |
198
+ * | gradient | yes | no | no |
199
+ *
200
+ * Additionally, several style properties must always be static
201
+ * (no $ref or $expr): overflow, border*, boxShadow, zIndex,
202
+ * and position offset properties (top/right/bottom/left).
203
+ */
204
+
205
+ /**
206
+ * Walk all nodes in the card and validate that each field's value
207
+ * respects its allowed value types ($ref / $expr permissions).
208
+ *
209
+ * @param views - The `views` object from a UGCCard.
210
+ * @returns An array of validation errors (empty if all values are valid).
211
+ */
212
+ declare function validateValueTypes(views: Record<string, unknown>): ValidationError[];
213
+
214
+ /**
215
+ * @safe-ugc-ui/validator — Style Validator
216
+ *
217
+ * Validates style properties according to the spec's style restrictions:
218
+ * - Forbidden CSS properties (spec 3.8)
219
+ * - Numeric range limits (spec 6.4)
220
+ * - Box-shadow count and value limits
221
+ * - Transform skew prohibition
222
+ * - Dangerous CSS function injection detection
223
+ * - Overflow: scroll prohibition (defense-in-depth)
224
+ * - Color format validation
225
+ * - Length format validation
226
+ */
227
+
228
+ /**
229
+ * Validate all style properties across every node in the card's view tree,
230
+ * and validate card-level style definitions.
231
+ *
232
+ * Checks:
233
+ * 1. Forbidden style properties (spec 3.8)
234
+ * 2. Numeric range limits (spec 6.4)
235
+ * 3. Box-shadow count and value limits
236
+ * 4. Transform skew prohibition
237
+ * 5. CSS function injection in string values
238
+ * 6. Overflow: scroll prohibition (defense-in-depth)
239
+ * 7. Color format validation
240
+ * 8. Length format validation
241
+ * 9. Range checks on string length values
242
+ * 10. $style reference validation and merging
243
+ */
244
+ declare function validateStyles(views: Record<string, unknown>, cardStyles?: Record<string, Record<string, unknown>>): ValidationError[];
245
+
246
+ /**
247
+ * @safe-ugc-ui/validator — Security Validation
248
+ *
249
+ * Enforces security rules from spec sections 3 and 8:
250
+ * - External URL blocking on Image/Avatar `src` fields (literal and $ref)
251
+ * - Asset path validation (`@assets/` prefix, no traversal)
252
+ * - cardAssets value validation
253
+ * - Forbidden CSS `url()` function in style string values
254
+ * - Position restrictions (fixed, sticky, absolute outside Stack)
255
+ * - Nested overflow:auto detection
256
+ * - Prototype pollution prevention in $ref paths
257
+ */
258
+
259
+ /**
260
+ * Run all security validation rules against every node in every view.
261
+ *
262
+ * Rules checked (spec sections 3 and 8):
263
+ * 1. External URL blocking on Image/Avatar `src` (literal and $ref)
264
+ * 2. Asset path validation
265
+ * 3. cardAssets value validation
266
+ * 4. Forbidden CSS `url()` in style string values
267
+ * 5. Position restrictions (fixed, sticky, absolute outside Stack)
268
+ * 6. Nested overflow:auto
269
+ * 7. Prototype pollution in $ref paths
270
+ *
271
+ * @param card - Object containing `views`, optional `state`, and optional `cardAssets`.
272
+ * @returns An array of validation errors (empty if all rules pass).
273
+ */
274
+ declare function validateSecurity(card: {
275
+ views: Record<string, unknown>;
276
+ state?: Record<string, unknown>;
277
+ cardAssets?: Record<string, string>;
278
+ cardStyles?: Record<string, Record<string, unknown>>;
279
+ }): ValidationError[];
280
+
281
+ /**
282
+ * @safe-ugc-ui/validator — Resource Limits Validation
283
+ *
284
+ * Validates resource limits per spec section 6.
285
+ *
286
+ * Uses a single traversal pass to collect all metrics (node count,
287
+ * text content size, style object size, loop iterations, nested loops,
288
+ * overflow:auto count, and stack nesting), then checks each against the
289
+ * defined constants from @safe-ugc-ui/types.
290
+ */
291
+
292
+ /**
293
+ * Validate all resource limits defined in spec section 6.
294
+ *
295
+ * Performs a single traversal of the card tree, collecting metrics for:
296
+ * - Total node count
297
+ * - Total text content bytes (UTF-8)
298
+ * - Total style object bytes (UTF-8, JSON-serialized)
299
+ * - Loop iteration counts
300
+ * - Nested loop depth
301
+ * - overflow:auto element count
302
+ * - Stack nesting depth
303
+ *
304
+ * @param card - A card object with optional `state` and required `views`.
305
+ * @returns An array of validation errors (empty if all limits are satisfied).
306
+ */
307
+ declare function validateLimits(card: {
308
+ state?: Record<string, unknown>;
309
+ views: Record<string, unknown>;
310
+ cardStyles?: Record<string, Record<string, unknown>>;
311
+ }): ValidationError[];
312
+
313
+ /**
314
+ * @safe-ugc-ui/validator -- Expression & Reference Constraint Validation
315
+ *
316
+ * Validates $expr and $ref values found in the card tree against the
317
+ * constraints defined in the spec's "expression constraint" section (6.3).
318
+ *
319
+ * A simple tokenizer splits expression strings into typed tokens so that
320
+ * structural limits (nesting, token count, forbidden operators) can be
321
+ * checked without executing the expression.
322
+ */
323
+
324
+ /**
325
+ * Validates all $expr and $ref values found in the card's views.
326
+ *
327
+ * Uses tree traversal to visit every node, then deep-scans each node's
328
+ * flattened node fields and `style` (and `condition`) for dynamic values.
329
+ *
330
+ * @param views - The `views` object from a UGCCard.
331
+ * @returns An array of validation errors (empty if all constraints pass).
332
+ */
333
+ declare function validateExprConstraints(views: Record<string, unknown>): ValidationError[];
334
+
335
+ /**
336
+ * @safe-ugc-ui/validator — Public API
337
+ *
338
+ * Provides two entry points for card validation:
339
+ *
340
+ * validateRaw(rawJson: string) — Recommended for raw JSON strings.
341
+ * Checks UTF-8 byte size BEFORE parsing. If too large, rejects without
342
+ * parsing (DoS prevention). Returns ValidationResult.
343
+ *
344
+ * validate(input: unknown) — For already-parsed objects.
345
+ * Skips size check. Returns ValidationResult.
346
+ *
347
+ * Pipeline:
348
+ * 1. (validateRaw only) UTF-8 byte size check (1MB limit)
349
+ * 2. (validateRaw only) JSON.parse — parse error → ValidationResult
350
+ * 3. Schema validation → fail → early return
351
+ * 4. All remaining checks run, errors accumulated:
352
+ * node → value-types → style → security → limits → expr-constraints
353
+ */
354
+
355
+ /**
356
+ * Validate an already-parsed card object.
357
+ *
358
+ * Skips the JSON byte size check (use `validateRaw` for raw strings).
359
+ *
360
+ * Pipeline:
361
+ * 1. Schema validation → fail → early return
362
+ * 2. All remaining checks (node, value-types, style, security, limits, expr)
363
+ *
364
+ * @param input - An unknown value (typically parsed JSON).
365
+ * @returns A ValidationResult — safe to render only if `valid` is true.
366
+ */
367
+ declare function validate(input: unknown): ValidationResult;
368
+ /**
369
+ * Validate a raw JSON string. Recommended entry point.
370
+ *
371
+ * Checks the byte size of the raw string BEFORE parsing to prevent
372
+ * JSON parsing DoS with oversized payloads.
373
+ *
374
+ * Pipeline:
375
+ * 1. UTF-8 byte size check (1MB max) → reject without parsing
376
+ * 2. JSON.parse → parse error → ValidationResult (no throw)
377
+ * 3. Schema validation → fail → early return
378
+ * 4. All remaining checks
379
+ *
380
+ * @param rawJson - A raw JSON string representing a UGC card.
381
+ * @returns A ValidationResult — safe to render only if `valid` is true.
382
+ */
383
+ declare function validateRaw(rawJson: string): ValidationResult;
384
+
385
+ export { type NodeVisitor, type TraversableNode, type TraversalContext, type ValidationError, type ValidationErrorCode, type ValidationResult, createError, invalidResult, merge, parseCard, toResult, traverseCard, traverseNode, validResult, validate, validateExprConstraints, validateLimits, validateNodes, validateRaw, validateSchema, validateSecurity, validateStyles, validateValueTypes };