@oxyhq/core 12.0.0 → 12.1.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.
Files changed (67) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/i18n/locales/ar-SA.json +26 -2
  3. package/dist/cjs/i18n/locales/ca-ES.json +26 -2
  4. package/dist/cjs/i18n/locales/de-DE.json +26 -2
  5. package/dist/cjs/i18n/locales/en-US.json +25 -3
  6. package/dist/cjs/i18n/locales/es-ES.json +25 -3
  7. package/dist/cjs/i18n/locales/fr-FR.json +26 -2
  8. package/dist/cjs/i18n/locales/it-IT.json +26 -2
  9. package/dist/cjs/i18n/locales/ja-JP.json +26 -2
  10. package/dist/cjs/i18n/locales/ko-KR.json +26 -2
  11. package/dist/cjs/i18n/locales/locales/ar-SA.json +26 -2
  12. package/dist/cjs/i18n/locales/locales/ca-ES.json +26 -2
  13. package/dist/cjs/i18n/locales/locales/de-DE.json +26 -2
  14. package/dist/cjs/i18n/locales/locales/en-US.json +25 -3
  15. package/dist/cjs/i18n/locales/locales/es-ES.json +25 -3
  16. package/dist/cjs/i18n/locales/locales/fr-FR.json +26 -2
  17. package/dist/cjs/i18n/locales/locales/it-IT.json +26 -2
  18. package/dist/cjs/i18n/locales/locales/ja-JP.json +26 -2
  19. package/dist/cjs/i18n/locales/locales/ko-KR.json +26 -2
  20. package/dist/cjs/i18n/locales/locales/pt-PT.json +26 -2
  21. package/dist/cjs/i18n/locales/locales/zh-CN.json +26 -2
  22. package/dist/cjs/i18n/locales/pt-PT.json +26 -2
  23. package/dist/cjs/i18n/locales/zh-CN.json +26 -2
  24. package/dist/cjs/session/accountDialogController.js +83 -21
  25. package/dist/esm/.tsbuildinfo +1 -1
  26. package/dist/esm/i18n/locales/ar-SA.json +26 -2
  27. package/dist/esm/i18n/locales/ca-ES.json +26 -2
  28. package/dist/esm/i18n/locales/de-DE.json +26 -2
  29. package/dist/esm/i18n/locales/en-US.json +25 -3
  30. package/dist/esm/i18n/locales/es-ES.json +25 -3
  31. package/dist/esm/i18n/locales/fr-FR.json +26 -2
  32. package/dist/esm/i18n/locales/it-IT.json +26 -2
  33. package/dist/esm/i18n/locales/ja-JP.json +26 -2
  34. package/dist/esm/i18n/locales/ko-KR.json +26 -2
  35. package/dist/esm/i18n/locales/locales/ar-SA.json +26 -2
  36. package/dist/esm/i18n/locales/locales/ca-ES.json +26 -2
  37. package/dist/esm/i18n/locales/locales/de-DE.json +26 -2
  38. package/dist/esm/i18n/locales/locales/en-US.json +25 -3
  39. package/dist/esm/i18n/locales/locales/es-ES.json +25 -3
  40. package/dist/esm/i18n/locales/locales/fr-FR.json +26 -2
  41. package/dist/esm/i18n/locales/locales/it-IT.json +26 -2
  42. package/dist/esm/i18n/locales/locales/ja-JP.json +26 -2
  43. package/dist/esm/i18n/locales/locales/ko-KR.json +26 -2
  44. package/dist/esm/i18n/locales/locales/pt-PT.json +26 -2
  45. package/dist/esm/i18n/locales/locales/zh-CN.json +26 -2
  46. package/dist/esm/i18n/locales/pt-PT.json +26 -2
  47. package/dist/esm/i18n/locales/zh-CN.json +26 -2
  48. package/dist/esm/session/accountDialogController.js +83 -21
  49. package/dist/types/.tsbuildinfo +1 -1
  50. package/dist/types/index.d.ts +1 -1
  51. package/dist/types/session/accountDialogController.d.ts +81 -16
  52. package/package.json +1 -1
  53. package/src/i18n/locales/ar-SA.json +26 -2
  54. package/src/i18n/locales/ca-ES.json +26 -2
  55. package/src/i18n/locales/de-DE.json +26 -2
  56. package/src/i18n/locales/en-US.json +25 -3
  57. package/src/i18n/locales/es-ES.json +25 -3
  58. package/src/i18n/locales/fr-FR.json +26 -2
  59. package/src/i18n/locales/it-IT.json +26 -2
  60. package/src/i18n/locales/ja-JP.json +26 -2
  61. package/src/i18n/locales/ko-KR.json +26 -2
  62. package/src/i18n/locales/pt-PT.json +26 -2
  63. package/src/i18n/locales/zh-CN.json +26 -2
  64. package/src/index.ts +1 -0
  65. package/src/session/__tests__/accountDialogController.test.ts +79 -2
  66. package/src/session/__tests__/accountProjection.test.ts +30 -0
  67. 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 only — `canOpenApp`
447
- // is undefined/false on web), deep-link straight into its approve screen with
448
- // the same `oxycommons://approve?...` payload the QR encodes. The QR + polling
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.maybeOpenCommons(handle.qrPayload);
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
- * When a `canOpenApp` probe is injected and reports Commons installed, open the
459
- * approve deep link via the injected `openUrl`. Best-effort and non-blocking: a
460
- * probe/open failure is logged and swallowed the QR/polling fallback remains.
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 onlya 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 maybeOpenCommons(qrPayload) {
463
- if (!this.canOpenApp || !this.openUrl)
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
- if (await this.canOpenApp(COMMONS_APP_SCHEME)) {
467
- this.openUrl(qrPayload);
468
- }
498
+ available = await this.canOpenApp(COMMONS_APP_SCHEME);
469
499
  }
470
500
  catch (error) {
471
- logger.debug('[AccountDialogController] Commons deep-link probe failed (QR fallback active)', { component: 'AccountDialogController' }, error);
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 `commitSession` (durable persist + hydration); falls back to
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
- if (this.commitSession) {
599
- await this.commitSession(session);
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. */