@oxyhq/core 5.5.0 → 6.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 (181) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +6 -3
  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 +47 -44
  12. package/dist/cjs/mixins/OxyServices.accounts.js +6 -13
  13. package/dist/cjs/mixins/OxyServices.auth.js +66 -201
  14. package/dist/cjs/mixins/OxyServices.authorizedApps.js +38 -0
  15. package/dist/cjs/mixins/OxyServices.deviceBoot.js +119 -0
  16. package/dist/cjs/mixins/index.js +13 -17
  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/utils/accountUtils.js +1 -55
  21. package/dist/cjs/utils/authWebUrl.js +6 -15
  22. package/dist/cjs/utils/fapiAutoDetect.js +16 -64
  23. package/dist/cjs/utils/platform.js +19 -0
  24. package/dist/cjs/utils/ssoBounce.js +15 -362
  25. package/dist/cjs/utils/validationUtils.js +57 -0
  26. package/dist/esm/.tsbuildinfo +1 -1
  27. package/dist/esm/HttpService.js +6 -3
  28. package/dist/esm/OxyServices.base.js +7 -102
  29. package/dist/esm/boot/coldBootV2.js +344 -0
  30. package/dist/esm/boot/deviceBootReturn.js +146 -0
  31. package/dist/esm/crypto/keyManager.js +95 -0
  32. package/dist/esm/i18n/locales/en-US.json +13 -1
  33. package/dist/esm/i18n/locales/es-ES.json +13 -1
  34. package/dist/esm/i18n/locales/locales/en-US.json +13 -1
  35. package/dist/esm/i18n/locales/locales/es-ES.json +13 -1
  36. package/dist/esm/index.js +28 -18
  37. package/dist/esm/mixins/OxyServices.accounts.js +6 -13
  38. package/dist/esm/mixins/OxyServices.auth.js +66 -201
  39. package/dist/esm/mixins/OxyServices.authorizedApps.js +35 -0
  40. package/dist/esm/mixins/OxyServices.deviceBoot.js +116 -0
  41. package/dist/esm/mixins/index.js +13 -17
  42. package/dist/esm/session/SessionClient.js +40 -1
  43. package/dist/esm/session/authStateStore.js +278 -0
  44. package/dist/esm/session/refresh.js +257 -0
  45. package/dist/esm/utils/accountUtils.js +0 -53
  46. package/dist/esm/utils/authWebUrl.js +6 -14
  47. package/dist/esm/utils/fapiAutoDetect.js +16 -63
  48. package/dist/esm/utils/platform.js +18 -0
  49. package/dist/esm/utils/ssoBounce.js +14 -345
  50. package/dist/esm/utils/validationUtils.js +56 -0
  51. package/dist/types/.tsbuildinfo +1 -1
  52. package/dist/types/HttpService.d.ts +13 -0
  53. package/dist/types/OxyServices.base.d.ts +0 -52
  54. package/dist/types/OxyServices.d.ts +0 -25
  55. package/dist/types/boot/coldBootV2.d.ts +76 -0
  56. package/dist/types/boot/deviceBootReturn.d.ts +83 -0
  57. package/dist/types/crypto/keyManager.d.ts +21 -0
  58. package/dist/types/index.d.ts +15 -21
  59. package/dist/types/mixins/OxyServices.accounts.d.ts +0 -2
  60. package/dist/types/mixins/OxyServices.analytics.d.ts +0 -2
  61. package/dist/types/mixins/OxyServices.appData.d.ts +0 -2
  62. package/dist/types/mixins/OxyServices.assets.d.ts +0 -2
  63. package/dist/types/mixins/OxyServices.auth.d.ts +35 -77
  64. package/dist/types/mixins/{OxyServices.redirect.d.ts → OxyServices.authorizedApps.d.ts} +35 -33
  65. package/dist/types/mixins/OxyServices.civic.d.ts +0 -2
  66. package/dist/types/mixins/OxyServices.connectedApps.d.ts +0 -2
  67. package/dist/types/mixins/OxyServices.contacts.d.ts +0 -2
  68. package/dist/types/mixins/OxyServices.deviceBoot.d.ts +110 -0
  69. package/dist/types/mixins/OxyServices.devices.d.ts +0 -2
  70. package/dist/types/mixins/OxyServices.features.d.ts +0 -2
  71. package/dist/types/mixins/OxyServices.identity.d.ts +0 -2
  72. package/dist/types/mixins/OxyServices.language.d.ts +0 -2
  73. package/dist/types/mixins/OxyServices.links.d.ts +0 -2
  74. package/dist/types/mixins/OxyServices.location.d.ts +0 -2
  75. package/dist/types/mixins/OxyServices.nodes.d.ts +0 -2
  76. package/dist/types/mixins/OxyServices.payment.d.ts +0 -2
  77. package/dist/types/mixins/OxyServices.privacy.d.ts +0 -2
  78. package/dist/types/mixins/OxyServices.reputation.d.ts +0 -2
  79. package/dist/types/mixins/OxyServices.security.d.ts +0 -2
  80. package/dist/types/mixins/OxyServices.topics.d.ts +0 -2
  81. package/dist/types/mixins/OxyServices.user.d.ts +0 -2
  82. package/dist/types/mixins/OxyServices.utility.d.ts +0 -2
  83. package/dist/types/mixins/index.d.ts +6 -9
  84. package/dist/types/models/interfaces.d.ts +0 -67
  85. package/dist/types/session/SessionClient.d.ts +25 -0
  86. package/dist/types/session/authStateStore.d.ts +119 -0
  87. package/dist/types/session/refresh.d.ts +93 -0
  88. package/dist/types/utils/accountUtils.d.ts +0 -14
  89. package/dist/types/utils/authWebUrl.d.ts +6 -12
  90. package/dist/types/utils/fapiAutoDetect.d.ts +15 -38
  91. package/dist/types/utils/platform.d.ts +14 -0
  92. package/dist/types/utils/ssoBounce.d.ts +14 -280
  93. package/dist/types/utils/validationUtils.d.ts +15 -0
  94. package/package.json +2 -2
  95. package/src/HttpService.ts +19 -3
  96. package/src/OxyServices.base.ts +7 -112
  97. package/src/OxyServices.ts +0 -38
  98. package/src/boot/__tests__/coldBootV2.test.ts +317 -0
  99. package/src/boot/__tests__/deviceBootReturn.test.ts +158 -0
  100. package/src/boot/coldBootV2.ts +426 -0
  101. package/src/boot/deviceBootReturn.ts +195 -0
  102. package/src/crypto/__tests__/sharedDeviceToken.test.ts +24 -0
  103. package/src/crypto/keyManager.ts +101 -0
  104. package/src/i18n/locales/en-US.json +13 -1
  105. package/src/i18n/locales/es-ES.json +13 -1
  106. package/src/index.ts +74 -65
  107. package/src/mixins/OxyServices.accounts.ts +6 -13
  108. package/src/mixins/OxyServices.auth.ts +78 -253
  109. package/src/mixins/OxyServices.authorizedApps.ts +75 -0
  110. package/src/mixins/OxyServices.deviceBoot.ts +146 -0
  111. package/src/mixins/__tests__/OxyServices.deviceBoot.test.ts +107 -0
  112. package/src/mixins/__tests__/accounts.test.ts +17 -44
  113. package/src/mixins/__tests__/authorizedApps.test.ts +63 -0
  114. package/src/mixins/__tests__/passwordSignIn.test.ts +91 -0
  115. package/src/mixins/index.ts +16 -22
  116. package/src/models/interfaces.ts +0 -79
  117. package/src/session/SessionClient.ts +52 -1
  118. package/src/session/__tests__/SessionClient.additive.test.ts +92 -0
  119. package/src/session/__tests__/SessionClient.rest.test.ts +25 -0
  120. package/src/session/__tests__/SessionClient.state.test.ts +18 -5
  121. package/src/session/__tests__/authStateStore.test.ts +209 -0
  122. package/src/session/__tests__/refresh.test.ts +256 -0
  123. package/src/session/authStateStore.ts +335 -0
  124. package/src/session/refresh.ts +334 -0
  125. package/src/utils/__tests__/authWebUrl.test.ts +5 -29
  126. package/src/utils/__tests__/fapiAutoDetect.test.ts +5 -126
  127. package/src/utils/__tests__/validationUtils.test.ts +30 -0
  128. package/src/utils/accountUtils.ts +0 -62
  129. package/src/utils/authWebUrl.ts +6 -15
  130. package/src/utils/fapiAutoDetect.ts +16 -60
  131. package/src/utils/platform.ts +21 -0
  132. package/src/utils/ssoBounce.ts +14 -393
  133. package/src/utils/validationUtils.ts +62 -0
  134. package/dist/cjs/AuthManager.js +0 -1110
  135. package/dist/cjs/AuthManagerTypes.js +0 -13
  136. package/dist/cjs/CrossDomainAuth.js +0 -206
  137. package/dist/cjs/mixins/OxyServices.fedcm.js +0 -823
  138. package/dist/cjs/mixins/OxyServices.redirect.js +0 -95
  139. package/dist/cjs/mixins/OxyServices.silent.js +0 -204
  140. package/dist/cjs/mixins/OxyServices.sso.js +0 -208
  141. package/dist/cjs/utils/ssoEstablish.js +0 -110
  142. package/dist/cjs/utils/ssoReturn.js +0 -275
  143. package/dist/esm/AuthManager.js +0 -1105
  144. package/dist/esm/AuthManagerTypes.js +0 -12
  145. package/dist/esm/CrossDomainAuth.js +0 -201
  146. package/dist/esm/mixins/OxyServices.fedcm.js +0 -821
  147. package/dist/esm/mixins/OxyServices.redirect.js +0 -92
  148. package/dist/esm/mixins/OxyServices.silent.js +0 -202
  149. package/dist/esm/mixins/OxyServices.sso.js +0 -204
  150. package/dist/esm/utils/ssoEstablish.js +0 -107
  151. package/dist/esm/utils/ssoReturn.js +0 -271
  152. package/dist/types/AuthManager.d.ts +0 -380
  153. package/dist/types/AuthManagerTypes.d.ts +0 -81
  154. package/dist/types/CrossDomainAuth.d.ts +0 -164
  155. package/dist/types/mixins/OxyServices.fedcm.d.ts +0 -331
  156. package/dist/types/mixins/OxyServices.silent.d.ts +0 -132
  157. package/dist/types/mixins/OxyServices.sso.d.ts +0 -138
  158. package/dist/types/utils/ssoEstablish.d.ts +0 -85
  159. package/dist/types/utils/ssoReturn.d.ts +0 -156
  160. package/src/AuthManager.ts +0 -1269
  161. package/src/AuthManagerTypes.ts +0 -86
  162. package/src/CrossDomainAuth.ts +0 -243
  163. package/src/__tests__/authManager.cookiePath.test.ts +0 -390
  164. package/src/__tests__/authManager.security.test.ts +0 -377
  165. package/src/__tests__/crossDomainAuth.test.ts +0 -116
  166. package/src/__tests__/establishDeviceRefreshSlot.test.ts +0 -221
  167. package/src/mixins/OxyServices.fedcm.ts +0 -1026
  168. package/src/mixins/OxyServices.redirect.ts +0 -122
  169. package/src/mixins/OxyServices.silent.ts +0 -272
  170. package/src/mixins/OxyServices.sso.ts +0 -261
  171. package/src/mixins/__tests__/constructorAuthWebUrl.test.ts +0 -85
  172. package/src/mixins/__tests__/fedcm.test.ts +0 -667
  173. package/src/mixins/__tests__/sessionBaseUrl.test.ts +0 -61
  174. package/src/mixins/__tests__/silent.test.ts +0 -102
  175. package/src/mixins/__tests__/sso.test.ts +0 -228
  176. package/src/utils/__tests__/consumeSsoReturn.test.ts +0 -816
  177. package/src/utils/__tests__/ssoBounce.test.ts +0 -219
  178. package/src/utils/__tests__/ssoEstablish.test.ts +0 -204
  179. package/src/utils/__tests__/ssoReturn.test.ts +0 -276
  180. package/src/utils/ssoEstablish.ts +0 -174
  181. package/src/utils/ssoReturn.ts +0 -389
@@ -1,287 +1,21 @@
1
1
  /**
2
- * Central cross-domain SSO bounce per-origin sessionStorage keys, the bounce
3
- * URL builder, and the small pure predicates shared by every consumer's
4
- * cold-boot `sso-return` / `sso-bounce` steps and bfcache `pageshow`
5
- * re-evaluation.
2
+ * SSO callback path constant.
6
3
  *
7
- * This is the single source of truth for the SSO bounce wire/storage contract.
8
- * `@oxyhq/auth` (`WebOxyProvider`) and `@oxyhq/services` (`OxyContext`) both
9
- * consume these helpers so the two providers behave identically.
4
+ * The client SSO-bounce machinery (per-origin sessionStorage keys, the bounce
5
+ * URL builder, the `guardActive` / `allowSsoBounce` / `getSsoCallbackBootstrapScript`
6
+ * predicates, …) was removed in the device-first cutover RP apps no longer
7
+ * bounce through `auth.oxy.so`. The one survivor is this path constant, still
8
+ * referenced by the api SSO controller and the IdP (both lista B, gated on the
9
+ * ecosystem bump). `@oxyhq/core/server` re-exports it for the api.
10
10
  *
11
- * TRUE central SSO (Google/Meta/Clerk style) works like this for a Relying
12
- * Party (mention.earth, homiio.com, alia.onl, …) with no local session:
13
- *
14
- * 1. `sso-bounce` (terminal, once): a TOP-LEVEL navigation to
15
- * `auth.oxy.so/sso?prompt=none&client_id=<origin>&return_to=<origin>{@link SSO_CALLBACK_PATH}&state=<s>`.
16
- * Before navigating it records, in this origin's `sessionStorage`, the
17
- * CSRF `state` ({@link ssoStateKey}), a guard timestamp ({@link ssoGuardKey},
18
- * the loop breaker), and the real destination URL ({@link ssoDestKey}) to
19
- * restore after the callback.
20
- * 2. The central IdP worker reads its first-party `fedcm_session`, mints a
21
- * session, stores it under an opaque single-use `code`, and 303-redirects
22
- * back to `<origin>{@link SSO_CALLBACK_PATH}#oxy_sso=ok&code=<code>&state=<s>`
23
- * (or `#oxy_sso=none` / `#oxy_sso=error`).
24
- * 3. `sso-return` parses the fragment (`parseSsoReturnFragment`), validates
25
- * `state`, exchanges the `code` via `oxyServices.exchangeSsoCode`, commits
26
- * the session, then restores the original destination.
27
- *
28
- * Loop proof (logged-out): first load all steps skip → `sso-bounce` sets
29
- * guard/state/dest + the outcome-independent attempted-flag
30
- * ({@link ssoAttemptedKey}) and navigates; the IdP (no central session) returns
31
- * `#oxy_sso=none`; the callback load's `sso-return` sees `none`, sets the
32
- * NO_SESSION flag ({@link ssoNoSessionKey}), and `sso-bounce` is then disabled.
33
- * Exactly ONE bounce, no loop. An interrupted bounce (user hit back
34
- * mid-redirect) self-heals once the {@link SSO_GUARD_TTL_MS} guard TTL lapses.
35
- * The attempted-flag is the definitive, outcome-INDEPENDENT loop breaker: it is
36
- * set pre-bounce so even if the return-side NO_SESSION write never lands, the
37
- * bounce can never re-fire this tab after the self-heal TTL lapses.
38
- *
39
- * All state lives in `sessionStorage` (per tab, cleared on tab close) and is
40
- * keyed per-origin so two RPs hosted in the same browser never collide. The
41
- * key strings and the 30s TTL are a wire/storage contract — they MUST match
42
- * the values the IdP and every consumer expect and must not change lightly.
11
+ * LEGACY(old-sdk): `SSO_CALLBACK_PATH` survives ONLY for the lista-B api/IdP SSO
12
+ * surface. Deletable once Homiio/Allo/Alia/Syra are bumped off the old SDK AND
13
+ * CloudWatch `/oxy/ecs` shows the `/sso*` + `/fedcm/*` routes quiet — the
14
+ * F-final sweep should remove this file then.
43
15
  */
44
16
  /**
45
- * The RP callback path the central IdP redirects back to after a bounce. The
46
- * SSO result is delivered in the fragment of this URL; the `sso-return` step
47
- * consumes it and then restores the user's real destination (stored under
48
- * {@link ssoDestKey}), so the user never lingers on this internal path.
17
+ * The RP callback path the central IdP redirects back to after a legacy SSO
18
+ * bounce. Kept as the single source of truth so the api/IdP never hardcode the
19
+ * literal.
49
20
  */
50
21
  export declare const SSO_CALLBACK_PATH = "/__oxy/sso-callback";
51
- /**
52
- * Self-healing TTL (ms) for the bounce guard. An in-flight bounce sets a
53
- * timestamp guard; if the bounce is interrupted before the callback lands
54
- * (e.g. the user navigates back mid-redirect), the guard would otherwise pin
55
- * the RP signed-out forever. After this window the guard is treated as stale
56
- * and a fresh single bounce is permitted. 30s comfortably exceeds a real
57
- * redirect round-trip while keeping a crash short-lived.
58
- */
59
- export declare const SSO_GUARD_TTL_MS = 30000;
60
- /** Per-origin CSRF state key (matched on return to defeat fragment forgery). */
61
- export declare function ssoStateKey(origin: string): string;
62
- /** Per-origin bounce guard key (a timestamp; loop breaker + self-heal TTL). */
63
- export declare function ssoGuardKey(origin: string): string;
64
- /** Per-origin destination key (the real URL to restore after the callback). */
65
- export declare function ssoDestKey(origin: string): string;
66
- /**
67
- * Per-origin "the central IdP has no session for me" key. Set after a
68
- * `none`/`error` return (or a failed/forged exchange) so `sso-bounce` does not
69
- * fire again this tab — the definitive loop breaker.
70
- */
71
- export declare function ssoNoSessionKey(origin: string): string;
72
- /**
73
- * Per-origin, OUTCOME-INDEPENDENT once-guard. Set in `sessionStorage` BEFORE
74
- * the terminal SSO bounce navigates. Gates the bounce so the silent
75
- * cross-domain probe fires AT MOST ONCE per tab session — independent of
76
- * whether the return-side NO_SESSION flag ever lands. The definitive loop
77
- * breaker; survives the 30s self-heal `ssoGuardKey` TTL. Cleared only on an
78
- * explicit sign-out/clear so a later cold boot (after the user signs in
79
- * centrally) can probe again.
80
- */
81
- export declare function ssoAttemptedKey(origin: string): string;
82
- /**
83
- * Per-origin DURABLE "this device/origin has had a signed-in Oxy session
84
- * before" hint.
85
- *
86
- * Unlike every other key in this module — which lives in per-tab
87
- * `sessionStorage` — this hint is written to DURABLE storage (web
88
- * `localStorage`; the services provider uses its own `storageKeyPrefix`-scoped
89
- * key in `@oxyhq/services`). It is set whenever a session is established or
90
- * restored and survives a session expiring; it is cleared ONLY on an explicit
91
- * full sign-out. It exists purely to drive {@link allowSsoBounce}: a returning
92
- * visitor (hint present) whose local session has lapsed still gets ONE terminal
93
- * `/sso` establish bounce to recover a session that lives only at the central
94
- * IdP, while a truly first-time anonymous visitor is never force-bounced.
95
- */
96
- export declare function ssoPriorSessionKey(origin: string): string;
97
- /**
98
- * Per-origin DURABLE "the user DELIBERATELY signed out on this device/origin"
99
- * flag.
100
- *
101
- * Like {@link ssoPriorSessionKey} this lives in DURABLE storage (web
102
- * `localStorage`; services uses its own `storageKeyPrefix`-scoped key), NOT the
103
- * per-tab `sessionStorage` the loop-breaker keys use — it must survive a reload.
104
- *
105
- * It exists purely to suppress AUTOMATIC silent restore after a deliberate
106
- * sign-out: a still-live IdP session (the central `fedcm_session`) would
107
- * otherwise let the per-apex `/auth/silent` iframe re-mint a session on the
108
- * very next cold boot, so a user who pressed "Sign out" gets silently signed
109
- * back in on reload. With this flag set, that silent cold-boot step is
110
- * skipped while the Gmail-style returning-account fast-path is otherwise
111
- * preserved.
112
- *
113
- * Lifecycle (mirrors the existing gate machinery — set on a definitive event,
114
- * cleared on its inverse):
115
- * - SET on EXPLICIT full sign-out (alongside clearing the prior-session hint
116
- * and the SSO bounce state).
117
- * - CLEARED on ANY deliberate sign-in (password, account switch, device
118
- * claim) so a real sign-in fully re-enables silent restore — there is no
119
- * "stuck signed out" state.
120
- *
121
- * NOTE: this gates only AUTOMATIC/silent restore. An INTERACTIVE sign-in always
122
- * clears it first, so the user can always sign back in.
123
- */
124
- export declare function ssoSignedOutKey(origin: string): string;
125
- /**
126
- * Per-origin key holding the LAST consumed SSO-return outcome (`ok` | `none` |
127
- * `error`, plus an optional machine-readable `reason` on the non-`ok` outcomes).
128
- *
129
- * Lives in per-tab `sessionStorage` like the other loop-breaker keys, and for
130
- * the same reason: a `none`/`error` return HARD-navigates the RP off the
131
- * internal callback path back to its real destination (a fresh document load),
132
- * so the outcome an RP wants to render ("the central IdP had no session — show a
133
- * branded sign-in screen instead of bouncing again") must survive that
134
- * round-trip. The RP reads it on the destination load to decide whether an
135
- * AUTOMATIC (guard-driven) sign-in should re-bounce or defer to a user gesture.
136
- *
137
- * Written as a small JSON blob (`{kind, reason?}`). Set whenever a return is
138
- * consumed; cleared on a successful session commit and on an explicit
139
- * user-gesture sign-in / full sign-out (so a deliberate retry is never
140
- * suppressed by a prior automatic none/error).
141
- */
142
- export declare function ssoOutcomeKey(origin: string): string;
143
- /**
144
- * Per-origin marker written by the pre-hydration callback bootstrap.
145
- *
146
- * Static Expo exports render unknown paths as `+not-found`; on
147
- * `/__oxy/sso-callback` that can fail hydration before the React provider has a
148
- * chance to run `consumeSsoReturn`. The bootstrap runs in the HTML head, moves
149
- * the URL to a hydratable route while preserving the SSO fragment, and writes
150
- * this marker so `consumeSsoReturn` still restores the original destination as
151
- * if the page were physically on the callback path.
152
- */
153
- export declare function ssoCallbackBootstrapKey(origin: string): string;
154
- /**
155
- * Inline script for Expo/static web apps.
156
- *
157
- * Must run before the app bundle hydrates. It is intentionally tiny and
158
- * dependency-free: if the browser lands on the internal callback route with an
159
- * Oxy SSO fragment, it marks the handoff and rewrites the path to `/` while
160
- * preserving `#oxy_sso=...`. The normal SDK cold-boot `sso-return` step then
161
- * consumes the fragment from a route that can hydrate. If the internal route is
162
- * reached without a valid SSO fragment, it leaves the route via a hard root
163
- * navigation because there is no session material to preserve.
164
- */
165
- export declare function getSsoCallbackBootstrapScript(): string;
166
- /**
167
- * Perform the terminal top-level SSO bounce navigation.
168
- *
169
- * A thin wrapper over `window.location.assign(url)` so the single navigation
170
- * seam lives in one place (and stays mockable in tests, where jsdom's
171
- * `Location.assign` is a non-configurable native method). In production this is
172
- * exactly `window.location.assign` — the document is torn down and replaced by
173
- * the central IdP page. Off-browser (SSR / native) it is a no-op: native never
174
- * bounces.
175
- */
176
- export declare function ssoNavigate(url: string): void;
177
- /**
178
- * Build the central IdP `/sso` bounce URL for an RP.
179
- *
180
- * Pure (no DOM access) so it is unit-testable and shared by every consumer's
181
- * terminal `sso-bounce` step. The IdP reads `client_id` (the RP origin) and
182
- * `return_to` to mint an origin-bound opaque code and 303-redirect back.
183
- *
184
- * The IdP base is resolved via {@link resolveCentralAuthUrl} so an explicit
185
- * `authWebUrl` override (e.g. a staging IdP) drives the SSO bounce exactly the
186
- * way it drives FedCM. When omitted, the central default {@link CENTRAL_AUTH_URL}
187
- * is used.
188
- *
189
- * @param origin - The RP origin (`window.location.origin`).
190
- * @param state - The CSRF state minted for this bounce.
191
- * @param authWebUrl - Optional explicit IdP base URL override. Falls back to
192
- * the central default when `undefined`/empty.
193
- * @returns The absolute `<idp-origin>/sso?...` URL string.
194
- */
195
- export declare function buildSsoBounceUrl(origin: string, state: string, authWebUrl?: string): string;
196
- /**
197
- * Whether `origin` IS the central IdP origin. The RP must NEVER bounce while
198
- * sitting on `auth.oxy.so` itself — doing so would loop the IdP against itself.
199
- *
200
- * Both sides are normalised via `new URL(...).origin` so a trailing-slash or
201
- * path difference never defeats the guard. Returns `false` on any parse
202
- * failure (an unparseable candidate is, by definition, not the central IdP).
203
- */
204
- export declare function isCentralIdPOrigin(origin: string): boolean;
205
- /**
206
- * Read the bounce guard and decide whether it is still ACTIVE.
207
- *
208
- * Active means: a guard value is present AND it parses to a finite timestamp
209
- * AND less than {@link SSO_GUARD_TTL_MS} has elapsed since it was set. An active
210
- * guard disables `sso-bounce` (a bounce is already in flight this tab). A
211
- * missing, malformed, or expired guard is NOT active, so a fresh bounce may
212
- * proceed (this is the 30s self-heal for an interrupted bounce).
213
- *
214
- * Defensive: a `getItem` that throws (e.g. a locked/disabled storage) is
215
- * treated as "not active" so the guard never wedges the flow.
216
- *
217
- * @param storage - The session storage to read (injected for testability).
218
- * @param origin - The page origin whose guard to evaluate.
219
- * @param now - Current epoch ms (injected for deterministic tests). Defaults to
220
- * `Date.now()`.
221
- */
222
- export declare function guardActive(storage: Pick<Storage, 'getItem'>, origin: string, now?: number): boolean;
223
- /**
224
- * Whether AUTOMATIC silent restore is SUPPRESSED for this origin because the
225
- * user deliberately signed out (the durable {@link ssoSignedOutKey} flag).
226
- *
227
- * When `true`, the silent cold-boot step that can re-mint a session from a
228
- * still-live IdP session WITHOUT user intent — the per-apex
229
- * `/auth/silent` iframe — MUST be skipped, so a user who pressed "Sign out" is
230
- * not silently signed back in on the next reload. Interactive sign-in clears the
231
- * flag, so this never blocks a deliberate re-sign-in.
232
- *
233
- * Defensive: a `getItem` that throws (locked/disabled storage) is treated as NOT
234
- * suppressed, so the gate fails toward the normal restore behaviour rather than
235
- * wedging the user out.
236
- *
237
- * @param storage - The DURABLE storage to read (web `localStorage`; injected for
238
- * testability).
239
- * @param origin - The page origin whose signed-out flag to evaluate.
240
- */
241
- export declare function silentRestoreSuppressed(storage: Pick<Storage, 'getItem'>, origin: string): boolean;
242
- /**
243
- * Inputs to the smart {@link allowSsoBounce} gate.
244
- */
245
- export interface SsoBounceGate {
246
- /**
247
- * Whether this device/origin has had a signed-in Oxy session before (the
248
- * durable {@link ssoPriorSessionKey} hint). Set whenever a session is
249
- * established or restored; survives session expiry; cleared only on explicit
250
- * full sign-out. `true` ⇒ a returning visitor.
251
- */
252
- readonly hasPriorSession: boolean;
253
- /**
254
- * Whether a local/stored session was recovered earlier this cold boot. At the
255
- * terminal bounce gate this is effectively always `false` (an earlier step
256
- * would have won and short-circuited), but it is part of the contract — "no
257
- * prior hint AND no local session" — so it is passed explicitly for fidelity
258
- * and robustness.
259
- */
260
- readonly hasLocalSession: boolean;
261
- }
262
- /**
263
- * Decide whether the terminal `/sso` establish-bounce is ALLOWED for this
264
- * visitor (the smart `enabled` gate for the `sso-bounce` cold-boot step).
265
- *
266
- * The terminal bounce is the ONLY cold-boot step that can recover a session
267
- * that lives SOLELY at the central IdP — the cross-apex Relying-Party case
268
- * (e.g. `mention.earth`, a different apex from `oxy.so`) whose device-local
269
- * session has expired and whose `Domain=oxy.so` refresh cookie never reaches
270
- * `api.<apex>`. It is also what plants the first-party per-apex `fedcm_session`
271
- * cookie that the EARLIER `silent-iframe` step later relies on. So it must fire
272
- * for a RETURNING user, yet it must NOT force a truly first-time anonymous
273
- * visitor off to the IdP.
274
- *
275
- * - ALLOW when there is a prior-signed-in hint OR a local session was
276
- * recovered this boot (a returning user) — so a central-only cross-domain
277
- * session recovers via ONE bounce, after which the per-apex cookie is
278
- * planted and subsequent loads restore silently with no bounce.
279
- * - else (no hint, no local session) SUPPRESS — a first-time anonymous
280
- * visitor browses without a forced redirect.
281
- *
282
- * This is the smart DEFAULT and the ONLY behaviour: apps never configure it.
283
- * It is also the GATE DECISION ONLY — callers still apply the per-tab loop
284
- * guards (`ssoAttemptedKey`, `ssoNoSessionKey`, {@link guardActive}) so an
285
- * allowed bounce still fires at most once per cold boot.
286
- */
287
- export declare function allowSsoBounce(gate: SsoBounceGate): boolean;
@@ -25,6 +25,21 @@ 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
+ * Whether `raw` already satisfies the display-name policy, i.e. it contains no
30
+ * disallowed characters AND no orphaned combining marks. Used to REJECT native
31
+ * (signup / profile edit) names with a 400 rather than silently stripping them,
32
+ * and to validate inline in the client editor.
33
+ *
34
+ * The orphaned-mark probe runs on the NFC-normalized form so a legitimate
35
+ * decomposed accent (`e`+◌́) — which normalization recomposes into `é` — is NOT
36
+ * rejected, while a lone, base-less mark (e.g. `"༘"`) IS.
37
+ *
38
+ * The function only checks the character set; an empty or whitespace-only string
39
+ * is considered valid (`true`). Call sites that require a non-empty name enforce
40
+ * that separately.
41
+ */
42
+ export declare function isValidDisplayName(raw: string): boolean;
28
43
  /**
29
44
  * Validate required string
30
45
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "5.5.0",
3
+ "version": "6.0.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",
@@ -94,7 +94,7 @@
94
94
  }
95
95
  },
96
96
  "dependencies": {
97
- "@oxyhq/contracts": "^0.8.0",
97
+ "@oxyhq/contracts": "^0.9.1",
98
98
  "@oxyhq/protocol": "^0.1.1",
99
99
  "bip39": "^3.1.0",
100
100
  "buffer": "^6.0.3",
@@ -70,6 +70,19 @@ export interface RequestOptions {
70
70
  signal?: AbortSignal;
71
71
  headers?: Record<string, string>;
72
72
  responseType?: 'blob';
73
+ /**
74
+ * Skip BOTH the bearer auth header (and its near-expiry preflight refresh)
75
+ * AND the 401-driven auto-refresh/retry for this request.
76
+ *
77
+ * Required for the body-authenticated refresh endpoint (`POST
78
+ * /auth/refresh-token`): it does not need a bearer, and — critically — it is
79
+ * itself invoked from inside the registered `AuthRefreshHandler`. If it went
80
+ * through the normal preflight, `getAuthHeader` would call
81
+ * `refreshAccessToken` while the handler-owning `tokenRefreshPromise` is
82
+ * still in flight and await ITSELF (deadlock). Skipping auth makes the
83
+ * refresh call fully independent of the current (near-expired) bearer.
84
+ */
85
+ skipAuth?: boolean;
73
86
  }
74
87
 
75
88
  interface RequestConfig extends RequestOptions {
@@ -403,8 +416,11 @@ export class HttpService {
403
416
  // Build URL with params
404
417
  const fullUrl = this.buildURL(url, params);
405
418
 
406
- // Get auth token (with auto-refresh)
407
- const authHeader = await this.getAuthHeader();
419
+ // Get auth token (with auto-refresh). `skipAuth` requests (the
420
+ // body-authenticated refresh endpoint) send NO bearer and skip the
421
+ // near-expiry preflight — see RequestOptions.skipAuth for the deadlock
422
+ // this avoids.
423
+ const authHeader = config.skipAuth ? null : await this.getAuthHeader();
408
424
 
409
425
  // CSRF protects cookie-authenticated browser writes. Bearer-authenticated
410
426
  // SDK clients are not vulnerable to ambient-cookie CSRF, and linked app
@@ -514,7 +530,7 @@ export class HttpService {
514
530
  // On 401, delegate refresh to AuthManager and retry once before
515
531
  // giving up. HttpService deliberately does not know any session
516
532
  // routes; the AuthManager is the single session authority.
517
- if (response.status === 401 && !config._isAuthRetry) {
533
+ if (response.status === 401 && !config._isAuthRetry && !config.skipAuth) {
518
534
  const refreshed = await this.refreshAccessToken('response-401');
519
535
  if (refreshed) {
520
536
  // `deduplicate: false` is REQUIRED on the retry (mirrors the 403
@@ -8,10 +8,6 @@ import type { OxyConfig as OxyConfigBase, ApiError, User } from './models/interf
8
8
  import { handleHttpError } from './utils/errorUtils';
9
9
  import { HttpService, type AuthRefreshReason, type RequestOptions } from './HttpService';
10
10
  import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
11
- import { resolveCentralAuthUrl } from './utils/authWebUrl';
12
- import { isWeb } from './utils/platform';
13
- import { registrableApex } from './utils/fapiAutoDetect';
14
- import { logger } from './utils/loggerUtils';
15
11
 
16
12
  export interface OxyConfig extends OxyConfigBase {
17
13
  cloudURL?: string;
@@ -44,23 +40,15 @@ export class OxyServicesBase {
44
40
  throw new Error('OxyConfig is required');
45
41
  }
46
42
 
47
- // Default `authWebUrl` to the CENTRAL IdP (`auth.oxy.so`) when the caller
48
- // did not pin it explicitly. TRUE central cross-domain SSO (Google/Meta/
49
- // Clerk style) routes every RP through the one central IdP it owns the
50
- // host-only `fedcm_session` cookie and the central session store so the
51
- // SDK no longer derives a per-apex `auth.<rp-apex>` IdP by default.
52
- // `autoDetectAuthWebUrl` is still exported for any call site that opts into
53
- // per-apex resolution, but it is NOT the constructor default anymore.
54
- // An explicit `authWebUrl` always wins (we only fill it when absent).
55
- const resolvedConfig: OxyConfig = config.authWebUrl
56
- ? config
57
- : { ...config, authWebUrl: resolveCentralAuthUrl(config.authWebUrl) };
58
-
59
- this.config = resolvedConfig;
60
- this.cloudURL = resolvedConfig.cloudURL || 'https://cloud.oxy.so';
43
+ // `authWebUrl` is a plain optional config value now (used only for building
44
+ // third-party "Sign in with Oxy" OAuth links). The SDK no longer derives or
45
+ // defaults an IdP host the device-first cold boot restores sessions from
46
+ // the persisted refresh store, not an `auth.<apex>` bounce.
47
+ this.config = config;
48
+ this.cloudURL = config.cloudURL || 'https://cloud.oxy.so';
61
49
 
62
50
  // Initialize unified HTTP service (handles auth, caching, deduplication, queuing, retry)
63
- this.httpService = new HttpService(resolvedConfig);
51
+ this.httpService = new HttpService(config);
64
52
  }
65
53
 
66
54
  // Test-only utility to reset tokens on this instance between jest tests
@@ -99,23 +87,6 @@ export class OxyServicesBase {
99
87
  return this.httpService.getBaseURL();
100
88
  }
101
89
 
102
- /**
103
- * Get the base URL the SDK's first-party session/refresh calls should target.
104
- *
105
- * Returns the configured `sessionBaseUrl` when provided, otherwise falls back
106
- * to the API `baseURL` (`getBaseURL()`). Per the 2026 session architecture
107
- * (docs/SESSION-ARCHITECTURE.md), non-`oxy.so` apps point this at their own
108
- * same-site backend (e.g. `https://api.mention.earth`) whose session bridge
109
- * forwards the user's refresh credential to `api.oxy.so`; `*.oxy.so` apps
110
- * leave it unset so it resolves to `https://api.oxy.so` and nothing changes.
111
- *
112
- * This is additive: it only exposes configuration for `@oxyhq/services` to
113
- * consume in a later phase. No refresh/auth logic in core reads it yet.
114
- */
115
- public getSessionBaseUrl(): string {
116
- return this.config.sessionBaseUrl ?? this.getBaseURL();
117
- }
118
-
119
90
  /**
120
91
  * Get the HTTP service instance
121
92
  * Useful for advanced use cases where direct access to the HTTP service is needed
@@ -313,82 +284,6 @@ export class OxyServicesBase {
313
284
  return this.httpService.getAccessToken();
314
285
  }
315
286
 
316
- /**
317
- * Register the CURRENTLY-ACTIVE session in the device's first-party
318
- * multi-account refresh-cookie set by calling `POST /auth/session`.
319
- *
320
- * This is the single, shared primitive every web primary-session commit and the
321
- * account switch use to plant their `oxy_rt_<authuser>` slot. It MUST be a
322
- * dedicated call to `/auth/session` rather than relying on whichever endpoint
323
- * established the session: that endpoint is frequently OUTSIDE the cookie's
324
- * `Path=/auth` scope (`/accounts/:id/switch`) or is a cross-origin/credential-
325
- * less restore (`/sso/exchange`, the IdP `/auth/silent` postMessage) that cannot
326
- * set an `api.oxy.so` cookie at all. `/auth/session` runs where the device's
327
- * existing slots ARE visible, so the server resolves this user's slot (reusing an
328
- * existing one or allocating a new one) without clobbering a sibling account,
329
- * mints a fresh access token bound to the same session, and returns the resolved
330
- * `authuser`.
331
- *
332
- * Behaviour:
333
- * - Requires a planted bearer (the caller must have already installed the
334
- * session's access token); `/auth/session` derives the session from it.
335
- * - On success re-plants the rotated access token (so the active token matches
336
- * the freshly-rotated cookie) and returns the device `authuser` slot.
337
- * - WEB-ONLY: on native there are no first-party refresh cookies → returns
338
- * `null`.
339
- * - FIRST-PARTY-ONLY: the cookie is host-only on the API host with
340
- * `SameSite=Lax`, so it only sticks when the page is SAME-SITE (same
341
- * registrable apex) as the API. On a cross-apex RP (`mention.earth` calling
342
- * `api.oxy.so`) the browser rejects the `Set-Cookie` as a third-party cookie,
343
- * so a returned slot would be a phantom never enumerated by `refresh-all`.
344
- * Those RPs durably restore via the per-apex `/auth/silent` iframe + `/sso`
345
- * bounce, NOT this device set → returns `null` without calling the API.
346
- * - BEST-EFFORT: a failure (e.g. transient network) never throws — the session
347
- * stays active in-memory; only its reload durability via the device set is at
348
- * risk. The caller treats `null` as "not registered in the device set".
349
- *
350
- * @returns The resolved device `authuser` slot, or `null` on native / cross-apex
351
- * / failure.
352
- */
353
- public async establishDeviceRefreshSlot(): Promise<number | null> {
354
- if (!isWeb()) {
355
- return null;
356
- }
357
- if (typeof window !== 'undefined' && window.location?.hostname) {
358
- const pageApex = registrableApex(window.location.hostname);
359
- let apiApex: string | null = null;
360
- try {
361
- apiApex = registrableApex(new URL(this.getBaseURL()).hostname);
362
- } catch {
363
- apiApex = null;
364
- }
365
- if (!pageApex || !apiApex || pageApex !== apiApex) {
366
- return null;
367
- }
368
- }
369
- try {
370
- const established = await this.makeRequest<{ accessToken?: string; authuser?: number }>(
371
- 'POST',
372
- '/auth/session',
373
- undefined,
374
- { cache: false },
375
- );
376
- // `/auth/session` mints a fresh access token off the same session; re-plant
377
- // it so the active token matches the rotated cookie.
378
- if (established?.accessToken) {
379
- this.setTokens(established.accessToken);
380
- }
381
- return typeof established?.authuser === 'number' ? established.authuser : null;
382
- } catch (error) {
383
- logger.warn(
384
- '[OxyServices] Failed to establish device refresh cookie via POST /auth/session; the session is active in-session but may not survive a reload as part of the device account set',
385
- { component: 'OxyServices', method: 'establishDeviceRefreshSlot' },
386
- error,
387
- );
388
- return null;
389
- }
390
- }
391
-
392
287
  /**
393
288
  * Decode the current access token and return its `exp` claim in SECONDS since
394
289
  * the Unix epoch (the raw JWT `exp` unit), or `null` when there is no token,
@@ -58,10 +58,6 @@
58
58
  */
59
59
  import { OxyServicesBase, type LinkedHttpClient, type OxyConfig } from './OxyServices.base';
60
60
  import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
61
- import type { SessionLoginResponse } from './models/session';
62
- import type { FedCMAuthOptions, FedCMConfig } from './mixins/OxyServices.fedcm';
63
- import type { SilentAuthOptions } from './mixins/OxyServices.silent';
64
- import type { RedirectAuthOptions } from './mixins/OxyServices.redirect';
65
61
 
66
62
  // Import mixin composition helper
67
63
  import { composeOxyServices } from './mixins';
@@ -111,16 +107,6 @@ const OxyServicesComposed = composeOxyServices();
111
107
  // We extend the composed constructor directly — its public surface is broadened
112
108
  // to the full mixin set via the interface declaration that follows.
113
109
  export class OxyServices extends OxyServicesComposed {
114
- /**
115
- * FedCM credential-request timeouts (ms). The runtime values are defined on
116
- * the FedCM mixin and inherited here via `extends`; these `declare` members
117
- * surface their types to TypeScript without re-emitting (or duplicating) the
118
- * literals, so consumers/tests can reference `OxyServices.FEDCM_SILENT_TIMEOUT`
119
- * with full typing.
120
- */
121
- declare static readonly FEDCM_TIMEOUT: number;
122
- declare static readonly FEDCM_SILENT_TIMEOUT: number;
123
-
124
110
  constructor(config: OxyConfig) {
125
111
  super(config);
126
112
  }
@@ -133,30 +119,6 @@ export class OxyServices extends OxyServicesComposed {
133
119
  export interface OxyServices extends InstanceType<ReturnType<typeof composeOxyServices>> {
134
120
  createLinkedClient(config: OxyConfig): LinkedHttpClient;
135
121
 
136
- // FedCM authentication
137
- isFedCMSupported(): boolean;
138
- signInWithFedCM(options?: FedCMAuthOptions): Promise<SessionLoginResponse>;
139
- silentSignInWithFedCM(): Promise<SessionLoginResponse | null>;
140
- revokeFedCMCredential(): Promise<void>;
141
- getFedCMConfig(): FedCMConfig;
142
-
143
- // Silent iframe SSO
144
- resolveAuthUrl(): string;
145
- silentSignIn(options?: SilentAuthOptions): Promise<SessionLoginResponse | null>;
146
- waitForIframeAuth(
147
- iframe: HTMLIFrameElement,
148
- timeout: number,
149
- expectedOrigin: string,
150
- ): Promise<SessionLoginResponse | null>;
151
-
152
- // Redirect authentication
153
- signInWithRedirect(options?: RedirectAuthOptions): void;
154
- signUpWithRedirect(options?: RedirectAuthOptions): void;
155
-
156
- // Central cross-domain SSO (opaque single-use code exchange)
157
- exchangeSsoCode(code: string, state?: string): Promise<SessionLoginResponse>;
158
- generateSsoState(): string;
159
-
160
122
  // Express.js middleware
161
123
  auth(options?: {
162
124
  debug?: boolean;