@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,142 @@
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
+ import type { DeviceSessionState } from '@oxyhq/contracts';
20
+ import type { User } from '../models/interfaces';
21
+ import type { AccountNode, AccountRelationship, AccountKind, AccountMember } from '../mixins/OxyServices.accounts';
22
+ /**
23
+ * The per-account user shape carried by a {@link SwitchableAccount}. The SDK's
24
+ * canonical {@link User} document — either a profile resolved via
25
+ * `oxyServices.getUsersByIds()` (device rows), the caller-supplied
26
+ * `activeUser` override (the freshest copy of the active row), or the `account`
27
+ * document embedded in an account-graph node (graph-only rows).
28
+ */
29
+ export type SwitchableAccountUser = User;
30
+ /**
31
+ * One account the signed-in user can switch INTO, in the uniform switch model.
32
+ *
33
+ * A switchable account is either a device sign-in, an account-graph node (owned
34
+ * org / shared-with-you), or BOTH (an account that has been switched into
35
+ * becomes a device session while still being a graph node — the two are deduped
36
+ * into a single row). Every row carries a canonical `accountId` (the uniform
37
+ * switch key); `sessionId` is present IFF the account is currently signed in on
38
+ * THIS device.
39
+ */
40
+ export interface SwitchableAccount {
41
+ /**
42
+ * Canonical account id (the underlying `User._id`). The single key EVERY
43
+ * switch uses — `controller.switchTo(accountId)`. Always present.
44
+ */
45
+ accountId: string;
46
+ /**
47
+ * Device session id, present IFF this account is signed in on THIS device.
48
+ * Absent for a graph account not yet switched into. Used only for
49
+ * device-scoped actions (per-account sign-out); switching ALWAYS goes through
50
+ * `switchTo(accountId)`.
51
+ */
52
+ sessionId?: string;
53
+ /**
54
+ * Device-local account slot index (0..N-1) carried on the underlying
55
+ * `SessionAccount`. Absent for graph-only rows.
56
+ */
57
+ authuser?: number;
58
+ /** Whether this account is the currently-active one (`accountId === activeAccountId`). */
59
+ isCurrent: boolean;
60
+ /** Whether this account is signed in on THIS device (has a `sessionId`). */
61
+ onDevice: boolean;
62
+ /**
63
+ * The caller's relationship to this account when it appears in the account
64
+ * graph: `self` (the caller's own personal account), `owner` (an org/project/
65
+ * bot the caller owns), or `member` (shared with the caller). Absent for an
66
+ * independent device sign-in that is NOT in the active account's graph.
67
+ */
68
+ relationship?: AccountRelationship;
69
+ /** Account classification (personal/organization/…). Cosmetic badge only. */
70
+ kind?: AccountKind;
71
+ /** Parent account id for 2-level tree grouping, or `null` for a root. */
72
+ parentAccountId?: string | null;
73
+ /**
74
+ * The caller's effective membership (role + permissions) in this account when
75
+ * it appears in the graph, or `null`/absent otherwise. Use `permissions` to
76
+ * gate per-account settings UI.
77
+ */
78
+ callerMembership?: AccountMember | null;
79
+ /** Friendly display name (never blank — falls back to a handle/sentinel). */
80
+ displayName: string;
81
+ /**
82
+ * Real account email, or `null` when the account genuinely has none. NEVER a
83
+ * synthesized `username@oxy.so` — a missing email falls back to the `@handle`
84
+ * secondary line.
85
+ */
86
+ email: string | null;
87
+ /** Resolved avatar thumbnail URL, or `undefined` when the account has no avatar. */
88
+ avatarUrl?: string;
89
+ /** Account's preferred Bloom color preset, or `null` when unset. */
90
+ color: string | null;
91
+ /** The underlying per-account user payload. */
92
+ user: SwitchableAccountUser;
93
+ }
94
+ /** Input to {@link projectSwitchableAccounts}. */
95
+ export interface ProjectSwitchableAccountsInput {
96
+ /**
97
+ * The device-scoped session state from `SessionClient.getState()`. `null`
98
+ * (or an empty account set) contributes no device rows.
99
+ */
100
+ state: DeviceSessionState | null;
101
+ /** The caller's account graph (`oxyServices.listAccounts()`). `[]` when none. */
102
+ graph: AccountNode[];
103
+ /**
104
+ * Per-account profiles resolved via `oxyServices.getUsersByIds()`, keyed by
105
+ * account id (`User.id`). Device accounts whose profile is absent here are
106
+ * omitted until a subsequent fetch resolves them (unless they are the active
107
+ * account and `activeUser` is supplied).
108
+ */
109
+ profilesById: Map<string, User>;
110
+ /**
111
+ * The freshest copy of the ACTIVE account's user (e.g. `useOxy().user`),
112
+ * preferred over `profilesById` for the active row so a just-committed profile
113
+ * edit is reflected immediately. Optional — the controller relies on
114
+ * `profilesById` alone when omitted.
115
+ */
116
+ activeUser?: User | null;
117
+ /** Locale for display-name resolution (passed to `getAccountDisplayName`). */
118
+ locale?: string;
119
+ /**
120
+ * Resolves an avatar file id to a thumbnail URL — bind to
121
+ * `(id) => id ? oxyServices.getFileDownloadUrl(id, 'thumb') : undefined`.
122
+ */
123
+ resolveAvatarUrl: (avatar: string | null | undefined) => string | undefined;
124
+ }
125
+ /**
126
+ * Pure union of device sign-ins and account-graph nodes into the flat
127
+ * {@link SwitchableAccount}[] every switcher renders.
128
+ *
129
+ * Order: device rows first (in `state.accounts` order, active flagged), then
130
+ * graph-only rows (in graph order). An account present as BOTH a device session
131
+ * and a graph node is deduped into ONE device row enriched with the graph
132
+ * metadata (relationship / kind / parent / membership).
133
+ */
134
+ export declare function projectSwitchableAccounts(input: ProjectSwitchableAccountsInput): SwitchableAccount[];
135
+ /**
136
+ * Every distinct account id referenced by a device session set AND an account
137
+ * graph, sorted for a stable profile-fetch key. Feed to
138
+ * `oxyServices.getUsersByIds(...)`; graph nodes already embed their `account`
139
+ * document, but including their ids lets the caller pass one id set and lets the
140
+ * projection prefer freshly-fetched profiles uniformly.
141
+ */
142
+ export declare function switchableAccountIds(state: DeviceSessionState | null, graph: AccountNode[]): string[];
@@ -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
  /**
@@ -24,7 +24,14 @@ import { createSessionClientHost } from './sessionClientHost';
24
24
  * specifier (bundler-fragile in Metro/Expo-web and Vite against the published
25
25
  * dist). When omitted, the client falls back to the lazy loader.
26
26
  */
27
- export declare function createSessionClient(oxyServices: OxyServices, transport: TokenTransport, socketFactory?: SocketIOFactory): {
27
+ export declare function createSessionClient(oxyServices: OxyServices, transport: TokenTransport, socketFactory?: SocketIOFactory,
28
+ /**
29
+ * Optional signed-out realtime wiring: `signedOutSocketAuth` (open the socket
30
+ * while signed out so an idle tab receives its device pushes) and
31
+ * `onSessionAppeared` (self-acquire when a sibling signs in). See
32
+ * {@link SessionClientOptions}.
33
+ */
34
+ extra?: Pick<SessionClientOptions, 'signedOutSocketAuth' | 'onSessionAppeared'>): {
28
35
  client: SessionClient;
29
36
  host: ReturnType<typeof createSessionClientHost>;
30
37
  };
@@ -12,7 +12,7 @@
12
12
  export declare const isDev: () => boolean;
13
13
  /**
14
14
  * Log a debug message (only in development)
15
- * @param prefix - Log prefix (e.g., '[FedCM]')
15
+ * @param prefix - Log prefix (e.g., '[ColdBoot]')
16
16
  * @param args - Arguments to log
17
17
  */
18
18
  export declare const debugLog: (prefix: string, ...args: unknown[]) => void;
@@ -30,12 +30,12 @@ export declare const debugWarn: (prefix: string, ...args: unknown[]) => void;
30
30
  export declare const debugError: (prefix: string, ...args: unknown[]) => void;
31
31
  /**
32
32
  * Create a namespaced debug logger
33
- * @param namespace - Logger namespace (e.g., 'FedCM', 'SilentAuth')
33
+ * @param namespace - Logger namespace (e.g., 'ColdBoot', 'DeviceAuth')
34
34
  * @returns Object with log, warn, error methods
35
35
  *
36
36
  * @example
37
37
  * ```ts
38
- * const debug = createDebugLogger('FedCM');
38
+ * const debug = createDebugLogger('ColdBoot');
39
39
  * debug.log('Starting authentication');
40
40
  * debug.warn('Token expires soon');
41
41
  * debug.error('Authentication failed', error);
@@ -11,9 +11,8 @@ export interface QuickAccount {
11
11
  avatarUrl?: string;
12
12
  /**
13
13
  * Device-local account slot index, 0..N-1 (Google-style multi-account).
14
- * Mirrors the server's `oxy_rt_${authuser}` cookie slot. Optional so that
15
- * pre-multi-account QuickAccounts (sessionId-only, non-cookie auth on RN)
16
- * remain valid; web flows always populate it after `refreshAllSessions`.
14
+ * Optional so that pre-multi-account QuickAccounts (sessionId-only) remain
15
+ * valid; the device session set populates it where available.
17
16
  */
18
17
  authuser?: number;
19
18
  /**
@@ -1,36 +1,16 @@
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
  * The registrable apex (eTLD+1) of the Oxy ecosystem's central Identity
23
- * Provider. The central IdP is reachable at `auth.${CENTRAL_IDP_APEX}` and the
24
- * ID-token assertion issuer is always `https://auth.${CENTRAL_IDP_APEX}`
25
- * regardless of which per-apex `auth.<rp>` host served a given request.
26
- *
27
- * Kept as a standalone constant so the IdP worker and the SDK derive the same
28
- * literal from one source of truth (the worker imports it to brand assertions).
13
+ * Provider, reachable at `auth.${CENTRAL_IDP_APEX}`. Single source of truth so
14
+ * the CORS helper (and anything else that needs the central apex) never drifts.
29
15
  */
30
16
  export declare const CENTRAL_IDP_APEX = "oxy.so";
31
- /**
32
- * The canonical central Identity Provider origin for the Oxy ecosystem.
33
- * No trailing slash. Derived from {@link CENTRAL_IDP_APEX} so the apex and the
34
- * full origin never drift apart. The IdP worker imports it to brand assertions.
35
- */
36
- export declare const CENTRAL_AUTH_URL = "https://auth.oxy.so";
@@ -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
@@ -92,15 +94,16 @@ export interface RunColdBootOptions<S> {
92
94
  * fails to settle before the deadline, the runner abandons the await for that
93
95
  * step (reporting it via `onStepDeadline`) and CONTINUES to the next step,
94
96
  * each now racing against an already-expired deadline. This is deliberate:
95
- * the runner keeps iterating so the TERMINAL step (e.g. the `/sso` bounce,
96
- * whose `run()` performs its side effect synchronously before its first
97
- * `await`) still gets to fire. A step that has nothing to contribute after
98
- * the deadline simply doesn't settle and is skipped in turn.
97
+ * the runner keeps iterating so the TERMINAL step (e.g. `coldBootV2`'s
98
+ * `bootstrap-hop`, whose `run()` performs its navigation side effect
99
+ * synchronously before its first `await`) still gets to fire. A step that
100
+ * has nothing to contribute after the deadline simply doesn't settle and is
101
+ * skipped in turn.
99
102
  *
100
103
  * Per-step timeouts inside `run()` remain the first line of defense and
101
104
  * should keep every step well under this budget on a healthy load; this only
102
- * trips when one of them regresses (the production FedCM-silent hang). When
103
- * omitted there is no overall deadline.
105
+ * trips when one of them regresses (a step hanging past its own timeout).
106
+ * When omitted there is no overall deadline.
104
107
  */
105
108
  readonly overallDeadlineMs?: number;
106
109
  /**
@@ -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
  * Compute the bare registrable apex (eTLD+1) of a hostname using the Public
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/core",
3
- "version": "6.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "OxyHQ SDK Foundation — API client, authentication, cryptographic identity, and shared utilities",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -94,7 +94,7 @@
94
94
  }
95
95
  },
96
96
  "dependencies": {
97
- "@oxyhq/contracts": "^0.9.1",
97
+ "@oxyhq/contracts": "^0.10.0",
98
98
  "@oxyhq/protocol": "^0.1.1",
99
99
  "bip39": "^3.1.0",
100
100
  "buffer": "^6.0.3",
@@ -820,7 +820,7 @@ export class HttpService {
820
820
  * `clearCacheByPrefix` sweeps and `clearCacheEntry` base-key matching.
821
821
  * The `clearCacheEntry` callsites all pass fixed, dataless logical keys
822
822
  * (`GET:/users/<id>`, `GET:/session/user/<sessionId>`,
823
- * `GET:/fedcm/me/authorized-apps`), so this readable suffix can never be
823
+ * `GET:/auth/grants`), so this readable suffix can never be
824
824
  * ambiguous with a serialized request body.
825
825
  */
826
826
  private static readonly CACHE_IDENTITY_DELIM = ' id=';
package/src/index.ts CHANGED
@@ -34,9 +34,6 @@ export type { AuthRefreshReason, AuthRefreshHandler } from './HttpService';
34
34
  // ---------------------------------------------------------------------------
35
35
  // Authentication
36
36
  // ---------------------------------------------------------------------------
37
- // Legacy "Connected apps" management surface (list/revoke FedCM authorization
38
- // grants). The FedCM sign-in machinery was removed in the device-first cutover.
39
- export type { AuthorizedApp } from './mixins/OxyServices.authorizedApps';
40
37
  export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth';
41
38
  export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
42
39
  // "Sign in with Oxy" — handoff (Workstream C)
@@ -498,14 +495,18 @@ export type { QuickAccount, DisplayNameUserShape } from './utils/accountUtils';
498
495
  // Registrable-domain + central-IdP-apex helpers.
499
496
  //
500
497
  // The client SSO-bounce / silent-iframe / FedCM machinery was removed in the
501
- // device-first cutover. These three symbols survive because the api SSO surface
502
- // and the IdP (both lista B, gated on the ecosystem bump) still import them:
503
- // `registrableApex` (eTLD+1), `CENTRAL_IDP_APEX`, and the `SSO_CALLBACK_PATH`
504
- // constant. `@oxyhq/core/server` re-exports `registrableApex` + `SSO_CALLBACK_PATH`
505
- // for the api; the IdP imports all three from here.
506
- // ---------------------------------------------------------------------------
507
- export { registrableApex } from './utils/fapiAutoDetect';
508
- export { CENTRAL_AUTH_URL, CENTRAL_IDP_APEX } from './utils/authWebUrl';
498
+ // device-first cutover (wave 2, ecosystem-wide bump complete). `registrableApex`
499
+ // (eTLD+1) and `CENTRAL_IDP_APEX` are still genuinely used: `registrableApex`
500
+ // via the `@oxyhq/core/server` re-export consumed by
501
+ // `packages/api/src/utils/sameSite.ts` for same-site origin checks, and
502
+ // `CENTRAL_IDP_APEX` by `server/cors.ts`'s `createOxyCors` (auto-allows
503
+ // `*.oxy.so`). `SSO_CALLBACK_PATH` has no remaining importer outside this
504
+ // module as of wave 2 — kept exported for now rather than removed here (an
505
+ // export deletion is a logic change, out of scope for a comment sweep); flag
506
+ // for a follow-up dead-export cleanup pass.
507
+ // ---------------------------------------------------------------------------
508
+ export { registrableApex } from './utils/registrableApex';
509
+ export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
509
510
  export { SSO_CALLBACK_PATH } from './utils/ssoBounce';
510
511
 
511
512
  export { runColdBoot } from './utils/coldBoot';
@@ -542,6 +543,37 @@ export {
542
543
  accountIdsOf,
543
544
  } from './session/projectSessionState';
544
545
 
546
+ // Unified account-list projection (THE single source of truth for the account
547
+ // chooser: device sign-ins ∪ account graph, deduped by accountId). Pure +
548
+ // I/O-free — the caller hydrates profiles via `getUsersByIds`. Shared by
549
+ // `@oxyhq/services`, `@oxyhq/auth`, and auth.oxy.so so the list can't diverge.
550
+ export {
551
+ projectSwitchableAccounts,
552
+ switchableAccountIds,
553
+ } from './session/accountProjection';
554
+ export type {
555
+ SwitchableAccount,
556
+ SwitchableAccountUser,
557
+ ProjectSwitchableAccountsInput,
558
+ } from './session/accountProjection';
559
+
560
+ // Headless controller for the unified account dialog. Framework-agnostic
561
+ // state machine + subscribe/getSnapshot store (bind via `useSyncExternalStore`)
562
+ // — no password/2FA logic (that lives at the IdP; `openPasswordAtOxyAuth` only
563
+ // hands off). Reuses `SessionClient.switchAccount` / `oxyServices.switchToAccount`
564
+ // for the uniform switch and the existing device-flow methods for sign-in.
565
+ export {
566
+ AccountDialogController,
567
+ createAccountDialogController,
568
+ } from './session/accountDialogController';
569
+ export type {
570
+ AccountDialogControllerOptions,
571
+ AccountDialogSnapshot,
572
+ AccountDialogView,
573
+ SignInFlowPhase,
574
+ SignInFlowState,
575
+ } from './session/accountDialogController';
576
+
545
577
  // ---------------------------------------------------------------------------
546
578
  // Device-first session machinery (auth centralization, wave 1) — additive.
547
579
  // Persisted auth-state store, the unified refresh handler + scheduler, the
@@ -28,8 +28,9 @@
28
28
  * SWITCHING INTO AN ACCOUNT: `switchToAccount(accountId)` mints a REAL session
29
29
  * for the target account and plants it as the active session — there is no
30
30
  * per-request "acting-as" header. Identity is carried by the session/token, not
31
- * a delegation header, so a switch propagates through reload and `refresh-all`
32
- * exactly like a login.
31
+ * a delegation header, so a switch propagates through reload and cross-domain
32
+ * exactly like a login, via the device-first session model (the server
33
+ * registers the switched session into the operator's device-set directly).
33
34
  */
34
35
  import type { User } from '../models/interfaces';
35
36
  import type { SessionLoginResponse } from '../models/session';
@@ -434,22 +435,22 @@ export interface AccountSuccessResult {
434
435
  /**
435
436
  * Result of {@link OxyServicesAccountsMixin.switchToAccount} — the freshly
436
437
  * minted session for the target account, in the SAME shape the canonical login
437
- * / `claimSessionByToken` responses use (`SessionLoginResponse`), plus the
438
- * device-local refresh-cookie slot index.
438
+ * / `claimSessionByToken` responses use (`SessionLoginResponse`).
439
439
  *
440
440
  * `accessToken` is the first access token for the new session (already planted
441
- * as the active token by `switchToAccount`). The refresh token is NOT in the
442
- * body the server sets it as the httpOnly `oxy_rt_<authuser>` cookie, which
443
- * joins the device multi-account set so the switched session survives reload and
444
- * propagates cross-domain via `/auth/refresh-all`. `user` is the target account.
441
+ * as the active token by `switchToAccount`). The switched session's survival
442
+ * across reload and cross-domain sync is device-first: the server registers
443
+ * it into the operator's `DeviceSession` set directly
444
+ * (`deviceSessionService.addAccount`, broadcast to the device room) there is
445
+ * no client-side refresh-cookie slot to establish. `user` is the target
446
+ * account.
445
447
  */
446
448
  export interface SwitchAccountResult extends SessionLoginResponse {
447
449
  /**
448
- * The device-local refresh-cookie slot index (`oxy_rt_<authuser>`) the server
449
- * assigned to the minted session. Surfaced so the consumer can register the
450
- * new session in its device multi-account set exactly like a login response.
451
- * Absent only when the server could not set the cookie (best-effort — the
452
- * switch itself still succeeds).
450
+ * Legacy device-local refresh-cookie slot index. The current server switch
451
+ * response never sets this field (device-set registration replaced the
452
+ * cookie-slot model) kept optional for backward type-compatibility with
453
+ * any caller still reading it, but always `undefined` in practice.
453
454
  */
454
455
  authuser?: number;
455
456
  }
@@ -515,24 +516,18 @@ export function OxyServicesAccountsMixin<T extends typeof OxyServicesBase>(Base:
515
516
  * target, directly or inherited — else 403; 404 if missing/archived; 403 if
516
517
  * the target is a personal account), then mints a REAL session for the
517
518
  * target account and returns it in the canonical login / `claimSessionByToken`
518
- * shape (`{ sessionId, deviceId, expiresAt, accessToken, user, authuser }`).
519
+ * shape (`{ sessionId, deviceId, expiresAt, accessToken, user }`).
519
520
  *
520
521
  * Unlike the removed `X-Acting-As` delegation header, the returned session
521
522
  * IS the new identity: this plants `accessToken` as the active token —
522
523
  * exactly like `claimSessionByToken` / `verifyChallenge` — so every
523
524
  * subsequent request authenticates as the target account.
524
525
  *
525
- * Joining the device multi-account set (so the switch survives a reload and
526
- * propagates cross-domain via `/auth/refresh-all`) requires a SECOND call, to
527
- * `POST /auth/session`, made here after the token is planted. The switch route
528
- * lives at `/accounts/*`, OUTSIDE the `oxy_rt_<authuser>` cookie's `Path=/auth`
529
- * scope, so the server never sees the device's existing slots from it and
530
- * would clobber slot 0 (destroying the operator's own session). `/auth/session`
531
- * runs where those cookies ARE visible, so the server allocates a NEW slot that
532
- * coexists with the operator's and returns its `authuser`. This step is
533
- * web-only (native multi-account uses stored sessions, not cookies) and
534
- * best-effort — a failure leaves the in-session switch intact; the switched
535
- * account simply won't survive a reload until the cookie is next established.
526
+ * A single call is all that's needed: the server registers the switched
527
+ * session into the operator's `DeviceSession` set directly, inheriting the
528
+ * operator's central `deviceId` so the switch survives a reload and syncs
529
+ * cross-domain via the same device-first session model as a normal login —
530
+ * there is no separate client-side cookie/slot step to make it stick.
536
531
  *
537
532
  * After planting, the SDK's identity-scoped GET cache is fully cleared so
538
533
  * every cached read re-fetches as the new account. (The consuming
@@ -542,7 +537,7 @@ export function OxyServicesAccountsMixin<T extends typeof OxyServicesBase>(Base:
542
537
  * same-user silent refreshes, so the sweep here is explicit.)
543
538
  *
544
539
  * @param accountId - The target account's Mongo `_id`.
545
- * @returns The minted session (planted) plus the device `authuser` slot.
540
+ * @returns The minted session, already planted as the active session.
546
541
  */
547
542
  async switchToAccount(accountId: string): Promise<SwitchAccountResult> {
548
543
  try {
@@ -7,7 +7,6 @@
7
7
 
8
8
  import { OxyServicesBase } from '../OxyServices.base';
9
9
  import { OxyServicesAuthMixin } from './OxyServices.auth';
10
- import { OxyServicesAuthorizedAppsMixin } from './OxyServices.authorizedApps';
11
10
  import { OxyServicesUserMixin } from './OxyServices.user';
12
11
  import { OxyServicesIdentityMixin } from './OxyServices.identity';
13
12
  import { OxyServicesPrivacyMixin } from './OxyServices.privacy';
@@ -42,7 +41,6 @@ import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
42
41
  */
43
42
  type AllMixinInstances =
44
43
  & InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>>
45
- & InstanceType<ReturnType<typeof OxyServicesAuthorizedAppsMixin<typeof OxyServicesBase>>>
46
44
  & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>>
47
45
  & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>>
48
46
  & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>>
@@ -96,11 +94,6 @@ const MIXIN_PIPELINE: MixinFunction[] = [
96
94
  // Base authentication
97
95
  OxyServicesAuthMixin,
98
96
 
99
- // Legacy FedCM "Connected apps" management (list/revoke authorized RP grants).
100
- // The FedCM sign-in surface was removed in the device-first cutover; only
101
- // this management pair survives until the AppGrant migration.
102
- OxyServicesAuthorizedAppsMixin,
103
-
104
97
  // User management (requires auth)
105
98
  OxyServicesUserMixin,
106
99
  // Self-sovereign identity (DID, signed records, auth-method ↔ VM mapping)
@@ -8,11 +8,10 @@ export interface ClientSession {
8
8
  userId?: string;
9
9
  isCurrent?: boolean;
10
10
  /**
11
- * Web-only: the device-local refresh-cookie slot index (0..N) that backs
12
- * this session. Populated from `POST /auth/refresh-all` and from login /
13
- * signup / fedcm-exchange responses. Required for per-session web token
14
- * refresh via `refreshTokenViaCookie({ authuser })` without a bearer token.
15
- * Absent on native (RN uses the bearer-protected session id directly).
11
+ * The account's ordinal slot (0..N) within the device's account set
12
+ * (`SessionAccount.authuser` in `@oxyhq/contracts`), projected from the
13
+ * device-first `DeviceSessionState` used purely for stable Google-style
14
+ * account-chooser ordering, not for any token-refresh mechanism.
16
15
  */
17
16
  authuser?: number;
18
17
  }
@@ -68,7 +68,7 @@ export { verifySecret } from './verifySecret';
68
68
  // SOURCE OF TRUTH shared with the IdP worker and the client FAPI auto-detect.
69
69
  // Pure host handling (no browser deps), so it is safe on the server subpath and
70
70
  // lets `@oxyhq/api` derive `auth.<apex>` without duplicating PSL logic.
71
- export { registrableApex } from '../utils/fapiAutoDetect';
71
+ export { registrableApex } from '../utils/registrableApex';
72
72
 
73
73
  // The single RP callback path the IdP redirects back to. A pure wire-contract
74
74
  // constant (no browser deps at module top level), re-used server-side so the