@oxyhq/services 13.3.0 → 14.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.
- package/lib/commonjs/index.js +11 -17
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/AccountMenu.js +103 -65
- package/lib/commonjs/ui/components/AccountMenu.js.map +1 -1
- package/lib/commonjs/ui/components/AccountSwitcher.js +84 -126
- package/lib/commonjs/ui/components/AccountSwitcher.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileMenu.js +140 -153
- package/lib/commonjs/ui/components/ProfileMenu.js.map +1 -1
- package/lib/commonjs/ui/components/SignInModal.js +4 -4
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +506 -466
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +59 -57
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js +23 -42
- package/lib/commonjs/ui/context/inSessionTokenRefresh.js.map +1 -1
- package/lib/commonjs/ui/context/silentSessionRestore.js +12 -21
- package/lib/commonjs/ui/context/silentSessionRestore.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +4 -3
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js +12 -7
- package/lib/commonjs/ui/hooks/useOxyAuthSession.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +17 -65
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +288 -0
- package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -0
- package/lib/commonjs/ui/hooks/useWebSSO.js +12 -191
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +2 -2
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/ui/session/createSessionClient.js +28 -0
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -0
- package/lib/commonjs/ui/session/index.js +52 -0
- package/lib/commonjs/ui/session/index.js.map +1 -0
- package/lib/commonjs/ui/session/projectSessionState.js +86 -0
- package/lib/commonjs/ui/session/projectSessionState.js.map +1 -0
- package/lib/commonjs/ui/session/sessionClientHost.js +30 -0
- package/lib/commonjs/ui/session/sessionClientHost.js.map +1 -0
- package/lib/commonjs/ui/session/tokenTransport.js +77 -0
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -0
- package/lib/commonjs/ui/utils/activeAuthuser.js +35 -79
- package/lib/commonjs/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/commonjs/ui/utils/sessionHelpers.js +1 -51
- package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/commonjs/utils/deviceFlowSignIn.js +70 -15
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/module/index.js +5 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/AccountMenu.js +103 -65
- package/lib/module/ui/components/AccountMenu.js.map +1 -1
- package/lib/module/ui/components/AccountSwitcher.js +85 -127
- package/lib/module/ui/components/AccountSwitcher.js.map +1 -1
- package/lib/module/ui/components/ProfileMenu.js +143 -156
- package/lib/module/ui/components/ProfileMenu.js.map +1 -1
- package/lib/module/ui/components/SignInModal.js +4 -4
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +510 -470
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +61 -60
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/context/inSessionTokenRefresh.js +24 -43
- package/lib/module/ui/context/inSessionTokenRefresh.js.map +1 -1
- package/lib/module/ui/context/silentSessionRestore.js +12 -20
- package/lib/module/ui/context/silentSessionRestore.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +4 -3
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useOxyAuthSession.js +12 -7
- package/lib/module/ui/hooks/useOxyAuthSession.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +18 -66
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/hooks/useSwitchableAccounts.js +283 -0
- package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -0
- package/lib/module/ui/hooks/useWebSSO.js +13 -192
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/module/ui/index.js +0 -1
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +2 -2
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/ui/session/createSessionClient.js +25 -0
- package/lib/module/ui/session/createSessionClient.js.map +1 -0
- package/lib/module/ui/session/index.js +17 -0
- package/lib/module/ui/session/index.js.map +1 -0
- package/lib/module/ui/session/projectSessionState.js +79 -0
- package/lib/module/ui/session/projectSessionState.js.map +1 -0
- package/lib/module/ui/session/sessionClientHost.js +26 -0
- package/lib/module/ui/session/sessionClientHost.js.map +1 -0
- package/lib/module/ui/session/tokenTransport.js +74 -0
- package/lib/module/ui/session/tokenTransport.js.map +1 -0
- package/lib/module/ui/utils/activeAuthuser.js +34 -76
- package/lib/module/ui/utils/activeAuthuser.js.map +1 -1
- package/lib/module/ui/utils/sessionHelpers.js +0 -49
- package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/module/utils/deviceFlowSignIn.js +71 -16
- package/lib/module/utils/deviceFlowSignIn.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -3
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts +4 -2
- package/lib/typescript/commonjs/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts +7 -8
- package/lib/typescript/commonjs/ui/components/AccountSwitcher.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts +12 -9
- package/lib/typescript/commonjs/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +16 -13
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +27 -3
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts +22 -17
- package/lib/typescript/commonjs/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts +12 -18
- package/lib/typescript/commonjs/ui/context/silentSessionRestore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts +2 -2
- package/lib/typescript/commonjs/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts +162 -0
- package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts +13 -52
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +15 -0
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/index.d.ts +15 -0
- package/lib/typescript/commonjs/ui/session/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts +45 -0
- package/lib/typescript/commonjs/ui/session/projectSessionState.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts +14 -0
- package/lib/typescript/commonjs/ui/session/sessionClientHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +24 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts +21 -29
- package/lib/typescript/commonjs/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts +0 -19
- package/lib/typescript/commonjs/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +51 -19
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -3
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountMenu.d.ts +4 -2
- package/lib/typescript/module/ui/components/AccountMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts +7 -8
- package/lib/typescript/module/ui/components/AccountSwitcher.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts +12 -9
- package/lib/typescript/module/ui/components/ProfileMenu.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +16 -13
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +27 -3
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts +22 -17
- package/lib/typescript/module/ui/context/inSessionTokenRefresh.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts +12 -18
- package/lib/typescript/module/ui/context/silentSessionRestore.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +1 -0
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts +2 -2
- package/lib/typescript/module/ui/hooks/useOxyAuthSession.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts +162 -0
- package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts +13 -52
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +15 -0
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/index.d.ts +15 -0
- package/lib/typescript/module/ui/session/index.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/projectSessionState.d.ts +45 -0
- package/lib/typescript/module/ui/session/projectSessionState.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts +14 -0
- package/lib/typescript/module/ui/session/sessionClientHost.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +24 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts +21 -29
- package/lib/typescript/module/ui/utils/activeAuthuser.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts +0 -19
- package/lib/typescript/module/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +51 -19
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +9 -9
- package/src/ui/components/AccountMenu.tsx +117 -70
- package/src/ui/components/AccountSwitcher.tsx +88 -134
- package/src/ui/components/ProfileMenu.tsx +158 -162
- package/src/ui/components/SignInModal.tsx +5 -5
- package/src/ui/context/OxyContext.tsx +560 -475
- package/src/ui/context/hooks/useAuthOperations.ts +72 -63
- package/src/ui/context/inSessionTokenRefresh.ts +23 -42
- package/src/ui/context/silentSessionRestore.ts +12 -26
- package/src/ui/hooks/useAuth.ts +4 -3
- package/src/ui/hooks/useOxyAuthSession.ts +13 -8
- package/src/ui/hooks/useSessionManagement.ts +16 -87
- package/src/ui/hooks/useSwitchableAccounts.ts +397 -0
- package/src/ui/hooks/useWebSSO.ts +13 -227
- package/src/ui/index.ts +0 -1
- package/src/ui/screens/OxyAuthScreen.tsx +2 -2
- package/src/ui/session/__tests__/createSessionClient.test.ts +41 -0
- package/src/ui/session/__tests__/projectSessionState.test.ts +114 -0
- package/src/ui/session/__tests__/sessionClientHost.test.ts +43 -0
- package/src/ui/session/__tests__/tokenTransport.test.ts +142 -0
- package/src/ui/session/createSessionClient.ts +21 -0
- package/src/ui/session/index.ts +19 -0
- package/src/ui/session/projectSessionState.ts +85 -0
- package/src/ui/session/sessionClientHost.ts +27 -0
- package/src/ui/session/tokenTransport.ts +76 -0
- package/src/ui/utils/activeAuthuser.ts +34 -76
- package/src/ui/utils/sessionHelpers.ts +0 -66
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +243 -34
- package/src/utils/deviceFlowSignIn.ts +98 -21
- package/lib/commonjs/ui/components/accountMenuRows.js +0 -33
- package/lib/commonjs/ui/components/accountMenuRows.js.map +0 -1
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js +0 -280
- package/lib/commonjs/ui/hooks/useDeviceAccounts.js.map +0 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +0 -203
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +0 -1
- package/lib/commonjs/utils/silentGuardKey.js +0 -54
- package/lib/commonjs/utils/silentGuardKey.js.map +0 -1
- package/lib/module/ui/components/accountMenuRows.js +0 -29
- package/lib/module/ui/components/accountMenuRows.js.map +0 -1
- package/lib/module/ui/hooks/useDeviceAccounts.js +0 -276
- package/lib/module/ui/hooks/useDeviceAccounts.js.map +0 -1
- package/lib/module/ui/hooks/useSessionSocket.js +0 -199
- package/lib/module/ui/hooks/useSessionSocket.js.map +0 -1
- package/lib/module/utils/silentGuardKey.js +0 -49
- package/lib/module/utils/silentGuardKey.js.map +0 -1
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts +0 -34
- package/lib/typescript/commonjs/ui/components/accountMenuRows.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts +0 -133
- package/lib/typescript/commonjs/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts +0 -14
- package/lib/typescript/commonjs/ui/hooks/useSessionSocket.d.ts.map +0 -1
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +0 -31
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +0 -1
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts +0 -34
- package/lib/typescript/module/ui/components/accountMenuRows.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts +0 -133
- package/lib/typescript/module/ui/hooks/useDeviceAccounts.d.ts.map +0 -1
- package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts +0 -14
- package/lib/typescript/module/ui/hooks/useSessionSocket.d.ts.map +0 -1
- package/lib/typescript/module/utils/silentGuardKey.d.ts +0 -31
- package/lib/typescript/module/utils/silentGuardKey.d.ts.map +0 -1
- package/src/ui/components/accountMenuRows.ts +0 -48
- package/src/ui/hooks/useDeviceAccounts.ts +0 -344
- package/src/ui/hooks/useSessionSocket.ts +0 -233
- package/src/ui/utils/__tests__/activeAuthuser.test.ts +0 -75
- package/src/utils/__tests__/silentGuardKey.test.ts +0 -82
- package/src/utils/silentGuardKey.ts +0 -46
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SessionClient } from '@oxyhq/core';
|
|
2
|
+
import { createSessionClient } from '../createSessionClient';
|
|
3
|
+
|
|
4
|
+
function fakeOxy() {
|
|
5
|
+
const listeners = new Set<(t: string | null) => void>();
|
|
6
|
+
return {
|
|
7
|
+
makeRequest: jest.fn().mockResolvedValue(undefined),
|
|
8
|
+
getBaseURL: jest.fn().mockReturnValue('https://api.oxy.so'),
|
|
9
|
+
getAccessToken: jest.fn().mockReturnValue(null),
|
|
10
|
+
setTokens: jest.fn(),
|
|
11
|
+
onTokensChanged: jest.fn((l: (t: string | null) => void) => {
|
|
12
|
+
listeners.add(l);
|
|
13
|
+
return () => listeners.delete(l);
|
|
14
|
+
}),
|
|
15
|
+
silentSignIn: jest.fn().mockResolvedValue(null),
|
|
16
|
+
signInWithSharedIdentity: jest.fn().mockResolvedValue(null),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('createSessionClient', () => {
|
|
21
|
+
test('wires a SessionClient instance backed by the host + token transport', () => {
|
|
22
|
+
const oxy = fakeOxy();
|
|
23
|
+
|
|
24
|
+
const { client, host } = createSessionClient(oxy as never);
|
|
25
|
+
|
|
26
|
+
expect(client).toBeInstanceOf(SessionClient);
|
|
27
|
+
expect(typeof client.bootstrap).toBe('function');
|
|
28
|
+
expect(client.getState()).toBeNull();
|
|
29
|
+
expect(typeof host.setCurrentAccountId).toBe('function');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('the returned host reflects setCurrentAccountId', () => {
|
|
33
|
+
const oxy = fakeOxy();
|
|
34
|
+
|
|
35
|
+
const { host } = createSessionClient(oxy as never);
|
|
36
|
+
|
|
37
|
+
expect(host.getCurrentAccountId()).toBeNull();
|
|
38
|
+
host.setCurrentAccountId('u1');
|
|
39
|
+
expect(host.getCurrentAccountId()).toBe('u1');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { DeviceSessionState } from '@oxyhq/contracts';
|
|
2
|
+
import type { User } from '@oxyhq/core';
|
|
3
|
+
import {
|
|
4
|
+
accountIdsOf,
|
|
5
|
+
activeSessionIdOf,
|
|
6
|
+
activeUserOf,
|
|
7
|
+
deviceStateToClientSessions,
|
|
8
|
+
} from '../projectSessionState';
|
|
9
|
+
|
|
10
|
+
function makeUser(id: string): User {
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
publicKey: `pk-${id}`,
|
|
14
|
+
username: `user-${id}`,
|
|
15
|
+
name: {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// DeviceSessionState.updatedAt is an epoch-ms number on the wire (see
|
|
20
|
+
// packages/contracts/src/deviceSession.ts: `updatedAt: z.number()`), not an
|
|
21
|
+
// ISO string.
|
|
22
|
+
const UPDATED_AT_MS = Date.UTC(2026, 6, 1, 0, 0, 0, 0);
|
|
23
|
+
const UPDATED_AT_ISO = new Date(UPDATED_AT_MS).toISOString();
|
|
24
|
+
|
|
25
|
+
const state: DeviceSessionState = {
|
|
26
|
+
deviceId: 'device-1',
|
|
27
|
+
accounts: [
|
|
28
|
+
{ accountId: 'a1', sessionId: 'sess-a1', authuser: 0 },
|
|
29
|
+
{ accountId: 'a2', sessionId: 'sess-a2', authuser: 1 },
|
|
30
|
+
],
|
|
31
|
+
activeAccountId: 'a2',
|
|
32
|
+
revision: 1,
|
|
33
|
+
updatedAt: UPDATED_AT_MS,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const usersById = new Map<string, User>([
|
|
37
|
+
['a1', makeUser('a1')],
|
|
38
|
+
['a2', makeUser('a2')],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
describe('projectSessionState', () => {
|
|
42
|
+
describe('activeSessionIdOf', () => {
|
|
43
|
+
test('returns the active account sessionId', () => {
|
|
44
|
+
expect(activeSessionIdOf(state)).toBe('sess-a2');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('returns null for null state', () => {
|
|
48
|
+
expect(activeSessionIdOf(null)).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('returns null when activeAccountId is null', () => {
|
|
52
|
+
expect(activeSessionIdOf({ ...state, activeAccountId: null })).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('deviceStateToClientSessions', () => {
|
|
57
|
+
test('maps every account in order with isCurrent + authuser', () => {
|
|
58
|
+
const sessions = deviceStateToClientSessions(state, usersById);
|
|
59
|
+
expect(sessions).toHaveLength(2);
|
|
60
|
+
expect(sessions[0]).toEqual({
|
|
61
|
+
sessionId: 'sess-a1',
|
|
62
|
+
deviceId: 'device-1',
|
|
63
|
+
expiresAt: UPDATED_AT_ISO,
|
|
64
|
+
lastActive: UPDATED_AT_ISO,
|
|
65
|
+
userId: 'a1',
|
|
66
|
+
isCurrent: false,
|
|
67
|
+
authuser: 0,
|
|
68
|
+
});
|
|
69
|
+
expect(sessions[1]).toEqual({
|
|
70
|
+
sessionId: 'sess-a2',
|
|
71
|
+
deviceId: 'device-1',
|
|
72
|
+
expiresAt: UPDATED_AT_ISO,
|
|
73
|
+
lastActive: UPDATED_AT_ISO,
|
|
74
|
+
userId: 'a2',
|
|
75
|
+
isCurrent: true,
|
|
76
|
+
authuser: 1,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('still projects a session for an account absent from usersById', () => {
|
|
81
|
+
const sessions = deviceStateToClientSessions(state, new Map());
|
|
82
|
+
expect(sessions).toHaveLength(2);
|
|
83
|
+
expect(sessions.map((session) => session.userId)).toEqual(['a1', 'a2']);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('activeUserOf', () => {
|
|
88
|
+
test('returns the active account user', () => {
|
|
89
|
+
expect(activeUserOf(state, usersById)).toEqual(makeUser('a2'));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('returns null for null state', () => {
|
|
93
|
+
expect(activeUserOf(null, usersById)).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('returns null when activeAccountId is null', () => {
|
|
97
|
+
expect(activeUserOf({ ...state, activeAccountId: null }, usersById)).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('returns null when the active account id is absent from usersById', () => {
|
|
101
|
+
expect(activeUserOf(state, new Map())).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('accountIdsOf', () => {
|
|
106
|
+
test('returns every account id in order', () => {
|
|
107
|
+
expect(accountIdsOf(state)).toEqual(['a1', 'a2']);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('returns [] for null state', () => {
|
|
111
|
+
expect(accountIdsOf(null)).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createSessionClientHost } from '../sessionClientHost';
|
|
2
|
+
|
|
3
|
+
function fakeOxy() {
|
|
4
|
+
const listeners = new Set<(t: string | null) => void>();
|
|
5
|
+
return {
|
|
6
|
+
makeRequest: jest.fn().mockResolvedValue({ ok: true }),
|
|
7
|
+
getBaseURL: jest.fn().mockReturnValue('https://api.oxy.so'),
|
|
8
|
+
getAccessToken: jest.fn().mockReturnValue('tok'),
|
|
9
|
+
setTokens: jest.fn(),
|
|
10
|
+
onTokensChanged: jest.fn((l: (t: string | null) => void) => { listeners.add(l); return () => listeners.delete(l); }),
|
|
11
|
+
_emit: (t: string | null) => listeners.forEach((l) => l(t)),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('delegates REST + token methods to oxyServices', async () => {
|
|
16
|
+
const oxy = fakeOxy();
|
|
17
|
+
const host = createSessionClientHost(oxy as never);
|
|
18
|
+
await host.makeRequest('GET', '/session/device/state', undefined, { cache: false });
|
|
19
|
+
expect(oxy.makeRequest).toHaveBeenCalledWith('GET', '/session/device/state', undefined, { cache: false });
|
|
20
|
+
expect(host.getBaseURL()).toBe('https://api.oxy.so');
|
|
21
|
+
expect(host.getAccessToken()).toBe('tok');
|
|
22
|
+
host.setTokens('new');
|
|
23
|
+
expect(oxy.setTokens).toHaveBeenCalledWith('new');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('getCurrentAccountId reflects setCurrentAccountId', () => {
|
|
27
|
+
const host = createSessionClientHost(fakeOxy() as never);
|
|
28
|
+
expect(host.getCurrentAccountId()).toBeNull();
|
|
29
|
+
host.setCurrentAccountId('u1');
|
|
30
|
+
expect(host.getCurrentAccountId()).toBe('u1');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('onTokensChanged forwards to oxyServices and unsubscribes', () => {
|
|
34
|
+
const oxy = fakeOxy();
|
|
35
|
+
const host = createSessionClientHost(oxy as never);
|
|
36
|
+
const cb = jest.fn();
|
|
37
|
+
const unsub = host.onTokensChanged(cb);
|
|
38
|
+
oxy._emit(null);
|
|
39
|
+
expect(cb).toHaveBeenCalledWith(null);
|
|
40
|
+
unsub();
|
|
41
|
+
oxy._emit('x');
|
|
42
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
43
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { DeviceSessionState } from '@oxyhq/contracts';
|
|
2
|
+
import { logger } from '@oxyhq/core';
|
|
3
|
+
import { createTokenTransport } from '../tokenTransport';
|
|
4
|
+
import { isWebBrowser } from '../../hooks/useWebSSO';
|
|
5
|
+
|
|
6
|
+
jest.mock('../../hooks/useWebSSO', () => ({
|
|
7
|
+
isWebBrowser: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
const mockedIsWebBrowser = isWebBrowser as jest.MockedFunction<typeof isWebBrowser>;
|
|
11
|
+
|
|
12
|
+
function fakeOxy(accessToken: string | null) {
|
|
13
|
+
return {
|
|
14
|
+
getAccessToken: jest.fn().mockReturnValue(accessToken),
|
|
15
|
+
silentSignIn: jest.fn().mockResolvedValue(null),
|
|
16
|
+
signInWithSharedIdentity: jest.fn().mockResolvedValue(null),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const state: DeviceSessionState = {
|
|
21
|
+
deviceId: 'device-1',
|
|
22
|
+
accounts: [{ accountId: 'a1', sessionId: 'sess-a1', authuser: 0 }],
|
|
23
|
+
activeAccountId: 'a1',
|
|
24
|
+
revision: 1,
|
|
25
|
+
updatedAt: Date.UTC(2026, 6, 1, 0, 0, 0, 0),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
describe('createTokenTransport', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('no-ops when a token is already present', async () => {
|
|
34
|
+
const oxy = fakeOxy('tok');
|
|
35
|
+
const transport = createTokenTransport(oxy as never);
|
|
36
|
+
|
|
37
|
+
await transport.ensureActiveToken(state);
|
|
38
|
+
|
|
39
|
+
expect(oxy.silentSignIn).not.toHaveBeenCalled();
|
|
40
|
+
expect(oxy.signInWithSharedIdentity).not.toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('mints via silentSignIn on web when no token is present', async () => {
|
|
44
|
+
mockedIsWebBrowser.mockReturnValue(true);
|
|
45
|
+
const oxy = fakeOxy(null);
|
|
46
|
+
const transport = createTokenTransport(oxy as never);
|
|
47
|
+
|
|
48
|
+
await transport.ensureActiveToken(state);
|
|
49
|
+
|
|
50
|
+
expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(oxy.signInWithSharedIdentity).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('mints via signInWithSharedIdentity off web when no token is present', async () => {
|
|
55
|
+
mockedIsWebBrowser.mockReturnValue(false);
|
|
56
|
+
const oxy = fakeOxy(null);
|
|
57
|
+
const transport = createTokenTransport(oxy as never);
|
|
58
|
+
|
|
59
|
+
await transport.ensureActiveToken(state);
|
|
60
|
+
|
|
61
|
+
expect(oxy.signInWithSharedIdentity).toHaveBeenCalledTimes(1);
|
|
62
|
+
expect(oxy.silentSignIn).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('resolves (never rejects) and logs a warning when the mint throws', async () => {
|
|
66
|
+
mockedIsWebBrowser.mockReturnValue(true);
|
|
67
|
+
const oxy = fakeOxy(null);
|
|
68
|
+
oxy.silentSignIn.mockRejectedValue(new Error('mint boom'));
|
|
69
|
+
const transport = createTokenTransport(oxy as never);
|
|
70
|
+
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
|
|
71
|
+
|
|
72
|
+
await expect(transport.ensureActiveToken(state)).resolves.toBeUndefined();
|
|
73
|
+
|
|
74
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
75
|
+
'ensureActiveToken: mint failed',
|
|
76
|
+
{ component: 'TokenTransport' },
|
|
77
|
+
expect.any(Error),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
warnSpy.mockRestore();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('resolves cleanly when the mint returns null', async () => {
|
|
84
|
+
mockedIsWebBrowser.mockReturnValue(true);
|
|
85
|
+
const oxy = fakeOxy(null);
|
|
86
|
+
const transport = createTokenTransport(oxy as never);
|
|
87
|
+
|
|
88
|
+
await expect(transport.ensureActiveToken(state)).resolves.toBeUndefined();
|
|
89
|
+
expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('coalesces concurrent calls into a single mint', async () => {
|
|
93
|
+
mockedIsWebBrowser.mockReturnValue(true);
|
|
94
|
+
const oxy = fakeOxy(null);
|
|
95
|
+
let resolveMint: ((value: null) => void) | null = null;
|
|
96
|
+
oxy.silentSignIn.mockImplementation(
|
|
97
|
+
() =>
|
|
98
|
+
new Promise((resolve) => {
|
|
99
|
+
resolveMint = resolve;
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
const transport = createTokenTransport(oxy as never);
|
|
103
|
+
|
|
104
|
+
const first = transport.ensureActiveToken(state);
|
|
105
|
+
const second = transport.ensureActiveToken(state);
|
|
106
|
+
|
|
107
|
+
expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
|
|
108
|
+
|
|
109
|
+
resolveMint?.(null);
|
|
110
|
+
await Promise.all([first, second]);
|
|
111
|
+
|
|
112
|
+
expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
|
|
113
|
+
|
|
114
|
+
// A later call after the in-flight mint settled starts a fresh mint
|
|
115
|
+
// (the mock reassigns `resolveMint` on the second silentSignIn call).
|
|
116
|
+
const third = transport.ensureActiveToken(state);
|
|
117
|
+
expect(oxy.silentSignIn).toHaveBeenCalledTimes(2);
|
|
118
|
+
resolveMint?.(null);
|
|
119
|
+
await third;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('treats a throwing getAccessToken as no-token and still mints', async () => {
|
|
123
|
+
mockedIsWebBrowser.mockReturnValue(true);
|
|
124
|
+
const oxy = fakeOxy(null);
|
|
125
|
+
oxy.getAccessToken.mockImplementation(() => {
|
|
126
|
+
throw new Error('storage unavailable');
|
|
127
|
+
});
|
|
128
|
+
const transport = createTokenTransport(oxy as never);
|
|
129
|
+
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => undefined);
|
|
130
|
+
|
|
131
|
+
await expect(transport.ensureActiveToken(state)).resolves.toBeUndefined();
|
|
132
|
+
|
|
133
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
134
|
+
'ensureActiveToken: getAccessToken threw',
|
|
135
|
+
{ component: 'TokenTransport' },
|
|
136
|
+
expect.any(Error),
|
|
137
|
+
);
|
|
138
|
+
expect(oxy.silentSignIn).toHaveBeenCalledTimes(1);
|
|
139
|
+
|
|
140
|
+
warnSpy.mockRestore();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { OxyServices } from '@oxyhq/core';
|
|
2
|
+
import { SessionClient } from '@oxyhq/core';
|
|
3
|
+
import { createSessionClientHost } from './sessionClientHost';
|
|
4
|
+
import { createTokenTransport } from './tokenTransport';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Wires a `SessionClient` for `@oxyhq/services`: builds the `SessionClientHost`
|
|
8
|
+
* adapter (Task 1) and the platform `TokenTransport` (Task 3) over the given
|
|
9
|
+
* `OxyServices` instance, and returns both the client and the host — the host
|
|
10
|
+
* is returned (not just the client) so `OxyContext` (Fase 3-B) can call
|
|
11
|
+
* `host.setCurrentAccountId(...)` as the active account changes.
|
|
12
|
+
*/
|
|
13
|
+
export function createSessionClient(oxyServices: OxyServices): {
|
|
14
|
+
client: SessionClient;
|
|
15
|
+
host: ReturnType<typeof createSessionClientHost>;
|
|
16
|
+
} {
|
|
17
|
+
const host = createSessionClientHost(oxyServices);
|
|
18
|
+
const transport = createTokenTransport(oxyServices);
|
|
19
|
+
const client = new SessionClient(host, { transport });
|
|
20
|
+
return { client, host };
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-sync integration layer (Fase 3-A) — intra-package use only.
|
|
3
|
+
*
|
|
4
|
+
* These modules let `@oxyhq/services` drive the platform-agnostic
|
|
5
|
+
* `SessionClient` (in `@oxyhq/core`): a thin host adapter over `OxyServices`,
|
|
6
|
+
* pure `DeviceSessionState → services` projection helpers, a platform token
|
|
7
|
+
* transport, and the factory that wires them together. Nothing here is
|
|
8
|
+
* re-exported from the package root — `OxyContext` consumes it directly in
|
|
9
|
+
* Fase 3-B.
|
|
10
|
+
*/
|
|
11
|
+
export { createSessionClient } from './createSessionClient';
|
|
12
|
+
export { createSessionClientHost } from './sessionClientHost';
|
|
13
|
+
export { createTokenTransport } from './tokenTransport';
|
|
14
|
+
export {
|
|
15
|
+
accountIdsOf,
|
|
16
|
+
activeSessionIdOf,
|
|
17
|
+
activeUserOf,
|
|
18
|
+
deviceStateToClientSessions,
|
|
19
|
+
} from './projectSessionState';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DeviceSessionState } from '@oxyhq/contracts';
|
|
2
|
+
import type { ClientSession, User } from '@oxyhq/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pure projection helpers: `DeviceSessionState` (the device-scoped
|
|
6
|
+
* multi-account session-sync state produced by `@oxyhq/core`'s
|
|
7
|
+
* `SessionClient`) -> the shapes `OxyContext` renders today
|
|
8
|
+
* (`ClientSession[]`, an active session id, an active `User`).
|
|
9
|
+
*
|
|
10
|
+
* No I/O. The caller fetches profiles via
|
|
11
|
+
* `oxyServices.getUsersByIds(accountIdsOf(state))` and builds `usersById`
|
|
12
|
+
* from the result before calling `deviceStateToClientSessions` /
|
|
13
|
+
* `activeUserOf`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Maps every `SessionAccount` in `state.accounts` to a `ClientSession`.
|
|
18
|
+
*
|
|
19
|
+
* `DeviceSessionState` carries no per-account `expiresAt` / `lastActive` —
|
|
20
|
+
* both are set to `state.updatedAt` (converted to an ISO-8601 string; the
|
|
21
|
+
* wire value is an epoch-ms number) as a provisional value. Fase 3-B
|
|
22
|
+
* rewrites `ClientSession` to make `expiresAt`/`lastActive` optional (they
|
|
23
|
+
* are not derivable from `DeviceSessionState`).
|
|
24
|
+
*
|
|
25
|
+
* `usersById` is accepted for signature symmetry with `activeUserOf` even
|
|
26
|
+
* though `ClientSession` only stores `userId` — a session is still
|
|
27
|
+
* projected for an account whose id is absent from `usersById` (no
|
|
28
|
+
* placeholder user is fabricated).
|
|
29
|
+
*/
|
|
30
|
+
export function deviceStateToClientSessions(
|
|
31
|
+
state: DeviceSessionState,
|
|
32
|
+
usersById: Map<string, User>,
|
|
33
|
+
): ClientSession[] {
|
|
34
|
+
const provisionalTimestamp = new Date(state.updatedAt).toISOString();
|
|
35
|
+
return state.accounts.map((account) => ({
|
|
36
|
+
sessionId: account.sessionId,
|
|
37
|
+
deviceId: state.deviceId,
|
|
38
|
+
// provisional: Fase 3-B rewrites ClientSession to make expiresAt/lastActive
|
|
39
|
+
// optional (they are not in DeviceSessionState)
|
|
40
|
+
expiresAt: provisionalTimestamp,
|
|
41
|
+
lastActive: provisionalTimestamp,
|
|
42
|
+
userId: account.accountId,
|
|
43
|
+
isCurrent: account.accountId === state.activeAccountId,
|
|
44
|
+
authuser: account.authuser,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The active account's `sessionId`, or `null` when there is no state or no
|
|
50
|
+
* active account is set.
|
|
51
|
+
*/
|
|
52
|
+
export function activeSessionIdOf(state: DeviceSessionState | null): string | null {
|
|
53
|
+
if (state === null || state.activeAccountId === null) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const activeAccountId = state.activeAccountId;
|
|
57
|
+
const activeAccount = state.accounts.find((account) => account.accountId === activeAccountId);
|
|
58
|
+
return activeAccount?.sessionId ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The active account's `User`, resolved from `usersById`. `null` when there
|
|
63
|
+
* is no state, no active account is set, or the active account id is absent
|
|
64
|
+
* from `usersById`.
|
|
65
|
+
*/
|
|
66
|
+
export function activeUserOf(
|
|
67
|
+
state: DeviceSessionState | null,
|
|
68
|
+
usersById: Map<string, User>,
|
|
69
|
+
): User | null {
|
|
70
|
+
if (state === null || state.activeAccountId === null) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return usersById.get(state.activeAccountId) ?? null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* All account ids in `state`, suitable for an `oxyServices.getUsersByIds(...)`
|
|
78
|
+
* fetch. `[]` for `null` state.
|
|
79
|
+
*/
|
|
80
|
+
export function accountIdsOf(state: DeviceSessionState | null): string[] {
|
|
81
|
+
if (state === null) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
return state.accounts.map((account) => account.accountId);
|
|
85
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OxyServices, SessionClientHost } from '@oxyhq/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin `SessionClientHost` adapter over an `OxyServices` instance.
|
|
5
|
+
*
|
|
6
|
+
* `SessionClient` (in `@oxyhq/core`) is host-agnostic: it only needs a REST +
|
|
7
|
+
* token surface. `OxyServices` already exposes all of that except
|
|
8
|
+
* `getCurrentAccountId`, which has no direct equivalent — the adapter holds a
|
|
9
|
+
* mutable ref set by the caller (`OxyContext`, in Fase 3-B) via
|
|
10
|
+
* `setCurrentAccountId`.
|
|
11
|
+
*/
|
|
12
|
+
export function createSessionClientHost(
|
|
13
|
+
oxyServices: OxyServices,
|
|
14
|
+
): SessionClientHost & { setCurrentAccountId(id: string | null): void } {
|
|
15
|
+
let currentAccountId: string | null = null;
|
|
16
|
+
return {
|
|
17
|
+
makeRequest: (method, url, data, options) => oxyServices.makeRequest(method, url, data, options),
|
|
18
|
+
getBaseURL: () => oxyServices.getBaseURL(),
|
|
19
|
+
getAccessToken: () => oxyServices.getAccessToken(),
|
|
20
|
+
onTokensChanged: (listener) => oxyServices.onTokensChanged(listener),
|
|
21
|
+
setTokens: (accessToken) => oxyServices.setTokens(accessToken),
|
|
22
|
+
getCurrentAccountId: () => currentAccountId,
|
|
23
|
+
setCurrentAccountId: (id) => {
|
|
24
|
+
currentAccountId = id;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { DeviceSessionState } from '@oxyhq/contracts';
|
|
2
|
+
import type { OxyServices, TokenTransport } from '@oxyhq/core';
|
|
3
|
+
import { logger } from '@oxyhq/core';
|
|
4
|
+
import { isWebBrowser } from '../hooks/useWebSSO';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Platform `TokenTransport` for `SessionClient`: mints an access token when
|
|
8
|
+
* none is currently held, reusing the SAME primitives `OxyContext`'s cold
|
|
9
|
+
* boot already relies on (never re-implemented here):
|
|
10
|
+
*
|
|
11
|
+
* - web: `oxyServices.silentSignIn()` (per-apex `/auth/silent` iframe).
|
|
12
|
+
* - native: `oxyServices.signInWithSharedIdentity()` (app-group keychain).
|
|
13
|
+
*
|
|
14
|
+
* Both primitives plant the token internally on success (the "Sign-In Token
|
|
15
|
+
* Planting" rule) — `ensureActiveToken` never calls `setTokens` itself.
|
|
16
|
+
*
|
|
17
|
+
* `ensureActiveToken` treats a PRESENT token as sufficient in this phase; it
|
|
18
|
+
* does not decode/match the token's subject against `state.activeAccountId`
|
|
19
|
+
* (that refinement is Fase 3-B). Account switching itself is server-driven
|
|
20
|
+
* via the `activeToken` carried in the sync envelope — this transport is
|
|
21
|
+
* only the fallback used when the envelope carried no token.
|
|
22
|
+
*
|
|
23
|
+
* A failed mint is logged and swallowed: it must never throw out of
|
|
24
|
+
* `ensureActiveToken`, since that would crash the caller (the SessionClient
|
|
25
|
+
* socket handler in Fase 3-B).
|
|
26
|
+
*/
|
|
27
|
+
export function createTokenTransport(oxyServices: OxyServices): TokenTransport {
|
|
28
|
+
// Coalesces concurrent mints: `SessionClient.applyState` can fire
|
|
29
|
+
// `ensureActiveToken` on rapid successive state pushes; a second call while a
|
|
30
|
+
// mint is already in flight must reuse it, not spawn a second silent iframe /
|
|
31
|
+
// shared-key challenge round-trip.
|
|
32
|
+
let inFlightMint: Promise<void> | null = null;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
async ensureActiveToken(state: DeviceSessionState): Promise<void> {
|
|
36
|
+
// Read the current token defensively: it is an in-memory getter that
|
|
37
|
+
// should never throw, but the documented contract is that this method
|
|
38
|
+
// never throws out — so a surprising throw is logged and treated as "no
|
|
39
|
+
// token" (fall through to mint) rather than rejecting the promise.
|
|
40
|
+
try {
|
|
41
|
+
if (oxyServices.getAccessToken()) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.warn('ensureActiveToken: getAccessToken threw', { component: 'TokenTransport' }, error);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (inFlightMint) {
|
|
49
|
+
return inFlightMint;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
inFlightMint = (async () => {
|
|
53
|
+
try {
|
|
54
|
+
const session = isWebBrowser()
|
|
55
|
+
? await oxyServices.silentSignIn()
|
|
56
|
+
: await oxyServices.signInWithSharedIdentity();
|
|
57
|
+
|
|
58
|
+
if (!session) {
|
|
59
|
+
logger.debug('ensureActiveToken: platform mint returned no session', {
|
|
60
|
+
component: 'TokenTransport',
|
|
61
|
+
deviceId: state.deviceId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.warn('ensureActiveToken: mint failed', { component: 'TokenTransport' }, error);
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await inFlightMint;
|
|
71
|
+
} finally {
|
|
72
|
+
inFlightMint = null;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|