@oxyhq/core 12.10.6 → 12.11.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/boot/sessionColdBoot.js +142 -37
- package/dist/cjs/i18n/locales/ar-SA.json +13 -1
- package/dist/cjs/i18n/locales/ca-ES.json +13 -1
- package/dist/cjs/i18n/locales/de-DE.json +13 -1
- package/dist/cjs/i18n/locales/en-US.json +13 -4
- package/dist/cjs/i18n/locales/es-ES.json +13 -4
- package/dist/cjs/i18n/locales/fr-FR.json +13 -1
- package/dist/cjs/i18n/locales/it-IT.json +13 -1
- package/dist/cjs/i18n/locales/ja-JP.json +13 -1
- package/dist/cjs/i18n/locales/ko-KR.json +13 -1
- package/dist/cjs/i18n/locales/locales/ar-SA.json +13 -1
- package/dist/cjs/i18n/locales/locales/ca-ES.json +13 -1
- package/dist/cjs/i18n/locales/locales/de-DE.json +13 -1
- package/dist/cjs/i18n/locales/locales/en-US.json +13 -4
- package/dist/cjs/i18n/locales/locales/es-ES.json +13 -4
- package/dist/cjs/i18n/locales/locales/fr-FR.json +13 -1
- package/dist/cjs/i18n/locales/locales/it-IT.json +13 -1
- package/dist/cjs/i18n/locales/locales/ja-JP.json +13 -1
- package/dist/cjs/i18n/locales/locales/ko-KR.json +13 -1
- package/dist/cjs/i18n/locales/locales/pt-PT.json +13 -1
- package/dist/cjs/i18n/locales/locales/zh-CN.json +13 -1
- package/dist/cjs/i18n/locales/pt-PT.json +13 -1
- package/dist/cjs/i18n/locales/zh-CN.json +13 -1
- package/dist/cjs/index.js +31 -5
- package/dist/cjs/mixins/OxyServices.auth.js +257 -17
- package/dist/cjs/mixins/OxyServices.deviceBoot.js +54 -3
- package/dist/cjs/mixins/OxyServices.notifications.js +63 -0
- package/dist/cjs/mixins/index.js +5 -0
- package/dist/cjs/session/SessionClient.js +40 -4
- package/dist/cjs/session/accountDialogController.js +288 -41
- package/dist/cjs/session/identityPin.js +256 -0
- package/dist/cjs/session/identitySession.js +135 -0
- package/dist/cjs/session/projectSessionState.js +54 -15
- package/dist/cjs/session/refresh.js +73 -8
- package/dist/cjs/utils/commonsDelivery.js +59 -0
- package/dist/cjs/utils/oauthPkce.js +33 -3
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/boot/sessionColdBoot.js +142 -37
- package/dist/esm/i18n/locales/ar-SA.json +13 -1
- package/dist/esm/i18n/locales/ca-ES.json +13 -1
- package/dist/esm/i18n/locales/de-DE.json +13 -1
- package/dist/esm/i18n/locales/en-US.json +13 -4
- package/dist/esm/i18n/locales/es-ES.json +13 -4
- package/dist/esm/i18n/locales/fr-FR.json +13 -1
- package/dist/esm/i18n/locales/it-IT.json +13 -1
- package/dist/esm/i18n/locales/ja-JP.json +13 -1
- package/dist/esm/i18n/locales/ko-KR.json +13 -1
- package/dist/esm/i18n/locales/locales/ar-SA.json +13 -1
- package/dist/esm/i18n/locales/locales/ca-ES.json +13 -1
- package/dist/esm/i18n/locales/locales/de-DE.json +13 -1
- package/dist/esm/i18n/locales/locales/en-US.json +13 -4
- package/dist/esm/i18n/locales/locales/es-ES.json +13 -4
- package/dist/esm/i18n/locales/locales/fr-FR.json +13 -1
- package/dist/esm/i18n/locales/locales/it-IT.json +13 -1
- package/dist/esm/i18n/locales/locales/ja-JP.json +13 -1
- package/dist/esm/i18n/locales/locales/ko-KR.json +13 -1
- package/dist/esm/i18n/locales/locales/pt-PT.json +13 -1
- package/dist/esm/i18n/locales/locales/zh-CN.json +13 -1
- package/dist/esm/i18n/locales/pt-PT.json +13 -1
- package/dist/esm/i18n/locales/zh-CN.json +13 -1
- package/dist/esm/index.js +14 -1
- package/dist/esm/mixins/OxyServices.auth.js +257 -17
- package/dist/esm/mixins/OxyServices.deviceBoot.js +52 -3
- package/dist/esm/mixins/OxyServices.notifications.js +60 -0
- package/dist/esm/mixins/index.js +5 -0
- package/dist/esm/session/SessionClient.js +40 -4
- package/dist/esm/session/accountDialogController.js +288 -41
- package/dist/esm/session/identityPin.js +249 -0
- package/dist/esm/session/identitySession.js +131 -0
- package/dist/esm/session/projectSessionState.js +54 -15
- package/dist/esm/session/refresh.js +73 -8
- package/dist/esm/utils/commonsDelivery.js +54 -0
- package/dist/esm/utils/oauthPkce.js +31 -2
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/boot/sessionColdBoot.d.ts +38 -1
- package/dist/types/index.d.ts +12 -4
- package/dist/types/mixins/OxyServices.auth.d.ts +262 -11
- package/dist/types/mixins/OxyServices.deviceBoot.d.ts +31 -1
- package/dist/types/mixins/OxyServices.notifications.d.ts +143 -0
- package/dist/types/mixins/index.d.ts +2 -1
- package/dist/types/session/SessionClient.d.ts +28 -0
- package/dist/types/session/accountDialogController.d.ts +187 -9
- package/dist/types/session/identityPin.d.ts +87 -0
- package/dist/types/session/identitySession.d.ts +105 -0
- package/dist/types/session/projectSessionState.d.ts +21 -19
- package/dist/types/session/refresh.d.ts +32 -1
- package/dist/types/utils/commonsDelivery.d.ts +105 -0
- package/dist/types/utils/oauthPkce.d.ts +21 -1
- package/package.json +2 -2
- package/src/boot/__tests__/sessionColdBoot.identity.test.ts +393 -0
- package/src/boot/sessionColdBoot.ts +183 -37
- package/src/i18n/locales/ar-SA.json +13 -1
- package/src/i18n/locales/ca-ES.json +13 -1
- package/src/i18n/locales/de-DE.json +13 -1
- package/src/i18n/locales/en-US.json +13 -4
- package/src/i18n/locales/es-ES.json +13 -4
- package/src/i18n/locales/fr-FR.json +13 -1
- package/src/i18n/locales/it-IT.json +13 -1
- package/src/i18n/locales/ja-JP.json +13 -1
- package/src/i18n/locales/ko-KR.json +13 -1
- package/src/i18n/locales/pt-PT.json +13 -1
- package/src/i18n/locales/zh-CN.json +13 -1
- package/src/index.ts +49 -0
- package/src/mixins/OxyServices.auth.ts +464 -24
- package/src/mixins/OxyServices.deviceBoot.ts +54 -2
- package/src/mixins/OxyServices.notifications.ts +142 -0
- package/src/mixins/__tests__/commonsSignIn.test.ts +754 -2
- package/src/mixins/__tests__/deviceTokenMintPinned.test.ts +113 -0
- package/src/mixins/__tests__/preSessionSkipAuth.test.ts +18 -0
- package/src/mixins/index.ts +6 -0
- package/src/session/SessionClient.ts +57 -4
- package/src/session/__tests__/SessionClient.identityPin.test.ts +175 -0
- package/src/session/__tests__/accountDialogController.test.ts +812 -1
- package/src/session/__tests__/identityPin.test.ts +131 -0
- package/src/session/__tests__/identitySession.test.ts +194 -0
- package/src/session/__tests__/projectSessionState.test.ts +88 -0
- package/src/session/__tests__/refresh.identityPin.test.ts +306 -0
- package/src/session/accountDialogController.ts +435 -44
- package/src/session/identityPin.ts +310 -0
- package/src/session/identitySession.ts +206 -0
- package/src/session/projectSessionState.ts +61 -13
- package/src/session/refresh.ts +103 -9
- package/src/utils/__tests__/oauthPkce.test.ts +51 -0
- package/src/utils/commonsDelivery.ts +132 -0
- package/src/utils/oauthPkce.ts +56 -3
|
@@ -24,6 +24,7 @@ import { OxyServicesUtilityMixin } from './OxyServices.utility';
|
|
|
24
24
|
import { OxyServicesFeaturesMixin } from './OxyServices.features';
|
|
25
25
|
import { OxyServicesTopicsMixin } from './OxyServices.topics';
|
|
26
26
|
import { OxyServicesContactsMixin } from './OxyServices.contacts';
|
|
27
|
+
import { OxyServicesNotificationsMixin } from './OxyServices.notifications';
|
|
27
28
|
import { OxyServicesAppDataMixin } from './OxyServices.appData';
|
|
28
29
|
import { OxyServicesCivicMixin } from './OxyServices.civic';
|
|
29
30
|
import { OxyServicesNodesMixin } from './OxyServices.nodes';
|
|
@@ -39,7 +40,7 @@ import { OxyServicesDeviceTransferMixin } from './OxyServices.deviceTransfer';
|
|
|
39
40
|
* If you add a new mixin to `MIXIN_PIPELINE`, add it here too so its methods
|
|
40
41
|
* are visible without a cast.
|
|
41
42
|
*/
|
|
42
|
-
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<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 OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
43
|
+
type AllMixinInstances = InstanceType<ReturnType<typeof OxyServicesAuthMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUserMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesIdentityBackupMixin<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 OxyServicesNotificationsMixin<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 OxyServicesDeviceTransferMixin<typeof OxyServicesBase>>> & InstanceType<ReturnType<typeof OxyServicesUtilityMixin<typeof OxyServicesBase>>>;
|
|
43
44
|
/**
|
|
44
45
|
* Constructor type for the fully composed mixin pipeline. Each mixin returns
|
|
45
46
|
* a new constructor that augments its input; reducing across the pipeline
|
|
@@ -66,6 +66,22 @@ export interface SessionClientOptions {
|
|
|
66
66
|
* absent it falls back to `getSocketIO()`.
|
|
67
67
|
*/
|
|
68
68
|
socketFactory?: SocketIOFactory;
|
|
69
|
+
/**
|
|
70
|
+
* The PINNED account id for an IDENTITY-BOUND client (the identity vault),
|
|
71
|
+
* or `null` for every ordinary account-mode client. Read as a function because
|
|
72
|
+
* the pin is resolved asynchronously at boot and can move when the identity
|
|
73
|
+
* session is re-established.
|
|
74
|
+
*
|
|
75
|
+
* While pinned this client tracks device state TRUTHFULLY — other apps'
|
|
76
|
+
* accounts and the device's real `activeAccountId` stay visible in `getState()`
|
|
77
|
+
* — but it NEVER re-binds its bearer to that active account: no `activeToken`
|
|
78
|
+
* is planted for a non-pinned account (from a sync response or a pushed
|
|
79
|
+
* `session_state`), the `TokenTransport` (whose entire job is converging on
|
|
80
|
+
* `activeAccountId`) is bypassed, and a push that switches the device does not
|
|
81
|
+
* trigger a token re-fetch. The pinned token's lifecycle belongs solely to the
|
|
82
|
+
* cold boot / re-mint lane, which mints it with an explicit `accountId`.
|
|
83
|
+
*/
|
|
84
|
+
getPinnedAccountId?: () => string | null;
|
|
69
85
|
}
|
|
70
86
|
type StateListener = (state: DeviceSessionState | null) => void;
|
|
71
87
|
export declare class SessionClient {
|
|
@@ -84,6 +100,13 @@ export declare class SessionClient {
|
|
|
84
100
|
private readonly boundServerEvents;
|
|
85
101
|
constructor(host: SessionClientHost, options?: SessionClientOptions);
|
|
86
102
|
getState(): DeviceSessionState | null;
|
|
103
|
+
/**
|
|
104
|
+
* The account this client's bearer is pinned to, or `null` when it follows the
|
|
105
|
+
* device's active account (the default). Resolvers are expected to be a plain
|
|
106
|
+
* synchronous read of already-resolved state (see
|
|
107
|
+
* {@link SessionClientOptions.getPinnedAccountId}).
|
|
108
|
+
*/
|
|
109
|
+
private pinnedAccountId;
|
|
87
110
|
subscribe(listener: StateListener): () => void;
|
|
88
111
|
/**
|
|
89
112
|
* Subscribe to a named server-pushed Socket.IO event (e.g. `civic:attested`).
|
|
@@ -138,6 +161,11 @@ export declare class SessionClient {
|
|
|
138
161
|
* sign-in. This adds, then switches to the target so the UI lands on the
|
|
139
162
|
* account the user just authenticated.
|
|
140
163
|
*
|
|
164
|
+
* An identity-bound (pinned) client only ADDS: its own session is minted for
|
|
165
|
+
* the pinned account explicitly, so switching the device would gratuitously
|
|
166
|
+
* re-elect the active account under every OTHER app on this device — a
|
|
167
|
+
* mutation a pinned client must never make.
|
|
168
|
+
*
|
|
141
169
|
* @param accountId - The signed-in account id (e.g. `session.user.id`). When
|
|
142
170
|
* omitted, falls back to the host's current-account ref. If neither
|
|
143
171
|
* resolves, the add still applies and no switch is performed.
|
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
* - the "Sign in with Oxy" device flow (same-device shared-keychain via
|
|
22
22
|
* `oxyServices.signInWithSharedIdentity`, else the cross-device QR handoff
|
|
23
23
|
* via `startCommonsSignIn` → poll → `claimSessionByToken`);
|
|
24
|
+
* - AUTOMATIC delivery selection for that flow (issue #691): the user presses
|
|
25
|
+
* ONE primary action and the controller — not the user — picks how the
|
|
26
|
+
* request reaches their Commons identity, by gathering the facts
|
|
27
|
+
* (`platform`, `commonsAvailability`, and the `targets` a bearer-authorized
|
|
28
|
+
* `deliverCommonsSignIn` reached) and handing them to the pure
|
|
29
|
+
* `selectCommonsDelivery`. Exactly one route is primary; alternatives are
|
|
30
|
+
* never chained behind it, they are state (`signIn.route`,
|
|
31
|
+
* `signIn.routeFailed`) the UI reveals on its own terms;
|
|
32
|
+
* - honest, non-sensitive PROGRESS for that flow (`signIn.progress`), derived
|
|
33
|
+
* only from real signals — the chosen route, `pushSentAt`, `openedAt`,
|
|
34
|
+
* `authorized`, and the commit — never from an optimistic timeline;
|
|
24
35
|
* - `commonsAvailability` — whether Commons is installed on this device
|
|
25
36
|
* (native only, via the injected `canOpenApp` probe), so the QR view can
|
|
26
37
|
* offer a "Get Commons" fallback instead of a same-device dead end.
|
|
@@ -48,6 +59,7 @@ import type { SessionLoginResponse, MinimalUserData } from '../models/session';
|
|
|
48
59
|
import type { SessionClient } from './SessionClient';
|
|
49
60
|
import type { SocketIOFactory } from './socketLoader';
|
|
50
61
|
import { type SwitchableAccount } from './accountProjection';
|
|
62
|
+
import { type CommonsDeliveryPlatform, type CommonsDeliveryRoute } from '../utils/commonsDelivery';
|
|
51
63
|
/** The dialog's top-level view. */
|
|
52
64
|
export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add' | 'signup';
|
|
53
65
|
/**
|
|
@@ -62,8 +74,40 @@ export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add' | 'signup';
|
|
|
62
74
|
* (native only). A probe error is treated as `'unavailable'` (fail-closed).
|
|
63
75
|
*/
|
|
64
76
|
export type CommonsAvailability = 'unknown' | 'checking' | 'available' | 'unavailable';
|
|
65
|
-
/**
|
|
66
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Lifecycle phase of the "Sign in with Oxy" device flow — the RESOURCE state
|
|
79
|
+
* (is there a live request, a claim in flight, a terminal outcome), as opposed
|
|
80
|
+
* to {@link SignInProgress}, which is what the user is told.
|
|
81
|
+
*
|
|
82
|
+
* `'completed'` is terminal-but-successful: the session was claimed and
|
|
83
|
+
* committed, nothing is in flight, and the surface may show "Identity
|
|
84
|
+
* confirmed" before it goes away. It is cleared back to `'idle'` the moment the
|
|
85
|
+
* dialog moves to another view (a new intention), so a later sign-in entry can
|
|
86
|
+
* never inherit the previous flow's terminal state.
|
|
87
|
+
*/
|
|
88
|
+
export type SignInFlowPhase = 'idle' | 'starting' | 'waiting' | 'authorized' | 'completed' | 'error';
|
|
89
|
+
/**
|
|
90
|
+
* Ordered, non-sensitive progress of the active request — the ONE thing the
|
|
91
|
+
* sign-in surface renders its status line from (issue #691, Phase 5).
|
|
92
|
+
*
|
|
93
|
+
* Every step is DERIVED from a real fact the controller observed; there is no
|
|
94
|
+
* optimistic sequence and no timer that advances it. The mapping the UI is
|
|
95
|
+
* expected to render (copy lives in the UI layer, never here):
|
|
96
|
+
*
|
|
97
|
+
* | value | shown as | advanced by |
|
|
98
|
+
* |------------------------|--------------------------------|-------------|
|
|
99
|
+
* | `idle` | (nothing) | no live request, or a terminal error |
|
|
100
|
+
* | `preparing` | "Preparing request" | the request is being created / its route is still being resolved |
|
|
101
|
+
* | `awaiting-approval` | route-specific waiting copy | the primary route is known (`qr` → the QR itself, `open-commons` → "Continue in Commons") |
|
|
102
|
+
* | `delivered-to-commons` | "Check Commons on your phone" | the server confirmed a push to ≥1 known Commons install (`await-push`, or `pushSentAt`) |
|
|
103
|
+
* | `opened-in-commons` | "Opened in Commons" | the approver reported `openedAt` |
|
|
104
|
+
* | `confirming-identity` | "Confirming identity" | the request reported `authorized`; the claim/commit is running |
|
|
105
|
+
* | `identity-confirmed` | "Identity confirmed" | the session was claimed and committed |
|
|
106
|
+
*
|
|
107
|
+
* `opened-in-commons` and `delivered-to-commons` are PROGRESS only — neither is
|
|
108
|
+
* evidence of an approval. Only `authorized` moves the flow forward.
|
|
109
|
+
*/
|
|
110
|
+
export type SignInProgress = 'idle' | 'preparing' | 'awaiting-approval' | 'delivered-to-commons' | 'opened-in-commons' | 'confirming-identity' | 'identity-confirmed';
|
|
67
111
|
/**
|
|
68
112
|
* Minimal structural handle over a popup `Window` — just enough for the
|
|
69
113
|
* cross-origin passkey hub flow ({@link AccountDialogController.startPasskeyHubSignIn}):
|
|
@@ -79,7 +123,14 @@ export interface PopupWindowHandle {
|
|
|
79
123
|
href: string;
|
|
80
124
|
};
|
|
81
125
|
}
|
|
82
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* State of the "Sign in with Oxy" (shared-key / QR) device flow.
|
|
128
|
+
*
|
|
129
|
+
* Everything here is safe to render. The flow's SECRET credential (the
|
|
130
|
+
* device-flow `sessionToken`) is deliberately absent — it never leaves the
|
|
131
|
+
* controller's private field, so no surface, log, or serialized snapshot can
|
|
132
|
+
* leak it.
|
|
133
|
+
*/
|
|
83
134
|
export interface SignInFlowState {
|
|
84
135
|
phase: SignInFlowPhase;
|
|
85
136
|
/**
|
|
@@ -96,6 +147,46 @@ export interface SignInFlowState {
|
|
|
96
147
|
expiresAt: number | null;
|
|
97
148
|
/** Human-readable error for the retry UI, or `null`. */
|
|
98
149
|
error: string | null;
|
|
150
|
+
/**
|
|
151
|
+
* The ONE primary delivery route the controller chose for this request
|
|
152
|
+
* ({@link selectCommonsDelivery}), or `null` while it is still being resolved.
|
|
153
|
+
*
|
|
154
|
+
* There is no chain: exactly one route is primary, and the UI renders exactly
|
|
155
|
+
* one action for it. Alternatives stay behind a "Having trouble?" affordance
|
|
156
|
+
* that the UI reveals on its own terms — see {@link routeFailed}.
|
|
157
|
+
*/
|
|
158
|
+
route: CommonsDeliveryRoute | null;
|
|
159
|
+
/**
|
|
160
|
+
* `true` when the chosen primary route could NOT be carried out on this
|
|
161
|
+
* device — today only `'open-commons'` can fail this way (no URL opener was
|
|
162
|
+
* injected, or opening the verified Commons link threw). It is the signal the
|
|
163
|
+
* UI needs to reveal its alternatives; the controller never cascades to
|
|
164
|
+
* another route by itself.
|
|
165
|
+
*
|
|
166
|
+
* A push that reached zero installations is NOT a failure: no capable Commons
|
|
167
|
+
* install is a normal outcome that simply resolves the primary route to
|
|
168
|
+
* `'qr'`.
|
|
169
|
+
*/
|
|
170
|
+
routeFailed: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* Server-reported ISO-8601 timestamp of when the request was pushed to a
|
|
173
|
+
* known Commons installation, or `null`. Progress only — never evidence of an
|
|
174
|
+
* approval. Monotone: once observed it is never cleared by a later, emptier
|
|
175
|
+
* poll response.
|
|
176
|
+
*/
|
|
177
|
+
pushSentAt: string | null;
|
|
178
|
+
/**
|
|
179
|
+
* Server-reported ISO-8601 timestamp of when the approval route was OPENED in
|
|
180
|
+
* Commons, or `null`. Progress only — never evidence of an approval. Monotone,
|
|
181
|
+
* like {@link pushSentAt}.
|
|
182
|
+
*/
|
|
183
|
+
openedAt: string | null;
|
|
184
|
+
/**
|
|
185
|
+
* The derived, ordered progress the surface renders. Always computed from the
|
|
186
|
+
* facts above by {@link deriveSignInProgress} — never assigned directly, so it
|
|
187
|
+
* cannot drift from them or run ahead of a real signal.
|
|
188
|
+
*/
|
|
189
|
+
progress: SignInProgress;
|
|
99
190
|
}
|
|
100
191
|
/** Immutable snapshot consumed by `useSyncExternalStore`. */
|
|
101
192
|
export interface AccountDialogSnapshot {
|
|
@@ -208,6 +299,18 @@ export interface AccountDialogControllerOptions {
|
|
|
208
299
|
* `https://auth.${CENTRAL_IDP_APEX}`). Overridable for local/staging testing.
|
|
209
300
|
*/
|
|
210
301
|
hubBaseUrl?: string;
|
|
302
|
+
/**
|
|
303
|
+
* Which surface the sign-in is initiated from — a FACT supplied by the
|
|
304
|
+
* consumer, because only the consumer can classify its own environment
|
|
305
|
+
* (native → `'mobile'`; web → `'mobile'` for a mobile browser, `'desktop'`
|
|
306
|
+
* otherwise). Headless core never sniffs a user agent or a platform global.
|
|
307
|
+
*
|
|
308
|
+
* Feeds {@link selectCommonsDelivery} verbatim. Defaults to `'unknown'`,
|
|
309
|
+
* which is a first-class value there: an unclassified surface never opts into
|
|
310
|
+
* the deep-link route, because a custom-scheme navigation that does not
|
|
311
|
+
* resolve is a dead end with no automatic way back.
|
|
312
|
+
*/
|
|
313
|
+
platform?: CommonsDeliveryPlatform;
|
|
211
314
|
}
|
|
212
315
|
type SnapshotListener = (snapshot: AccountDialogSnapshot) => void;
|
|
213
316
|
export declare class AccountDialogController {
|
|
@@ -224,6 +327,7 @@ export declare class AccountDialogController {
|
|
|
224
327
|
private readonly socketFactory?;
|
|
225
328
|
private readonly openPopup?;
|
|
226
329
|
private readonly hubBaseUrl;
|
|
330
|
+
private readonly platform;
|
|
227
331
|
private readonly listeners;
|
|
228
332
|
private view;
|
|
229
333
|
private graph;
|
|
@@ -375,8 +479,57 @@ export declare class AccountDialogController {
|
|
|
375
479
|
* same `startCommonsSignIn` → poll/socket wiring either way. Returns the
|
|
376
480
|
* handle on success (already reflected in `signIn`), or `null` on failure
|
|
377
481
|
* (already set as `signIn.error`).
|
|
482
|
+
*
|
|
483
|
+
* @param opts.deliver - Whether to run automatic Commons delivery selection
|
|
484
|
+
* ({@link resolveDeliveryRoute}). `true` for the normal one-primary-action
|
|
485
|
+
* entry; `false` when the caller already owns the primary surface (the
|
|
486
|
+
* passkey hub popup), where the request's Commons route is simply the QR.
|
|
378
487
|
*/
|
|
379
488
|
private startDeviceFlowSession;
|
|
489
|
+
/**
|
|
490
|
+
* Choose and carry out the ONE primary delivery route for the active request
|
|
491
|
+
* (issue #691, "Automatic delivery selection").
|
|
492
|
+
*
|
|
493
|
+
* The controller gathers the facts — is a verified Commons link openable on
|
|
494
|
+
* THIS device, and how many known Commons installations did Oxy actually push
|
|
495
|
+
* to — and hands them to the pure {@link selectCommonsDelivery}. The selector
|
|
496
|
+
* owns the decision; this method owns only the observations and the single
|
|
497
|
+
* action the chosen route implies. It never cascades: a route that yields
|
|
498
|
+
* nothing resolves to QR *before* a route is chosen, never after.
|
|
499
|
+
*/
|
|
500
|
+
private resolveDeliveryRoute;
|
|
501
|
+
/**
|
|
502
|
+
* Whether `sessionToken` is still THE request this surface is waiting on.
|
|
503
|
+
*
|
|
504
|
+
* Guards every step of the asynchronous route resolution: a cancelled,
|
|
505
|
+
* superseded, failed, or already-approved flow must neither open Commons nor
|
|
506
|
+
* mutate the surface — an approval that lands mid-resolution would otherwise
|
|
507
|
+
* be followed by a pointless app switch.
|
|
508
|
+
*/
|
|
509
|
+
private isAwaitingApproval;
|
|
510
|
+
/**
|
|
511
|
+
* Ask Oxy to deliver the pending request to the identity's known Commons
|
|
512
|
+
* installations, returning how many it reached (`0` when delivery is not
|
|
513
|
+
* applicable, not permitted, or reached nobody).
|
|
514
|
+
*
|
|
515
|
+
* Two hard rules, both from the issue:
|
|
516
|
+
* - **Never push from an unauthenticated surface.** `deliverCommonsSignIn`
|
|
517
|
+
* is bearer-required precisely because a request that merely carries a
|
|
518
|
+
* typed-in username must never be able to ring somebody's phone. Without a
|
|
519
|
+
* planted bearer the call is not made AT ALL — not made-and-failed.
|
|
520
|
+
* - **Zero targets and a failed delivery are the same normal outcome.** Both
|
|
521
|
+
* return `0`, which resolves the primary route to QR silently. Neither is
|
|
522
|
+
* surfaced as an error: there is nothing the user did wrong and nothing for
|
|
523
|
+
* them to fix.
|
|
524
|
+
*/
|
|
525
|
+
private deliverToKnownCommons;
|
|
526
|
+
/**
|
|
527
|
+
* Open the verified Commons link for the `'open-commons'` route. Returns
|
|
528
|
+
* whether the navigation was actually handed off, so a missing opener or a
|
|
529
|
+
* throwing one becomes an observable `routeFailed` instead of a silent dead
|
|
530
|
+
* end the user stares at.
|
|
531
|
+
*/
|
|
532
|
+
private openCommonsLink;
|
|
380
533
|
/**
|
|
381
534
|
* Poll {@link PopupWindowHandle.closed} so a user who dismisses the hub
|
|
382
535
|
* popup without completing sign-in gets prompt feedback (there is no DOM
|
|
@@ -402,14 +555,22 @@ export declare class AccountDialogController {
|
|
|
402
555
|
*/
|
|
403
556
|
private resolveCommonsAvailability;
|
|
404
557
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
558
|
+
* Tear down the active sign-in device flow (timers + socket + popup + token),
|
|
559
|
+
* WITHDRAW the request server-side, and reset to idle.
|
|
560
|
+
*
|
|
561
|
+
* Cancellation has to converge in both directions: the surface closing must
|
|
562
|
+
* cancel the request, not just stop listening to it. Without the withdrawal a
|
|
563
|
+
* dismissed QR would stay approvable until it expired, so a later scan of a
|
|
564
|
+
* stale code could authorize a session nobody is waiting for.
|
|
409
565
|
*/
|
|
410
|
-
private deepLinkIntoCommonsIfAvailable;
|
|
411
|
-
/** Tear down the active sign-in device flow (timers + socket + popup + token) and reset to idle. */
|
|
412
566
|
cancelSignIn(): void;
|
|
567
|
+
/**
|
|
568
|
+
* Best-effort server-side withdrawal of a request this surface abandoned
|
|
569
|
+
* (`POST /auth/session/deny/:authorizeCode`). Fire-and-forget by design: the
|
|
570
|
+
* local teardown already happened, and a race with an approval that just
|
|
571
|
+
* landed legitimately rejects here — neither outcome is worth surfacing.
|
|
572
|
+
*/
|
|
573
|
+
private withdrawRequest;
|
|
413
574
|
private scheduleNextPoll;
|
|
414
575
|
/**
|
|
415
576
|
* Run one status check + (on approval) claim. Triggered by the fallback timer
|
|
@@ -419,6 +580,15 @@ export declare class AccountDialogController {
|
|
|
419
580
|
* re-checks the authoritative status via `pollCommonsSignIn`.
|
|
420
581
|
*/
|
|
421
582
|
private pollOnce;
|
|
583
|
+
/**
|
|
584
|
+
* Record server-reported delivery progress on the active flow.
|
|
585
|
+
*
|
|
586
|
+
* Monotone and additive: a timestamp is only ever adopted, never replaced or
|
|
587
|
+
* cleared, so an older API build (or a partial payload) that omits a field can
|
|
588
|
+
* at most fail to advance the surface — it can never walk it backwards.
|
|
589
|
+
* Emits only on a real change, so a steady poll does not churn the snapshot.
|
|
590
|
+
*/
|
|
591
|
+
private recordDeliveryProgress;
|
|
422
592
|
private claimAndComplete;
|
|
423
593
|
/**
|
|
424
594
|
* Commit an authorized session, notify, and return to the account list. Shared
|
|
@@ -454,7 +624,15 @@ export declare class AccountDialogController {
|
|
|
454
624
|
private openAuthSessionSocket;
|
|
455
625
|
/** Tear down the `/auth-session` socket, if any. Idempotent. */
|
|
456
626
|
private closeAuthSessionSocket;
|
|
627
|
+
/**
|
|
628
|
+
* Replace the device-flow state from its FACTS, re-deriving `progress`. The
|
|
629
|
+
* only writer of `this.signIn` besides the two terminal constants — which is
|
|
630
|
+
* what keeps `progress` impossible to set by hand, and therefore impossible
|
|
631
|
+
* to advance without a fact behind it.
|
|
632
|
+
*/
|
|
457
633
|
private setSignIn;
|
|
634
|
+
/** Update a subset of the device-flow facts, re-deriving `progress`. */
|
|
635
|
+
private patchSignIn;
|
|
458
636
|
private computeSnapshot;
|
|
459
637
|
/** Recompute the snapshot and notify subscribers. */
|
|
460
638
|
private emit;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity pin — the durable binding between THIS device's PRIMARY identity key
|
|
3
|
+
* and the account that key authenticates as.
|
|
4
|
+
*
|
|
5
|
+
* An identity-bound client (Commons, the identity vault) must always act as the
|
|
6
|
+
* owner of the local signing key — permanently. The `DeviceSession` it shares
|
|
7
|
+
* with every other Oxy app on the same device tracks a MUTABLE `activeAccountId`
|
|
8
|
+
* that any sibling app can switch; following it silently changes both the user
|
|
9
|
+
* such a client renders and the bearer it sends. The pin is the client-side
|
|
10
|
+
* record that breaks that coupling: `{ publicKey, accountId }`, written when an
|
|
11
|
+
* identity-mode session is established (the only moment both facts are known
|
|
12
|
+
* first-hand), read on every boot, and reconciled against the live
|
|
13
|
+
* `KeyManager.getPublicKey()` before it is trusted (see
|
|
14
|
+
* `resolveIdentityPin` in `identitySession.ts`).
|
|
15
|
+
*
|
|
16
|
+
* It holds NO secret — a public key and an account id, both already known to the
|
|
17
|
+
* server — so it lives in the same per-origin storage as {@link PersistedAuthState}
|
|
18
|
+
* (`authStateStore.ts`) rather than the keychain, and mirrors that module's
|
|
19
|
+
* store shape: `load / save / clear`, an in-memory mirror, a read-back-verified
|
|
20
|
+
* durable write, and graceful degradation when storage is unavailable.
|
|
21
|
+
*
|
|
22
|
+
* ESM-safe (no `require()`); no react/react-native/expo imports.
|
|
23
|
+
*/
|
|
24
|
+
import type { NativeKeyValueStorage } from './authStateStore';
|
|
25
|
+
/**
|
|
26
|
+
* The persisted identity binding.
|
|
27
|
+
*
|
|
28
|
+
* `publicKey` is the device's PRIMARY identity public key (lower-case hex, as
|
|
29
|
+
* `KeyManager` stores it) — never the cross-app shared-slot key, which may hold
|
|
30
|
+
* a different identity. `accountId` is the account the server resolved for that
|
|
31
|
+
* key when the session was established.
|
|
32
|
+
*/
|
|
33
|
+
export interface IdentityPin {
|
|
34
|
+
publicKey: string;
|
|
35
|
+
accountId: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The storage seam for {@link IdentityPin}. Async throughout so one interface
|
|
39
|
+
* fits both synchronous web `localStorage` and asynchronous native
|
|
40
|
+
* SecureStore/AsyncStorage.
|
|
41
|
+
*/
|
|
42
|
+
export interface IdentityPinStore {
|
|
43
|
+
load(): Promise<IdentityPin | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Persist the pin and report whether it durably landed.
|
|
46
|
+
*
|
|
47
|
+
* `true` means the value is retained consistent with this store's durability
|
|
48
|
+
* guarantee (a durable backing whose read-back matched, or a degraded/in-memory
|
|
49
|
+
* store that held it in memory). `false` means a DURABLE backing was expected
|
|
50
|
+
* but the write did not land — the in-memory mirror keeps this process pinned,
|
|
51
|
+
* but the next cold boot will have to re-establish the identity session (which
|
|
52
|
+
* rewrites the pin) instead of taking the fast pinned-mint lane.
|
|
53
|
+
*/
|
|
54
|
+
save(pin: IdentityPin): Promise<boolean>;
|
|
55
|
+
clear(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Versioned storage key. Deliberately distinct from the `oxy.auth.*` keys so a
|
|
59
|
+
* session sign-out (which clears the auth blob) and an identity change (which
|
|
60
|
+
* clears the pin) can never take each other down.
|
|
61
|
+
*/
|
|
62
|
+
export declare const IDENTITY_PIN_STORAGE_KEY = "oxy.identity.pin.v1";
|
|
63
|
+
/**
|
|
64
|
+
* Whether `pin` still describes the identity currently on this device.
|
|
65
|
+
*
|
|
66
|
+
* `publicKey` comparison is case-insensitive (hex). A `null` local key is a
|
|
67
|
+
* definitive "no identity here" and therefore never a match — the caller clears
|
|
68
|
+
* the pin rather than binding to an account whose key is gone.
|
|
69
|
+
*/
|
|
70
|
+
export declare function identityPinMatches(pin: IdentityPin | null, localPublicKey: string | null): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* A process-lifetime, in-memory {@link IdentityPinStore}. Used directly for
|
|
73
|
+
* tests/SSR and as the degraded fallback of the web store.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createMemoryIdentityPinStore(): IdentityPinStore;
|
|
76
|
+
/**
|
|
77
|
+
* A `localStorage`-backed {@link IdentityPinStore}. Degrades to an in-memory
|
|
78
|
+
* store when `localStorage` is unreachable, and keeps an in-memory mirror so a
|
|
79
|
+
* failed persist still pins THIS page's lifetime.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createWebIdentityPinStore(): IdentityPinStore;
|
|
82
|
+
/**
|
|
83
|
+
* A native {@link IdentityPinStore} over an injected async key/value store —
|
|
84
|
+
* the same seam `createNativeAuthStateStore` uses, so `@oxyhq/core` never
|
|
85
|
+
* imports `expo-secure-store`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function createNativeIdentityPinStore(storage: NativeKeyValueStorage): IdentityPinStore;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity-bound session — pin reconciliation + establishment.
|
|
3
|
+
*
|
|
4
|
+
* This is the SDK half of "an identity vault's authenticated user is whoever
|
|
5
|
+
* owns the local primary key". Two operations, both consumed by the cold boot
|
|
6
|
+
* (`boot/sessionColdBoot.ts`) and the re-mint lane (`session/refresh.ts`):
|
|
7
|
+
*
|
|
8
|
+
* - {@link resolveIdentityPin} — read the persisted `{publicKey, accountId}`
|
|
9
|
+
* pin and reconcile it against the identity currently on this device. A
|
|
10
|
+
* definitive mismatch (the key was replaced, or is gone) CLEARS the pin; an
|
|
11
|
+
* INDETERMINATE read (keychain locked / storage threw) leaves it untouched.
|
|
12
|
+
* - {@link establishIdentitySession} — mint a session from the PRIMARY local
|
|
13
|
+
* key (`getPublicKey` → `requestChallenge` → `signChallenge` →
|
|
14
|
+
* `verifyChallenge`), persist the resulting device credential, and write the
|
|
15
|
+
* pin. This is the identity-mode replacement for the shared-keychain lane,
|
|
16
|
+
* which reads the CROSS-APP shared slot and may therefore hold a different
|
|
17
|
+
* identity than this device's primary.
|
|
18
|
+
*
|
|
19
|
+
* Both take their key/signature functions from an injectable {@link IdentityBinding}
|
|
20
|
+
* (defaulting to `KeyManager` / `SignatureService`) so the lanes above stay
|
|
21
|
+
* testable without a keychain.
|
|
22
|
+
*
|
|
23
|
+
* ESM-safe (no `require()`); no react/react-native/expo imports.
|
|
24
|
+
*/
|
|
25
|
+
import type { OxyServices } from '../OxyServices';
|
|
26
|
+
import type { AuthStateStore } from './authStateStore';
|
|
27
|
+
import type { SessionLoginResponse } from '../models/session';
|
|
28
|
+
import { type AuthChallenge } from '../crypto/signatureService';
|
|
29
|
+
import { type IdentityPin, type IdentityPinStore } from './identityPin';
|
|
30
|
+
/** Per-call transport overrides forwarded to the challenge/verify round-trips. */
|
|
31
|
+
export interface IdentityRequestOptions {
|
|
32
|
+
retry?: boolean;
|
|
33
|
+
timeout?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Everything an identity-bound client needs to resolve and re-establish its
|
|
37
|
+
* session. `@oxyhq/services` builds one of these (platform-appropriate pin
|
|
38
|
+
* store) and passes it to the cold boot and the refresh handler.
|
|
39
|
+
*/
|
|
40
|
+
export interface IdentityBinding {
|
|
41
|
+
/** Where the `{publicKey, accountId}` pin is persisted. */
|
|
42
|
+
pinStore: IdentityPinStore;
|
|
43
|
+
/**
|
|
44
|
+
* Reads this device's PRIMARY identity public key. Defaults to
|
|
45
|
+
* `KeyManager.getPublicKey()`. NEVER the shared-slot key.
|
|
46
|
+
*/
|
|
47
|
+
readPublicKey?: () => Promise<string | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Signs a server challenge with the PRIMARY local private key. Defaults to
|
|
50
|
+
* `SignatureService.signChallenge`.
|
|
51
|
+
*/
|
|
52
|
+
signChallenge?: (challenge: string) => Promise<AuthChallenge>;
|
|
53
|
+
/** Optional device labels forwarded to `verifyChallenge`. */
|
|
54
|
+
deviceName?: string;
|
|
55
|
+
deviceFingerprint?: string;
|
|
56
|
+
}
|
|
57
|
+
/** The result of a successful {@link establishIdentitySession}. */
|
|
58
|
+
export interface EstablishedIdentitySession {
|
|
59
|
+
session: SessionLoginResponse;
|
|
60
|
+
/** The pin as written: the local key plus the account the server resolved for it. */
|
|
61
|
+
pin: IdentityPin;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read the persisted pin and reconcile it against the identity on this device.
|
|
65
|
+
*
|
|
66
|
+
* Returns the pin ONLY when the local primary public key still matches it — that
|
|
67
|
+
* is the single condition under which a client may bind its token and its
|
|
68
|
+
* rendered user to that account.
|
|
69
|
+
*
|
|
70
|
+
* Outcomes:
|
|
71
|
+
* - no pin stored → `null` (nothing to clear).
|
|
72
|
+
* - local key MATCHES → the pin (trusted).
|
|
73
|
+
* - local key read succeeded and differs / is absent → the identity was
|
|
74
|
+
* replaced or lost: CLEAR the pin and return `null`. Keeping it would pin the
|
|
75
|
+
* client to an account it can no longer prove control of.
|
|
76
|
+
* - the local key read THREW (keychain locked, storage unavailable) → return
|
|
77
|
+
* `null` WITHOUT clearing. A read that produced no verdict is never evidence
|
|
78
|
+
* of an identity change (the same rule `KeyManager` applies to its own
|
|
79
|
+
* transient errors); the caller falls through to the identity sign-in lane,
|
|
80
|
+
* which fails closed on a locked keychain rather than adopting a foreign
|
|
81
|
+
* account.
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveIdentityPin(binding: IdentityBinding): Promise<IdentityPin | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Establish a session for the device's PRIMARY identity key and pin it.
|
|
86
|
+
*
|
|
87
|
+
* `requestChallenge` → `signChallenge` → `verifyChallenge`; `verifyChallenge`
|
|
88
|
+
* plants the access token itself. The server resolves the account from the
|
|
89
|
+
* VERIFIED SIGNER, so the returned session is identity-authoritative — it
|
|
90
|
+
* ignores the device's `activeAccountId` entirely.
|
|
91
|
+
*
|
|
92
|
+
* Side effects on success: the durable device credential is persisted (so the
|
|
93
|
+
* next boot can take the fast pinned-mint lane) and the pin is written.
|
|
94
|
+
*
|
|
95
|
+
* Returns `null` — never throws — for the two "nothing to do" cases: no local
|
|
96
|
+
* identity (including every web caller, where `KeyManager` has no key), and a
|
|
97
|
+
* verify that produced no access token. Network/crypto failures propagate so the
|
|
98
|
+
* caller can classify them.
|
|
99
|
+
*/
|
|
100
|
+
export declare function establishIdentitySession(args: {
|
|
101
|
+
oxy: OxyServices;
|
|
102
|
+
store: AuthStateStore;
|
|
103
|
+
binding: IdentityBinding;
|
|
104
|
+
requestOptions?: IdentityRequestOptions;
|
|
105
|
+
}): Promise<EstablishedIdentitySession | null>;
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import type { DeviceSessionState } from '@oxyhq/contracts';
|
|
2
2
|
import type { ClientSession } from '../models/session';
|
|
3
3
|
import type { User } from '../models/interfaces';
|
|
4
|
-
/**
|
|
5
|
-
* Pure projection helpers: `DeviceSessionState` (the device-scoped
|
|
6
|
-
* multi-account session-sync state produced by `SessionClient`) -> the
|
|
7
|
-
* shapes `@oxyhq/services` consumers render today
|
|
8
|
-
* (`ClientSession[]`, an active session id, an active `User`).
|
|
9
|
-
*
|
|
10
|
-
* No I/O. The caller fetches profiles via
|
|
11
|
-
* `oxyServices.getUsersByIds(accountIdsOf(state))` and builds `usersById`
|
|
12
|
-
* from the result before calling `deviceStateToClientSessions` /
|
|
13
|
-
* `activeUserOf`.
|
|
14
|
-
*/
|
|
15
4
|
/**
|
|
16
5
|
* Maps every `SessionAccount` in `state.accounts` to a `ClientSession`.
|
|
17
6
|
*
|
|
@@ -23,19 +12,32 @@ import type { User } from '../models/interfaces';
|
|
|
23
12
|
* though `ClientSession` only stores `userId` — a session is still
|
|
24
13
|
* projected for an account whose id is absent from `usersById` (no
|
|
25
14
|
* placeholder user is fabricated).
|
|
15
|
+
*
|
|
16
|
+
* `isCurrent` marks the PINNED account when one is supplied, so it can never
|
|
17
|
+
* disagree with {@link activeSessionIdOf} / {@link activeUserOf} for the same
|
|
18
|
+
* pin.
|
|
26
19
|
*/
|
|
27
|
-
export declare function deviceStateToClientSessions(state: DeviceSessionState, usersById: Map<string, User
|
|
20
|
+
export declare function deviceStateToClientSessions(state: DeviceSessionState, usersById: Map<string, User>, pinnedAccountId?: string | null): ClientSession[];
|
|
28
21
|
/**
|
|
29
|
-
* The
|
|
30
|
-
*
|
|
22
|
+
* The bound account's `sessionId`, or `null` when there is no state, no bound
|
|
23
|
+
* account, or that account has no session on this device.
|
|
24
|
+
*
|
|
25
|
+
* A pinned account that is ABSENT from `state.accounts` yields `null` — the
|
|
26
|
+
* honest signal that this device no longer carries a session for the pinned
|
|
27
|
+
* identity, which the caller answers by re-establishing the identity session
|
|
28
|
+
* (never by adopting the device's active account).
|
|
31
29
|
*/
|
|
32
|
-
export declare function activeSessionIdOf(state: DeviceSessionState | null): string | null;
|
|
30
|
+
export declare function activeSessionIdOf(state: DeviceSessionState | null, pinnedAccountId?: string | null): string | null;
|
|
33
31
|
/**
|
|
34
|
-
* The
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* The bound account's `User`, resolved from `usersById`. `null` when there is no
|
|
33
|
+
* state, no bound account, or the bound account id is absent from `usersById`.
|
|
34
|
+
*
|
|
35
|
+
* A pinned user is resolved from `usersById` alone — deliberately NOT gated on
|
|
36
|
+
* device membership — so a transient device-state gap cannot flicker the
|
|
37
|
+
* identity vault's rendered user. Whether the pinned session still exists on the
|
|
38
|
+
* device is answered by {@link activeSessionIdOf}.
|
|
37
39
|
*/
|
|
38
|
-
export declare function activeUserOf(state: DeviceSessionState | null, usersById: Map<string, User
|
|
40
|
+
export declare function activeUserOf(state: DeviceSessionState | null, usersById: Map<string, User>, pinnedAccountId?: string | null): User | null;
|
|
39
41
|
/**
|
|
40
42
|
* All account ids in `state`, suitable for an `oxyServices.getUsersByIds(...)`
|
|
41
43
|
* fetch. `[]` for `null` state.
|