@oxyhq/core 2.1.0 → 2.1.2

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": "2.1.0",
3
+ "version": "2.1.2",
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",
@@ -24,6 +24,7 @@
24
24
 
25
25
  import type { OxyServices } from './OxyServices';
26
26
  import type { SessionLoginResponse } from './models/session';
27
+ import { logger } from './utils/loggerUtils';
27
28
 
28
29
  export interface CrossDomainAuthOptions {
29
30
  /**
@@ -143,7 +144,7 @@ export class CrossDomainAuth {
143
144
  this.closeOrphanPopup(options.popup);
144
145
  return session;
145
146
  } catch (error) {
146
- console.warn('[CrossDomainAuth] FedCM failed, trying popup...', error);
147
+ logger.warn('FedCM failed, trying popup', { component: 'CrossDomainAuth', method: 'autoSignIn' }, error);
147
148
  }
148
149
  }
149
150
 
@@ -152,7 +153,7 @@ export class CrossDomainAuth {
152
153
  options.onMethodSelected?.('popup');
153
154
  return await this.signInWithPopup(options);
154
155
  } catch (error) {
155
- console.warn('[CrossDomainAuth] Popup failed, falling back to redirect...', error);
156
+ logger.warn('Popup failed, falling back to redirect', { component: 'CrossDomainAuth', method: 'autoSignIn' }, error);
156
157
  // Popup path failed — close the pre-opened popup before redirecting.
157
158
  this.closeOrphanPopup(options.popup);
158
159
  }
@@ -226,7 +227,7 @@ export class CrossDomainAuth {
226
227
  return session;
227
228
  }
228
229
  } catch (error) {
229
- console.warn('[CrossDomainAuth] FedCM silent sign-in failed:', error);
230
+ logger.debug('FedCM silent sign-in did not resolve', { component: 'CrossDomainAuth', method: 'silentSignIn' }, error);
230
231
  }
231
232
  }
232
233
 
@@ -234,7 +235,7 @@ export class CrossDomainAuth {
234
235
  try {
235
236
  return await this.oxyServices.silentSignIn();
236
237
  } catch (error) {
237
- console.warn('[CrossDomainAuth] Silent sign-in failed:', error);
238
+ logger.debug('iframe silent sign-in did not resolve', { component: 'CrossDomainAuth', method: 'silentSignIn' }, error);
238
239
  return null;
239
240
  }
240
241
  }
@@ -326,7 +327,7 @@ export class CrossDomainAuth {
326
327
  };
327
328
  }
328
329
  } catch (error) {
329
- console.warn('[CrossDomainAuth] Stored session invalid:', error);
330
+ logger.debug('stored session invalid', { component: 'CrossDomainAuth', method: 'initialize' }, error);
330
331
  }
331
332
  }
332
333
 
@@ -8,6 +8,7 @@ import type { OxyConfig as OxyConfigBase, ApiError, User } from './models/interf
8
8
  import { handleHttpError } from './utils/errorUtils';
9
9
  import { HttpService, type RequestOptions } from './HttpService';
10
10
  import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
11
+ import { autoDetectAuthWebUrl } from './utils/fapiAutoDetect';
11
12
 
12
13
  export interface OxyConfig extends OxyConfigBase {
13
14
  cloudURL?: string;
@@ -34,11 +35,25 @@ export class OxyServicesBase {
34
35
  if (!config || typeof config !== 'object') {
35
36
  throw new Error('OxyConfig is required');
36
37
  }
37
- this.config = config;
38
- this.cloudURL = config.cloudURL || 'https://cloud.oxy.so';
38
+
39
+ // Auto-detect the first-party IdP (`auth.<rp-apex>`) when the caller did not
40
+ // pin `authWebUrl` explicitly. This mirrors the provider-`baseURL` path in
41
+ // `@oxyhq/services` (OxyContext), so apps that construct their OWN
42
+ // `OxyServices` instance and pass it to `<OxyProvider oxyServices={...} />`
43
+ // get the same same-site IdP resolution. On web at `https://mention.earth`
44
+ // this yields `https://auth.mention.earth`; on native/SSR (no `window`)
45
+ // `autoDetectAuthWebUrl()` returns `undefined`, leaving the auth mixins'
46
+ // `DEFAULT_AUTH_URL` fallback in effect — native behavior is unchanged.
47
+ // An explicit `authWebUrl` always wins (we only fill it when absent).
48
+ const resolvedConfig: OxyConfig = config.authWebUrl
49
+ ? config
50
+ : { ...config, authWebUrl: autoDetectAuthWebUrl() };
51
+
52
+ this.config = resolvedConfig;
53
+ this.cloudURL = resolvedConfig.cloudURL || 'https://cloud.oxy.so';
39
54
 
40
55
  // Initialize unified HTTP service (handles auth, caching, deduplication, queuing, retry)
41
- this.httpService = new HttpService(config);
56
+ this.httpService = new HttpService(resolvedConfig);
42
57
  }
43
58
 
44
59
  // Test-only utility to reset tokens on this instance between jest tests
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `OxyServices` constructor first-party IdP auto-detection.
3
+ *
4
+ * `@oxyhq/services` 8.2.0 added cross-domain SSO that auto-detects the IdP as
5
+ * `https://auth.<rp-apex>` via `autoDetectAuthWebUrl()`. That detection used to
6
+ * run ONLY on the provider-`baseURL` branch of OxyContext — apps that construct
7
+ * their OWN `OxyServices` instance and pass it to
8
+ * `<OxyProvider oxyServices={...} />` never hit it, so an omitted `authWebUrl`
9
+ * fell back to the hardcoded `DEFAULT_AUTH_URL` ('https://auth.oxy.so'),
10
+ * forcing a third-party IdP and breaking Safari/Firefox cross-domain restore.
11
+ *
12
+ * The constructor now derives `authWebUrl` itself when the caller omits it, so
13
+ * BOTH construction paths behave identically:
14
+ * - web at `https://mention.earth` -> `https://auth.mention.earth`
15
+ * - native/SSR (no `window`) -> undefined (mixins fall back to
16
+ * `DEFAULT_AUTH_URL`, exactly as before)
17
+ * - explicit `authWebUrl` -> respected verbatim (explicit wins)
18
+ */
19
+
20
+ import { OxyServices } from '../../OxyServices';
21
+
22
+ // The hardcoded fallback the auth mixins resolve to when `authWebUrl` is unset
23
+ // (`this.config.authWebUrl || DEFAULT_AUTH_URL`). Mirrors the static
24
+ // `DEFAULT_AUTH_URL` on the redirect/popup mixins. Native/SSR must keep
25
+ // resolving to this exact value after the constructor auto-detect change.
26
+ const DEFAULT_AUTH_URL = 'https://auth.oxy.so';
27
+
28
+ function installWindowLocation(hostname: string, protocol = 'https:'): void {
29
+ (globalThis as unknown as { window: unknown }).window = {
30
+ location: { hostname, protocol },
31
+ };
32
+ }
33
+
34
+ function clearWindow(): void {
35
+ delete (globalThis as Record<string, unknown>).window;
36
+ }
37
+
38
+ describe('OxyServices constructor — first-party authWebUrl auto-detection', () => {
39
+ afterEach(() => {
40
+ clearWindow();
41
+ });
42
+
43
+ it('leaves authWebUrl undefined on native/SSR (no window)', () => {
44
+ // No `window` is installed -> autoDetectAuthWebUrl() returns undefined.
45
+ const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
46
+
47
+ expect(oxy.config.authWebUrl).toBeUndefined();
48
+ // Auth flows must still resolve to the hardcoded default on native.
49
+ expect(oxy.config.authWebUrl || DEFAULT_AUTH_URL).toBe('https://auth.oxy.so');
50
+ });
51
+
52
+ it('derives auth.<apex> on web when authWebUrl is omitted', () => {
53
+ installWindowLocation('mention.earth');
54
+
55
+ const oxy = new OxyServices({ baseURL: 'https://api.mention.earth' });
56
+
57
+ expect(oxy.config.authWebUrl).toBe('https://auth.mention.earth');
58
+ });
59
+
60
+ it('strips a leading subdomain down to the apex on web', () => {
61
+ installWindowLocation('www.homiio.com');
62
+
63
+ const oxy = new OxyServices({ baseURL: 'https://api.homiio.com' });
64
+
65
+ expect(oxy.config.authWebUrl).toBe('https://auth.homiio.com');
66
+ });
67
+
68
+ it('derives auth.<host> for preview hosts (e.g. *.pages.dev)', () => {
69
+ installWindowLocation('foo.pages.dev');
70
+
71
+ const oxy = new OxyServices({ baseURL: 'https://api.oxy.so' });
72
+
73
+ expect(oxy.config.authWebUrl).toBe('https://auth.pages.dev');
74
+ });
75
+
76
+ it('respects an explicit authWebUrl even when a window is present (explicit wins)', () => {
77
+ installWindowLocation('mention.earth');
78
+
79
+ const oxy = new OxyServices({
80
+ baseURL: 'https://api.mention.earth',
81
+ authWebUrl: 'https://auth.oxy.so',
82
+ });
83
+
84
+ // The page is mention.earth, but the caller pinned auth.oxy.so — honour it.
85
+ expect(oxy.config.authWebUrl).toBe('https://auth.oxy.so');
86
+ });
87
+
88
+ it('does not mutate the caller-supplied config object', () => {
89
+ installWindowLocation('mention.earth');
90
+
91
+ const input = { baseURL: 'https://api.mention.earth' };
92
+ const oxy = new OxyServices(input);
93
+
94
+ // The stored config carries the detected IdP...
95
+ expect(oxy.config.authWebUrl).toBe('https://auth.mention.earth');
96
+ // ...but the caller's own object reference is untouched.
97
+ expect((input as { authWebUrl?: string }).authWebUrl).toBeUndefined();
98
+ });
99
+
100
+ it('falls back to DEFAULT_AUTH_URL on host shapes auto-detect declines (localhost)', () => {
101
+ installWindowLocation('localhost', 'http:');
102
+
103
+ const oxy = new OxyServices({ baseURL: 'http://localhost:3000' });
104
+
105
+ expect(oxy.config.authWebUrl).toBeUndefined();
106
+ expect(oxy.config.authWebUrl || DEFAULT_AUTH_URL).toBe('https://auth.oxy.so');
107
+ });
108
+ });