@oxyhq/core 1.11.14 → 1.11.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "1.11.14",
3
+ "version": "1.11.15",
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",
@@ -212,8 +212,53 @@ export function OxyServicesPopupAuthMixin<T extends typeof OxyServicesBase>(Base
212
212
  try {
213
213
  const session = await this.waitForIframeAuth(iframe, timeout, clientId);
214
214
 
215
- if (session && (session as any).accessToken) {
216
- this.httpService.setTokens((session as any).accessToken);
215
+ // Bail early on incomplete responses. The iframe contract requires
216
+ // both an access token and a session id; anything less is unusable.
217
+ // Returning `null` here (without installing the token) prevents a
218
+ // stale credential from being committed to HttpService when the
219
+ // user is actually signed out — that pattern caused a `getCurrentUser`
220
+ // -> 401 -> token-clear loop in consumer apps because callers gated
221
+ // on `session?.user` and never installed the user via
222
+ // `handleAuthSuccess`, while HttpService quietly held the token.
223
+ const accessToken = session ? (session as { accessToken?: string }).accessToken : undefined;
224
+ if (!session || !accessToken || !session.sessionId) {
225
+ return null;
226
+ }
227
+
228
+ // Snapshot the previous token so we can roll back if the user
229
+ // lookup below fails — this avoids leaving a half-committed session
230
+ // (token installed, user missing) which would let the next
231
+ // authenticated request 401 with no way to recover.
232
+ const previousAccessToken = this.httpService.getAccessToken();
233
+ this.httpService.setTokens(accessToken);
234
+
235
+ // The iframe typically returns `{ sessionId, accessToken }` without
236
+ // user data. Fetch the user explicitly so callers receive a
237
+ // fully-formed session and never need a second `/users/me` round
238
+ // trip. If this fails the session is unusable — revert the token
239
+ // and return null so the caller treats this exactly like a
240
+ // missing-session response.
241
+ if (!session.user) {
242
+ try {
243
+ const userData = await this.makeRequest<unknown>(
244
+ 'GET',
245
+ `/session/user/${session.sessionId}`,
246
+ undefined,
247
+ { cache: false, retry: false }
248
+ );
249
+ if (!userData) {
250
+ throw new Error('Empty user response');
251
+ }
252
+ (session as { user?: unknown }).user = userData;
253
+ } catch (userError) {
254
+ debug.warn('silentSignIn: failed to fetch user data, rolling back token', userError);
255
+ if (previousAccessToken) {
256
+ this.httpService.setTokens(previousAccessToken);
257
+ } else {
258
+ this.httpService.clearTokens();
259
+ }
260
+ return null;
261
+ }
217
262
  }
218
263
 
219
264
  return session;