@oxyhq/core 20.1.0 → 21.0.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/HttpService.js +47 -8
- package/dist/cjs/boot/sessionColdBoot.js +107 -8
- package/dist/cjs/i18n/locales/en-US.json +26 -4
- package/dist/cjs/i18n/locales/es-ES.json +26 -4
- package/dist/cjs/i18n/locales/locales/en-US.json +26 -4
- package/dist/cjs/i18n/locales/locales/es-ES.json +26 -4
- package/dist/cjs/index.js +57 -16
- package/dist/cjs/inference/OxyInferenceClient.js +330 -0
- package/dist/cjs/mixins/OxyServices.accounts.js +5 -72
- package/dist/cjs/mixins/OxyServices.auth.js +27 -3
- package/dist/cjs/mixins/OxyServices.inference.js +59 -0
- package/dist/cjs/mixins/OxyServices.utility.js +18 -6
- package/dist/cjs/mixins/index.js +6 -0
- package/dist/cjs/server/auth.js +76 -0
- package/dist/cjs/server/index.js +5 -1
- package/dist/cjs/session/SessionClient.js +361 -1
- package/dist/cjs/session/accountDialogController.js +121 -147
- package/dist/cjs/session/accountSwitchTargets.js +75 -0
- package/dist/cjs/session/deviceDirectory.js +143 -0
- package/dist/cjs/session/deviceSwitcherRows.js +76 -0
- package/dist/cjs/session/projectSessionState.js +8 -1
- package/dist/cjs/session/sharedDeviceCredential.js +247 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +47 -8
- package/dist/esm/boot/sessionColdBoot.js +107 -8
- package/dist/esm/i18n/locales/en-US.json +26 -4
- package/dist/esm/i18n/locales/es-ES.json +26 -4
- package/dist/esm/i18n/locales/locales/en-US.json +26 -4
- package/dist/esm/i18n/locales/locales/es-ES.json +26 -4
- package/dist/esm/index.js +36 -10
- package/dist/esm/inference/OxyInferenceClient.js +325 -0
- package/dist/esm/mixins/OxyServices.accounts.js +5 -72
- package/dist/esm/mixins/OxyServices.auth.js +27 -3
- package/dist/esm/mixins/OxyServices.inference.js +56 -0
- package/dist/esm/mixins/OxyServices.utility.js +18 -6
- package/dist/esm/mixins/index.js +6 -0
- package/dist/esm/server/auth.js +72 -0
- package/dist/esm/server/index.js +1 -1
- package/dist/esm/session/SessionClient.js +362 -2
- package/dist/esm/session/accountDialogController.js +121 -147
- package/dist/esm/session/accountSwitchTargets.js +71 -0
- package/dist/esm/session/deviceDirectory.js +135 -0
- package/dist/esm/session/deviceSwitcherRows.js +72 -0
- package/dist/esm/session/projectSessionState.js +8 -2
- package/dist/esm/session/sharedDeviceCredential.js +239 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +39 -1
- package/dist/types/boot/sessionColdBoot.d.ts +24 -4
- package/dist/types/index.d.ts +11 -4
- package/dist/types/inference/OxyInferenceClient.d.ts +324 -0
- package/dist/types/mixins/OxyServices.accounts.d.ts +73 -95
- package/dist/types/mixins/OxyServices.auth.d.ts +75 -3
- package/dist/types/mixins/OxyServices.inference.d.ts +95 -0
- package/dist/types/mixins/OxyServices.utility.d.ts +44 -13
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/models/session.d.ts +11 -0
- package/dist/types/server/auth.d.ts +80 -0
- package/dist/types/server/index.d.ts +2 -2
- package/dist/types/session/SessionClient.d.ts +202 -1
- package/dist/types/session/accountDialogController.d.ts +76 -64
- package/dist/types/session/accountSwitchTargets.d.ts +64 -0
- package/dist/types/session/deviceDirectory.d.ts +182 -0
- package/dist/types/session/deviceSwitcherRows.d.ts +92 -0
- package/dist/types/session/projectSessionState.d.ts +29 -0
- package/dist/types/session/sharedDeviceCredential.d.ts +202 -0
- package/package.json +3 -3
- package/src/HttpService.ts +50 -10
- package/src/__tests__/httpServiceUnwrapEnvelope.test.ts +115 -0
- package/src/boot/__tests__/sessionColdBoot.sharedDevice.test.ts +325 -0
- package/src/boot/sessionColdBoot.ts +133 -9
- package/src/i18n/locales/en-US.json +26 -4
- package/src/i18n/locales/es-ES.json +26 -4
- package/src/index.ts +94 -25
- package/src/inference/OxyInferenceClient.ts +590 -0
- package/src/inference/__tests__/OxyInferenceClient.test.ts +383 -0
- package/src/mixins/OxyServices.accounts.ts +75 -176
- package/src/mixins/OxyServices.auth.ts +67 -5
- package/src/mixins/OxyServices.inference.ts +57 -0
- package/src/mixins/OxyServices.utility.ts +58 -14
- package/src/mixins/__tests__/accounts.test.ts +57 -102
- package/src/mixins/__tests__/inferenceFactory.test.ts +58 -0
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +54 -1
- package/src/mixins/__tests__/serviceAuth.test.ts +2 -0
- package/src/mixins/index.ts +8 -0
- package/src/models/session.ts +11 -0
- package/src/server/__tests__/serviceTokenAttribution.test.ts +396 -0
- package/src/server/auth.ts +118 -0
- package/src/server/index.ts +6 -0
- package/src/session/SessionClient.ts +386 -1
- package/src/session/__tests__/SessionClient.directory.test.ts +688 -0
- package/src/session/__tests__/accountDialogController.test.ts +411 -278
- package/src/session/__tests__/accountDialogShape.test.ts +118 -0
- package/src/session/__tests__/accountSwitchTargets.test.ts +132 -0
- package/src/session/__tests__/deviceDirectory.test.ts +422 -0
- package/src/session/__tests__/deviceSwitcherRows.test.ts +223 -0
- package/src/session/__tests__/projectSessionState.test.ts +17 -0
- package/src/session/__tests__/sharedDeviceCredential.test.ts +300 -0
- package/src/session/accountDialogController.ts +141 -179
- package/src/session/accountSwitchTargets.ts +87 -0
- package/src/session/deviceDirectory.ts +269 -0
- package/src/session/deviceSwitcherRows.ts +145 -0
- package/src/session/projectSessionState.ts +9 -3
- package/src/session/sharedDeviceCredential.ts +349 -0
- package/dist/cjs/session/accountProjection.js +0 -213
- package/dist/esm/session/accountProjection.js +0 -207
- package/dist/types/session/accountProjection.d.ts +0 -198
- package/src/session/__tests__/accountProjection.test.ts +0 -447
- package/src/session/accountProjection.ts +0 -354
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified account-list projection — THE single source of truth.
|
|
3
|
-
*
|
|
4
|
-
* Produces the flat `SwitchableAccount[]` every account chooser renders, by
|
|
5
|
-
* merging the device's server-authoritative session set (`DeviceSessionState`
|
|
6
|
-
* from {@link SessionClient}) with the caller's account graph (`AccountNode[]`
|
|
7
|
-
* from `oxyServices.listAccounts()`), deduped by `accountId`. This lives in
|
|
8
|
-
* `@oxyhq/core` so every `@oxyhq/services` platform variant — and
|
|
9
|
-
* `auth.oxy.so` — all render the SAME list from the SAME logic and cannot
|
|
10
|
-
* diverge.
|
|
11
|
-
*
|
|
12
|
-
* Pure and I/O-free: the caller resolves per-account profiles via
|
|
13
|
-
* `oxyServices.getUsersByIds(...)` and passes them in as `profilesById`, and
|
|
14
|
-
* binds `resolveAvatarUrl` to `oxyServices.getFileDownloadUrl`. This is the same
|
|
15
|
-
* split the former `@oxyhq/services` `buildSwitchableAccounts` used — hoisted
|
|
16
|
-
* into core, keyed directly on `DeviceSessionState` (whose `activeAccountId` is
|
|
17
|
-
* atomic, so no cross-call current-row reconciliation is needed).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type { DeviceSessionState } from '@oxyhq/contracts';
|
|
21
|
-
import { isActAsEligibleKind } from '@oxyhq/contracts';
|
|
22
|
-
import type { User } from '../models/interfaces';
|
|
23
|
-
import type {
|
|
24
|
-
AccountNode,
|
|
25
|
-
AccountRelationship,
|
|
26
|
-
AccountKind,
|
|
27
|
-
AccountMember,
|
|
28
|
-
} from '../mixins/OxyServices.accounts';
|
|
29
|
-
import { getAccountDisplayName, getAccountFallbackHandle } from '../utils/accountUtils';
|
|
30
|
-
import { getNormalizedUserHandle } from '../utils/userHandle';
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The per-account user shape carried by a {@link SwitchableAccount}. The SDK's
|
|
34
|
-
* canonical {@link User} document — either a profile resolved via
|
|
35
|
-
* `oxyServices.getUsersByIds()` (device rows), the caller-supplied
|
|
36
|
-
* `activeUser` override (the freshest copy of the active row), or the `account`
|
|
37
|
-
* document embedded in an account-graph node (graph-only rows).
|
|
38
|
-
*/
|
|
39
|
-
export type SwitchableAccountUser = User;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* One account the signed-in user can switch INTO, in the uniform switch model.
|
|
43
|
-
*
|
|
44
|
-
* A switchable account is either a device sign-in, an account-graph node (owned
|
|
45
|
-
* org / shared-with-you), or BOTH (an account that has been switched into
|
|
46
|
-
* becomes a device session while still being a graph node — the two are deduped
|
|
47
|
-
* into a single row). Every row carries a canonical `accountId` (the uniform
|
|
48
|
-
* switch key); `sessionId` is present IFF the account is currently signed in on
|
|
49
|
-
* THIS device.
|
|
50
|
-
*/
|
|
51
|
-
export interface SwitchableAccount {
|
|
52
|
-
/**
|
|
53
|
-
* Canonical account id (the underlying `User._id`). The single key EVERY
|
|
54
|
-
* switch uses — `controller.switchTo(accountId)`. Always present.
|
|
55
|
-
*/
|
|
56
|
-
accountId: string;
|
|
57
|
-
/**
|
|
58
|
-
* Device session id, present IFF this account is signed in on THIS device.
|
|
59
|
-
* Absent for a graph account not yet switched into. Used only for
|
|
60
|
-
* device-scoped actions (per-account sign-out); switching ALWAYS goes through
|
|
61
|
-
* `switchTo(accountId)`.
|
|
62
|
-
*/
|
|
63
|
-
sessionId?: string;
|
|
64
|
-
/**
|
|
65
|
-
* Device-local account slot index (0..N-1) carried on the underlying
|
|
66
|
-
* `SessionAccount`. Absent for graph-only rows.
|
|
67
|
-
*/
|
|
68
|
-
authuser?: number;
|
|
69
|
-
/** Whether this account is the currently-active one (`accountId === activeAccountId`). */
|
|
70
|
-
isCurrent: boolean;
|
|
71
|
-
/** Whether this account is signed in on THIS device (has a `sessionId`). */
|
|
72
|
-
onDevice: boolean;
|
|
73
|
-
/**
|
|
74
|
-
* The caller's relationship to this account when it appears in the account
|
|
75
|
-
* graph: `self` (the caller's own personal account), `owner` (an org/project/
|
|
76
|
-
* bot the caller owns), or `member` (shared with the caller). Absent for an
|
|
77
|
-
* independent device sign-in that is NOT in the active account's graph.
|
|
78
|
-
*/
|
|
79
|
-
relationship?: AccountRelationship;
|
|
80
|
-
/** Account classification (personal/organization/…). Cosmetic badge only. */
|
|
81
|
-
kind?: AccountKind;
|
|
82
|
-
/** Parent account id for 2-level tree grouping, or `null` for a root. */
|
|
83
|
-
parentAccountId?: string | null;
|
|
84
|
-
/**
|
|
85
|
-
* The caller's effective membership (role + permissions) in this account when
|
|
86
|
-
* it appears in the graph, or `null`/absent otherwise. Use `permissions` to
|
|
87
|
-
* gate per-account settings UI.
|
|
88
|
-
*/
|
|
89
|
-
callerMembership?: AccountMember | null;
|
|
90
|
-
/** Friendly display name (never blank — falls back to a handle/sentinel). */
|
|
91
|
-
displayName: string;
|
|
92
|
-
/**
|
|
93
|
-
* Real account email, or `null` when the account genuinely has none. NEVER a
|
|
94
|
-
* synthesized `username@oxy.so` — a missing email falls back to the `@handle`
|
|
95
|
-
* secondary line.
|
|
96
|
-
*/
|
|
97
|
-
email: string | null;
|
|
98
|
-
/** Resolved avatar thumbnail URL, or `undefined` when the account has no avatar. */
|
|
99
|
-
avatarUrl?: string;
|
|
100
|
-
/** Account's preferred Bloom color preset, or `null` when unset. */
|
|
101
|
-
color: string | null;
|
|
102
|
-
/** The underlying per-account user payload. */
|
|
103
|
-
user: SwitchableAccountUser;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Whether the caller can BECOME this account — the one question every account
|
|
108
|
-
* switcher asks, answered here so no surface has to re-derive it.
|
|
109
|
-
*
|
|
110
|
-
* Two independent grounds, either of which suffices:
|
|
111
|
-
*
|
|
112
|
-
* - **It is already the caller's own identity** (`relationship: 'self'`).
|
|
113
|
-
* `GET /accounts` resolves its caller through `resolveOperatorId`, so `self`
|
|
114
|
-
* is the HUMAN operator's personal account even while they are operating an
|
|
115
|
-
* org — never the operated account. Kind is irrelevant on this ground: the
|
|
116
|
-
* caller IS that account, so returning to it asks the server for nothing.
|
|
117
|
-
* - **The server will mint a session for it** — `isActAsEligibleKind(kind)` is
|
|
118
|
-
* the exact predicate `POST /accounts/:id/switch` enforces, so a row offered
|
|
119
|
-
* on this ground is never a dead button.
|
|
120
|
-
*
|
|
121
|
-
* `isActAsEligibleKind` ALONE is not this question, and reaching for it
|
|
122
|
-
* directly is the mistake this function exists to prevent: it is false for
|
|
123
|
-
* `personal` as well as `channel`, so a switcher gated on it alone renders an
|
|
124
|
-
* empty list rather than a filtered one. Equally, `kind !== 'channel'` is not
|
|
125
|
-
* this question either — it silently admits every kind invented after it was
|
|
126
|
-
* written, which is the same trap `isActAsEligibleKind` was introduced to close
|
|
127
|
-
* on the server.
|
|
128
|
-
*
|
|
129
|
-
* Takes a structural subset rather than a whole {@link AccountNode} so a caller
|
|
130
|
-
* holding a projected {@link SwitchableAccount} can ask it too.
|
|
131
|
-
*/
|
|
132
|
-
export function isSwitchTargetAccount(
|
|
133
|
-
node: { kind?: AccountKind | null; relationship?: AccountRelationship },
|
|
134
|
-
): boolean {
|
|
135
|
-
return node.relationship === 'self' || isActAsEligibleKind(node.kind);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Whether the caller may switch INTO this account — the server-side
|
|
140
|
-
* `account:act_as` gate plus the structural {@link isSwitchTargetAccount} rule.
|
|
141
|
-
*
|
|
142
|
-
* `relationship: 'self'` always passes (returning to the caller's own personal
|
|
143
|
-
* account). Every other ground requires a switch-eligible kind AND
|
|
144
|
-
* `account:act_as` in the resolved membership permissions. When permissions are
|
|
145
|
-
* absent but the relationship is `owner`, the owner baseline is assumed — the
|
|
146
|
-
* API always resolves effective permissions for owned accounts, but test
|
|
147
|
-
* fixtures and stale rows may omit the membership blob.
|
|
148
|
-
*/
|
|
149
|
-
export function canSwitchIntoAccount(
|
|
150
|
-
node: {
|
|
151
|
-
kind?: AccountKind | null;
|
|
152
|
-
relationship?: AccountRelationship;
|
|
153
|
-
callerMembership?: AccountMember | null;
|
|
154
|
-
},
|
|
155
|
-
): boolean {
|
|
156
|
-
if (node.relationship === 'self') {
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
if (!isSwitchTargetAccount(node)) {
|
|
160
|
-
return false;
|
|
161
|
-
}
|
|
162
|
-
const permissions = node.callerMembership?.permissions;
|
|
163
|
-
if (permissions) {
|
|
164
|
-
return permissions.includes('account:act_as');
|
|
165
|
-
}
|
|
166
|
-
return node.relationship === 'owner';
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Input to {@link projectSwitchableAccounts}. */
|
|
170
|
-
export interface ProjectSwitchableAccountsInput {
|
|
171
|
-
/**
|
|
172
|
-
* The device-scoped session state from `SessionClient.getState()`. `null`
|
|
173
|
-
* (or an empty account set) contributes no device rows.
|
|
174
|
-
*/
|
|
175
|
-
state: DeviceSessionState | null;
|
|
176
|
-
/** The caller's account graph (`oxyServices.listAccounts()`). `[]` when none. */
|
|
177
|
-
graph: AccountNode[];
|
|
178
|
-
/**
|
|
179
|
-
* Per-account profiles resolved via `oxyServices.getUsersByIds()`, keyed by
|
|
180
|
-
* account id (`User.id`). Device accounts whose profile is absent here are
|
|
181
|
-
* omitted until a subsequent fetch resolves them (unless they are the active
|
|
182
|
-
* account and `activeUser` is supplied).
|
|
183
|
-
*/
|
|
184
|
-
profilesById: Map<string, User>;
|
|
185
|
-
/**
|
|
186
|
-
* The freshest copy of the ACTIVE account's user (e.g. `useOxy().user`),
|
|
187
|
-
* preferred over `profilesById` for the active row so a just-committed profile
|
|
188
|
-
* edit is reflected immediately. Optional — the controller relies on
|
|
189
|
-
* `profilesById` alone when omitted.
|
|
190
|
-
*/
|
|
191
|
-
activeUser?: User | null;
|
|
192
|
-
/** Locale for display-name resolution (passed to `getAccountDisplayName`). */
|
|
193
|
-
locale?: string;
|
|
194
|
-
/**
|
|
195
|
-
* Resolves an avatar file id to a thumbnail URL — bind to
|
|
196
|
-
* `(id) => id ? oxyServices.getFileDownloadUrl(id, 'thumb') : undefined`.
|
|
197
|
-
*/
|
|
198
|
-
resolveAvatarUrl: (avatar: string | null | undefined) => string | undefined;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Pure union of device sign-ins and account-graph nodes into the flat
|
|
203
|
-
* {@link SwitchableAccount}[] every switcher renders.
|
|
204
|
-
*
|
|
205
|
-
* Order: device rows first (in `state.accounts` order, active flagged), then
|
|
206
|
-
* graph-only rows (in graph order). An account present as BOTH a device session
|
|
207
|
-
* and a graph node is deduped into ONE device row enriched with the graph
|
|
208
|
-
* metadata (relationship / kind / parent / membership).
|
|
209
|
-
*
|
|
210
|
-
* Graph nodes the caller cannot switch into — a `channel`, or a managed account
|
|
211
|
-
* whose membership lacks `account:act_as` — are omitted.
|
|
212
|
-
* {@link canSwitchIntoAccount} is the rule; see the filter below.
|
|
213
|
-
*/
|
|
214
|
-
export function projectSwitchableAccounts(input: ProjectSwitchableAccountsInput): SwitchableAccount[] {
|
|
215
|
-
const { state, graph, profilesById, activeUser, locale, resolveAvatarUrl } = input;
|
|
216
|
-
const activeAccountId = state?.activeAccountId ?? null;
|
|
217
|
-
|
|
218
|
-
const toRow = (
|
|
219
|
-
accountUser: User,
|
|
220
|
-
opts: {
|
|
221
|
-
sessionId?: string;
|
|
222
|
-
authuser?: number;
|
|
223
|
-
relationship?: AccountRelationship;
|
|
224
|
-
kind?: AccountKind;
|
|
225
|
-
parentAccountId?: string | null;
|
|
226
|
-
callerMembership?: AccountMember | null;
|
|
227
|
-
},
|
|
228
|
-
): SwitchableAccount => {
|
|
229
|
-
const accountId = accountUser.id?.toString() ?? '';
|
|
230
|
-
const handle = getAccountFallbackHandle(accountUser);
|
|
231
|
-
const secondaryHandle = handle ? `@${handle}` : null;
|
|
232
|
-
return {
|
|
233
|
-
accountId,
|
|
234
|
-
sessionId: opts.sessionId,
|
|
235
|
-
authuser: opts.authuser,
|
|
236
|
-
isCurrent: Boolean(accountId) && accountId === activeAccountId,
|
|
237
|
-
onDevice: Boolean(opts.sessionId),
|
|
238
|
-
relationship: opts.relationship,
|
|
239
|
-
kind: opts.kind,
|
|
240
|
-
parentAccountId: opts.parentAccountId,
|
|
241
|
-
callerMembership: opts.callerMembership,
|
|
242
|
-
displayName:
|
|
243
|
-
accountUser.name?.displayName ??
|
|
244
|
-
getNormalizedUserHandle(accountUser) ??
|
|
245
|
-
getAccountDisplayName(null, locale),
|
|
246
|
-
// Real email, or the `@handle` fallback (NEVER synthesized).
|
|
247
|
-
email: accountUser.email ?? secondaryHandle,
|
|
248
|
-
avatarUrl: resolveAvatarUrl(accountUser.avatar),
|
|
249
|
-
color: accountUser.color ?? null,
|
|
250
|
-
user: accountUser,
|
|
251
|
-
};
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
// --- Device rows (from the server-authoritative session set) ---
|
|
255
|
-
const deviceRows = (state?.accounts ?? []).flatMap((account): SwitchableAccount[] => {
|
|
256
|
-
const isActive = account.accountId === activeAccountId;
|
|
257
|
-
// The active row prefers the freshest `activeUser` (when supplied), then the
|
|
258
|
-
// batch-resolved profile; every other row uses the batch-resolved profile.
|
|
259
|
-
const accountUser: User | undefined = isActive && activeUser
|
|
260
|
-
? activeUser
|
|
261
|
-
: profilesById.get(account.accountId);
|
|
262
|
-
if (!accountUser) {
|
|
263
|
-
return [];
|
|
264
|
-
}
|
|
265
|
-
return [toRow(accountUser, { sessionId: account.sessionId, authuser: account.authuser })];
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// --- Merge graph nodes, deduping by account id ---
|
|
269
|
-
const byAccountId = new Map<string, SwitchableAccount>();
|
|
270
|
-
const order: string[] = [];
|
|
271
|
-
const remember = (row: SwitchableAccount): void => {
|
|
272
|
-
if (!row.accountId || byAccountId.has(row.accountId)) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
byAccountId.set(row.accountId, row);
|
|
276
|
-
order.push(row.accountId);
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
for (const row of deviceRows) {
|
|
280
|
-
remember(row);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
for (const node of graph) {
|
|
284
|
-
const existing = byAccountId.get(node.accountId);
|
|
285
|
-
if (existing) {
|
|
286
|
-
// On-device account that is ALSO in the graph: enrich the device row with
|
|
287
|
-
// graph metadata; keep its (freshest) profile + sessionId + active flag.
|
|
288
|
-
byAccountId.set(node.accountId, {
|
|
289
|
-
...existing,
|
|
290
|
-
relationship: node.relationship,
|
|
291
|
-
kind: node.kind,
|
|
292
|
-
parentAccountId: node.parentAccountId,
|
|
293
|
-
callerMembership: node.callerMembership,
|
|
294
|
-
});
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
// Graph-only account (owned org / shared, not yet a device session).
|
|
298
|
-
//
|
|
299
|
-
// This lane is why a no-login account is NOT kept out of the switcher "by
|
|
300
|
-
// construction": the graph contributes accounts that have no device session
|
|
301
|
-
// and no credentials at all, which is exactly how an org first becomes
|
|
302
|
-
// switchable. So a kind that must never be switched into has to be filtered
|
|
303
|
-
// HERE — offering a row the server would 403 is a dead button.
|
|
304
|
-
//
|
|
305
|
-
// An account already on the device skipped this check via the branch above,
|
|
306
|
-
// and correctly: whatever its kind, the caller is signed into it, so
|
|
307
|
-
// switching is a local activation that asks the server for nothing.
|
|
308
|
-
if (!canSwitchIntoAccount(node)) {
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
remember(toRow(node.account, {
|
|
312
|
-
relationship: node.relationship,
|
|
313
|
-
kind: node.kind,
|
|
314
|
-
parentAccountId: node.parentAccountId,
|
|
315
|
-
callerMembership: node.callerMembership,
|
|
316
|
-
}));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return order.flatMap((id) => {
|
|
320
|
-
const row = byAccountId.get(id);
|
|
321
|
-
return row ? [row] : [];
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Every distinct account id referenced by a device session set AND an account
|
|
327
|
-
* graph, sorted for a stable profile-fetch key. Feed to
|
|
328
|
-
* `oxyServices.getUsersByIds(...)`; graph nodes already embed their `account`
|
|
329
|
-
* document, but including their ids lets the caller pass one id set and lets the
|
|
330
|
-
* projection prefer freshly-fetched profiles uniformly.
|
|
331
|
-
*
|
|
332
|
-
* Applies the SAME {@link canSwitchIntoAccount} filter as
|
|
333
|
-
* {@link projectSwitchableAccounts} to graph nodes, so this never fetches a
|
|
334
|
-
* profile for a row the projection will drop — and, just as importantly, never
|
|
335
|
-
* SKIPS one the projection will keep, which would leave that row unrendered
|
|
336
|
-
* until some later fetch happened to resolve it.
|
|
337
|
-
*/
|
|
338
|
-
export function switchableAccountIds(
|
|
339
|
-
state: DeviceSessionState | null,
|
|
340
|
-
graph: AccountNode[],
|
|
341
|
-
): string[] {
|
|
342
|
-
const ids = new Set<string>();
|
|
343
|
-
for (const account of state?.accounts ?? []) {
|
|
344
|
-
if (account.accountId) {
|
|
345
|
-
ids.add(account.accountId);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
for (const node of graph) {
|
|
349
|
-
if (node.accountId && canSwitchIntoAccount(node)) {
|
|
350
|
-
ids.add(node.accountId);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
return Array.from(ids).sort();
|
|
354
|
-
}
|