@oxyhq/core 12.3.0 → 12.4.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.
@@ -77,7 +77,7 @@ export { buildSearchParams, buildUrl, buildPaginationParams, safeJsonParse, } fr
77
77
  export type { PaginationParams, ApiResponse, ErrorResponse, } from './utils/apiUtils';
78
78
  export { ErrorCodes, createApiError, handleHttpError, validateRequiredFields, } from './utils/errorUtils';
79
79
  export { retryAsync } from './utils/asyncUtils';
80
- export { EMAIL_REGEX, USERNAME_REGEX, PASSWORD_REGEX, isValidEmail, isValidUsername, isValidPassword, isValidDisplayName, isRequiredString, isRequiredNumber, isRequiredBoolean, isValidArray, isValidObject, isValidUUID, isValidURL, isValidDate, isValidFileSize, isValidFileType, sanitizeString, sanitizeHTML, isValidObjectId, validateAndSanitizeUserInput, } from './utils/validationUtils';
80
+ export { EMAIL_REGEX, USERNAME_REGEX, PASSWORD_REGEX, isValidEmail, isValidUsername, isValidPassword, isValidDisplayName, DISPLAY_NAME_ALLOWED_SCRIPTS, DISPLAY_NAME_DISALLOWED_SOURCE, DISPLAY_NAME_ORPHANED_MARK_SOURCE, isRequiredString, isRequiredNumber, isRequiredBoolean, isValidArray, isValidObject, isValidUUID, isValidURL, isValidDate, isValidFileSize, isValidFileType, sanitizeString, sanitizeHTML, isValidObjectId, validateAndSanitizeUserInput, } from './utils/validationUtils';
81
81
  export { normalizeInlineText, normalizeMultilineText, } from './utils/textNormalization';
82
82
  export { logger, createLogger, configureLogger, getLoggerConfig, resetLoggerConfig, consoleSink, isDev, } from './logger';
83
83
  export type { Logger, LogLevel, EmittableLogLevel, LogContext, LogEntry, LogSink, LoggerConfig, } from './logger';
@@ -25,6 +25,71 @@ export declare function isValidUsername(username: string): boolean;
25
25
  * Validate password strength
26
26
  */
27
27
  export declare function isValidPassword(password: string): boolean;
28
+ /**
29
+ * Display-name character policy.
30
+ *
31
+ * A clean display name is composed ONLY of:
32
+ * - letters from a curated ALLOWLIST of scripts that real names use
33
+ * ({@link DISPLAY_NAME_ALLOWED_SCRIPTS}) — NOT `\p{L}` (letters of ANY
34
+ * script), which admits decorative / historic / limited-use scripts whose
35
+ * characters are `\p{L}` yet never appear in a real name (e.g. `ᯅ` U+1BC5
36
+ * Batak, Runic, Deseret, dingbat letters),
37
+ * - combining marks / accents (`\p{M}`, e.g. the acute accent in a decomposed
38
+ * "é"),
39
+ * - Unicode space separators (`\p{Zs}`: the ASCII space, NBSP, ideographic
40
+ * space, …) — but NOT control whitespace such as tab, newline, or carriage
41
+ * return, which would break layout or enable multi-line spoofing,
42
+ * - the straight apostrophe (`'`, e.g. "O'Brien").
43
+ *
44
+ * Everything else is rejected: emoji (🐧), symbols (⁂ ⏚), `:emoji:` shortcodes,
45
+ * digits, hyphens, dots, control whitespace (tab/newline/CR), letters from
46
+ * non-allowlisted scripts, and any other punctuation. The allowed set never
47
+ * includes `<`, `>`, `&`, or `"`, so a value that passes this predicate can
48
+ * never contain an HTML/XSS vector.
49
+ *
50
+ * The allowlist is expressed with Unicode Script_Extensions (`\p{scx=…}`)
51
+ * escapes so a letter shared by several scripts (e.g. a Han ideograph used in
52
+ * both Chinese and Japanese) still matches. It is the set of scripts Unicode
53
+ * UTS #39 marks "Recommended" for general interchange / identifiers, plus
54
+ * Cherokee and Mongolian (both in real modern name use). "Common" script is
55
+ * deliberately EXCLUDED — that is where ASCII digits and general punctuation
56
+ * live, and this policy excludes those; the space separators, combining marks,
57
+ * and apostrophe a name needs are added back explicitly. Limited-use / excluded
58
+ * / historic scripts (Batak, Runic, Deseret, Adlam, …) are simply absent.
59
+ *
60
+ * This is the SINGLE definition of the policy: the character-class sources below
61
+ * are the ONE source of truth, shared between the API strip/gate
62
+ * (`@oxyhq/api` `displayNameSanitize.ts` builds its global-flag patterns from
63
+ * them) and client-side inline validation (the RN profile editor via
64
+ * {@link isValidDisplayName}) so the two can never drift. It is platform-agnostic
65
+ * (no react/react-native/expo).
66
+ */
67
+ /**
68
+ * The curated allowlist of Unicode scripts permitted in a display name, as a
69
+ * character-class body of Script_Extensions (`scx`) property escapes. Ordered by
70
+ * rough script family for readability; order has no semantic effect.
71
+ */
72
+ export declare const DISPLAY_NAME_ALLOWED_SCRIPTS: string;
73
+ /**
74
+ * Source of the disallowed-character pattern: the negation of the full allowed
75
+ * set (allowlisted scripts + combining marks `\p{M}` + space separators `\p{Zs}`
76
+ * + the straight apostrophe). Consumers compile this with the `u` flag (and `g`
77
+ * for a global strip). The whitespace class is `\p{Zs}` (space separators only),
78
+ * NOT `\s` — the latter would admit tab/newline/carriage return, which break
79
+ * layout and enable multi-line spoofing.
80
+ */
81
+ export declare const DISPLAY_NAME_DISALLOWED_SOURCE = "[^\\p{scx=Latin}\\p{scx=Greek}\\p{scx=Cyrillic}\\p{scx=Armenian}\\p{scx=Hebrew}\\p{scx=Arabic}\\p{scx=Thaana}\\p{scx=Devanagari}\\p{scx=Bengali}\\p{scx=Gurmukhi}\\p{scx=Gujarati}\\p{scx=Oriya}\\p{scx=Tamil}\\p{scx=Telugu}\\p{scx=Kannada}\\p{scx=Malayalam}\\p{scx=Sinhala}\\p{scx=Thai}\\p{scx=Lao}\\p{scx=Tibetan}\\p{scx=Myanmar}\\p{scx=Georgian}\\p{scx=Hangul}\\p{scx=Ethiopic}\\p{scx=Cherokee}\\p{scx=Khmer}\\p{scx=Mongolian}\\p{scx=Hiragana}\\p{scx=Katakana}\\p{scx=Bopomofo}\\p{scx=Han}\\p{M}\\p{Zs}']";
82
+ /**
83
+ * Source of the orphaned combining-mark pattern: a run of `\p{M}` NOT attached
84
+ * to a base letter (preceded by string start, whitespace, the apostrophe, or a
85
+ * position vacated by a stripped character). A mark preceded by `\p{L}` (a base
86
+ * letter, e.g. the decomposed accent in "Renée") or by another `\p{M}` (a
87
+ * multi-mark cluster) is NOT matched because the negative lookbehind fails at its
88
+ * position. Used both as a non-global probe (`.test`) and, with the `g` flag, to
89
+ * strip whole orphaned runs. The lookbehind intentionally still uses the broad
90
+ * `\p{L}` so that a mark riding on an allowlisted base letter is preserved.
91
+ */
92
+ export declare const DISPLAY_NAME_ORPHANED_MARK_SOURCE = "(?<![\\p{L}\\p{M}])\\p{M}+";
28
93
  /**
29
94
  * Whether `raw` already satisfies the display-name policy, i.e. it contains no
30
95
  * disallowed characters AND no orphaned combining marks. Used to REJECT native
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "12.3.0",
3
+ "version": "12.4.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
package/src/index.ts CHANGED
@@ -457,6 +457,9 @@ export {
457
457
  isValidUsername,
458
458
  isValidPassword,
459
459
  isValidDisplayName,
460
+ DISPLAY_NAME_ALLOWED_SCRIPTS,
461
+ DISPLAY_NAME_DISALLOWED_SOURCE,
462
+ DISPLAY_NAME_ORPHANED_MARK_SOURCE,
460
463
  isRequiredString,
461
464
  isRequiredNumber,
462
465
  isRequiredBoolean,
@@ -148,6 +148,22 @@ describe('Validation Utils', () => {
148
148
  expect(isValidDisplayName('')).toBe(true); // empty is valid; non-empty enforced elsewhere
149
149
  });
150
150
 
151
+ it.each([
152
+ ['Владимир', 'Cyrillic'],
153
+ ['مُحَمَد', 'Arabic with harakat'],
154
+ ['נתן', 'Hebrew'],
155
+ ['नमस्ते', 'Devanagari'],
156
+ ['김철수', 'Hangul'],
157
+ ['Αριστοτέλης', 'Greek'],
158
+ ['Արամ', 'Armenian'],
159
+ ['დავით', 'Georgian'],
160
+ ['สมชาย', 'Thai'],
161
+ ['ᏔᎳ', 'Cherokee'],
162
+ ['ᠮᠣᠩᠭᠣᠯ', 'Mongolian'],
163
+ ])('should return true for allowlisted-script real name %p (%s)', (name) => {
164
+ expect(isValidDisplayName(name)).toBe(true);
165
+ });
166
+
151
167
  it('should return false for emoji, symbols, digits, and punctuation', () => {
152
168
  expect(isValidDisplayName('nixCraft \u{1f427}')).toBe(false); // penguin emoji
153
169
  expect(isValidDisplayName('Agent007')).toBe(false);
@@ -155,6 +171,17 @@ describe('Validation Utils', () => {
155
171
  expect(isValidDisplayName('J.R.')).toBe(false);
156
172
  });
157
173
 
174
+ it.each([
175
+ ['ᯅ', 'Batak U+1BC5 (Limited-Use script)'],
176
+ ['ᚠ', 'Runic'],
177
+ ['Miguel de Icaza ᯅ', 'a Latin name with a trailing Batak letter'],
178
+ ])('should return false for non-allowlisted-script letter %p (%s)', (name) => {
179
+ // These characters are General_Category Lo (`\p{L}`), so the old
180
+ // all-scripts policy accepted them; the curated script allowlist rejects
181
+ // decorative / limited-use scripts a real name never uses.
182
+ expect(isValidDisplayName(name)).toBe(false);
183
+ });
184
+
158
185
  it('should return false for control whitespace (tab/newline/CR)', () => {
159
186
  // \p{Zs} (space separators only) rejects layout-breaking / multi-line
160
187
  // spoofing whitespace that \s would have admitted.
@@ -43,7 +43,11 @@ export function isValidPassword(password: string): boolean {
43
43
  * Display-name character policy.
44
44
  *
45
45
  * A clean display name is composed ONLY of:
46
- * - letters of any script (`\p{L}`),
46
+ * - letters from a curated ALLOWLIST of scripts that real names use
47
+ * ({@link DISPLAY_NAME_ALLOWED_SCRIPTS}) — NOT `\p{L}` (letters of ANY
48
+ * script), which admits decorative / historic / limited-use scripts whose
49
+ * characters are `\p{L}` yet never appear in a real name (e.g. `ᯅ` U+1BC5
50
+ * Batak, Runic, Deseret, dingbat letters),
47
51
  * - combining marks / accents (`\p{M}`, e.g. the acute accent in a decomposed
48
52
  * "é"),
49
53
  * - Unicode space separators (`\p{Zs}`: the ASCII space, NBSP, ideographic
@@ -52,34 +56,71 @@ export function isValidPassword(password: string): boolean {
52
56
  * - the straight apostrophe (`'`, e.g. "O'Brien").
53
57
  *
54
58
  * Everything else is rejected: emoji (🐧), symbols (⁂ ⏚), `:emoji:` shortcodes,
55
- * digits, hyphens, dots, control whitespace (tab/newline/CR), and any other
56
- * punctuation. The allowed set `\p{L}\p{M}\p{Zs}'` explicitly EXCLUDES `<`, `>`,
57
- * `&`, and `"`, so a value that passes this predicate can never contain an
58
- * HTML/XSS vector.
59
+ * digits, hyphens, dots, control whitespace (tab/newline/CR), letters from
60
+ * non-allowlisted scripts, and any other punctuation. The allowed set never
61
+ * includes `<`, `>`, `&`, or `"`, so a value that passes this predicate can
62
+ * never contain an HTML/XSS vector.
59
63
  *
60
- * This is the SINGLE definition of the policy, shared between the API 400-gate
61
- * (`@oxyhq/api` `displayNameSanitize.ts`) and client-side inline validation
62
- * (the RN profile editor) so the two can never drift. It is platform-agnostic
64
+ * The allowlist is expressed with Unicode Script_Extensions (`\p{scx=…}`)
65
+ * escapes so a letter shared by several scripts (e.g. a Han ideograph used in
66
+ * both Chinese and Japanese) still matches. It is the set of scripts Unicode
67
+ * UTS #39 marks "Recommended" for general interchange / identifiers, plus
68
+ * Cherokee and Mongolian (both in real modern name use). "Common" script is
69
+ * deliberately EXCLUDED — that is where ASCII digits and general punctuation
70
+ * live, and this policy excludes those; the space separators, combining marks,
71
+ * and apostrophe a name needs are added back explicitly. Limited-use / excluded
72
+ * / historic scripts (Batak, Runic, Deseret, Adlam, …) are simply absent.
73
+ *
74
+ * This is the SINGLE definition of the policy: the character-class sources below
75
+ * are the ONE source of truth, shared between the API strip/gate
76
+ * (`@oxyhq/api` `displayNameSanitize.ts` builds its global-flag patterns from
77
+ * them) and client-side inline validation (the RN profile editor via
78
+ * {@link isValidDisplayName}) so the two can never drift. It is platform-agnostic
63
79
  * (no react/react-native/expo).
64
80
  */
65
81
 
66
82
  /**
67
- * Single test for the presence of a disallowed character (non-global). The
68
- * whitespace class is `\p{Zs}` (space separators only), NOT `\s` the latter
69
- * would admit tab/newline/carriage return, which break layout and enable
70
- * multi-line spoofing.
83
+ * The curated allowlist of Unicode scripts permitted in a display name, as a
84
+ * character-class body of Script_Extensions (`scx`) property escapes. Ordered by
85
+ * rough script family for readability; order has no semantic effect.
86
+ */
87
+ export const DISPLAY_NAME_ALLOWED_SCRIPTS =
88
+ '\\p{scx=Latin}\\p{scx=Greek}\\p{scx=Cyrillic}\\p{scx=Armenian}' +
89
+ '\\p{scx=Hebrew}\\p{scx=Arabic}\\p{scx=Thaana}\\p{scx=Devanagari}' +
90
+ '\\p{scx=Bengali}\\p{scx=Gurmukhi}\\p{scx=Gujarati}\\p{scx=Oriya}' +
91
+ '\\p{scx=Tamil}\\p{scx=Telugu}\\p{scx=Kannada}\\p{scx=Malayalam}' +
92
+ '\\p{scx=Sinhala}\\p{scx=Thai}\\p{scx=Lao}\\p{scx=Tibetan}' +
93
+ '\\p{scx=Myanmar}\\p{scx=Georgian}\\p{scx=Hangul}\\p{scx=Ethiopic}' +
94
+ '\\p{scx=Cherokee}\\p{scx=Khmer}\\p{scx=Mongolian}\\p{scx=Hiragana}' +
95
+ '\\p{scx=Katakana}\\p{scx=Bopomofo}\\p{scx=Han}';
96
+
97
+ /**
98
+ * Source of the disallowed-character pattern: the negation of the full allowed
99
+ * set (allowlisted scripts + combining marks `\p{M}` + space separators `\p{Zs}`
100
+ * + the straight apostrophe). Consumers compile this with the `u` flag (and `g`
101
+ * for a global strip). The whitespace class is `\p{Zs}` (space separators only),
102
+ * NOT `\s` — the latter would admit tab/newline/carriage return, which break
103
+ * layout and enable multi-line spoofing.
71
104
  */
72
- const DISALLOWED_PROBE = /[^\p{L}\p{M}\p{Zs}']/u;
105
+ export const DISPLAY_NAME_DISALLOWED_SOURCE = `[^${DISPLAY_NAME_ALLOWED_SCRIPTS}\\p{M}\\p{Zs}']`;
73
106
 
74
107
  /**
75
- * Single test for the presence of an orphaned combining mark (non-global) a
76
- * `\p{M}` not attached to a base letter (string start, whitespace, the
77
- * apostrophe, or a position vacated by a stripped character). A mark preceded by
78
- * `\p{L}` (a base letter, e.g. the decomposed accent in "Renée") or by another
79
- * `\p{M}` (a multi-mark cluster) is NOT matched because the negative lookbehind
80
- * fails at its position.
108
+ * Source of the orphaned combining-mark pattern: a run of `\p{M}` NOT attached
109
+ * to a base letter (preceded by string start, whitespace, the apostrophe, or a
110
+ * position vacated by a stripped character). A mark preceded by `\p{L}` (a base
111
+ * letter, e.g. the decomposed accent in "Renée") or by another `\p{M}` (a
112
+ * multi-mark cluster) is NOT matched because the negative lookbehind fails at its
113
+ * position. Used both as a non-global probe (`.test`) and, with the `g` flag, to
114
+ * strip whole orphaned runs. The lookbehind intentionally still uses the broad
115
+ * `\p{L}` so that a mark riding on an allowlisted base letter is preserved.
81
116
  */
82
- const ORPHANED_MARK_PROBE = /(?<![\p{L}\p{M}])\p{M}/u;
117
+ export const DISPLAY_NAME_ORPHANED_MARK_SOURCE = '(?<![\\p{L}\\p{M}])\\p{M}+';
118
+
119
+ /** Non-global probe for the presence of a disallowed character. */
120
+ const DISALLOWED_PROBE = new RegExp(DISPLAY_NAME_DISALLOWED_SOURCE, 'u');
121
+
122
+ /** Non-global probe for the presence of an orphaned combining mark. */
123
+ const ORPHANED_MARK_PROBE = new RegExp(DISPLAY_NAME_ORPHANED_MARK_SOURCE, 'u');
83
124
 
84
125
  /**
85
126
  * Whether `raw` already satisfies the display-name policy, i.e. it contains no