@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.
Files changed (70) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/HttpService.js +1 -1
  3. package/dist/cjs/index.js +32 -11
  4. package/dist/cjs/mixins/OxyServices.accounts.js +7 -13
  5. package/dist/cjs/mixins/index.js +0 -5
  6. package/dist/cjs/server/index.js +2 -2
  7. package/dist/cjs/session/SessionClient.js +181 -10
  8. package/dist/cjs/session/accountDialogController.js +541 -0
  9. package/dist/cjs/session/accountProjection.js +131 -0
  10. package/dist/cjs/session/createSessionClient.js +9 -2
  11. package/dist/cjs/shared/utils/debugUtils.js +3 -3
  12. package/dist/cjs/utils/authWebUrl.js +10 -30
  13. package/dist/cjs/utils/coldBoot.js +10 -8
  14. package/dist/cjs/utils/{fapiAutoDetect.js → registrableApex.js} +8 -10
  15. package/dist/esm/.tsbuildinfo +1 -1
  16. package/dist/esm/HttpService.js +1 -1
  17. package/dist/esm/index.js +26 -8
  18. package/dist/esm/mixins/OxyServices.accounts.js +7 -13
  19. package/dist/esm/mixins/index.js +0 -5
  20. package/dist/esm/server/index.js +1 -1
  21. package/dist/esm/session/SessionClient.js +181 -10
  22. package/dist/esm/session/accountDialogController.js +536 -0
  23. package/dist/esm/session/accountProjection.js +127 -0
  24. package/dist/esm/session/createSessionClient.js +9 -2
  25. package/dist/esm/shared/utils/debugUtils.js +3 -3
  26. package/dist/esm/utils/authWebUrl.js +9 -29
  27. package/dist/esm/utils/coldBoot.js +10 -8
  28. package/dist/esm/utils/{fapiAutoDetect.js → registrableApex.js} +8 -10
  29. package/dist/types/.tsbuildinfo +1 -1
  30. package/dist/types/HttpService.d.ts +1 -1
  31. package/dist/types/index.d.ts +6 -3
  32. package/dist/types/mixins/OxyServices.accounts.d.ts +24 -27
  33. package/dist/types/mixins/index.d.ts +1 -2
  34. package/dist/types/models/session.d.ts +4 -5
  35. package/dist/types/server/index.d.ts +1 -1
  36. package/dist/types/session/SessionClient.d.ts +52 -1
  37. package/dist/types/session/accountDialogController.d.ts +246 -0
  38. package/dist/types/session/accountProjection.d.ts +142 -0
  39. package/dist/types/session/createSessionClient.d.ts +9 -2
  40. package/dist/types/shared/utils/debugUtils.d.ts +3 -3
  41. package/dist/types/utils/accountUtils.d.ts +2 -3
  42. package/dist/types/utils/authWebUrl.d.ts +9 -29
  43. package/dist/types/utils/coldBoot.d.ts +17 -14
  44. package/dist/types/utils/{fapiAutoDetect.d.ts → registrableApex.d.ts} +8 -10
  45. package/package.json +2 -2
  46. package/src/HttpService.ts +1 -1
  47. package/src/index.ts +43 -11
  48. package/src/mixins/OxyServices.accounts.ts +21 -26
  49. package/src/mixins/index.ts +0 -7
  50. package/src/models/session.ts +4 -5
  51. package/src/server/index.ts +1 -1
  52. package/src/session/SessionClient.ts +202 -12
  53. package/src/session/__tests__/SessionClient.signedOut.test.ts +224 -0
  54. package/src/session/__tests__/accountDialogController.test.ts +469 -0
  55. package/src/session/__tests__/accountProjection.test.ts +181 -0
  56. package/src/session/accountDialogController.ts +682 -0
  57. package/src/session/accountProjection.ts +263 -0
  58. package/src/session/createSessionClient.ts +9 -2
  59. package/src/shared/utils/debugUtils.ts +3 -3
  60. package/src/utils/__tests__/authWebUrl.test.ts +5 -16
  61. package/src/utils/__tests__/{fapiAutoDetect.test.ts → registrableApex.test.ts} +1 -1
  62. package/src/utils/accountUtils.ts +2 -3
  63. package/src/utils/authWebUrl.ts +9 -30
  64. package/src/utils/coldBoot.ts +17 -14
  65. package/src/utils/{fapiAutoDetect.ts → registrableApex.ts} +8 -10
  66. package/dist/cjs/mixins/OxyServices.authorizedApps.js +0 -38
  67. package/dist/esm/mixins/OxyServices.authorizedApps.js +0 -35
  68. package/dist/types/mixins/OxyServices.authorizedApps.d.ts +0 -94
  69. package/src/mixins/OxyServices.authorizedApps.ts +0 -75
  70. 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., '[FedCM]')
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., 'FedCM', 'SilentAuth')
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('FedCM');
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/origin constants.
2
+ * Central IdP apex constant.
3
3
  *
4
- * `resolveCentralAuthUrl` was removed in the device-first cutover (the SDK no
5
- * longer resolves an IdP host). The `CENTRAL_IDP_APEX` / `CENTRAL_AUTH_URL`
6
- * constants survive because the IdP worker imports them to brand assertions.
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 { CENTRAL_AUTH_URL, CENTRAL_IDP_APEX } from '../authWebUrl';
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 '../fapiAutoDetect';
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
- * Mirrors the server's `oxy_rt_${authuser}` cookie slot. Optional so that
18
- * pre-multi-account QuickAccounts (sessionId-only, non-cookie auth on RN)
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
  /**
@@ -1,38 +1,17 @@
1
1
  /**
2
- * Central IdP (auth web) URL resolution for cross-domain SSO.
2
+ * Central IdP apex constant.
3
3
  *
4
- * The Oxy ecosystem runs a single, central Identity Provider at
5
- * `auth.oxy.so`. For TRUE central cross-domain SSO (Google/Meta/Clerk style),
6
- * FedCM and the opaque-code SSO bounce always target this one origin it owns
7
- * the host-only `fedcm_session` cookie and the central session store reachable
8
- * via `api.oxy.so`. Relying Parties (mention.earth, homiio.com, alia.onl, …)
9
- * delegate to it rather than standing up a per-apex IdP.
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. The central IdP is reachable at `auth.${CENTRAL_IDP_APEX}` and the
25
- * ID-token assertion issuer is always `https://auth.${CENTRAL_IDP_APEX}`
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}`;
@@ -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 (silent FedCM, a persisted refresh token, a cross-domain
7
- * claim, a redirect SSO return, ...). They must be attempted in a deterministic
8
- * order*, and the FIRST one that yields a session wins — every later step is
9
- * skipped. This module encodes exactly that contract and nothing else.
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, which is precisely why the FedCM silent-SSO guard had to live
16
- * in consumers rather than a core singleton).
17
- * - Architecture-agnostic: both candidate cross-domain SSO designs consume
18
- * this same primitive; it knows nothing about HOW a step resolves a session.
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. the `/sso` bounce,
108
- * whose `run()` performs its side effect synchronously before its first
109
- * `await`) still gets to fire. A step that has nothing to contribute after
110
- * the deadline simply doesn't settle and is skipped in turn.
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 (the production FedCM-silent hang). When
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 (`autoDetectAuthWebUrl`) was removed in
5
- * the device-first cutover the SDK no longer derives a per-apex `auth.<rp>`
6
- * IdP host. What survives is the pure registrable-domain kernel, still used by
7
- * the api SSO surface, `fedcm.service`, `deviceAuth` (same-apex trust checks),
8
- * and the IdP worker (all lista B / server-side). `@oxyhq/core/server`
9
- * re-exports it for the api.
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
- * LEGACY(old-sdk): `registrableApex` survives ONLY for the lista-B server/IdP
12
- * SSO surface. Deletable once Homiio/Allo/Alia/Syra are bumped off the old SDK
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
- }