@carecard/validate 3.37.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.
@@ -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,9 +144,9 @@ 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 visible, well-formed Unicode names up to 1000 UTF-16 code units. */
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 NFKC-trimmed filename metadata up to 255 code points, excluding paths and unsafe controls. */
149
+ /** Checks the plain-text allowlist and NFKC-trimmed filename metadata up to 255 code points. */
150
150
  export const isFileNameString: BoolValidator;
151
151
  /** Checks if the string is safe for search queries. */
152
152
  export const isSafeSearchString: BoolValidator;
@@ -158,6 +158,8 @@ export const isJwtString: BoolValidator;
158
158
  export const isUsernameString: BoolValidator;
159
159
  /** Checks if the string is a valid phone number. */
160
160
  export const isPhoneNumber: BoolValidator;
161
+ /** Checks national or international phone contact snapshots with at most 15 international digits. */
162
+ export const isPhoneContactString: BoolValidator;
161
163
  /** Checks if the string is URL-safe. */
162
164
  export const isUrlSafeString: BoolValidator;
163
165
  /** Checks if the string is a valid Canadian province abbreviation (ON, QC). */
@@ -168,8 +170,10 @@ export const isBoolValue: BoolValidator;
168
170
  export const isPostalCodeString: BoolValidator;
169
171
  /** Checks if the string contains only allowed "safe" characters. */
170
172
  export const isSafeString: BoolValidator;
171
- /** Checks if the value is non-empty text up to the supported maximum length. */
173
+ /** Checks allowlisted visible Unicode text up to 10000 UTF-16 code units. */
172
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;
173
177
  /** Checks if a string exists within a given array of strings, case-insensitive. */
174
178
  export const isInStringArray: InStringArrayValidator;
175
179
  /** Checks if the string is one of the supported user role request statuses. */
@@ -211,12 +215,14 @@ export const validate: {
211
215
  isJwtString: typeof isJwtString;
212
216
  isUsernameString: typeof isUsernameString;
213
217
  isPhoneNumber: typeof isPhoneNumber;
218
+ isPhoneContactString: typeof isPhoneContactString;
214
219
  isUrlSafeString: typeof isUrlSafeString;
215
220
  isProvinceString: typeof isProvinceString;
216
221
  isBoolValue: typeof isBoolValue;
217
222
  isPostalCodeString: typeof isPostalCodeString;
218
223
  isSafeString: typeof isSafeString;
219
224
  isTextString: typeof isTextString;
225
+ isReactionString: typeof isReactionString;
220
226
  isInStringArray: typeof isInStringArray;
221
227
  isUserRoleRequestStatusString: typeof isUserRoleRequestStatusString;
222
228
  isUserRoleRequestRoleString: typeof isUserRoleRequestRoleString;
package/lib/validate.js CHANGED
@@ -68,21 +68,34 @@ const isStreetString = str => {
68
68
  return streetRegex.test(value);
69
69
  };
70
70
 
71
- // Pattern: Predicate - shares Unicode safety without changing the original text.
72
- const hasSafeVisibleName = value => {
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
+ );
73
84
  return (
74
- value.isWellFormed() &&
75
- !/[\p{Cc}\p{Zl}\p{Zp}\u202a-\u202e\u2066-\u2069]/u.test(value) &&
76
- /[\p{L}\p{N}\p{P}\p{S}\p{Co}]/u.test(value.replace(/\p{Default_Ignorable_Code_Point}/gu, ''))
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
+ )
77
90
  );
78
91
  };
79
92
 
80
- // Pattern: Predicate - accepts visible international names within the existing size budget.
93
+ // Pattern: Predicate - validates the original name before callers trim accepted spaces.
81
94
  const isNameString = str => {
82
95
  if (typeof str !== 'string' || str.length === 0 || str.length > 1000) {
83
96
  return false;
84
97
  }
85
- return hasSafeVisibleName(str.trim());
98
+ return hasAllowlistedVisibleText(str);
86
99
  };
87
100
 
88
101
  // Pattern: Predicate - validates canonical display metadata, never a filesystem path.
@@ -90,21 +103,17 @@ const isFileNameString = str => {
90
103
  if (typeof str !== 'string') {
91
104
  return false;
92
105
  }
93
- const fileName = str.normalize('NFKC').trim();
94
- return (
95
- [...fileName].length <= 255 &&
96
- hasSafeVisibleName(fileName) &&
97
- fileName !== '.' &&
98
- fileName !== '..' &&
99
- !/[/\\]/u.test(fileName)
100
- );
106
+ return hasAllowlistedVisibleText(str) && [...str.normalize('NFKC').trim()].length <= 255;
101
107
  };
102
108
 
109
+ // Pattern: Predicate - combines the text allowlist with the existing ASCII search grammar.
103
110
  const isSafeSearchString = str => {
104
111
  if (typeof str !== 'string' || str.length === 0) {
105
112
  return false;
106
113
  }
107
- return /^[A-Za-z][0-9a-zA-Z\-_.,'()@ ]{1,100}$/.test(str.trim());
114
+ return (
115
+ hasAllowlistedVisibleText(str) && /^[A-Za-z][0-9a-zA-Z\-_.,'()@ ]{1,100}$/.test(str.trim())
116
+ );
108
117
  };
109
118
 
110
119
  const EMAIL_LOCAL_PART_PATTERN =
@@ -182,6 +191,14 @@ function isPhoneNumber(str) {
182
191
  return /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(str);
183
192
  }
184
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
+
185
202
  const isUrlSafeString = inputString => {
186
203
  if (
187
204
  typeof inputString !== 'string' ||
@@ -219,15 +236,33 @@ const isPostalCodeString = inputString => {
219
236
  return false;
220
237
  };
221
238
 
239
+ // Pattern: Predicate - retains the stricter ASCII subset of the shared text allowlist.
222
240
  const isSafeString = str => {
223
241
  if (typeof str !== 'string' || str.length === 0 || str.length > 10000) {
224
242
  return false;
225
243
  }
226
- return /^[\da-zA-Z-_.,#*'()[\]: ]+$/.test(str);
244
+ return hasAllowlistedVisibleText(str) && /^[\da-zA-Z\-_.,'() ]+$/.test(str);
227
245
  };
228
246
 
247
+ // Pattern: Predicate - applies the shared allowlist within the existing text size budget.
229
248
  const isTextString = str => {
230
- return typeof str === 'string' && str.length > 0 && str.length <= 10000;
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
+ );
231
266
  };
232
267
 
233
268
  const isInStringArray = (StringArray, inputString) => {
@@ -382,12 +417,14 @@ module.exports = {
382
417
  validatePassword,
383
418
  isUsernameString,
384
419
  isPhoneNumber,
420
+ isPhoneContactString,
385
421
  isUrlSafeString,
386
422
  isProvinceString,
387
423
  isBoolValue,
388
424
  isPostalCodeString,
389
425
  isSafeString,
390
426
  isTextString,
427
+ isReactionString,
391
428
  isInStringArray,
392
429
  isUserRoleRequestStatusString,
393
430
  isUserRoleRequestRoleString,
@@ -2,6 +2,7 @@
2
2
  const {
3
3
  isEmailString,
4
4
  isPhoneNumber,
5
+ isPhoneContactString,
5
6
  isUrlSafeString,
6
7
  isValidUuidString,
7
8
  isCcIdString,
@@ -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
 
@@ -226,11 +228,15 @@ function validateProperties(obj = {}) {
226
228
  break;
227
229
  case 'document_description':
228
230
  case 'documentDescription':
231
+ if (isTextString(value)) {
232
+ returnObj[key] = value;
233
+ }
234
+ break;
229
235
  case 'requested_by_phone':
230
236
  case 'requestedByPhone':
231
237
  case 'approved_by_phone':
232
238
  case 'approvedByPhone':
233
- if (isTextString(value)) {
239
+ if (isPhoneContactString(value)) {
234
240
  returnObj[key] = value;
235
241
  }
236
242
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carecard/validate",
3
- "version": "3.37.0",
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.37.0"
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)` | 1 to 1000 char string that starts with a letter and uses letters, numbers, spaces, `_`, `-`, `.`, `,`, `'`, `(`, or `)`. Leading/trailing spaces are trimmed before pattern validation. |
95
- | `isSafeSearchString(value)` | Trimmed string that starts with a letter and then uses letters, numbers, spaces, `_`, `-`, `.`, `,`, `'`, `(`, `)`, or `@`. |
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)` | 1 to 10000 chars using letters, numbers, spaces, `-`, `_`, `.`, `,`, `#`, `*`, `'`, `(`, `)`, `[`, `]`, or `:`. |
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`, `username`, `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` |
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
- | `isSafeSearchString` | `search_string`, `searchString` |
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`, `nick_name`, `nickName`, `requested_by_name`, `requestedByName`, `requested_by_phone`, `requestedByPhone`, `approved_by_name`, `approvedByName`, `approved_by_phone`, `approvedByPhone` |
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` |