@plasius/schema 1.1.0 → 1.1.1
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/dist/index.cjs +1934 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +391 -0
- package/dist/index.d.ts +391 -0
- package/dist/index.js +1883 -0
- package/dist/index.js.map +1 -0
- package/package.json +18 -6
- package/.eslintrc.cjs +0 -7
- package/.github/workflows/cd.yml +0 -236
- package/.github/workflows/ci.yml +0 -16
- package/.nvmrc +0 -1
- package/.vscode/launch.json +0 -15
- package/CHANGELOG.md +0 -120
- package/CODE_OF_CONDUCT.md +0 -79
- package/CONTRIBUTING.md +0 -201
- package/CONTRIBUTORS.md +0 -27
- package/SECURITY.md +0 -17
- package/docs/adrs/adr-0001: schema.md +0 -45
- package/docs/adrs/adr-template.md +0 -67
- package/legal/CLA-REGISTRY.csv +0 -2
- package/legal/CLA.md +0 -22
- package/legal/CORPORATE_CLA.md +0 -57
- package/legal/INDIVIDUAL_CLA.md +0 -91
- package/sbom.cdx.json +0 -66
- package/src/components.ts +0 -39
- package/src/field.builder.ts +0 -239
- package/src/field.ts +0 -153
- package/src/index.ts +0 -7
- package/src/infer.ts +0 -34
- package/src/pii.ts +0 -165
- package/src/schema.ts +0 -893
- package/src/types.ts +0 -156
- package/src/validation/countryCode.ISO3166.ts +0 -256
- package/src/validation/currencyCode.ISO4217.ts +0 -191
- package/src/validation/dateTime.ISO8601.ts +0 -60
- package/src/validation/email.RFC5322.ts +0 -9
- package/src/validation/generalText.OWASP.ts +0 -39
- package/src/validation/index.ts +0 -13
- package/src/validation/languageCode.BCP47.ts +0 -299
- package/src/validation/name.OWASP.ts +0 -25
- package/src/validation/percentage.ISO80000-1.ts +0 -8
- package/src/validation/phone.E.164.ts +0 -9
- package/src/validation/richtext.OWASP.ts +0 -34
- package/src/validation/url.WHATWG.ts +0 -16
- package/src/validation/user.MS-GOOGLE-APPLE.ts +0 -31
- package/src/validation/uuid.RFC4122.ts +0 -10
- package/src/validation/version.SEMVER2.0.0.ts +0 -10
- package/tests/field.builder.test.ts +0 -81
- package/tests/fields.test.ts +0 -213
- package/tests/pii.test.ts +0 -139
- package/tests/schema.test.ts +0 -501
- package/tests/test-utils.ts +0 -97
- package/tests/validate.test.ts +0 -97
- package/tests/validation.test.ts +0 -98
- package/tsconfig.build.json +0 -19
- package/tsconfig.json +0 -7
- package/tsup.config.ts +0 -10
- package/vitest.config.js +0 -20
package/src/validation/index.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export * from "./email.RFC5322.js";
|
|
2
|
-
export * from "./phone.E.164.js";
|
|
3
|
-
export * from "./url.WHATWG.js";
|
|
4
|
-
export * from "./uuid.RFC4122.js";
|
|
5
|
-
export * from "./dateTime.ISO8601.js";
|
|
6
|
-
export * from "./countryCode.ISO3166.js";
|
|
7
|
-
export * from "./currencyCode.ISO4217.js";
|
|
8
|
-
export * from "./generalText.OWASP.js";
|
|
9
|
-
export * from "./version.SEMVER2.0.0.js";
|
|
10
|
-
export * from "./percentage.ISO80000-1.js";
|
|
11
|
-
export * from "./richtext.OWASP.js";
|
|
12
|
-
export * from "./name.OWASP.js";
|
|
13
|
-
export * from "./user.MS-GOOGLE-APPLE.js";
|
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ISO 639-1 language codes (two-letter).
|
|
5
|
-
* Tip: Keep this list as source of truth for supported languages in your app.
|
|
6
|
-
*/
|
|
7
|
-
export enum IsoLanguageCode {
|
|
8
|
-
Afar = "aa",
|
|
9
|
-
Abkhazian = "ab",
|
|
10
|
-
Afrikaans = "af",
|
|
11
|
-
Akan = "ak",
|
|
12
|
-
Albanian = "sq",
|
|
13
|
-
Amharic = "am",
|
|
14
|
-
Arabic = "ar",
|
|
15
|
-
Aragonese = "an",
|
|
16
|
-
Armenian = "hy",
|
|
17
|
-
Assamese = "as",
|
|
18
|
-
Avaric = "av",
|
|
19
|
-
Aymara = "ay",
|
|
20
|
-
Azerbaijani = "az",
|
|
21
|
-
Bashkir = "ba",
|
|
22
|
-
Bambara = "bm",
|
|
23
|
-
Basque = "eu",
|
|
24
|
-
Belarusian = "be",
|
|
25
|
-
Bengali = "bn",
|
|
26
|
-
Bislama = "bi",
|
|
27
|
-
Bosnian = "bs",
|
|
28
|
-
Breton = "br",
|
|
29
|
-
Bulgarian = "bg",
|
|
30
|
-
Burmese = "my",
|
|
31
|
-
Catalan = "ca",
|
|
32
|
-
Chamorro = "ch",
|
|
33
|
-
Chechen = "ce",
|
|
34
|
-
Chinese = "zh",
|
|
35
|
-
ChurchSlavic = "cu",
|
|
36
|
-
Chuvash = "cv",
|
|
37
|
-
Cornish = "kw",
|
|
38
|
-
Corsican = "co",
|
|
39
|
-
Cree = "cr",
|
|
40
|
-
Croatian = "hr",
|
|
41
|
-
Czech = "cs",
|
|
42
|
-
Danish = "da",
|
|
43
|
-
Divehi = "dv",
|
|
44
|
-
Dutch = "nl",
|
|
45
|
-
Dzongkha = "dz",
|
|
46
|
-
English = "en",
|
|
47
|
-
Esperanto = "eo",
|
|
48
|
-
Estonian = "et",
|
|
49
|
-
Ewe = "ee",
|
|
50
|
-
Faroese = "fo",
|
|
51
|
-
Fijian = "fj",
|
|
52
|
-
Finnish = "fi",
|
|
53
|
-
French = "fr",
|
|
54
|
-
WesternFrisian = "fy",
|
|
55
|
-
Fulah = "ff",
|
|
56
|
-
Gaelic = "gd",
|
|
57
|
-
Galician = "gl",
|
|
58
|
-
Ganda = "lg",
|
|
59
|
-
Georgian = "ka",
|
|
60
|
-
German = "de",
|
|
61
|
-
Greek = "el",
|
|
62
|
-
Kalaallisut = "kl",
|
|
63
|
-
Guarani = "gn",
|
|
64
|
-
Gujarati = "gu",
|
|
65
|
-
Haitian = "ht",
|
|
66
|
-
Hausa = "ha",
|
|
67
|
-
Hebrew = "he",
|
|
68
|
-
Herero = "hz",
|
|
69
|
-
Hindi = "hi",
|
|
70
|
-
HiriMotu = "ho",
|
|
71
|
-
Hungarian = "hu",
|
|
72
|
-
Icelandic = "is",
|
|
73
|
-
Ido = "io",
|
|
74
|
-
Igbo = "ig",
|
|
75
|
-
Indonesian = "id",
|
|
76
|
-
Interlingua = "ia",
|
|
77
|
-
Interlingue = "ie",
|
|
78
|
-
Inuktitut = "iu",
|
|
79
|
-
Inupiaq = "ik",
|
|
80
|
-
Irish = "ga",
|
|
81
|
-
Italian = "it",
|
|
82
|
-
Japanese = "ja",
|
|
83
|
-
Javanese = "jv",
|
|
84
|
-
Kannada = "kn",
|
|
85
|
-
Kanuri = "kr",
|
|
86
|
-
Kashmiri = "ks",
|
|
87
|
-
Kazakh = "kk",
|
|
88
|
-
CentralKhmer = "km",
|
|
89
|
-
Kikuyu = "ki",
|
|
90
|
-
Kinyarwanda = "rw",
|
|
91
|
-
Kyrgyz = "ky",
|
|
92
|
-
Komi = "kv",
|
|
93
|
-
Kongo = "kg",
|
|
94
|
-
Korean = "ko",
|
|
95
|
-
Kuanyama = "kj",
|
|
96
|
-
Kurdish = "ku",
|
|
97
|
-
Lao = "lo",
|
|
98
|
-
Latin = "la",
|
|
99
|
-
Latvian = "lv",
|
|
100
|
-
Limburgan = "li",
|
|
101
|
-
Lingala = "ln",
|
|
102
|
-
Lithuanian = "lt",
|
|
103
|
-
LubaKatanga = "lu",
|
|
104
|
-
Luxembourgish = "lb",
|
|
105
|
-
Macedonian = "mk",
|
|
106
|
-
Malagasy = "mg",
|
|
107
|
-
Malay = "ms",
|
|
108
|
-
Malayalam = "ml",
|
|
109
|
-
Maltese = "mt",
|
|
110
|
-
Manx = "gv",
|
|
111
|
-
Maori = "mi",
|
|
112
|
-
Marathi = "mr",
|
|
113
|
-
Marshallese = "mh",
|
|
114
|
-
Mongolian = "mn",
|
|
115
|
-
Nauru = "na",
|
|
116
|
-
Navajo = "nv",
|
|
117
|
-
NorthNdebele = "nd",
|
|
118
|
-
SouthNdebele = "nr",
|
|
119
|
-
Ndonga = "ng",
|
|
120
|
-
Nepali = "ne",
|
|
121
|
-
Norwegian = "no",
|
|
122
|
-
NorwegianBokmal = "nb",
|
|
123
|
-
NorwegianNynorsk = "nn",
|
|
124
|
-
SichuanYi = "ii",
|
|
125
|
-
Occitan = "oc",
|
|
126
|
-
Ojibwa = "oj",
|
|
127
|
-
Oriya = "or",
|
|
128
|
-
Oromo = "om",
|
|
129
|
-
Ossetian = "os",
|
|
130
|
-
Pali = "pi",
|
|
131
|
-
Pashto = "ps",
|
|
132
|
-
Persian = "fa",
|
|
133
|
-
Polish = "pl",
|
|
134
|
-
Portuguese = "pt",
|
|
135
|
-
Punjabi = "pa",
|
|
136
|
-
Quechua = "qu",
|
|
137
|
-
Romansh = "rm",
|
|
138
|
-
Romanian = "ro",
|
|
139
|
-
Rundi = "rn",
|
|
140
|
-
Russian = "ru",
|
|
141
|
-
Samoan = "sm",
|
|
142
|
-
Sango = "sg",
|
|
143
|
-
Sanskrit = "sa",
|
|
144
|
-
Sardinian = "sc",
|
|
145
|
-
Serbian = "sr",
|
|
146
|
-
Shona = "sn",
|
|
147
|
-
Sindhi = "sd",
|
|
148
|
-
Sinhala = "si",
|
|
149
|
-
Slovak = "sk",
|
|
150
|
-
Slovenian = "sl",
|
|
151
|
-
Somali = "so",
|
|
152
|
-
SouthernSotho = "st",
|
|
153
|
-
Spanish = "es",
|
|
154
|
-
Sundanese = "su",
|
|
155
|
-
Swahili = "sw",
|
|
156
|
-
Swati = "ss",
|
|
157
|
-
Swedish = "sv",
|
|
158
|
-
Tagalog = "tl",
|
|
159
|
-
Tahitian = "ty",
|
|
160
|
-
Tajik = "tg",
|
|
161
|
-
Tamil = "ta",
|
|
162
|
-
Tatar = "tt",
|
|
163
|
-
Telugu = "te",
|
|
164
|
-
Thai = "th",
|
|
165
|
-
Tibetan = "bo",
|
|
166
|
-
Tigrinya = "ti",
|
|
167
|
-
Tonga = "to",
|
|
168
|
-
Tsonga = "ts",
|
|
169
|
-
Tswana = "tn",
|
|
170
|
-
Turkish = "tr",
|
|
171
|
-
Turkmen = "tk",
|
|
172
|
-
Twi = "tw",
|
|
173
|
-
Uighur = "ug",
|
|
174
|
-
Ukrainian = "uk",
|
|
175
|
-
Urdu = "ur",
|
|
176
|
-
Uzbek = "uz",
|
|
177
|
-
Venda = "ve",
|
|
178
|
-
Vietnamese = "vi",
|
|
179
|
-
Volapuk = "vo",
|
|
180
|
-
Walloon = "wa",
|
|
181
|
-
Welsh = "cy",
|
|
182
|
-
Wolof = "wo",
|
|
183
|
-
Xhosa = "xh",
|
|
184
|
-
Yiddish = "yi",
|
|
185
|
-
Yoruba = "yo",
|
|
186
|
-
Zhuang = "za",
|
|
187
|
-
Zulu = "zu",
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Fast lookup set for enum values (lowercase 2-letter codes). */
|
|
191
|
-
const ISO_LANGUAGE_SET: ReadonlySet<string> = new Set<string>(
|
|
192
|
-
Object.values(IsoLanguageCode)
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
/** Type guard: primary language must be one of the enum values. */
|
|
196
|
-
export function isIsoLanguageCode(value: unknown): value is IsoLanguageCode {
|
|
197
|
-
return typeof value === "string" && ISO_LANGUAGE_SET.has(value.toLowerCase());
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Region validator per BCP 47:
|
|
202
|
-
* - ISO 3166-1 alpha-2: 2 uppercase letters (e.g., GB, US)
|
|
203
|
-
* - UN M.49 numeric: 3 digits (e.g., 419 for Latin America)
|
|
204
|
-
*
|
|
205
|
-
* NOTE: This validates *shape* not membership against the 3166 list.
|
|
206
|
-
* If you want hard membership, we can add a Set of all alpha-2 regions.
|
|
207
|
-
*/
|
|
208
|
-
export function isRegionSubtag(value: string): boolean {
|
|
209
|
-
return /^[A-Z]{2}$/.test(value) || /^\d{3}$/.test(value);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Script subtag per ISO 15924: one capital + three lowercase (e.g., Latn, Cyrl, Hans). */
|
|
213
|
-
export function isScriptSubtag(value: string): boolean {
|
|
214
|
-
return /^[A-Z][a-z]{3}$/.test(value);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** Variant subtag per BCP 47: 5–8 alnum, or 4 starting with a digit. */
|
|
218
|
-
export function isVariantSubtag(value: string): boolean {
|
|
219
|
-
return /^([0-9][A-Za-z0-9]{3}|[A-Za-z0-9]{5,8})$/.test(value);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Extension sequence: singleton (alnum except 'x') + one or more 2–8 alnum subtags. */
|
|
223
|
-
export function isExtensionSingleton(value: string): boolean {
|
|
224
|
-
return /^[0-9A-WY-Za-wy-z]$/.test(value); // any alnum except 'x' (private-use)
|
|
225
|
-
}
|
|
226
|
-
export function isExtensionSubtag(value: string): boolean {
|
|
227
|
-
return /^[A-Za-z0-9]{2,8}$/.test(value);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** Private-use subtag: 'x' then one or more 1–8 alnum subtags. */
|
|
231
|
-
export function isPrivateUseSingleton(value: string): boolean {
|
|
232
|
-
return value.toLowerCase() === "x";
|
|
233
|
-
}
|
|
234
|
-
export function isPrivateUseSubtag(value: string): boolean {
|
|
235
|
-
return /^[A-Za-z0-9]{1,8}$/.test(value);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Validates:
|
|
240
|
-
* - plain language: "en"
|
|
241
|
-
* - language + region: "en-GB"
|
|
242
|
-
* - language + script + region: "sr-Cyrl-RS"
|
|
243
|
-
* - language + variants: "sl-rozaj-biske", "de-CH-1996"
|
|
244
|
-
* - extensions: "en-GB-u-ca-gregory"
|
|
245
|
-
* - private-use: "en-x-klingon" or just "x-piglatin"
|
|
246
|
-
*
|
|
247
|
-
* Returns true only if the primary language is in IsoLanguageCode
|
|
248
|
-
* and the rest of the tag conforms to BCP 47 structure.
|
|
249
|
-
*/
|
|
250
|
-
export function validateLanguage(value: unknown): boolean {
|
|
251
|
-
if (typeof value !== "string" || value.length === 0) return false;
|
|
252
|
-
|
|
253
|
-
const parts = value.split("-");
|
|
254
|
-
let i = 0;
|
|
255
|
-
|
|
256
|
-
// 1) primary language (must be enum member; we use lowercase for comparison)
|
|
257
|
-
const lang = parts[i];
|
|
258
|
-
if (!lang || !isIsoLanguageCode(lang)) return false;
|
|
259
|
-
i += 1;
|
|
260
|
-
|
|
261
|
-
// 2) optional script
|
|
262
|
-
if (i < parts.length && isScriptSubtag(parts[i] as string)) {
|
|
263
|
-
i += 1;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 3) optional region
|
|
267
|
-
if (i < parts.length && isRegionSubtag((parts[i] as string).toUpperCase())) {
|
|
268
|
-
// region must be uppercase if alpha; we normalize for check only
|
|
269
|
-
i += 1;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// 4) zero or more variants
|
|
273
|
-
while (i < parts.length && isVariantSubtag((parts[i] as string))) {
|
|
274
|
-
i += 1;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// 5) zero or more extensions
|
|
278
|
-
// extension = singleton ; 2–8 ; ( ; 2–8 )*
|
|
279
|
-
while (i < parts.length && isExtensionSingleton((parts[i] as string))) {
|
|
280
|
-
i += 1;
|
|
281
|
-
// must have at least one following subtag of length 2–8
|
|
282
|
-
if (!(i < parts.length && isExtensionSubtag(parts[i]!))) return false;
|
|
283
|
-
while (i < parts.length && isExtensionSubtag(parts[i]!)) {
|
|
284
|
-
i += 1;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// 6) optional private-use: 'x' 1*('-' (1*8alnum))
|
|
289
|
-
if (i < parts.length && isPrivateUseSingleton(parts[i]!)) {
|
|
290
|
-
i += 1;
|
|
291
|
-
if (!(i < parts.length && isPrivateUseSubtag(parts[i]!))) return false;
|
|
292
|
-
while (i < parts.length && isPrivateUseSubtag(parts[i]!)) {
|
|
293
|
-
i += 1;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// no leftovers
|
|
298
|
-
return i === parts.length;
|
|
299
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates that a name is safe, culturally inclusive, and matches global best practice.
|
|
3
|
-
* Global Standard: OWASP Input Validation Cheat Sheet + ICAO Doc 9303 + IETF PRECIS
|
|
4
|
-
*/
|
|
5
|
-
export function validateName(value: unknown): boolean {
|
|
6
|
-
if (typeof value !== "string") return false;
|
|
7
|
-
|
|
8
|
-
const trimmed = value.trim();
|
|
9
|
-
if (trimmed.length === 0) return false;
|
|
10
|
-
|
|
11
|
-
// Limit length (ISO guidance: max 256 is typical)
|
|
12
|
-
if (trimmed.length > 256) return false;
|
|
13
|
-
|
|
14
|
-
// Reject ASCII control chars (U+0000–U+001F and U+007F)
|
|
15
|
-
for (const ch of trimmed) {
|
|
16
|
-
const cp = ch.codePointAt(0)!;
|
|
17
|
-
if ((cp >= 0x00 && cp <= 0x1F) || cp === 0x7F) return false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Core pattern
|
|
21
|
-
const namePattern = /^[\p{L}\p{M}'\- ]+$/u;
|
|
22
|
-
if (!namePattern.test(trimmed)) return false;
|
|
23
|
-
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates that a number is a percentage value (0 to 100 inclusive).
|
|
3
|
-
* Global Standard: ISO 80000-1 percentage definition.
|
|
4
|
-
*/
|
|
5
|
-
export function validatePercentage(value: unknown): boolean {
|
|
6
|
-
if (typeof value !== "number") return false;
|
|
7
|
-
return value >= 0 && value <= 100;
|
|
8
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates a phone number string in strict E.164 format.
|
|
3
|
-
* Returns true for strings like "+441632960960" (max 15 digits, starting with a '+').
|
|
4
|
-
*/
|
|
5
|
-
export const validatePhone = (value: unknown): boolean => {
|
|
6
|
-
if (typeof value !== "string") return false;
|
|
7
|
-
const phoneRegex = /^\+[1-9]\d{1,14}$/; // E.164 format
|
|
8
|
-
return phoneRegex.test(value);
|
|
9
|
-
};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates rich text input to ensure it contains only safe HTML/Markdown.
|
|
3
|
-
* Global Standard: OWASP HTML Sanitization Guidelines (2024).
|
|
4
|
-
* This validator checks for dangerous patterns — does not sanitize — assumes text will be sanitized downstream.
|
|
5
|
-
*/
|
|
6
|
-
export function validateRichText(value: unknown): boolean {
|
|
7
|
-
if (typeof value !== "string") return false;
|
|
8
|
-
const trimmed = value.trim();
|
|
9
|
-
if (trimmed.length === 0) return true; // Allow empty rich text
|
|
10
|
-
|
|
11
|
-
// Reject known dangerous tags
|
|
12
|
-
if (
|
|
13
|
-
/<(script|iframe|object|embed|style|link|meta|base|form|input|button|textarea|select)\b/i.test(
|
|
14
|
-
trimmed
|
|
15
|
-
)
|
|
16
|
-
) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Reject javascript: links
|
|
21
|
-
if (/javascript:/i.test(trimmed)) {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Reject event handlers (onload, onclick, etc)
|
|
26
|
-
if (/on\w+=["']?/i.test(trimmed)) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Optionally: limit max length (e.g. 10,000 chars)
|
|
31
|
-
if (trimmed.length > 10000) return false;
|
|
32
|
-
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates a URL string using the WHATWG URL API.
|
|
3
|
-
* Accepts only 'http' or 'https' protocols.
|
|
4
|
-
* Returns true if the URL is syntactically valid.
|
|
5
|
-
*/
|
|
6
|
-
export const validateUrl = (value: unknown): boolean => {
|
|
7
|
-
if (typeof value !== "string") return false;
|
|
8
|
-
try {
|
|
9
|
-
const url = new URL(value);
|
|
10
|
-
// Enforce scheme:
|
|
11
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
12
|
-
return true;
|
|
13
|
-
} catch {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates that a user ID is a valid `sub` from one of the supported identity providers.
|
|
3
|
-
* Global Standard: OpenID Connect Core 1.0 `sub` claim.
|
|
4
|
-
*/
|
|
5
|
-
export function validateUserId(value: unknown): boolean {
|
|
6
|
-
if (typeof value !== "string") return false;
|
|
7
|
-
|
|
8
|
-
const trimmed = value.trim();
|
|
9
|
-
if (trimmed.length === 0) return false;
|
|
10
|
-
|
|
11
|
-
// Google: all digits
|
|
12
|
-
const googlePattern = /^\d{21,22}$/;
|
|
13
|
-
|
|
14
|
-
// Microsoft: UUID v4
|
|
15
|
-
const microsoftPattern =
|
|
16
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
17
|
-
|
|
18
|
-
// Apple: opaque, usually UUID but allow any printable string up to 255 chars
|
|
19
|
-
const applePattern = /^[\w\-.]{6,255}$/; // Alphanumeric + - . _ (common safe characters)
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
googlePattern.test(trimmed) ||
|
|
23
|
-
microsoftPattern.test(trimmed) ||
|
|
24
|
-
applePattern.test(trimmed)
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function validateUserIdArray(value: unknown): boolean {
|
|
29
|
-
if (!Array.isArray(value)) return false;
|
|
30
|
-
return value.every(validateUserId);
|
|
31
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates a string against the RFC 4122 format for UUIDs.
|
|
3
|
-
* Matches UUIDs of versions 1 to 5, e.g., "123e4567-e89b-12d3-a456-426614174000".
|
|
4
|
-
*/
|
|
5
|
-
export const validateUUID = (value: unknown): boolean => {
|
|
6
|
-
if (typeof value !== "string") return false;
|
|
7
|
-
const uuidRegex =
|
|
8
|
-
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
|
-
return uuidRegex.test(value);
|
|
10
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validates that a version string conforms to Semantic Versioning (SemVer 2.0.0).
|
|
3
|
-
* Global Standard: https://semver.org/
|
|
4
|
-
*/
|
|
5
|
-
export function validateSemVer(value: unknown): boolean {
|
|
6
|
-
if (typeof value !== "string") return false;
|
|
7
|
-
return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.test(
|
|
8
|
-
value
|
|
9
|
-
);
|
|
10
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { field } from "../src/field";
|
|
3
|
-
import { createSchema } from "../src/schema";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* This test verifies that when an older entity (v1.0.0) is validated against a newer
|
|
7
|
-
* schema (v2.0.0), a field-level `.upgrade()` function is invoked to transform the
|
|
8
|
-
* value into the new shape, and that validation passes with the upgraded value.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
describe("schema field upgrade flow", () => {
|
|
12
|
-
it("upgrades old displayName string → new object shape", () => {
|
|
13
|
-
const userV2 = createSchema(
|
|
14
|
-
{
|
|
15
|
-
// v2 requires an object { given, family } instead of a plain string
|
|
16
|
-
displayName: field
|
|
17
|
-
.object({
|
|
18
|
-
given: field.string().required(),
|
|
19
|
-
family: field.string().required(),
|
|
20
|
-
})
|
|
21
|
-
.version("2.0.0")
|
|
22
|
-
.upgrade((value, { entityFrom, entityTo, fieldTo, fieldName }) => {
|
|
23
|
-
if (typeof value === "string") {
|
|
24
|
-
const parts = value.trim().split(/\s+/);
|
|
25
|
-
const given = parts.shift() ?? "";
|
|
26
|
-
const family = parts.join(" ") || "Unknown";
|
|
27
|
-
return { ok: true, value: { given, family } };
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
ok: false,
|
|
31
|
-
error: `Cannot upgrade ${fieldName} from non-string`,
|
|
32
|
-
};
|
|
33
|
-
}),
|
|
34
|
-
},
|
|
35
|
-
"User",
|
|
36
|
-
{ version: "2.0.0", table: "users" }
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const oldEntity = {
|
|
40
|
-
version: "1.0.0",
|
|
41
|
-
displayName: "Ada Lovelace",
|
|
42
|
-
} as const;
|
|
43
|
-
|
|
44
|
-
const res = userV2.validate(oldEntity);
|
|
45
|
-
// Expect our validation contract: no errors and transformed value
|
|
46
|
-
expect(Array.isArray(res.errors)).toBe(true);
|
|
47
|
-
expect(res.errors?.length).toBe(0);
|
|
48
|
-
expect(res.value?.displayName).toEqual({
|
|
49
|
-
given: "Ada",
|
|
50
|
-
family: "Lovelace",
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("fails validation when upgrade is not possible", () => {
|
|
55
|
-
const userV2 = createSchema(
|
|
56
|
-
{
|
|
57
|
-
age: field
|
|
58
|
-
.number()
|
|
59
|
-
.version("2.0.0")
|
|
60
|
-
.upgrade((value) => {
|
|
61
|
-
// Only upgrade from numeric strings like "42"
|
|
62
|
-
if (typeof value === "string" && /^\d+$/.test(value)) {
|
|
63
|
-
return { ok: true, value: Number(value) };
|
|
64
|
-
}
|
|
65
|
-
return { ok: false, error: "age cannot be upgraded" };
|
|
66
|
-
})
|
|
67
|
-
.required(),
|
|
68
|
-
},
|
|
69
|
-
"User",
|
|
70
|
-
{ version: "2.0.0", table: "users" }
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const oldEntity = { version: "1.0.0", age: "forty two" } as const;
|
|
74
|
-
const res = userV2.validate(oldEntity);
|
|
75
|
-
|
|
76
|
-
expect(Array.isArray(res.errors)).toBe(true);
|
|
77
|
-
expect(res.errors?.length).toBeGreaterThan(0);
|
|
78
|
-
// Should keep original invalid value when upgrade fails
|
|
79
|
-
expect(res.value?.age).toBe("forty two");
|
|
80
|
-
});
|
|
81
|
-
});
|