@oxyhq/core 2.1.1 → 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.1",
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",
@@ -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
+ });