@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,213 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
-
3
- // --- Mock FieldBuilder so we can assert how field.ts wires it up ---
4
- // We mock the *dependency module* that field.ts imports: "./field.builder.js"
5
- vi.mock("../src/field.builder.js", () => {
6
- class MockFieldBuilder<T = unknown> {
7
- public type: string;
8
- public options?: Record<string, unknown>;
9
- public calls: { method: string; args: any[] }[] = [];
10
-
11
- constructor(type: string, options?: Record<string, unknown>) {
12
- this.type = type;
13
- this.options = options;
14
- }
15
-
16
- validator(fn: (value: unknown) => boolean) {
17
- this.calls.push({ method: "validator", args: [fn] });
18
- return this;
19
- }
20
-
21
- PID(cfg: Record<string, unknown>) {
22
- this.calls.push({ method: "PID", args: [cfg] });
23
- return this;
24
- }
25
-
26
- min(n: number) {
27
- this.calls.push({ method: "min", args: [n] });
28
- return this;
29
- }
30
-
31
- max(n: number) {
32
- this.calls.push({ method: "max", args: [n] });
33
- return this;
34
- }
35
-
36
- description(text: string) {
37
- this.calls.push({ method: "description", args: [text] });
38
- return this;
39
- }
40
- }
41
-
42
- return { FieldBuilder: MockFieldBuilder };
43
- });
44
-
45
- // Import after the mock so field.ts uses our mocked FieldBuilder
46
- import { field } from "../src/field";
47
-
48
- // Small helper to fetch a recorded call by method name
49
- const getCall = (fb: any, method: string) =>
50
- fb.calls.find((c: any) => c.method === method);
51
-
52
- // Helper to run the first validator that was attached and return its boolean result
53
- const runFirstValidator = (fb: any, value: unknown) => {
54
- const call = getCall(fb, "validator");
55
- if (!call) throw new Error("No validator attached");
56
- const fn = call.args[0] as (v: unknown) => boolean;
57
- return fn(value);
58
- };
59
-
60
- // --- Tests ---
61
-
62
- describe("field factory basics", () => {
63
- it("string/number/boolean create FieldBuilder of correct type", () => {
64
- const s = field.string();
65
- const n = field.number();
66
- const b = field.boolean();
67
-
68
- expect(s.type).toBe("string");
69
- expect(n.type).toBe("number");
70
- expect(b.type).toBe("boolean");
71
- });
72
- });
73
-
74
- describe("validators: email/phone/url/uuid", () => {
75
- it("email wires PID, validator, and description and validates", () => {
76
- const fb = field.email() as any;
77
- expect(fb.type).toBe("string");
78
-
79
- // PID + description were set
80
- expect(getCall(fb, "PID").args[0]).toMatchObject({
81
- classification: "high",
82
- action: "encrypt",
83
- });
84
- expect(getCall(fb, "description").args[0]).toMatch(/email address/i);
85
-
86
- // Validator behaves sensibly
87
- expect(runFirstValidator(fb, "user@example.com")).toBe(true);
88
- expect(runFirstValidator(fb, "not-an-email")).toBe(false);
89
- });
90
-
91
- it("phone wires PID and validates", () => {
92
- const fb = field.phone() as any;
93
- expect(getCall(fb, "PID").args[0]).toMatchObject({
94
- classification: "high",
95
- action: "encrypt",
96
- });
97
- expect(getCall(fb, "description").args[0]).toMatch(/phone number/i);
98
- expect(runFirstValidator(fb, "+442079460018")).toBe(true);
99
- expect(runFirstValidator(fb, "123")).toBe(false);
100
- expect(runFirstValidator(fb, "123 123 123")).toBe(false);
101
- expect(runFirstValidator(fb, "+441234567890123456")).toBe(false);
102
- });
103
-
104
- it("url wires PID and validates", () => {
105
- const fb = field.url() as any;
106
- expect(getCall(fb, "PID").args[0]).toMatchObject({
107
- classification: "low",
108
- action: "hash",
109
- });
110
- expect(getCall(fb, "description").args[0]).toMatch(/url/i);
111
- expect(runFirstValidator(fb, "https://example.com/path")).toBe(true);
112
- expect(runFirstValidator(fb, "ht!tp://bad-url")).toBe(false);
113
- });
114
-
115
- it("uuid wires PID and validates", () => {
116
- const fb = field.uuid() as any;
117
- expect(getCall(fb, "PID").args[0]).toMatchObject({
118
- classification: "low",
119
- action: "hash",
120
- });
121
- expect(getCall(fb, "description").args[0]).toMatch(/uuid/i);
122
- expect(runFirstValidator(fb, "550e8400-e29b-41d4-a716-446655440000")).toBe(
123
- true
124
- );
125
- expect(runFirstValidator(fb, "not-a-uuid")).toBe(false);
126
- });
127
- });
128
-
129
- describe("date/time validators", () => {
130
- it("dateTimeISO validates full ISO 8601 date-time", () => {
131
- const fb = field.dateTimeISO() as any;
132
- expect(getCall(fb, "description").args[0]).toMatch(/ISO 8601/);
133
- expect(runFirstValidator(fb, "2023-10-05T14:30:00Z")).toBe(true);
134
- expect(runFirstValidator(fb, "2023-13-02T23:04:05Z")).toBe(false);
135
- expect(runFirstValidator(fb, "2023-11-02T28:04:05Z")).toBe(false);
136
- });
137
-
138
- it("dateISO (date only) validates", () => {
139
- const fb = field.dateISO() as any;
140
- expect(getCall(fb, "description").args[0]).toMatch(/date only/i);
141
- expect(runFirstValidator(fb, "2024-02-29")).toBe(true); // leap day valid
142
- expect(runFirstValidator(fb, "2024-02-30")).toBe(false);
143
- });
144
-
145
- it("timeISO (time only) validates", () => {
146
- const fb = field.timeISO() as any;
147
- expect(getCall(fb, "description").args[0]).toMatch(/time only/i);
148
- expect(runFirstValidator(fb, "23:59:59")).toBe(true);
149
- expect(runFirstValidator(fb, "24:00:00")).toBe(false);
150
- });
151
- });
152
-
153
- describe("text validators", () => {
154
- it("richText uses validateRichText and marks lower sensitivity", () => {
155
- const fb = field.richText() as any;
156
- expect(getCall(fb, "PID").args[0]).toMatchObject({
157
- classification: "low",
158
- action: "clear",
159
- });
160
- expect(runFirstValidator(fb, "<p>Hello</p>")).toBe(true);
161
- expect(runFirstValidator(fb, "<script>alert('x')</script>")).toBe(false);
162
- });
163
-
164
- it("generalText uses validateSafeText", () => {
165
- const fb = field.generalText() as any;
166
- expect(getCall(fb, "PID").args[0]).toMatchObject({
167
- classification: "none",
168
- action: "none",
169
- });
170
- expect(runFirstValidator(fb, "Just a sentence.")).toBe(true);
171
- expect(runFirstValidator(fb, "<b>Not allowed</b>")).toBe(false);
172
- });
173
- });
174
-
175
- describe("geo & versioning", () => {
176
- it("latitude enforces -90..90", () => {
177
- const fb = field.latitude() as any;
178
- expect(getCall(fb, "min").args[0]).toBe(-90);
179
- expect(getCall(fb, "max").args[0]).toBe(90);
180
- expect(getCall(fb, "description").args[0]).toMatch(/WGS 84/);
181
- });
182
-
183
- it("longitude enforces -180..180", () => {
184
- const fb = field.longitude() as any;
185
- expect(getCall(fb, "min").args[0]).toBe(-180);
186
- expect(getCall(fb, "max").args[0]).toBe(180);
187
- expect(getCall(fb, "description").args[0]).toMatch(/WGS 84/);
188
- });
189
-
190
- it("version uses semver validator", () => {
191
- const fb = field.version() as any;
192
- expect(getCall(fb, "description").args[0]).toMatch(/semantic version/i);
193
- expect(runFirstValidator(fb, "1.2.3")).toBe(true);
194
- expect(runFirstValidator(fb, "1.2")).toBe(false);
195
- });
196
- });
197
-
198
- describe("codes: country & language", () => {
199
- it("countryCode uses ISO 3166 validator", () => {
200
- const fb = field.countryCode() as any;
201
- expect(getCall(fb, "description").args[0]).toMatch(/ISO 3166/i);
202
- expect(runFirstValidator(fb, "GB")).toBe(true);
203
- expect(runFirstValidator(fb, "ZZ")).toBe(false);
204
- });
205
-
206
- it("languageCode uses BCP 47 validator", () => {
207
- const fb = field.languageCode() as any;
208
- expect(getCall(fb, "description").args[0]).toMatch(/BCP 47/i);
209
- expect(runFirstValidator(fb, "en")).toBe(true);
210
- expect(runFirstValidator(fb, "en-GB")).toBe(true);
211
- expect(runFirstValidator(fb, "english-UK")).toBe(false);
212
- });
213
- });
package/tests/pii.test.ts DELETED
@@ -1,139 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { createSchema } from "../src/schema.js";
3
- import { field } from "../src/field.js";
4
- import { expectValid, expectInvalid } from "./test-utils.js";
5
-
6
- // --- PII: sanitizeForLog should clean output correctly ---
7
- describe("PII log sanitization", () => {
8
- const LogSchema = createSchema(
9
- {
10
- email: field.string().PID({
11
- classification: "high",
12
- action: "encrypt",
13
- logHandling: "redact",
14
- purpose: "user contact",
15
- }),
16
- sessionId: field.string().PID({
17
- classification: "low",
18
- action: "hash",
19
- logHandling: "pseudonym",
20
- purpose: "session correlation",
21
- }),
22
- nickname: field.string().PID({
23
- classification: "none",
24
- action: "none",
25
- logHandling: "plain",
26
- }),
27
- internalNote: field.string().PID({
28
- classification: "low",
29
- action: "clear",
30
- logHandling: "omit",
31
- purpose: "ops-only",
32
- }),
33
- },
34
- "LogSchema",
35
- { version: "1.0.0", piiEnforcement: "strict" }
36
- );
37
-
38
- it("should redact, pseudonymize, pass-through, and omit according to logHandling", () => {
39
- const input = {
40
- email: "a@b.com",
41
- sessionId: "abc123",
42
- nickname: "phil",
43
- internalNote: "sensitive",
44
- other: "visible-but-not-in-shape",
45
- } as const;
46
-
47
- const pseudo = (v: any) => `pseudo(${String(v).length})`;
48
- const out = LogSchema.sanitizeForLog(input as any, pseudo);
49
-
50
- // redact
51
- expect(out.email).toBe("[REDACTED]");
52
- // pseudonym
53
- expect(out.sessionId).toBe("pseudo(6)");
54
- // plain
55
- expect(out.nickname).toBe("phil");
56
- // omitted entirely
57
- expect(Object.prototype.hasOwnProperty.call(out, "internalNote")).toBe(
58
- false
59
- );
60
- // unknown fields are not included (shape-driven)
61
- expect(Object.prototype.hasOwnProperty.call(out, "other")).toBe(false);
62
-
63
- // ensure no storage artifacts leak into logs
64
- expect(Object.keys(out).some((k) => k.endsWith("Encrypted"))).toBe(false);
65
- expect(Object.keys(out).some((k) => k.endsWith("Hash"))).toBe(false);
66
- });
67
-
68
- it("should handle missing optional fields by simply excluding them from sanitized output", () => {
69
- const minimal = { email: "x@y.com", sessionId: "id" } as const;
70
- const out = LogSchema.sanitizeForLog(minimal as any, (v) => `p(${v})`);
71
- expect(out.email).toBe("[REDACTED]");
72
- expect(out.sessionId).toBe("p(id)");
73
- // nickname/internalNote omitted if not present
74
- expect(out.nickname).toBe(undefined);
75
- expect(Object.prototype.hasOwnProperty.call(out, "internalNote")).toBe(
76
- false
77
- );
78
- });
79
-
80
- // --- PII: auditing of shape metadata ---
81
- describe("PII auditing", () => {
82
- it("getPiiAudit should include only fields with classification !== 'none' and capture action/logHandling/purpose", () => {
83
- const audit = LogSchema.getPiiAudit();
84
- expect(audit).toEqual([
85
- {
86
- field: "email",
87
- classification: "high",
88
- action: "encrypt",
89
- logHandling: "redact",
90
- purpose: "user contact",
91
- },
92
- {
93
- field: "sessionId",
94
- classification: "low",
95
- action: "hash",
96
- logHandling: "pseudonym",
97
- purpose: "session correlation",
98
- },
99
- {
100
- field: "internalNote",
101
- classification: "low",
102
- action: "clear",
103
- logHandling: "omit",
104
- purpose: "ops-only",
105
- },
106
- ]);
107
- });
108
-
109
- it("getPiiAudit should not include fields with classification 'none' (e.g., nickname)", () => {
110
- const audit = LogSchema.getPiiAudit();
111
- const fields = audit?.map((a) => a.field);
112
- expect(fields?.includes("nickname")).toBe(false);
113
- expect(fields).toEqual(["email", "sessionId", "internalNote"]);
114
- });
115
-
116
- it("audit content should be independent of enforcement mode", () => {
117
- const AltSchema = createSchema(
118
- {
119
- email: field.string().PID({
120
- classification: "high",
121
- action: "encrypt",
122
- logHandling: "redact",
123
- }),
124
- },
125
- "Alt",
126
- { version: "1.0.0", piiEnforcement: "none" }
127
- );
128
- expect(AltSchema.getPiiAudit()).toEqual([
129
- {
130
- field: "email",
131
- classification: "high",
132
- action: "encrypt",
133
- logHandling: "redact",
134
- purpose: undefined,
135
- },
136
- ]);
137
- });
138
- });
139
- });