@oxyhq/core 12.10.6 → 12.11.1
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 +66 -6
- 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/OxyServices.privacy.js +6 -0
- package/dist/cjs/mixins/OxyServices.user.js +1 -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/HttpService.js +66 -6
- 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/OxyServices.privacy.js +6 -0
- package/dist/esm/mixins/OxyServices.user.js +1 -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/HttpService.d.ts +18 -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/OxyServices.user.d.ts +2 -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 +3 -3
- package/src/HttpService.ts +67 -6
- package/src/__tests__/inSessionRefresh.test.ts +67 -0
- 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/OxyServices.privacy.ts +6 -0
- package/src/mixins/OxyServices.user.ts +3 -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/__tests__/privacyCacheInvalidation.test.ts +2 -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
|
@@ -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.
|
|
@@ -59,6 +70,12 @@ import {
|
|
|
59
70
|
} from './accountProjection';
|
|
60
71
|
import type { AccountNode } from '../mixins/OxyServices.accounts';
|
|
61
72
|
import type { CommonsSignInHandle } from '../mixins/OxyServices.auth';
|
|
73
|
+
import {
|
|
74
|
+
pushTargetsFromDelivery,
|
|
75
|
+
selectCommonsDelivery,
|
|
76
|
+
type CommonsDeliveryPlatform,
|
|
77
|
+
type CommonsDeliveryRoute,
|
|
78
|
+
} from '../utils/commonsDelivery';
|
|
62
79
|
|
|
63
80
|
/** The dialog's top-level view. */
|
|
64
81
|
export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add' | 'signup';
|
|
@@ -76,8 +93,54 @@ export type AccountDialogView = 'accounts' | 'signin' | 'qr' | 'add' | 'signup';
|
|
|
76
93
|
*/
|
|
77
94
|
export type CommonsAvailability = 'unknown' | 'checking' | 'available' | 'unavailable';
|
|
78
95
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Lifecycle phase of the "Sign in with Oxy" device flow — the RESOURCE state
|
|
98
|
+
* (is there a live request, a claim in flight, a terminal outcome), as opposed
|
|
99
|
+
* to {@link SignInProgress}, which is what the user is told.
|
|
100
|
+
*
|
|
101
|
+
* `'completed'` is terminal-but-successful: the session was claimed and
|
|
102
|
+
* committed, nothing is in flight, and the surface may show "Identity
|
|
103
|
+
* confirmed" before it goes away. It is cleared back to `'idle'` the moment the
|
|
104
|
+
* dialog moves to another view (a new intention), so a later sign-in entry can
|
|
105
|
+
* never inherit the previous flow's terminal state.
|
|
106
|
+
*/
|
|
107
|
+
export type SignInFlowPhase =
|
|
108
|
+
| 'idle'
|
|
109
|
+
| 'starting'
|
|
110
|
+
| 'waiting'
|
|
111
|
+
| 'authorized'
|
|
112
|
+
| 'completed'
|
|
113
|
+
| 'error';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ordered, non-sensitive progress of the active request — the ONE thing the
|
|
117
|
+
* sign-in surface renders its status line from (issue #691, Phase 5).
|
|
118
|
+
*
|
|
119
|
+
* Every step is DERIVED from a real fact the controller observed; there is no
|
|
120
|
+
* optimistic sequence and no timer that advances it. The mapping the UI is
|
|
121
|
+
* expected to render (copy lives in the UI layer, never here):
|
|
122
|
+
*
|
|
123
|
+
* | value | shown as | advanced by |
|
|
124
|
+
* |------------------------|--------------------------------|-------------|
|
|
125
|
+
* | `idle` | (nothing) | no live request, or a terminal error |
|
|
126
|
+
* | `preparing` | "Preparing request" | the request is being created / its route is still being resolved |
|
|
127
|
+
* | `awaiting-approval` | route-specific waiting copy | the primary route is known (`qr` → the QR itself, `open-commons` → "Continue in Commons") |
|
|
128
|
+
* | `delivered-to-commons` | "Check Commons on your phone" | the server confirmed a push to ≥1 known Commons install (`await-push`, or `pushSentAt`) |
|
|
129
|
+
* | `opened-in-commons` | "Opened in Commons" | the approver reported `openedAt` |
|
|
130
|
+
* | `confirming-identity` | "Confirming identity" | the request reported `authorized`; the claim/commit is running |
|
|
131
|
+
* | `identity-confirmed` | "Identity confirmed" | the session was claimed and committed |
|
|
132
|
+
*
|
|
133
|
+
* `opened-in-commons` and `delivered-to-commons` are PROGRESS only — neither is
|
|
134
|
+
* evidence of an approval. Only `authorized` moves the flow forward.
|
|
135
|
+
*/
|
|
136
|
+
export type SignInProgress =
|
|
137
|
+
| 'idle'
|
|
138
|
+
| 'preparing'
|
|
139
|
+
| 'awaiting-approval'
|
|
140
|
+
| 'delivered-to-commons'
|
|
141
|
+
| 'opened-in-commons'
|
|
142
|
+
| 'confirming-identity'
|
|
143
|
+
| 'identity-confirmed';
|
|
81
144
|
|
|
82
145
|
/**
|
|
83
146
|
* Minimal structural handle over a popup `Window` — just enough for the
|
|
@@ -93,7 +156,14 @@ export interface PopupWindowHandle {
|
|
|
93
156
|
location: { href: string };
|
|
94
157
|
}
|
|
95
158
|
|
|
96
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* State of the "Sign in with Oxy" (shared-key / QR) device flow.
|
|
161
|
+
*
|
|
162
|
+
* Everything here is safe to render. The flow's SECRET credential (the
|
|
163
|
+
* device-flow `sessionToken`) is deliberately absent — it never leaves the
|
|
164
|
+
* controller's private field, so no surface, log, or serialized snapshot can
|
|
165
|
+
* leak it.
|
|
166
|
+
*/
|
|
97
167
|
export interface SignInFlowState {
|
|
98
168
|
phase: SignInFlowPhase;
|
|
99
169
|
/**
|
|
@@ -110,6 +180,82 @@ export interface SignInFlowState {
|
|
|
110
180
|
expiresAt: number | null;
|
|
111
181
|
/** Human-readable error for the retry UI, or `null`. */
|
|
112
182
|
error: string | null;
|
|
183
|
+
/**
|
|
184
|
+
* The ONE primary delivery route the controller chose for this request
|
|
185
|
+
* ({@link selectCommonsDelivery}), or `null` while it is still being resolved.
|
|
186
|
+
*
|
|
187
|
+
* There is no chain: exactly one route is primary, and the UI renders exactly
|
|
188
|
+
* one action for it. Alternatives stay behind a "Having trouble?" affordance
|
|
189
|
+
* that the UI reveals on its own terms — see {@link routeFailed}.
|
|
190
|
+
*/
|
|
191
|
+
route: CommonsDeliveryRoute | null;
|
|
192
|
+
/**
|
|
193
|
+
* `true` when the chosen primary route could NOT be carried out on this
|
|
194
|
+
* device — today only `'open-commons'` can fail this way (no URL opener was
|
|
195
|
+
* injected, or opening the verified Commons link threw). It is the signal the
|
|
196
|
+
* UI needs to reveal its alternatives; the controller never cascades to
|
|
197
|
+
* another route by itself.
|
|
198
|
+
*
|
|
199
|
+
* A push that reached zero installations is NOT a failure: no capable Commons
|
|
200
|
+
* install is a normal outcome that simply resolves the primary route to
|
|
201
|
+
* `'qr'`.
|
|
202
|
+
*/
|
|
203
|
+
routeFailed: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Server-reported ISO-8601 timestamp of when the request was pushed to a
|
|
206
|
+
* known Commons installation, or `null`. Progress only — never evidence of an
|
|
207
|
+
* approval. Monotone: once observed it is never cleared by a later, emptier
|
|
208
|
+
* poll response.
|
|
209
|
+
*/
|
|
210
|
+
pushSentAt: string | null;
|
|
211
|
+
/**
|
|
212
|
+
* Server-reported ISO-8601 timestamp of when the approval route was OPENED in
|
|
213
|
+
* Commons, or `null`. Progress only — never evidence of an approval. Monotone,
|
|
214
|
+
* like {@link pushSentAt}.
|
|
215
|
+
*/
|
|
216
|
+
openedAt: string | null;
|
|
217
|
+
/**
|
|
218
|
+
* The derived, ordered progress the surface renders. Always computed from the
|
|
219
|
+
* facts above by {@link deriveSignInProgress} — never assigned directly, so it
|
|
220
|
+
* cannot drift from them or run ahead of a real signal.
|
|
221
|
+
*/
|
|
222
|
+
progress: SignInProgress;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The observable FACTS of a device flow — {@link SignInFlowState} minus the
|
|
227
|
+
* value derived from them. Every mutation of the flow goes through this shape,
|
|
228
|
+
* which is what makes `progress` structurally impossible to set by hand.
|
|
229
|
+
*/
|
|
230
|
+
type SignInFlowFacts = Omit<SignInFlowState, 'progress'>;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Derive the surface-facing progress from the flow's real facts. Pure, total,
|
|
234
|
+
* and the single place the ladder is defined.
|
|
235
|
+
*
|
|
236
|
+
* Ordering within `'waiting'` is most-specific-first, so a late-arriving weaker
|
|
237
|
+
* signal can never pull the display backwards.
|
|
238
|
+
*/
|
|
239
|
+
function deriveSignInProgress(facts: SignInFlowFacts): SignInProgress {
|
|
240
|
+
switch (facts.phase) {
|
|
241
|
+
case 'idle':
|
|
242
|
+
case 'error':
|
|
243
|
+
return 'idle';
|
|
244
|
+
case 'starting':
|
|
245
|
+
return 'preparing';
|
|
246
|
+
case 'authorized':
|
|
247
|
+
return 'confirming-identity';
|
|
248
|
+
case 'completed':
|
|
249
|
+
return 'identity-confirmed';
|
|
250
|
+
case 'waiting':
|
|
251
|
+
if (facts.openedAt !== null) return 'opened-in-commons';
|
|
252
|
+
// `route === 'await-push'` is itself a server-confirmed dispatch (the
|
|
253
|
+
// route is only chosen when `deliverCommonsSignIn` reported ≥1 target),
|
|
254
|
+
// so it is a real signal — not an optimistic assumption that a push will
|
|
255
|
+
// arrive. `pushSentAt` is the same fact re-confirmed by the status poll.
|
|
256
|
+
if (facts.pushSentAt !== null || facts.route === 'await-push') return 'delivered-to-commons';
|
|
257
|
+
return facts.route === null ? 'preparing' : 'awaiting-approval';
|
|
258
|
+
}
|
|
113
259
|
}
|
|
114
260
|
|
|
115
261
|
/** Immutable snapshot consumed by `useSyncExternalStore`. */
|
|
@@ -224,6 +370,18 @@ export interface AccountDialogControllerOptions {
|
|
|
224
370
|
* `https://auth.${CENTRAL_IDP_APEX}`). Overridable for local/staging testing.
|
|
225
371
|
*/
|
|
226
372
|
hubBaseUrl?: string;
|
|
373
|
+
/**
|
|
374
|
+
* Which surface the sign-in is initiated from — a FACT supplied by the
|
|
375
|
+
* consumer, because only the consumer can classify its own environment
|
|
376
|
+
* (native → `'mobile'`; web → `'mobile'` for a mobile browser, `'desktop'`
|
|
377
|
+
* otherwise). Headless core never sniffs a user agent or a platform global.
|
|
378
|
+
*
|
|
379
|
+
* Feeds {@link selectCommonsDelivery} verbatim. Defaults to `'unknown'`,
|
|
380
|
+
* which is a first-class value there: an unclassified surface never opts into
|
|
381
|
+
* the deep-link route, because a custom-scheme navigation that does not
|
|
382
|
+
* resolve is a dead end with no automatic way back.
|
|
383
|
+
*/
|
|
384
|
+
platform?: CommonsDeliveryPlatform;
|
|
227
385
|
}
|
|
228
386
|
|
|
229
387
|
/**
|
|
@@ -244,12 +402,32 @@ const AUTH_SESSION_NAMESPACE = '/auth-session';
|
|
|
244
402
|
*/
|
|
245
403
|
const COMMONS_APP_SCHEME = 'oxycommons://';
|
|
246
404
|
|
|
247
|
-
const
|
|
405
|
+
const IDLE_SIGN_IN_FACTS: SignInFlowFacts = {
|
|
248
406
|
phase: 'idle',
|
|
249
407
|
authorizeCode: null,
|
|
250
408
|
qrPayload: null,
|
|
251
409
|
expiresAt: null,
|
|
252
410
|
error: null,
|
|
411
|
+
route: null,
|
|
412
|
+
routeFailed: false,
|
|
413
|
+
pushSentAt: null,
|
|
414
|
+
openedAt: null,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const IDLE_SIGN_IN: SignInFlowState = {
|
|
418
|
+
...IDLE_SIGN_IN_FACTS,
|
|
419
|
+
progress: deriveSignInProgress(IDLE_SIGN_IN_FACTS),
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Terminal SUCCESS state: the session was claimed and committed. Holds no live
|
|
424
|
+
* resources and no request handles — only the terminal progress the surface
|
|
425
|
+
* shows ("Identity confirmed") before it closes.
|
|
426
|
+
*/
|
|
427
|
+
const COMPLETED_SIGN_IN: SignInFlowState = {
|
|
428
|
+
...IDLE_SIGN_IN_FACTS,
|
|
429
|
+
phase: 'completed',
|
|
430
|
+
progress: deriveSignInProgress({ ...IDLE_SIGN_IN_FACTS, phase: 'completed' }),
|
|
253
431
|
};
|
|
254
432
|
|
|
255
433
|
function errorMessage(error: unknown): string {
|
|
@@ -272,6 +450,7 @@ export class AccountDialogController {
|
|
|
272
450
|
private readonly socketFactory?: SocketIOFactory;
|
|
273
451
|
private readonly openPopup?: () => PopupWindowHandle | null;
|
|
274
452
|
private readonly hubBaseUrl: string;
|
|
453
|
+
private readonly platform: CommonsDeliveryPlatform;
|
|
275
454
|
|
|
276
455
|
private readonly listeners = new Set<SnapshotListener>();
|
|
277
456
|
|
|
@@ -329,6 +508,7 @@ export class AccountDialogController {
|
|
|
329
508
|
this.socketFactory = options.socketFactory;
|
|
330
509
|
this.openPopup = options.openPopup;
|
|
331
510
|
this.hubBaseUrl = options.hubBaseUrl ?? `https://auth.${CENTRAL_IDP_APEX}`;
|
|
511
|
+
this.platform = options.platform ?? 'unknown';
|
|
332
512
|
this.snapshot = this.computeSnapshot();
|
|
333
513
|
}
|
|
334
514
|
|
|
@@ -466,6 +646,13 @@ export class AccountDialogController {
|
|
|
466
646
|
setView(view: AccountDialogView): void {
|
|
467
647
|
if (this.view === view) return;
|
|
468
648
|
this.view = view;
|
|
649
|
+
// A `'completed'` flow owns no timers, socket, popup, or token — it is only
|
|
650
|
+
// the terminal "Identity confirmed" the finished surface showed. Moving to
|
|
651
|
+
// another view is a NEW intention, so drop it; otherwise a later `add()`
|
|
652
|
+
// would open on the previous sign-in's terminal state.
|
|
653
|
+
if (this.signIn.phase === 'completed') {
|
|
654
|
+
this.signIn = IDLE_SIGN_IN;
|
|
655
|
+
}
|
|
469
656
|
this.emit();
|
|
470
657
|
}
|
|
471
658
|
|
|
@@ -652,7 +839,7 @@ export class AccountDialogController {
|
|
|
652
839
|
*/
|
|
653
840
|
async signInWithOxy(): Promise<void> {
|
|
654
841
|
this.setView('qr');
|
|
655
|
-
this.setSignIn({ ...
|
|
842
|
+
this.setSignIn({ ...IDLE_SIGN_IN_FACTS, phase: 'starting' });
|
|
656
843
|
try {
|
|
657
844
|
const session = await this.oxyServices.signInWithSharedIdentity();
|
|
658
845
|
if (session) {
|
|
@@ -676,7 +863,7 @@ export class AccountDialogController {
|
|
|
676
863
|
async showQr(): Promise<void> {
|
|
677
864
|
this.cancelSignIn();
|
|
678
865
|
this.setView('qr');
|
|
679
|
-
await this.startDeviceFlowSession();
|
|
866
|
+
await this.startDeviceFlowSession({ deliver: true });
|
|
680
867
|
}
|
|
681
868
|
|
|
682
869
|
/**
|
|
@@ -703,10 +890,16 @@ export class AccountDialogController {
|
|
|
703
890
|
this.setView('qr');
|
|
704
891
|
const popup = this.openPopup?.() ?? null;
|
|
705
892
|
if (!popup) {
|
|
706
|
-
await this.startDeviceFlowSession();
|
|
893
|
+
await this.startDeviceFlowSession({ deliver: true });
|
|
707
894
|
return;
|
|
708
895
|
}
|
|
709
|
-
|
|
896
|
+
// The hub popup IS the primary surface here, chosen explicitly by the user —
|
|
897
|
+
// so this flow does NOT run automatic Commons delivery (ringing the user's
|
|
898
|
+
// phone because they asked for a passkey would be exactly the "menu of
|
|
899
|
+
// methods" the one-primary-action rule forbids). The underlying request is
|
|
900
|
+
// the same `AuthSession`, and its Commons route stays the QR the view
|
|
901
|
+
// renders beneath the popup.
|
|
902
|
+
const handle = await this.startDeviceFlowSession({ deliver: false });
|
|
710
903
|
if (!handle) {
|
|
711
904
|
popup.close();
|
|
712
905
|
return;
|
|
@@ -729,42 +922,157 @@ export class AccountDialogController {
|
|
|
729
922
|
* same `startCommonsSignIn` → poll/socket wiring either way. Returns the
|
|
730
923
|
* handle on success (already reflected in `signIn`), or `null` on failure
|
|
731
924
|
* (already set as `signIn.error`).
|
|
925
|
+
*
|
|
926
|
+
* @param opts.deliver - Whether to run automatic Commons delivery selection
|
|
927
|
+
* ({@link resolveDeliveryRoute}). `true` for the normal one-primary-action
|
|
928
|
+
* entry; `false` when the caller already owns the primary surface (the
|
|
929
|
+
* passkey hub popup), where the request's Commons route is simply the QR.
|
|
732
930
|
*/
|
|
733
|
-
private async startDeviceFlowSession(): Promise<CommonsSignInHandle | null> {
|
|
931
|
+
private async startDeviceFlowSession(opts: { deliver: boolean }): Promise<CommonsSignInHandle | null> {
|
|
734
932
|
if (!this.clientId) {
|
|
735
|
-
this.setSignIn({ ...
|
|
933
|
+
this.setSignIn({ ...IDLE_SIGN_IN_FACTS, phase: 'error', error: 'This app is not configured for sign-in (missing clientId).' });
|
|
736
934
|
return null;
|
|
737
935
|
}
|
|
738
|
-
this.setSignIn({ ...
|
|
936
|
+
this.setSignIn({ ...IDLE_SIGN_IN_FACTS, phase: 'starting' });
|
|
739
937
|
try {
|
|
740
938
|
const handle = await this.oxyServices.startCommonsSignIn({ clientId: this.clientId });
|
|
741
939
|
this.signInToken = handle.sessionToken;
|
|
742
940
|
this.setSignIn({
|
|
941
|
+
...IDLE_SIGN_IN_FACTS,
|
|
743
942
|
phase: 'waiting',
|
|
744
943
|
authorizeCode: handle.authorizeCode,
|
|
745
944
|
qrPayload: handle.qrPayload,
|
|
746
945
|
expiresAt: handle.expiresAt,
|
|
747
|
-
|
|
946
|
+
// No route yet: the surface shows "Preparing request" until the primary
|
|
947
|
+
// route is resolved below. It is never guessed in the meantime.
|
|
948
|
+
route: opts.deliver ? null : 'qr',
|
|
748
949
|
});
|
|
749
950
|
// Primary path: an instant `auth_update` wake over the `/auth-session`
|
|
750
951
|
// socket. The poll below is only the fallback for when the socket can't
|
|
751
952
|
// connect, so it now runs at the slow fallback cadence.
|
|
752
953
|
this.openAuthSessionSocket(handle.sessionToken);
|
|
753
954
|
this.scheduleNextPoll(handle.sessionToken);
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
void this.deepLinkIntoCommonsIfAvailable(handle.qrPayload);
|
|
955
|
+
if (opts.deliver) {
|
|
956
|
+
// Non-blocking on purpose: the QR/authorizeCode are already renderable
|
|
957
|
+
// and the popup caller can navigate immediately, while the route (a
|
|
958
|
+
// local probe plus at most one delivery round-trip) resolves behind it.
|
|
959
|
+
void this.resolveDeliveryRoute(handle);
|
|
960
|
+
}
|
|
761
961
|
return handle;
|
|
762
962
|
} catch (error) {
|
|
763
|
-
this.setSignIn({ ...
|
|
963
|
+
this.setSignIn({ ...IDLE_SIGN_IN_FACTS, phase: 'error', error: errorMessage(error) });
|
|
764
964
|
return null;
|
|
765
965
|
}
|
|
766
966
|
}
|
|
767
967
|
|
|
968
|
+
/**
|
|
969
|
+
* Choose and carry out the ONE primary delivery route for the active request
|
|
970
|
+
* (issue #691, "Automatic delivery selection").
|
|
971
|
+
*
|
|
972
|
+
* The controller gathers the facts — is a verified Commons link openable on
|
|
973
|
+
* THIS device, and how many known Commons installations did Oxy actually push
|
|
974
|
+
* to — and hands them to the pure {@link selectCommonsDelivery}. The selector
|
|
975
|
+
* owns the decision; this method owns only the observations and the single
|
|
976
|
+
* action the chosen route implies. It never cascades: a route that yields
|
|
977
|
+
* nothing resolves to QR *before* a route is chosen, never after.
|
|
978
|
+
*/
|
|
979
|
+
private async resolveDeliveryRoute(handle: CommonsSignInHandle): Promise<void> {
|
|
980
|
+
if (!this.isAwaitingApproval(handle.sessionToken)) return;
|
|
981
|
+
|
|
982
|
+
// Fact 1 — a VERIFIED Commons link openable on this very device. Native
|
|
983
|
+
// only; on web `commonsAvailability` stays `'unknown'` (a browser cannot be
|
|
984
|
+
// asked whether a custom scheme is registered) and this is simply `false`.
|
|
985
|
+
if (
|
|
986
|
+
this.canOpenApp &&
|
|
987
|
+
(this.commonsAvailability === 'unknown' || this.commonsAvailability === 'checking')
|
|
988
|
+
) {
|
|
989
|
+
await this.resolveCommonsAvailability();
|
|
990
|
+
if (!this.isAwaitingApproval(handle.sessionToken)) return;
|
|
991
|
+
}
|
|
992
|
+
const commonsAvailable = this.commonsAvailability === 'available';
|
|
993
|
+
|
|
994
|
+
// Fact 2 — how many known Commons installations the server pushed to.
|
|
995
|
+
const pushTargets = await this.deliverToKnownCommons(handle.authorizeCode, commonsAvailable);
|
|
996
|
+
if (!this.isAwaitingApproval(handle.sessionToken)) return;
|
|
997
|
+
|
|
998
|
+
const route = selectCommonsDelivery({ platform: this.platform, commonsAvailable, pushTargets });
|
|
999
|
+
// The only route that has an action to perform on this device — and the only
|
|
1000
|
+
// one that can fail here. `'await-push'` was already dispatched server-side;
|
|
1001
|
+
// `'qr'` is rendered by the surface from `qrPayload`.
|
|
1002
|
+
const routeFailed = route === 'open-commons' ? !this.openCommonsLink(handle.qrPayload) : false;
|
|
1003
|
+
this.patchSignIn({ route, routeFailed });
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Whether `sessionToken` is still THE request this surface is waiting on.
|
|
1008
|
+
*
|
|
1009
|
+
* Guards every step of the asynchronous route resolution: a cancelled,
|
|
1010
|
+
* superseded, failed, or already-approved flow must neither open Commons nor
|
|
1011
|
+
* mutate the surface — an approval that lands mid-resolution would otherwise
|
|
1012
|
+
* be followed by a pointless app switch.
|
|
1013
|
+
*/
|
|
1014
|
+
private isAwaitingApproval(sessionToken: string): boolean {
|
|
1015
|
+
return this.signInToken === sessionToken && this.signIn.phase === 'waiting';
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Ask Oxy to deliver the pending request to the identity's known Commons
|
|
1020
|
+
* installations, returning how many it reached (`0` when delivery is not
|
|
1021
|
+
* applicable, not permitted, or reached nobody).
|
|
1022
|
+
*
|
|
1023
|
+
* Two hard rules, both from the issue:
|
|
1024
|
+
* - **Never push from an unauthenticated surface.** `deliverCommonsSignIn`
|
|
1025
|
+
* is bearer-required precisely because a request that merely carries a
|
|
1026
|
+
* typed-in username must never be able to ring somebody's phone. Without a
|
|
1027
|
+
* planted bearer the call is not made AT ALL — not made-and-failed.
|
|
1028
|
+
* - **Zero targets and a failed delivery are the same normal outcome.** Both
|
|
1029
|
+
* return `0`, which resolves the primary route to QR silently. Neither is
|
|
1030
|
+
* surfaced as an error: there is nothing the user did wrong and nothing for
|
|
1031
|
+
* them to fix.
|
|
1032
|
+
*/
|
|
1033
|
+
private async deliverToKnownCommons(authorizeCode: string, commonsAvailable: boolean): Promise<number> {
|
|
1034
|
+
// Route 1 (mobile with a verified local Commons link) reaches the identity
|
|
1035
|
+
// on this very device — pushing as well would notify a second surface for a
|
|
1036
|
+
// request the user is about to confirm here.
|
|
1037
|
+
if (this.platform === 'mobile' && commonsAvailable) return 0;
|
|
1038
|
+
if (!this.isAuthenticated()) return 0;
|
|
1039
|
+
try {
|
|
1040
|
+
const result = await this.oxyServices.deliverCommonsSignIn(authorizeCode);
|
|
1041
|
+
// `delivered: false` with `targets > 0` is a transport failure, not a
|
|
1042
|
+
// reachable install — the shared helper owns that rule so the surface
|
|
1043
|
+
// can never park a user on "check your phone" for a push that failed.
|
|
1044
|
+
return pushTargetsFromDelivery(result);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
logger.debug(
|
|
1047
|
+
'[AccountDialogController] Commons delivery unavailable (QR route)',
|
|
1048
|
+
{ component: 'AccountDialogController' },
|
|
1049
|
+
error,
|
|
1050
|
+
);
|
|
1051
|
+
return 0;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Open the verified Commons link for the `'open-commons'` route. Returns
|
|
1057
|
+
* whether the navigation was actually handed off, so a missing opener or a
|
|
1058
|
+
* throwing one becomes an observable `routeFailed` instead of a silent dead
|
|
1059
|
+
* end the user stares at.
|
|
1060
|
+
*/
|
|
1061
|
+
private openCommonsLink(qrPayload: string): boolean {
|
|
1062
|
+
if (!this.openUrl) return false;
|
|
1063
|
+
try {
|
|
1064
|
+
this.openUrl(qrPayload);
|
|
1065
|
+
return true;
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
logger.debug(
|
|
1068
|
+
'[AccountDialogController] Commons deep link failed',
|
|
1069
|
+
{ component: 'AccountDialogController' },
|
|
1070
|
+
error,
|
|
1071
|
+
);
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
768
1076
|
/**
|
|
769
1077
|
* Poll {@link PopupWindowHandle.closed} so a user who dismisses the hub
|
|
770
1078
|
* popup without completing sign-in gets prompt feedback (there is no DOM
|
|
@@ -778,7 +1086,12 @@ export class AccountDialogController {
|
|
|
778
1086
|
if (!popup.closed) return;
|
|
779
1087
|
this.clearPopupWatchTimer();
|
|
780
1088
|
if (this.signIn.phase === 'starting' || this.signIn.phase === 'waiting') {
|
|
1089
|
+
// Closing the surface cancels the REQUEST too, not just this listener.
|
|
1090
|
+
const pendingCode = this.signIn.authorizeCode;
|
|
781
1091
|
this.failSignIn('Sign-in was cancelled.');
|
|
1092
|
+
if (pendingCode) {
|
|
1093
|
+
void this.withdrawRequest(pendingCode);
|
|
1094
|
+
}
|
|
782
1095
|
}
|
|
783
1096
|
}, 1000);
|
|
784
1097
|
}
|
|
@@ -835,32 +1148,48 @@ export class AccountDialogController {
|
|
|
835
1148
|
}
|
|
836
1149
|
|
|
837
1150
|
/**
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
1151
|
+
* Tear down the active sign-in device flow (timers + socket + popup + token),
|
|
1152
|
+
* WITHDRAW the request server-side, and reset to idle.
|
|
1153
|
+
*
|
|
1154
|
+
* Cancellation has to converge in both directions: the surface closing must
|
|
1155
|
+
* cancel the request, not just stop listening to it. Without the withdrawal a
|
|
1156
|
+
* dismissed QR would stay approvable until it expired, so a later scan of a
|
|
1157
|
+
* stale code could authorize a session nobody is waiting for.
|
|
842
1158
|
*/
|
|
843
|
-
private async deepLinkIntoCommonsIfAvailable(qrPayload: string): Promise<void> {
|
|
844
|
-
if (!this.openUrl) return;
|
|
845
|
-
if (this.commonsAvailability === 'unknown' || this.commonsAvailability === 'checking') {
|
|
846
|
-
// The eager `start()` probe hasn't resolved yet (or was never run, e.g.
|
|
847
|
-
// `showQr` called without a prior `start()`) — resolve it now rather
|
|
848
|
-
// than skipping the deep link.
|
|
849
|
-
await this.resolveCommonsAvailability();
|
|
850
|
-
}
|
|
851
|
-
if (this.commonsAvailability === 'available') {
|
|
852
|
-
this.openUrl(qrPayload);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/** Tear down the active sign-in device flow (timers + socket + popup + token) and reset to idle. */
|
|
857
1159
|
cancelSignIn(): void {
|
|
1160
|
+
// Capture before the teardown clears it, and only for a request that can
|
|
1161
|
+
// still be approved — a completed/failed flow has nothing to withdraw.
|
|
1162
|
+
const pendingCode =
|
|
1163
|
+
this.signIn.phase === 'starting' || this.signIn.phase === 'waiting'
|
|
1164
|
+
? this.signIn.authorizeCode
|
|
1165
|
+
: null;
|
|
858
1166
|
this.clearPollTimer();
|
|
859
1167
|
this.closeAuthSessionSocket();
|
|
860
1168
|
this.closeActivePopup();
|
|
861
1169
|
this.signInToken = null;
|
|
862
1170
|
if (this.signIn !== IDLE_SIGN_IN) {
|
|
863
|
-
this.setSignIn(
|
|
1171
|
+
this.setSignIn(IDLE_SIGN_IN_FACTS);
|
|
1172
|
+
}
|
|
1173
|
+
if (pendingCode) {
|
|
1174
|
+
void this.withdrawRequest(pendingCode);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Best-effort server-side withdrawal of a request this surface abandoned
|
|
1180
|
+
* (`POST /auth/session/deny/:authorizeCode`). Fire-and-forget by design: the
|
|
1181
|
+
* local teardown already happened, and a race with an approval that just
|
|
1182
|
+
* landed legitimately rejects here — neither outcome is worth surfacing.
|
|
1183
|
+
*/
|
|
1184
|
+
private async withdrawRequest(authorizeCode: string): Promise<void> {
|
|
1185
|
+
try {
|
|
1186
|
+
await this.oxyServices.denyCommonsSignIn(authorizeCode);
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
logger.debug(
|
|
1189
|
+
'[AccountDialogController] request withdrawal failed',
|
|
1190
|
+
{ component: 'AccountDialogController' },
|
|
1191
|
+
error,
|
|
1192
|
+
);
|
|
864
1193
|
}
|
|
865
1194
|
}
|
|
866
1195
|
|
|
@@ -895,6 +1224,18 @@ export class AccountDialogController {
|
|
|
895
1224
|
try {
|
|
896
1225
|
const status = await this.oxyServices.pollCommonsSignIn(sessionToken);
|
|
897
1226
|
if (this.signInToken !== sessionToken) return; // cancelled mid-request
|
|
1227
|
+
// Delivery PROGRESS first: it is reported alongside every status, and
|
|
1228
|
+
// recording it before the terminal branches means a poll that also
|
|
1229
|
+
// carries the approval still leaves an honest trail behind it.
|
|
1230
|
+
this.recordDeliveryProgress(status.pushSentAt, status.openedAt);
|
|
1231
|
+
const purpose = status.purpose === 'oauth_authorization' ? 'oauth_authorization' : 'device_sign_in';
|
|
1232
|
+
if (status.authorized && purpose === 'oauth_authorization') {
|
|
1233
|
+
// OAuth-bound sessions mint no sessionId on approval — they finalize
|
|
1234
|
+
// into an authorization code. The account dialog only starts device
|
|
1235
|
+
// sign-in today; stop rather than poll until expiry.
|
|
1236
|
+
this.failSignIn('This sign-in flow cannot be completed here. Use the app\'s OAuth sign-in instead.');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
898
1239
|
if (status.authorized && status.sessionId) {
|
|
899
1240
|
this.clearPollTimer();
|
|
900
1241
|
await this.claimAndComplete(status.sessionId, sessionToken);
|
|
@@ -920,8 +1261,23 @@ export class AccountDialogController {
|
|
|
920
1261
|
}
|
|
921
1262
|
}
|
|
922
1263
|
|
|
1264
|
+
/**
|
|
1265
|
+
* Record server-reported delivery progress on the active flow.
|
|
1266
|
+
*
|
|
1267
|
+
* Monotone and additive: a timestamp is only ever adopted, never replaced or
|
|
1268
|
+
* cleared, so an older API build (or a partial payload) that omits a field can
|
|
1269
|
+
* at most fail to advance the surface — it can never walk it backwards.
|
|
1270
|
+
* Emits only on a real change, so a steady poll does not churn the snapshot.
|
|
1271
|
+
*/
|
|
1272
|
+
private recordDeliveryProgress(pushSentAt: string | null, openedAt: string | null): void {
|
|
1273
|
+
const nextPushSentAt = this.signIn.pushSentAt ?? pushSentAt ?? null;
|
|
1274
|
+
const nextOpenedAt = this.signIn.openedAt ?? openedAt ?? null;
|
|
1275
|
+
if (nextPushSentAt === this.signIn.pushSentAt && nextOpenedAt === this.signIn.openedAt) return;
|
|
1276
|
+
this.patchSignIn({ pushSentAt: nextPushSentAt, openedAt: nextOpenedAt });
|
|
1277
|
+
}
|
|
1278
|
+
|
|
923
1279
|
private async claimAndComplete(sessionId: string, sessionToken: string): Promise<void> {
|
|
924
|
-
this.
|
|
1280
|
+
this.patchSignIn({ phase: 'authorized' });
|
|
925
1281
|
let claimed: {
|
|
926
1282
|
accessToken: string;
|
|
927
1283
|
sessionId: string;
|
|
@@ -979,7 +1335,9 @@ export class AccountDialogController {
|
|
|
979
1335
|
this.clearPollTimer();
|
|
980
1336
|
this.closeAuthSessionSocket();
|
|
981
1337
|
this.closeActivePopup();
|
|
982
|
-
|
|
1338
|
+
// Terminal SUCCESS, not idle: the surface gets one honest frame to show
|
|
1339
|
+
// "Identity confirmed" before it closes. Cleared on the next view change.
|
|
1340
|
+
this.signIn = COMPLETED_SIGN_IN;
|
|
983
1341
|
this.view = 'accounts';
|
|
984
1342
|
this.emit();
|
|
985
1343
|
this.onSignedIn?.(user);
|
|
@@ -1017,7 +1375,7 @@ export class AccountDialogController {
|
|
|
1017
1375
|
this.closeAuthSessionSocket();
|
|
1018
1376
|
this.closeActivePopup();
|
|
1019
1377
|
this.signInToken = null;
|
|
1020
|
-
this.setSignIn({ ...
|
|
1378
|
+
this.setSignIn({ ...IDLE_SIGN_IN_FACTS, phase: 'error', error: message });
|
|
1021
1379
|
}
|
|
1022
1380
|
|
|
1023
1381
|
private clearPollTimer(): void {
|
|
@@ -1097,11 +1455,44 @@ export class AccountDialogController {
|
|
|
1097
1455
|
// Snapshot plumbing
|
|
1098
1456
|
// =========================================================================
|
|
1099
1457
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1458
|
+
/**
|
|
1459
|
+
* Replace the device-flow state from its FACTS, re-deriving `progress`. The
|
|
1460
|
+
* only writer of `this.signIn` besides the two terminal constants — which is
|
|
1461
|
+
* what keeps `progress` impossible to set by hand, and therefore impossible
|
|
1462
|
+
* to advance without a fact behind it.
|
|
1463
|
+
*/
|
|
1464
|
+
private setSignIn(facts: SignInFlowFacts): void {
|
|
1465
|
+
this.signIn = { ...facts, progress: deriveSignInProgress(facts) };
|
|
1102
1466
|
this.emit();
|
|
1103
1467
|
}
|
|
1104
1468
|
|
|
1469
|
+
/** Update a subset of the device-flow facts, re-deriving `progress`. */
|
|
1470
|
+
private patchSignIn(patch: Partial<SignInFlowFacts>): void {
|
|
1471
|
+
const {
|
|
1472
|
+
phase,
|
|
1473
|
+
authorizeCode,
|
|
1474
|
+
qrPayload,
|
|
1475
|
+
expiresAt,
|
|
1476
|
+
error,
|
|
1477
|
+
route,
|
|
1478
|
+
routeFailed,
|
|
1479
|
+
pushSentAt,
|
|
1480
|
+
openedAt,
|
|
1481
|
+
} = this.signIn;
|
|
1482
|
+
this.setSignIn({
|
|
1483
|
+
phase,
|
|
1484
|
+
authorizeCode,
|
|
1485
|
+
qrPayload,
|
|
1486
|
+
expiresAt,
|
|
1487
|
+
error,
|
|
1488
|
+
route,
|
|
1489
|
+
routeFailed,
|
|
1490
|
+
pushSentAt,
|
|
1491
|
+
openedAt,
|
|
1492
|
+
...patch,
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1105
1496
|
private computeSnapshot(): AccountDialogSnapshot {
|
|
1106
1497
|
const state = this.sessionClient.getState();
|
|
1107
1498
|
return {
|