@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.
Files changed (58) hide show
  1. package/dist/index.cjs +1934 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.cts +391 -0
  4. package/dist/index.d.ts +391 -0
  5. package/dist/index.js +1883 -0
  6. package/dist/index.js.map +1 -0
  7. package/package.json +18 -6
  8. package/.eslintrc.cjs +0 -7
  9. package/.github/workflows/cd.yml +0 -236
  10. package/.github/workflows/ci.yml +0 -16
  11. package/.nvmrc +0 -1
  12. package/.vscode/launch.json +0 -15
  13. package/CHANGELOG.md +0 -120
  14. package/CODE_OF_CONDUCT.md +0 -79
  15. package/CONTRIBUTING.md +0 -201
  16. package/CONTRIBUTORS.md +0 -27
  17. package/SECURITY.md +0 -17
  18. package/docs/adrs/adr-0001: schema.md +0 -45
  19. package/docs/adrs/adr-template.md +0 -67
  20. package/legal/CLA-REGISTRY.csv +0 -2
  21. package/legal/CLA.md +0 -22
  22. package/legal/CORPORATE_CLA.md +0 -57
  23. package/legal/INDIVIDUAL_CLA.md +0 -91
  24. package/sbom.cdx.json +0 -66
  25. package/src/components.ts +0 -39
  26. package/src/field.builder.ts +0 -239
  27. package/src/field.ts +0 -153
  28. package/src/index.ts +0 -7
  29. package/src/infer.ts +0 -34
  30. package/src/pii.ts +0 -165
  31. package/src/schema.ts +0 -893
  32. package/src/types.ts +0 -156
  33. package/src/validation/countryCode.ISO3166.ts +0 -256
  34. package/src/validation/currencyCode.ISO4217.ts +0 -191
  35. package/src/validation/dateTime.ISO8601.ts +0 -60
  36. package/src/validation/email.RFC5322.ts +0 -9
  37. package/src/validation/generalText.OWASP.ts +0 -39
  38. package/src/validation/index.ts +0 -13
  39. package/src/validation/languageCode.BCP47.ts +0 -299
  40. package/src/validation/name.OWASP.ts +0 -25
  41. package/src/validation/percentage.ISO80000-1.ts +0 -8
  42. package/src/validation/phone.E.164.ts +0 -9
  43. package/src/validation/richtext.OWASP.ts +0 -34
  44. package/src/validation/url.WHATWG.ts +0 -16
  45. package/src/validation/user.MS-GOOGLE-APPLE.ts +0 -31
  46. package/src/validation/uuid.RFC4122.ts +0 -10
  47. package/src/validation/version.SEMVER2.0.0.ts +0 -10
  48. package/tests/field.builder.test.ts +0 -81
  49. package/tests/fields.test.ts +0 -213
  50. package/tests/pii.test.ts +0 -139
  51. package/tests/schema.test.ts +0 -501
  52. package/tests/test-utils.ts +0 -97
  53. package/tests/validate.test.ts +0 -97
  54. package/tests/validation.test.ts +0 -98
  55. package/tsconfig.build.json +0 -19
  56. package/tsconfig.json +0 -7
  57. package/tsup.config.ts +0 -10
  58. package/vitest.config.js +0 -20
@@ -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
- });