@oxy.so/core 1.12.1 → 1.16.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.
Files changed (92) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/crypto/webIdentityCarrier.js +1 -1
  3. package/dist/cjs/i18n/locales/ar-SA.json +33 -88
  4. package/dist/cjs/i18n/locales/ca-ES.json +35 -90
  5. package/dist/cjs/i18n/locales/de-DE.json +33 -88
  6. package/dist/cjs/i18n/locales/en-US.json +45 -141
  7. package/dist/cjs/i18n/locales/es-ES.json +44 -140
  8. package/dist/cjs/i18n/locales/fr-FR.json +33 -88
  9. package/dist/cjs/i18n/locales/it-IT.json +33 -88
  10. package/dist/cjs/i18n/locales/ja-JP.json +33 -88
  11. package/dist/cjs/i18n/locales/ko-KR.json +33 -88
  12. package/dist/cjs/i18n/locales/locales/ar-SA.json +33 -88
  13. package/dist/cjs/i18n/locales/locales/ca-ES.json +35 -90
  14. package/dist/cjs/i18n/locales/locales/de-DE.json +33 -88
  15. package/dist/cjs/i18n/locales/locales/en-US.json +45 -141
  16. package/dist/cjs/i18n/locales/locales/es-ES.json +44 -140
  17. package/dist/cjs/i18n/locales/locales/fr-FR.json +33 -88
  18. package/dist/cjs/i18n/locales/locales/it-IT.json +33 -88
  19. package/dist/cjs/i18n/locales/locales/ja-JP.json +33 -88
  20. package/dist/cjs/i18n/locales/locales/ko-KR.json +33 -88
  21. package/dist/cjs/i18n/locales/locales/pt-PT.json +33 -88
  22. package/dist/cjs/i18n/locales/locales/zh-CN.json +34 -89
  23. package/dist/cjs/i18n/locales/pt-PT.json +33 -88
  24. package/dist/cjs/i18n/locales/zh-CN.json +34 -89
  25. package/dist/cjs/index.js +10 -7
  26. package/dist/cjs/mixins/OxyServices.utility.js +140 -0
  27. package/dist/cjs/server/securityHeaders.js +19 -1
  28. package/dist/cjs/session/accountDialogController.js +37 -142
  29. package/dist/cjs/utils/authWebUrl.js +7 -6
  30. package/dist/cjs/utils/oauthPkce.js +5 -1
  31. package/dist/esm/.tsbuildinfo +1 -1
  32. package/dist/esm/crypto/webIdentityCarrier.js +1 -1
  33. package/dist/esm/i18n/locales/ar-SA.json +33 -88
  34. package/dist/esm/i18n/locales/ca-ES.json +35 -90
  35. package/dist/esm/i18n/locales/de-DE.json +33 -88
  36. package/dist/esm/i18n/locales/en-US.json +45 -141
  37. package/dist/esm/i18n/locales/es-ES.json +44 -140
  38. package/dist/esm/i18n/locales/fr-FR.json +33 -88
  39. package/dist/esm/i18n/locales/it-IT.json +33 -88
  40. package/dist/esm/i18n/locales/ja-JP.json +33 -88
  41. package/dist/esm/i18n/locales/ko-KR.json +33 -88
  42. package/dist/esm/i18n/locales/locales/ar-SA.json +33 -88
  43. package/dist/esm/i18n/locales/locales/ca-ES.json +35 -90
  44. package/dist/esm/i18n/locales/locales/de-DE.json +33 -88
  45. package/dist/esm/i18n/locales/locales/en-US.json +45 -141
  46. package/dist/esm/i18n/locales/locales/es-ES.json +44 -140
  47. package/dist/esm/i18n/locales/locales/fr-FR.json +33 -88
  48. package/dist/esm/i18n/locales/locales/it-IT.json +33 -88
  49. package/dist/esm/i18n/locales/locales/ja-JP.json +33 -88
  50. package/dist/esm/i18n/locales/locales/ko-KR.json +33 -88
  51. package/dist/esm/i18n/locales/locales/pt-PT.json +33 -88
  52. package/dist/esm/i18n/locales/locales/zh-CN.json +34 -89
  53. package/dist/esm/i18n/locales/pt-PT.json +33 -88
  54. package/dist/esm/i18n/locales/zh-CN.json +34 -89
  55. package/dist/esm/index.js +2 -1
  56. package/dist/esm/mixins/OxyServices.utility.js +138 -0
  57. package/dist/esm/server/securityHeaders.js +19 -1
  58. package/dist/esm/session/accountDialogController.js +37 -142
  59. package/dist/esm/utils/authWebUrl.js +6 -5
  60. package/dist/esm/utils/oauthPkce.js +5 -1
  61. package/dist/types/.tsbuildinfo +1 -1
  62. package/dist/types/crypto/webIdentityCarrier.d.ts +1 -1
  63. package/dist/types/index.d.ts +5 -3
  64. package/dist/types/mixins/OxyServices.utility.d.ts +84 -1
  65. package/dist/types/server/requesterAssertion.d.ts +2 -2
  66. package/dist/types/server/securityHeaders.d.ts +9 -12
  67. package/dist/types/session/accountDialogController.d.ts +29 -98
  68. package/dist/types/utils/authWebUrl.d.ts +6 -5
  69. package/dist/types/utils/oauthPkce.d.ts +10 -0
  70. package/package.json +2 -2
  71. package/src/crypto/webIdentityCarrier.ts +1 -1
  72. package/src/i18n/locales/ar-SA.json +33 -88
  73. package/src/i18n/locales/ca-ES.json +35 -90
  74. package/src/i18n/locales/de-DE.json +33 -88
  75. package/src/i18n/locales/en-US.json +45 -141
  76. package/src/i18n/locales/es-ES.json +44 -140
  77. package/src/i18n/locales/fr-FR.json +33 -88
  78. package/src/i18n/locales/it-IT.json +33 -88
  79. package/src/i18n/locales/ja-JP.json +33 -88
  80. package/src/i18n/locales/ko-KR.json +33 -88
  81. package/src/i18n/locales/pt-PT.json +33 -88
  82. package/src/i18n/locales/zh-CN.json +34 -89
  83. package/src/index.ts +9 -3
  84. package/src/mixins/OxyServices.utility.ts +208 -0
  85. package/src/mixins/__tests__/accountEvents.test.ts +158 -0
  86. package/src/server/__tests__/securityHeaders.test.ts +17 -0
  87. package/src/server/securityHeaders.ts +29 -1
  88. package/src/session/__tests__/accountDialogController.test.ts +42 -184
  89. package/src/session/accountDialogController.ts +50 -185
  90. package/src/utils/__tests__/oauthPkce.test.ts +7 -0
  91. package/src/utils/authWebUrl.ts +6 -5
  92. package/src/utils/oauthPkce.ts +16 -1
@@ -201,8 +201,6 @@ interface Harness {
201
201
  function makeHarness(
202
202
  over: Partial<{
203
203
  clientId: string | null;
204
- openPopup: () => import('../accountDialogController').PopupWindowHandle | null;
205
- hubBaseUrl: string;
206
204
  platform: import('../../utils/commonsDelivery').CommonsDeliveryPlatform;
207
205
  openUrl: (url: string) => void;
208
206
  canOpenApp: (url: string) => Promise<boolean>;
@@ -220,8 +218,6 @@ function makeHarness(
220
218
  commitSession,
221
219
  onSignedIn,
222
220
  pollIntervalMs: 1000,
223
- openPopup: over.openPopup,
224
- hubBaseUrl: over.hubBaseUrl,
225
221
  platform: over.platform,
226
222
  openUrl: over.openUrl,
227
223
  canOpenApp: over.canOpenApp,
@@ -710,129 +706,57 @@ describe('AccountDialogController — sign in with Oxy', () => {
710
706
  });
711
707
  });
712
708
 
713
- /** A controllable fake `PopupWindowHandle` for `startPasskeyHubSignIn` tests. */
714
- function fakePopup(): import('../accountDialogController').PopupWindowHandle & { setClosed: () => void } {
715
- let closed = false;
716
- return {
717
- get closed() {
718
- return closed;
719
- },
720
- close: jest.fn(() => {
721
- closed = true;
722
- }),
723
- location: { href: '' },
724
- setClosed: () => {
725
- closed = true;
726
- },
727
- };
728
- }
729
-
730
- describe('AccountDialogController — startPasskeyHubSignIn (b2 passkey hub popup)', () => {
731
- it('opens the popup synchronously, then navigates it to the hub URL with the authorizeCode once the session exists', async () => {
732
- const popup = fakePopup();
733
- const openPopup = jest.fn(() => popup);
734
- const { controller, oxy } = makeHarness({ openPopup, hubBaseUrl: 'https://id.oxy.so' });
735
- oxy.startCommonsSignIn.mockResolvedValue({
736
- sessionToken: 'secret-tok',
737
- authorizeCode: 'AUTH-CODE',
738
- qrPayload: 'oxycommons://approve?v=1&code=AUTH-CODE',
739
- expiresAt: Date.now() + 300_000,
740
- status: 'pending',
741
- });
742
-
743
- await controller.startPasskeyHubSignIn();
744
-
745
- expect(openPopup).toHaveBeenCalledTimes(1);
746
- expect(popup.location.href).toBe('https://id.oxy.so/continue?code=AUTH-CODE');
747
- // Same underlying device-flow session showQr would create — the QR view
748
- // still renders as a fallback/alternative alongside the popup.
749
- const snap = controller.getSnapshot();
750
- expect(snap.view).toBe('qr');
751
- expect(snap.signIn.phase).toBe('waiting');
752
- expect(snap.signIn.authorizeCode).toBe('AUTH-CODE');
753
- controller.cancelSignIn();
709
+ describe('AccountDialogController — startInlineQr (the sign-in entry\'s embedded QR)', () => {
710
+ const pending = (code: string) => ({
711
+ sessionToken: `secret-${code}`,
712
+ authorizeCode: code,
713
+ qrPayload: `oxycommons://approve?v=1&code=${code}`,
714
+ expiresAt: Date.now() + 300_000,
715
+ status: 'pending',
754
716
  });
755
717
 
756
- it('falls back to the plain QR flow (no navigation) when the popup is blocked', async () => {
757
- const openPopup = jest.fn(() => null);
758
- const { controller, oxy } = makeHarness({ openPopup });
759
- oxy.startCommonsSignIn.mockResolvedValue({
760
- sessionToken: 'secret-tok',
761
- authorizeCode: 'AUTH-CODE',
762
- qrPayload: 'oxycommons://approve?v=1&code=AUTH-CODE',
763
- expiresAt: Date.now() + 300_000,
764
- status: 'pending',
765
- });
718
+ it('starts a QR-only request without leaving the entry — no push, no Commons, no view change', async () => {
719
+ const { controller, oxy } = makeHarness();
720
+ oxy.startCommonsSignIn.mockResolvedValue(pending('AUTH-CODE'));
721
+ controller.setView('signin');
766
722
 
767
- await controller.startPasskeyHubSignIn();
723
+ await controller.startInlineQr();
768
724
 
769
- expect(oxy.startCommonsSignIn).toHaveBeenCalledWith({ clientId: 'oxy_dk_test' });
770
725
  const snap = controller.getSnapshot();
771
- expect(snap.view).toBe('qr');
726
+ expect(snap.view).toBe('signin');
727
+ expect(snap.signIn.inline).toBe(true);
772
728
  expect(snap.signIn.phase).toBe('waiting');
729
+ expect(snap.signIn.route).toBe('qr');
773
730
  expect(snap.signIn.qrPayload).toBe('oxycommons://approve?v=1&code=AUTH-CODE');
731
+ // A bearer is planted in this harness, so an asked-for request WOULD push.
732
+ expect(oxy.deliverCommonsSignIn).not.toHaveBeenCalled();
774
733
  controller.cancelSignIn();
775
734
  });
776
735
 
777
- it('closes the popup without navigating it when device-flow session creation fails', async () => {
778
- const popup = fakePopup();
779
- const openPopup = jest.fn(() => popup);
780
- const { controller, oxy } = makeHarness({ openPopup });
781
- oxy.startCommonsSignIn.mockRejectedValue(new Error('network down'));
782
-
783
- await controller.startPasskeyHubSignIn();
784
-
785
- expect(popup.close).toHaveBeenCalledTimes(1);
786
- expect(popup.location.href).toBe('');
787
- expect(controller.getSnapshot().signIn.phase).toBe('error');
788
- });
789
-
790
- it('surfaces "cancelled" and closes the popup watcher when the user closes the popup before authorizing', async () => {
791
- jest.useFakeTimers();
792
- try {
793
- const popup = fakePopup();
794
- const openPopup = jest.fn(() => popup);
795
- const { controller, oxy } = makeHarness({ openPopup });
796
- oxy.startCommonsSignIn.mockResolvedValue({
797
- sessionToken: 'secret-tok',
798
- authorizeCode: 'AUTH-CODE',
799
- qrPayload: 'oxycommons://approve?v=1&code=AUTH-CODE',
800
- expiresAt: Date.now() + 300_000,
801
- status: 'pending',
802
- });
803
- oxy.pollCommonsSignIn.mockResolvedValue({ authorized: false, status: 'pending' });
804
-
805
- await controller.startPasskeyHubSignIn();
806
- expect(controller.getSnapshot().signIn.phase).toBe('waiting');
736
+ it('"Try again" repeats the embedded QR', async () => {
737
+ const { controller, oxy } = makeHarness();
738
+ oxy.startCommonsSignIn.mockResolvedValue(pending('AUTH-CODE'));
739
+ await controller.startInlineQr();
807
740
 
808
- popup.setClosed();
809
- await jest.advanceTimersByTimeAsync(1000); // the 1s popup-close watchdog tick
741
+ await controller.retrySignIn();
810
742
 
811
- const snap = controller.getSnapshot();
812
- expect(snap.signIn.phase).toBe('error');
813
- expect(snap.signIn.error).toMatch(/cancelled/i);
814
- // A VOLUNTARY exit — the surface must be able to tell it from a failure.
815
- expect(snap.signIn.failure).toBe('cancelled');
816
- } finally {
817
- jest.useRealTimers();
818
- }
743
+ expect(oxy.startCommonsSignIn).toHaveBeenCalledTimes(2);
744
+ expect(controller.getSnapshot().signIn.inline).toBe(true);
745
+ controller.cancelSignIn();
819
746
  });
820
747
 
821
- it('closes the popup again (never navigates it) when clientId is missing', async () => {
822
- const popup = fakePopup();
823
- const openPopup = jest.fn(() => popup);
824
- const { controller, oxy } = makeHarness({ clientId: null, openPopup });
748
+ it('gives way to a sign-in the person chooses, withdrawing its own request', async () => {
749
+ const { controller, oxy } = makeHarness();
750
+ oxy.startCommonsSignIn.mockResolvedValueOnce(pending('INLINE')).mockResolvedValueOnce(pending('ASKED'));
751
+ await controller.startInlineQr();
825
752
 
826
- await controller.startPasskeyHubSignIn();
753
+ await controller.showQr();
827
754
 
828
- // openPopup is invoked unconditionally (before the clientId check, since it
829
- // must run synchronously) — but the popup must be closed again rather than
830
- // navigated, since the flow can't proceed without a clientId.
831
- expect(oxy.startCommonsSignIn).not.toHaveBeenCalled();
832
- expect(popup.close).toHaveBeenCalledTimes(1);
833
- expect(popup.location.href).toBe('');
834
- expect(controller.getSnapshot().signIn.phase).toBe('error');
835
- expect(controller.getSnapshot().signIn.error).toMatch(/clientId/);
755
+ expect(oxy.denyCommonsSignIn).toHaveBeenCalledWith('INLINE');
756
+ const snap = controller.getSnapshot();
757
+ expect(snap.signIn.inline).toBe(false);
758
+ expect(snap.signIn.authorizeCode).toBe('ASKED');
759
+ controller.cancelSignIn();
836
760
  });
837
761
  });
838
762
 
@@ -1384,30 +1308,6 @@ describe('AccountDialogController — automatic delivery selection (#691 phase 5
1384
1308
  }
1385
1309
  });
1386
1310
 
1387
- it('does not run automatic delivery for the passkey hub flow (the popup is the primary surface)', async () => {
1388
- const popup = fakePopup();
1389
- const oxy = makeOxy();
1390
- oxy.startCommonsSignIn.mockResolvedValue({ ...DELIVERY_HANDLE, expiresAt: Date.now() + 600_000 });
1391
- oxy.pollCommonsSignIn.mockResolvedValue({ authorized: false, status: 'pending' });
1392
- oxy.deliverCommonsSignIn.mockResolvedValue({ delivered: true, targets: 1 });
1393
- const controller = new AccountDialogController({
1394
- oxyServices: oxy as unknown as OxyServices,
1395
- sessionClient: new TestSessionClient(host().host),
1396
- clientId: 'oxy_dk_test',
1397
- pollIntervalMs: 1000,
1398
- platform: 'desktop',
1399
- openPopup: () => popup,
1400
- hubBaseUrl: 'https://auth.oxy.so',
1401
- });
1402
-
1403
- await controller.startPasskeyHubSignIn();
1404
- await flush();
1405
-
1406
- expect(oxy.deliverCommonsSignIn).not.toHaveBeenCalled();
1407
- expect(controller.getSnapshot().signIn.route).toBe('qr');
1408
- controller.cancelSignIn();
1409
- });
1410
-
1411
1311
  it('never leaks the secret device-flow token into the snapshot', async () => {
1412
1312
  const { controller } = makeDeliveryHarness({ platform: 'desktop' });
1413
1313
 
@@ -1429,6 +1329,7 @@ describe('AccountDialogController — automatic delivery selection (#691 phase 5
1429
1329
  'openedAt',
1430
1330
  'progress',
1431
1331
  'attempt',
1332
+ 'inline',
1432
1333
  ]);
1433
1334
  // Only the PUBLIC handles are exposed.
1434
1335
  expect(snap.signIn.authorizeCode).toBe('AUTH-CODE');
@@ -1684,6 +1585,7 @@ describe('AccountDialogController — cancellation converges (#691 phase 5)', ()
1684
1585
  openedAt: null,
1685
1586
  progress: 'idle',
1686
1587
  attempt: expect.any(Number),
1588
+ inline: false,
1687
1589
  });
1688
1590
  await jest.advanceTimersByTimeAsync(10_000);
1689
1591
  expect(oxy.pollCommonsSignIn).not.toHaveBeenCalled();
@@ -1735,10 +1637,9 @@ describe('AccountDialogController — cancellation converges (#691 phase 5)', ()
1735
1637
  }
1736
1638
  });
1737
1639
 
1738
- it('destroy tears down the poll timer, the auth-session socket, and the popup watcher', async () => {
1640
+ it('destroy tears down the poll timer and the auth-session socket', async () => {
1739
1641
  jest.useFakeTimers();
1740
1642
  try {
1741
- const popup = fakePopup();
1742
1643
  const oxy = makeOxy();
1743
1644
  oxy.startCommonsSignIn.mockResolvedValue({ ...DELIVERY_HANDLE, expiresAt: Date.now() + 600_000 });
1744
1645
  oxy.pollCommonsSignIn.mockResolvedValue({ authorized: false, status: 'pending' });
@@ -1764,11 +1665,10 @@ describe('AccountDialogController — cancellation converges (#691 phase 5)', ()
1764
1665
  clientId: 'oxy_dk_test',
1765
1666
  pollIntervalMs: 1000,
1766
1667
  platform: 'desktop',
1767
- openPopup: () => popup,
1768
1668
  socketFactory: factory as unknown as SocketIOFactory,
1769
1669
  });
1770
1670
 
1771
- await controller.startPasskeyHubSignIn();
1671
+ await controller.showQr();
1772
1672
  await jest.advanceTimersByTimeAsync(0);
1773
1673
  expect(controller.getSnapshot().signIn.phase).toBe('waiting');
1774
1674
 
@@ -1776,8 +1676,7 @@ describe('AccountDialogController — cancellation converges (#691 phase 5)', ()
1776
1676
 
1777
1677
  expect(socket).not.toBeNull();
1778
1678
  expect(socket?.disconnected).toBe(true);
1779
- expect(popup.close).toHaveBeenCalled();
1780
- // No poll timer and no popup watchdog survive the teardown.
1679
+ // No poll timer survives the teardown.
1781
1680
  await jest.advanceTimersByTimeAsync(30_000);
1782
1681
  expect(oxy.pollCommonsSignIn).not.toHaveBeenCalled();
1783
1682
  } finally {
@@ -2305,33 +2204,8 @@ describe('AccountDialogController — an abandoned attempt stays abandoned', ()
2305
2204
  });
2306
2205
 
2307
2206
  describe('AccountDialogController — "Try again" repeats the user\'s choice', () => {
2308
- it('reopens the passkey hub after a failed hub attempt', async () => {
2309
- // A real window stays closed once closed, so every open is a fresh one.
2310
- let popup = fakePopup();
2311
- const openPopup = jest.fn(() => {
2312
- popup = fakePopup();
2313
- return popup;
2314
- });
2315
- const { controller, oxy } = makeHarness({ openPopup });
2316
- oxy.startCommonsSignIn.mockRejectedValue(new Error('boom'));
2317
-
2318
- await controller.startPasskeyHubSignIn();
2319
- expect(controller.getSnapshot().signIn.phase).toBe('error');
2320
-
2321
- oxy.startCommonsSignIn.mockResolvedValue({ ...DELIVERY_HANDLE, expiresAt: Date.now() + 600_000 });
2322
- const retry = controller.retrySignIn();
2323
- // Opened synchronously, inside the press — before any await.
2324
- expect(openPopup).toHaveBeenCalledTimes(2);
2325
- await retry;
2326
-
2327
- expect(oxy.deliverCommonsSignIn).not.toHaveBeenCalled();
2328
- expect(popup.location.href).toContain('/continue?code=AUTH-CODE');
2329
- controller.cancelSignIn();
2330
- });
2331
-
2332
- it('retries "Sign in with Oxy" (never the hub) after a failed Oxy attempt', async () => {
2333
- const openPopup = jest.fn(() => fakePopup());
2334
- const { controller, oxy } = makeHarness({ openPopup });
2207
+ it('retries "Sign in with Oxy" after a failed Oxy attempt', async () => {
2208
+ const { controller, oxy } = makeHarness();
2335
2209
  oxy.startCommonsSignIn.mockRejectedValueOnce(new Error('boom'));
2336
2210
 
2337
2211
  await controller.signInWithOxy();
@@ -2340,27 +2214,11 @@ describe('AccountDialogController — "Try again" repeats the user\'s choice', (
2340
2214
  oxy.startCommonsSignIn.mockResolvedValue({ ...DELIVERY_HANDLE, expiresAt: Date.now() + 600_000 });
2341
2215
  await controller.retrySignIn();
2342
2216
 
2343
- expect(openPopup).not.toHaveBeenCalled();
2344
2217
  expect(oxy.signInWithSharedIdentity).toHaveBeenCalledTimes(2);
2345
2218
  expect(controller.getSnapshot().signIn.phase).toBe('waiting');
2346
2219
  controller.cancelSignIn();
2347
2220
  });
2348
2221
 
2349
- it('closes a hub popup that was opened for an attempt the user cancelled while its request was created', async () => {
2350
- const popup = fakePopup();
2351
- const { controller, oxy } = makeHarness({ openPopup: () => popup });
2352
- const created = deferred<unknown>();
2353
- oxy.startCommonsSignIn.mockReturnValue(created.promise);
2354
-
2355
- const running = controller.startPasskeyHubSignIn();
2356
- controller.cancelSignIn();
2357
- // Closed at once, not left blank until the slow request returns.
2358
- expect(popup.close).toHaveBeenCalled();
2359
-
2360
- created.resolve({ ...DELIVERY_HANDLE, expiresAt: Date.now() + 600_000 });
2361
- await running;
2362
- expect(popup.location.href).toBe('');
2363
- });
2364
2222
  });
2365
2223
 
2366
2224
  describe('AccountDialogController — device mutations report their own outcome', () => {