@oxyhq/core 5.5.0 → 7.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 (196) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +7 -4
  3. package/dist/cjs/OxyServices.base.js +7 -102
  4. package/dist/cjs/boot/coldBootV2.js +350 -0
  5. package/dist/cjs/boot/deviceBootReturn.js +152 -0
  6. package/dist/cjs/crypto/keyManager.js +95 -0
  7. package/dist/cjs/i18n/locales/en-US.json +13 -1
  8. package/dist/cjs/i18n/locales/es-ES.json +13 -1
  9. package/dist/cjs/i18n/locales/locales/en-US.json +13 -1
  10. package/dist/cjs/i18n/locales/locales/es-ES.json +13 -1
  11. package/dist/cjs/index.js +54 -45
  12. package/dist/cjs/mixins/OxyServices.accounts.js +13 -26
  13. package/dist/cjs/mixins/OxyServices.auth.js +66 -201
  14. package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
  15. package/dist/cjs/mixins/index.js +8 -17
  16. package/dist/cjs/server/index.js +2 -2
  17. package/dist/cjs/session/SessionClient.js +40 -1
  18. package/dist/cjs/session/authStateStore.js +284 -0
  19. package/dist/cjs/session/refresh.js +264 -0
  20. package/dist/cjs/shared/utils/debugUtils.js +3 -3
  21. package/dist/cjs/utils/accountUtils.js +1 -55
  22. package/dist/cjs/utils/authWebUrl.js +10 -39
  23. package/dist/cjs/utils/coldBoot.js +10 -8
  24. package/dist/cjs/utils/platform.js +19 -0
  25. package/dist/cjs/utils/registrableApex.js +49 -0
  26. package/dist/cjs/utils/ssoBounce.js +15 -362
  27. package/dist/cjs/utils/validationUtils.js +57 -0
  28. package/dist/esm/.tsbuildinfo +1 -1
  29. package/dist/esm/HttpService.js +7 -4
  30. package/dist/esm/OxyServices.base.js +7 -102
  31. package/dist/esm/boot/coldBootV2.js +344 -0
  32. package/dist/esm/boot/deviceBootReturn.js +146 -0
  33. package/dist/esm/crypto/keyManager.js +95 -0
  34. package/dist/esm/i18n/locales/en-US.json +13 -1
  35. package/dist/esm/i18n/locales/es-ES.json +13 -1
  36. package/dist/esm/i18n/locales/locales/en-US.json +13 -1
  37. package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
  38. package/dist/esm/index.js +34 -17
  39. package/dist/esm/mixins/OxyServices.accounts.js +13 -26
  40. package/dist/esm/mixins/OxyServices.auth.js +66 -201
  41. package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
  42. package/dist/esm/mixins/index.js +8 -17
  43. package/dist/esm/server/index.js +1 -1
  44. package/dist/esm/session/SessionClient.js +40 -1
  45. package/dist/esm/session/authStateStore.js +278 -0
  46. package/dist/esm/session/refresh.js +257 -0
  47. package/dist/esm/shared/utils/debugUtils.js +3 -3
  48. package/dist/esm/utils/accountUtils.js +0 -53
  49. package/dist/esm/utils/authWebUrl.js +9 -37
  50. package/dist/esm/utils/coldBoot.js +10 -8
  51. package/dist/esm/utils/platform.js +18 -0
  52. package/dist/esm/utils/registrableApex.js +46 -0
  53. package/dist/esm/utils/ssoBounce.js +14 -345
  54. package/dist/esm/utils/validationUtils.js +56 -0
  55. package/dist/types/.tsbuildinfo +1 -1
  56. package/dist/types/HttpService.d.ts +14 -1
  57. package/dist/types/OxyServices.base.d.ts +0 -52
  58. package/dist/types/OxyServices.d.ts +0 -25
  59. package/dist/types/boot/coldBootV2.d.ts +76 -0
  60. package/dist/types/boot/deviceBootReturn.d.ts +83 -0
  61. package/dist/types/crypto/keyManager.d.ts +21 -0
  62. package/dist/types/index.d.ts +14 -21
  63. package/dist/types/mixins/OxyServices.accounts.d.ts +24 -29
  64. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  65. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  66. package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
  67. package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
  68. package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
  69. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  70. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  71. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
  72. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  73. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  74. package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
  75. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  76. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  77. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  78. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
  79. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  80. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  81. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  82. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  83. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  84. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  85. package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
  86. package/dist/types/mixins/index.d.ts +5 -9
  87. package/dist/types/models/interfaces.d.ts +0 -67
  88. package/dist/types/models/session.d.ts +4 -5
  89. package/dist/types/server/index.d.ts +1 -1
  90. package/dist/types/session/SessionClient.d.ts +26 -1
  91. package/dist/types/session/authStateStore.d.ts +119 -0
  92. package/dist/types/session/refresh.d.ts +93 -0
  93. package/dist/types/shared/utils/debugUtils.d.ts +3 -3
  94. package/dist/types/utils/accountUtils.d.ts +2 -17
  95. package/dist/types/utils/authWebUrl.d.ts +9 -35
  96. package/dist/types/utils/coldBoot.d.ts +17 -14
  97. package/dist/types/utils/platform.d.ts +14 -0
  98. package/dist/types/utils/registrableApex.d.ts +31 -0
  99. package/dist/types/utils/ssoBounce.d.ts +14 -280
  100. package/dist/types/utils/validationUtils.d.ts +15 -0
  101. package/package.json +2 -2
  102. package/src/HttpService.ts +20 -4
  103. package/src/OxyServices.base.ts +7 -112
  104. package/src/OxyServices.ts +0 -38
  105. package/src/boot/__tests__/coldBootV2.test.ts +317 -0
  106. package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
  107. package/src/boot/coldBootV2.ts +426 -0
  108. package/src/boot/deviceBootReturn.ts +195 -0
  109. package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
  110. package/src/crypto/keyManager.ts +101 -0
  111. package/src/i18n/locales/en-US.json +13 -1
  112. package/src/i18n/locales/es-ES.json +13 -1
  113. package/src/index.ts +75 -65
  114. package/src/mixins/OxyServices.accounts.ts +27 -39
  115. package/src/mixins/OxyServices.auth.ts +78 -253
  116. package/src/mixins/OxyServices.deviceBoot.ts +146 -0
  117. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
  118. package/src/mixins/__tests__/accounts.test.ts +17 -44
  119. package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
  120. package/src/mixins/index.ts +10 -23
  121. package/src/models/interfaces.ts +0 -79
  122. package/src/models/session.ts +4 -5
  123. package/src/server/index.ts +1 -1
  124. package/src/session/SessionClient.ts +53 -2
  125. package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
  126. package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
  127. package/src/session/__tests__/SessionClient.state.test.ts +18 -5
  128. package/src/session/__tests__/authStateStore.test.ts +209 -0
  129. package/src/session/__tests__/refresh.test.ts +256 -0
  130. package/src/session/authStateStore.ts +335 -0
  131. package/src/session/refresh.ts +334 -0
  132. package/src/shared/utils/debugUtils.ts +3 -3
  133. package/src/utils/__tests__/authWebUrl.test.ts +5 -40
  134. package/src/utils/__tests__/registrableApex.test.ts +62 -0
  135. package/src/utils/__tests__/validationUtils.test.ts +30 -0
  136. package/src/utils/accountUtils.ts +2 -65
  137. package/src/utils/authWebUrl.ts +9 -39
  138. package/src/utils/coldBoot.ts +17 -14
  139. package/src/utils/platform.ts +21 -0
  140. package/src/utils/registrableApex.ts +45 -0
  141. package/src/utils/ssoBounce.ts +14 -393
  142. package/src/utils/validationUtils.ts +62 -0
  143. package/dist/cjs/AuthManager.js +0 -1110
  144. package/dist/cjs/AuthManagerTypes.js +0 -13
  145. package/dist/cjs/CrossDomainAuth.js +0 -206
  146. package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
  147. package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
  148. package/dist/cjs/mixins/OxyServices.silent.js +0 -204
  149. package/dist/cjs/mixins/OxyServices.sso.js +0 -208
  150. package/dist/cjs/utils/fapiAutoDetect.js +0 -99
  151. package/dist/cjs/utils/ssoEstablish.js +0 -110
  152. package/dist/cjs/utils/ssoReturn.js +0 -275
  153. package/dist/esm/AuthManager.js +0 -1105
  154. package/dist/esm/AuthManagerTypes.js +0 -12
  155. package/dist/esm/CrossDomainAuth.js +0 -201
  156. package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
  157. package/dist/esm/mixins/OxyServices.redirect.js +0 -92
  158. package/dist/esm/mixins/OxyServices.silent.js +0 -202
  159. package/dist/esm/mixins/OxyServices.sso.js +0 -204
  160. package/dist/esm/utils/fapiAutoDetect.js +0 -95
  161. package/dist/esm/utils/ssoEstablish.js +0 -107
  162. package/dist/esm/utils/ssoReturn.js +0 -271
  163. package/dist/types/AuthManager.d.ts +0 -380
  164. package/dist/types/AuthManagerTypes.d.ts +0 -81
  165. package/dist/types/CrossDomainAuth.d.ts +0 -164
  166. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
  167. package/dist/types/mixins/OxyServices.redirect.d.ts +0 -92
  168. package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
  169. package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
  170. package/dist/types/utils/fapiAutoDetect.d.ts +0 -56
  171. package/dist/types/utils/ssoEstablish.d.ts +0 -85
  172. package/dist/types/utils/ssoReturn.d.ts +0 -156
  173. package/src/AuthManager.ts +0 -1269
  174. package/src/AuthManagerTypes.ts +0 -86
  175. package/src/CrossDomainAuth.ts +0 -243
  176. package/src/__tests__/authManager.cookiePath.test.ts +0 -390
  177. package/src/__tests__/authManager.security.test.ts +0 -377
  178. package/src/__tests__/crossDomainAuth.test.ts +0 -116
  179. package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
  180. package/src/mixins/OxyServices.fedcm.ts +0 -1026
  181. package/src/mixins/OxyServices.redirect.ts +0 -122
  182. package/src/mixins/OxyServices.silent.ts +0 -272
  183. package/src/mixins/OxyServices.sso.ts +0 -261
  184. package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
  185. package/src/mixins/__tests__/fedcm.test.ts +0 -667
  186. package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
  187. package/src/mixins/__tests__/silent.test.ts +0 -102
  188. package/src/mixins/__tests__/sso.test.ts +0 -228
  189. package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
  190. package/src/utils/__tests__/fapiAutoDetect.test.ts +0 -183
  191. package/src/utils/__tests__/ssoBounce.test.ts +0 -219
  192. package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
  193. package/src/utils/__tests__/ssoReturn.test.ts +0 -276
  194. package/src/utils/fapiAutoDetect.ts +0 -91
  195. package/src/utils/ssoEstablish.ts +0 -174
  196. package/src/utils/ssoReturn.ts +0 -389
@@ -1,48 +1,19 @@
1
1
  "use strict";
2
2
  /**
3
- * Central IdP (auth web) URL resolution for cross-domain SSO.
3
+ * Central IdP apex constant.
4
4
  *
5
- * The Oxy ecosystem runs a single, central Identity Provider at
6
- * `auth.oxy.so`. For TRUE central cross-domain SSO (Google/Meta/Clerk style),
7
- * FedCM and the opaque-code SSO bounce always target this one origin it owns
8
- * the host-only `fedcm_session` cookie and the central session store reachable
9
- * via `api.oxy.so`. Relying Parties (mention.earth, homiio.com, alia.onl, …)
10
- * delegate to it rather than standing up a per-apex IdP.
11
- *
12
- * This module is intentionally pure: it performs no DOM access, reads no
13
- * `window`/`location`, and has no side effects. It is the single source of
14
- * truth for the central IdP origin so call sites never hardcode the literal.
15
- *
16
- * Note: this is distinct from `autoDetectAuthWebUrl` (per-apex `auth.<rp-apex>`
17
- * derivation). The central-SSO path deliberately does NOT auto-detect per-apex
18
- * IdPs — it is central only. An explicitly-configured `authWebUrl` still wins.
5
+ * The client SSO/FedCM resolvers (`resolveCentralAuthUrl`, `CENTRAL_AUTH_URL`)
6
+ * were removed in the device-first / legacy-final cutovers. The lone survivor is
7
+ * `CENTRAL_IDP_APEX`, kept because it has a LIVE consumer
8
+ * `@oxyhq/core/server`'s CORS helper (`server/cors.ts`'s `createOxyCors`)
9
+ * auto-allows `*.oxy.so` from it. That CORS use is permanent, so this stays
10
+ * past the SSO/FedCM teardown.
19
11
  */
20
12
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.CENTRAL_AUTH_URL = exports.CENTRAL_IDP_APEX = void 0;
22
- exports.resolveCentralAuthUrl = resolveCentralAuthUrl;
13
+ exports.CENTRAL_IDP_APEX = void 0;
23
14
  /**
24
15
  * The registrable apex (eTLD+1) of the Oxy ecosystem's central Identity
25
- * Provider. The central IdP is reachable at `auth.${CENTRAL_IDP_APEX}` and the
26
- * ID-token assertion issuer is always `https://auth.${CENTRAL_IDP_APEX}`
27
- * regardless of which per-apex `auth.<rp>` host served a given request.
28
- *
29
- * Kept as a standalone constant so the IdP worker and the SDK derive the same
30
- * literal from one source of truth (the worker imports it to brand assertions).
16
+ * Provider, reachable at `auth.${CENTRAL_IDP_APEX}`. Single source of truth so
17
+ * the CORS helper (and anything else that needs the central apex) never drifts.
31
18
  */
32
19
  exports.CENTRAL_IDP_APEX = 'oxy.so';
33
- /**
34
- * The canonical central Identity Provider origin for the Oxy ecosystem.
35
- * No trailing slash. Derived from {@link CENTRAL_IDP_APEX} so the apex and the
36
- * full origin never drift apart.
37
- */
38
- exports.CENTRAL_AUTH_URL = `https://auth.${exports.CENTRAL_IDP_APEX}`;
39
- /**
40
- * Resolve the central IdP origin, honouring an explicit override.
41
- *
42
- * @param explicit - A caller-supplied auth web URL, or `undefined`/empty to use
43
- * the central default. An explicit non-empty value always wins.
44
- * @returns The explicit value when provided, otherwise {@link CENTRAL_AUTH_URL}.
45
- */
46
- function resolveCentralAuthUrl(explicit) {
47
- return explicit ?? exports.CENTRAL_AUTH_URL;
48
- }
@@ -4,19 +4,21 @@
4
4
  * authentication resolution.
5
5
  *
6
6
  * On a fresh page load / app launch the SDK may have several ways to recover an
7
- * existing session (silent FedCM, a persisted refresh token, a cross-domain
8
- * claim, a redirect SSO return, ...). They must be attempted in a deterministic
9
- * order*, and the FIRST one that yields a session wins — every later step is
10
- * skipped. This module encodes exactly that contract and nothing else.
7
+ * existing session (a persisted refresh-token family, a shared-keychain
8
+ * identity, a cross-domain boot-fragment return, ...). They must be attempted
9
+ * in a deterministic order, and the FIRST one that yields a session wins —
10
+ * every later step is skipped. This module encodes exactly that contract and
11
+ * nothing else.
11
12
  *
12
13
  * Design constraints (all enforced):
13
14
  * - PURE: no DOM, no `navigator`, no `window`, no React, no platform globals.
14
15
  * - NO module-level mutable state. Every call to {@link runColdBoot} is fully
15
16
  * self-contained, so it is safe under bundler re-evaluation (e.g. the Metro
16
- * web bundle, which is precisely why the FedCM silent-SSO guard had to live
17
- * in consumers rather than a core singleton).
18
- * - Architecture-agnostic: both candidate cross-domain SSO designs consume
19
- * this same primitive; it knows nothing about HOW a step resolves a session.
17
+ * web bundle the reason any run-once guard for a step must live in the
18
+ * calling consumer, never in a core module-level singleton).
19
+ * - Architecture-agnostic: it knows nothing about HOW a step resolves a
20
+ * session; `runSessionColdBoot` (`boot/coldBootV2.ts`) is the current
21
+ * device-first consumer.
20
22
  *
21
23
  * A step is skipped (without running) when its `enabled` predicate returns
22
24
  * false. Any thrown error — from either `enabled` or `run` — is reported via
@@ -13,6 +13,7 @@ exports.isNative = isNative;
13
13
  exports.isIOS = isIOS;
14
14
  exports.isAndroid = isAndroid;
15
15
  exports.setPlatformOS = setPlatformOS;
16
+ exports.isWebBrowser = isWebBrowser;
16
17
  /**
17
18
  * Detect the current platform without importing react-native
18
19
  *
@@ -91,3 +92,21 @@ function setPlatformOS(os) {
91
92
  cachedPlatform = os;
92
93
  globalThis.__REACT_NATIVE_PLATFORM__ = os;
93
94
  }
95
+ /**
96
+ * True only in a real web browser (a DOM is present), false on React Native
97
+ * and Node/SSR.
98
+ *
99
+ * Native defines a global `window` but no `document`, so the DOM probe — not a
100
+ * bare `window` check — is the reliable discriminator. This is the single
101
+ * source of truth consumed by `@oxyhq/services` and `@oxyhq/auth` (both dropped
102
+ * their local copies), so every consumer shares the exact same predicate.
103
+ *
104
+ * NOTE: this is a live runtime probe (not the cached `getPlatformOS()` verdict)
105
+ * because it must reflect the actual DOM availability at call time in the
106
+ * consumer's bundle.
107
+ */
108
+ function isWebBrowser() {
109
+ return (typeof window !== 'undefined' &&
110
+ typeof document !== 'undefined' &&
111
+ typeof document.documentElement !== 'undefined');
112
+ }
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /**
3
+ * Registrable-apex (eTLD+1) host kernel.
4
+ *
5
+ * The client FAPI auto-detection helper was removed in the device-first cutover
6
+ * (which is why this file is now named for what it actually is, not the old
7
+ * `fapiAutoDetect`). What survives is the pure registrable-domain kernel, still
8
+ * used server-side by the api's device-first same-apex trust checks
9
+ * (`deviceAuth.ts`'s `POST /auth/device/web-session`, via `sameSite.ts`'s
10
+ * `isSameSiteTrustedRequest`) and the `@oxyhq/core/server` CORS/re-export layer.
11
+ *
12
+ * `registrableApex` is NOT legacy — the device-first same-apex check is a live
13
+ * consumer, so this kernel stays regardless of the SSO/FedCM removal.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.registrableApex = registrableApex;
17
+ const tldts_1 = require("tldts");
18
+ /**
19
+ * Compute the bare registrable apex (eTLD+1) of a hostname using the Public
20
+ * Suffix List, including private hosted suffixes.
21
+ *
22
+ * Performs NO protocol handling and builds NO URL — it only answers "what is
23
+ * the registrable domain of this host, or is that undefinable?".
24
+ *
25
+ * Returns `null` (apex undefinable) for:
26
+ * - empty input;
27
+ * - IPv4 literals (`192.168.1.10`);
28
+ * - IPv6 literals or any host carrying a port (`[::1]`, anything with `:`);
29
+ * - single-label hosts (`intranet`, `localhost`);
30
+ * - public suffixes without a registrable label (e.g. `co.uk`, `github.io`).
31
+ *
32
+ * @param hostname - A bare hostname (no scheme), e.g. `www.mention.earth`.
33
+ * @returns The eTLD+1 (`mention.earth`), or `null` when undefinable.
34
+ */
35
+ function registrableApex(hostname) {
36
+ if (!hostname)
37
+ return null;
38
+ const host = hostname.toLowerCase();
39
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(host))
40
+ return null;
41
+ // IPv6 literals are bracketed; any remaining ':' implies a port — neither
42
+ // yields a registrable apex.
43
+ if (host.startsWith('[') || host.includes(':'))
44
+ return null;
45
+ // The Public Suffix List (private suffixes included) is the source of truth
46
+ // for what an attacker can register. A multi-part public suffix like `co.uk`
47
+ // or a hosted suffix like `github.io` returns no registrable domain.
48
+ return (0, tldts_1.getDomain)(host, { allowPrivateDomains: true });
49
+ }
@@ -1,371 +1,24 @@
1
1
  "use strict";
2
2
  /**
3
- * Central cross-domain SSO bounce per-origin sessionStorage keys, the bounce
4
- * URL builder, and the small pure predicates shared by every consumer's
5
- * cold-boot `sso-return` / `sso-bounce` steps and bfcache `pageshow`
6
- * re-evaluation.
3
+ * SSO callback path constant.
7
4
  *
8
- * This is the single source of truth for the SSO bounce wire/storage contract.
9
- * `@oxyhq/auth` (`WebOxyProvider`) and `@oxyhq/services` (`OxyContext`) both
10
- * consume these helpers so the two providers behave identically.
5
+ * The client SSO-bounce machinery (per-origin sessionStorage keys, the bounce
6
+ * URL builder, the `guardActive` / `allowSsoBounce` / `getSsoCallbackBootstrapScript`
7
+ * predicates, …) was removed in the device-first cutover RP apps no longer
8
+ * bounce through `auth.oxy.so`. The one survivor is this path constant, still
9
+ * referenced by the api SSO controller and the IdP (both lista B, gated on the
10
+ * ecosystem bump). `@oxyhq/core/server` re-exports it for the api.
11
11
  *
12
- * TRUE central SSO (Google/Meta/Clerk style) works like this for a Relying
13
- * Party (mention.earth, homiio.com, alia.onl, …) with no local session:
14
- *
15
- * 1. `sso-bounce` (terminal, once): a TOP-LEVEL navigation to
16
- * `auth.oxy.so/sso?prompt=none&client_id=<origin>&return_to=<origin>{@link SSO_CALLBACK_PATH}&state=<s>`.
17
- * Before navigating it records, in this origin's `sessionStorage`, the
18
- * CSRF `state` ({@link ssoStateKey}), a guard timestamp ({@link ssoGuardKey},
19
- * the loop breaker), and the real destination URL ({@link ssoDestKey}) to
20
- * restore after the callback.
21
- * 2. The central IdP worker reads its first-party `fedcm_session`, mints a
22
- * session, stores it under an opaque single-use `code`, and 303-redirects
23
- * back to `<origin>{@link SSO_CALLBACK_PATH}#oxy_sso=ok&code=<code>&state=<s>`
24
- * (or `#oxy_sso=none` / `#oxy_sso=error`).
25
- * 3. `sso-return` parses the fragment (`parseSsoReturnFragment`), validates
26
- * `state`, exchanges the `code` via `oxyServices.exchangeSsoCode`, commits
27
- * the session, then restores the original destination.
28
- *
29
- * Loop proof (logged-out): first load all steps skip → `sso-bounce` sets
30
- * guard/state/dest + the outcome-independent attempted-flag
31
- * ({@link ssoAttemptedKey}) and navigates; the IdP (no central session) returns
32
- * `#oxy_sso=none`; the callback load's `sso-return` sees `none`, sets the
33
- * NO_SESSION flag ({@link ssoNoSessionKey}), and `sso-bounce` is then disabled.
34
- * Exactly ONE bounce, no loop. An interrupted bounce (user hit back
35
- * mid-redirect) self-heals once the {@link SSO_GUARD_TTL_MS} guard TTL lapses.
36
- * The attempted-flag is the definitive, outcome-INDEPENDENT loop breaker: it is
37
- * set pre-bounce so even if the return-side NO_SESSION write never lands, the
38
- * bounce can never re-fire this tab after the self-heal TTL lapses.
39
- *
40
- * All state lives in `sessionStorage` (per tab, cleared on tab close) and is
41
- * keyed per-origin so two RPs hosted in the same browser never collide. The
42
- * key strings and the 30s TTL are a wire/storage contract — they MUST match
43
- * the values the IdP and every consumer expect and must not change lightly.
12
+ * LEGACY(old-sdk): `SSO_CALLBACK_PATH` survives ONLY for the lista-B api/IdP SSO
13
+ * surface. Deletable once Homiio/Allo/Alia/Syra are bumped off the old SDK AND
14
+ * CloudWatch `/oxy/ecs` shows the `/sso*` + `/fedcm/*` routes quiet — the
15
+ * F-final sweep should remove this file then.
44
16
  */
45
17
  Object.defineProperty(exports, "__esModule", { value: true });
46
- exports.SSO_GUARD_TTL_MS = exports.SSO_CALLBACK_PATH = void 0;
47
- exports.ssoStateKey = ssoStateKey;
48
- exports.ssoGuardKey = ssoGuardKey;
49
- exports.ssoDestKey = ssoDestKey;
50
- exports.ssoNoSessionKey = ssoNoSessionKey;
51
- exports.ssoAttemptedKey = ssoAttemptedKey;
52
- exports.ssoPriorSessionKey = ssoPriorSessionKey;
53
- exports.ssoSignedOutKey = ssoSignedOutKey;
54
- exports.ssoOutcomeKey = ssoOutcomeKey;
55
- exports.ssoCallbackBootstrapKey = ssoCallbackBootstrapKey;
56
- exports.getSsoCallbackBootstrapScript = getSsoCallbackBootstrapScript;
57
- exports.ssoNavigate = ssoNavigate;
58
- exports.buildSsoBounceUrl = buildSsoBounceUrl;
59
- exports.isCentralIdPOrigin = isCentralIdPOrigin;
60
- exports.guardActive = guardActive;
61
- exports.silentRestoreSuppressed = silentRestoreSuppressed;
62
- exports.allowSsoBounce = allowSsoBounce;
63
- const authWebUrl_1 = require("./authWebUrl");
18
+ exports.SSO_CALLBACK_PATH = void 0;
64
19
  /**
65
- * The RP callback path the central IdP redirects back to after a bounce. The
66
- * SSO result is delivered in the fragment of this URL; the `sso-return` step
67
- * consumes it and then restores the user's real destination (stored under
68
- * {@link ssoDestKey}), so the user never lingers on this internal path.
20
+ * The RP callback path the central IdP redirects back to after a legacy SSO
21
+ * bounce. Kept as the single source of truth so the api/IdP never hardcode the
22
+ * literal.
69
23
  */
70
24
  exports.SSO_CALLBACK_PATH = '/__oxy/sso-callback';
71
- /**
72
- * Self-healing TTL (ms) for the bounce guard. An in-flight bounce sets a
73
- * timestamp guard; if the bounce is interrupted before the callback lands
74
- * (e.g. the user navigates back mid-redirect), the guard would otherwise pin
75
- * the RP signed-out forever. After this window the guard is treated as stale
76
- * and a fresh single bounce is permitted. 30s comfortably exceeds a real
77
- * redirect round-trip while keeping a crash short-lived.
78
- */
79
- exports.SSO_GUARD_TTL_MS = 30000;
80
- const STATE_KEY_PREFIX = 'oxy_sso_state:';
81
- const GUARD_KEY_PREFIX = 'oxy_sso_guard:';
82
- const DEST_KEY_PREFIX = 'oxy_sso_dest:';
83
- const NO_SESSION_KEY_PREFIX = 'oxy_sso_no_session:';
84
- const ATTEMPTED_KEY_PREFIX = 'oxy_sso_attempted:';
85
- const CALLBACK_BOOTSTRAP_KEY_PREFIX = 'oxy_sso_callback_bootstrap:';
86
- const PRIOR_SESSION_KEY_PREFIX = 'oxy_sso_prior_session:';
87
- const SIGNED_OUT_KEY_PREFIX = 'oxy_signed_out:';
88
- const OUTCOME_KEY_PREFIX = 'oxy_sso_outcome:';
89
- /** Per-origin CSRF state key (matched on return to defeat fragment forgery). */
90
- function ssoStateKey(origin) {
91
- return `${STATE_KEY_PREFIX}${origin}`;
92
- }
93
- /** Per-origin bounce guard key (a timestamp; loop breaker + self-heal TTL). */
94
- function ssoGuardKey(origin) {
95
- return `${GUARD_KEY_PREFIX}${origin}`;
96
- }
97
- /** Per-origin destination key (the real URL to restore after the callback). */
98
- function ssoDestKey(origin) {
99
- return `${DEST_KEY_PREFIX}${origin}`;
100
- }
101
- /**
102
- * Per-origin "the central IdP has no session for me" key. Set after a
103
- * `none`/`error` return (or a failed/forged exchange) so `sso-bounce` does not
104
- * fire again this tab — the definitive loop breaker.
105
- */
106
- function ssoNoSessionKey(origin) {
107
- return `${NO_SESSION_KEY_PREFIX}${origin}`;
108
- }
109
- /**
110
- * Per-origin, OUTCOME-INDEPENDENT once-guard. Set in `sessionStorage` BEFORE
111
- * the terminal SSO bounce navigates. Gates the bounce so the silent
112
- * cross-domain probe fires AT MOST ONCE per tab session — independent of
113
- * whether the return-side NO_SESSION flag ever lands. The definitive loop
114
- * breaker; survives the 30s self-heal `ssoGuardKey` TTL. Cleared only on an
115
- * explicit sign-out/clear so a later cold boot (after the user signs in
116
- * centrally) can probe again.
117
- */
118
- function ssoAttemptedKey(origin) {
119
- return `${ATTEMPTED_KEY_PREFIX}${origin}`;
120
- }
121
- /**
122
- * Per-origin DURABLE "this device/origin has had a signed-in Oxy session
123
- * before" hint.
124
- *
125
- * Unlike every other key in this module — which lives in per-tab
126
- * `sessionStorage` — this hint is written to DURABLE storage (web
127
- * `localStorage`; the services provider uses its own `storageKeyPrefix`-scoped
128
- * key in `@oxyhq/services`). It is set whenever a session is established or
129
- * restored and survives a session expiring; it is cleared ONLY on an explicit
130
- * full sign-out. It exists purely to drive {@link allowSsoBounce}: a returning
131
- * visitor (hint present) whose local session has lapsed still gets ONE terminal
132
- * `/sso` establish bounce to recover a session that lives only at the central
133
- * IdP, while a truly first-time anonymous visitor is never force-bounced.
134
- */
135
- function ssoPriorSessionKey(origin) {
136
- return `${PRIOR_SESSION_KEY_PREFIX}${origin}`;
137
- }
138
- /**
139
- * Per-origin DURABLE "the user DELIBERATELY signed out on this device/origin"
140
- * flag.
141
- *
142
- * Like {@link ssoPriorSessionKey} this lives in DURABLE storage (web
143
- * `localStorage`; services uses its own `storageKeyPrefix`-scoped key), NOT the
144
- * per-tab `sessionStorage` the loop-breaker keys use — it must survive a reload.
145
- *
146
- * It exists purely to suppress AUTOMATIC silent restore after a deliberate
147
- * sign-out: a still-live IdP session (the central `fedcm_session`) would
148
- * otherwise let the per-apex `/auth/silent` iframe re-mint a session on the
149
- * very next cold boot, so a user who pressed "Sign out" gets silently signed
150
- * back in on reload. With this flag set, that silent cold-boot step is
151
- * skipped while the Gmail-style returning-account fast-path is otherwise
152
- * preserved.
153
- *
154
- * Lifecycle (mirrors the existing gate machinery — set on a definitive event,
155
- * cleared on its inverse):
156
- * - SET on EXPLICIT full sign-out (alongside clearing the prior-session hint
157
- * and the SSO bounce state).
158
- * - CLEARED on ANY deliberate sign-in (password, account switch, device
159
- * claim) so a real sign-in fully re-enables silent restore — there is no
160
- * "stuck signed out" state.
161
- *
162
- * NOTE: this gates only AUTOMATIC/silent restore. An INTERACTIVE sign-in always
163
- * clears it first, so the user can always sign back in.
164
- */
165
- function ssoSignedOutKey(origin) {
166
- return `${SIGNED_OUT_KEY_PREFIX}${origin}`;
167
- }
168
- /**
169
- * Per-origin key holding the LAST consumed SSO-return outcome (`ok` | `none` |
170
- * `error`, plus an optional machine-readable `reason` on the non-`ok` outcomes).
171
- *
172
- * Lives in per-tab `sessionStorage` like the other loop-breaker keys, and for
173
- * the same reason: a `none`/`error` return HARD-navigates the RP off the
174
- * internal callback path back to its real destination (a fresh document load),
175
- * so the outcome an RP wants to render ("the central IdP had no session — show a
176
- * branded sign-in screen instead of bouncing again") must survive that
177
- * round-trip. The RP reads it on the destination load to decide whether an
178
- * AUTOMATIC (guard-driven) sign-in should re-bounce or defer to a user gesture.
179
- *
180
- * Written as a small JSON blob (`{kind, reason?}`). Set whenever a return is
181
- * consumed; cleared on a successful session commit and on an explicit
182
- * user-gesture sign-in / full sign-out (so a deliberate retry is never
183
- * suppressed by a prior automatic none/error).
184
- */
185
- function ssoOutcomeKey(origin) {
186
- return `${OUTCOME_KEY_PREFIX}${origin}`;
187
- }
188
- /**
189
- * Per-origin marker written by the pre-hydration callback bootstrap.
190
- *
191
- * Static Expo exports render unknown paths as `+not-found`; on
192
- * `/__oxy/sso-callback` that can fail hydration before the React provider has a
193
- * chance to run `consumeSsoReturn`. The bootstrap runs in the HTML head, moves
194
- * the URL to a hydratable route while preserving the SSO fragment, and writes
195
- * this marker so `consumeSsoReturn` still restores the original destination as
196
- * if the page were physically on the callback path.
197
- */
198
- function ssoCallbackBootstrapKey(origin) {
199
- return `${CALLBACK_BOOTSTRAP_KEY_PREFIX}${origin}`;
200
- }
201
- /**
202
- * Inline script for Expo/static web apps.
203
- *
204
- * Must run before the app bundle hydrates. It is intentionally tiny and
205
- * dependency-free: if the browser lands on the internal callback route with an
206
- * Oxy SSO fragment, it marks the handoff and rewrites the path to `/` while
207
- * preserving `#oxy_sso=...`. The normal SDK cold-boot `sso-return` step then
208
- * consumes the fragment from a route that can hydrate. If the internal route is
209
- * reached without a valid SSO fragment, it leaves the route via a hard root
210
- * navigation because there is no session material to preserve.
211
- */
212
- function getSsoCallbackBootstrapScript() {
213
- const callbackPath = JSON.stringify(exports.SSO_CALLBACK_PATH);
214
- const bootstrapPrefix = JSON.stringify(CALLBACK_BOOTSTRAP_KEY_PREFIX);
215
- return `(function(){var p=${callbackPath};if(window.location.pathname!==p)return;var h=window.location.hash||"";if(!/(?:^#|&)oxy_sso=(?:ok|none|error)(?:&|$)/.test(h)){window.location.replace("/");return;}try{window.sessionStorage.setItem(${bootstrapPrefix}+window.location.origin,"1");}catch(e){window.__oxySsoCallbackBootstrapError=e instanceof Error?e.message:String(e);}try{window.history.replaceState(null,"","/"+h);}catch(e){window.__oxySsoCallbackBootstrapError=e instanceof Error?e.message:String(e);window.location.replace("/"+h);}})();`;
216
- }
217
- /**
218
- * Perform the terminal top-level SSO bounce navigation.
219
- *
220
- * A thin wrapper over `window.location.assign(url)` so the single navigation
221
- * seam lives in one place (and stays mockable in tests, where jsdom's
222
- * `Location.assign` is a non-configurable native method). In production this is
223
- * exactly `window.location.assign` — the document is torn down and replaced by
224
- * the central IdP page. Off-browser (SSR / native) it is a no-op: native never
225
- * bounces.
226
- */
227
- function ssoNavigate(url) {
228
- if (typeof window === 'undefined' || typeof window.location === 'undefined') {
229
- return;
230
- }
231
- window.location.assign(url);
232
- }
233
- /**
234
- * Build the central IdP `/sso` bounce URL for an RP.
235
- *
236
- * Pure (no DOM access) so it is unit-testable and shared by every consumer's
237
- * terminal `sso-bounce` step. The IdP reads `client_id` (the RP origin) and
238
- * `return_to` to mint an origin-bound opaque code and 303-redirect back.
239
- *
240
- * The IdP base is resolved via {@link resolveCentralAuthUrl} so an explicit
241
- * `authWebUrl` override (e.g. a staging IdP) drives the SSO bounce exactly the
242
- * way it drives FedCM. When omitted, the central default {@link CENTRAL_AUTH_URL}
243
- * is used.
244
- *
245
- * @param origin - The RP origin (`window.location.origin`).
246
- * @param state - The CSRF state minted for this bounce.
247
- * @param authWebUrl - Optional explicit IdP base URL override. Falls back to
248
- * the central default when `undefined`/empty.
249
- * @returns The absolute `<idp-origin>/sso?...` URL string.
250
- */
251
- function buildSsoBounceUrl(origin, state, authWebUrl) {
252
- const url = new URL('/sso', (0, authWebUrl_1.resolveCentralAuthUrl)(authWebUrl));
253
- url.searchParams.set('prompt', 'none');
254
- url.searchParams.set('client_id', origin);
255
- url.searchParams.set('return_to', origin + exports.SSO_CALLBACK_PATH);
256
- url.searchParams.set('state', state);
257
- return url.toString();
258
- }
259
- /**
260
- * Whether `origin` IS the central IdP origin. The RP must NEVER bounce while
261
- * sitting on `auth.oxy.so` itself — doing so would loop the IdP against itself.
262
- *
263
- * Both sides are normalised via `new URL(...).origin` so a trailing-slash or
264
- * path difference never defeats the guard. Returns `false` on any parse
265
- * failure (an unparseable candidate is, by definition, not the central IdP).
266
- */
267
- function isCentralIdPOrigin(origin) {
268
- let centralOrigin;
269
- try {
270
- centralOrigin = new URL(authWebUrl_1.CENTRAL_AUTH_URL).origin;
271
- }
272
- catch {
273
- return false;
274
- }
275
- let candidateOrigin;
276
- try {
277
- candidateOrigin = new URL(origin).origin;
278
- }
279
- catch {
280
- return false;
281
- }
282
- return candidateOrigin === centralOrigin;
283
- }
284
- /**
285
- * Read the bounce guard and decide whether it is still ACTIVE.
286
- *
287
- * Active means: a guard value is present AND it parses to a finite timestamp
288
- * AND less than {@link SSO_GUARD_TTL_MS} has elapsed since it was set. An active
289
- * guard disables `sso-bounce` (a bounce is already in flight this tab). A
290
- * missing, malformed, or expired guard is NOT active, so a fresh bounce may
291
- * proceed (this is the 30s self-heal for an interrupted bounce).
292
- *
293
- * Defensive: a `getItem` that throws (e.g. a locked/disabled storage) is
294
- * treated as "not active" so the guard never wedges the flow.
295
- *
296
- * @param storage - The session storage to read (injected for testability).
297
- * @param origin - The page origin whose guard to evaluate.
298
- * @param now - Current epoch ms (injected for deterministic tests). Defaults to
299
- * `Date.now()`.
300
- */
301
- function guardActive(storage, origin, now = Date.now()) {
302
- let raw;
303
- try {
304
- raw = storage.getItem(ssoGuardKey(origin));
305
- }
306
- catch {
307
- return false;
308
- }
309
- if (raw === null || raw.length === 0) {
310
- return false;
311
- }
312
- const ts = Number(raw);
313
- if (!Number.isFinite(ts)) {
314
- return false;
315
- }
316
- return now - ts < exports.SSO_GUARD_TTL_MS;
317
- }
318
- /**
319
- * Whether AUTOMATIC silent restore is SUPPRESSED for this origin because the
320
- * user deliberately signed out (the durable {@link ssoSignedOutKey} flag).
321
- *
322
- * When `true`, the silent cold-boot step that can re-mint a session from a
323
- * still-live IdP session WITHOUT user intent — the per-apex
324
- * `/auth/silent` iframe — MUST be skipped, so a user who pressed "Sign out" is
325
- * not silently signed back in on the next reload. Interactive sign-in clears the
326
- * flag, so this never blocks a deliberate re-sign-in.
327
- *
328
- * Defensive: a `getItem` that throws (locked/disabled storage) is treated as NOT
329
- * suppressed, so the gate fails toward the normal restore behaviour rather than
330
- * wedging the user out.
331
- *
332
- * @param storage - The DURABLE storage to read (web `localStorage`; injected for
333
- * testability).
334
- * @param origin - The page origin whose signed-out flag to evaluate.
335
- */
336
- function silentRestoreSuppressed(storage, origin) {
337
- try {
338
- return storage.getItem(ssoSignedOutKey(origin)) === '1';
339
- }
340
- catch {
341
- return false;
342
- }
343
- }
344
- /**
345
- * Decide whether the terminal `/sso` establish-bounce is ALLOWED for this
346
- * visitor (the smart `enabled` gate for the `sso-bounce` cold-boot step).
347
- *
348
- * The terminal bounce is the ONLY cold-boot step that can recover a session
349
- * that lives SOLELY at the central IdP — the cross-apex Relying-Party case
350
- * (e.g. `mention.earth`, a different apex from `oxy.so`) whose device-local
351
- * session has expired and whose `Domain=oxy.so` refresh cookie never reaches
352
- * `api.<apex>`. It is also what plants the first-party per-apex `fedcm_session`
353
- * cookie that the EARLIER `silent-iframe` step later relies on. So it must fire
354
- * for a RETURNING user, yet it must NOT force a truly first-time anonymous
355
- * visitor off to the IdP.
356
- *
357
- * - ALLOW when there is a prior-signed-in hint OR a local session was
358
- * recovered this boot (a returning user) — so a central-only cross-domain
359
- * session recovers via ONE bounce, after which the per-apex cookie is
360
- * planted and subsequent loads restore silently with no bounce.
361
- * - else (no hint, no local session) SUPPRESS — a first-time anonymous
362
- * visitor browses without a forced redirect.
363
- *
364
- * This is the smart DEFAULT and the ONLY behaviour: apps never configure it.
365
- * It is also the GATE DECISION ONLY — callers still apply the per-tab loop
366
- * guards (`ssoAttemptedKey`, `ssoNoSessionKey`, {@link guardActive}) so an
367
- * allowed bounce still fires at most once per cold boot.
368
- */
369
- function allowSsoBounce(gate) {
370
- return gate.hasPriorSession || gate.hasLocalSession;
371
- }
@@ -7,6 +7,7 @@ exports.PASSWORD_REGEX = exports.USERNAME_REGEX = exports.EMAIL_REGEX = void 0;
7
7
  exports.isValidEmail = isValidEmail;
8
8
  exports.isValidUsername = isValidUsername;
9
9
  exports.isValidPassword = isValidPassword;
10
+ exports.isValidDisplayName = isValidDisplayName;
10
11
  exports.isRequiredString = isRequiredString;
11
12
  exports.isRequiredNumber = isRequiredNumber;
12
13
  exports.isRequiredBoolean = isRequiredBoolean;
@@ -52,6 +53,62 @@ function isValidUsername(username) {
52
53
  function isValidPassword(password) {
53
54
  return exports.PASSWORD_REGEX.test(password);
54
55
  }
56
+ /**
57
+ * Display-name character policy.
58
+ *
59
+ * A clean display name is composed ONLY of:
60
+ * - letters of any script (`\p{L}`),
61
+ * - combining marks / accents (`\p{M}`, e.g. the acute accent in a decomposed
62
+ * "é"),
63
+ * - Unicode space separators (`\p{Zs}`: the ASCII space, NBSP, ideographic
64
+ * space, …) — but NOT control whitespace such as tab, newline, or carriage
65
+ * return, which would break layout or enable multi-line spoofing,
66
+ * - the straight apostrophe (`'`, e.g. "O'Brien").
67
+ *
68
+ * Everything else is rejected: emoji (🐧), symbols (⁂ ⏚), `:emoji:` shortcodes,
69
+ * digits, hyphens, dots, control whitespace (tab/newline/CR), and any other
70
+ * punctuation. The allowed set `\p{L}\p{M}\p{Zs}'` explicitly EXCLUDES `<`, `>`,
71
+ * `&`, and `"`, so a value that passes this predicate can never contain an
72
+ * HTML/XSS vector.
73
+ *
74
+ * This is the SINGLE definition of the policy, shared between the API 400-gate
75
+ * (`@oxyhq/api` `displayNameSanitize.ts`) and client-side inline validation
76
+ * (the RN profile editor) so the two can never drift. It is platform-agnostic
77
+ * (no react/react-native/expo).
78
+ */
79
+ /**
80
+ * Single test for the presence of a disallowed character (non-global). The
81
+ * whitespace class is `\p{Zs}` (space separators only), NOT `\s` — the latter
82
+ * would admit tab/newline/carriage return, which break layout and enable
83
+ * multi-line spoofing.
84
+ */
85
+ const DISALLOWED_PROBE = /[^\p{L}\p{M}\p{Zs}']/u;
86
+ /**
87
+ * Single test for the presence of an orphaned combining mark (non-global) — a
88
+ * `\p{M}` not attached to a base letter (string start, whitespace, the
89
+ * apostrophe, or a position vacated by a stripped character). A mark preceded by
90
+ * `\p{L}` (a base letter, e.g. the decomposed accent in "Renée") or by another
91
+ * `\p{M}` (a multi-mark cluster) is NOT matched because the negative lookbehind
92
+ * fails at its position.
93
+ */
94
+ const ORPHANED_MARK_PROBE = /(?<![\p{L}\p{M}])\p{M}/u;
95
+ /**
96
+ * Whether `raw` already satisfies the display-name policy, i.e. it contains no
97
+ * disallowed characters AND no orphaned combining marks. Used to REJECT native
98
+ * (signup / profile edit) names with a 400 rather than silently stripping them,
99
+ * and to validate inline in the client editor.
100
+ *
101
+ * The orphaned-mark probe runs on the NFC-normalized form so a legitimate
102
+ * decomposed accent (`e`+◌́) — which normalization recomposes into `é` — is NOT
103
+ * rejected, while a lone, base-less mark (e.g. `"༘"`) IS.
104
+ *
105
+ * The function only checks the character set; an empty or whitespace-only string
106
+ * is considered valid (`true`). Call sites that require a non-empty name enforce
107
+ * that separately.
108
+ */
109
+ function isValidDisplayName(raw) {
110
+ return (!DISALLOWED_PROBE.test(raw) && !ORPHANED_MARK_PROBE.test(raw.normalize('NFC')));
111
+ }
55
112
  /**
56
113
  * Validate required string
57
114
  */