@oxyhq/services 9.0.0 → 10.1.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.
- package/lib/commonjs/ui/components/OxyProvider.js +2 -2
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/SignInModal.js +26 -12
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +35 -19
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useWebSSO.js +7 -8
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +65 -23
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/utils/deviceFlowSignIn.js +55 -0
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -0
- package/lib/commonjs/utils/silentGuardKey.js +54 -0
- package/lib/commonjs/utils/silentGuardKey.js.map +1 -0
- package/lib/module/ui/components/OxyProvider.js +2 -2
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/SignInModal.js +26 -12
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +35 -19
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useWebSSO.js +7 -8
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +65 -23
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/utils/deviceFlowSignIn.js +51 -0
- package/lib/module/utils/deviceFlowSignIn.js.map +1 -0
- package/lib/module/utils/silentGuardKey.js +49 -0
- package/lib/module/utils/silentGuardKey.js.map +1 -0
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +13 -9
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useServicesMutations.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useServicesMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +8 -6
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +61 -0
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +31 -0
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +13 -9
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useServicesMutations.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useServicesMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +8 -6
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +61 -0
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -0
- package/lib/typescript/module/utils/silentGuardKey.d.ts +31 -0
- package/lib/typescript/module/utils/silentGuardKey.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/ui/components/OxyProvider.tsx +2 -2
- package/src/ui/components/SignInModal.tsx +26 -12
- package/src/ui/context/OxyContext.tsx +50 -33
- package/src/ui/hooks/useWebSSO.ts +7 -8
- package/src/ui/screens/OxyAuthScreen.tsx +65 -22
- package/src/ui/types/navigation.ts +8 -6
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +104 -0
- package/src/utils/__tests__/silentGuardKey.test.ts +82 -0
- package/src/utils/deviceFlowSignIn.ts +76 -0
- package/src/utils/silentGuardKey.ts +46 -0
- package/lib/commonjs/ui/utils/appName.js +0 -62
- package/lib/commonjs/ui/utils/appName.js.map +0 -1
- package/lib/module/ui/utils/appName.js +0 -59
- package/lib/module/ui/utils/appName.js.map +0 -1
- package/lib/typescript/commonjs/ui/utils/appName.d.ts +0 -22
- package/lib/typescript/commonjs/ui/utils/appName.d.ts.map +0 -1
- package/lib/typescript/module/ui/utils/appName.d.ts +0 -22
- package/lib/typescript/module/ui/utils/appName.d.ts.map +0 -1
- package/src/ui/utils/__tests__/appName.test.ts +0 -52
- package/src/ui/utils/appName.ts +0 -62
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*
|
|
4
|
+
* Regression coverage for the NATIVE device-flow sign-in bug:
|
|
5
|
+
*
|
|
6
|
+
* On native, tapping "Sign In with Oxy" opens `OxyAuthScreen`, which creates
|
|
7
|
+
* a device-flow AuthSession and opens auth.oxy.so/authorize. The user signs
|
|
8
|
+
* in successfully, the API authorizes the session and notifies the client via
|
|
9
|
+
* the auth-session socket — but the screen then called `switchSession`
|
|
10
|
+
* DIRECTLY without first claiming the bearer with the secret `sessionToken`.
|
|
11
|
+
* `switchSession` -> `getTokenBySession` (`GET /session/token/:id`) requires a
|
|
12
|
+
* bearer the client did not yet hold, so it 401'd: the session was authorized
|
|
13
|
+
* server-side but the app never became authenticated ("nothing happens").
|
|
14
|
+
*
|
|
15
|
+
* The web `SignInModal` already claimed first; the native screen did not.
|
|
16
|
+
* `completeDeviceFlowSignIn` consolidates the claim->switch sequence so both
|
|
17
|
+
* paths are identical. These tests pin the ORDER (claim before switch) and the
|
|
18
|
+
* fail-fast behaviour.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { User } from '@oxyhq/core';
|
|
22
|
+
import {
|
|
23
|
+
completeDeviceFlowSignIn,
|
|
24
|
+
type DeviceFlowClient,
|
|
25
|
+
} from '../deviceFlowSignIn';
|
|
26
|
+
|
|
27
|
+
const SESSION_ID = 'session-id-123';
|
|
28
|
+
const SESSION_TOKEN = 'a'.repeat(32);
|
|
29
|
+
const USER = { id: 'user-1', username: 'nate', privacySettings: {} } as User;
|
|
30
|
+
|
|
31
|
+
describe('completeDeviceFlowSignIn', () => {
|
|
32
|
+
it('claims the sessionToken BEFORE switching the session', async () => {
|
|
33
|
+
const order: string[] = [];
|
|
34
|
+
|
|
35
|
+
const oxyServices: DeviceFlowClient = {
|
|
36
|
+
claimSessionByToken: jest.fn(async (token: string) => {
|
|
37
|
+
expect(token).toBe(SESSION_TOKEN);
|
|
38
|
+
order.push('claim');
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
const switchSession = jest.fn(async (sessionId: string): Promise<User> => {
|
|
42
|
+
expect(sessionId).toBe(SESSION_ID);
|
|
43
|
+
order.push('switch');
|
|
44
|
+
return USER;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const user = await completeDeviceFlowSignIn({
|
|
48
|
+
oxyServices,
|
|
49
|
+
sessionId: SESSION_ID,
|
|
50
|
+
sessionToken: SESSION_TOKEN,
|
|
51
|
+
switchSession,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(order).toEqual(['claim', 'switch']);
|
|
55
|
+
expect(oxyServices.claimSessionByToken).toHaveBeenCalledWith(SESSION_TOKEN);
|
|
56
|
+
expect(switchSession).toHaveBeenCalledWith(SESSION_ID);
|
|
57
|
+
expect(user).toBe(USER);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('does NOT switch the session when the claim fails (the native regression)', async () => {
|
|
61
|
+
const claimError = new Error('claim failed (401)');
|
|
62
|
+
const oxyServices: DeviceFlowClient = {
|
|
63
|
+
claimSessionByToken: jest.fn(async () => {
|
|
64
|
+
throw claimError;
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
const switchSession = jest.fn(async (): Promise<User> => USER);
|
|
68
|
+
|
|
69
|
+
await expect(
|
|
70
|
+
completeDeviceFlowSignIn({
|
|
71
|
+
oxyServices,
|
|
72
|
+
sessionId: SESSION_ID,
|
|
73
|
+
sessionToken: SESSION_TOKEN,
|
|
74
|
+
switchSession,
|
|
75
|
+
}),
|
|
76
|
+
).rejects.toThrow('claim failed (401)');
|
|
77
|
+
|
|
78
|
+
// The bearer was never planted, so we must not attempt the bearer-protected
|
|
79
|
+
// switch — surfacing the failure to the caller instead.
|
|
80
|
+
expect(switchSession).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('propagates a switchSession failure after a successful claim', async () => {
|
|
84
|
+
const switchError = new Error('session invalid');
|
|
85
|
+
const oxyServices: DeviceFlowClient = {
|
|
86
|
+
claimSessionByToken: jest.fn(async () => undefined),
|
|
87
|
+
};
|
|
88
|
+
const switchSession = jest.fn(async (): Promise<User> => {
|
|
89
|
+
throw switchError;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await expect(
|
|
93
|
+
completeDeviceFlowSignIn({
|
|
94
|
+
oxyServices,
|
|
95
|
+
sessionId: SESSION_ID,
|
|
96
|
+
sessionToken: SESSION_TOKEN,
|
|
97
|
+
switchSession,
|
|
98
|
+
}),
|
|
99
|
+
).rejects.toThrow('session invalid');
|
|
100
|
+
|
|
101
|
+
expect(oxyServices.claimSessionByToken).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(switchSession).toHaveBeenCalledTimes(1);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*
|
|
4
|
+
* Regression coverage for the native session-restore crash:
|
|
5
|
+
*
|
|
6
|
+
* W [component:OxyContext]: Failed to restore sessions from storage
|
|
7
|
+
* [TypeError: Cannot read property 'origin' of undefined]
|
|
8
|
+
*
|
|
9
|
+
* `silentColdBootKey` (OxyContext) and `ssoSignature` (useWebSSO) both build an
|
|
10
|
+
* `origin|baseURL` guard signature UNCONDITIONALLY at the top of the cold-boot
|
|
11
|
+
* path, on every platform. React Native aliases a global `window` (so
|
|
12
|
+
* `typeof window !== 'undefined'` is `true`) but provides NO `window.location`.
|
|
13
|
+
* The previous `typeof window`-only guard then read `window.location.origin`
|
|
14
|
+
* and threw `Cannot read property 'origin' of undefined`, escaping session
|
|
15
|
+
* restore entirely. Both call sites now delegate to the shared, guarded
|
|
16
|
+
* `buildSilentGuardKey`, verified here under all three platform shapes.
|
|
17
|
+
*
|
|
18
|
+
* Runs in the `node` environment so `window` is genuinely controllable — under
|
|
19
|
+
* jsdom `window.location` is non-configurable and cannot be removed, so the
|
|
20
|
+
* native shape (window present, location absent) is not reproducible there.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { buildSilentGuardKey, safeWindowOrigin } from '../silentGuardKey';
|
|
24
|
+
|
|
25
|
+
describe('silentGuardKey native safety', () => {
|
|
26
|
+
const globalRef = globalThis as { window?: unknown };
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
delete globalRef.window;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('safeWindowOrigin', () => {
|
|
33
|
+
it('returns "no-origin" when there is no window (Node / SSR)', () => {
|
|
34
|
+
delete globalRef.window;
|
|
35
|
+
expect(safeWindowOrigin()).toBe('no-origin');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns "no-origin" on React Native (window present, no location)', () => {
|
|
39
|
+
// EXACT native shape: RN aliases a global `window` to the JS global, but
|
|
40
|
+
// there is no `window.location`. The old `typeof window`-only guard threw
|
|
41
|
+
// here; the new guard must return the sentinel without throwing.
|
|
42
|
+
globalRef.window = {};
|
|
43
|
+
expect(() => safeWindowOrigin()).not.toThrow();
|
|
44
|
+
expect(safeWindowOrigin()).toBe('no-origin');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns the browser origin on web', () => {
|
|
48
|
+
globalRef.window = { location: { origin: 'https://app.mention.earth' } };
|
|
49
|
+
expect(safeWindowOrigin()).toBe('https://app.mention.earth');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('buildSilentGuardKey', () => {
|
|
54
|
+
it('does not throw and composes "no-origin|" on React Native', () => {
|
|
55
|
+
globalRef.window = {};
|
|
56
|
+
const getBaseURL = () => 'https://api.mention.earth';
|
|
57
|
+
expect(() => buildSilentGuardKey(getBaseURL)).not.toThrow();
|
|
58
|
+
expect(buildSilentGuardKey(getBaseURL)).toBe('no-origin|https://api.mention.earth');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('composes "origin|baseURL" on web', () => {
|
|
62
|
+
globalRef.window = { location: { origin: 'https://app.mention.earth' } };
|
|
63
|
+
expect(buildSilentGuardKey(() => 'https://api.mention.earth')).toBe(
|
|
64
|
+
'https://app.mention.earth|https://api.mention.earth',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('degrades baseURL to empty when getBaseURL is absent', () => {
|
|
69
|
+
globalRef.window = {};
|
|
70
|
+
expect(buildSilentGuardKey()).toBe('no-origin|');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('degrades baseURL to empty when getBaseURL throws', () => {
|
|
74
|
+
globalRef.window = {};
|
|
75
|
+
const throwing = (): string => {
|
|
76
|
+
throw new Error('client not initialised');
|
|
77
|
+
};
|
|
78
|
+
expect(() => buildSilentGuardKey(throwing)).not.toThrow();
|
|
79
|
+
expect(buildSilentGuardKey(throwing)).toBe('no-origin|');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, pure orchestration for completing the cross-app device-flow sign-in
|
|
3
|
+
* (the QR-code / "Open Oxy Auth" path used on native and web).
|
|
4
|
+
*
|
|
5
|
+
* THE BUG THIS FIXES (native): once another authenticated device approves the
|
|
6
|
+
* pending AuthSession, the originating client is notified (socket / poll /
|
|
7
|
+
* deep-link) with the authorized `sessionId`. Before any session-management
|
|
8
|
+
* code can use it, the client MUST exchange the secret 128-bit `sessionToken`
|
|
9
|
+
* (held only by this client, generated for THIS flow) for the first access
|
|
10
|
+
* token via `claimSessionByToken` — the device-flow equivalent of OAuth's
|
|
11
|
+
* code-for-token exchange (RFC 8628 §3.4).
|
|
12
|
+
*
|
|
13
|
+
* Skipping the claim leaves the SDK with NO bearer token, so the subsequent
|
|
14
|
+
* `switchSession` -> `getTokenBySession` (`GET /session/token/:id`) call 401s
|
|
15
|
+
* against the C1-hardened API: the session is authorized server-side but the
|
|
16
|
+
* app never becomes authenticated and the UI sits "Waiting for
|
|
17
|
+
* authorization..." forever. The web `SignInModal` already claimed first; the
|
|
18
|
+
* native `OxyAuthScreen` did not. Consolidating the claim→switch sequence here
|
|
19
|
+
* keeps both paths identical and unit-testable, and prevents future drift.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { User } from '@oxyhq/core';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The minimal `OxyServices` surface this orchestration needs. Kept as a
|
|
26
|
+
* structural type (rather than importing the full client) so the helper is
|
|
27
|
+
* trivially unit-testable with a stub and never pulls the RN/Expo runtime into
|
|
28
|
+
* a test bundle.
|
|
29
|
+
*/
|
|
30
|
+
export interface DeviceFlowClient {
|
|
31
|
+
/**
|
|
32
|
+
* Exchange the device-flow `sessionToken` for the first access + refresh
|
|
33
|
+
* token, planting them on the client. Single-use; replay is rejected by the
|
|
34
|
+
* API. No bearer required — the high-entropy `sessionToken` IS the credential.
|
|
35
|
+
*/
|
|
36
|
+
claimSessionByToken: (sessionToken: string) => Promise<unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CompleteDeviceFlowSignInOptions {
|
|
40
|
+
/** The OxyServices client (or any object exposing `claimSessionByToken`). */
|
|
41
|
+
oxyServices: DeviceFlowClient;
|
|
42
|
+
/** The authorized device session id, delivered by the socket / poll / link. */
|
|
43
|
+
sessionId: string;
|
|
44
|
+
/**
|
|
45
|
+
* The secret `sessionToken` generated for THIS flow and registered via
|
|
46
|
+
* `POST /auth/session/create`. Required to claim the first access token.
|
|
47
|
+
*/
|
|
48
|
+
sessionToken: string;
|
|
49
|
+
/**
|
|
50
|
+
* The session-management `switchSession` from `useOxy()`. Hydrates the
|
|
51
|
+
* activated session (validates, fetches the user, persists, updates state).
|
|
52
|
+
* Runs AFTER the bearer is planted so its bearer-protected calls succeed.
|
|
53
|
+
*/
|
|
54
|
+
switchSession: (sessionId: string) => Promise<User>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Complete a device-flow sign-in: claim the first access token with the secret
|
|
59
|
+
* `sessionToken` (planting the bearer), then hydrate the session via
|
|
60
|
+
* `switchSession`. Returns the authenticated user.
|
|
61
|
+
*
|
|
62
|
+
* Throws if either the claim or the switch fails; callers surface a retry UI.
|
|
63
|
+
*/
|
|
64
|
+
export async function completeDeviceFlowSignIn({
|
|
65
|
+
oxyServices,
|
|
66
|
+
sessionId,
|
|
67
|
+
sessionToken,
|
|
68
|
+
switchSession,
|
|
69
|
+
}: CompleteDeviceFlowSignInOptions): Promise<User> {
|
|
70
|
+
// 1) Plant the bearer + refresh tokens. Without this the bearer-protected
|
|
71
|
+
// `getTokenBySession` inside `switchSession` 401s (the native regression).
|
|
72
|
+
await oxyServices.claimSessionByToken(sessionToken);
|
|
73
|
+
|
|
74
|
+
// 2) Bearer is now planted — hydrate the session through the normal path.
|
|
75
|
+
return switchSession(sessionId);
|
|
76
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, pure helpers for building the `origin|baseURL` signature used as the
|
|
3
|
+
* module-level run-once guard key for cold-boot silent-SSO probes
|
|
4
|
+
* (`silentColdBootKey` in `OxyContext`, `ssoSignature` in `useWebSSO`).
|
|
5
|
+
*
|
|
6
|
+
* NATIVE SAFETY (the bug this fixes): React Native aliases a global `window`
|
|
7
|
+
* (it points at the JS global object), so `typeof window !== 'undefined'` is
|
|
8
|
+
* `true` on native — but `window.location` is `undefined`. Reading
|
|
9
|
+
* `window.location.origin` after only a `typeof window` check therefore throws
|
|
10
|
+
* `TypeError: Cannot read property 'origin' of undefined` on native. Because
|
|
11
|
+
* the key is built UNCONDITIONALLY at the top of the cold-boot path (before its
|
|
12
|
+
* try/catch), that throw escaped session restore entirely and broke
|
|
13
|
+
* cross-session restore on native. Both prior copies of the guard had the same
|
|
14
|
+
* insufficient `typeof window` check and were prone to drift, so the read is
|
|
15
|
+
* consolidated here behind a guard that also verifies `window.location`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read `window.location.origin` safely on every platform.
|
|
20
|
+
*
|
|
21
|
+
* Returns the browser origin on web, and the sentinel `'no-origin'` anywhere
|
|
22
|
+
* `window.location` is absent (React Native, SSR/Node). Never throws.
|
|
23
|
+
*/
|
|
24
|
+
export function safeWindowOrigin(): string {
|
|
25
|
+
if (typeof window !== 'undefined' && typeof window.location !== 'undefined') {
|
|
26
|
+
return window.location.origin;
|
|
27
|
+
}
|
|
28
|
+
return 'no-origin';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the stable `origin|baseURL` signature for the silent-SSO run-once
|
|
33
|
+
* guard. Two providers pointed at the same API from the same origin share one
|
|
34
|
+
* attempt. `getBaseURL` is invoked defensively (it may be absent or throw on a
|
|
35
|
+
* partially-initialised client); any failure degrades to an empty baseURL.
|
|
36
|
+
*/
|
|
37
|
+
export function buildSilentGuardKey(getBaseURL?: () => string | undefined): string {
|
|
38
|
+
const origin = safeWindowOrigin();
|
|
39
|
+
let baseURL = '';
|
|
40
|
+
try {
|
|
41
|
+
baseURL = getBaseURL?.() ?? '';
|
|
42
|
+
} catch {
|
|
43
|
+
baseURL = '';
|
|
44
|
+
}
|
|
45
|
+
return `${origin}|${baseURL}`;
|
|
46
|
+
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
value: true
|
|
5
|
-
});
|
|
6
|
-
exports.resolveAppDisplayName = resolveAppDisplayName;
|
|
7
|
-
var _reactNative = require("react-native");
|
|
8
|
-
/**
|
|
9
|
-
* The `storageKeyPrefix` default applied by `OxyContextProvider`. When the
|
|
10
|
-
* consumer never overrides it, the prefix carries no app-identity signal and
|
|
11
|
-
* must NOT be used to derive a display name (it would surface "Oxy_session").
|
|
12
|
-
*/
|
|
13
|
-
const DEFAULT_STORAGE_KEY_PREFIX = 'oxy_session';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Capitalize the first character of a non-empty string. Used to turn a lower
|
|
17
|
-
* case `storageKeyPrefix` (e.g. `"mention"`) into a presentable label
|
|
18
|
-
* (`"Mention"`). Pure; leaves the remainder untouched so multi-word or already
|
|
19
|
-
* capitalized values are preserved.
|
|
20
|
-
*/
|
|
21
|
-
function capitalizeFirst(value) {
|
|
22
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Resolve a human-readable application display name for the consent / sign-in
|
|
27
|
-
* UI shown by the central Oxy auth experience (e.g. "Mention wants to access
|
|
28
|
-
* your Oxy account"). This is sent as the `appId` field on
|
|
29
|
-
* `POST /auth/session/create` and rendered verbatim by the auth consent page.
|
|
30
|
-
*
|
|
31
|
-
* Resolution order (first non-empty wins):
|
|
32
|
-
* 1. An explicit `appName` declared by the consumer on `OxyProvider`.
|
|
33
|
-
* 2. The capitalized `storageKeyPrefix` — but only when the consumer actually
|
|
34
|
-
* overrode the default. Apps already pass a brand-shaped prefix
|
|
35
|
-
* (`"mention"`, `"homiio"`, …) so this gives most apps a correct name with
|
|
36
|
-
* zero extra config.
|
|
37
|
-
* 3. On web only, a meaningful `document.title` (trimmed). This rescues
|
|
38
|
-
* zero-config web apps that set a page title but no prefix.
|
|
39
|
-
* 4. `Platform.OS` as the terminal fallback. On web this yields the historical
|
|
40
|
-
* `"web"` value — now reached ONLY when an app supplies neither an explicit
|
|
41
|
-
* name, a custom prefix, nor a document title.
|
|
42
|
-
*
|
|
43
|
-
* The result is never empty.
|
|
44
|
-
*/
|
|
45
|
-
function resolveAppDisplayName(appName, storageKeyPrefix) {
|
|
46
|
-
const explicit = appName?.trim();
|
|
47
|
-
if (explicit) {
|
|
48
|
-
return explicit;
|
|
49
|
-
}
|
|
50
|
-
const prefix = storageKeyPrefix?.trim();
|
|
51
|
-
if (prefix && prefix !== DEFAULT_STORAGE_KEY_PREFIX) {
|
|
52
|
-
return capitalizeFirst(prefix);
|
|
53
|
-
}
|
|
54
|
-
if (_reactNative.Platform.OS === 'web' && typeof document !== 'undefined') {
|
|
55
|
-
const title = document.title?.trim();
|
|
56
|
-
if (title) {
|
|
57
|
-
return title;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return _reactNative.Platform.OS;
|
|
61
|
-
}
|
|
62
|
-
//# sourceMappingURL=appName.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["_reactNative","require","DEFAULT_STORAGE_KEY_PREFIX","capitalizeFirst","value","charAt","toUpperCase","slice","resolveAppDisplayName","appName","storageKeyPrefix","explicit","trim","prefix","Platform","OS","document","title"],"sourceRoot":"../../../../src","sources":["ui/utils/appName.ts"],"mappings":";;;;;;AAAA,IAAAA,YAAA,GAAAC,OAAA;AAEA;AACA;AACA;AACA;AACA;AACA,MAAMC,0BAA0B,GAAG,aAAa;;AAEhD;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,eAAeA,CAACC,KAAa,EAAU;EAC9C,OAAOA,KAAK,CAACC,MAAM,CAAC,CAAC,CAAC,CAACC,WAAW,CAAC,CAAC,GAAGF,KAAK,CAACG,KAAK,CAAC,CAAC,CAAC;AACvD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,qBAAqBA,CACnCC,OAA2B,EAC3BC,gBAAoC,EAC5B;EACR,MAAMC,QAAQ,GAAGF,OAAO,EAAEG,IAAI,CAAC,CAAC;EAChC,IAAID,QAAQ,EAAE;IACZ,OAAOA,QAAQ;EACjB;EAEA,MAAME,MAAM,GAAGH,gBAAgB,EAAEE,IAAI,CAAC,CAAC;EACvC,IAAIC,MAAM,IAAIA,MAAM,KAAKX,0BAA0B,EAAE;IACnD,OAAOC,eAAe,CAACU,MAAM,CAAC;EAChC;EAEA,IAAIC,qBAAQ,CAACC,EAAE,KAAK,KAAK,IAAI,OAAOC,QAAQ,KAAK,WAAW,EAAE;IAC5D,MAAMC,KAAK,GAAGD,QAAQ,CAACC,KAAK,EAAEL,IAAI,CAAC,CAAC;IACpC,IAAIK,KAAK,EAAE;MACT,OAAOA,KAAK;IACd;EACF;EAEA,OAAOH,qBAAQ,CAACC,EAAE;AACpB","ignoreList":[]}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { Platform } from 'react-native';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The `storageKeyPrefix` default applied by `OxyContextProvider`. When the
|
|
7
|
-
* consumer never overrides it, the prefix carries no app-identity signal and
|
|
8
|
-
* must NOT be used to derive a display name (it would surface "Oxy_session").
|
|
9
|
-
*/
|
|
10
|
-
const DEFAULT_STORAGE_KEY_PREFIX = 'oxy_session';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Capitalize the first character of a non-empty string. Used to turn a lower
|
|
14
|
-
* case `storageKeyPrefix` (e.g. `"mention"`) into a presentable label
|
|
15
|
-
* (`"Mention"`). Pure; leaves the remainder untouched so multi-word or already
|
|
16
|
-
* capitalized values are preserved.
|
|
17
|
-
*/
|
|
18
|
-
function capitalizeFirst(value) {
|
|
19
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Resolve a human-readable application display name for the consent / sign-in
|
|
24
|
-
* UI shown by the central Oxy auth experience (e.g. "Mention wants to access
|
|
25
|
-
* your Oxy account"). This is sent as the `appId` field on
|
|
26
|
-
* `POST /auth/session/create` and rendered verbatim by the auth consent page.
|
|
27
|
-
*
|
|
28
|
-
* Resolution order (first non-empty wins):
|
|
29
|
-
* 1. An explicit `appName` declared by the consumer on `OxyProvider`.
|
|
30
|
-
* 2. The capitalized `storageKeyPrefix` — but only when the consumer actually
|
|
31
|
-
* overrode the default. Apps already pass a brand-shaped prefix
|
|
32
|
-
* (`"mention"`, `"homiio"`, …) so this gives most apps a correct name with
|
|
33
|
-
* zero extra config.
|
|
34
|
-
* 3. On web only, a meaningful `document.title` (trimmed). This rescues
|
|
35
|
-
* zero-config web apps that set a page title but no prefix.
|
|
36
|
-
* 4. `Platform.OS` as the terminal fallback. On web this yields the historical
|
|
37
|
-
* `"web"` value — now reached ONLY when an app supplies neither an explicit
|
|
38
|
-
* name, a custom prefix, nor a document title.
|
|
39
|
-
*
|
|
40
|
-
* The result is never empty.
|
|
41
|
-
*/
|
|
42
|
-
export function resolveAppDisplayName(appName, storageKeyPrefix) {
|
|
43
|
-
const explicit = appName?.trim();
|
|
44
|
-
if (explicit) {
|
|
45
|
-
return explicit;
|
|
46
|
-
}
|
|
47
|
-
const prefix = storageKeyPrefix?.trim();
|
|
48
|
-
if (prefix && prefix !== DEFAULT_STORAGE_KEY_PREFIX) {
|
|
49
|
-
return capitalizeFirst(prefix);
|
|
50
|
-
}
|
|
51
|
-
if (Platform.OS === 'web' && typeof document !== 'undefined') {
|
|
52
|
-
const title = document.title?.trim();
|
|
53
|
-
if (title) {
|
|
54
|
-
return title;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return Platform.OS;
|
|
58
|
-
}
|
|
59
|
-
//# sourceMappingURL=appName.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"names":["Platform","DEFAULT_STORAGE_KEY_PREFIX","capitalizeFirst","value","charAt","toUpperCase","slice","resolveAppDisplayName","appName","storageKeyPrefix","explicit","trim","prefix","OS","document","title"],"sourceRoot":"../../../../src","sources":["ui/utils/appName.ts"],"mappings":";;AAAA,SAASA,QAAQ,QAAQ,cAAc;;AAEvC;AACA;AACA;AACA;AACA;AACA,MAAMC,0BAA0B,GAAG,aAAa;;AAEhD;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,eAAeA,CAACC,KAAa,EAAU;EAC9C,OAAOA,KAAK,CAACC,MAAM,CAAC,CAAC,CAAC,CAACC,WAAW,CAAC,CAAC,GAAGF,KAAK,CAACG,KAAK,CAAC,CAAC,CAAC;AACvD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,qBAAqBA,CACnCC,OAA2B,EAC3BC,gBAAoC,EAC5B;EACR,MAAMC,QAAQ,GAAGF,OAAO,EAAEG,IAAI,CAAC,CAAC;EAChC,IAAID,QAAQ,EAAE;IACZ,OAAOA,QAAQ;EACjB;EAEA,MAAME,MAAM,GAAGH,gBAAgB,EAAEE,IAAI,CAAC,CAAC;EACvC,IAAIC,MAAM,IAAIA,MAAM,KAAKX,0BAA0B,EAAE;IACnD,OAAOC,eAAe,CAACU,MAAM,CAAC;EAChC;EAEA,IAAIZ,QAAQ,CAACa,EAAE,KAAK,KAAK,IAAI,OAAOC,QAAQ,KAAK,WAAW,EAAE;IAC5D,MAAMC,KAAK,GAAGD,QAAQ,CAACC,KAAK,EAAEJ,IAAI,CAAC,CAAC;IACpC,IAAII,KAAK,EAAE;MACT,OAAOA,KAAK;IACd;EACF;EAEA,OAAOf,QAAQ,CAACa,EAAE;AACpB","ignoreList":[]}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve a human-readable application display name for the consent / sign-in
|
|
3
|
-
* UI shown by the central Oxy auth experience (e.g. "Mention wants to access
|
|
4
|
-
* your Oxy account"). This is sent as the `appId` field on
|
|
5
|
-
* `POST /auth/session/create` and rendered verbatim by the auth consent page.
|
|
6
|
-
*
|
|
7
|
-
* Resolution order (first non-empty wins):
|
|
8
|
-
* 1. An explicit `appName` declared by the consumer on `OxyProvider`.
|
|
9
|
-
* 2. The capitalized `storageKeyPrefix` — but only when the consumer actually
|
|
10
|
-
* overrode the default. Apps already pass a brand-shaped prefix
|
|
11
|
-
* (`"mention"`, `"homiio"`, …) so this gives most apps a correct name with
|
|
12
|
-
* zero extra config.
|
|
13
|
-
* 3. On web only, a meaningful `document.title` (trimmed). This rescues
|
|
14
|
-
* zero-config web apps that set a page title but no prefix.
|
|
15
|
-
* 4. `Platform.OS` as the terminal fallback. On web this yields the historical
|
|
16
|
-
* `"web"` value — now reached ONLY when an app supplies neither an explicit
|
|
17
|
-
* name, a custom prefix, nor a document title.
|
|
18
|
-
*
|
|
19
|
-
* The result is never empty.
|
|
20
|
-
*/
|
|
21
|
-
export declare function resolveAppDisplayName(appName: string | undefined, storageKeyPrefix: string | undefined): string;
|
|
22
|
-
//# sourceMappingURL=appName.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"appName.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/appName.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,MAAM,CAmBR"}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve a human-readable application display name for the consent / sign-in
|
|
3
|
-
* UI shown by the central Oxy auth experience (e.g. "Mention wants to access
|
|
4
|
-
* your Oxy account"). This is sent as the `appId` field on
|
|
5
|
-
* `POST /auth/session/create` and rendered verbatim by the auth consent page.
|
|
6
|
-
*
|
|
7
|
-
* Resolution order (first non-empty wins):
|
|
8
|
-
* 1. An explicit `appName` declared by the consumer on `OxyProvider`.
|
|
9
|
-
* 2. The capitalized `storageKeyPrefix` — but only when the consumer actually
|
|
10
|
-
* overrode the default. Apps already pass a brand-shaped prefix
|
|
11
|
-
* (`"mention"`, `"homiio"`, …) so this gives most apps a correct name with
|
|
12
|
-
* zero extra config.
|
|
13
|
-
* 3. On web only, a meaningful `document.title` (trimmed). This rescues
|
|
14
|
-
* zero-config web apps that set a page title but no prefix.
|
|
15
|
-
* 4. `Platform.OS` as the terminal fallback. On web this yields the historical
|
|
16
|
-
* `"web"` value — now reached ONLY when an app supplies neither an explicit
|
|
17
|
-
* name, a custom prefix, nor a document title.
|
|
18
|
-
*
|
|
19
|
-
* The result is never empty.
|
|
20
|
-
*/
|
|
21
|
-
export declare function resolveAppDisplayName(appName: string | undefined, storageKeyPrefix: string | undefined): string;
|
|
22
|
-
//# sourceMappingURL=appName.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"appName.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/appName.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,MAAM,CAmBR"}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { resolveAppDisplayName } from '../appName';
|
|
2
|
-
|
|
3
|
-
// The shared react-native mock pins `Platform.OS` to 'web', which is exactly
|
|
4
|
-
// the platform on which the historical "web wants to access your Oxy account"
|
|
5
|
-
// regression occurred. These tests assert the resolution order that prevents it.
|
|
6
|
-
|
|
7
|
-
describe('resolveAppDisplayName', () => {
|
|
8
|
-
const originalTitle = typeof document !== 'undefined' ? document.title : '';
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
if (typeof document !== 'undefined') {
|
|
12
|
-
document.title = originalTitle;
|
|
13
|
-
}
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('prefers an explicit appName, trimmed', () => {
|
|
17
|
-
expect(resolveAppDisplayName(' Mention ', 'oxy_session')).toBe('Mention');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('explicit appName wins over a custom storageKeyPrefix', () => {
|
|
21
|
-
expect(resolveAppDisplayName('Mention', 'homiio')).toBe('Mention');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('capitalizes a custom storageKeyPrefix when no appName is given', () => {
|
|
25
|
-
expect(resolveAppDisplayName(undefined, 'mention')).toBe('Mention');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('ignores the default storageKeyPrefix (never surfaces "Oxy_session")', () => {
|
|
29
|
-
if (typeof document !== 'undefined') {
|
|
30
|
-
document.title = '';
|
|
31
|
-
}
|
|
32
|
-
expect(resolveAppDisplayName(undefined, 'oxy_session')).toBe('web');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('falls back to document.title on web when no name or custom prefix is set', () => {
|
|
36
|
-
if (typeof document !== 'undefined') {
|
|
37
|
-
document.title = 'Homiio';
|
|
38
|
-
}
|
|
39
|
-
expect(resolveAppDisplayName(undefined, 'oxy_session')).toBe('Homiio');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('falls back to the platform only when nothing else is available', () => {
|
|
43
|
-
if (typeof document !== 'undefined') {
|
|
44
|
-
document.title = '';
|
|
45
|
-
}
|
|
46
|
-
expect(resolveAppDisplayName(undefined, undefined)).toBe('web');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('treats a whitespace-only appName as absent', () => {
|
|
50
|
-
expect(resolveAppDisplayName(' ', 'mention')).toBe('Mention');
|
|
51
|
-
});
|
|
52
|
-
});
|
package/src/ui/utils/appName.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Platform } from 'react-native';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The `storageKeyPrefix` default applied by `OxyContextProvider`. When the
|
|
5
|
-
* consumer never overrides it, the prefix carries no app-identity signal and
|
|
6
|
-
* must NOT be used to derive a display name (it would surface "Oxy_session").
|
|
7
|
-
*/
|
|
8
|
-
const DEFAULT_STORAGE_KEY_PREFIX = 'oxy_session';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Capitalize the first character of a non-empty string. Used to turn a lower
|
|
12
|
-
* case `storageKeyPrefix` (e.g. `"mention"`) into a presentable label
|
|
13
|
-
* (`"Mention"`). Pure; leaves the remainder untouched so multi-word or already
|
|
14
|
-
* capitalized values are preserved.
|
|
15
|
-
*/
|
|
16
|
-
function capitalizeFirst(value: string): string {
|
|
17
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Resolve a human-readable application display name for the consent / sign-in
|
|
22
|
-
* UI shown by the central Oxy auth experience (e.g. "Mention wants to access
|
|
23
|
-
* your Oxy account"). This is sent as the `appId` field on
|
|
24
|
-
* `POST /auth/session/create` and rendered verbatim by the auth consent page.
|
|
25
|
-
*
|
|
26
|
-
* Resolution order (first non-empty wins):
|
|
27
|
-
* 1. An explicit `appName` declared by the consumer on `OxyProvider`.
|
|
28
|
-
* 2. The capitalized `storageKeyPrefix` — but only when the consumer actually
|
|
29
|
-
* overrode the default. Apps already pass a brand-shaped prefix
|
|
30
|
-
* (`"mention"`, `"homiio"`, …) so this gives most apps a correct name with
|
|
31
|
-
* zero extra config.
|
|
32
|
-
* 3. On web only, a meaningful `document.title` (trimmed). This rescues
|
|
33
|
-
* zero-config web apps that set a page title but no prefix.
|
|
34
|
-
* 4. `Platform.OS` as the terminal fallback. On web this yields the historical
|
|
35
|
-
* `"web"` value — now reached ONLY when an app supplies neither an explicit
|
|
36
|
-
* name, a custom prefix, nor a document title.
|
|
37
|
-
*
|
|
38
|
-
* The result is never empty.
|
|
39
|
-
*/
|
|
40
|
-
export function resolveAppDisplayName(
|
|
41
|
-
appName: string | undefined,
|
|
42
|
-
storageKeyPrefix: string | undefined,
|
|
43
|
-
): string {
|
|
44
|
-
const explicit = appName?.trim();
|
|
45
|
-
if (explicit) {
|
|
46
|
-
return explicit;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const prefix = storageKeyPrefix?.trim();
|
|
50
|
-
if (prefix && prefix !== DEFAULT_STORAGE_KEY_PREFIX) {
|
|
51
|
-
return capitalizeFirst(prefix);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (Platform.OS === 'web' && typeof document !== 'undefined') {
|
|
55
|
-
const title = document.title?.trim();
|
|
56
|
-
if (title) {
|
|
57
|
-
return title;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return Platform.OS;
|
|
62
|
-
}
|