@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
|
@@ -157,7 +157,7 @@ export declare class HttpService {
|
|
|
157
157
|
* `clearCacheByPrefix` sweeps and `clearCacheEntry` base-key matching.
|
|
158
158
|
* The `clearCacheEntry` callsites all pass fixed, dataless logical keys
|
|
159
159
|
* (`GET:/users/<id>`, `GET:/session/user/<sessionId>`,
|
|
160
|
-
* `GET:/
|
|
160
|
+
* `GET:/auth/grants`), so this readable suffix can never be
|
|
161
161
|
* ambiguous with a serialized request body.
|
|
162
162
|
*/
|
|
163
163
|
private static readonly CACHE_IDENTITY_DELIM;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -21,7 +21,6 @@ export { OxyServices, OxyAuthenticationError, OxyAuthenticationTimeoutError } fr
|
|
|
21
21
|
export { OXY_CLOUD_URL, oxyClient } from './OxyServices';
|
|
22
22
|
export type { LinkedHttpClient } from './OxyServices.base';
|
|
23
23
|
export type { AuthRefreshReason, AuthRefreshHandler } from './HttpService';
|
|
24
|
-
export type { AuthorizedApp } from './mixins/OxyServices.authorizedApps';
|
|
25
24
|
export { ServiceCredentialMismatchError } from './mixins/OxyServices.auth';
|
|
26
25
|
export type { ServiceTokenResponse } from './mixins/OxyServices.auth';
|
|
27
26
|
export type { CommonsSignInHandle, CommonsSignInStatus, CommonsApprovalInfo, CommonsSignInActionResult, } from './mixins/OxyServices.auth';
|
|
@@ -80,8 +79,8 @@ export type { LogContext } from './utils/loggerUtils';
|
|
|
80
79
|
export { updateAvatarVisibility } from './utils/avatarUtils';
|
|
81
80
|
export { buildAccountsArray, createQuickAccount, getAccountDisplayName, getAccountFallbackHandle, formatPublicKeyHandle, getAccountColor, } from './utils/accountUtils';
|
|
82
81
|
export type { QuickAccount, DisplayNameUserShape } from './utils/accountUtils';
|
|
83
|
-
export { registrableApex } from './utils/
|
|
84
|
-
export {
|
|
82
|
+
export { registrableApex } from './utils/registrableApex';
|
|
83
|
+
export { CENTRAL_IDP_APEX } from './utils/authWebUrl';
|
|
85
84
|
export { SSO_CALLBACK_PATH } from './utils/ssoBounce';
|
|
86
85
|
export { runColdBoot } from './utils/coldBoot';
|
|
87
86
|
export type { ColdBootStep, ColdBootStepResult, ColdBootSession, ColdBootSkip, ColdBootOutcome, RunColdBootOptions, } from './utils/coldBoot';
|
|
@@ -91,6 +90,10 @@ export type { SocketIOFactory, MinimalSocket } from './session/socketLoader';
|
|
|
91
90
|
export { createSessionClientHost } from './session/sessionClientHost';
|
|
92
91
|
export { createSessionClient } from './session/createSessionClient';
|
|
93
92
|
export { deviceStateToClientSessions, activeSessionIdOf, activeUserOf, accountIdsOf, } from './session/projectSessionState';
|
|
93
|
+
export { projectSwitchableAccounts, switchableAccountIds, } from './session/accountProjection';
|
|
94
|
+
export type { SwitchableAccount, SwitchableAccountUser, ProjectSwitchableAccountsInput, } from './session/accountProjection';
|
|
95
|
+
export { AccountDialogController, createAccountDialogController, } from './session/accountDialogController';
|
|
96
|
+
export type { AccountDialogControllerOptions, AccountDialogSnapshot, AccountDialogView, SignInFlowPhase, SignInFlowState, } from './session/accountDialogController';
|
|
94
97
|
export { createWebAuthStateStore, createNativeAuthStateStore, createMemoryAuthStateStore, AUTH_STATE_STORAGE_KEY, DEVICE_TOKEN_STORAGE_KEY, } from './session/authStateStore';
|
|
95
98
|
export type { PersistedAuthState, AuthStateStore, NativeKeyValueStorage, } from './session/authStateStore';
|
|
96
99
|
export { refreshPersistedSession, createAuthRefreshHandler, installAuthRefreshHandler, startTokenRefreshScheduler, TOKEN_REFRESH_LEAD_MS, } from './session/refresh';
|
|
@@ -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
|
|
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';
|
|
@@ -387,22 +388,22 @@ export interface AccountSuccessResult {
|
|
|
387
388
|
/**
|
|
388
389
|
* Result of {@link OxyServicesAccountsMixin.switchToAccount} — the freshly
|
|
389
390
|
* minted session for the target account, in the SAME shape the canonical login
|
|
390
|
-
* / `claimSessionByToken` responses use (`SessionLoginResponse`)
|
|
391
|
-
* device-local refresh-cookie slot index.
|
|
391
|
+
* / `claimSessionByToken` responses use (`SessionLoginResponse`).
|
|
392
392
|
*
|
|
393
393
|
* `accessToken` is the first access token for the new session (already planted
|
|
394
|
-
* as the active token by `switchToAccount`). The
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
394
|
+
* as the active token by `switchToAccount`). The switched session's survival
|
|
395
|
+
* across reload and cross-domain sync is device-first: the server registers
|
|
396
|
+
* it into the operator's `DeviceSession` set directly
|
|
397
|
+
* (`deviceSessionService.addAccount`, broadcast to the device room) — there is
|
|
398
|
+
* no client-side refresh-cookie slot to establish. `user` is the target
|
|
399
|
+
* account.
|
|
398
400
|
*/
|
|
399
401
|
export interface SwitchAccountResult extends SessionLoginResponse {
|
|
400
402
|
/**
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* switch itself still succeeds).
|
|
403
|
+
* Legacy device-local refresh-cookie slot index. The current server switch
|
|
404
|
+
* response never sets this field (device-set registration replaced the
|
|
405
|
+
* cookie-slot model) — kept optional for backward type-compatibility with
|
|
406
|
+
* any caller still reading it, but always `undefined` in practice.
|
|
406
407
|
*/
|
|
407
408
|
authuser?: number;
|
|
408
409
|
}
|
|
@@ -432,24 +433,18 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
432
433
|
* target, directly or inherited — else 403; 404 if missing/archived; 403 if
|
|
433
434
|
* the target is a personal account), then mints a REAL session for the
|
|
434
435
|
* target account and returns it in the canonical login / `claimSessionByToken`
|
|
435
|
-
* shape (`{ sessionId, deviceId, expiresAt, accessToken, user
|
|
436
|
+
* shape (`{ sessionId, deviceId, expiresAt, accessToken, user }`).
|
|
436
437
|
*
|
|
437
438
|
* Unlike the removed `X-Acting-As` delegation header, the returned session
|
|
438
439
|
* IS the new identity: this plants `accessToken` as the active token —
|
|
439
440
|
* exactly like `claimSessionByToken` / `verifyChallenge` — so every
|
|
440
441
|
* subsequent request authenticates as the target account.
|
|
441
442
|
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
* would clobber slot 0 (destroying the operator's own session). `/auth/session`
|
|
448
|
-
* runs where those cookies ARE visible, so the server allocates a NEW slot that
|
|
449
|
-
* coexists with the operator's and returns its `authuser`. This step is
|
|
450
|
-
* web-only (native multi-account uses stored sessions, not cookies) and
|
|
451
|
-
* best-effort — a failure leaves the in-session switch intact; the switched
|
|
452
|
-
* account simply won't survive a reload until the cookie is next established.
|
|
443
|
+
* A single call is all that's needed: the server registers the switched
|
|
444
|
+
* session into the operator's `DeviceSession` set directly, inheriting the
|
|
445
|
+
* operator's central `deviceId` so the switch survives a reload and syncs
|
|
446
|
+
* cross-domain via the same device-first session model as a normal login —
|
|
447
|
+
* there is no separate client-side cookie/slot step to make it stick.
|
|
453
448
|
*
|
|
454
449
|
* After planting, the SDK's identity-scoped GET cache is fully cleared so
|
|
455
450
|
* every cached read re-fetches as the new account. (The consuming
|
|
@@ -459,7 +454,7 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
459
454
|
* same-user silent refreshes, so the sweep here is explicit.)
|
|
460
455
|
*
|
|
461
456
|
* @param accountId - The target account's Mongo `_id`.
|
|
462
|
-
* @returns The minted session
|
|
457
|
+
* @returns The minted session, already planted as the active session.
|
|
463
458
|
*/
|
|
464
459
|
switchToAccount(accountId: string): Promise<SwitchAccountResult>;
|
|
465
460
|
/**
|
|
@@ -694,7 +689,9 @@ export declare function OxyServicesAccountsMixin<T extends typeof OxyServicesBas
|
|
|
694
689
|
handleError(error: unknown): Error;
|
|
695
690
|
healthCheck(): Promise<{
|
|
696
691
|
status: string;
|
|
697
|
-
users
|
|
692
|
+
users
|
|
693
|
+
/** Aggregate totals for an application over the requested period. */
|
|
694
|
+
? /** Aggregate totals for an application over the requested period. */: number;
|
|
698
695
|
timestamp?: string;
|
|
699
696
|
[key: string]: any;
|
|
700
697
|
}>;
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { OxyServicesBase } from '../OxyServices.base';
|
|
8
8
|
import { OxyServicesAuthMixin } from './OxyServices.auth';
|
|
9
|
-
import { OxyServicesAuthorizedAppsMixin } from './OxyServices.authorizedApps';
|
|
10
9
|
import { OxyServicesUserMixin } from './OxyServices.user';
|
|
11
10
|
import { OxyServicesIdentityMixin } from './OxyServices.identity';
|
|
12
11
|
import { OxyServicesPrivacyMixin } from './OxyServices.privacy';
|
|
@@ -38,7 +37,7 @@ import { OxyServicesDeviceBootMixin } from './OxyServices.deviceBoot';
|
|
|
38
37
|
* If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
|
|
39
38
|
* are visible without a cast.
|
|
40
39
|
*/
|
|
41
|
-
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof
|
|
40
|
+
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPrivacyMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLanguageMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesPaymentMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesReputationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAssetsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAccountsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesConnectedAppsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLocationMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAnalyticsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDevicesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesSecurityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesFeaturesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesTopicsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesContactsMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesAppDataMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesCivicMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesNodesMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesLinksMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesDeviceBootMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
42
41
|
/**
|
|
43
42
|
* Constructor type for the fully composed mixin pipeline. Each mixin returns
|
|
44
43
|
* a new constructor that augments its input; reducing across the pipeline
|
|
@@ -7,11 +7,10 @@ export interface ClientSession {
|
|
|
7
7
|
userId?: string;
|
|
8
8
|
isCurrent?: boolean;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Absent on native (RN uses the bearer-protected session id directly).
|
|
10
|
+
* The account's ordinal slot (0..N) within the device's account set
|
|
11
|
+
* (`SessionAccount.authuser` in `@oxyhq/contracts`), projected from the
|
|
12
|
+
* device-first `DeviceSessionState` — used purely for stable Google-style
|
|
13
|
+
* account-chooser ordering, not for any token-refresh mechanism.
|
|
15
14
|
*/
|
|
16
15
|
authuser?: number;
|
|
17
16
|
}
|
|
@@ -23,5 +23,5 @@ export type { SafeFetchOptions, SafeFetchResult, SsrfCheckFail, SsrfCheckOk, Ssr
|
|
|
23
23
|
export { createOxyCors } from './cors';
|
|
24
24
|
export type { OxyCorsOptions } from './cors';
|
|
25
25
|
export { verifySecret } from './verifySecret';
|
|
26
|
-
export { registrableApex } from '../utils/
|
|
26
|
+
export { registrableApex } from '../utils/registrableApex';
|
|
27
27
|
export { SSO_CALLBACK_PATH } from '../utils/ssoBounce';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type DeviceSessionState } from '@oxyhq/contracts';
|
|
2
2
|
import type { MinimalSocket, SocketIOFactory } from './socketLoader';
|
|
3
3
|
export interface TokenTransport {
|
|
4
|
-
/** Ensure this app holds a per-domain access token for state.activeAccountId (mint via
|
|
4
|
+
/** Ensure this app holds a per-domain access token for state.activeAccountId (mint via the persisted refresh family / shared keychain). Best-effort. */
|
|
5
5
|
ensureActiveToken(state: DeviceSessionState): Promise<void>;
|
|
6
6
|
}
|
|
7
7
|
export interface SessionClientHost {
|
|
@@ -38,6 +38,30 @@ export interface SessionClientOptions {
|
|
|
38
38
|
* absent it falls back to `getSocketIO()`.
|
|
39
39
|
*/
|
|
40
40
|
socketFactory?: SocketIOFactory;
|
|
41
|
+
/**
|
|
42
|
+
* Gate + credential for opening the realtime socket while SIGNED OUT (no
|
|
43
|
+
* access token), so an idle tab still joins its `device:<id>` room and can
|
|
44
|
+
* self-acquire the moment a sibling app/tab signs in on the same device.
|
|
45
|
+
* Returns:
|
|
46
|
+
* - `true` → connect and rely on the first-party `oxy_device` cookie
|
|
47
|
+
* riding the same-site handshake (web `*.oxy.so`; the cookie is HttpOnly
|
|
48
|
+
* so JS cannot read it, but the browser sends it automatically).
|
|
49
|
+
* - a string → connect and present it as `deviceToken` in the handshake
|
|
50
|
+
* auth (native shared-keychain device token; RN has no cookie jar).
|
|
51
|
+
* - `false`/`null` → do NOT open a signed-out socket (the default — e.g. a
|
|
52
|
+
* native app with no known device yet).
|
|
53
|
+
* Called at connect time (and each reconnect); may be async (keychain read).
|
|
54
|
+
*/
|
|
55
|
+
signedOutSocketAuth?: () => boolean | string | null | Promise<boolean | string | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Invoked when a `session_state` push (or a same-origin BroadcastChannel wake)
|
|
58
|
+
* arrives while this tab is SIGNED OUT and the pushed device state has at
|
|
59
|
+
* least one account — i.e. a sibling just signed in on this device. The
|
|
60
|
+
* consumer runs its session acquisition (cold boot / `requestWebSession`),
|
|
61
|
+
* which plants a token and flips the tab to signed-in. Guarded + idempotent:
|
|
62
|
+
* only one acquisition runs at a time, and a returned promise gates the next.
|
|
63
|
+
*/
|
|
64
|
+
onSessionAppeared?: () => void | Promise<void>;
|
|
41
65
|
}
|
|
42
66
|
type StateListener = (state: DeviceSessionState | null) => void;
|
|
43
67
|
export declare class SessionClient {
|
|
@@ -48,6 +72,12 @@ export declare class SessionClient {
|
|
|
48
72
|
protected socket: MinimalSocket | null;
|
|
49
73
|
private tokenUnsub;
|
|
50
74
|
private started;
|
|
75
|
+
/** In-flight guard so a burst of pushes triggers at most ONE acquisition. */
|
|
76
|
+
private acquiring;
|
|
77
|
+
/** True while the live socket is an anonymous (signed-out) device connection. */
|
|
78
|
+
private socketAnonymous;
|
|
79
|
+
/** Same-origin cross-tab wake channel; null on platforms without BroadcastChannel. */
|
|
80
|
+
private channel;
|
|
51
81
|
constructor(host: SessionClientHost, options?: SessionClientOptions);
|
|
52
82
|
getState(): DeviceSessionState | null;
|
|
53
83
|
subscribe(listener: StateListener): () => void;
|
|
@@ -88,5 +118,26 @@ export declare class SessionClient {
|
|
|
88
118
|
start(): Promise<void>;
|
|
89
119
|
stop(): void;
|
|
90
120
|
private connectSocket;
|
|
121
|
+
/**
|
|
122
|
+
* Run the consumer's session acquisition at most once at a time. A returned
|
|
123
|
+
* promise gates the next attempt (reset on settle), so a failed acquisition
|
|
124
|
+
* can retry on the NEXT push while a burst of identical pushes cannot pile up.
|
|
125
|
+
*/
|
|
126
|
+
private requestAcquisition;
|
|
127
|
+
/**
|
|
128
|
+
* Open the same-origin `BroadcastChannel` (web only). A sibling tab that
|
|
129
|
+
* commits a session posts a wake ping; on receipt a signed-in tab re-syncs its
|
|
130
|
+
* device state and a signed-out tab self-acquires — instant + network-free for
|
|
131
|
+
* the common "two tabs of the same origin" case, with no state (and no tokens)
|
|
132
|
+
* ever crossing the channel. No-op on native (no BroadcastChannel).
|
|
133
|
+
*/
|
|
134
|
+
private openBroadcastChannel;
|
|
135
|
+
/**
|
|
136
|
+
* Wake same-origin sibling tabs after a locally-initiated session mutation.
|
|
137
|
+
* Opens the channel lazily: a sign-in registers the account (`addCurrentAccount`
|
|
138
|
+
* / `switchAccount`) BEFORE `start()` runs, so the ping must not depend on
|
|
139
|
+
* `start()` having opened the channel first.
|
|
140
|
+
*/
|
|
141
|
+
private postCommitPing;
|
|
91
142
|
}
|
|
92
143
|
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless controller for the unified Oxy account dialog.
|
|
3
|
+
*
|
|
4
|
+
* A framework-agnostic state machine + subscribe/getSnapshot store (the same
|
|
5
|
+
* pattern {@link SessionClient} uses — no React, no RN) that both
|
|
6
|
+
* `@oxyhq/services` (RN `OxyProvider`) and `@oxyhq/auth` (web `WebOxyProvider`)
|
|
7
|
+
* bind to via `useSyncExternalStore`, so the account chooser is ONE
|
|
8
|
+
* implementation across the ecosystem instead of the five drifting copies it
|
|
9
|
+
* replaces.
|
|
10
|
+
*
|
|
11
|
+
* The controller owns:
|
|
12
|
+
* - the unified account list (via {@link projectSwitchableAccounts}), fetched
|
|
13
|
+
* from `SessionClient` state ∪ `oxyServices.listAccounts()` and hydrated
|
|
14
|
+
* with `oxyServices.getUsersByIds()`;
|
|
15
|
+
* - the dialog `view` state machine (`accounts` | `signin` | `qr` | `add`);
|
|
16
|
+
* - `switchTo` (the uniform switch: `SessionClient.switchAccount` for an
|
|
17
|
+
* account already on the device, `oxyServices.switchToAccount` to mint on
|
|
18
|
+
* first entry into a graph account — reusing the existing SDK primitives, no
|
|
19
|
+
* new switch path);
|
|
20
|
+
* - the "Sign in with Oxy" device flow (same-device shared-keychain via
|
|
21
|
+
* `oxyServices.signInWithSharedIdentity`, else the cross-device QR handoff
|
|
22
|
+
* via `startCommonsSignIn` → poll → `claimSessionByToken`).
|
|
23
|
+
*
|
|
24
|
+
* It deliberately owns NO password/2FA logic — those live at the IdP
|
|
25
|
+
* (auth.oxy.so). {@link AccountDialogController.openPasswordAtOxyAuth} only
|
|
26
|
+
* builds the hand-off URL; device-first convergence syncs the session back.
|
|
27
|
+
*/
|
|
28
|
+
import type { OxyServices } from '../OxyServices';
|
|
29
|
+
import type { SessionLoginResponse, MinimalUserData } from '../models/session';
|
|
30
|
+
import { SessionClient } from './SessionClient';
|
|
31
|
+
import { type SwitchableAccount } from './accountProjection';
|
|
32
|
+
/** The dialog's top-level view. */
|
|
33
|
+
export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add';
|
|
34
|
+
/** Lifecycle phase of the "Sign in with Oxy" device flow. */
|
|
35
|
+
export type SignInFlowPhase = 'idle' | 'starting' | 'waiting' | 'authorized' | 'error';
|
|
36
|
+
/** State of the "Sign in with Oxy" (shared-key / QR) device flow. */
|
|
37
|
+
export interface SignInFlowState {
|
|
38
|
+
phase: SignInFlowPhase;
|
|
39
|
+
/**
|
|
40
|
+
* The PUBLIC, single-use authorize code (safe to display), or `null`. NOT the
|
|
41
|
+
* secret `sessionToken` — the approver resolves the app identity from this.
|
|
42
|
+
*/
|
|
43
|
+
authorizeCode: string | null;
|
|
44
|
+
/**
|
|
45
|
+
* The structured deep-link / QR payload (`oxycommons://approve?...`) to render
|
|
46
|
+
* as a QR (cross-device) and open as a deep link (same-device), or `null`.
|
|
47
|
+
*/
|
|
48
|
+
qrPayload: string | null;
|
|
49
|
+
/** Server-authoritative expiry (epoch ms), or `null`. */
|
|
50
|
+
expiresAt: number | null;
|
|
51
|
+
/** Human-readable error for the retry UI, or `null`. */
|
|
52
|
+
error: string | null;
|
|
53
|
+
}
|
|
54
|
+
/** Immutable snapshot consumed by `useSyncExternalStore`. */
|
|
55
|
+
export interface AccountDialogSnapshot {
|
|
56
|
+
/** The current view. */
|
|
57
|
+
view: AccountDialogView;
|
|
58
|
+
/** The unified, deduped account list (device sign-ins ∪ graph accounts). */
|
|
59
|
+
accounts: SwitchableAccount[];
|
|
60
|
+
/** The currently-active account id, or `null` when signed out. */
|
|
61
|
+
activeAccountId: string | null;
|
|
62
|
+
/** `true` while the initial account-list fetch is in flight with no data yet. */
|
|
63
|
+
loading: boolean;
|
|
64
|
+
/** A human-readable account-list error, or `null`. */
|
|
65
|
+
error: string | null;
|
|
66
|
+
/** The `accountId` of an in-flight switch, or `null`. */
|
|
67
|
+
switchingAccountId: string | null;
|
|
68
|
+
/** The "Sign in with Oxy" device-flow state. */
|
|
69
|
+
signIn: SignInFlowState;
|
|
70
|
+
}
|
|
71
|
+
/** Construction options for {@link AccountDialogController}. */
|
|
72
|
+
export interface AccountDialogControllerOptions {
|
|
73
|
+
/** The API client. Source of graph accounts, profiles, and the sign-in methods. */
|
|
74
|
+
oxyServices: OxyServices;
|
|
75
|
+
/** The device-first session authority. Source of device rows + the switch path. */
|
|
76
|
+
sessionClient: SessionClient;
|
|
77
|
+
/**
|
|
78
|
+
* The RP's registered OAuth client id (ApplicationCredential publicKey).
|
|
79
|
+
* Required for the QR handoff (`startCommonsSignIn`); when absent, `showQr`
|
|
80
|
+
* fails with a clear configuration error instead of creating a session the
|
|
81
|
+
* server would reject.
|
|
82
|
+
*/
|
|
83
|
+
clientId?: string | null;
|
|
84
|
+
/** Locale for display-name resolution. */
|
|
85
|
+
locale?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Commit a freshly-authorized session (device flow / shared identity / minted
|
|
88
|
+
* graph switch) into the host's session set — device-first registration +
|
|
89
|
+
* durable persist + profile hydration. The consumer supplies its provider's
|
|
90
|
+
* commit path (`useOxy().handleWebSession` / the auth-sdk equivalent). Called
|
|
91
|
+
* AFTER the SDK has planted the access token. When omitted the controller
|
|
92
|
+
* falls back to `SessionClient.registerAndActivate` (registration + activation
|
|
93
|
+
* only — no provider-side durable persist/hydration).
|
|
94
|
+
*/
|
|
95
|
+
commitSession?: (session: SessionLoginResponse & {
|
|
96
|
+
refreshToken?: string;
|
|
97
|
+
}) => Promise<void>;
|
|
98
|
+
/** Notified after a completed sign-in (bearer planted + session committed). */
|
|
99
|
+
onSignedIn?: (user: MinimalUserData) => void;
|
|
100
|
+
/** Central IdP apex for `openPasswordAtOxyAuth` (defaults to `CENTRAL_IDP_APEX`). */
|
|
101
|
+
idpApex?: string;
|
|
102
|
+
/** QR device-flow poll interval in ms (default 3000). */
|
|
103
|
+
pollIntervalMs?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Optional URL opener. When provided, `openPasswordAtOxyAuth` invokes it with
|
|
106
|
+
* the built URL in addition to returning it (web: `location.assign`; native:
|
|
107
|
+
* `Linking.openURL`). Headless core never touches `window`/`Linking` itself.
|
|
108
|
+
*/
|
|
109
|
+
openUrl?: (url: string) => void;
|
|
110
|
+
}
|
|
111
|
+
type SnapshotListener = (snapshot: AccountDialogSnapshot) => void;
|
|
112
|
+
export declare class AccountDialogController {
|
|
113
|
+
private readonly oxyServices;
|
|
114
|
+
private readonly sessionClient;
|
|
115
|
+
private readonly clientId;
|
|
116
|
+
private readonly locale?;
|
|
117
|
+
private readonly commitSession?;
|
|
118
|
+
private readonly onSignedIn?;
|
|
119
|
+
private readonly idpApex;
|
|
120
|
+
private readonly pollIntervalMs;
|
|
121
|
+
private readonly openUrl?;
|
|
122
|
+
private readonly listeners;
|
|
123
|
+
private view;
|
|
124
|
+
private graph;
|
|
125
|
+
private profilesById;
|
|
126
|
+
private loading;
|
|
127
|
+
private error;
|
|
128
|
+
private switchingAccountId;
|
|
129
|
+
private signIn;
|
|
130
|
+
/** The secret device-flow token of the active QR flow (never surfaced). */
|
|
131
|
+
private signInToken;
|
|
132
|
+
private pollTimer;
|
|
133
|
+
private unsubscribeSession;
|
|
134
|
+
private started;
|
|
135
|
+
private refreshSeq;
|
|
136
|
+
private snapshot;
|
|
137
|
+
constructor(options: AccountDialogControllerOptions);
|
|
138
|
+
/** Returns the current immutable snapshot (stable reference between changes). */
|
|
139
|
+
getSnapshot(): AccountDialogSnapshot;
|
|
140
|
+
/** Subscribe to snapshot changes. Returns an unsubscribe function. */
|
|
141
|
+
subscribe(listener: SnapshotListener): () => void;
|
|
142
|
+
/**
|
|
143
|
+
* Begin driving the dialog: subscribe to `SessionClient` state and load the
|
|
144
|
+
* account list. Idempotent — a second `start()` is a no-op. Pair with
|
|
145
|
+
* {@link destroy}.
|
|
146
|
+
*/
|
|
147
|
+
start(): void;
|
|
148
|
+
/**
|
|
149
|
+
* Stop driving the dialog: unsubscribe from `SessionClient` and tear down the
|
|
150
|
+
* active sign-in flow (timers). Idempotent.
|
|
151
|
+
*/
|
|
152
|
+
destroy(): void;
|
|
153
|
+
/** Set the dialog view directly. */
|
|
154
|
+
setView(view: AccountDialogView): void;
|
|
155
|
+
/** Return to the account list and cancel any in-flight sign-in flow. */
|
|
156
|
+
close(): void;
|
|
157
|
+
/** Switch to the "add account" view (the sign-in entry chooser). */
|
|
158
|
+
add(): void;
|
|
159
|
+
/**
|
|
160
|
+
* Reload the account graph and per-account profiles, then re-project. Safe to
|
|
161
|
+
* call repeatedly; concurrent calls are reconciled by a sequence guard so a
|
|
162
|
+
* slow earlier fetch never overwrites a newer result.
|
|
163
|
+
*/
|
|
164
|
+
refresh(): Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* Fetch profiles for any account id (device set ∪ graph) not yet resolved.
|
|
167
|
+
* Cheap no-op when everything is already hydrated — used from the session
|
|
168
|
+
* subscription so a newly-added device account gets a name/avatar.
|
|
169
|
+
*/
|
|
170
|
+
private ensureProfiles;
|
|
171
|
+
private loadProfiles;
|
|
172
|
+
/**
|
|
173
|
+
* Switch the active account to `accountId`.
|
|
174
|
+
*
|
|
175
|
+
* Uniform switch model, mirroring the SDK's existing path — NOT a new switch
|
|
176
|
+
* mechanism:
|
|
177
|
+
* - already on this device → `SessionClient.switchAccount` (device-first
|
|
178
|
+
* switch of `/session/device/switch`);
|
|
179
|
+
* - a graph account not yet on the device (first entry) →
|
|
180
|
+
* `oxyServices.switchToAccount` mints + plants a real session and the
|
|
181
|
+
* server registers it into the device set, then it is committed
|
|
182
|
+
* (`commitSession` when supplied, else `SessionClient.registerAndActivate`).
|
|
183
|
+
*
|
|
184
|
+
* The resulting device-state change flows back through the `SessionClient`
|
|
185
|
+
* subscription, which re-projects the active row. Concurrent switches are
|
|
186
|
+
* ignored while one is in flight.
|
|
187
|
+
*/
|
|
188
|
+
switchTo(accountId: string): Promise<void>;
|
|
189
|
+
/**
|
|
190
|
+
* Start "Sign in with Oxy". Native devices with a shared identity mint a
|
|
191
|
+
* session silently (`signInWithSharedIdentity`); everything else (web, or a
|
|
192
|
+
* native device without a shared identity) falls through to the cross-device
|
|
193
|
+
* QR handoff.
|
|
194
|
+
*/
|
|
195
|
+
signInWithOxy(): Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Begin (or restart) the cross-device QR handoff: create a device-flow
|
|
198
|
+
* session, surface its `authorizeCode` + `qrPayload`, and poll for approval.
|
|
199
|
+
* On approval the secret token is exchanged (`claimSessionByToken`) and the
|
|
200
|
+
* session committed. Requires `clientId`.
|
|
201
|
+
*/
|
|
202
|
+
showQr(): Promise<void>;
|
|
203
|
+
/** Tear down the active sign-in device flow (timers + token) and reset to idle. */
|
|
204
|
+
cancelSignIn(): void;
|
|
205
|
+
/**
|
|
206
|
+
* Build (and, when an `openUrl` handler was supplied, open) the auth.oxy.so
|
|
207
|
+
* password sign-in URL. Password + 2FA are NOT in the SDK — they live at the
|
|
208
|
+
* IdP; this only hands off. Device-first: after login at the IdP the device
|
|
209
|
+
* session converges and the caller is woken via the device socket /
|
|
210
|
+
* `BroadcastChannel`, so the URL only needs to point at the IdP sign-in with
|
|
211
|
+
* the right return.
|
|
212
|
+
*
|
|
213
|
+
* @param params.returnUrl - Where the IdP returns after login. Defaults to the
|
|
214
|
+
* current document URL on web (`globalThis.location.href`); pass explicitly
|
|
215
|
+
* on native (no `location`).
|
|
216
|
+
* @param params.state - Optional opaque state echoed back on return.
|
|
217
|
+
* @returns The absolute auth.oxy.so sign-in URL.
|
|
218
|
+
*/
|
|
219
|
+
openPasswordAtOxyAuth(params?: {
|
|
220
|
+
returnUrl?: string;
|
|
221
|
+
state?: string;
|
|
222
|
+
}): string;
|
|
223
|
+
private scheduleNextPoll;
|
|
224
|
+
private pollOnce;
|
|
225
|
+
private claimAndComplete;
|
|
226
|
+
/**
|
|
227
|
+
* Commit an authorized session, notify, and return to the account list. Shared
|
|
228
|
+
* by the shared-key, QR, and mint-switch paths so they cannot drift.
|
|
229
|
+
*/
|
|
230
|
+
private completeSignIn;
|
|
231
|
+
/**
|
|
232
|
+
* Register a token-planted session into the device set. Prefers the
|
|
233
|
+
* consumer's `commitSession` (durable persist + hydration); falls back to
|
|
234
|
+
* `SessionClient.registerAndActivate` (registration + activation only).
|
|
235
|
+
*/
|
|
236
|
+
private commitAuthorizedSession;
|
|
237
|
+
private failSignIn;
|
|
238
|
+
private clearPollTimer;
|
|
239
|
+
private setSignIn;
|
|
240
|
+
private computeSnapshot;
|
|
241
|
+
/** Recompute the snapshot and notify subscribers. */
|
|
242
|
+
private emit;
|
|
243
|
+
}
|
|
244
|
+
/** Factory mirroring `createSessionClient`, for ergonomic wiring by consumers. */
|
|
245
|
+
export declare function createAccountDialogController(options: AccountDialogControllerOptions): AccountDialogController;
|
|
246
|
+
export {};
|