@oxyhq/core 6.0.0 → 7.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +1 -1
- package/dist/cjs/index.js +32 -11
- package/dist/cjs/mixins/OxyServices.accounts.js +7 -13
- package/dist/cjs/mixins/index.js +0 -5
- package/dist/cjs/server/index.js +2 -2
- package/dist/cjs/session/SessionClient.js +181 -10
- package/dist/cjs/session/accountDialogController.js +541 -0
- package/dist/cjs/session/accountProjection.js +131 -0
- package/dist/cjs/session/createSessionClient.js +9 -2
- package/dist/cjs/shared/utils/debugUtils.js +3 -3
- package/dist/cjs/utils/authWebUrl.js +10 -30
- package/dist/cjs/utils/coldBoot.js +10 -8
- package/dist/cjs/utils/{fapiAutoDetect.js → registrableApex.js} +8 -10
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +1 -1
- package/dist/esm/index.js +26 -8
- package/dist/esm/mixins/OxyServices.accounts.js +7 -13
- package/dist/esm/mixins/index.js +0 -5
- package/dist/esm/server/index.js +1 -1
- package/dist/esm/session/SessionClient.js +181 -10
- package/dist/esm/session/accountDialogController.js +536 -0
- package/dist/esm/session/accountProjection.js +127 -0
- package/dist/esm/session/createSessionClient.js +9 -2
- package/dist/esm/shared/utils/debugUtils.js +3 -3
- package/dist/esm/utils/authWebUrl.js +9 -29
- package/dist/esm/utils/coldBoot.js +10 -8
- package/dist/esm/utils/{fapiAutoDetect.js → registrableApex.js} +8 -10
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +1 -1
- package/dist/types/index.d.ts +6 -3
- package/dist/types/mixins/OxyServices.accounts.d.ts +24 -27
- package/dist/types/mixins/index.d.ts +1 -2
- package/dist/types/models/session.d.ts +4 -5
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/session/SessionClient.d.ts +52 -1
- package/dist/types/session/accountDialogController.d.ts +246 -0
- package/dist/types/session/accountProjection.d.ts +142 -0
- package/dist/types/session/createSessionClient.d.ts +9 -2
- package/dist/types/shared/utils/debugUtils.d.ts +3 -3
- package/dist/types/utils/accountUtils.d.ts +2 -3
- package/dist/types/utils/authWebUrl.d.ts +9 -29
- package/dist/types/utils/coldBoot.d.ts +17 -14
- package/dist/types/utils/{fapiAutoDetect.d.ts → registrableApex.d.ts} +8 -10
- package/package.json +2 -2
- package/src/HttpService.ts +1 -1
- package/src/index.ts +43 -11
- package/src/mixins/OxyServices.accounts.ts +21 -26
- package/src/mixins/index.ts +0 -7
- package/src/models/session.ts +4 -5
- package/src/server/index.ts +1 -1
- package/src/session/SessionClient.ts +202 -12
- package/src/session/__tests__/SessionClient.signedOut.test.ts +224 -0
- package/src/session/__tests__/accountDialogController.test.ts +469 -0
- package/src/session/__tests__/accountProjection.test.ts +181 -0
- package/src/session/accountDialogController.ts +682 -0
- package/src/session/accountProjection.ts +263 -0
- package/src/session/createSessionClient.ts +9 -2
- package/src/shared/utils/debugUtils.ts +3 -3
- package/src/utils/__tests__/authWebUrl.test.ts +5 -16
- package/src/utils/__tests__/{fapiAutoDetect.test.ts → registrableApex.test.ts} +1 -1
- package/src/utils/accountUtils.ts +2 -3
- package/src/utils/authWebUrl.ts +9 -30
- package/src/utils/coldBoot.ts +17 -14
- package/src/utils/{fapiAutoDetect.ts → registrableApex.ts} +8 -10
- package/dist/cjs/mixins/OxyServices.authorizedApps.js +0 -38
- package/dist/esm/mixins/OxyServices.authorizedApps.js +0 -35
- package/dist/types/mixins/OxyServices.authorizedApps.d.ts +0 -94
- package/src/mixins/OxyServices.authorizedApps.ts +0 -75
- package/src/mixins/__tests__/authorizedApps.test.ts +0 -63
|
@@ -0,0 +1,263 @@
|
|
|
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 `@oxyhq/services` (RN) and `@oxyhq/auth` (web) — 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 type { User } from '../models/interfaces';
|
|
22
|
+
import type {
|
|
23
|
+
AccountNode,
|
|
24
|
+
AccountRelationship,
|
|
25
|
+
AccountKind,
|
|
26
|
+
AccountMember,
|
|
27
|
+
} from '../mixins/OxyServices.accounts';
|
|
28
|
+
import { getAccountDisplayName, getAccountFallbackHandle } from '../utils/accountUtils';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The per-account user shape carried by a {@link SwitchableAccount}. The SDK's
|
|
32
|
+
* canonical {@link User} document — either a profile resolved via
|
|
33
|
+
* `oxyServices.getUsersByIds()` (device rows), the caller-supplied
|
|
34
|
+
* `activeUser` override (the freshest copy of the active row), or the `account`
|
|
35
|
+
* document embedded in an account-graph node (graph-only rows).
|
|
36
|
+
*/
|
|
37
|
+
export type SwitchableAccountUser = User;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One account the signed-in user can switch INTO, in the uniform switch model.
|
|
41
|
+
*
|
|
42
|
+
* A switchable account is either a device sign-in, an account-graph node (owned
|
|
43
|
+
* org / shared-with-you), or BOTH (an account that has been switched into
|
|
44
|
+
* becomes a device session while still being a graph node — the two are deduped
|
|
45
|
+
* into a single row). Every row carries a canonical `accountId` (the uniform
|
|
46
|
+
* switch key); `sessionId` is present IFF the account is currently signed in on
|
|
47
|
+
* THIS device.
|
|
48
|
+
*/
|
|
49
|
+
export interface SwitchableAccount {
|
|
50
|
+
/**
|
|
51
|
+
* Canonical account id (the underlying `User._id`). The single key EVERY
|
|
52
|
+
* switch uses — `controller.switchTo(accountId)`. Always present.
|
|
53
|
+
*/
|
|
54
|
+
accountId: string;
|
|
55
|
+
/**
|
|
56
|
+
* Device session id, present IFF this account is signed in on THIS device.
|
|
57
|
+
* Absent for a graph account not yet switched into. Used only for
|
|
58
|
+
* device-scoped actions (per-account sign-out); switching ALWAYS goes through
|
|
59
|
+
* `switchTo(accountId)`.
|
|
60
|
+
*/
|
|
61
|
+
sessionId?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Device-local account slot index (0..N-1) carried on the underlying
|
|
64
|
+
* `SessionAccount`. Absent for graph-only rows.
|
|
65
|
+
*/
|
|
66
|
+
authuser?: number;
|
|
67
|
+
/** Whether this account is the currently-active one (`accountId === activeAccountId`). */
|
|
68
|
+
isCurrent: boolean;
|
|
69
|
+
/** Whether this account is signed in on THIS device (has a `sessionId`). */
|
|
70
|
+
onDevice: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The caller's relationship to this account when it appears in the account
|
|
73
|
+
* graph: `self` (the caller's own personal account), `owner` (an org/project/
|
|
74
|
+
* bot the caller owns), or `member` (shared with the caller). Absent for an
|
|
75
|
+
* independent device sign-in that is NOT in the active account's graph.
|
|
76
|
+
*/
|
|
77
|
+
relationship?: AccountRelationship;
|
|
78
|
+
/** Account classification (personal/organization/…). Cosmetic badge only. */
|
|
79
|
+
kind?: AccountKind;
|
|
80
|
+
/** Parent account id for 2-level tree grouping, or `null` for a root. */
|
|
81
|
+
parentAccountId?: string | null;
|
|
82
|
+
/**
|
|
83
|
+
* The caller's effective membership (role + permissions) in this account when
|
|
84
|
+
* it appears in the graph, or `null`/absent otherwise. Use `permissions` to
|
|
85
|
+
* gate per-account settings UI.
|
|
86
|
+
*/
|
|
87
|
+
callerMembership?: AccountMember | null;
|
|
88
|
+
/** Friendly display name (never blank — falls back to a handle/sentinel). */
|
|
89
|
+
displayName: string;
|
|
90
|
+
/**
|
|
91
|
+
* Real account email, or `null` when the account genuinely has none. NEVER a
|
|
92
|
+
* synthesized `username@oxy.so` — a missing email falls back to the `@handle`
|
|
93
|
+
* secondary line.
|
|
94
|
+
*/
|
|
95
|
+
email: string | null;
|
|
96
|
+
/** Resolved avatar thumbnail URL, or `undefined` when the account has no avatar. */
|
|
97
|
+
avatarUrl?: string;
|
|
98
|
+
/** Account's preferred Bloom color preset, or `null` when unset. */
|
|
99
|
+
color: string | null;
|
|
100
|
+
/** The underlying per-account user payload. */
|
|
101
|
+
user: SwitchableAccountUser;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Input to {@link projectSwitchableAccounts}. */
|
|
105
|
+
export interface ProjectSwitchableAccountsInput {
|
|
106
|
+
/**
|
|
107
|
+
* The device-scoped session state from `SessionClient.getState()`. `null`
|
|
108
|
+
* (or an empty account set) contributes no device rows.
|
|
109
|
+
*/
|
|
110
|
+
state: DeviceSessionState | null;
|
|
111
|
+
/** The caller's account graph (`oxyServices.listAccounts()`). `[]` when none. */
|
|
112
|
+
graph: AccountNode[];
|
|
113
|
+
/**
|
|
114
|
+
* Per-account profiles resolved via `oxyServices.getUsersByIds()`, keyed by
|
|
115
|
+
* account id (`User.id`). Device accounts whose profile is absent here are
|
|
116
|
+
* omitted until a subsequent fetch resolves them (unless they are the active
|
|
117
|
+
* account and `activeUser` is supplied).
|
|
118
|
+
*/
|
|
119
|
+
profilesById: Map<string, User>;
|
|
120
|
+
/**
|
|
121
|
+
* The freshest copy of the ACTIVE account's user (e.g. `useOxy().user`),
|
|
122
|
+
* preferred over `profilesById` for the active row so a just-committed profile
|
|
123
|
+
* edit is reflected immediately. Optional — the controller relies on
|
|
124
|
+
* `profilesById` alone when omitted.
|
|
125
|
+
*/
|
|
126
|
+
activeUser?: User | null;
|
|
127
|
+
/** Locale for display-name resolution (passed to `getAccountDisplayName`). */
|
|
128
|
+
locale?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Resolves an avatar file id to a thumbnail URL — bind to
|
|
131
|
+
* `(id) => id ? oxyServices.getFileDownloadUrl(id, 'thumb') : undefined`.
|
|
132
|
+
*/
|
|
133
|
+
resolveAvatarUrl: (avatar: string | null | undefined) => string | undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pure union of device sign-ins and account-graph nodes into the flat
|
|
138
|
+
* {@link SwitchableAccount}[] every switcher renders.
|
|
139
|
+
*
|
|
140
|
+
* Order: device rows first (in `state.accounts` order, active flagged), then
|
|
141
|
+
* graph-only rows (in graph order). An account present as BOTH a device session
|
|
142
|
+
* and a graph node is deduped into ONE device row enriched with the graph
|
|
143
|
+
* metadata (relationship / kind / parent / membership).
|
|
144
|
+
*/
|
|
145
|
+
export function projectSwitchableAccounts(input: ProjectSwitchableAccountsInput): SwitchableAccount[] {
|
|
146
|
+
const { state, graph, profilesById, activeUser, locale, resolveAvatarUrl } = input;
|
|
147
|
+
const activeAccountId = state?.activeAccountId ?? null;
|
|
148
|
+
|
|
149
|
+
const toRow = (
|
|
150
|
+
accountUser: User,
|
|
151
|
+
opts: {
|
|
152
|
+
sessionId?: string;
|
|
153
|
+
authuser?: number;
|
|
154
|
+
relationship?: AccountRelationship;
|
|
155
|
+
kind?: AccountKind;
|
|
156
|
+
parentAccountId?: string | null;
|
|
157
|
+
callerMembership?: AccountMember | null;
|
|
158
|
+
},
|
|
159
|
+
): SwitchableAccount => {
|
|
160
|
+
const accountId = accountUser.id?.toString() ?? '';
|
|
161
|
+
const handle = getAccountFallbackHandle(accountUser);
|
|
162
|
+
const secondaryHandle = handle ? `@${handle}` : null;
|
|
163
|
+
return {
|
|
164
|
+
accountId,
|
|
165
|
+
sessionId: opts.sessionId,
|
|
166
|
+
authuser: opts.authuser,
|
|
167
|
+
isCurrent: Boolean(accountId) && accountId === activeAccountId,
|
|
168
|
+
onDevice: Boolean(opts.sessionId),
|
|
169
|
+
relationship: opts.relationship,
|
|
170
|
+
kind: opts.kind,
|
|
171
|
+
parentAccountId: opts.parentAccountId,
|
|
172
|
+
callerMembership: opts.callerMembership,
|
|
173
|
+
displayName: getAccountDisplayName(accountUser, locale),
|
|
174
|
+
// Real email, or the `@handle` fallback (NEVER synthesized).
|
|
175
|
+
email: accountUser.email ?? secondaryHandle,
|
|
176
|
+
avatarUrl: resolveAvatarUrl(accountUser.avatar),
|
|
177
|
+
color: accountUser.color ?? null,
|
|
178
|
+
user: accountUser,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// --- Device rows (from the server-authoritative session set) ---
|
|
183
|
+
const deviceRows = (state?.accounts ?? []).flatMap((account): SwitchableAccount[] => {
|
|
184
|
+
const isActive = account.accountId === activeAccountId;
|
|
185
|
+
// The active row prefers the freshest `activeUser` (when supplied), then the
|
|
186
|
+
// batch-resolved profile; every other row uses the batch-resolved profile.
|
|
187
|
+
const accountUser: User | undefined = isActive && activeUser
|
|
188
|
+
? activeUser
|
|
189
|
+
: profilesById.get(account.accountId);
|
|
190
|
+
if (!accountUser) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
return [toRow(accountUser, { sessionId: account.sessionId, authuser: account.authuser })];
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// --- Merge graph nodes, deduping by account id ---
|
|
197
|
+
const byAccountId = new Map<string, SwitchableAccount>();
|
|
198
|
+
const order: string[] = [];
|
|
199
|
+
const remember = (row: SwitchableAccount): void => {
|
|
200
|
+
if (!row.accountId || byAccountId.has(row.accountId)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
byAccountId.set(row.accountId, row);
|
|
204
|
+
order.push(row.accountId);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
for (const row of deviceRows) {
|
|
208
|
+
remember(row);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const node of graph) {
|
|
212
|
+
const existing = byAccountId.get(node.accountId);
|
|
213
|
+
if (existing) {
|
|
214
|
+
// On-device account that is ALSO in the graph: enrich the device row with
|
|
215
|
+
// graph metadata; keep its (freshest) profile + sessionId + active flag.
|
|
216
|
+
byAccountId.set(node.accountId, {
|
|
217
|
+
...existing,
|
|
218
|
+
relationship: node.relationship,
|
|
219
|
+
kind: node.kind,
|
|
220
|
+
parentAccountId: node.parentAccountId,
|
|
221
|
+
callerMembership: node.callerMembership,
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
// Graph-only account (owned org / shared, not yet a device session).
|
|
226
|
+
remember(toRow(node.account, {
|
|
227
|
+
relationship: node.relationship,
|
|
228
|
+
kind: node.kind,
|
|
229
|
+
parentAccountId: node.parentAccountId,
|
|
230
|
+
callerMembership: node.callerMembership,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return order.flatMap((id) => {
|
|
235
|
+
const row = byAccountId.get(id);
|
|
236
|
+
return row ? [row] : [];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Every distinct account id referenced by a device session set AND an account
|
|
242
|
+
* graph, sorted for a stable profile-fetch key. Feed to
|
|
243
|
+
* `oxyServices.getUsersByIds(...)`; graph nodes already embed their `account`
|
|
244
|
+
* document, but including their ids lets the caller pass one id set and lets the
|
|
245
|
+
* projection prefer freshly-fetched profiles uniformly.
|
|
246
|
+
*/
|
|
247
|
+
export function switchableAccountIds(
|
|
248
|
+
state: DeviceSessionState | null,
|
|
249
|
+
graph: AccountNode[],
|
|
250
|
+
): string[] {
|
|
251
|
+
const ids = new Set<string>();
|
|
252
|
+
for (const account of state?.accounts ?? []) {
|
|
253
|
+
if (account.accountId) {
|
|
254
|
+
ids.add(account.accountId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const node of graph) {
|
|
258
|
+
if (node.accountId) {
|
|
259
|
+
ids.add(node.accountId);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return Array.from(ids).sort();
|
|
263
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OxyServices } from '../OxyServices';
|
|
2
|
-
import { SessionClient, type TokenTransport } from './SessionClient';
|
|
2
|
+
import { SessionClient, type SessionClientOptions, type TokenTransport } from './SessionClient';
|
|
3
3
|
import type { SocketIOFactory } from './socketLoader';
|
|
4
4
|
import { createSessionClientHost } from './sessionClientHost';
|
|
5
5
|
|
|
@@ -29,11 +29,18 @@ export function createSessionClient(
|
|
|
29
29
|
oxyServices: OxyServices,
|
|
30
30
|
transport: TokenTransport,
|
|
31
31
|
socketFactory?: SocketIOFactory,
|
|
32
|
+
/**
|
|
33
|
+
* Optional signed-out realtime wiring: `signedOutSocketAuth` (open the socket
|
|
34
|
+
* while signed out so an idle tab receives its device pushes) and
|
|
35
|
+
* `onSessionAppeared` (self-acquire when a sibling signs in). See
|
|
36
|
+
* {@link SessionClientOptions}.
|
|
37
|
+
*/
|
|
38
|
+
extra?: Pick<SessionClientOptions, 'signedOutSocketAuth' | 'onSessionAppeared'>,
|
|
32
39
|
): {
|
|
33
40
|
client: SessionClient;
|
|
34
41
|
host: ReturnType<typeof createSessionClientHost>;
|
|
35
42
|
} {
|
|
36
43
|
const host = createSessionClientHost(oxyServices);
|
|
37
|
-
const client = new SessionClient(host, { transport, socketFactory });
|
|
44
|
+
const client = new SessionClient(host, { transport, socketFactory, ...extra });
|
|
38
45
|
return { client, host };
|
|
39
46
|
}
|
|
@@ -24,7 +24,7 @@ export const isDev = (): boolean => {
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Log a debug message (only in development)
|
|
27
|
-
* @param prefix - Log prefix (e.g., '[
|
|
27
|
+
* @param prefix - Log prefix (e.g., '[ColdBoot]')
|
|
28
28
|
* @param args - Arguments to log
|
|
29
29
|
*/
|
|
30
30
|
export const debugLog = (prefix: string, ...args: unknown[]): void => {
|
|
@@ -57,12 +57,12 @@ export const debugError = (prefix: string, ...args: unknown[]): void => {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Create a namespaced debug logger
|
|
60
|
-
* @param namespace - Logger namespace (e.g., '
|
|
60
|
+
* @param namespace - Logger namespace (e.g., 'ColdBoot', 'DeviceAuth')
|
|
61
61
|
* @returns Object with log, warn, error methods
|
|
62
62
|
*
|
|
63
63
|
* @example
|
|
64
64
|
* ```ts
|
|
65
|
-
* const debug = createDebugLogger('
|
|
65
|
+
* const debug = createDebugLogger('ColdBoot');
|
|
66
66
|
* debug.log('Starting authentication');
|
|
67
67
|
* debug.warn('Token expires soon');
|
|
68
68
|
* debug.error('Authentication failed', error);
|
|
@@ -1,26 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Central IdP apex
|
|
2
|
+
* Central IdP apex constant.
|
|
3
3
|
*
|
|
4
|
-
* `resolveCentralAuthUrl`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `resolveCentralAuthUrl` + `CENTRAL_AUTH_URL` were removed in the device-first /
|
|
5
|
+
* legacy-final cutovers. `CENTRAL_IDP_APEX` survives because live server-side
|
|
6
|
+
* callers (the `@oxyhq/core/server` CORS helper + the IdP worker) import it.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { CENTRAL_IDP_APEX } from '../authWebUrl';
|
|
10
10
|
|
|
11
11
|
describe('CENTRAL_IDP_APEX', () => {
|
|
12
12
|
it('is the central IdP registrable apex', () => {
|
|
13
13
|
expect(CENTRAL_IDP_APEX).toBe('oxy.so');
|
|
14
14
|
});
|
|
15
15
|
});
|
|
16
|
-
|
|
17
|
-
describe('CENTRAL_AUTH_URL', () => {
|
|
18
|
-
it('is the central IdP origin with no trailing slash', () => {
|
|
19
|
-
expect(CENTRAL_AUTH_URL).toBe('https://auth.oxy.so');
|
|
20
|
-
expect(CENTRAL_AUTH_URL.endsWith('/')).toBe(false);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('is derived from CENTRAL_IDP_APEX (apex and origin never drift)', () => {
|
|
24
|
-
expect(CENTRAL_AUTH_URL).toBe(`https://auth.${CENTRAL_IDP_APEX}`);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* surface + the IdP). These cases lock its Public-Suffix-List behaviour.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { registrableApex } from '../
|
|
9
|
+
import { registrableApex } from '../registrableApex';
|
|
10
10
|
|
|
11
11
|
describe('registrableApex', () => {
|
|
12
12
|
it('returns the eTLD+1 for a normal two-label host', () => {
|
|
@@ -14,9 +14,8 @@ export interface QuickAccount {
|
|
|
14
14
|
avatarUrl?: string;
|
|
15
15
|
/**
|
|
16
16
|
* Device-local account slot index, 0..N-1 (Google-style multi-account).
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* remain valid; web flows always populate it after `refreshAllSessions`.
|
|
17
|
+
* Optional so that pre-multi-account QuickAccounts (sessionId-only) remain
|
|
18
|
+
* valid; the device session set populates it where available.
|
|
20
19
|
*/
|
|
21
20
|
authuser?: number;
|
|
22
21
|
/**
|
package/src/utils/authWebUrl.ts
CHANGED
|
@@ -1,38 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Central IdP
|
|
2
|
+
* Central IdP apex constant.
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* This module is intentionally pure: it performs no DOM access, reads no
|
|
12
|
-
* `window`/`location`, and has no side effects. It is the single source of
|
|
13
|
-
* truth for the central IdP origin so call sites never hardcode the literal.
|
|
14
|
-
*
|
|
15
|
-
* LEGACY(old-sdk): the client resolver (`resolveCentralAuthUrl`) was removed in
|
|
16
|
-
* the device-first cutover; `CENTRAL_IDP_APEX` / `CENTRAL_AUTH_URL` survive ONLY
|
|
17
|
-
* because the lista-B IdP worker imports them to brand assertions. Deletable
|
|
18
|
-
* once Homiio/Allo/Alia/Syra are bumped off the old SDK AND CloudWatch
|
|
19
|
-
* `/oxy/ecs` shows the `/sso*` + `/fedcm/*` routes quiet — F-final sweep.
|
|
4
|
+
* The client SSO/FedCM resolvers (`resolveCentralAuthUrl`, `CENTRAL_AUTH_URL`)
|
|
5
|
+
* were removed in the device-first / legacy-final cutovers. The lone survivor is
|
|
6
|
+
* `CENTRAL_IDP_APEX`, kept because it has a LIVE consumer —
|
|
7
|
+
* `@oxyhq/core/server`'s CORS helper (`server/cors.ts`'s `createOxyCors`)
|
|
8
|
+
* auto-allows `*.oxy.so` from it. That CORS use is permanent, so this stays
|
|
9
|
+
* past the SSO/FedCM teardown.
|
|
20
10
|
*/
|
|
21
11
|
|
|
22
12
|
/**
|
|
23
13
|
* The registrable apex (eTLD+1) of the Oxy ecosystem's central Identity
|
|
24
|
-
* Provider
|
|
25
|
-
*
|
|
26
|
-
* regardless of which per-apex `auth.<rp>` host served a given request.
|
|
27
|
-
*
|
|
28
|
-
* Kept as a standalone constant so the IdP worker and the SDK derive the same
|
|
29
|
-
* literal from one source of truth (the worker imports it to brand assertions).
|
|
14
|
+
* Provider, reachable at `auth.${CENTRAL_IDP_APEX}`. Single source of truth so
|
|
15
|
+
* the CORS helper (and anything else that needs the central apex) never drifts.
|
|
30
16
|
*/
|
|
31
17
|
export const CENTRAL_IDP_APEX = 'oxy.so';
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* The canonical central Identity Provider origin for the Oxy ecosystem.
|
|
35
|
-
* No trailing slash. Derived from {@link CENTRAL_IDP_APEX} so the apex and the
|
|
36
|
-
* full origin never drift apart. The IdP worker imports it to brand assertions.
|
|
37
|
-
*/
|
|
38
|
-
export const CENTRAL_AUTH_URL = `https://auth.${CENTRAL_IDP_APEX}`;
|
package/src/utils/coldBoot.ts
CHANGED
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
* authentication resolution.
|
|
4
4
|
*
|
|
5
5
|
* On a fresh page load / app launch the SDK may have several ways to recover an
|
|
6
|
-
* existing session (
|
|
7
|
-
*
|
|
8
|
-
* order
|
|
9
|
-
* skipped. This module encodes exactly that contract and
|
|
6
|
+
* existing session (a persisted refresh-token family, a shared-keychain
|
|
7
|
+
* identity, a cross-domain boot-fragment return, ...). They must be attempted
|
|
8
|
+
* in a deterministic order, and the FIRST one that yields a session wins —
|
|
9
|
+
* every later step is skipped. This module encodes exactly that contract and
|
|
10
|
+
* nothing else.
|
|
10
11
|
*
|
|
11
12
|
* Design constraints (all enforced):
|
|
12
13
|
* - PURE: no DOM, no `navigator`, no `window`, no React, no platform globals.
|
|
13
14
|
* - NO module-level mutable state. Every call to {@link runColdBoot} is fully
|
|
14
15
|
* self-contained, so it is safe under bundler re-evaluation (e.g. the Metro
|
|
15
|
-
* web bundle
|
|
16
|
-
*
|
|
17
|
-
* - Architecture-agnostic:
|
|
18
|
-
*
|
|
16
|
+
* web bundle — the reason any run-once guard for a step must live in the
|
|
17
|
+
* calling consumer, never in a core module-level singleton).
|
|
18
|
+
* - Architecture-agnostic: it knows nothing about HOW a step resolves a
|
|
19
|
+
* session; `runSessionColdBoot` (`boot/coldBootV2.ts`) is the current
|
|
20
|
+
* device-first consumer.
|
|
19
21
|
*
|
|
20
22
|
* A step is skipped (without running) when its `enabled` predicate returns
|
|
21
23
|
* false. Any thrown error — from either `enabled` or `run` — is reported via
|
|
@@ -104,15 +106,16 @@ export interface RunColdBootOptions<S> {
|
|
|
104
106
|
* fails to settle before the deadline, the runner abandons the await for that
|
|
105
107
|
* step (reporting it via `onStepDeadline`) and CONTINUES to the next step,
|
|
106
108
|
* each now racing against an already-expired deadline. This is deliberate:
|
|
107
|
-
* the runner keeps iterating so the TERMINAL step (e.g.
|
|
108
|
-
* whose `run()` performs its side effect
|
|
109
|
-
* `await`) still gets to fire. A step that
|
|
110
|
-
* the deadline simply doesn't settle and is
|
|
109
|
+
* the runner keeps iterating so the TERMINAL step (e.g. `coldBootV2`'s
|
|
110
|
+
* `bootstrap-hop`, whose `run()` performs its navigation side effect
|
|
111
|
+
* synchronously before its first `await`) still gets to fire. A step that
|
|
112
|
+
* has nothing to contribute after the deadline simply doesn't settle and is
|
|
113
|
+
* skipped in turn.
|
|
111
114
|
*
|
|
112
115
|
* Per-step timeouts inside `run()` remain the first line of defense and
|
|
113
116
|
* should keep every step well under this budget on a healthy load; this only
|
|
114
|
-
* trips when one of them regresses (
|
|
115
|
-
* omitted there is no overall deadline.
|
|
117
|
+
* trips when one of them regresses (a step hanging past its own timeout).
|
|
118
|
+
* When omitted there is no overall deadline.
|
|
116
119
|
*/
|
|
117
120
|
readonly overallDeadlineMs?: number;
|
|
118
121
|
/**
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Registrable-apex (eTLD+1) host kernel.
|
|
3
3
|
*
|
|
4
|
-
* The client FAPI auto-detection helper
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* The client FAPI auto-detection helper was removed in the device-first cutover
|
|
5
|
+
* (which is why this file is now named for what it actually is, not the old
|
|
6
|
+
* `fapiAutoDetect`). What survives is the pure registrable-domain kernel, still
|
|
7
|
+
* used server-side by the api's device-first same-apex trust checks
|
|
8
|
+
* (`deviceAuth.ts`'s `POST /auth/device/web-session`, via `sameSite.ts`'s
|
|
9
|
+
* `isSameSiteTrustedRequest`) and the `@oxyhq/core/server` CORS/re-export layer.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* AND CloudWatch `/oxy/ecs` shows the `/sso*` + `/fedcm/*` routes quiet — the
|
|
14
|
-
* F-final sweep should remove this file then.
|
|
11
|
+
* `registrableApex` is NOT legacy — the device-first same-apex check is a live
|
|
12
|
+
* consumer, so this kernel stays regardless of the SSO/FedCM removal.
|
|
15
13
|
*/
|
|
16
14
|
|
|
17
15
|
import { getDomain } from 'tldts';
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OxyServicesAuthorizedAppsMixin = OxyServicesAuthorizedAppsMixin;
|
|
4
|
-
function OxyServicesAuthorizedAppsMixin(Base) {
|
|
5
|
-
return class extends Base {
|
|
6
|
-
/**
|
|
7
|
-
* List the authenticated user's authorized RP apps — the intersection of the
|
|
8
|
-
* user's FedCM grants and the currently-approved RP catalog. Powers the
|
|
9
|
-
* "Connected apps" management UI. Requires a real user session.
|
|
10
|
-
*/
|
|
11
|
-
async listAuthorizedApps() {
|
|
12
|
-
try {
|
|
13
|
-
const response = await this.makeRequest('GET', '/fedcm/me/authorized-apps', undefined, {
|
|
14
|
-
cache: true,
|
|
15
|
-
cacheTTL: 30 * 1000, // 30 second cache — short, this drives a manageable UI
|
|
16
|
-
});
|
|
17
|
-
return response?.apps ?? [];
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
throw this.handleError(error);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Revoke the authenticated user's authorization for a specific RP origin.
|
|
25
|
-
* The corresponding cache entry is invalidated so a subsequent
|
|
26
|
-
* `listAuthorizedApps()` sees fresh data.
|
|
27
|
-
*/
|
|
28
|
-
async revokeAuthorizedApp(origin) {
|
|
29
|
-
try {
|
|
30
|
-
await this.makeRequest('DELETE', `/fedcm/me/authorized-apps/${encodeURIComponent(origin)}`, undefined, { cache: false });
|
|
31
|
-
this.clearCacheEntry('GET:/fedcm/me/authorized-apps');
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
throw this.handleError(error);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export function OxyServicesAuthorizedAppsMixin(Base) {
|
|
2
|
-
return class extends Base {
|
|
3
|
-
/**
|
|
4
|
-
* List the authenticated user's authorized RP apps — the intersection of the
|
|
5
|
-
* user's FedCM grants and the currently-approved RP catalog. Powers the
|
|
6
|
-
* "Connected apps" management UI. Requires a real user session.
|
|
7
|
-
*/
|
|
8
|
-
async listAuthorizedApps() {
|
|
9
|
-
try {
|
|
10
|
-
const response = await this.makeRequest('GET', '/fedcm/me/authorized-apps', undefined, {
|
|
11
|
-
cache: true,
|
|
12
|
-
cacheTTL: 30 * 1000, // 30 second cache — short, this drives a manageable UI
|
|
13
|
-
});
|
|
14
|
-
return response?.apps ?? [];
|
|
15
|
-
}
|
|
16
|
-
catch (error) {
|
|
17
|
-
throw this.handleError(error);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Revoke the authenticated user's authorization for a specific RP origin.
|
|
22
|
-
* The corresponding cache entry is invalidated so a subsequent
|
|
23
|
-
* `listAuthorizedApps()` sees fresh data.
|
|
24
|
-
*/
|
|
25
|
-
async revokeAuthorizedApp(origin) {
|
|
26
|
-
try {
|
|
27
|
-
await this.makeRequest('DELETE', `/fedcm/me/authorized-apps/${encodeURIComponent(origin)}`, undefined, { cache: false });
|
|
28
|
-
this.clearCacheEntry('GET:/fedcm/me/authorized-apps');
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
throw this.handleError(error);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
}
|