@oxyhq/core 12.0.0 → 12.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/session/accountDialogController.js +83 -21
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/session/accountDialogController.js +83 -21
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/session/accountDialogController.d.ts +81 -16
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/session/__tests__/accountDialogController.test.ts +79 -2
- package/src/session/__tests__/accountProjection.test.ts +30 -0
- package/src/session/accountDialogController.ts +128 -27
|
@@ -12,17 +12,23 @@
|
|
|
12
12
|
* - the unified account list (via {@link projectSwitchableAccounts}), fetched
|
|
13
13
|
* from `SessionClient` state ∪ `oxyServices.listAccounts()` and hydrated
|
|
14
14
|
* with `oxyServices.getUsersByIds()`;
|
|
15
|
-
* - the dialog `view` state machine (`accounts` | `signin` | `qr` | `add`
|
|
15
|
+
* - the dialog `view` state machine (`accounts` | `signin` | `qr` | `add` |
|
|
16
|
+
* `signup`);
|
|
16
17
|
* - `switchTo` (the uniform switch: `SessionClient.switchAccount` for an
|
|
17
18
|
* account already on the device, `oxyServices.switchToAccount` to mint on
|
|
18
19
|
* first entry into a graph account — reusing the existing SDK primitives, no
|
|
19
20
|
* new switch path);
|
|
20
21
|
* - the "Sign in with Oxy" device flow (same-device shared-keychain via
|
|
21
22
|
* `oxyServices.signInWithSharedIdentity`, else the cross-device QR handoff
|
|
22
|
-
* via `startCommonsSignIn` → poll → `claimSessionByToken`)
|
|
23
|
+
* via `startCommonsSignIn` → poll → `claimSessionByToken`);
|
|
24
|
+
* - `commonsAvailability` — whether Commons is installed on this device
|
|
25
|
+
* (native only, via the injected `canOpenApp` probe), so the QR view can
|
|
26
|
+
* offer a "Get Commons" fallback instead of a same-device dead end.
|
|
23
27
|
*
|
|
24
28
|
* Sign-in is passkey (WebAuthn) or the Commons QR / shared-keychain handoff —
|
|
25
|
-
* password, social login, and 2FA were removed ecosystem-wide.
|
|
29
|
+
* password, social login, and 2FA were removed ecosystem-wide. Account
|
|
30
|
+
* creation (`signup` view) is the same two identity backends: a passkey
|
|
31
|
+
* ceremony on web, or a Commons-created identity.
|
|
26
32
|
*/
|
|
27
33
|
import { logger } from '../logger/index.js';
|
|
28
34
|
import { extractErrorStatus } from '../utils/errorUtils.js';
|
|
@@ -63,6 +69,7 @@ export class AccountDialogController {
|
|
|
63
69
|
this.error = null;
|
|
64
70
|
this.switchingAccountId = null;
|
|
65
71
|
this.signIn = IDLE_SIGN_IN;
|
|
72
|
+
this.commonsAvailability = 'unknown';
|
|
66
73
|
// --- Sign-in device-flow bookkeeping ---
|
|
67
74
|
/** The secret device-flow token of the active QR flow (never surfaced). */
|
|
68
75
|
this.signInToken = null;
|
|
@@ -91,6 +98,7 @@ export class AccountDialogController {
|
|
|
91
98
|
this.clientId = options.clientId ?? null;
|
|
92
99
|
this.locale = options.locale;
|
|
93
100
|
this.commitSession = options.commitSession;
|
|
101
|
+
this.commitSwitchedSession = options.commitSwitchedSession;
|
|
94
102
|
this.onSignedIn = options.onSignedIn;
|
|
95
103
|
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
96
104
|
this.openUrl = options.openUrl;
|
|
@@ -147,6 +155,11 @@ export class AccountDialogController {
|
|
|
147
155
|
// bearer is already planted (warm start); when signed out (cold boot before
|
|
148
156
|
// restore) it re-projects from device state and makes NO private call.
|
|
149
157
|
void this.refresh();
|
|
158
|
+
// Eager, cached Commons-availability probe (native only — a no-op when no
|
|
159
|
+
// `canOpenApp` was injected). It's a cheap local OS check, so by the time a
|
|
160
|
+
// user actually opens the sign-in entry it has almost always resolved —
|
|
161
|
+
// `showQr`'s own lazy probe below is only the safety net for the rare race.
|
|
162
|
+
void this.resolveCommonsAvailability();
|
|
150
163
|
}
|
|
151
164
|
/**
|
|
152
165
|
* Stop driving the dialog: unsubscribe from `SessionClient` and tear down the
|
|
@@ -230,6 +243,10 @@ export class AccountDialogController {
|
|
|
230
243
|
add() {
|
|
231
244
|
this.setView('add');
|
|
232
245
|
}
|
|
246
|
+
/** Switch to the "create account" view (passkey / Commons signup entry). */
|
|
247
|
+
startSignup() {
|
|
248
|
+
this.setView('signup');
|
|
249
|
+
}
|
|
233
250
|
// =========================================================================
|
|
234
251
|
// Account list
|
|
235
252
|
// =========================================================================
|
|
@@ -375,7 +392,11 @@ export class AccountDialogController {
|
|
|
375
392
|
expiresAt: result.expiresAt,
|
|
376
393
|
user: result.user,
|
|
377
394
|
accessToken: result.accessToken,
|
|
378
|
-
}, result.user
|
|
395
|
+
}, result.user,
|
|
396
|
+
// A switch is IN-PLACE: commit without the hub-sync redirect (the
|
|
397
|
+
// device is already known/synced). Cross-tab/app propagation rides the
|
|
398
|
+
// server's `session_state` socket broadcast, not a navigation.
|
|
399
|
+
{ fromSwitch: true });
|
|
379
400
|
}
|
|
380
401
|
// Re-project + refetch immediately; the subscription also fires.
|
|
381
402
|
await this.refresh();
|
|
@@ -443,32 +464,63 @@ export class AccountDialogController {
|
|
|
443
464
|
// connect, so it now runs at the slow fallback cadence.
|
|
444
465
|
this.openAuthSessionSocket(handle.sessionToken);
|
|
445
466
|
this.scheduleNextPoll(handle.sessionToken);
|
|
446
|
-
// Same-device convenience: if Commons is installed (native
|
|
447
|
-
//
|
|
448
|
-
//
|
|
467
|
+
// Same-device convenience: if Commons is confirmed installed (native
|
|
468
|
+
// only — stays `'unknown'` on web, where this never opens anything),
|
|
469
|
+
// deep-link straight into its approve screen with the same
|
|
470
|
+
// `oxycommons://approve?...` payload the QR encodes. The QR + polling
|
|
449
471
|
// stay live as the fallback, so a user who dismisses the app-open still
|
|
450
472
|
// completes the sign-in by scanning.
|
|
451
|
-
void this.
|
|
473
|
+
void this.deepLinkIntoCommonsIfAvailable(handle.qrPayload);
|
|
452
474
|
}
|
|
453
475
|
catch (error) {
|
|
454
476
|
this.setSignIn({ ...IDLE_SIGN_IN, phase: 'error', error: errorMessage(error) });
|
|
455
477
|
}
|
|
456
478
|
}
|
|
457
479
|
/**
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
480
|
+
* Resolve whether Commons is installed on this device via the injected
|
|
481
|
+
* `canOpenApp` probe, updating {@link commonsAvailability} as durable,
|
|
482
|
+
* observable snapshot state. Native only — a no-op when `canOpenApp` was
|
|
483
|
+
* not injected (web), where `commonsAvailability` stays `'unknown'` forever
|
|
484
|
+
* and the QR view renders unconditionally (no gating).
|
|
485
|
+
*
|
|
486
|
+
* Replaces the old `maybeOpenCommons` fire-and-forget probe, whose outcome
|
|
487
|
+
* was only ever reflected by whether Commons silently opened — a probe
|
|
488
|
+
* failure or "not installed" answer was swallowed into a debug log with no
|
|
489
|
+
* way for the UI to react. `commonsAvailability` fixes that.
|
|
461
490
|
*/
|
|
462
|
-
async
|
|
463
|
-
if (!this.canOpenApp
|
|
491
|
+
async resolveCommonsAvailability() {
|
|
492
|
+
if (!this.canOpenApp)
|
|
464
493
|
return;
|
|
494
|
+
this.commonsAvailability = 'checking';
|
|
495
|
+
this.emit();
|
|
496
|
+
let available = false;
|
|
465
497
|
try {
|
|
466
|
-
|
|
467
|
-
this.openUrl(qrPayload);
|
|
468
|
-
}
|
|
498
|
+
available = await this.canOpenApp(COMMONS_APP_SCHEME);
|
|
469
499
|
}
|
|
470
500
|
catch (error) {
|
|
471
|
-
logger.debug('[AccountDialogController] Commons
|
|
501
|
+
logger.debug('[AccountDialogController] Commons availability probe failed', { component: 'AccountDialogController' }, error);
|
|
502
|
+
available = false; // fail-closed — treat a probe error as "not installed"
|
|
503
|
+
}
|
|
504
|
+
this.commonsAvailability = available ? 'available' : 'unavailable';
|
|
505
|
+
this.emit();
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* When Commons is confirmed installed, deep-link straight into its approve
|
|
509
|
+
* screen via the injected `openUrl` with the same `oxycommons://approve?...`
|
|
510
|
+
* payload the QR encodes. Best-effort and non-blocking — the QR/polling
|
|
511
|
+
* fallback stays live regardless of the outcome here.
|
|
512
|
+
*/
|
|
513
|
+
async deepLinkIntoCommonsIfAvailable(qrPayload) {
|
|
514
|
+
if (!this.openUrl)
|
|
515
|
+
return;
|
|
516
|
+
if (this.commonsAvailability === 'unknown' || this.commonsAvailability === 'checking') {
|
|
517
|
+
// The eager `start()` probe hasn't resolved yet (or was never run, e.g.
|
|
518
|
+
// `showQr` called without a prior `start()`) — resolve it now rather
|
|
519
|
+
// than skipping the deep link.
|
|
520
|
+
await this.resolveCommonsAvailability();
|
|
521
|
+
}
|
|
522
|
+
if (this.commonsAvailability === 'available') {
|
|
523
|
+
this.openUrl(qrPayload);
|
|
472
524
|
}
|
|
473
525
|
}
|
|
474
526
|
/** Tear down the active sign-in device flow (timers + socket + token) and reset to idle. */
|
|
@@ -591,12 +643,21 @@ export class AccountDialogController {
|
|
|
591
643
|
}
|
|
592
644
|
/**
|
|
593
645
|
* Register a token-planted session into the device set. Prefers the
|
|
594
|
-
* consumer's
|
|
646
|
+
* consumer's commit funnel (durable persist + hydration); falls back to
|
|
595
647
|
* `SessionClient.registerAndActivate` (registration + activation only).
|
|
648
|
+
*
|
|
649
|
+
* A SWITCH (`opts.fromSwitch`) uses the IN-PLACE `commitSwitchedSession` funnel
|
|
650
|
+
* so it never runs the cross-origin hub-sync redirect; a SIGN-IN uses
|
|
651
|
+
* `commitSession` (which may hub-sync on an official web origin). When the
|
|
652
|
+
* switch funnel is not wired it falls back to the sign-in funnel, then to
|
|
653
|
+
* `registerAndActivate`.
|
|
596
654
|
*/
|
|
597
|
-
async commitAuthorizedSession(session, user) {
|
|
598
|
-
|
|
599
|
-
|
|
655
|
+
async commitAuthorizedSession(session, user, opts) {
|
|
656
|
+
const commit = opts?.fromSwitch
|
|
657
|
+
? this.commitSwitchedSession ?? this.commitSession
|
|
658
|
+
: this.commitSession;
|
|
659
|
+
if (commit) {
|
|
660
|
+
await commit(session);
|
|
600
661
|
}
|
|
601
662
|
else {
|
|
602
663
|
await this.sessionClient.registerAndActivate(user.id);
|
|
@@ -708,6 +769,7 @@ export class AccountDialogController {
|
|
|
708
769
|
error: this.error,
|
|
709
770
|
switchingAccountId: this.switchingAccountId,
|
|
710
771
|
signIn: this.signIn,
|
|
772
|
+
commonsAvailability: this.commonsAvailability,
|
|
711
773
|
};
|
|
712
774
|
}
|
|
713
775
|
/** Recompute the snapshot and notify subscribers. */
|