@oxyhq/core 12.3.0 → 12.4.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/esm/index.js CHANGED
@@ -107,7 +107,7 @@ export { retryAsync } from './utils/asyncUtils.js';
107
107
  // ---------------------------------------------------------------------------
108
108
  // Validation
109
109
  // ---------------------------------------------------------------------------
110
- 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.js';
110
+ 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.js';
111
111
  // ---------------------------------------------------------------------------
112
112
  // Text normalization
113
113
  // ---------------------------------------------------------------------------
@@ -67,9 +67,20 @@ export async function refreshDeviceSecretArm(deps) {
67
67
  // Structural read (not `instanceof Error`): the thrown value can be a
68
68
  // plain ApiError-shaped object or come from another realm.
69
69
  const message = error?.message;
70
- return typeof message === 'string' && message.includes('no_active_session')
71
- ? { status: 'no-session' }
72
- : { status: 'invalid-secret' };
70
+ const body = typeof message === 'string' ? message : '';
71
+ // ONLY the server's explicit `invalid_device_secret` proves the presented
72
+ // secret is bad and may clear the durable device credential. `no_active_session`
73
+ // is an authoritative signed-out. ANY OTHER 401 — a middleware/CSRF/proxy 401,
74
+ // an ALB/starting-instance 401, a CORS error page, etc., all common during a
75
+ // deploy/restart window — is NOT proof the secret diverged: treat it as
76
+ // transient and KEEP the credential so a later attempt self-heals. Wiping the
77
+ // credential on an ambiguous 401 is what logged users out on every deploy,
78
+ // ecosystem-wide.
79
+ if (body.includes('invalid_device_secret'))
80
+ return { status: 'invalid-secret' };
81
+ if (body.includes('no_active_session'))
82
+ return { status: 'no-session' };
83
+ return { status: 'transient' };
73
84
  }
74
85
  return { status: 'transient' };
75
86
  }
@@ -36,7 +36,11 @@ export function isValidPassword(password) {
36
36
  * Display-name character policy.
37
37
  *
38
38
  * A clean display name is composed ONLY of:
39
- * - letters of any script (`\p{L}`),
39
+ * - letters from a curated ALLOWLIST of scripts that real names use
40
+ * ({@link DISPLAY_NAME_ALLOWED_SCRIPTS}) — NOT `\p{L}` (letters of ANY
41
+ * script), which admits decorative / historic / limited-use scripts whose
42
+ * characters are `\p{L}` yet never appear in a real name (e.g. `ᯅ` U+1BC5
43
+ * Batak, Runic, Deseret, dingbat letters),
40
44
  * - combining marks / accents (`\p{M}`, e.g. the acute accent in a decomposed
41
45
  * "é"),
42
46
  * - Unicode space separators (`\p{Zs}`: the ASCII space, NBSP, ideographic
@@ -45,32 +49,65 @@ export function isValidPassword(password) {
45
49
  * - the straight apostrophe (`'`, e.g. "O'Brien").
46
50
  *
47
51
  * Everything else is rejected: emoji (🐧), symbols (⁂ ⏚), `:emoji:` shortcodes,
48
- * digits, hyphens, dots, control whitespace (tab/newline/CR), and any other
49
- * punctuation. The allowed set `\p{L}\p{M}\p{Zs}'` explicitly EXCLUDES `<`, `>`,
50
- * `&`, and `"`, so a value that passes this predicate can never contain an
51
- * HTML/XSS vector.
52
+ * digits, hyphens, dots, control whitespace (tab/newline/CR), letters from
53
+ * non-allowlisted scripts, and any other punctuation. The allowed set never
54
+ * includes `<`, `>`, `&`, or `"`, so a value that passes this predicate can
55
+ * never contain an HTML/XSS vector.
52
56
  *
53
- * This is the SINGLE definition of the policy, shared between the API 400-gate
54
- * (`@oxyhq/api` `displayNameSanitize.ts`) and client-side inline validation
55
- * (the RN profile editor) so the two can never drift. It is platform-agnostic
57
+ * The allowlist is expressed with Unicode Script_Extensions (`\p{scx=…}`)
58
+ * escapes so a letter shared by several scripts (e.g. a Han ideograph used in
59
+ * both Chinese and Japanese) still matches. It is the set of scripts Unicode
60
+ * UTS #39 marks "Recommended" for general interchange / identifiers, plus
61
+ * Cherokee and Mongolian (both in real modern name use). "Common" script is
62
+ * deliberately EXCLUDED — that is where ASCII digits and general punctuation
63
+ * live, and this policy excludes those; the space separators, combining marks,
64
+ * and apostrophe a name needs are added back explicitly. Limited-use / excluded
65
+ * / historic scripts (Batak, Runic, Deseret, Adlam, …) are simply absent.
66
+ *
67
+ * This is the SINGLE definition of the policy: the character-class sources below
68
+ * are the ONE source of truth, shared between the API strip/gate
69
+ * (`@oxyhq/api` `displayNameSanitize.ts` builds its global-flag patterns from
70
+ * them) and client-side inline validation (the RN profile editor via
71
+ * {@link isValidDisplayName}) so the two can never drift. It is platform-agnostic
56
72
  * (no react/react-native/expo).
57
73
  */
58
74
  /**
59
- * Single test for the presence of a disallowed character (non-global). The
60
- * whitespace class is `\p{Zs}` (space separators only), NOT `\s` the latter
61
- * would admit tab/newline/carriage return, which break layout and enable
62
- * multi-line spoofing.
63
- */
64
- const DISALLOWED_PROBE = /[^\p{L}\p{M}\p{Zs}']/u;
65
- /**
66
- * Single test for the presence of an orphaned combining mark (non-global) — a
67
- * `\p{M}` not attached to a base letter (string start, whitespace, the
68
- * apostrophe, or a position vacated by a stripped character). A mark preceded by
69
- * `\p{L}` (a base letter, e.g. the decomposed accent in "Renée") or by another
70
- * `\p{M}` (a multi-mark cluster) is NOT matched because the negative lookbehind
71
- * fails at its position.
72
- */
73
- const ORPHANED_MARK_PROBE = /(?<![\p{L}\p{M}])\p{M}/u;
75
+ * The curated allowlist of Unicode scripts permitted in a display name, as a
76
+ * character-class body of Script_Extensions (`scx`) property escapes. Ordered by
77
+ * rough script family for readability; order has no semantic effect.
78
+ */
79
+ export const DISPLAY_NAME_ALLOWED_SCRIPTS = '\\p{scx=Latin}\\p{scx=Greek}\\p{scx=Cyrillic}\\p{scx=Armenian}' +
80
+ '\\p{scx=Hebrew}\\p{scx=Arabic}\\p{scx=Thaana}\\p{scx=Devanagari}' +
81
+ '\\p{scx=Bengali}\\p{scx=Gurmukhi}\\p{scx=Gujarati}\\p{scx=Oriya}' +
82
+ '\\p{scx=Tamil}\\p{scx=Telugu}\\p{scx=Kannada}\\p{scx=Malayalam}' +
83
+ '\\p{scx=Sinhala}\\p{scx=Thai}\\p{scx=Lao}\\p{scx=Tibetan}' +
84
+ '\\p{scx=Myanmar}\\p{scx=Georgian}\\p{scx=Hangul}\\p{scx=Ethiopic}' +
85
+ '\\p{scx=Cherokee}\\p{scx=Khmer}\\p{scx=Mongolian}\\p{scx=Hiragana}' +
86
+ '\\p{scx=Katakana}\\p{scx=Bopomofo}\\p{scx=Han}';
87
+ /**
88
+ * Source of the disallowed-character pattern: the negation of the full allowed
89
+ * set (allowlisted scripts + combining marks `\p{M}` + space separators `\p{Zs}`
90
+ * + the straight apostrophe). Consumers compile this with the `u` flag (and `g`
91
+ * for a global strip). The whitespace class is `\p{Zs}` (space separators only),
92
+ * NOT `\s` — the latter would admit tab/newline/carriage return, which break
93
+ * layout and enable multi-line spoofing.
94
+ */
95
+ export const DISPLAY_NAME_DISALLOWED_SOURCE = `[^${DISPLAY_NAME_ALLOWED_SCRIPTS}\\p{M}\\p{Zs}']`;
96
+ /**
97
+ * Source of the orphaned combining-mark pattern: a run of `\p{M}` NOT attached
98
+ * to a base letter (preceded by string start, whitespace, the apostrophe, or a
99
+ * position vacated by a stripped character). A mark preceded by `\p{L}` (a base
100
+ * letter, e.g. the decomposed accent in "Renée") or by another `\p{M}` (a
101
+ * multi-mark cluster) is NOT matched because the negative lookbehind fails at its
102
+ * position. Used both as a non-global probe (`.test`) and, with the `g` flag, to
103
+ * strip whole orphaned runs. The lookbehind intentionally still uses the broad
104
+ * `\p{L}` so that a mark riding on an allowlisted base letter is preserved.
105
+ */
106
+ export const DISPLAY_NAME_ORPHANED_MARK_SOURCE = '(?<![\\p{L}\\p{M}])\\p{M}+';
107
+ /** Non-global probe for the presence of a disallowed character. */
108
+ const DISALLOWED_PROBE = new RegExp(DISPLAY_NAME_DISALLOWED_SOURCE, 'u');
109
+ /** Non-global probe for the presence of an orphaned combining mark. */
110
+ const ORPHANED_MARK_PROBE = new RegExp(DISPLAY_NAME_ORPHANED_MARK_SOURCE, 'u');
74
111
  /**
75
112
  * Whether `raw` already satisfies the display-name policy, i.e. it contains no
76
113
  * disallowed characters AND no orphaned combining marks. Used to REJECT native