@oxyhq/core 7.0.0 → 7.1.1
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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +17 -2
- package/dist/cjs/session/SessionClient.js +181 -10
- package/dist/cjs/session/accountDialogController.js +626 -0
- package/dist/cjs/session/accountProjection.js +131 -0
- package/dist/cjs/session/createSessionClient.js +9 -2
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/index.js +11 -0
- package/dist/esm/session/SessionClient.js +181 -10
- package/dist/esm/session/accountDialogController.js +621 -0
- package/dist/esm/session/accountProjection.js +127 -0
- package/dist/esm/session/createSessionClient.js +9 -2
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/session/SessionClient.d.ts +51 -0
- package/dist/types/session/accountDialogController.d.ts +273 -0
- package/dist/types/session/accountProjection.d.ts +142 -0
- package/dist/types/session/createSessionClient.d.ts +9 -2
- package/package.json +1 -1
- package/src/index.ts +31 -0
- package/src/session/SessionClient.ts +201 -11
- package/src/session/__tests__/SessionClient.signedOut.test.ts +224 -0
- package/src/session/__tests__/accountDialogController.test.ts +592 -0
- package/src/session/__tests__/accountProjection.test.ts +181 -0
- package/src/session/accountDialogController.ts +769 -0
- package/src/session/accountProjection.ts +263 -0
- package/src/session/createSessionClient.ts +9 -2
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { DeviceSessionState } from '@oxyhq/contracts';
|
|
2
|
+
import type { User } from '../../models/interfaces';
|
|
3
|
+
import type { AccountNode } from '../../mixins/OxyServices.accounts';
|
|
4
|
+
import {
|
|
5
|
+
projectSwitchableAccounts,
|
|
6
|
+
switchableAccountIds,
|
|
7
|
+
} from '../accountProjection';
|
|
8
|
+
|
|
9
|
+
function user(id: string, over: Partial<User> = {}): User {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
publicKey: `pk_${id}`,
|
|
13
|
+
username: `user_${id}`,
|
|
14
|
+
name: { displayName: `User ${id}` },
|
|
15
|
+
...over,
|
|
16
|
+
} as User;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function state(
|
|
20
|
+
accounts: Array<{ accountId: string; sessionId: string; authuser?: number }>,
|
|
21
|
+
activeAccountId: string | null,
|
|
22
|
+
): DeviceSessionState {
|
|
23
|
+
return {
|
|
24
|
+
deviceId: 'device-1',
|
|
25
|
+
accounts: accounts.map((a) => ({ accountId: a.accountId, sessionId: a.sessionId, authuser: a.authuser ?? 0 })),
|
|
26
|
+
activeAccountId,
|
|
27
|
+
revision: 1,
|
|
28
|
+
updatedAt: 1_720_000_000_000,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function graphNode(id: string, over: Partial<AccountNode> = {}): AccountNode {
|
|
33
|
+
return {
|
|
34
|
+
accountId: id,
|
|
35
|
+
kind: 'organization',
|
|
36
|
+
parentAccountId: null,
|
|
37
|
+
account: user(id),
|
|
38
|
+
relationship: 'owner',
|
|
39
|
+
callerMembership: null,
|
|
40
|
+
...over,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mapOf = (...users: User[]): Map<string, User> => {
|
|
45
|
+
const map = new Map<string, User>();
|
|
46
|
+
for (const u of users) map.set(u.id, u);
|
|
47
|
+
return map;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const noAvatar = (): undefined => undefined;
|
|
51
|
+
|
|
52
|
+
describe('projectSwitchableAccounts', () => {
|
|
53
|
+
it('returns [] for null state and empty graph', () => {
|
|
54
|
+
expect(
|
|
55
|
+
projectSwitchableAccounts({ state: null, graph: [], profilesById: new Map(), resolveAvatarUrl: noAvatar }),
|
|
56
|
+
).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('projects device rows and flags the active account current', () => {
|
|
60
|
+
const rows = projectSwitchableAccounts({
|
|
61
|
+
state: state([{ accountId: 'a1', sessionId: 's1', authuser: 0 }, { accountId: 'a2', sessionId: 's2', authuser: 1 }], 'a2'),
|
|
62
|
+
graph: [],
|
|
63
|
+
profilesById: mapOf(user('a1'), user('a2')),
|
|
64
|
+
resolveAvatarUrl: noAvatar,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(rows.map((r) => r.accountId)).toEqual(['a1', 'a2']);
|
|
68
|
+
expect(rows.map((r) => r.isCurrent)).toEqual([false, true]);
|
|
69
|
+
expect(rows.every((r) => r.onDevice)).toBe(true);
|
|
70
|
+
expect(rows[1].sessionId).toBe('s2');
|
|
71
|
+
expect(rows[1].authuser).toBe(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('omits device accounts whose profile is not resolved (except the active one via activeUser)', () => {
|
|
75
|
+
const rows = projectSwitchableAccounts({
|
|
76
|
+
state: state([{ accountId: 'a1', sessionId: 's1' }, { accountId: 'a2', sessionId: 's2' }], 'a1'),
|
|
77
|
+
graph: [],
|
|
78
|
+
// a2 has no resolved profile; a1 is active and provided via activeUser.
|
|
79
|
+
profilesById: new Map(),
|
|
80
|
+
activeUser: user('a1', { name: { displayName: 'Fresh A1' } }),
|
|
81
|
+
resolveAvatarUrl: noAvatar,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(rows.map((r) => r.accountId)).toEqual(['a1']);
|
|
85
|
+
expect(rows[0].displayName).toBe('Fresh A1');
|
|
86
|
+
expect(rows[0].isCurrent).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('prefers activeUser over profilesById for the active row (freshness)', () => {
|
|
90
|
+
const rows = projectSwitchableAccounts({
|
|
91
|
+
state: state([{ accountId: 'a1', sessionId: 's1' }], 'a1'),
|
|
92
|
+
graph: [],
|
|
93
|
+
profilesById: mapOf(user('a1', { name: { displayName: 'Stale' } })),
|
|
94
|
+
activeUser: user('a1', { name: { displayName: 'Fresh' } }),
|
|
95
|
+
resolveAvatarUrl: noAvatar,
|
|
96
|
+
});
|
|
97
|
+
expect(rows[0].displayName).toBe('Fresh');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('merges graph-only accounts after device rows, carrying graph metadata', () => {
|
|
101
|
+
const rows = projectSwitchableAccounts({
|
|
102
|
+
state: state([{ accountId: 'a1', sessionId: 's1' }], 'a1'),
|
|
103
|
+
graph: [graphNode('org1', { kind: 'organization', relationship: 'owner', parentAccountId: 'a1' })],
|
|
104
|
+
profilesById: mapOf(user('a1')),
|
|
105
|
+
resolveAvatarUrl: noAvatar,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(rows.map((r) => r.accountId)).toEqual(['a1', 'org1']);
|
|
109
|
+
const org = rows[1];
|
|
110
|
+
expect(org.onDevice).toBe(false);
|
|
111
|
+
expect(org.isCurrent).toBe(false);
|
|
112
|
+
expect(org.sessionId).toBeUndefined();
|
|
113
|
+
expect(org.kind).toBe('organization');
|
|
114
|
+
expect(org.relationship).toBe('owner');
|
|
115
|
+
expect(org.parentAccountId).toBe('a1');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('dedups an account present as BOTH device session and graph node into ONE enriched row', () => {
|
|
119
|
+
const rows = projectSwitchableAccounts({
|
|
120
|
+
state: state([{ accountId: 'a1', sessionId: 's1', authuser: 0 }], 'a1'),
|
|
121
|
+
graph: [graphNode('a1', { kind: 'personal', relationship: 'self', callerMembership: null })],
|
|
122
|
+
profilesById: mapOf(user('a1')),
|
|
123
|
+
resolveAvatarUrl: noAvatar,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(rows).toHaveLength(1);
|
|
127
|
+
const row = rows[0];
|
|
128
|
+
// Keeps the device sessionId + active flag, gains the graph metadata.
|
|
129
|
+
expect(row.sessionId).toBe('s1');
|
|
130
|
+
expect(row.onDevice).toBe(true);
|
|
131
|
+
expect(row.isCurrent).toBe(true);
|
|
132
|
+
expect(row.kind).toBe('personal');
|
|
133
|
+
expect(row.relationship).toBe('self');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('resolves avatar url via the injected resolver and falls back email to @handle', () => {
|
|
137
|
+
const rows = projectSwitchableAccounts({
|
|
138
|
+
state: state([{ accountId: 'a1', sessionId: 's1' }], 'a1'),
|
|
139
|
+
graph: [],
|
|
140
|
+
profilesById: mapOf(user('a1', { avatar: 'file123', email: undefined, username: 'nate' })),
|
|
141
|
+
resolveAvatarUrl: (avatar) => (avatar ? `https://cdn/${avatar}` : undefined),
|
|
142
|
+
});
|
|
143
|
+
expect(rows[0].avatarUrl).toBe('https://cdn/file123');
|
|
144
|
+
// No real email → `@handle` secondary line, never synthesized.
|
|
145
|
+
expect(rows[0].email).toBe('@nate');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('uses a real email when present', () => {
|
|
149
|
+
const rows = projectSwitchableAccounts({
|
|
150
|
+
state: state([{ accountId: 'a1', sessionId: 's1' }], 'a1'),
|
|
151
|
+
graph: [],
|
|
152
|
+
profilesById: mapOf(user('a1', { email: 'real@oxy.so' })),
|
|
153
|
+
resolveAvatarUrl: noAvatar,
|
|
154
|
+
});
|
|
155
|
+
expect(rows[0].email).toBe('real@oxy.so');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('marks no row current when activeAccountId is null', () => {
|
|
159
|
+
const rows = projectSwitchableAccounts({
|
|
160
|
+
state: state([{ accountId: 'a1', sessionId: 's1' }], null),
|
|
161
|
+
graph: [],
|
|
162
|
+
profilesById: mapOf(user('a1')),
|
|
163
|
+
resolveAvatarUrl: noAvatar,
|
|
164
|
+
});
|
|
165
|
+
expect(rows.every((r) => !r.isCurrent)).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('switchableAccountIds', () => {
|
|
170
|
+
it('unions device + graph ids, deduped and sorted', () => {
|
|
171
|
+
const ids = switchableAccountIds(
|
|
172
|
+
state([{ accountId: 'b', sessionId: 's1' }, { accountId: 'a', sessionId: 's2' }], 'a'),
|
|
173
|
+
[graphNode('c'), graphNode('a')],
|
|
174
|
+
);
|
|
175
|
+
expect(ids).toEqual(['a', 'b', 'c']);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns [] for null state and empty graph', () => {
|
|
179
|
+
expect(switchableAccountIds(null, [])).toEqual([]);
|
|
180
|
+
});
|
|
181
|
+
});
|