@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/tests/fields.test.ts
DELETED
|
@@ -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
|
-
});
|