@oxyhq/services 9.0.0 → 10.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/lib/commonjs/ui/components/OxyProvider.js +2 -2
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/SignInModal.js +26 -12
- package/lib/commonjs/ui/components/SignInModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +35 -19
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useWebSSO.js +7 -8
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/commonjs/ui/screens/OxyAuthScreen.js +65 -23
- package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/commonjs/utils/deviceFlowSignIn.js +55 -0
- package/lib/commonjs/utils/deviceFlowSignIn.js.map +1 -0
- package/lib/commonjs/utils/silentGuardKey.js +54 -0
- package/lib/commonjs/utils/silentGuardKey.js.map +1 -0
- package/lib/module/ui/components/OxyProvider.js +2 -2
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/SignInModal.js +26 -12
- package/lib/module/ui/components/SignInModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +35 -19
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useWebSSO.js +7 -8
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/module/ui/screens/OxyAuthScreen.js +65 -23
- package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
- package/lib/module/utils/deviceFlowSignIn.js +51 -0
- package/lib/module/utils/deviceFlowSignIn.js.map +1 -0
- package/lib/module/utils/silentGuardKey.js +49 -0
- package/lib/module/utils/silentGuardKey.js.map +1 -0
- package/lib/typescript/commonjs/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +13 -9
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useServicesMutations.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useServicesMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +8 -6
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts +61 -0
- package/lib/typescript/commonjs/utils/deviceFlowSignIn.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts +31 -0
- package/lib/typescript/commonjs/utils/silentGuardKey.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/SignInModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +13 -9
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useServicesMutations.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useServicesMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useWebSSO.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/OxyAuthScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +8 -6
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts +61 -0
- package/lib/typescript/module/utils/deviceFlowSignIn.d.ts.map +1 -0
- package/lib/typescript/module/utils/silentGuardKey.d.ts +31 -0
- package/lib/typescript/module/utils/silentGuardKey.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/ui/components/OxyProvider.tsx +2 -2
- package/src/ui/components/SignInModal.tsx +26 -12
- package/src/ui/context/OxyContext.tsx +50 -33
- package/src/ui/hooks/useWebSSO.ts +7 -8
- package/src/ui/screens/OxyAuthScreen.tsx +65 -22
- package/src/ui/types/navigation.ts +8 -6
- package/src/utils/__tests__/deviceFlowSignIn.test.ts +104 -0
- package/src/utils/__tests__/silentGuardKey.test.ts +82 -0
- package/src/utils/deviceFlowSignIn.ts +76 -0
- package/src/utils/silentGuardKey.ts +46 -0
- package/lib/commonjs/ui/utils/appName.js +0 -62
- package/lib/commonjs/ui/utils/appName.js.map +0 -1
- package/lib/module/ui/utils/appName.js +0 -59
- package/lib/module/ui/utils/appName.js.map +0 -1
- package/lib/typescript/commonjs/ui/utils/appName.d.ts +0 -22
- package/lib/typescript/commonjs/ui/utils/appName.d.ts.map +0 -1
- package/lib/typescript/module/ui/utils/appName.d.ts +0 -22
- package/lib/typescript/module/ui/utils/appName.d.ts.map +0 -1
- package/src/ui/utils/__tests__/appName.test.ts +0 -52
- package/src/ui/utils/appName.ts +0 -62
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../../src/ui/types/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAKtD,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,MAAM,WAAW,cAAc;IAC3B,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,MAAM,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAE5B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACxE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,eAAe,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAG9C,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IAGlC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAGjE,aAAa,CAAC,EAAE,SAAS,CAAC;IAG1B,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAGnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IAMtB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../../src/ui/types/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAKtD,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B,MAAM,WAAW,cAAc;IAC3B,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,MAAM,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAE5B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACxE,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,eAAe,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAG9C,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IAGlC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACrD,YAAY,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAGjE,aAAa,CAAC,EAAE,SAAS,CAAC;IAG1B,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAGnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IAMtB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,WAAW,CAAC;CAC7B"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, pure orchestration for completing the cross-app device-flow sign-in
|
|
3
|
+
* (the QR-code / "Open Oxy Auth" path used on native and web).
|
|
4
|
+
*
|
|
5
|
+
* THE BUG THIS FIXES (native): once another authenticated device approves the
|
|
6
|
+
* pending AuthSession, the originating client is notified (socket / poll /
|
|
7
|
+
* deep-link) with the authorized `sessionId`. Before any session-management
|
|
8
|
+
* code can use it, the client MUST exchange the secret 128-bit `sessionToken`
|
|
9
|
+
* (held only by this client, generated for THIS flow) for the first access
|
|
10
|
+
* token via `claimSessionByToken` — the device-flow equivalent of OAuth's
|
|
11
|
+
* code-for-token exchange (RFC 8628 §3.4).
|
|
12
|
+
*
|
|
13
|
+
* Skipping the claim leaves the SDK with NO bearer token, so the subsequent
|
|
14
|
+
* `switchSession` -> `getTokenBySession` (`GET /session/token/:id`) call 401s
|
|
15
|
+
* against the C1-hardened API: the session is authorized server-side but the
|
|
16
|
+
* app never becomes authenticated and the UI sits "Waiting for
|
|
17
|
+
* authorization..." forever. The web `SignInModal` already claimed first; the
|
|
18
|
+
* native `OxyAuthScreen` did not. Consolidating the claim→switch sequence here
|
|
19
|
+
* keeps both paths identical and unit-testable, and prevents future drift.
|
|
20
|
+
*/
|
|
21
|
+
import type { User } from '@oxyhq/core';
|
|
22
|
+
/**
|
|
23
|
+
* The minimal `OxyServices` surface this orchestration needs. Kept as a
|
|
24
|
+
* structural type (rather than importing the full client) so the helper is
|
|
25
|
+
* trivially unit-testable with a stub and never pulls the RN/Expo runtime into
|
|
26
|
+
* a test bundle.
|
|
27
|
+
*/
|
|
28
|
+
export interface DeviceFlowClient {
|
|
29
|
+
/**
|
|
30
|
+
* Exchange the device-flow `sessionToken` for the first access + refresh
|
|
31
|
+
* token, planting them on the client. Single-use; replay is rejected by the
|
|
32
|
+
* API. No bearer required — the high-entropy `sessionToken` IS the credential.
|
|
33
|
+
*/
|
|
34
|
+
claimSessionByToken: (sessionToken: string) => Promise<unknown>;
|
|
35
|
+
}
|
|
36
|
+
export interface CompleteDeviceFlowSignInOptions {
|
|
37
|
+
/** The OxyServices client (or any object exposing `claimSessionByToken`). */
|
|
38
|
+
oxyServices: DeviceFlowClient;
|
|
39
|
+
/** The authorized device session id, delivered by the socket / poll / link. */
|
|
40
|
+
sessionId: string;
|
|
41
|
+
/**
|
|
42
|
+
* The secret `sessionToken` generated for THIS flow and registered via
|
|
43
|
+
* `POST /auth/session/create`. Required to claim the first access token.
|
|
44
|
+
*/
|
|
45
|
+
sessionToken: string;
|
|
46
|
+
/**
|
|
47
|
+
* The session-management `switchSession` from `useOxy()`. Hydrates the
|
|
48
|
+
* activated session (validates, fetches the user, persists, updates state).
|
|
49
|
+
* Runs AFTER the bearer is planted so its bearer-protected calls succeed.
|
|
50
|
+
*/
|
|
51
|
+
switchSession: (sessionId: string) => Promise<User>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Complete a device-flow sign-in: claim the first access token with the secret
|
|
55
|
+
* `sessionToken` (planting the bearer), then hydrate the session via
|
|
56
|
+
* `switchSession`. Returns the authenticated user.
|
|
57
|
+
*
|
|
58
|
+
* Throws if either the claim or the switch fails; callers surface a retry UI.
|
|
59
|
+
*/
|
|
60
|
+
export declare function completeDeviceFlowSignIn({ oxyServices, sessionId, sessionToken, switchSession, }: CompleteDeviceFlowSignInOptions): Promise<User>;
|
|
61
|
+
//# sourceMappingURL=deviceFlowSignIn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deviceFlowSignIn.d.ts","sourceRoot":"","sources":["../../../../src/utils/deviceFlowSignIn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,mBAAmB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,+BAA+B;IAC9C,6EAA6E;IAC7E,WAAW,EAAE,gBAAgB,CAAC;IAC9B,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAAC,EAC7C,WAAW,EACX,SAAS,EACT,YAAY,EACZ,aAAa,GACd,EAAE,+BAA+B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOjD"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, pure helpers for building the `origin|baseURL` signature used as the
|
|
3
|
+
* module-level run-once guard key for cold-boot silent-SSO probes
|
|
4
|
+
* (`silentColdBootKey` in `OxyContext`, `ssoSignature` in `useWebSSO`).
|
|
5
|
+
*
|
|
6
|
+
* NATIVE SAFETY (the bug this fixes): React Native aliases a global `window`
|
|
7
|
+
* (it points at the JS global object), so `typeof window !== 'undefined'` is
|
|
8
|
+
* `true` on native — but `window.location` is `undefined`. Reading
|
|
9
|
+
* `window.location.origin` after only a `typeof window` check therefore throws
|
|
10
|
+
* `TypeError: Cannot read property 'origin' of undefined` on native. Because
|
|
11
|
+
* the key is built UNCONDITIONALLY at the top of the cold-boot path (before its
|
|
12
|
+
* try/catch), that throw escaped session restore entirely and broke
|
|
13
|
+
* cross-session restore on native. Both prior copies of the guard had the same
|
|
14
|
+
* insufficient `typeof window` check and were prone to drift, so the read is
|
|
15
|
+
* consolidated here behind a guard that also verifies `window.location`.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Read `window.location.origin` safely on every platform.
|
|
19
|
+
*
|
|
20
|
+
* Returns the browser origin on web, and the sentinel `'no-origin'` anywhere
|
|
21
|
+
* `window.location` is absent (React Native, SSR/Node). Never throws.
|
|
22
|
+
*/
|
|
23
|
+
export declare function safeWindowOrigin(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Build the stable `origin|baseURL` signature for the silent-SSO run-once
|
|
26
|
+
* guard. Two providers pointed at the same API from the same origin share one
|
|
27
|
+
* attempt. `getBaseURL` is invoked defensively (it may be absent or throw on a
|
|
28
|
+
* partially-initialised client); any failure degrades to an empty baseURL.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildSilentGuardKey(getBaseURL?: () => string | undefined): string;
|
|
31
|
+
//# sourceMappingURL=silentGuardKey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"silentGuardKey.d.ts","sourceRoot":"","sources":["../../../../src/utils/silentGuardKey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAKzC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,MAAM,CASjF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/services",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.1.0",
|
|
4
4
|
"description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"peerDependencies": {
|
|
161
161
|
"@expo/vector-icons": "^15.0.3",
|
|
162
162
|
"@oxyhq/bloom": ">=0.5.0",
|
|
163
|
-
"@oxyhq/core": "^3.
|
|
163
|
+
"@oxyhq/core": "^3.2.0",
|
|
164
164
|
"@react-native-community/netinfo": "^11.4.1",
|
|
165
165
|
"@tanstack/query-async-storage-persister": "^5.100",
|
|
166
166
|
"@tanstack/query-sync-storage-persister": "^5.100",
|
|
@@ -103,7 +103,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
|
|
|
103
103
|
children,
|
|
104
104
|
onAuthStateChange,
|
|
105
105
|
storageKeyPrefix,
|
|
106
|
-
|
|
106
|
+
clientId,
|
|
107
107
|
baseURL,
|
|
108
108
|
authWebUrl,
|
|
109
109
|
authRedirectUri,
|
|
@@ -297,7 +297,7 @@ const OxyProvider: FC<OxyProviderProps> = ({
|
|
|
297
297
|
authWebUrl={authWebUrl}
|
|
298
298
|
authRedirectUri={authRedirectUri}
|
|
299
299
|
storageKeyPrefix={storageKeyPrefix}
|
|
300
|
-
|
|
300
|
+
clientId={clientId}
|
|
301
301
|
onAuthStateChange={onAuthStateChange as OxyContextProviderProps['onAuthStateChange']}
|
|
302
302
|
>
|
|
303
303
|
{children}
|
|
@@ -35,6 +35,7 @@ import { Loading } from '@oxyhq/bloom/loading';
|
|
|
35
35
|
import { useOxy } from '../context/OxyContext';
|
|
36
36
|
import OxyLogo from './OxyLogo';
|
|
37
37
|
import { createDebugLogger } from '@oxyhq/core';
|
|
38
|
+
import { completeDeviceFlowSignIn } from '../../utils/deviceFlowSignIn';
|
|
38
39
|
|
|
39
40
|
const debug = createDebugLogger('SignInModal');
|
|
40
41
|
|
|
@@ -93,7 +94,7 @@ const SignInModal: React.FC = () => {
|
|
|
93
94
|
|
|
94
95
|
const insets = useSafeAreaInsets();
|
|
95
96
|
const theme = useTheme();
|
|
96
|
-
const { oxyServices, switchSession,
|
|
97
|
+
const { oxyServices, switchSession, clientId } = useOxy();
|
|
97
98
|
|
|
98
99
|
const socketRef = useRef<Socket | null>(null);
|
|
99
100
|
const pollingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
@@ -153,16 +154,18 @@ const SignInModal: React.FC = () => {
|
|
|
153
154
|
isProcessingRef.current = true;
|
|
154
155
|
|
|
155
156
|
try {
|
|
156
|
-
//
|
|
157
|
-
// session.
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// Now the SDK has a bearer token, the normal session
|
|
162
|
-
// management path can hydrate state from the sessionId.
|
|
163
|
-
if (switchSession) {
|
|
164
|
-
await switchSession(sessionId);
|
|
157
|
+
// Claim the first access token with the secret sessionToken, then
|
|
158
|
+
// hydrate the session. Shared with the native `OxyAuthScreen` via
|
|
159
|
+
// `completeDeviceFlowSignIn` so the two paths cannot drift.
|
|
160
|
+
if (!switchSession) {
|
|
161
|
+
throw new Error('Session management unavailable');
|
|
165
162
|
}
|
|
163
|
+
await completeDeviceFlowSignIn({
|
|
164
|
+
oxyServices,
|
|
165
|
+
sessionId,
|
|
166
|
+
sessionToken,
|
|
167
|
+
switchSession,
|
|
168
|
+
});
|
|
166
169
|
|
|
167
170
|
hideSignInModal();
|
|
168
171
|
} catch (err) {
|
|
@@ -266,6 +269,17 @@ const SignInModal: React.FC = () => {
|
|
|
266
269
|
setError(null);
|
|
267
270
|
isProcessingRef.current = false;
|
|
268
271
|
|
|
272
|
+
// The cross-app device sign-in flow identifies the requesting app by its
|
|
273
|
+
// real registered OAuth client id (ApplicationCredential publicKey).
|
|
274
|
+
// Without it the API cannot resolve the consent identity, so we fail
|
|
275
|
+
// fast with a clear configuration error rather than creating a session
|
|
276
|
+
// the server would reject.
|
|
277
|
+
if (!clientId) {
|
|
278
|
+
setError('This app is not configured for sign-in (missing clientId).');
|
|
279
|
+
setIsLoading(false);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
269
283
|
try {
|
|
270
284
|
const sessionToken = generateSessionToken();
|
|
271
285
|
const expiresAt = Date.now() + AUTH_SESSION_EXPIRY_MS;
|
|
@@ -273,7 +287,7 @@ const SignInModal: React.FC = () => {
|
|
|
273
287
|
await oxyServices.makeRequest('POST', '/auth/session/create', {
|
|
274
288
|
sessionToken,
|
|
275
289
|
expiresAt,
|
|
276
|
-
|
|
290
|
+
clientId,
|
|
277
291
|
}, { cache: false });
|
|
278
292
|
|
|
279
293
|
setAuthSession({ sessionToken, expiresAt });
|
|
@@ -284,7 +298,7 @@ const SignInModal: React.FC = () => {
|
|
|
284
298
|
} finally {
|
|
285
299
|
setIsLoading(false);
|
|
286
300
|
}
|
|
287
|
-
}, [oxyServices, connectSocket,
|
|
301
|
+
}, [oxyServices, connectSocket, clientId]);
|
|
288
302
|
|
|
289
303
|
// Generate a cryptographically random session token.
|
|
290
304
|
// 16 random bytes -> 32 hex chars (128 bits of entropy) — unguessable.
|
|
@@ -42,7 +42,6 @@ import { useDeviceManagement } from '../hooks/useDeviceManagement';
|
|
|
42
42
|
import { getStorageKeys, createPlatformStorage, type StorageInterface } from '../utils/storageHelpers';
|
|
43
43
|
import { isInvalidSessionError, isTimeoutOrNetworkError } from '../utils/errorHandlers';
|
|
44
44
|
import { readActiveAuthuser, writeActiveAuthuser } from '../utils/activeAuthuser';
|
|
45
|
-
import { resolveAppDisplayName } from '../utils/appName';
|
|
46
45
|
import type { RouteName } from '../navigation/routes';
|
|
47
46
|
import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
|
|
48
47
|
import { useQueryClient } from '@tanstack/react-query';
|
|
@@ -51,6 +50,7 @@ import { useAvatarPicker } from '../hooks/useAvatarPicker';
|
|
|
51
50
|
import { useAccountStore } from '../stores/accountStore';
|
|
52
51
|
import { logger as loggerUtil } from '@oxyhq/core';
|
|
53
52
|
import { useWebSSO, isWebBrowser } from '../hooks/useWebSSO';
|
|
53
|
+
import { buildSilentGuardKey } from '../../utils/silentGuardKey';
|
|
54
54
|
|
|
55
55
|
export interface OxyContextState {
|
|
56
56
|
user: User | null;
|
|
@@ -97,7 +97,7 @@ export interface OxyContextState {
|
|
|
97
97
|
// Session management
|
|
98
98
|
logout: (targetSessionId?: string) => Promise<void>;
|
|
99
99
|
logoutAll: () => Promise<void>;
|
|
100
|
-
switchSession: (sessionId: string) => Promise<
|
|
100
|
+
switchSession: (sessionId: string) => Promise<User>;
|
|
101
101
|
removeSession: (sessionId: string) => Promise<void>;
|
|
102
102
|
refreshSessions: () => Promise<void>;
|
|
103
103
|
setLanguage: (languageId: string) => Promise<void>;
|
|
@@ -116,12 +116,16 @@ export interface OxyContextState {
|
|
|
116
116
|
clearAllAccountData: () => Promise<void>;
|
|
117
117
|
storageKeyPrefix: string;
|
|
118
118
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* `
|
|
119
|
+
* The app's Oxy OAuth client id / ApplicationCredential publicKey, as
|
|
120
|
+
* supplied via the `clientId` prop. Required for the cross-app device
|
|
121
|
+
* sign-in flow: the sign-in components send it to
|
|
122
|
+
* `POST /auth/session/create` so the API can identify the requesting app by
|
|
123
|
+
* its real registered client id (the consent identity is then resolved
|
|
124
|
+
* server-side and shown by the central auth web). `null` when the consuming
|
|
125
|
+
* app did not configure a client id — the device sign-in flow surfaces a
|
|
126
|
+
* configuration error in that case.
|
|
123
127
|
*/
|
|
124
|
-
|
|
128
|
+
clientId: string | null;
|
|
125
129
|
oxyServices: OxyServices;
|
|
126
130
|
useFollow?: UseFollowHook;
|
|
127
131
|
showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
|
|
@@ -149,10 +153,10 @@ export interface OxyContextProviderProps {
|
|
|
149
153
|
authRedirectUri?: string;
|
|
150
154
|
storageKeyPrefix?: string;
|
|
151
155
|
/**
|
|
152
|
-
*
|
|
153
|
-
* sign-in
|
|
156
|
+
* The app's Oxy OAuth client id / ApplicationCredential publicKey; required
|
|
157
|
+
* for the cross-app device sign-in flow. See {@link OxyContextState.clientId}.
|
|
154
158
|
*/
|
|
155
|
-
|
|
159
|
+
clientId?: string;
|
|
156
160
|
onAuthStateChange?: (user: User | null) => void;
|
|
157
161
|
onError?: (error: ApiError) => void;
|
|
158
162
|
}
|
|
@@ -181,14 +185,15 @@ const servicesSilentAttempted = new Set<string>();
|
|
|
181
185
|
* Build the `origin|baseURL` signature used as the silent-cold-boot guard key.
|
|
182
186
|
*/
|
|
183
187
|
function silentColdBootKey(oxyServices: OxyServices): string {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
// `buildSilentGuardKey` reads `window.location.origin` behind a guard that
|
|
189
|
+
// also verifies `window.location` exists. This is critical: it runs
|
|
190
|
+
// UNCONDITIONALLY at the top of `restoreSessionsFromStorage` (before the
|
|
191
|
+
// cold-boot try/catch) on EVERY platform, and React Native aliases a global
|
|
192
|
+
// `window` with NO `window.location`. Without that guard the read threw
|
|
193
|
+
// `Cannot read property 'origin' of undefined` on native, escaping the
|
|
194
|
+
// restore path so `markAuthResolved` never ran and stored-session restore was
|
|
195
|
+
// never reached.
|
|
196
|
+
return buildSilentGuardKey(() => oxyServices.getBaseURL?.());
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
/**
|
|
@@ -258,7 +263,12 @@ const COLD_BOOT_OVERALL_DEADLINE = 20000;
|
|
|
258
263
|
* off-browser.
|
|
259
264
|
*/
|
|
260
265
|
function isSameSiteIdP(idpOrigin: string): boolean {
|
|
261
|
-
|
|
266
|
+
// Native defines a global `window` but no `window.location`; guard the
|
|
267
|
+
// latter so reading `.hostname` can never throw off-browser. (Only reachable
|
|
268
|
+
// from the web-only visibility check, but kept robust for parity.)
|
|
269
|
+
if (typeof window === 'undefined' || typeof window.location === 'undefined') {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
262
272
|
let idpHostname: string;
|
|
263
273
|
try {
|
|
264
274
|
idpHostname = new URL(idpOrigin).hostname;
|
|
@@ -314,7 +324,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
314
324
|
authWebUrl,
|
|
315
325
|
authRedirectUri,
|
|
316
326
|
storageKeyPrefix = 'oxy_session',
|
|
317
|
-
|
|
327
|
+
clientId: clientIdProp,
|
|
318
328
|
onAuthStateChange,
|
|
319
329
|
onError,
|
|
320
330
|
}) => {
|
|
@@ -424,13 +434,16 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
424
434
|
|
|
425
435
|
const storageKeys = useMemo(() => getStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
|
|
426
436
|
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
)
|
|
437
|
+
// The app's Oxy OAuth client id surfaced on the context so the cross-app
|
|
438
|
+
// device sign-in components (SignInModal / OxyAuthScreen) can identify the
|
|
439
|
+
// requesting app to `POST /auth/session/create`. Normalized to a trimmed
|
|
440
|
+
// non-empty string, or `null` when the consumer did not configure one — the
|
|
441
|
+
// sign-in components surface a clear configuration error in that case rather
|
|
442
|
+
// than falling back to any display string.
|
|
443
|
+
const clientId = useMemo(() => {
|
|
444
|
+
const trimmed = clientIdProp?.trim();
|
|
445
|
+
return trimmed ? trimmed : null;
|
|
446
|
+
}, [clientIdProp]);
|
|
434
447
|
|
|
435
448
|
// Storage initialization.
|
|
436
449
|
//
|
|
@@ -1542,8 +1555,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1542
1555
|
});
|
|
1543
1556
|
|
|
1544
1557
|
const switchSessionForContext = useCallback(
|
|
1545
|
-
async (sessionId: string): Promise<
|
|
1546
|
-
|
|
1558
|
+
async (sessionId: string): Promise<User> => {
|
|
1559
|
+
// Propagate the activated user so callers (the device-flow sign-in,
|
|
1560
|
+
// `useSwitchSession`'s cache write, account chooser) receive it. The
|
|
1561
|
+
// underlying session-management `switchSession` already resolves the
|
|
1562
|
+
// `User`; the previous `Promise<void>` wrapper discarded it.
|
|
1563
|
+
return switchSession(sessionId);
|
|
1547
1564
|
},
|
|
1548
1565
|
[switchSession],
|
|
1549
1566
|
);
|
|
@@ -1666,7 +1683,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1666
1683
|
clearSessionState,
|
|
1667
1684
|
clearAllAccountData,
|
|
1668
1685
|
storageKeyPrefix,
|
|
1669
|
-
|
|
1686
|
+
clientId,
|
|
1670
1687
|
oxyServices,
|
|
1671
1688
|
useFollow: useFollowHook,
|
|
1672
1689
|
showBottomSheet: showBottomSheetForContext,
|
|
@@ -1695,7 +1712,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1695
1712
|
logoutAllDeviceSessions,
|
|
1696
1713
|
oxyServices,
|
|
1697
1714
|
storageKeyPrefix,
|
|
1698
|
-
|
|
1715
|
+
clientId,
|
|
1699
1716
|
refreshSessionsWithUser,
|
|
1700
1717
|
sessions,
|
|
1701
1718
|
setLanguage,
|
|
@@ -1763,7 +1780,7 @@ const LOADING_STATE: OxyContextState = {
|
|
|
1763
1780
|
handlePopupSession: () => rejectMissingProvider<void>(),
|
|
1764
1781
|
logout: () => rejectMissingProvider<void>(),
|
|
1765
1782
|
logoutAll: () => rejectMissingProvider<void>(),
|
|
1766
|
-
switchSession: () => rejectMissingProvider<
|
|
1783
|
+
switchSession: () => rejectMissingProvider<User>(),
|
|
1767
1784
|
removeSession: () => rejectMissingProvider<void>(),
|
|
1768
1785
|
refreshSessions: () => rejectMissingProvider<void>(),
|
|
1769
1786
|
setLanguage: () => rejectMissingProvider<void>(),
|
|
@@ -1773,7 +1790,7 @@ const LOADING_STATE: OxyContextState = {
|
|
|
1773
1790
|
clearSessionState: () => rejectMissingProvider<void>(),
|
|
1774
1791
|
clearAllAccountData: () => rejectMissingProvider<void>(),
|
|
1775
1792
|
storageKeyPrefix: 'oxy_session',
|
|
1776
|
-
|
|
1793
|
+
clientId: null,
|
|
1777
1794
|
oxyServices: LOADING_STATE_OXY_SERVICES,
|
|
1778
1795
|
openAvatarPicker: () => {},
|
|
1779
1796
|
actingAs: null,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { useEffect, useRef, useCallback } from 'react';
|
|
19
19
|
import type { OxyServices } from '@oxyhq/core';
|
|
20
20
|
import type { SessionLoginResponse } from '@oxyhq/core';
|
|
21
|
+
import { buildSilentGuardKey } from '../../utils/silentGuardKey';
|
|
21
22
|
|
|
22
23
|
interface UseWebSSOOptions {
|
|
23
24
|
oxyServices: OxyServices;
|
|
@@ -58,14 +59,12 @@ const silentSSOAttempted = new Set<string>();
|
|
|
58
59
|
* pointed at the same API from the same origin share one attempt.
|
|
59
60
|
*/
|
|
60
61
|
function ssoSignature(oxyServices: OxyServices): string {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
return `${origin}|${baseURL}`;
|
|
62
|
+
// Shared with `OxyContext.silentColdBootKey`. `buildSilentGuardKey` reads
|
|
63
|
+
// `window.location.origin` behind a guard that also verifies
|
|
64
|
+
// `window.location` exists — React Native aliases a global `window` with NO
|
|
65
|
+
// `window.location`, so a `typeof window`-only check would throw
|
|
66
|
+
// `Cannot read property 'origin' of undefined` off-browser.
|
|
67
|
+
return buildSilentGuardKey(() => oxyServices.getBaseURL?.());
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
/**
|
|
@@ -30,6 +30,7 @@ import { useOxy } from '../context/OxyContext';
|
|
|
30
30
|
import QRCode from 'react-native-qrcode-svg';
|
|
31
31
|
import OxyLogo from '../components/OxyLogo';
|
|
32
32
|
import { createDebugLogger } from '@oxyhq/core';
|
|
33
|
+
import { completeDeviceFlowSignIn } from '../../utils/deviceFlowSignIn';
|
|
33
34
|
|
|
34
35
|
const debug = createDebugLogger('OxyAuthScreen');
|
|
35
36
|
|
|
@@ -119,7 +120,7 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
119
120
|
theme,
|
|
120
121
|
}) => {
|
|
121
122
|
const bloomTheme = useTheme();
|
|
122
|
-
const { oxyServices,
|
|
123
|
+
const { oxyServices, switchSession, clientId } = useOxy();
|
|
123
124
|
|
|
124
125
|
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
|
|
125
126
|
const [isLoading, setIsLoading] = useState(true);
|
|
@@ -132,25 +133,42 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
132
133
|
const isProcessingRef = useRef(false);
|
|
133
134
|
const linkingHandledRef = useRef(false);
|
|
134
135
|
|
|
135
|
-
// Handle successful authorization
|
|
136
|
-
|
|
136
|
+
// Handle successful authorization.
|
|
137
|
+
//
|
|
138
|
+
// The auth-session socket / poll (or deep-link return) hands us the
|
|
139
|
+
// authorized `sessionId`. Before any session-management code can touch it we
|
|
140
|
+
// MUST first exchange the secret `sessionToken` (held only by this client,
|
|
141
|
+
// generated for THIS flow) for the first access token via
|
|
142
|
+
// `claimSessionByToken` — the device-flow equivalent of OAuth's
|
|
143
|
+
// code-for-token exchange (RFC 8628 §3.4).
|
|
144
|
+
//
|
|
145
|
+
// Without that exchange the SDK has no bearer token, so the subsequent
|
|
146
|
+
// `switchSession` -> `getTokenBySession` call (`GET /session/token/:id`) 401s
|
|
147
|
+
// against the C1-hardened API — the session is authorized server-side but the
|
|
148
|
+
// app never becomes authenticated and the sheet sits "Waiting for
|
|
149
|
+
// authorization..." forever. Once `claimSessionByToken` plants the tokens in
|
|
150
|
+
// the HttpService, the rest of the session wiring flows through the normal
|
|
151
|
+
// `switchSession` path. This mirrors `SignInModal`'s web flow exactly.
|
|
152
|
+
const handleAuthSuccess = useCallback(async (sessionId: string, sessionToken: string) => {
|
|
137
153
|
if (isProcessingRef.current) return;
|
|
138
154
|
isProcessingRef.current = true;
|
|
139
155
|
|
|
140
156
|
try {
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
// Claim the first access token with the secret sessionToken, then
|
|
158
|
+
// hydrate the session. The claim step is what the native screen was
|
|
159
|
+
// previously missing — see `completeDeviceFlowSignIn`. `switchSession`
|
|
160
|
+
// is always provided by the context here; the helper requires it.
|
|
161
|
+
if (!switchSession) {
|
|
162
|
+
throw new Error('Session management unavailable');
|
|
163
|
+
}
|
|
164
|
+
const user = await completeDeviceFlowSignIn({
|
|
165
|
+
oxyServices,
|
|
166
|
+
sessionId,
|
|
167
|
+
sessionToken,
|
|
168
|
+
switchSession,
|
|
169
|
+
});
|
|
170
|
+
if (onAuthenticated) {
|
|
171
|
+
onAuthenticated(user);
|
|
154
172
|
}
|
|
155
173
|
} catch (err) {
|
|
156
174
|
debug.error('Error completing auth:', err);
|
|
@@ -189,7 +207,9 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
189
207
|
|
|
190
208
|
if (payload.status === 'authorized' && payload.sessionId) {
|
|
191
209
|
cleanup();
|
|
192
|
-
|
|
210
|
+
// `sessionToken` is this flow's secret credential (in closure) — pass
|
|
211
|
+
// it through so `handleAuthSuccess` can claim the first access token.
|
|
212
|
+
handleAuthSuccess(payload.sessionId, sessionToken);
|
|
193
213
|
} else if (payload.status === 'cancelled') {
|
|
194
214
|
cleanup();
|
|
195
215
|
setError('Authorization was denied.');
|
|
@@ -228,7 +248,9 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
228
248
|
|
|
229
249
|
if (response.authorized && response.sessionId) {
|
|
230
250
|
cleanup();
|
|
231
|
-
|
|
251
|
+
// Pass the original sessionToken (in closure) through; the claim
|
|
252
|
+
// exchange needs it to mint the first access token.
|
|
253
|
+
handleAuthSuccess(response.sessionId, sessionToken);
|
|
232
254
|
} else if (response.status === 'cancelled') {
|
|
233
255
|
cleanup();
|
|
234
256
|
setError('Authorization was denied.');
|
|
@@ -264,6 +286,17 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
264
286
|
setError(null);
|
|
265
287
|
isProcessingRef.current = false;
|
|
266
288
|
|
|
289
|
+
// The cross-app device sign-in flow identifies the requesting app by its
|
|
290
|
+
// real registered OAuth client id (ApplicationCredential publicKey).
|
|
291
|
+
// Without it the API cannot resolve the consent identity, so we fail fast
|
|
292
|
+
// with a clear configuration error rather than creating a session the
|
|
293
|
+
// server would reject.
|
|
294
|
+
if (!clientId) {
|
|
295
|
+
setError('This app is not configured for sign-in (missing clientId).');
|
|
296
|
+
setIsLoading(false);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
267
300
|
try {
|
|
268
301
|
// Generate a unique session token for this auth request
|
|
269
302
|
const sessionToken = generateSessionToken();
|
|
@@ -273,7 +306,7 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
273
306
|
await oxyServices.makeRequest('POST', '/auth/session/create', {
|
|
274
307
|
sessionToken,
|
|
275
308
|
expiresAt,
|
|
276
|
-
|
|
309
|
+
clientId,
|
|
277
310
|
}, { cache: false });
|
|
278
311
|
|
|
279
312
|
setAuthSession({ sessionToken, expiresAt });
|
|
@@ -286,7 +319,7 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
286
319
|
} finally {
|
|
287
320
|
setIsLoading(false);
|
|
288
321
|
}
|
|
289
|
-
}, [oxyServices, connectSocket,
|
|
322
|
+
}, [oxyServices, connectSocket, clientId]);
|
|
290
323
|
|
|
291
324
|
// Generate a random session token
|
|
292
325
|
const generateSessionToken = (): string => {
|
|
@@ -368,10 +401,20 @@ const OxyAuthScreen: React.FC<BaseScreenProps> = ({
|
|
|
368
401
|
}
|
|
369
402
|
|
|
370
403
|
if (params.sessionId) {
|
|
404
|
+
// The deep-link return carries only `session_id` — the secret
|
|
405
|
+
// `sessionToken` for this flow lives in component state (generated in
|
|
406
|
+
// `generateAuthSession`). Without it we cannot claim the first access
|
|
407
|
+
// token, so the flow would 401 in `handleAuthSuccess`. If it is somehow
|
|
408
|
+
// unavailable, fall through to the socket/poll path (which carries the
|
|
409
|
+
// token in closure) rather than attempting an unauthenticated claim.
|
|
410
|
+
const flowSessionToken = authSession?.sessionToken;
|
|
411
|
+
if (!flowSessionToken) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
371
414
|
cleanup();
|
|
372
|
-
handleAuthSuccess(params.sessionId);
|
|
415
|
+
handleAuthSuccess(params.sessionId, flowSessionToken);
|
|
373
416
|
}
|
|
374
|
-
}, [cleanup, handleAuthSuccess]);
|
|
417
|
+
}, [authSession, cleanup, handleAuthSuccess]);
|
|
375
418
|
|
|
376
419
|
useEffect(() => {
|
|
377
420
|
if (Platform.OS === 'web') {
|
|
@@ -53,13 +53,15 @@ export interface OxyProviderProps {
|
|
|
53
53
|
onAuthStateChange?: (user: unknown) => void;
|
|
54
54
|
storageKeyPrefix?: string;
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
56
|
+
* The app's Oxy OAuth client id / ApplicationCredential publicKey.
|
|
57
|
+
* Required for the cross-app device sign-in flow: the QR / popup
|
|
58
|
+
* sign-in registers a device-flow session via `POST /auth/session/create`,
|
|
59
|
+
* which now identifies the requesting app by this real registered
|
|
60
|
+
* client id. The central Oxy auth experience resolves and renders the
|
|
61
|
+
* consent identity from it server-side. Without it the device sign-in
|
|
62
|
+
* flow cannot start.
|
|
61
63
|
*/
|
|
62
|
-
|
|
64
|
+
clientId?: string;
|
|
63
65
|
baseURL?: string;
|
|
64
66
|
authWebUrl?: string;
|
|
65
67
|
authRedirectUri?: string;
|