@oxyhq/core 16.0.0 → 17.0.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.
Files changed (47) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +28 -6
  3. package/dist/cjs/i18n/locales/en-US.json +5 -3
  4. package/dist/cjs/i18n/locales/es-ES.json +5 -3
  5. package/dist/cjs/i18n/locales/locales/en-US.json +5 -3
  6. package/dist/cjs/i18n/locales/locales/es-ES.json +5 -3
  7. package/dist/cjs/index.js +6 -3
  8. package/dist/cjs/mixins/OxyServices.auth.js +57 -19
  9. package/dist/cjs/server/index.js +7 -1
  10. package/dist/cjs/server/userInvalidation.js +172 -0
  11. package/dist/cjs/utils/displayNamePolicyRanges.generated.js +28 -3
  12. package/dist/cjs/utils/validationUtils.js +112 -21
  13. package/dist/esm/.tsbuildinfo +1 -1
  14. package/dist/esm/HttpService.js +28 -6
  15. package/dist/esm/i18n/locales/en-US.json +5 -3
  16. package/dist/esm/i18n/locales/es-ES.json +5 -3
  17. package/dist/esm/i18n/locales/locales/en-US.json +5 -3
  18. package/dist/esm/i18n/locales/locales/es-ES.json +5 -3
  19. package/dist/esm/index.js +1 -1
  20. package/dist/esm/mixins/OxyServices.auth.js +57 -19
  21. package/dist/esm/server/index.js +3 -0
  22. package/dist/esm/server/userInvalidation.js +167 -0
  23. package/dist/esm/utils/displayNamePolicyRanges.generated.js +27 -2
  24. package/dist/esm/utils/validationUtils.js +112 -21
  25. package/dist/types/.tsbuildinfo +1 -1
  26. package/dist/types/HttpService.d.ts +13 -4
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/mixins/OxyServices.auth.d.ts +19 -0
  29. package/dist/types/server/index.d.ts +2 -0
  30. package/dist/types/server/userInvalidation.d.ts +133 -0
  31. package/dist/types/utils/displayNamePolicyRanges.generated.d.ts +27 -2
  32. package/dist/types/utils/validationUtils.d.ts +106 -20
  33. package/package.json +1 -1
  34. package/src/HttpService.ts +32 -7
  35. package/src/__tests__/httpServiceFormEncoded.test.ts +142 -0
  36. package/src/i18n/locales/en-US.json +5 -3
  37. package/src/i18n/locales/es-ES.json +5 -3
  38. package/src/index.ts +4 -1
  39. package/src/mixins/OxyServices.auth.ts +76 -20
  40. package/src/mixins/__tests__/preSessionSkipAuth.test.ts +72 -11
  41. package/src/server/__tests__/userInvalidation.test.ts +208 -0
  42. package/src/server/index.ts +13 -0
  43. package/src/server/userInvalidation.ts +221 -0
  44. package/src/utils/__tests__/coldBoot.test.ts +9 -4
  45. package/src/utils/__tests__/validationUtils.test.ts +292 -1
  46. package/src/utils/displayNamePolicyRanges.generated.ts +31 -2
  47. package/src/utils/validationUtils.ts +117 -20
@@ -1,7 +1,14 @@
1
1
  /**
2
2
  * Validation utilities for common data validation patterns
3
3
  */
4
- import { DISPLAY_NAME_ALLOWED_SCRIPTS_RANGES, DISPLAY_NAME_COMBINING_MARKS_RANGES, DISPLAY_NAME_SPACE_SEPARATORS_RANGES, DISPLAY_NAME_LETTERS_RANGES, } from './displayNamePolicyRanges.generated.js';
4
+ import { DISPLAY_NAME_ALLOWED_SCRIPTS_RANGES, DISPLAY_NAME_COMBINING_MARKS_RANGES, DISPLAY_NAME_SPACE_SEPARATORS_RANGES, DISPLAY_NAME_LETTERS_RANGES, DISPLAY_NAME_NAME_SEPARATORS_RANGES, } from './displayNamePolicyRanges.generated.js';
5
+ /**
6
+ * Maximum stored length of a display name, in code units after cleaning.
7
+ * Shared by the API write path and client input surfaces.
8
+ */
9
+ export const MAX_DISPLAY_NAME_LENGTH = 80;
10
+ /** Shared 400 / inline-validation copy for native display-name policy rejections. */
11
+ export const DISPLAY_NAME_INVALID_MESSAGE = 'Name may only contain letters, spaces, apostrophes, and name separators (·, ־, ་, ・).';
5
12
  /**
6
13
  * Email validation regex
7
14
  */
@@ -48,7 +55,16 @@ export function isValidPassword(password) {
48
55
  * ideographic space, …) — but NOT control whitespace such as tab, newline,
49
56
  * or carriage return, which would break layout or enable multi-line
50
57
  * spoofing,
51
- * - the straight apostrophe (`'`, e.g. "O'Brien").
58
+ * - the straight apostrophe (`'`, e.g. "O'Brien"),
59
+ * - four name SEPARATORS — `·` U+00B7, `־` U+05BE, `་` U+0F0B, `・` U+30FB —
60
+ * but ONLY directly between two letters (`Codeur·euses`, `お坐・エガード`,
61
+ * `אברמסקי־קרוננברג`, `འོད་ཟེར`). Every one is General_Category P, so the
62
+ * letters intersection strips them by default; they are re-admitted because
63
+ * stripping them SPLITS a real name in two. The flanking condition is not a
64
+ * refinement but the whole point: the same characters are also used as
65
+ * ornament (a trailing `Roberto ·`), and unconditional admission would let
66
+ * that through. See {@link DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE}. The
67
+ * ASCII hyphen is NOT among them — `Jean-Luc` stays rejected.
52
68
  *
53
69
  * Everything else is rejected: emoji (🐧), symbols (⁂ ⏚), `:emoji:` shortcodes,
54
70
  * digits, hyphens, dots, control whitespace (tab/newline/CR), letters from
@@ -58,7 +74,15 @@ export function isValidPassword(password) {
58
74
  *
59
75
  * The allowlist is expressed with Unicode Script_Extensions (scx=…) so a letter
60
76
  * shared by several scripts (e.g. a Han ideograph used in both Chinese and
61
- * Japanese) still matches. It is the set of scripts Unicode UTS #39 marks
77
+ * Japanese) still matches, INTERSECTED with General_Category L so only the
78
+ * scripts' LETTERS are admitted. That intersection is load-bearing, not a
79
+ * refinement: `scx=X` also carries script X's own digits, punctuation and
80
+ * symbols, so the un-intersected allowlist admitted 1831 non-letter code points
81
+ * — script digits (`٠١٢`, `०१२`), 1082 symbols (`֍ ۞ ৳`), 180 punctuation marks
82
+ * (`։ ، ؛ ।`), and 9 INVISIBLE format/control characters, among them U+061C
83
+ * ARABIC LETTER MARK (a bidi control that can visually reorder a name) and
84
+ * U+180E MONGOLIAN VOWEL SEPARATOR. Without it the rejections listed above held
85
+ * for ASCII input only. It is the set of scripts Unicode UTS #39 marks
62
86
  * "Recommended" for general interchange / identifiers, plus Cherokee and
63
87
  * Mongolian (both in real modern name use). "Common" script is deliberately
64
88
  * EXCLUDED — that is where ASCII digits and general punctuation live, and this
@@ -66,6 +90,29 @@ export function isValidPassword(password) {
66
90
  * name needs are added back explicitly. Limited-use / excluded / historic
67
91
  * scripts (Batak, Runic, Deseret, Adlam, …) are simply absent.
68
92
  *
93
+ * CODE-POINT DENYLIST — a character policy classifies FORM, never MEANING, so a
94
+ * code point can be a perfectly ordinary letter to Unicode and a hate symbol to
95
+ * a reader. `卐` U+5350 and `卍` U+534D are the case in point: both are CJK
96
+ * Unified Ideographs (General_Category Lo, Script_Extensions Han), i.e. the same
97
+ * kind of thing as `山` in `山田太郎`, so NO script-level or category-level rule
98
+ * can separate them — every rule that would exclude these two also excludes Han
99
+ * itself, rejecting every real Chinese, Japanese and Korean name. They are
100
+ * therefore enumerated and SUBTRACTED from the allowlist at generation time (see
101
+ * `SYMBOL_LETTER_DENYLIST` in `scripts/generateDisplayNamePolicyRanges.mjs`).
102
+ * Because the subtraction happens inside the one emitted allowlist, every
103
+ * consumer enforces it with no extra probe and none can forget it; the generator
104
+ * additionally REFUSES an entry that an existing lever already excludes (e.g.
105
+ * the Tibetan svasti signs U+0FD5–U+0FD8, General_Category So, already dropped
106
+ * by the intersection above), so the list cannot silently accumulate dead
107
+ * weight.
108
+ *
109
+ * REMAINING LIMIT — that closes the "letter that reads as a symbol" gap, not the
110
+ * other one: slurs and abusive phrasing spelled in ordinary allowlisted letters
111
+ * pass by construction, because they are built from exactly the characters every
112
+ * real name needs. No character-level rule — allowlist, intersection, or
113
+ * denylist — can reject them; that needs a word-level moderation layer, which is
114
+ * deliberately not attempted here.
115
+ *
69
116
  * HERMES / RANGES: the class bodies below are built from explicit Unicode
70
117
  * code-point RANGES ({@link DISPLAY_NAME_ALLOWED_SCRIPTS_RANGES} et al. from
71
118
  * `./displayNamePolicyRanges.generated`), NOT from `scx`/General_Category
@@ -86,20 +133,27 @@ export function isValidPassword(password) {
86
133
  */
87
134
  /**
88
135
  * The curated allowlist of Unicode scripts permitted in a display name, as a
89
- * character-class body of explicit code-point ranges (the compressed union of
90
- * the 30 allowlisted Script_Extensions, generated on V8). Interpolated into the
91
- * negated class below.
136
+ * character-class body of explicit code-point ranges: the union of the 30
137
+ * allowlisted Script_Extensions, INTERSECTED with General_Category L (so letters
138
+ * only), MINUS the symbol-letter denylist — 120823 code points. Interpolated
139
+ * into the negated class below, which is why both the reject gate here and the
140
+ * `@oxyhq/api` strip path inherit the denylist without a second pattern.
92
141
  */
93
142
  export const DISPLAY_NAME_ALLOWED_SCRIPTS = DISPLAY_NAME_ALLOWED_SCRIPTS_RANGES;
94
143
  /**
95
144
  * Source of the disallowed-character pattern: the negation of the full allowed
96
145
  * set (allowlisted scripts + combining marks + space separators + the straight
97
- * apostrophe). Consumers compile this with the `u` flag (and `g` for a global
98
- * strip). The whitespace class is space separators only (General_Category Zs),
99
- * NOT `\s` — the latter would admit tab/newline/carriage return, which break
100
- * layout and enable multi-line spoofing.
146
+ * apostrophe + the name separators). Consumers compile this with the `u` flag
147
+ * (and `g` for a global strip). The whitespace class is space separators only
148
+ * (General_Category Zs), NOT `\s` — the latter would admit tab/newline/carriage
149
+ * return, which break layout and enable multi-line spoofing.
150
+ *
151
+ * The name separators are admitted here only as CHARACTERS. Their position rule
152
+ * is a separate pattern ({@link DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE}), for
153
+ * the same reason combining marks are: a negated character class cannot express
154
+ * "allowed only in this context". Both halves must be applied together.
101
155
  */
102
- export const DISPLAY_NAME_DISALLOWED_SOURCE = `[^${DISPLAY_NAME_ALLOWED_SCRIPTS}${DISPLAY_NAME_COMBINING_MARKS_RANGES}${DISPLAY_NAME_SPACE_SEPARATORS_RANGES}']`;
156
+ export const DISPLAY_NAME_DISALLOWED_SOURCE = `[^${DISPLAY_NAME_ALLOWED_SCRIPTS}${DISPLAY_NAME_COMBINING_MARKS_RANGES}${DISPLAY_NAME_SPACE_SEPARATORS_RANGES}${DISPLAY_NAME_NAME_SEPARATORS_RANGES}']`;
103
157
  /**
104
158
  * Source of the orphaned combining-mark pattern: a run of combining marks NOT
105
159
  * attached to a base letter (preceded by string start, whitespace, the
@@ -112,26 +166,63 @@ export const DISPLAY_NAME_DISALLOWED_SOURCE = `[^${DISPLAY_NAME_ALLOWED_SCRIPTS}
112
166
  * an allowlisted base letter is preserved.
113
167
  */
114
168
  export const DISPLAY_NAME_ORPHANED_MARK_SOURCE = `(?<![${DISPLAY_NAME_LETTERS_RANGES}${DISPLAY_NAME_COMBINING_MARKS_RANGES}])[${DISPLAY_NAME_COMBINING_MARKS_RANGES}]+`;
169
+ /**
170
+ * Source of the unflanked name-separator pattern — the positional half of the
171
+ * name-separator rule, and the exact counterpart of
172
+ * {@link DISPLAY_NAME_ORPHANED_MARK_SOURCE}: a combining mark is allowed only
173
+ * when it rides a base letter, and a name separator is allowed only when it JOINS
174
+ * two letters. Matches a separator that is missing a letter on either side, so
175
+ * consumers can reject it (non-global probe) or strip it (`g` flag).
176
+ *
177
+ * Two alternatives, one per side, because JS has no "not surrounded by" atom:
178
+ * - `(?<![letters][marks])[sep]` — nothing letter-like immediately BEFORE.
179
+ * - `[sep](?![letters])` — no letter immediately AFTER.
180
+ *
181
+ * The left-hand class deliberately includes combining marks as well as letters:
182
+ * a base letter can carry an accent (`Renée·euses`, or a decomposed `e`+◌́), and
183
+ * that mark sits between the letter and the separator. Treating a mark as
184
+ * letter-like is what stops an accent from defeating the flanking test — the
185
+ * same reason the orphaned-mark lookbehind uses the identical pair of classes.
186
+ * The right-hand class is letters only: a mark AFTER a separator has no base and
187
+ * is removed by the orphaned-mark rule, so the separator is genuinely unflanked.
188
+ *
189
+ * Both lookarounds are single-character (no variable-length lookbehind), and the
190
+ * classes are explicit code-point ranges, so the compiled regex is within what
191
+ * Hermes accepts.
192
+ *
193
+ * `Codeur·euses` and `お坐・エガード` match NOTHING here and survive. A leading
194
+ * `·Roberto`, a trailing `Roberto ·`, and a doubled `a··b` each match and are
195
+ * stripped — which is what keeps a decorative middle dot from riding in on the
196
+ * back of the orthographic one.
197
+ */
198
+ export const DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE = `(?<![${DISPLAY_NAME_LETTERS_RANGES}${DISPLAY_NAME_COMBINING_MARKS_RANGES}])[${DISPLAY_NAME_NAME_SEPARATORS_RANGES}]|[${DISPLAY_NAME_NAME_SEPARATORS_RANGES}](?![${DISPLAY_NAME_LETTERS_RANGES}])`;
115
199
  /** Non-global probe for the presence of a disallowed character. */
116
200
  const DISALLOWED_PROBE = new RegExp(DISPLAY_NAME_DISALLOWED_SOURCE, 'u');
117
201
  /** Non-global probe for the presence of an orphaned combining mark. */
118
202
  const ORPHANED_MARK_PROBE = new RegExp(DISPLAY_NAME_ORPHANED_MARK_SOURCE, 'u');
203
+ /** Non-global probe for the presence of an unflanked name separator. */
204
+ const UNFLANKED_SEPARATOR_PROBE = new RegExp(DISPLAY_NAME_UNFLANKED_SEPARATOR_SOURCE, 'u');
119
205
  /**
120
206
  * Whether `raw` already satisfies the display-name policy, i.e. it contains no
121
- * disallowed characters AND no orphaned combining marks. Used to REJECT native
122
- * (signup / profile edit) names with a 400 rather than silently stripping them,
123
- * and to validate inline in the client editor.
207
+ * disallowed characters, no orphaned combining marks, and no unflanked name
208
+ * separator. Used to REJECT native (signup / profile edit) names with a 400
209
+ * rather than silently stripping them, and to validate inline in the client
210
+ * editor.
124
211
  *
125
- * The orphaned-mark probe runs on the NFC-normalized form so a legitimate
126
- * decomposed accent (`e`+◌́) which normalization recomposes into `é` is NOT
127
- * rejected, while a lone, base-less mark (e.g. `"༘"`) IS.
212
+ * The two positional probes run on the NFC-normalized form, the character probe
213
+ * on the raw input. Normalization matters for both: a legitimate decomposed
214
+ * accent (`e`+◌́) recomposes into `é`, so it is NOT rejected as an orphaned mark,
215
+ * and a separator after that accent sees a base letter rather than a mark.
128
216
  *
129
- * The function only checks the character set; an empty or whitespace-only string
130
- * is considered valid (`true`). Call sites that require a non-empty name enforce
131
- * that separately.
217
+ * The function only checks the character set and separator placement; an empty
218
+ * or whitespace-only string is considered valid (`true`). Call sites that require
219
+ * a non-empty name enforce that separately.
132
220
  */
133
221
  export function isValidDisplayName(raw) {
134
- return (!DISALLOWED_PROBE.test(raw) && !ORPHANED_MARK_PROBE.test(raw.normalize('NFC')));
222
+ const normalized = raw.normalize('NFC');
223
+ return (!DISALLOWED_PROBE.test(raw) &&
224
+ !ORPHANED_MARK_PROBE.test(normalized) &&
225
+ !UNFLANKED_SEPARATOR_PROBE.test(normalized));
135
226
  }
136
227
  /**
137
228
  * Validate required string