@plasius/schema 1.0.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.
Files changed (44) hide show
  1. package/.eslintrc.cjs +7 -0
  2. package/.github/workflows/cd.yml +54 -0
  3. package/.github/workflows/ci.yml +16 -0
  4. package/.vscode/launch.json +15 -0
  5. package/CODE_OF_CONDUCT.md +79 -0
  6. package/CONTRIBUTORS.md +27 -0
  7. package/LICENSE +203 -0
  8. package/README.md +45 -0
  9. package/SECURITY.md +17 -0
  10. package/legal/CLA-REGISTRY.csv +2 -0
  11. package/legal/CLA.md +22 -0
  12. package/legal/CORPORATE_CLA.md +55 -0
  13. package/legal/INDIVIDUAL_CLA.md +91 -0
  14. package/package.json +48 -0
  15. package/src/components.ts +39 -0
  16. package/src/field.builder.ts +119 -0
  17. package/src/field.ts +14 -0
  18. package/src/index.ts +7 -0
  19. package/src/infer.ts +34 -0
  20. package/src/pii.ts +165 -0
  21. package/src/schema.ts +757 -0
  22. package/src/types.ts +156 -0
  23. package/src/validation/countryCode.ISO3166.ts +256 -0
  24. package/src/validation/currencyCode.ISO4217.ts +191 -0
  25. package/src/validation/dateTime.ISO8601.ts +9 -0
  26. package/src/validation/email.RFC5322.ts +9 -0
  27. package/src/validation/generalText.OWASP.ts +39 -0
  28. package/src/validation/index.ts +13 -0
  29. package/src/validation/name.OWASP.ts +25 -0
  30. package/src/validation/percentage.ISO80000-1.ts +8 -0
  31. package/src/validation/phone.E.164.ts +9 -0
  32. package/src/validation/richtext.OWASP.ts +34 -0
  33. package/src/validation/url.WHATWG.ts +16 -0
  34. package/src/validation/user.MS-GOOGLE-APPLE.ts +31 -0
  35. package/src/validation/uuid.RFC4122.ts +10 -0
  36. package/src/validation/version.SEMVER2.0.0.ts +8 -0
  37. package/tests/pii.test.ts +139 -0
  38. package/tests/schema.test.ts +501 -0
  39. package/tests/test-utils.ts +97 -0
  40. package/tests/validate.test.ts +97 -0
  41. package/tests/validation.test.ts +98 -0
  42. package/tsconfig.build.json +19 -0
  43. package/tsconfig.json +7 -0
  44. package/tsup.config.ts +10 -0
package/src/types.ts ADDED
@@ -0,0 +1,156 @@
1
+ import FieldBuilder from "./field.builder.js";
2
+ import { Infer } from "./infer.js";
3
+ import { PII, PIIAction, PIIClassification, PIILogHandling } from "./pii.js";
4
+
5
+ export type FieldTypeMap = {
6
+ string: string;
7
+ number: number;
8
+ boolean: boolean;
9
+ object: Record<string, unknown>;
10
+ "string[]": string[];
11
+ "number[]": number[];
12
+ "boolean[]": boolean[];
13
+ "object[]": Record<string, unknown>[];
14
+ ref: RefEntityId;
15
+ "ref[]": RefEntityId[];
16
+ };
17
+
18
+ export type FieldType = keyof FieldTypeMap;
19
+
20
+ export type PIIEnforcement = "strict" | "warn" | "none";
21
+
22
+ export interface SchemaOptions {
23
+ version?: string;
24
+ table?: string;
25
+ schemaValidator?: (value: any) => boolean;
26
+ piiEnforcement?: PIIEnforcement; // How should PII be enforced?
27
+ }
28
+ export type SchemaShape = Record<string, FieldBuilder<any>>;
29
+
30
+ export interface FieldDefinition<T = unknown> {
31
+ type: FieldType;
32
+ __valueType?: T;
33
+ optional?: boolean;
34
+ immutable?: boolean;
35
+ description?: string;
36
+ refType?: string;
37
+ version?: string;
38
+ deprecated?: boolean;
39
+ deprecatedVersion?: string;
40
+ system?: boolean;
41
+ autoValidate?: boolean;
42
+ refPolicy?: "eager" | "lazy";
43
+ enum?: string[];
44
+ _shape?: SchemaShape;
45
+ pii?: PII;
46
+ validator?: (value: any) => boolean;
47
+ }
48
+
49
+ export interface ValidateCompositionOptions {
50
+ resolveEntity: (type: string, id: string) => Promise<any | null>;
51
+ validatorContext?: { visited: Set<string> };
52
+ maxDepth?: number;
53
+ log?: (msg: string) => void; // Optional for trace/debug
54
+ onlyFields?: string[]; // NEW
55
+ }
56
+
57
+ export interface Schema<T extends SchemaShape> {
58
+ //// The shape of the schema.
59
+ _shape: T;
60
+
61
+ //// System metadata about the schema
62
+ meta: { entityType: string; version: string };
63
+
64
+ //// Methods for schema validation
65
+ schemaValidator: (entity: Infer<T>) => boolean;
66
+
67
+ // Validate an input object against the schema
68
+ validate: (
69
+ input: unknown,
70
+ existing?: Record<string, any>
71
+ ) => ValidationResult<Infer<T>>;
72
+
73
+ // Validate an input object against the schema, with options for composition validation
74
+ validateComposition: (
75
+ entity: Infer<T>,
76
+ options: ValidateCompositionOptions
77
+ ) => Promise<void>;
78
+
79
+ //// Optional methods for schema metadata
80
+
81
+ // Get the tableName for this schema
82
+ tableName?: () => string | undefined; // Optional method to get the table name
83
+
84
+ //// 🔒 Optional methods for PII handling
85
+
86
+ // 🔒 Auto-prepare for read (decrypt PII)
87
+ prepareForRead(
88
+ stored: Record<string, any>,
89
+ decryptFn: (value: string) => any | null
90
+ ): Record<string, any>;
91
+
92
+ // 🔒 Auto-prepare for storage (encrypt/hash PII)
93
+ prepareForStorage(
94
+ input: Record<string, any>,
95
+ encryptFn: (value: any) => string,
96
+ hashFn: (value: any) => string
97
+ ): Record<string, any>;
98
+
99
+ // 🔒 Sanitize data for logging (e.g., redact PII)
100
+ sanitizeForLog(
101
+ data: Record<string, any>,
102
+ pseudonymFn: (value: any) => string
103
+ ): Record<string, any>;
104
+
105
+ // 🔒 Get PII audit information
106
+ getPiiAudit(): Array<{
107
+ field: string;
108
+ classification: PIIClassification;
109
+ action: PIIAction;
110
+ logHandling?: PIILogHandling;
111
+ purpose?: string;
112
+ }> | null;
113
+
114
+ // 🔒 Scrub PII for deletion (e.g., clear or hash sensitive data)
115
+ scrubPiiForDelete(stored: Record<string, any>): Record<string, any>;
116
+
117
+ describe(): {
118
+ entityType: string;
119
+ version: string;
120
+ shape: Record<
121
+ string,
122
+ {
123
+ type: FieldType;
124
+ optional: boolean;
125
+ immutable: boolean;
126
+ description: string;
127
+ version: string;
128
+ deprecated: boolean;
129
+ deprecatedVersion: string | null;
130
+ system: boolean;
131
+ enum: string[] | null;
132
+ refType: string | null;
133
+ pii: PII | null;
134
+ }
135
+ >;
136
+ };
137
+ }
138
+
139
+ export type DeepReadonly<T> = {
140
+ readonly [K in keyof T]: T[K] extends object
141
+ ? T[K] extends (...args: any[]) => any
142
+ ? T[K]
143
+ : DeepReadonly<T[K]>
144
+ : T[K];
145
+ };
146
+
147
+ export interface ValidationResult<T> {
148
+ valid: boolean;
149
+ value?: DeepReadonly<T>;
150
+ errors?: string[];
151
+ }
152
+
153
+ export type RefEntityId<T extends string = string> = {
154
+ type: T;
155
+ id: string;
156
+ };
@@ -0,0 +1,256 @@
1
+ export const isoCountryCodes = new Set([
2
+ "AD",
3
+ "AE",
4
+ "AF",
5
+ "AG",
6
+ "AI",
7
+ "AL",
8
+ "AM",
9
+ "AO",
10
+ "AQ",
11
+ "AR",
12
+ "AS",
13
+ "AT",
14
+ "AU",
15
+ "AW",
16
+ "AX",
17
+ "AZ",
18
+ "BA",
19
+ "BB",
20
+ "BD",
21
+ "BE",
22
+ "BF",
23
+ "BG",
24
+ "BH",
25
+ "BI",
26
+ "BJ",
27
+ "BL",
28
+ "BM",
29
+ "BN",
30
+ "BO",
31
+ "BQ",
32
+ "BR",
33
+ "BS",
34
+ "BT",
35
+ "BV",
36
+ "BW",
37
+ "BY",
38
+ "BZ",
39
+ "CA",
40
+ "CC",
41
+ "CD",
42
+ "CF",
43
+ "CG",
44
+ "CH",
45
+ "CI",
46
+ "CK",
47
+ "CL",
48
+ "CM",
49
+ "CN",
50
+ "CO",
51
+ "CR",
52
+ "CU",
53
+ "CV",
54
+ "CW",
55
+ "CX",
56
+ "CY",
57
+ "CZ",
58
+ "DE",
59
+ "DJ",
60
+ "DK",
61
+ "DM",
62
+ "DO",
63
+ "DZ",
64
+ "EC",
65
+ "EE",
66
+ "EG",
67
+ "EH",
68
+ "ER",
69
+ "ES",
70
+ "ET",
71
+ "FI",
72
+ "FJ",
73
+ "FM",
74
+ "FO",
75
+ "FR",
76
+ "GA",
77
+ "GB",
78
+ "GD",
79
+ "GE",
80
+ "GF",
81
+ "GG",
82
+ "GH",
83
+ "GI",
84
+ "GL",
85
+ "GM",
86
+ "GN",
87
+ "GP",
88
+ "GQ",
89
+ "GR",
90
+ "GT",
91
+ "GU",
92
+ "GW",
93
+ "GY",
94
+ "HK",
95
+ "HM",
96
+ "HN",
97
+ "HR",
98
+ "HT",
99
+ "HU",
100
+ "ID",
101
+ "IE",
102
+ "IL",
103
+ "IM",
104
+ "IN",
105
+ "IO",
106
+ "IQ",
107
+ "IR",
108
+ "IS",
109
+ "IT",
110
+ "JE",
111
+ "JM",
112
+ "JO",
113
+ "JP",
114
+ "KE",
115
+ "KG",
116
+ "KH",
117
+ "KI",
118
+ "KM",
119
+ "KN",
120
+ "KP",
121
+ "KR",
122
+ "KW",
123
+ "KY",
124
+ "KZ",
125
+ "LA",
126
+ "LB",
127
+ "LC",
128
+ "LI",
129
+ "LK",
130
+ "LR",
131
+ "LS",
132
+ "LT",
133
+ "LU",
134
+ "LV",
135
+ "LY",
136
+ "MA",
137
+ "MC",
138
+ "MD",
139
+ "ME",
140
+ "MF",
141
+ "MG",
142
+ "MH",
143
+ "MK",
144
+ "ML",
145
+ "MM",
146
+ "MN",
147
+ "MO",
148
+ "MP",
149
+ "MQ",
150
+ "MR",
151
+ "MS",
152
+ "MT",
153
+ "MU",
154
+ "MV",
155
+ "MW",
156
+ "MX",
157
+ "MY",
158
+ "MZ",
159
+ "NA",
160
+ "NC",
161
+ "NE",
162
+ "NF",
163
+ "NG",
164
+ "NI",
165
+ "NL",
166
+ "NO",
167
+ "NP",
168
+ "NR",
169
+ "NU",
170
+ "NZ",
171
+ "OM",
172
+ "PA",
173
+ "PE",
174
+ "PF",
175
+ "PG",
176
+ "PH",
177
+ "PK",
178
+ "PL",
179
+ "PM",
180
+ "PN",
181
+ "PR",
182
+ "PT",
183
+ "PW",
184
+ "PY",
185
+ "QA",
186
+ "RE",
187
+ "RO",
188
+ "RS",
189
+ "RU",
190
+ "RW",
191
+ "SA",
192
+ "SB",
193
+ "SC",
194
+ "SD",
195
+ "SE",
196
+ "SG",
197
+ "SH",
198
+ "SI",
199
+ "SJ",
200
+ "SK",
201
+ "SL",
202
+ "SM",
203
+ "SN",
204
+ "SO",
205
+ "SR",
206
+ "SS",
207
+ "ST",
208
+ "SV",
209
+ "SX",
210
+ "SY",
211
+ "SZ",
212
+ "TC",
213
+ "TD",
214
+ "TF",
215
+ "TG",
216
+ "TH",
217
+ "TJ",
218
+ "TK",
219
+ "TL",
220
+ "TM",
221
+ "TN",
222
+ "TO",
223
+ "TR",
224
+ "TT",
225
+ "TV",
226
+ "TZ",
227
+ "UA",
228
+ "UG",
229
+ "UM",
230
+ "US",
231
+ "UY",
232
+ "UZ",
233
+ "VA",
234
+ "VC",
235
+ "VE",
236
+ "VG",
237
+ "VI",
238
+ "VN",
239
+ "VU",
240
+ "WF",
241
+ "WS",
242
+ "YE",
243
+ "YT",
244
+ "ZA",
245
+ "ZM",
246
+ "ZW",
247
+ ]);
248
+
249
+ /**
250
+ * Validates whether a string is a valid ISO 3166-1 alpha-2 country code.
251
+ * Performs a case-insensitive lookup against a predefined set of known codes.
252
+ */
253
+ export const validateCountryCode = (value: unknown): boolean => {
254
+ if (typeof value !== "string") return false;
255
+ return isoCountryCodes.has(value.toUpperCase());
256
+ };
@@ -0,0 +1,191 @@
1
+ export const isoCurrencyCodes = new Set([
2
+ "AED",
3
+ "AFN",
4
+ "ALL",
5
+ "AMD",
6
+ "ANG",
7
+ "AOA",
8
+ "ARS",
9
+ "AUD",
10
+ "AWG",
11
+ "AZN",
12
+ "BAM",
13
+ "BBD",
14
+ "BDT",
15
+ "BGN",
16
+ "BHD",
17
+ "BIF",
18
+ "BMD",
19
+ "BND",
20
+ "BOB",
21
+ "BOV",
22
+ "BRL",
23
+ "BSD",
24
+ "BTN",
25
+ "BWP",
26
+ "BYN",
27
+ "BZD",
28
+ "CAD",
29
+ "CDF",
30
+ "CHE",
31
+ "CHF",
32
+ "CHW",
33
+ "CLF",
34
+ "CLP",
35
+ "CNY",
36
+ "COP",
37
+ "COU",
38
+ "CRC",
39
+ "CUC",
40
+ "CUP",
41
+ "CVE",
42
+ "CZK",
43
+ "DJF",
44
+ "DKK",
45
+ "DOP",
46
+ "DZD",
47
+ "EGP",
48
+ "ERN",
49
+ "ETB",
50
+ "EUR",
51
+ "FJD",
52
+ "FKP",
53
+ "GBP",
54
+ "GEL",
55
+ "GHS",
56
+ "GIP",
57
+ "GMD",
58
+ "GNF",
59
+ "GTQ",
60
+ "GYD",
61
+ "HKD",
62
+ "HNL",
63
+ "HRK",
64
+ "HTG",
65
+ "HUF",
66
+ "IDR",
67
+ "ILS",
68
+ "INR",
69
+ "IQD",
70
+ "IRR",
71
+ "ISK",
72
+ "JMD",
73
+ "JOD",
74
+ "JPY",
75
+ "KES",
76
+ "KGS",
77
+ "KHR",
78
+ "KMF",
79
+ "KPW",
80
+ "KRW",
81
+ "KWD",
82
+ "KYD",
83
+ "KZT",
84
+ "LAK",
85
+ "LBP",
86
+ "LKR",
87
+ "LRD",
88
+ "LSL",
89
+ "LYD",
90
+ "MAD",
91
+ "MDL",
92
+ "MGA",
93
+ "MKD",
94
+ "MMK",
95
+ "MNT",
96
+ "MOP",
97
+ "MRU",
98
+ "MUR",
99
+ "MVR",
100
+ "MWK",
101
+ "MXN",
102
+ "MXV",
103
+ "MYR",
104
+ "MZN",
105
+ "NAD",
106
+ "NGN",
107
+ "NIO",
108
+ "NOK",
109
+ "NPR",
110
+ "NZD",
111
+ "OMR",
112
+ "PAB",
113
+ "PEN",
114
+ "PGK",
115
+ "PHP",
116
+ "PKR",
117
+ "PLN",
118
+ "PYG",
119
+ "QAR",
120
+ "RON",
121
+ "RSD",
122
+ "RUB",
123
+ "RWF",
124
+ "SAR",
125
+ "SBD",
126
+ "SCR",
127
+ "SDG",
128
+ "SEK",
129
+ "SGD",
130
+ "SHP",
131
+ "SLL",
132
+ "SOS",
133
+ "SRD",
134
+ "SSP",
135
+ "STN",
136
+ "SVC",
137
+ "SYP",
138
+ "SZL",
139
+ "THB",
140
+ "TJS",
141
+ "TMT",
142
+ "TND",
143
+ "TOP",
144
+ "TRY",
145
+ "TTD",
146
+ "TWD",
147
+ "TZS",
148
+ "UAH",
149
+ "UGX",
150
+ "USD",
151
+ "USN",
152
+ "UYI",
153
+ "UYU",
154
+ "UYW",
155
+ "UZS",
156
+ "VES",
157
+ "VND",
158
+ "VUV",
159
+ "WST",
160
+ "XAF",
161
+ "XAG",
162
+ "XAU",
163
+ "XBA",
164
+ "XBB",
165
+ "XBC",
166
+ "XBD",
167
+ "XCD",
168
+ "XDR",
169
+ "XOF",
170
+ "XPD",
171
+ "XPF",
172
+ "XPT",
173
+ "XSU",
174
+ "XTS",
175
+ "XUA",
176
+ "XXX",
177
+ "YER",
178
+ "ZAR",
179
+ "ZMW",
180
+ "ZWL",
181
+ ]);
182
+
183
+
184
+ /**
185
+ * Validates whether a string is a valid ISO 4217 currency code.
186
+ * Performs a case-insensitive lookup against a predefined set of known codes.
187
+ */
188
+ export const validateCurrencyCode = (value: unknown): boolean => {
189
+ if (typeof value !== "string") return false;
190
+ return isoCurrencyCodes.has(value.toUpperCase());
191
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Validates whether a string is a properly formatted ISO 8601 datetime.
3
+ * Ensures the string parses as a valid Date and matches the canonical toISOString() format.
4
+ */
5
+ export const validateDateTimeISO = (value: unknown): boolean => {
6
+ if (typeof value !== "string") return false;
7
+ const date = new Date(value);
8
+ return !isNaN(date.getTime()) && value === date.toISOString();
9
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Validates an email string using a simplified RFC 5322-compliant regex.
3
+ * Returns true if the input is a string in the format `name@domain.tld`.
4
+ */
5
+ export const validateEmail = (value: unknown): boolean => {
6
+ if (typeof value !== "string") return false;
7
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8
+ return emailRegex.test(value);
9
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Validates that a text field is safe for storage (per OWASP guidelines).
3
+ * Applies to general text fields: names, descriptions, titles, etc.
4
+ * Global Standard: OWASP Input Validation Cheat Sheet (2024)
5
+ */
6
+ export function validateSafeText(value: unknown): boolean {
7
+ if (typeof value !== "string") return false;
8
+
9
+ // Trimmed version should not be empty
10
+ const trimmed = value.trim();
11
+ if (trimmed.length === 0) return false;
12
+
13
+ // Reject control chars
14
+ for (let i = 0; i < trimmed.length; i++) {
15
+ const code = trimmed.codePointAt(i);
16
+ if (code !== undefined && (code >= 0x00 && code <= 0x1F || code === 0x7F)) {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ // Reject dangerous characters
22
+ if (/['"<>\\{}();]/.test(trimmed)) return false;
23
+
24
+ // Reject SQL-style injection patterns
25
+ if (
26
+ /(--|\b(SELECT|UPDATE|DELETE|INSERT|DROP|ALTER|EXEC|UNION|GRANT|REVOKE)\b|\/\*|\*\/|@@)/i.test(
27
+ trimmed
28
+ )
29
+ )
30
+ return false;
31
+
32
+ // Reject null char
33
+ if (trimmed.includes("\u0000")) return false;
34
+
35
+ // Optional: limit length
36
+ if (trimmed.length > 1024) return false;
37
+
38
+ return true;
39
+ }
@@ -0,0 +1,13 @@
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";
@@ -0,0 +1,25 @@
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
+ }
@@ -0,0 +1,8 @@
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
+ }