@oxyhq/services 21.0.1 → 22.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.
Files changed (117) hide show
  1. package/lib/commonjs/index.js +12 -0
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/ui/boot/runProviderColdBoot.js +12 -2
  4. package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
  5. package/lib/commonjs/ui/components/OxyAccountDialog.js +27 -546
  6. package/lib/commonjs/ui/components/OxyAccountDialog.js.map +1 -1
  7. package/lib/commonjs/ui/components/OxyAuthChooser.js +881 -0
  8. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -0
  9. package/lib/commonjs/ui/components/OxyProvider.js +2 -0
  10. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  11. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js +158 -0
  12. package/lib/commonjs/ui/components/oxyAuthChooserStyles.js.map +1 -0
  13. package/lib/commonjs/ui/context/OxyContext.js +50 -72
  14. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  15. package/lib/commonjs/ui/context/passkeyFlow.js +9 -8
  16. package/lib/commonjs/ui/context/passkeyFlow.js.map +1 -1
  17. package/lib/commonjs/ui/context/useOxyAccountGraph.js +6 -2
  18. package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js +2 -1
  20. package/lib/commonjs/ui/hooks/useSwitchableAccounts.js.map +1 -1
  21. package/lib/commonjs/ui/utils/commonsStoreLinks.js +42 -0
  22. package/lib/commonjs/ui/utils/commonsStoreLinks.js.map +1 -0
  23. package/lib/commonjs/ui/utils/crossOriginRestore.js +26 -0
  24. package/lib/commonjs/ui/utils/crossOriginRestore.js.map +1 -1
  25. package/lib/module/index.js +5 -0
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/ui/boot/runProviderColdBoot.js +13 -3
  28. package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
  29. package/lib/module/ui/components/OxyAccountDialog.js +29 -550
  30. package/lib/module/ui/components/OxyAccountDialog.js.map +1 -1
  31. package/lib/module/ui/components/OxyAuthChooser.js +878 -0
  32. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -0
  33. package/lib/module/ui/components/OxyProvider.js +2 -0
  34. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  35. package/lib/module/ui/components/oxyAuthChooserStyles.js +155 -0
  36. package/lib/module/ui/components/oxyAuthChooserStyles.js.map +1 -0
  37. package/lib/module/ui/context/OxyContext.js +50 -72
  38. package/lib/module/ui/context/OxyContext.js.map +1 -1
  39. package/lib/module/ui/context/passkeyFlow.js +9 -8
  40. package/lib/module/ui/context/passkeyFlow.js.map +1 -1
  41. package/lib/module/ui/context/useOxyAccountGraph.js +6 -2
  42. package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
  43. package/lib/module/ui/hooks/useSwitchableAccounts.js +2 -1
  44. package/lib/module/ui/hooks/useSwitchableAccounts.js.map +1 -1
  45. package/lib/module/ui/utils/commonsStoreLinks.js +37 -0
  46. package/lib/module/ui/utils/commonsStoreLinks.js.map +1 -0
  47. package/lib/module/ui/utils/crossOriginRestore.js +26 -1
  48. package/lib/module/ui/utils/crossOriginRestore.js.map +1 -1
  49. package/lib/typescript/commonjs/index.d.ts +3 -1
  50. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts +6 -0
  52. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts +10 -20
  54. package/lib/typescript/commonjs/ui/components/OxyAccountDialog.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +60 -0
  56. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts +148 -0
  59. package/lib/typescript/commonjs/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
  60. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +1 -1
  61. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +22 -27
  63. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts +15 -4
  65. package/lib/typescript/commonjs/ui/context/passkeyFlow.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +1 -0
  67. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/types/navigation.d.ts +8 -0
  70. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts +24 -0
  72. package/lib/typescript/commonjs/ui/utils/commonsStoreLinks.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/ui/utils/crossOriginRestore.d.ts +19 -0
  74. package/lib/typescript/commonjs/ui/utils/crossOriginRestore.d.ts.map +1 -1
  75. package/lib/typescript/module/index.d.ts +3 -1
  76. package/lib/typescript/module/index.d.ts.map +1 -1
  77. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts +6 -0
  78. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  79. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts +10 -20
  80. package/lib/typescript/module/ui/components/OxyAccountDialog.d.ts.map +1 -1
  81. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +60 -0
  82. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -0
  83. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  84. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts +148 -0
  85. package/lib/typescript/module/ui/components/oxyAuthChooserStyles.d.ts.map +1 -0
  86. package/lib/typescript/module/ui/context/OxyContext.d.ts +1 -1
  87. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  88. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +22 -27
  89. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  90. package/lib/typescript/module/ui/context/passkeyFlow.d.ts +15 -4
  91. package/lib/typescript/module/ui/context/passkeyFlow.d.ts.map +1 -1
  92. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +1 -0
  93. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/hooks/useSwitchableAccounts.d.ts.map +1 -1
  95. package/lib/typescript/module/ui/types/navigation.d.ts +8 -0
  96. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  97. package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts +24 -0
  98. package/lib/typescript/module/ui/utils/commonsStoreLinks.d.ts.map +1 -0
  99. package/lib/typescript/module/ui/utils/crossOriginRestore.d.ts +19 -0
  100. package/lib/typescript/module/ui/utils/crossOriginRestore.d.ts.map +1 -1
  101. package/package.json +5 -5
  102. package/src/index.ts +8 -1
  103. package/src/ui/boot/runProviderColdBoot.ts +24 -1
  104. package/src/ui/components/OxyAccountDialog.tsx +33 -571
  105. package/src/ui/components/OxyAuthChooser.tsx +883 -0
  106. package/src/ui/components/OxyProvider.tsx +2 -0
  107. package/src/ui/components/oxyAuthChooserStyles.ts +152 -0
  108. package/src/ui/context/OxyContext.tsx +56 -82
  109. package/src/ui/context/__tests__/passkeyFlow.test.ts +22 -15
  110. package/src/ui/context/oxyContextTypes.ts +27 -26
  111. package/src/ui/context/passkeyFlow.ts +17 -9
  112. package/src/ui/context/useOxyAccountGraph.ts +4 -2
  113. package/src/ui/hooks/useSwitchableAccounts.ts +1 -0
  114. package/src/ui/types/navigation.ts +8 -0
  115. package/src/ui/utils/__tests__/crossOriginRestore.test.ts +143 -0
  116. package/src/ui/utils/commonsStoreLinks.ts +38 -0
  117. package/src/ui/utils/crossOriginRestore.ts +34 -0
@@ -0,0 +1,143 @@
1
+ import type { OxyServices } from '@oxyhq/core';
2
+
3
+ // Spy on the platform redirect so no real navigation happens and we can assert
4
+ // the authorize URL the silent-restore flow hands to the browser.
5
+ jest.mock('../../components/oauthNavigation', () => ({
6
+ redirectToAuthorize: jest.fn(),
7
+ }));
8
+
9
+ // jsdom's `location` is non-configurable, so the origin the code reads cannot be
10
+ // reassigned per-test. Instead, drive the ORIGIN-CLASSIFICATION predicates
11
+ // (whose real behaviour is covered by core's officialOrigins.test.ts) and keep
12
+ // every other core helper real (PKCE, authorize-URL builder, handshake store).
13
+ jest.mock('@oxyhq/core', () => {
14
+ const actual = jest.requireActual('@oxyhq/core');
15
+ return {
16
+ __esModule: true,
17
+ ...actual,
18
+ isLoopbackOrigin: jest.fn(),
19
+ isOfficialWebOrigin: jest.fn(),
20
+ isIdpHubOrigin: jest.fn(),
21
+ };
22
+ });
23
+
24
+ import {
25
+ isLoopbackOrigin,
26
+ isOfficialWebOrigin,
27
+ isIdpHubOrigin,
28
+ } from '@oxyhq/core';
29
+ import { redirectToAuthorize } from '../../components/oauthNavigation';
30
+ import {
31
+ isSilentRestoreEligibleOrigin,
32
+ maybeStartSilentOAuthRestore,
33
+ clearCrossOriginRestoreGuards,
34
+ } from '../crossOriginRestore';
35
+
36
+ const mockRedirect = redirectToAuthorize as jest.Mock;
37
+ const mockIsLoopback = isLoopbackOrigin as jest.Mock;
38
+ const mockIsOfficial = isOfficialWebOrigin as jest.Mock;
39
+ const mockIsIdpHub = isIdpHubOrigin as jest.Mock;
40
+
41
+ const OXY_SERVICES_STUB = {} as OxyServices;
42
+
43
+ /** Default to an eligible official origin; individual tests narrow this. */
44
+ function makeOriginEligible(): void {
45
+ mockIsLoopback.mockReturnValue(false);
46
+ mockIsIdpHub.mockReturnValue(false);
47
+ mockIsOfficial.mockReturnValue(true);
48
+ }
49
+
50
+ describe('crossOriginRestore', () => {
51
+ beforeEach(() => {
52
+ mockRedirect.mockClear();
53
+ mockIsLoopback.mockReset();
54
+ mockIsOfficial.mockReset();
55
+ mockIsIdpHub.mockReset();
56
+ globalThis.sessionStorage?.clear();
57
+ clearCrossOriginRestoreGuards();
58
+ });
59
+
60
+ describe('isSilentRestoreEligibleOrigin', () => {
61
+ it('rejects loopback / local-dev origins', () => {
62
+ mockIsLoopback.mockReturnValue(true);
63
+ mockIsIdpHub.mockReturnValue(false);
64
+ mockIsOfficial.mockReturnValue(true);
65
+ expect(isSilentRestoreEligibleOrigin('http://localhost:3000')).toBe(false);
66
+ });
67
+
68
+ it('rejects the central IdP hub origin (no self-hop)', () => {
69
+ mockIsLoopback.mockReturnValue(false);
70
+ mockIsIdpHub.mockReturnValue(true);
71
+ mockIsOfficial.mockReturnValue(true);
72
+ expect(isSilentRestoreEligibleOrigin('https://auth.oxy.so')).toBe(false);
73
+ });
74
+
75
+ it('rejects non-official origins', () => {
76
+ mockIsLoopback.mockReturnValue(false);
77
+ mockIsIdpHub.mockReturnValue(false);
78
+ mockIsOfficial.mockReturnValue(false);
79
+ expect(isSilentRestoreEligibleOrigin('https://evil.example')).toBe(false);
80
+ });
81
+
82
+ it('accepts official, non-loopback, non-IdP origins', () => {
83
+ makeOriginEligible();
84
+ expect(isSilentRestoreEligibleOrigin('https://accounts.oxy.so')).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('maybeStartSilentOAuthRestore', () => {
89
+ it('is SKIPPED on a loopback origin (no redirect)', async () => {
90
+ mockIsLoopback.mockReturnValue(true);
91
+ mockIsIdpHub.mockReturnValue(false);
92
+ mockIsOfficial.mockReturnValue(true);
93
+ const redirected = await maybeStartSilentOAuthRestore({
94
+ oxyServices: OXY_SERVICES_STUB,
95
+ clientId: 'oxy_dk_test',
96
+ });
97
+ expect(redirected).toBe(false);
98
+ expect(mockRedirect).not.toHaveBeenCalled();
99
+ });
100
+
101
+ it('is SKIPPED on a non-official origin (no redirect)', async () => {
102
+ mockIsLoopback.mockReturnValue(false);
103
+ mockIsIdpHub.mockReturnValue(false);
104
+ mockIsOfficial.mockReturnValue(false);
105
+ const redirected = await maybeStartSilentOAuthRestore({
106
+ oxyServices: OXY_SERVICES_STUB,
107
+ clientId: 'oxy_dk_test',
108
+ });
109
+ expect(redirected).toBe(false);
110
+ expect(mockRedirect).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it('redirects to the PROD IdP by default on an eligible origin', async () => {
114
+ makeOriginEligible();
115
+ const redirected = await maybeStartSilentOAuthRestore({
116
+ oxyServices: OXY_SERVICES_STUB,
117
+ clientId: 'oxy_dk_test',
118
+ });
119
+ expect(redirected).toBe(true);
120
+ expect(mockRedirect).toHaveBeenCalledTimes(1);
121
+ const url = mockRedirect.mock.calls[0][0] as string;
122
+ expect(url.startsWith('https://auth.oxy.so/authorize?')).toBe(true);
123
+ const params = new URL(url).searchParams;
124
+ expect(params.get('client_id')).toBe('oxy_dk_test');
125
+ expect(params.get('prompt')).toBe('none');
126
+ });
127
+
128
+ it('honors an authorizeBaseUrl override (self-hosted / staging IdP)', async () => {
129
+ makeOriginEligible();
130
+ const redirected = await maybeStartSilentOAuthRestore({
131
+ oxyServices: OXY_SERVICES_STUB,
132
+ clientId: 'oxy_dk_test',
133
+ authorizeBaseUrl: 'https://auth.staging.oxy.so/authorize',
134
+ });
135
+ expect(redirected).toBe(true);
136
+ const url = mockRedirect.mock.calls[0][0] as string;
137
+ expect(url.startsWith('https://auth.staging.oxy.so/authorize?')).toBe(true);
138
+ const params = new URL(url).searchParams;
139
+ expect(params.get('client_id')).toBe('oxy_dk_test');
140
+ expect(params.get('prompt')).toBe('none');
141
+ });
142
+ });
143
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Where to send a user who doesn't have Commons installed yet.
3
+ *
4
+ * Commons is not published to either app store as of this writing (no submit
5
+ * profile in its `eas.json`, no store listing) — so these are ALWAYS
6
+ * env-overridable and NEVER hardcoded, and the landing-page fallback exists
7
+ * precisely so a "Get Commons" affordance is never a dead link in the
8
+ * meantime. Once Commons ships to TestFlight/Play, set the two store env vars
9
+ * and every consumer picks them up with no code change.
10
+ */
11
+
12
+ /** Real per-platform store URLs, or `undefined` where not yet configured. */
13
+ export interface CommonsStoreLinks {
14
+ ios: string | undefined;
15
+ android: string | undefined;
16
+ }
17
+
18
+ /** Per-platform Commons store URLs, from env — `undefined` when unset. */
19
+ export function getCommonsStoreLinks(): CommonsStoreLinks {
20
+ return {
21
+ ios: process.env.EXPO_PUBLIC_COMMONS_APP_STORE_URL || undefined,
22
+ android: process.env.EXPO_PUBLIC_COMMONS_PLAY_STORE_URL || undefined,
23
+ };
24
+ }
25
+
26
+ /** Placeholder landing page — the fallback while Commons has no store listing. */
27
+ const DEFAULT_COMMONS_LANDING_URL = 'https://oxy.so/commons';
28
+
29
+ /**
30
+ * Resolve the "Get Commons" href for the current platform. Never `undefined`
31
+ * — falls back to a landing page so the affordance is always a working link,
32
+ * store listing or not.
33
+ */
34
+ export function getCommonsAcquisitionUrl(platformOS: 'ios' | 'android' | string): string {
35
+ const links = getCommonsStoreLinks();
36
+ const storeUrl = platformOS === 'ios' ? links.ios : platformOS === 'android' ? links.android : undefined;
37
+ return storeUrl ?? process.env.EXPO_PUBLIC_COMMONS_LANDING_URL ?? DEFAULT_COMMONS_LANDING_URL;
38
+ }
@@ -3,6 +3,9 @@ import {
3
3
  buildOAuthAuthorizeUrl,
4
4
  generateOAuthState,
5
5
  generatePkcePair,
6
+ isIdpHubOrigin,
7
+ isLoopbackOrigin,
8
+ isOfficialWebOrigin,
6
9
  OXY_CROSS_ORIGIN_RESTORE_ATTEMPTED_KEY,
7
10
  OXY_SILENT_OAUTH_ATTEMPTED_KEY,
8
11
  persistOAuthHandshake,
@@ -43,11 +46,35 @@ export function clearCrossOriginRestoreGuards(): void {
43
46
  }
44
47
  }
45
48
 
49
+ /**
50
+ * Whether cold boot should attempt silent cross-origin OAuth restore for the
51
+ * given web origin.
52
+ *
53
+ * Mirrors the hub-sync WRITE gate (`syncHubAfterSignIn`): only OFFICIAL Oxy web
54
+ * origins participate in cross-app SSO — a random third-party / preview origin
55
+ * must never have its tab bounced to the IdP. Additionally, loopback / local-dev
56
+ * origins are EXCLUDED here even though {@link isOfficialWebOrigin} treats them
57
+ * as official: a developer's local app must never be redirected to a hosted IdP
58
+ * on cold boot (it signs in through the in-app dialog instead). The central IdP
59
+ * hub's own origin is excluded to avoid a wasteful self-hop.
60
+ */
61
+ export function isSilentRestoreEligibleOrigin(origin: string): boolean {
62
+ if (isLoopbackOrigin(origin)) return false;
63
+ if (isIdpHubOrigin()) return false;
64
+ return isOfficialWebOrigin(origin);
65
+ }
66
+
46
67
  export interface SilentOAuthRestoreOptions {
47
68
  oxyServices: OxyServices;
48
69
  clientId: string;
49
70
  redirectUri?: string;
50
71
  scope?: string;
72
+ /**
73
+ * Authorize endpoint override (env-configurable per deployment). Defaults to
74
+ * the production Oxy IdP when unset, so a local/staging app points silent
75
+ * restore at its OWN IdP instead of production `auth.oxy.so`.
76
+ */
77
+ authorizeBaseUrl?: string;
51
78
  }
52
79
 
53
80
  /**
@@ -62,6 +89,12 @@ export async function maybeStartSilentOAuthRestore(
62
89
  const location = (globalThis as { location?: Location }).location;
63
90
  if (!location) return false;
64
91
 
92
+ // Defense-in-depth: never bounce a loopback / non-official / IdP-hub origin to
93
+ // a hosted authorize endpoint (the caller gates on this too).
94
+ if (!isSilentRestoreEligibleOrigin(location.origin)) {
95
+ return false;
96
+ }
97
+
65
98
  if (isCrossOriginRestoreBlocked()) {
66
99
  return false;
67
100
  }
@@ -83,6 +116,7 @@ export async function maybeStartSilentOAuthRestore(
83
116
  );
84
117
 
85
118
  const authorizeUrl = buildOAuthAuthorizeUrl({
119
+ authorizeBaseUrl: opts.authorizeBaseUrl,
86
120
  clientId: opts.clientId,
87
121
  redirectUri,
88
122
  state,