@carecard/validate 3.36.0 → 3.38.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.
- package/.agents/skills/carecard-api-contract/SKILL.md +101 -0
- package/.agents/skills/pkg-validate-coding-standards-and-best-practices/SKILL.md +9 -0
- package/.agents/skills/pkg-validate-validation-library/SKILL.md +17 -0
- package/index.d.ts +11 -2
- package/lib/validate.js +67 -4
- package/lib/validateProperties.js +29 -10
- package/package.json +2 -2
- package/readme.md +37 -6
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: carecard-api-contract
|
|
3
|
+
description: 'Mandatory for every task after carecard-must-do and the repository engineering standards. Maintain non-negotiable API conformity across server calls, direct browser uploads and messaging, seed data, mocks, tests, and documentation.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CareCard API Contract
|
|
7
|
+
|
|
8
|
+
## Mandatory Use
|
|
9
|
+
|
|
10
|
+
Every agent must read and apply this skill on every task, including planning,
|
|
11
|
+
read-only investigation, coding, review, tests, documentation, and skill work.
|
|
12
|
+
Load `carecard-must-do` first, then the repository engineering standards, then
|
|
13
|
+
this skill before narrower skills. Use this repository's own copy; no test,
|
|
14
|
+
hook, build, or runtime dependency on another checkout is permitted.
|
|
15
|
+
|
|
16
|
+
## Non-Negotiable API Conformity
|
|
17
|
+
|
|
18
|
+
The authoritative API contracts are the implemented request and response
|
|
19
|
+
boundaries between `ms-*` and `app-*` server components, called through the
|
|
20
|
+
`app-*` service layer. The implemented direct browser contracts for file
|
|
21
|
+
uploads and messaging are also authoritative.
|
|
22
|
+
|
|
23
|
+
Seed data in `cc-seed-data`, mock data and test data in every `app-*` repository,
|
|
24
|
+
API documentation, E2E test data, helpers, examples, and every other place
|
|
25
|
+
where these contracts are represented must always use and conform to those
|
|
26
|
+
authoritative contracts. A mock, seed, test, document, or example must never
|
|
27
|
+
redefine an API contract. This requirement is non-negotiable.
|
|
28
|
+
|
|
29
|
+
Every field in every non-authoritative request and response must have the same
|
|
30
|
+
data type and nullability as its authoritative boundary. Preserve UUID string
|
|
31
|
+
formats, timestamp and date-only string formats, booleans, numbers, arrays,
|
|
32
|
+
objects, and explicit null values. Preserve endpoint-specific numeric strings
|
|
33
|
+
when the owner returns them, including PostgreSQL BIGINT message sequences;
|
|
34
|
+
do not substitute numbers just because an app transform later parses them.
|
|
35
|
+
|
|
36
|
+
## Determine the Contract Before Changing Representations
|
|
37
|
+
|
|
38
|
+
- Trace each request through the application service query, validation, and
|
|
39
|
+
transform to the owning microservice route, controller, and response writer.
|
|
40
|
+
Inspect database/model behavior when it determines the returned data.
|
|
41
|
+
- Compare method, path, authentication, parameters, headers, body fields,
|
|
42
|
+
casing, types, identifiers, required/optional/null values, status codes,
|
|
43
|
+
response data, errors, envelope metadata, and pagination.
|
|
44
|
+
- Distinguish raw HTTP data from transformed application objects and relational
|
|
45
|
+
seed rows. Seed rows must satisfy the owning schema and produce conforming
|
|
46
|
+
API results; they are not HTTP response envelopes.
|
|
47
|
+
- Preserve intentional mock-only controls as explicit development seams. They
|
|
48
|
+
do not authorize different production API payloads or outcomes.
|
|
49
|
+
- For direct browser uploads, trace the application-owned intent request and
|
|
50
|
+
the byte upload with its one-use bearer credential. Preserve the actual
|
|
51
|
+
multipart body, response, and failure contract.
|
|
52
|
+
- For direct browser messaging, trace application-owned ticket issuance,
|
|
53
|
+
handshake authentication, commands, acknowledgements, events, and recovery.
|
|
54
|
+
Preserve endpoint-specific formats; do not wrap binary content, bodyless
|
|
55
|
+
responses, tickets, or realtime acknowledgements in a generic JSON envelope.
|
|
56
|
+
|
|
57
|
+
## Repair and Maintain All Affected Representations
|
|
58
|
+
|
|
59
|
+
- Fix verified drift at its originating input, generator, mock factory, or
|
|
60
|
+
fixture and update every dependent representation in the same task.
|
|
61
|
+
- Edit seed facts/generators in `cc-seed-data`; regenerate ignored outputs
|
|
62
|
+
rather than patching generated dumps or creating editable seed copies.
|
|
63
|
+
- Reuse existing service types and repository-native helpers. Do not add
|
|
64
|
+
compatibility aliases, alternate shapes, a parallel schema framework, or
|
|
65
|
+
runtime cross-repository imports to conceal a mismatch.
|
|
66
|
+
- When an authoritative contract intentionally evolves, identify and update
|
|
67
|
+
every affected seed, mock, test, example, API document, E2E scenario, and skill.
|
|
68
|
+
Documentation inventories remain derived evidence, not a new authority.
|
|
69
|
+
- Keep equivalent copies of this skill aligned and preserve repository-local
|
|
70
|
+
ownership. Do not infer authorization to publish, deploy, or reset data.
|
|
71
|
+
|
|
72
|
+
## Behavioral Validation
|
|
73
|
+
|
|
74
|
+
Use TDD for executable changes: establish a failing observable request,
|
|
75
|
+
response, event, or persisted-data assertion before implementation. Exercise
|
|
76
|
+
public callable boundaries; do not test source text or internal structure.
|
|
77
|
+
Cover affected success and failure outcomes, and run the validation required
|
|
78
|
+
by the current task and the owning repository. Validate documentation and
|
|
79
|
+
skill changes with focused non-test checks. Never claim an unrun check passed.
|
|
80
|
+
|
|
81
|
+
## Shared Engineering Requirements
|
|
82
|
+
|
|
83
|
+
Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits, run the relevant focused non-test validation before changing the prose; do not add automated tests that inspect prose, files, or repository structure.
|
|
84
|
+
|
|
85
|
+
Non-negotiable repository isolation rule: Every repository must run its Husky hooks and tests using only files, code, fixtures, dependencies, and services contained within that repository. Tests and Husky scripts must not import, require, read, execute, or otherwise depend on sibling repositories or paths outside the repository root. app-e2e-tests is the only exception because cross-repository end-to-end testing is its explicit responsibility.
|
|
86
|
+
|
|
87
|
+
Non-negotiable code organization rule: Functions with the same or equivalent behavior must use the same or clearly corresponding descriptive names across CareCard repositories, and equivalent functionality must live in files with the same names within each repository's established architecture. No backward compatibility names, aliases, or duplicate locations are allowed.
|
|
88
|
+
|
|
89
|
+
### Non-Negotiable Microservice Layer Testing Contract
|
|
90
|
+
|
|
91
|
+
For every `ms-*` microservice, each applicable testing layer below is mandatory. In this contract, testing a function directly means invoking its documented callable boundary with controlled inputs. It never means testing its internals or other implementation details directly. Every function under test must be treated as a black box.
|
|
92
|
+
|
|
93
|
+
- Every database function and every stable database-access function must be exercised through its callable database boundary against the repository-owned test database. Place this coverage in `test/db/<related-model>[.<behavior>].test.<ext>`.
|
|
94
|
+
- Every model function that reads or changes database data must be exercised through its callable model interface. Place this coverage in `test/<sub-app>/model.test.<ext>` or a more specific model test file in the same directory.
|
|
95
|
+
- Every controller function used by the application must be exercised through its callable controller interface with controlled request, response, and error inputs. Place this coverage in `test/<sub-app>/controller.test.<ext>` or a more specific controller test file in the same directory.
|
|
96
|
+
- Every application route must be exercised with real HTTP requests against the composed application mounted on a repository-managed Node HTTP server. Place this coverage in `test/<sub-app>/app.test.<ext>`; use `test/app/app.test.<ext>` for routes without a sub-app.
|
|
97
|
+
- Every large helper function and every helper with independent domain behavior, validation, mapping, branching, state transition, object mutation, or failure behavior must have behavior coverage in `test/<sub-app>/helper.test.<ext>` or an appropriately named test file. Exercise a documented callable helper interface as a black box. Cover a private helper implementation through the callable boundary that owns it, and never export or invoke it solely for testing.
|
|
98
|
+
- Tests must provide inputs and assert observable outputs, errors, persisted state, side effects, emitted behavior, or intentional object mutation.
|
|
99
|
+
- Tests must not directly invoke, inspect, spy on, or assert implementation details, including private functions or state, SQL or query text and construction, algorithms, branches, control flow, variable names, collaborator sequences, or internal call counts.
|
|
100
|
+
- A behavior-preserving rewrite behind an unchanged callable contract must not require functional test changes.
|
|
101
|
+
- Use the repository-native test extension, such as `.js`, `.ts`, or `.mjs`. A microservice without a particular layer does not need an empty placeholder test file.
|
|
@@ -5,6 +5,15 @@ description: 'Use after carecard-must-do. Mandatory for every pkg-validate task,
|
|
|
5
5
|
|
|
6
6
|
# Pkg Validate Coding Standards And Best Practices
|
|
7
7
|
|
|
8
|
+
## Mandatory API Contract Skill
|
|
9
|
+
|
|
10
|
+
Non-negotiable: On every task, read and apply
|
|
11
|
+
[carecard-api-contract](../carecard-api-contract/SKILL.md) after
|
|
12
|
+
`carecard-must-do` and this engineering skill, before any narrower skill. This
|
|
13
|
+
applies to planning, read-only work, implementation, tests, documentation, and
|
|
14
|
+
skill maintenance. All non-authoritative representations must always conform
|
|
15
|
+
to the implemented server and direct browser API contracts.
|
|
16
|
+
|
|
8
17
|
## Release 1.1.1 and production data
|
|
9
18
|
|
|
10
19
|
CareCard Release 1.1.1 is the production baseline. Seeding with
|
|
@@ -111,6 +111,23 @@ depend on those folders being present.
|
|
|
111
111
|
|
|
112
112
|
## Validation Behavior
|
|
113
113
|
|
|
114
|
+
- `isNameString`, `isFileNameString`, and `isTextString` share a character
|
|
115
|
+
allowlist: Unicode letters, marks, numbers, space separators, dashes, emoji,
|
|
116
|
+
script/emoji joiners, and `_ . , ' ’ ( ) @ &`. Require a visible letter,
|
|
117
|
+
number, or emoji. Check the complete raw input and its NFKC representation
|
|
118
|
+
before trimming; reject controls, malformed Unicode, private-use characters,
|
|
119
|
+
tags, paths, and syntax outside the allowlist. Preserve accepted display
|
|
120
|
+
names; only filename metadata is returned NFKC-normalized and trimmed.
|
|
121
|
+
Permit only the explicitly allowlisted compatibility punctuation forms and
|
|
122
|
+
check their normalized characters too; do not admit unrelated symbols merely
|
|
123
|
+
because NFKC converts them to letters.
|
|
124
|
+
- `isSafeString` keeps its narrower ASCII subset and `isSafeSearchString`
|
|
125
|
+
keeps its existing search grammar. Neither may hide invalid edge characters
|
|
126
|
+
by trimming before validation. Use format-specific validators for passwords,
|
|
127
|
+
emails, URLs, JSON, addresses, and phones. Role-request phone snapshots use
|
|
128
|
+
`isPhoneContactString`, including an optional international dialing prefix.
|
|
129
|
+
- `isReactionString` preserves allowlisted text and bounded ASCII named tokens
|
|
130
|
+
such as `:reviewed:`. Do not use reaction-token syntax to relax human text.
|
|
114
131
|
- General low-level validators should be deterministic predicate functions that
|
|
115
132
|
return `true` or `false`.
|
|
116
133
|
- `isEmailString` is the sole CareCard email-format decision. It accepts an
|
package/index.d.ts
CHANGED
|
@@ -144,8 +144,10 @@ export const isCcIdString: BoolValidator;
|
|
|
144
144
|
export const isCharactersString: BoolValidator;
|
|
145
145
|
/** Checks if the string is a valid street address format. */
|
|
146
146
|
export const isStreetString: BoolValidator;
|
|
147
|
-
/** Checks
|
|
147
|
+
/** Checks allowlisted visible Unicode names up to 1000 UTF-16 code units, including raw input edges. */
|
|
148
148
|
export const isNameString: BoolValidator;
|
|
149
|
+
/** Checks the plain-text allowlist and NFKC-trimmed filename metadata up to 255 code points. */
|
|
150
|
+
export const isFileNameString: BoolValidator;
|
|
149
151
|
/** Checks if the string is safe for search queries. */
|
|
150
152
|
export const isSafeSearchString: BoolValidator;
|
|
151
153
|
/** Checks if the string is a valid email address. */
|
|
@@ -156,6 +158,8 @@ export const isJwtString: BoolValidator;
|
|
|
156
158
|
export const isUsernameString: BoolValidator;
|
|
157
159
|
/** Checks if the string is a valid phone number. */
|
|
158
160
|
export const isPhoneNumber: BoolValidator;
|
|
161
|
+
/** Checks national or international phone contact snapshots with at most 15 international digits. */
|
|
162
|
+
export const isPhoneContactString: BoolValidator;
|
|
159
163
|
/** Checks if the string is URL-safe. */
|
|
160
164
|
export const isUrlSafeString: BoolValidator;
|
|
161
165
|
/** Checks if the string is a valid Canadian province abbreviation (ON, QC). */
|
|
@@ -166,8 +170,10 @@ export const isBoolValue: BoolValidator;
|
|
|
166
170
|
export const isPostalCodeString: BoolValidator;
|
|
167
171
|
/** Checks if the string contains only allowed "safe" characters. */
|
|
168
172
|
export const isSafeString: BoolValidator;
|
|
169
|
-
/** Checks
|
|
173
|
+
/** Checks allowlisted visible Unicode text up to 10000 UTF-16 code units. */
|
|
170
174
|
export const isTextString: BoolValidator;
|
|
175
|
+
/** Checks allowlisted text or ASCII named reaction tokens, at most 64 trimmed code units. */
|
|
176
|
+
export const isReactionString: BoolValidator;
|
|
171
177
|
/** Checks if a string exists within a given array of strings, case-insensitive. */
|
|
172
178
|
export const isInStringArray: InStringArrayValidator;
|
|
173
179
|
/** Checks if the string is one of the supported user role request statuses. */
|
|
@@ -203,17 +209,20 @@ export const validate: {
|
|
|
203
209
|
isCharactersString: typeof isCharactersString;
|
|
204
210
|
isStreetString: typeof isStreetString;
|
|
205
211
|
isNameString: typeof isNameString;
|
|
212
|
+
isFileNameString: typeof isFileNameString;
|
|
206
213
|
isSafeSearchString: typeof isSafeSearchString;
|
|
207
214
|
isEmailString: typeof isEmailString;
|
|
208
215
|
isJwtString: typeof isJwtString;
|
|
209
216
|
isUsernameString: typeof isUsernameString;
|
|
210
217
|
isPhoneNumber: typeof isPhoneNumber;
|
|
218
|
+
isPhoneContactString: typeof isPhoneContactString;
|
|
211
219
|
isUrlSafeString: typeof isUrlSafeString;
|
|
212
220
|
isProvinceString: typeof isProvinceString;
|
|
213
221
|
isBoolValue: typeof isBoolValue;
|
|
214
222
|
isPostalCodeString: typeof isPostalCodeString;
|
|
215
223
|
isSafeString: typeof isSafeString;
|
|
216
224
|
isTextString: typeof isTextString;
|
|
225
|
+
isReactionString: typeof isReactionString;
|
|
217
226
|
isInStringArray: typeof isInStringArray;
|
|
218
227
|
isUserRoleRequestStatusString: typeof isUserRoleRequestStatusString;
|
|
219
228
|
isUserRoleRequestRoleString: typeof isUserRoleRequestRoleString;
|
package/lib/validate.js
CHANGED
|
@@ -68,18 +68,52 @@ const isStreetString = str => {
|
|
|
68
68
|
return streetRegex.test(value);
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
// Pattern: Predicate - checks every character against one Unicode plain-text allowlist.
|
|
72
|
+
const hasAllowlistedVisibleText = value => {
|
|
73
|
+
if (!value.isWellFormed()) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const normalizedValue = value.normalize('NFKC');
|
|
77
|
+
const nonAllowlistedCharacterPattern =
|
|
78
|
+
/[^\u200c\u200d\p{L}\p{M}\p{N}\p{Zs}\p{Pd}\p{Extended_Pictographic}\p{Emoji_Presentation}\p{Emoji_Modifier}_.,'’()@&]/u;
|
|
79
|
+
// These known punctuation forms are allowlisted; their NFKC output is checked below.
|
|
80
|
+
const originalCharactersToCheck = value.replace(
|
|
81
|
+
/[\u2024-\u2026\u207d\u207e\u208d\u208e\ufe10\ufe19\ufe30\ufe33-\ufe36\ufe4d-\ufe50\ufe52\ufe59\ufe5a\ufe60\ufe6b\uff06-\uff09\uff0c\uff0e\uff20\uff3f]/gu,
|
|
82
|
+
'',
|
|
83
|
+
);
|
|
84
|
+
return (
|
|
85
|
+
!nonAllowlistedCharacterPattern.test(originalCharactersToCheck) &&
|
|
86
|
+
!nonAllowlistedCharacterPattern.test(normalizedValue) &&
|
|
87
|
+
/[\p{L}\p{N}\p{Extended_Pictographic}\p{Emoji_Presentation}]/u.test(
|
|
88
|
+
normalizedValue.replace(/\p{Default_Ignorable_Code_Point}/gu, ''),
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Pattern: Predicate - validates the original name before callers trim accepted spaces.
|
|
71
94
|
const isNameString = str => {
|
|
72
95
|
if (typeof str !== 'string' || str.length === 0 || str.length > 1000) {
|
|
73
96
|
return false;
|
|
74
97
|
}
|
|
75
|
-
return
|
|
98
|
+
return hasAllowlistedVisibleText(str);
|
|
76
99
|
};
|
|
77
100
|
|
|
101
|
+
// Pattern: Predicate - validates canonical display metadata, never a filesystem path.
|
|
102
|
+
const isFileNameString = str => {
|
|
103
|
+
if (typeof str !== 'string') {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return hasAllowlistedVisibleText(str) && [...str.normalize('NFKC').trim()].length <= 255;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Pattern: Predicate - combines the text allowlist with the existing ASCII search grammar.
|
|
78
110
|
const isSafeSearchString = str => {
|
|
79
111
|
if (typeof str !== 'string' || str.length === 0) {
|
|
80
112
|
return false;
|
|
81
113
|
}
|
|
82
|
-
return
|
|
114
|
+
return (
|
|
115
|
+
hasAllowlistedVisibleText(str) && /^[A-Za-z][0-9a-zA-Z\-_.,'()@ ]{1,100}$/.test(str.trim())
|
|
116
|
+
);
|
|
83
117
|
};
|
|
84
118
|
|
|
85
119
|
const EMAIL_LOCAL_PART_PATTERN =
|
|
@@ -157,6 +191,14 @@ function isPhoneNumber(str) {
|
|
|
157
191
|
return /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(str);
|
|
158
192
|
}
|
|
159
193
|
|
|
194
|
+
// Pattern: Predicate - validates national or international contact snapshots as phone syntax.
|
|
195
|
+
const isPhoneContactString = str => {
|
|
196
|
+
if (typeof str !== 'string' || str.length === 0 || str.length > 32 || /[^+0-9(). -]/.test(str)) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return isPhoneNumber(str) || /^\+[1-9](?:[ .-]?[0-9]){6,14}$/.test(str);
|
|
200
|
+
};
|
|
201
|
+
|
|
160
202
|
const isUrlSafeString = inputString => {
|
|
161
203
|
if (
|
|
162
204
|
typeof inputString !== 'string' ||
|
|
@@ -194,15 +236,33 @@ const isPostalCodeString = inputString => {
|
|
|
194
236
|
return false;
|
|
195
237
|
};
|
|
196
238
|
|
|
239
|
+
// Pattern: Predicate - retains the stricter ASCII subset of the shared text allowlist.
|
|
197
240
|
const isSafeString = str => {
|
|
198
241
|
if (typeof str !== 'string' || str.length === 0 || str.length > 10000) {
|
|
199
242
|
return false;
|
|
200
243
|
}
|
|
201
|
-
return /^[\da-zA-Z
|
|
244
|
+
return hasAllowlistedVisibleText(str) && /^[\da-zA-Z\-_.,'() ]+$/.test(str);
|
|
202
245
|
};
|
|
203
246
|
|
|
247
|
+
// Pattern: Predicate - applies the shared allowlist within the existing text size budget.
|
|
204
248
|
const isTextString = str => {
|
|
205
|
-
return
|
|
249
|
+
return (
|
|
250
|
+
typeof str === 'string' &&
|
|
251
|
+
str.length > 0 &&
|
|
252
|
+
str.length <= 10000 &&
|
|
253
|
+
hasAllowlistedVisibleText(str)
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Pattern: Predicate - preserves bounded named reaction tokens alongside allowlisted text.
|
|
258
|
+
const isReactionString = str => {
|
|
259
|
+
if (typeof str !== 'string' || str.trim().length > 64) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return (
|
|
263
|
+
isTextString(str) ||
|
|
264
|
+
(!/[^\p{Zs}:a-zA-Z0-9_-]/u.test(str) && /^:[a-zA-Z0-9_-]+:$/.test(str.trim()))
|
|
265
|
+
);
|
|
206
266
|
};
|
|
207
267
|
|
|
208
268
|
const isInStringArray = (StringArray, inputString) => {
|
|
@@ -350,18 +410,21 @@ module.exports = {
|
|
|
350
410
|
isCharactersString,
|
|
351
411
|
isStreetString,
|
|
352
412
|
isNameString,
|
|
413
|
+
isFileNameString,
|
|
353
414
|
isSafeSearchString,
|
|
354
415
|
isEmailString,
|
|
355
416
|
isJwtString,
|
|
356
417
|
validatePassword,
|
|
357
418
|
isUsernameString,
|
|
358
419
|
isPhoneNumber,
|
|
420
|
+
isPhoneContactString,
|
|
359
421
|
isUrlSafeString,
|
|
360
422
|
isProvinceString,
|
|
361
423
|
isBoolValue,
|
|
362
424
|
isPostalCodeString,
|
|
363
425
|
isSafeString,
|
|
364
426
|
isTextString,
|
|
427
|
+
isReactionString,
|
|
365
428
|
isInStringArray,
|
|
366
429
|
isUserRoleRequestStatusString,
|
|
367
430
|
isUserRoleRequestRoleString,
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
const {
|
|
3
3
|
isEmailString,
|
|
4
4
|
isPhoneNumber,
|
|
5
|
+
isPhoneContactString,
|
|
5
6
|
isUrlSafeString,
|
|
6
7
|
isValidUuidString,
|
|
7
8
|
isCcIdString,
|
|
8
9
|
validatePassword,
|
|
9
10
|
isNameString,
|
|
10
|
-
|
|
11
|
+
isFileNameString,
|
|
11
12
|
isCharactersString,
|
|
12
13
|
isValidIntegerString,
|
|
13
14
|
isValidJsonString,
|
|
@@ -26,6 +27,7 @@ const {
|
|
|
26
27
|
isCountryCodeString,
|
|
27
28
|
} = require('./validate');
|
|
28
29
|
|
|
30
|
+
// Pattern: Boundary Validation - preserves field-specific formats while omitting invalid properties.
|
|
29
31
|
function validateProperties(obj = {}) {
|
|
30
32
|
const returnObj = {};
|
|
31
33
|
|
|
@@ -35,7 +37,14 @@ function validateProperties(obj = {}) {
|
|
|
35
37
|
case 'firstName':
|
|
36
38
|
case 'last_name':
|
|
37
39
|
case 'lastName':
|
|
38
|
-
case '
|
|
40
|
+
case 'middle_name':
|
|
41
|
+
case 'middleName':
|
|
42
|
+
case 'nick_name':
|
|
43
|
+
case 'nickName':
|
|
44
|
+
case 'requested_by_name':
|
|
45
|
+
case 'requestedByName':
|
|
46
|
+
case 'approved_by_name':
|
|
47
|
+
case 'approvedByName':
|
|
39
48
|
case 'new_status':
|
|
40
49
|
case 'newStatus':
|
|
41
50
|
case 'description':
|
|
@@ -79,6 +88,18 @@ function validateProperties(obj = {}) {
|
|
|
79
88
|
returnObj[key] = value;
|
|
80
89
|
}
|
|
81
90
|
break;
|
|
91
|
+
case 'username':
|
|
92
|
+
if (isNameString(value) && /^[A-Za-z][0-9a-zA-Z-_.,'() ]+$/.test(value.trim())) {
|
|
93
|
+
returnObj[key] = value;
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case 'file_name':
|
|
97
|
+
case 'fileName':
|
|
98
|
+
case 'filename':
|
|
99
|
+
if (isFileNameString(value)) {
|
|
100
|
+
returnObj[key] = value.normalize('NFKC').trim();
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
82
103
|
case 'street':
|
|
83
104
|
if (isStreetString(value)) {
|
|
84
105
|
returnObj[key] = value;
|
|
@@ -101,7 +122,7 @@ function validateProperties(obj = {}) {
|
|
|
101
122
|
break;
|
|
102
123
|
case 'search_string':
|
|
103
124
|
case 'searchString':
|
|
104
|
-
if (
|
|
125
|
+
if (isNameString(value) && value.trim().length <= 101) {
|
|
105
126
|
returnObj[key] = value;
|
|
106
127
|
}
|
|
107
128
|
break;
|
|
@@ -205,19 +226,17 @@ function validateProperties(obj = {}) {
|
|
|
205
226
|
returnObj[key] = value;
|
|
206
227
|
}
|
|
207
228
|
break;
|
|
208
|
-
case 'requested_by_name':
|
|
209
|
-
case 'requestedByName':
|
|
210
229
|
case 'document_description':
|
|
211
230
|
case 'documentDescription':
|
|
212
|
-
|
|
213
|
-
|
|
231
|
+
if (isTextString(value)) {
|
|
232
|
+
returnObj[key] = value;
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
214
235
|
case 'requested_by_phone':
|
|
215
236
|
case 'requestedByPhone':
|
|
216
|
-
case 'approved_by_name':
|
|
217
|
-
case 'approvedByName':
|
|
218
237
|
case 'approved_by_phone':
|
|
219
238
|
case 'approvedByPhone':
|
|
220
|
-
if (
|
|
239
|
+
if (isPhoneContactString(value)) {
|
|
221
240
|
returnObj[key] = value;
|
|
222
241
|
}
|
|
223
242
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carecard/validate",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.38.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/CareCard-ca/pkg-validate.git"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"typescript": "6.0.3"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@carecard/common-util": "3.
|
|
50
|
+
"@carecard/common-util": "3.38.0"
|
|
51
51
|
},
|
|
52
52
|
"nyc": {
|
|
53
53
|
"all": true,
|
package/readme.md
CHANGED
|
@@ -91,18 +91,21 @@ General direct validators return `true` or `false`.
|
|
|
91
91
|
| `isValidUuidString(value)` | Canonical UUID string in `8-4-4-4-12` format, case-insensitive. |
|
|
92
92
|
| `isCharactersString(value)` | 1 to 1000 chars containing letters, numbers, spaces, `_`, or `-`. |
|
|
93
93
|
| `isStreetString(value)` | Non-empty street-like string up to 1000 chars using letters, numbers, spaces, `,`, `.`, `/`, `#`, or `-`, and not starting with `,`, `_`, or `-`. |
|
|
94
|
-
| `isNameString(value)` |
|
|
95
|
-
| `
|
|
94
|
+
| `isNameString(value)` | Visible Unicode plain text from the allowlist below; at most 1000 original UTF-16 code units. Checks raw input and NFKC without rewriting the name. |
|
|
95
|
+
| `isFileNameString(value)` | Same allowlist, with a 255-code-point limit after NFKC and trimming. Rejects paths and punctuation-only names; preserves `.hidden` and `a..b.pdf`. Never use display metadata as a storage path. |
|
|
96
|
+
| `isSafeSearchString(value)` | Checks raw input before trimming; retains the ASCII search grammar (first letter, then letters, numbers, spaces, `_`, `-`, `.`, `,`, `'`, `(`, `)`, or `@`). |
|
|
96
97
|
| `isEmailString(value)` | Unmodified ASCII email up to 254 chars with one dot-atom local part (max 64 chars) and a multi-label DNS/punycode domain. Quoted local parts, domain literals, Unicode EAI, whitespace, empty labels, invalid DNS labels, numeric final labels, and single-label domains are rejected. |
|
|
97
98
|
| `isJwtString(value)` | Non-blank JWT-like string up to 8192 chars that starts with `eyJ` and contains only letters, numbers, `-`, `_`, and `.`. |
|
|
98
99
|
| `validatePassword(value)` | A well-formed Unicode password from 15 to 128 code points after NFC normalization that is not an exact case-sensitive blocklist match. Returns a discriminated result with the normalized value or a stable failure reason. All Unicode characters, including spaces and emoji, are permitted without composition rules. |
|
|
99
100
|
| `isUsernameString(value)` | 1 to 200 alphanumeric chars. |
|
|
100
101
|
| `isPhoneNumber(value)` | North American 10-digit phone number with optional parentheses around the area code and optional space, `-`, or `.` separators. |
|
|
102
|
+
| `isPhoneContactString(value)` | National phone syntax or an international number starting with `+`, containing 7 to 15 digits and optional single space, `-`, or `.` separators. |
|
|
101
103
|
| `isUrlSafeString(value)` | Non-blank string up to 2048 chars using letters, numbers, `-`, `_`, and `.`. |
|
|
102
104
|
| `isProvinceString(value)` | `ON` or `QC`, case-insensitive. |
|
|
103
105
|
| `isBoolValue(value)` | Boolean `true`/`false` or strings `"true"`/`"false"`. |
|
|
104
106
|
| `isPostalCodeString(value)` | Canadian postal code format, case-insensitive, with optional middle space. |
|
|
105
|
-
| `isSafeString(value)` |
|
|
107
|
+
| `isSafeString(value)` | Visible ASCII plain text, at most 10000 units, using letters, numbers, spaces, `-`, `_`, `.`, `,`, `'`, `(`, or `)`. |
|
|
108
|
+
| `isTextString(value)` | Same Unicode plain-text allowlist as names, at most 10000 original UTF-16 code units. |
|
|
106
109
|
| `isInStringArray(array, value)` | `value`, after lowercase/trim validation as a name string, is included in the supplied array. |
|
|
107
110
|
| `isCountryCodeString(value)` | Country dialing code in `+1` to `+999` format. |
|
|
108
111
|
| `isValidDomainName(value)` | Domain name with at least one dot, valid DNS-like labels, and max total length 253. |
|
|
@@ -112,6 +115,31 @@ General direct validators return `true` or `false`.
|
|
|
112
115
|
| `isValidUrl(value)` | Absolute `http://` or `https://` URL up to 2048 chars. |
|
|
113
116
|
| `isValidArrayOfStrings(value)` | Array where every element passes `isSafeString`. |
|
|
114
117
|
|
|
118
|
+
The plain-text allowlist permits Unicode letters, combining marks, numbers,
|
|
119
|
+
space separators, dashes, emoji, script/emoji joiners, and the punctuation
|
|
120
|
+
`_ . , ' ’ ( ) @ &`. A value must contain a visible letter, number, or emoji.
|
|
121
|
+
Validate raw input before trimming so control characters at either edge cannot
|
|
122
|
+
be hidden. Compatibility normalization is checked without changing accepted
|
|
123
|
+
names; filename output is normalized as documented above. Explicitly allowed
|
|
124
|
+
compatibility punctuation includes fullwidth forms such as `A&B` and the
|
|
125
|
+
ellipsis in `Review…`. Unrelated symbols such as `℡`, `㏂`, and `㍿` are rejected
|
|
126
|
+
even when normalization would turn them into letters.
|
|
127
|
+
|
|
128
|
+
`isReactionString(value)` accepts allowlisted text or an ASCII named reaction
|
|
129
|
+
token such as `:reviewed:` or `:route-covered:`, up to 64 characters after
|
|
130
|
+
trimming ordinary spaces. Named tokens contain only letters, digits, `_` and
|
|
131
|
+
`-` between their two colons. Raw controls and unsupported syntax are rejected.
|
|
132
|
+
|
|
133
|
+
| Accepted examples | Rejected examples |
|
|
134
|
+
| --------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
135
|
+
| `José`, `李`, `محمد`, `123`, `O’Neil`, `A&B`, `👩🏽⚕️` | `<Research>`, `<script>alert(1)</script>`, `$(id)`, backticks |
|
|
136
|
+
| `A_B (Research), Inc.`, `Peter @ Singh`, `Jean–Luc` | `javascript:alert(1)`, `C++`, `Report #1.pdf`, SQL punctuation containing `;` |
|
|
137
|
+
| `A&B.pdf`, `.hidden`, `a..b.pdf`, `李.pdf` | `../secret`, `A/B`, `A\B`, `.`, `..`, controls, private-use glyphs |
|
|
138
|
+
|
|
139
|
+
Passwords, emails, URLs, JSON, addresses, phones and reaction tokens keep their own structured
|
|
140
|
+
format contracts. This allowlist does not replace output encoding, parameterized
|
|
141
|
+
queries, upload-content validation, or storage-path generation.
|
|
142
|
+
|
|
115
143
|
## `validateProperties(obj)`
|
|
116
144
|
|
|
117
145
|
`validateProperties` accepts an object and returns a new object that contains
|
|
@@ -141,11 +169,13 @@ where the package supports both.
|
|
|
141
169
|
|
|
142
170
|
| Validator | Keys |
|
|
143
171
|
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
144
|
-
| `isNameString` | `first_name`, `firstName`, `last_name`, `lastName`, `
|
|
172
|
+
| `isNameString` | `first_name`, `firstName`, `last_name`, `lastName`, `middle_name`, `middleName`, `nick_name`, `nickName`, `requested_by_name`, `requestedByName`, `approved_by_name`, `approvedByName`, `new_status`, `newStatus`, `description`, `comment`, `status`, `name`, `title`, `brand`, `short_description`, `shortDescription`, `college_name`, `collegeName`, `campus_name`, `campusName`, `role`, `role_id`, `roleId`, `campus`, `institution_name`, `institutionName`, `program_name`, `programName`, `role_name`, `roleName`, `document_name`, `documentName`, `document_required_for_role_name`, `documentRequiredForRoleName`, `reason`, `entity_type`, `entityType`, `action_type`, `actionType`, `city`, `state`, `country`, `type` |
|
|
145
173
|
| `isStreetString` | `street` |
|
|
146
174
|
| `isCharactersString` | `postal_code`, `postalCode`, `period` |
|
|
147
175
|
| `isBoolValue` | `is_primary`, `isPrimary`, `active`, `document_optional`, `documentOptional` |
|
|
148
|
-
| `
|
|
176
|
+
| `isNameString` with a trimmed 101-unit limit | `search_string`, `searchString` |
|
|
177
|
+
| `isFileNameString` with NFKC/trim output | `file_name`, `fileName`, `filename` |
|
|
178
|
+
| Existing username creation grammar | `username`: starts with an ASCII letter, followed by the previously supported ASCII letters, digits, spaces and punctuation; up to 1000 UTF-16 units. Login-specific rules stay separate. |
|
|
149
179
|
| `validatePassword` | `password`, `new_password`, `newPassword` |
|
|
150
180
|
| `isEmailString` | `email`, `requested_by_email`, `requestedByEmail`, `approved_by_email`, `approvedByEmail` |
|
|
151
181
|
| `isPhoneNumber` | `phone_number`, `phoneNumber` |
|
|
@@ -156,7 +186,8 @@ where the package supports both.
|
|
|
156
186
|
| `isValidIntegerString` | `offset_number`, `offsetNumber`, `number_of_orders`, `numberOfOrders`, `price`, `from`, `number`, `limit`, `offset` |
|
|
157
187
|
| `isValidJsonString` on the raw value | `about` |
|
|
158
188
|
| `isValidJsonString(JSON.stringify(value))` | `weight`, `dimensions`, `permission`, `scope_data`, `scopeData`, `meta_data`, `metaData`, `metadata` |
|
|
159
|
-
| `isTextString` | `document_description`, `documentDescription
|
|
189
|
+
| `isTextString` | `document_description`, `documentDescription` |
|
|
190
|
+
| `isPhoneContactString` | `requested_by_phone`, `requestedByPhone`, `approved_by_phone`, `approvedByPhone` |
|
|
160
191
|
| `isValidArrayOfStrings` | `aliases` |
|
|
161
192
|
| `isImageUrl` or `isValidUrl` | `image_url`, `imageUrl`, `website`, `file_url`, `fileUrl` |
|
|
162
193
|
| `isValidDomainName` | `domain_name`, `domainName`, `domain`, `email_domain`, `emailDomain`, `email_domain_name`, `emailDomainName` |
|