@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 ADDED
@@ -0,0 +1,34 @@
1
+ # @407dev/field-types
2
+
3
+ ## Purpose
4
+ - Registry and built-in field type definitions for scalar and semantic CMS field types (`text`, `number`, `toggle`, `select`, `image`, `video`, `richtext`, `media.videoList`, `geo.area`, `address`, `address.list`).
5
+ - Validates field constraints, default values, safe URI schemes, and ProseMirror rich text documents against node/mark whitelists.
6
+ - Provides normalization repairs (e.g. polygon ring closure) for editor commit workflows.
7
+
8
+ ## Surface
9
+ - `registry.ts`: `registerFieldType()`, `getFieldType()`, `hasFieldType()`, `listFieldTypeIds()`, `listFieldTypes()`, `fieldTypeAliases()`, `resolveTypeAlias()`, `normalizeFieldValue()`, `unknownTypeIds()`, `validateFieldValue()`, `resetRegistry()`, `registerBaseTypes()`.
10
+ - `url.ts`: `stripUrlControlChars()`, `isSafeMediaSrc()`, `isSafeActionHref()`.
11
+ - `richtext.ts`: `validateProseMirrorDoc()`, `isValidLinkHref()`, `isValidImageSrc()`, `RichTextConstraints`.
12
+ - `types.ts`: `FieldTypeDefinition`, `FieldValidationError`, `FieldValidationResult`.
13
+ - Built-in types: `textType`, `numberType`, `toggleType`, `selectType`, `imageType`, `videoType`, `richtextType`, `videoListType`, `geoAreaType`, `addressType`, `addressListType`.
14
+
15
+ ## Patterns
16
+ - Pure validation functions that never throw on invalid input, returning `{ valid: boolean; errors?: FieldValidationError[] }`.
17
+ - Safe URI sanitizer strips Unicode and ASCII control characters (`\x00`–`\x1F`, `\x7F`–`\x9F`) before evaluating scheme allowlist.
18
+ - In-house $O(n^2)$ orientation-based self-intersection test for polygon rings without external geometry libraries.
19
+
20
+ ## Integrations
21
+ - Consumed by `apps/api` for manifest field type verification and validation during entry/field writes.
22
+ - Consumed by `packages/astro` to dynamically generate synchronous field helper methods and render components.
23
+ - Consumed by `packages/cli` during AST extraction and manifest type checking.
24
+ - Consumed by `apps/web` for field popovers, group modal normalization, and widget form validation.
25
+
26
+ ## Constraints
27
+ - Published package: builds to ESM and `.d.ts` via `tsup`.
28
+ - Must not depend on `apps/*` or Astro runtime packages.
29
+ - Zero external runtime dependencies: no Turf.js, no validator packages.
30
+
31
+ ## Gotchas
32
+ - `normalize?(value)` is called exclusively by the editor before save; `apps/api` stores values as received.
33
+ - `geo.area` self-intersection algorithm validates that exterior boundary edges do not self-intersect; hole-inside-shell containment validation is explicitly out of scope.
34
+
@@ -0,0 +1,226 @@
1
+ interface FieldValidationError {
2
+ path: string;
3
+ message: string;
4
+ keyword: string;
5
+ }
6
+ type FieldValidationResult = {
7
+ valid: true;
8
+ } | {
9
+ valid: false;
10
+ errors: FieldValidationError[];
11
+ };
12
+ interface FieldTypeDefinition<T = unknown> {
13
+ id: string;
14
+ aliases?: string[];
15
+ constraintsSchema: object;
16
+ defaultValue?: T;
17
+ renderHint?: {
18
+ tag?: string;
19
+ };
20
+ normalize?(value: unknown, constraints?: Record<string, unknown>): unknown;
21
+ validate(value: unknown, constraints?: Record<string, unknown>): FieldValidationResult;
22
+ }
23
+
24
+ /**
25
+ * Registers a new or custom field type.
26
+ */
27
+ declare function registerFieldType(def: FieldTypeDefinition): void;
28
+ /**
29
+ * Retrieves a field type definition by ID.
30
+ */
31
+ declare function getFieldType(id: string): FieldTypeDefinition | undefined;
32
+ /**
33
+ * Checks whether a field type ID is registered.
34
+ */
35
+ declare function hasFieldType(id: string): boolean;
36
+ /**
37
+ * Returns all registered field type definitions.
38
+ */
39
+ declare function listFieldTypes(): FieldTypeDefinition[];
40
+ /**
41
+ * Returns all registered field type IDs.
42
+ */
43
+ declare function listFieldTypeIds(): string[];
44
+ /**
45
+ * Returns a map of all registered aliases to their canonical field type IDs.
46
+ */
47
+ declare function fieldTypeAliases(): Map<string, string>;
48
+ /**
49
+ * Resolves a field method/alias name to its canonical field type ID.
50
+ */
51
+ declare function resolveTypeAlias(name: string): string;
52
+ /**
53
+ * Normalizes a field value using the field type's normalize hook if defined.
54
+ * Never throws — returns input value unchanged if no normalize hook or on error.
55
+ */
56
+ declare function normalizeFieldValue(typeId: string, value: unknown, constraints?: Record<string, unknown>): unknown;
57
+ /**
58
+ * Given a list of field type IDs, returns any IDs that are not registered.
59
+ */
60
+ declare function unknownTypeIds(ids: string[]): string[];
61
+ /**
62
+ * Validates a field value against a field type's validator and constraints.
63
+ * Never throws — returns an unknown_type error if type is not registered.
64
+ */
65
+ declare function validateFieldValue(typeId: string, value: unknown, constraints?: Record<string, unknown>): FieldValidationResult;
66
+ /**
67
+ * Resets the registry (useful for test isolation).
68
+ */
69
+ declare function resetRegistry(): void;
70
+
71
+ interface RichTextConstraints {
72
+ nodes?: string[];
73
+ marks?: string[];
74
+ maxDepth?: number;
75
+ maxNodes?: number;
76
+ }
77
+ declare const DEFAULT_ALLOWED_NODES: string[];
78
+ declare const DEFAULT_ALLOWED_MARKS: string[];
79
+ declare function isValidLinkHref(rawHref: string): boolean;
80
+ /**
81
+ * Validates a ProseMirror document structure against constraints and strict security rules.
82
+ */
83
+ declare function validateProseMirrorDoc(doc: unknown, constraints?: RichTextConstraints): FieldValidationResult;
84
+
85
+ /**
86
+ * Shared URL guards and validation routines.
87
+ */
88
+ /**
89
+ * Strips all ASCII control characters, whitespace, and invisible Unicode control codes from a raw URL.
90
+ */
91
+ declare function stripUrlControlChars(raw: string): string;
92
+ /**
93
+ * Checks whether a cleaned URL string is a safe relative path or anchor.
94
+ * Must start with exactly one '/' not followed by '/' or '\', or start with '#'.
95
+ * Rejects any backslashes or percent-encoded backslashes before or at path root.
96
+ */
97
+ declare function isSafeRelativeOrAnchor(cleaned: string): boolean;
98
+ /**
99
+ * Validates a media src (video/image/audio).
100
+ * Allows relative paths, `#anchor`, UUID asset references, and `https://` URLs (with optional host allowlist).
101
+ */
102
+ declare function isSafeMediaSrc(raw: string, allowedHosts?: string[]): boolean;
103
+ /**
104
+ * Validates an action href (link/CTA).
105
+ * Default-restrictive: allows relative paths or `#anchor` only.
106
+ * Absolute `https://` is permitted ONLY when its host is in `allowedHosts`.
107
+ * Rejects `mailto:`, `tel:`, `data:`, `blob:`, `javascript:`, and non-allowed absolute URLs.
108
+ */
109
+ declare function isSafeActionHref(raw: string, allowedHosts?: string[]): boolean;
110
+
111
+ interface TextConstraints {
112
+ minLength?: number;
113
+ maxLength?: number;
114
+ pattern?: string;
115
+ }
116
+ declare const textType: FieldTypeDefinition<string>;
117
+
118
+ interface NumberConstraints {
119
+ min?: number;
120
+ max?: number;
121
+ step?: number;
122
+ integer?: boolean;
123
+ }
124
+ declare const numberType: FieldTypeDefinition<number>;
125
+
126
+ declare const toggleType: FieldTypeDefinition<boolean>;
127
+
128
+ interface SelectOption {
129
+ label: string;
130
+ value: string;
131
+ }
132
+ interface SelectConstraints {
133
+ options?: Array<string | SelectOption>;
134
+ }
135
+ declare const selectType: FieldTypeDefinition<string>;
136
+
137
+ interface ImageConstraints {
138
+ maxSizeBytes?: number;
139
+ allowedMimeTypes?: string[];
140
+ }
141
+ declare const imageType: FieldTypeDefinition<string | null>;
142
+
143
+ declare const richtextType: FieldTypeDefinition<Record<string, unknown>>;
144
+
145
+ interface VideoConstraints {
146
+ maxSizeBytes?: number;
147
+ allowedMimeTypes?: string[];
148
+ }
149
+ declare const videoType: FieldTypeDefinition<string | null>;
150
+
151
+ type MediaRef = string | {
152
+ kind: 'asset';
153
+ id: string;
154
+ } | {
155
+ kind: 'url';
156
+ src: string;
157
+ };
158
+ interface VideoItem {
159
+ src: MediaRef;
160
+ poster?: MediaRef;
161
+ caption?: string;
162
+ href?: string;
163
+ }
164
+ interface VideoListConstraints {
165
+ maxItems?: number;
166
+ allowedSrcHosts?: string[];
167
+ allowedHrefHosts?: string[];
168
+ maxCaptionLength?: number;
169
+ }
170
+ declare const videoListType: FieldTypeDefinition<VideoItem[]>;
171
+
172
+ interface GeoAreaConstraints {
173
+ maxVertices?: number;
174
+ maxRings?: number;
175
+ }
176
+ interface GeoJsonPolygon {
177
+ type: 'Polygon';
178
+ coordinates: number[][][];
179
+ }
180
+ interface GeoJsonMultiPolygon {
181
+ type: 'MultiPolygon';
182
+ coordinates: number[][][][];
183
+ }
184
+ type GeoAreaValue = GeoJsonPolygon | GeoJsonMultiPolygon | null;
185
+ /**
186
+ * Pure repair pass for GeoJSON geometry:
187
+ * Auto-closes rings and removes consecutive duplicate positions.
188
+ */
189
+ declare function normalizeGeoArea(value: unknown): unknown;
190
+ declare const geoAreaType: FieldTypeDefinition<GeoAreaValue>;
191
+
192
+ interface Address {
193
+ line1: string;
194
+ line2?: string;
195
+ city: string;
196
+ region: string;
197
+ postal: string;
198
+ country: string;
199
+ lat?: number;
200
+ lng?: number;
201
+ }
202
+ interface AddressConstraints {
203
+ countryFormat?: 'iso2' | 'free';
204
+ }
205
+ interface AddressListConstraints extends AddressConstraints {
206
+ minItems?: number;
207
+ maxItems?: number;
208
+ }
209
+ declare const addressType: FieldTypeDefinition<Address | null>;
210
+ declare const addressListType: FieldTypeDefinition<Address[]>;
211
+
212
+ interface StreamVideoValue {
213
+ uid: string;
214
+ durationMs?: number;
215
+ thumbnail?: string;
216
+ ready?: boolean;
217
+ }
218
+ interface StreamVideoConstraints {
219
+ maxDurationSeconds?: number;
220
+ allowedThumbnailHosts?: string[];
221
+ }
222
+ declare const streamVideoType: FieldTypeDefinition<StreamVideoValue | null>;
223
+
224
+ declare function registerBaseTypes(): void;
225
+
226
+ export { type Address, type AddressConstraints, type AddressListConstraints, DEFAULT_ALLOWED_MARKS, DEFAULT_ALLOWED_NODES, type FieldTypeDefinition, type FieldValidationError, type FieldValidationResult, type GeoAreaConstraints, type GeoAreaValue, type GeoJsonMultiPolygon, type GeoJsonPolygon, type ImageConstraints, type MediaRef, type NumberConstraints, type RichTextConstraints, type SelectConstraints, type SelectOption, type StreamVideoConstraints, type StreamVideoValue, type TextConstraints, type VideoConstraints, type VideoItem, type VideoListConstraints, 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 };