@oxy.so/services 10.0.0 → 11.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/android/build.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +6 -6
- package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionModule.kt +106 -41
- package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionProvider.kt +45 -32
- package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionStore.kt +22 -27
- package/android/src/main/java/so/oxy/identity/OxyIdentityModule.kt +89 -43
- package/android/src/main/java/so/oxy/security/OxyCallerPolicy.kt +97 -0
- package/android/src/main/java/so/oxy/session/OxyBackgroundSessionStore.kt +4 -10
- package/android/src/main/java/so/oxy/storage/OxyEncryptedPrefs.kt +17 -18
- package/ios/OxyIdentityModule.swift +13 -14
- package/lib/commonjs/ui/boot/runProviderColdBoot.js +16 -3
- package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
- package/lib/commonjs/ui/components/OxySignInButton.js +25 -7
- package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +198 -41
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/commitSessionFlow.js +5 -0
- package/lib/commonjs/ui/context/commitSessionFlow.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +56 -9
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/context/isolatedOAuthSession.js +52 -0
- package/lib/commonjs/ui/context/isolatedOAuthSession.js.map +1 -0
- package/lib/commonjs/ui/context/useOxyAccountGraph.js +7 -5
- package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +7 -3
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/oauth/applicationSessionLane.js +32 -0
- package/lib/commonjs/ui/oauth/applicationSessionLane.js.map +1 -0
- package/lib/commonjs/ui/oauth/completeOAuthCode.js +4 -2
- package/lib/commonjs/ui/oauth/completeOAuthCode.js.map +1 -1
- package/lib/commonjs/ui/oauth/nativeAuthTransport.js +119 -0
- package/lib/commonjs/ui/oauth/nativeAuthTransport.js.map +1 -0
- package/lib/commonjs/ui/session/createSessionClient.js +14 -2
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
- package/lib/commonjs/ui/session/deviceCredentialTracker.js +8 -0
- package/lib/commonjs/ui/session/deviceCredentialTracker.js.map +1 -1
- package/lib/commonjs/ui/session/tokenLossRecovery.js +1 -1
- package/lib/module/ui/boot/runProviderColdBoot.js +16 -3
- package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
- package/lib/module/ui/components/OxySignInButton.js +25 -7
- package/lib/module/ui/components/OxySignInButton.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +200 -43
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/commitSessionFlow.js +5 -0
- package/lib/module/ui/context/commitSessionFlow.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +56 -9
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/context/isolatedOAuthSession.js +44 -0
- package/lib/module/ui/context/isolatedOAuthSession.js.map +1 -0
- package/lib/module/ui/context/useOxyAccountGraph.js +7 -5
- package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +7 -3
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/oauth/applicationSessionLane.js +26 -0
- package/lib/module/ui/oauth/applicationSessionLane.js.map +1 -0
- package/lib/module/ui/oauth/completeOAuthCode.js +4 -2
- package/lib/module/ui/oauth/completeOAuthCode.js.map +1 -1
- package/lib/module/ui/oauth/nativeAuthTransport.js +115 -0
- package/lib/module/ui/oauth/nativeAuthTransport.js.map +1 -0
- package/lib/module/ui/session/createSessionClient.js +14 -2
- package/lib/module/ui/session/createSessionClient.js.map +1 -1
- package/lib/module/ui/session/deviceCredentialTracker.js +8 -0
- package/lib/module/ui/session/deviceCredentialTracker.js.map +1 -1
- package/lib/module/ui/session/tokenLossRecovery.js +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts +9 -2
- package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts +2 -0
- package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +3 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/commitSessionFlow.d.ts +2 -0
- package/lib/typescript/commonjs/ui/context/commitSessionFlow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +3 -2
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/isolatedOAuthSession.d.ts +19 -0
- package/lib/typescript/commonjs/ui/context/isolatedOAuthSession.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +3 -0
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +2 -1
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/oauth/applicationSessionLane.d.ts +6 -0
- package/lib/typescript/commonjs/ui/oauth/applicationSessionLane.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/oauth/completeOAuthCode.d.ts +2 -2
- package/lib/typescript/commonjs/ui/oauth/nativeAuthTransport.d.ts +28 -0
- package/lib/typescript/commonjs/ui/oauth/nativeAuthTransport.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +1 -1
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/deviceCredentialTracker.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts +9 -2
- package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxySignInButton.d.ts +2 -0
- package/lib/typescript/module/ui/components/OxySignInButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +3 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/commitSessionFlow.d.ts +2 -0
- package/lib/typescript/module/ui/context/commitSessionFlow.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +3 -2
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/isolatedOAuthSession.d.ts +19 -0
- package/lib/typescript/module/ui/context/isolatedOAuthSession.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +3 -0
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +2 -1
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/oauth/applicationSessionLane.d.ts +6 -0
- package/lib/typescript/module/ui/oauth/applicationSessionLane.d.ts.map +1 -0
- package/lib/typescript/module/ui/oauth/completeOAuthCode.d.ts +2 -2
- package/lib/typescript/module/ui/oauth/nativeAuthTransport.d.ts +28 -0
- package/lib/typescript/module/ui/oauth/nativeAuthTransport.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +1 -1
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/deviceCredentialTracker.d.ts.map +1 -1
- package/package.json +30 -30
- package/plugins/withOxySharedPermissions.js +89 -0
- package/plugins/withSharedDeviceSessionProvider.js +26 -71
- package/src/index.ts +2 -0
- package/src/ui/boot/__tests__/coldBootNoIdpRedirect.test.ts +56 -2
- package/src/ui/boot/runProviderColdBoot.ts +24 -4
- package/src/ui/components/OxySignInButton.tsx +20 -4
- package/src/ui/context/OxyContext.tsx +171 -40
- package/src/ui/context/__tests__/commitSessionFlow.test.ts +13 -0
- package/src/ui/context/__tests__/isolatedOAuthSession.test.ts +35 -0
- package/src/ui/context/commitSessionFlow.ts +8 -0
- package/src/ui/context/hooks/useAuthOperations.ts +55 -9
- package/src/ui/context/isolatedOAuthSession.ts +48 -0
- package/src/ui/context/oxyContextTypes.ts +4 -0
- package/src/ui/context/useOxyAccountGraph.ts +8 -5
- package/src/ui/hooks/useAuth.ts +6 -2
- package/src/ui/oauth/__tests__/completeOAuthCode.test.ts +1 -1
- package/src/ui/oauth/__tests__/nativeAuthTransport.test.ts +57 -0
- package/src/ui/oauth/__tests__/oauthBearerCommit.http.test.ts +131 -0
- package/src/ui/oauth/applicationSessionLane.ts +26 -0
- package/src/ui/oauth/completeOAuthCode.ts +3 -3
- package/src/ui/oauth/nativeAuthTransport.ts +67 -0
- package/src/ui/session/__tests__/tokenLossRecovery.test.ts +2 -2
- package/src/ui/session/createSessionClient.ts +13 -1
- package/src/ui/session/deviceCredentialTracker.ts +7 -1
- package/src/ui/session/tokenLossRecovery.ts +1 -1
- package/android/src/main/java/so/oxy/identity/OxyIdentityProvider.kt +0 -77
- package/android/src/main/java/so/oxy/identity/OxyIdentityStore.kt +0 -66
- package/plugins/withSharedDeviceSessionReader.js +0 -57
- package/plugins/withSharedIdentityProvider.js +0 -83
- package/plugins/withSharedIdentityReader.js +0 -46
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config plugin: withOxySharedPermissions (EVERY Oxy Android app).
|
|
3
|
+
*
|
|
4
|
+
* Oxy Android apps do not share a UID. They share the Oxy identity and the
|
|
5
|
+
* device session with Commons (and Accounts) through signature-protected
|
|
6
|
+
* ContentProvider `call()` IPC, the way apps use `AccountManager`. Crossing a UID
|
|
7
|
+
* needs two `signature`-level permissions:
|
|
8
|
+
*
|
|
9
|
+
* - `so.oxy.permission.IDENTITY` — Commons' identity host
|
|
10
|
+
* (`so.oxy.commons[.dev].identity`): public key, challenge proofs, scoped
|
|
11
|
+
* derivations. Never the private key.
|
|
12
|
+
* - `so.oxy.permission.DEVICE_SESSION` — the device-session hosts
|
|
13
|
+
* (`so.oxy.commons[.dev].devicesession`, `so.oxy.accounts[.dev].devicesession`).
|
|
14
|
+
*
|
|
15
|
+
* This plugin makes the app both DECLARE and REQUEST both, and adds `<queries>`
|
|
16
|
+
* for every host authority:
|
|
17
|
+
*
|
|
18
|
+
* - Declared by every app, not only by the hosts: a signature permission is
|
|
19
|
+
* granted when the requesting app is installed, if the declaring app is
|
|
20
|
+
* already there. With every app declaring it, install order never matters.
|
|
21
|
+
* Android accepts the same declaration from several packages signed with one
|
|
22
|
+
* certificate, and rejects it from a differently-signed one
|
|
23
|
+
* (`INSTALL_FAILED_DUPLICATE_PERMISSION`), which is the protection we want.
|
|
24
|
+
* Every declaration must be identical, which is why this ONE plugin owns
|
|
25
|
+
* them; the provider plugins do not declare anything.
|
|
26
|
+
* - `<queries>`: Android 11+ package visibility hides a provider the app has
|
|
27
|
+
* not declared an interest in, and `ContentResolver.call` then silently finds
|
|
28
|
+
* nothing.
|
|
29
|
+
*
|
|
30
|
+
* `signature` is the whole trust boundary at the manifest level; each provider
|
|
31
|
+
* also checks the calling package and its certificate itself.
|
|
32
|
+
*/
|
|
33
|
+
const { withAndroidManifest } = require('@expo/config-plugins');
|
|
34
|
+
|
|
35
|
+
const IDENTITY_PERMISSION = 'so.oxy.permission.IDENTITY';
|
|
36
|
+
const DEVICE_SESSION_PERMISSION = 'so.oxy.permission.DEVICE_SESSION';
|
|
37
|
+
const PERMISSIONS = [IDENTITY_PERMISSION, DEVICE_SESSION_PERMISSION];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Every host authority an Oxy app calls. Keep in step with `COMMONS_AUTHORITIES`
|
|
41
|
+
* in `OxyIdentityModule.kt` and `HOST_AUTHORITIES` in `OxyDeviceSessionModule.kt`
|
|
42
|
+
* (a test compares them).
|
|
43
|
+
*/
|
|
44
|
+
const HOST_AUTHORITIES = [
|
|
45
|
+
'so.oxy.commons.identity',
|
|
46
|
+
'so.oxy.commons.dev.identity',
|
|
47
|
+
'so.oxy.commons.devicesession',
|
|
48
|
+
'so.oxy.commons.dev.devicesession',
|
|
49
|
+
'so.oxy.accounts.devicesession',
|
|
50
|
+
'so.oxy.accounts.dev.devicesession',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function withOxySharedPermissions(config) {
|
|
54
|
+
return withAndroidManifest(config, (modConfig) => {
|
|
55
|
+
const manifest = modConfig.modResults.manifest;
|
|
56
|
+
|
|
57
|
+
manifest.permission = manifest.permission ?? [];
|
|
58
|
+
manifest['uses-permission'] = manifest['uses-permission'] ?? [];
|
|
59
|
+
for (const name of PERMISSIONS) {
|
|
60
|
+
if (!manifest.permission.some((p) => p.$['android:name'] === name)) {
|
|
61
|
+
manifest.permission.push({
|
|
62
|
+
$: { 'android:name': name, 'android:protectionLevel': 'signature' },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!manifest['uses-permission'].some((p) => p.$['android:name'] === name)) {
|
|
66
|
+
manifest['uses-permission'].push({ $: { 'android:name': name } });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
manifest.queries = manifest.queries ?? [];
|
|
71
|
+
if (manifest.queries.length === 0) {
|
|
72
|
+
manifest.queries.push({});
|
|
73
|
+
}
|
|
74
|
+
const queries = manifest.queries[0];
|
|
75
|
+
queries.provider = queries.provider ?? [];
|
|
76
|
+
for (const authority of HOST_AUTHORITIES) {
|
|
77
|
+
if (!queries.provider.some((p) => p.$['android:authorities'] === authority)) {
|
|
78
|
+
queries.provider.push({ $: { 'android:authorities': authority } });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return modConfig;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = withOxySharedPermissions;
|
|
87
|
+
module.exports.IDENTITY_PERMISSION = IDENTITY_PERMISSION;
|
|
88
|
+
module.exports.DEVICE_SESSION_PERMISSION = DEVICE_SESSION_PERMISSION;
|
|
89
|
+
module.exports.HOST_AUTHORITIES = HOST_AUTHORITIES;
|
|
@@ -1,86 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config plugin: withSharedDeviceSessionProvider (
|
|
2
|
+
* Config plugin: withSharedDeviceSessionProvider (the device-session HOSTS:
|
|
3
|
+
* Commons and Accounts, prod and dev).
|
|
3
4
|
*
|
|
4
|
-
* The shared DeviceSession credential is how
|
|
5
|
+
* The shared DeviceSession credential is how the official Oxy apps end up on
|
|
5
6
|
* ONE `DeviceSession` — and therefore one globally active account context —
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
7
|
+
* without any of them touching the identity key. Oxy apps do not share a UID,
|
|
8
|
+
* so the credential lives in the hosts' own storage, and this plugin declares
|
|
9
|
+
* the provider every other app reads and writes it through:
|
|
10
|
+
* `OxyDeviceSessionProvider` at `${applicationId}.devicesession` (AGP
|
|
11
|
+
* substitutes `${applicationId}` at build), guarded by the `signature`-level
|
|
12
|
+
* `so.oxy.permission.DEVICE_SESSION`.
|
|
8
13
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* it — the trust boundary is the signing key, not the deprecated `sharedUserId`.
|
|
13
|
-
* - Requests that same permission so this app can also read cross-authority.
|
|
14
|
-
* - Declares `OxyDeviceSessionProvider` at authority
|
|
15
|
-
* `${applicationId}.devicesession` (AGP substitutes `${applicationId}` at
|
|
16
|
-
* build), guarded by that permission. The provider is READ-only across the
|
|
17
|
-
* process boundary: a sibling may join the device session, never overwrite it.
|
|
18
|
-
* - Adds `<queries>` entries so Android 11+ package-visibility filtering never
|
|
19
|
-
* hides the sibling providers from the resolver.
|
|
14
|
+
* It declares NO permission itself: every Oxy app, host or not, declares and
|
|
15
|
+
* requests both Oxy permissions through `withOxySharedPermissions`, so the
|
|
16
|
+
* declarations stay identical. Apply that plugin too.
|
|
20
17
|
*
|
|
21
|
-
* WHICH APPS USE THIS PLUGIN: exactly the apps listed in
|
|
22
|
-
*
|
|
23
|
-
* data directory, so they all serve the SAME file and the sweep in
|
|
24
|
-
* `OxyDeviceSessionModule` is deterministic whichever one answers. An app that is
|
|
25
|
-
* same-signature but outside that UID must use `withSharedDeviceSessionReader`
|
|
26
|
-
* instead — hosting from there would publish a second, independent copy of "the
|
|
27
|
-
* shared credential" and make the winner depend on list order.
|
|
28
|
-
*
|
|
29
|
-
* Keep this list identical to `PROVIDER_AUTHORITIES` in
|
|
30
|
-
* `android/src/main/java/so/oxy/devicesession/OxyDeviceSessionModule.kt`.
|
|
31
|
-
*
|
|
32
|
-
* This is deliberately a SEPARATE permission and a SEPARATE provider from
|
|
33
|
-
* `withSharedIdentityProvider`. Two secrets with different blast radii: the
|
|
34
|
-
* identity key is self-custody and irreplaceable, the device credential is
|
|
35
|
-
* server-revocable and re-publishable. An app that only needs a session gets only
|
|
36
|
-
* the second one.
|
|
18
|
+
* WHICH APPS USE THIS PLUGIN: exactly the apps whose authorities are listed in
|
|
19
|
+
* `HOST_AUTHORITIES` of `OxyDeviceSessionModule.kt` (a test compares them).
|
|
37
20
|
*/
|
|
38
21
|
const { withAndroidManifest } = require('@expo/config-plugins');
|
|
39
22
|
|
|
40
|
-
const
|
|
23
|
+
const DEVICE_SESSION_PERMISSION = 'so.oxy.permission.DEVICE_SESSION';
|
|
41
24
|
const PROVIDER_CLASS = 'so.oxy.devicesession.OxyDeviceSessionProvider';
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
|
|
26
|
+
/** The authorities this plugin can end up hosting, one per host app. */
|
|
27
|
+
const HOST_AUTHORITIES = [
|
|
45
28
|
'so.oxy.commons.devicesession',
|
|
46
29
|
'so.oxy.commons.dev.devicesession',
|
|
30
|
+
'so.oxy.accounts.devicesession',
|
|
31
|
+
'so.oxy.accounts.dev.devicesession',
|
|
47
32
|
];
|
|
48
33
|
|
|
49
|
-
|
|
34
|
+
function withSharedDeviceSessionProvider(config) {
|
|
50
35
|
return withAndroidManifest(config, (modConfig) => {
|
|
51
36
|
const manifest = modConfig.modResults.manifest;
|
|
52
|
-
|
|
53
|
-
// 1. Define the signature-level permission.
|
|
54
|
-
manifest['permission'] = manifest['permission'] ?? [];
|
|
55
|
-
if (!manifest['permission'].some((p) => p.$['android:name'] === READ_DEVICE_SESSION_PERMISSION)) {
|
|
56
|
-
manifest['permission'].push({
|
|
57
|
-
$: {
|
|
58
|
-
'android:name': READ_DEVICE_SESSION_PERMISSION,
|
|
59
|
-
'android:protectionLevel': 'signature',
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// 2. Request it (a hub app reads its siblings too — prod ⇆ dev variant).
|
|
65
|
-
manifest['uses-permission'] = manifest['uses-permission'] ?? [];
|
|
66
|
-
if (!manifest['uses-permission'].some((p) => p.$['android:name'] === READ_DEVICE_SESSION_PERMISSION)) {
|
|
67
|
-
manifest['uses-permission'].push({ $: { 'android:name': READ_DEVICE_SESSION_PERMISSION } });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 3. Make the sibling provider authorities visible under package filtering.
|
|
71
|
-
manifest['queries'] = manifest['queries'] ?? [];
|
|
72
|
-
if (manifest['queries'].length === 0) {
|
|
73
|
-
manifest['queries'].push({});
|
|
74
|
-
}
|
|
75
|
-
const queries = manifest['queries'][0];
|
|
76
|
-
queries.provider = queries.provider ?? [];
|
|
77
|
-
for (const authority of PROVIDER_AUTHORITIES) {
|
|
78
|
-
if (!queries.provider.some((p) => p.$['android:authorities'] === authority)) {
|
|
79
|
-
queries.provider.push({ $: { 'android:authorities': authority } });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 4. Host the provider.
|
|
84
37
|
const app = manifest.application?.[0];
|
|
85
38
|
if (!app) {
|
|
86
39
|
throw new Error('withSharedDeviceSessionProvider: AndroidManifest has no <application>');
|
|
@@ -92,11 +45,13 @@ module.exports = function withSharedDeviceSessionProvider(config) {
|
|
|
92
45
|
'android:name': PROVIDER_CLASS,
|
|
93
46
|
'android:authorities': '${applicationId}.devicesession',
|
|
94
47
|
'android:exported': 'true',
|
|
95
|
-
'android:permission':
|
|
48
|
+
'android:permission': DEVICE_SESSION_PERMISSION,
|
|
96
49
|
},
|
|
97
50
|
});
|
|
98
51
|
}
|
|
99
|
-
|
|
100
52
|
return modConfig;
|
|
101
53
|
});
|
|
102
|
-
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = withSharedDeviceSessionProvider;
|
|
57
|
+
module.exports.HOST_AUTHORITIES = HOST_AUTHORITIES;
|
package/src/index.ts
CHANGED
|
@@ -78,7 +78,7 @@ function makeHarness(overrides: Partial<RunProviderColdBootOptions> = {}): Harne
|
|
|
78
78
|
const sessionClientStart = jest.fn(async () => undefined);
|
|
79
79
|
|
|
80
80
|
const options: RunProviderColdBootOptions = {
|
|
81
|
-
oxyServices: {} as RunProviderColdBootOptions['oxyServices'],
|
|
81
|
+
oxyServices: { apps: { getPublic: jest.fn(async () => ({ id: 'registered-app', name: 'Registered App', type: 'first_party', isOfficial: false, isInternal: false, scopes: [] })) } } as unknown as RunProviderColdBootOptions['oxyServices'],
|
|
82
82
|
authStore: makeEmptyAuthStore(),
|
|
83
83
|
clientId: CLIENT_ID,
|
|
84
84
|
sessionClient: { start: sessionClientStart } as unknown as SessionClient,
|
|
@@ -198,7 +198,7 @@ describe('runProviderColdBoot — no SDK-initiated IdP navigation (#691 phase 7b
|
|
|
198
198
|
await commit.commitSession({ sessionId: 's1', userId: 'u1' });
|
|
199
199
|
expect(options.commitSession).toHaveBeenCalledWith(
|
|
200
200
|
{ sessionId: 's1', userId: 'u1' },
|
|
201
|
-
{ activate: true },
|
|
201
|
+
{ activate: true, oauth: true },
|
|
202
202
|
);
|
|
203
203
|
});
|
|
204
204
|
});
|
|
@@ -213,3 +213,57 @@ describe('runProviderColdBoot — no SDK-initiated IdP navigation (#691 phase 7b
|
|
|
213
213
|
expect(mockRedirectToAuthorize).not.toHaveBeenCalled();
|
|
214
214
|
});
|
|
215
215
|
});
|
|
216
|
+
|
|
217
|
+
describe('registered cold-boot classification', () => {
|
|
218
|
+
beforeEach(() => { mockRunSessionColdBoot.mockReset(); mockTryCompleteOAuthReturn.mockReset(); mockTryCompleteOAuthReturn.mockResolvedValue(false); });
|
|
219
|
+
it('external classification bypasses prior secrets, shared device and identity pin work', async () => {
|
|
220
|
+
const { options } = makeHarness();
|
|
221
|
+
(options.oxyServices.apps.getPublic as jest.Mock).mockResolvedValue({ id: 'external', name: 'External', type: 'third_party', isOfficial: false, isInternal: false, scopes: [] });
|
|
222
|
+
const beforeDeviceBoot = jest.fn();
|
|
223
|
+
await runProviderColdBoot({ ...options, beforeDeviceBoot });
|
|
224
|
+
expect(options.oxyServices.apps.getPublic).toHaveBeenCalledWith(CLIENT_ID, { cache: false });
|
|
225
|
+
expect(options.authStore.load).not.toHaveBeenCalled();
|
|
226
|
+
expect(options.authStore.clear).not.toHaveBeenCalled();
|
|
227
|
+
expect(options.syncDeviceCredentialToHost).not.toHaveBeenCalled();
|
|
228
|
+
expect(beforeDeviceBoot).not.toHaveBeenCalled();
|
|
229
|
+
expect(mockRunSessionColdBoot).not.toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
it.each(['missing', 'inactive', 'offline', 'invalid'])('fails closed for %s before secrets and callback work', async (mode) => {
|
|
232
|
+
const { options } = makeHarness();
|
|
233
|
+
if (mode === 'missing') options.clientId = undefined;
|
|
234
|
+
else if (mode === 'invalid') (options.oxyServices.apps.getPublic as jest.Mock).mockResolvedValue({ type: 'unknown' });
|
|
235
|
+
else (options.oxyServices.apps.getPublic as jest.Mock).mockRejectedValue(new Error(mode));
|
|
236
|
+
await runProviderColdBoot(options);
|
|
237
|
+
expect(options.markAuthResolved).toHaveBeenCalled();
|
|
238
|
+
expect(options.authStore.load).not.toHaveBeenCalled();
|
|
239
|
+
expect(options.authStore.clear).not.toHaveBeenCalled();
|
|
240
|
+
expect(options.syncDeviceCredentialToHost).not.toHaveBeenCalled();
|
|
241
|
+
expect(mockRunSessionColdBoot).not.toHaveBeenCalled();
|
|
242
|
+
expect(mockTryCompleteOAuthReturn).not.toHaveBeenCalled();
|
|
243
|
+
});
|
|
244
|
+
it('a cold external OAuth callback never probes a previous device secret', async () => {
|
|
245
|
+
const { options } = makeHarness();
|
|
246
|
+
(options.oxyServices.apps.getPublic as jest.Mock).mockResolvedValue({ id: 'external', name: 'External', type: 'third_party', isOfficial: false, isInternal: false, scopes: [] });
|
|
247
|
+
mockTryCompleteOAuthReturn.mockImplementation(async ({ commitSession }) => {
|
|
248
|
+
await commitSession({ sessionId: 'isolated', userId: 'external-user', accessToken: 'isolated-bearer' }); return true;
|
|
249
|
+
});
|
|
250
|
+
await runProviderColdBoot(options);
|
|
251
|
+
expect(options.commitSession).toHaveBeenCalledWith({ sessionId: 'isolated', userId: 'external-user', accessToken: 'isolated-bearer' }, { activate: true, oauth: true });
|
|
252
|
+
expect(options.authStore.load).not.toHaveBeenCalled();
|
|
253
|
+
expect(options.syncDeviceCredentialToHost).not.toHaveBeenCalled();
|
|
254
|
+
expect(mockRunSessionColdBoot).not.toHaveBeenCalled();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('an unresponsive registry resolves signed-out without a late privileged continuation', async () => {
|
|
259
|
+
jest.useFakeTimers();
|
|
260
|
+
try {
|
|
261
|
+
const {options}=makeHarness(); let release!: (app:unknown)=>void;
|
|
262
|
+
(options.oxyServices.apps.getPublic as jest.Mock).mockImplementation(()=>new Promise(resolve=>{release=resolve;}));
|
|
263
|
+
mockRunSessionColdBoot.mockReset(); mockTryCompleteOAuthReturn.mockReset();
|
|
264
|
+
const boot=runProviderColdBoot(options); await jest.advanceTimersByTimeAsync(5000); await boot;
|
|
265
|
+
expect(options.markAuthResolved).toHaveBeenCalled(); expect(options.authStore.load).not.toHaveBeenCalled();
|
|
266
|
+
release({id:'app',name:'Late',type:'first_party',isOfficial:true,isInternal:false,scopes:[]}); await Promise.resolve();
|
|
267
|
+
expect(mockRunSessionColdBoot).not.toHaveBeenCalled(); expect(mockTryCompleteOAuthReturn).not.toHaveBeenCalled();
|
|
268
|
+
} finally {jest.useRealTimers();}
|
|
269
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveApplicationSessionLane } from '../oauth/applicationSessionLane';
|
|
1
2
|
import { logger as loggerUtil, type OxyServices } from '@oxy.so/core';
|
|
2
3
|
import { runSessionColdBoot, type AuthStateStore, type IdentityBinding, type SessionMode } from '@oxy.so/core/session';
|
|
3
4
|
import type { SessionClient } from '@oxy.so/core/session';
|
|
@@ -80,8 +81,14 @@ export interface RunProviderColdBootOptions {
|
|
|
80
81
|
/** The identity binding required by `sessionMode: 'identity'`. */
|
|
81
82
|
identity?: IdentityBinding;
|
|
82
83
|
sessionClient: SessionClient;
|
|
84
|
+
/** Provider-owned fresh classifier, also used by explicit action recovery. */
|
|
85
|
+
resolveSessionLane?: () => Promise<'device' | 'oauth'>;
|
|
86
|
+
/** Identity pin work may run only after a fresh device classification. */
|
|
87
|
+
onClassified?: (lane: 'device' | 'oauth') => void | Promise<void>;
|
|
88
|
+
beforeDeviceBoot?: () => Promise<void>;
|
|
89
|
+
afterDeviceBoot?: () => Promise<void>;
|
|
83
90
|
syncDeviceCredentialToHost: () => Promise<void>;
|
|
84
|
-
commitSession: (input: CommitInput, options: { activate: boolean }) => Promise<void>;
|
|
91
|
+
commitSession: (input: CommitInput, options: { activate: boolean; oauth?: boolean }) => Promise<void>;
|
|
85
92
|
markAuthResolved: () => void;
|
|
86
93
|
setTokenReady: (ready: boolean) => void;
|
|
87
94
|
}
|
|
@@ -91,8 +98,8 @@ export interface RunProviderColdBootOptions {
|
|
|
91
98
|
*
|
|
92
99
|
* Ordered pipeline:
|
|
93
100
|
* 1. Complete an OAuth authorization-code return already on the URL (web)
|
|
94
|
-
* 2. `runSessionColdBoot` — device-secret mint (+ native
|
|
95
|
-
* primary-identity-key lane in `sessionMode: 'identity'`)
|
|
101
|
+
* 2. `runSessionColdBoot` — device-secret mint (+ the native Commons-proof lane,
|
|
102
|
+
* or the primary-identity-key lane in `sessionMode: 'identity'`)
|
|
96
103
|
*
|
|
97
104
|
* Step 1 is ACCOUNT-MODE ONLY: it commits whichever account the IdP resolves,
|
|
98
105
|
* which for an identity-bound client is somebody else's account by construction.
|
|
@@ -124,6 +131,11 @@ export async function runProviderColdBoot(opts: RunProviderColdBootOptions): Pro
|
|
|
124
131
|
setTokenReady(false);
|
|
125
132
|
|
|
126
133
|
try {
|
|
134
|
+
// Classification precedes every local-secret, shared-credential and identity
|
|
135
|
+
// probe. Unavailable registry metadata fails closed, including offline boot.
|
|
136
|
+
const lane = await (opts.resolveSessionLane?.() ?? resolveApplicationSessionLane(oxyServices, clientId));
|
|
137
|
+
await opts.onClassified?.(lane);
|
|
138
|
+
if (identityBound && lane !== 'device') return;
|
|
127
139
|
// The redirect transport's RETURN leg. An app reaches it whenever the browser
|
|
128
140
|
// BLOCKED the sign-in popup and `startWebOAuthSignIn` fell back to a
|
|
129
141
|
// full-page redirect (`popup-blocked` / `popup-navigation-failed`), so it is
|
|
@@ -136,7 +148,7 @@ export async function runProviderColdBoot(opts: RunProviderColdBootOptions): Pro
|
|
|
136
148
|
oxyServices,
|
|
137
149
|
clientId,
|
|
138
150
|
authRedirectUri,
|
|
139
|
-
commitSession: (input) => commitSession(input, { activate: true }),
|
|
151
|
+
commitSession: (input) => commitSession(input, { activate: true, oauth: true }),
|
|
140
152
|
});
|
|
141
153
|
if (oauthCompleted) {
|
|
142
154
|
setTokenReady(true);
|
|
@@ -144,6 +156,13 @@ export async function runProviderColdBoot(opts: RunProviderColdBootOptions): Pro
|
|
|
144
156
|
return;
|
|
145
157
|
}
|
|
146
158
|
|
|
159
|
+
if (lane === 'oauth') {
|
|
160
|
+
setTokenReady(true);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await opts.beforeDeviceBoot?.();
|
|
165
|
+
|
|
147
166
|
// Best-effort connectivity probe up front: an EXPLICIT offline verdict skips
|
|
148
167
|
// the two doomed network steps so routing settles immediately instead of
|
|
149
168
|
// burning the overall deadline on a mint that cannot succeed. Any ambiguity
|
|
@@ -224,6 +243,7 @@ export async function runProviderColdBoot(opts: RunProviderColdBootOptions): Pro
|
|
|
224
243
|
}
|
|
225
244
|
},
|
|
226
245
|
});
|
|
246
|
+
await opts.afterDeviceBoot?.();
|
|
227
247
|
} catch (error) {
|
|
228
248
|
if (__DEV__) {
|
|
229
249
|
loggerUtil.error(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyApplicationSessionLane } from '../oauth/applicationSessionLane';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
import { useCallback, useState, useEffect, useRef } from 'react';
|
|
3
4
|
import { type ViewStyle, type TextStyle, type StyleProp, Platform } from 'react-native';
|
|
@@ -142,6 +143,8 @@ export interface OxySignInButtonProps {
|
|
|
142
143
|
* ```
|
|
143
144
|
*/
|
|
144
145
|
onOAuthResult?: (result: OxyOAuthResult) => void;
|
|
146
|
+
/** Opt in to the shared SDK native finalizer. Mutually exclusive with onOAuthResult. */
|
|
147
|
+
nativeOAuthCompletion?: 'sdk';
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
const ACTION_KEYS: Record<OxySignInButtonAction, string> = {
|
|
@@ -188,9 +191,10 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
188
191
|
showWhenAuthenticated = false,
|
|
189
192
|
oauthRedirectUri,
|
|
190
193
|
onOAuthResult,
|
|
194
|
+
nativeOAuthCompletion,
|
|
191
195
|
}) => {
|
|
192
196
|
const { t } = useI18n();
|
|
193
|
-
const { openAccountDialog, oxyServices, clientId, webAuthMode, startWebOAuthSignIn } = useOxy();
|
|
197
|
+
const { openAccountDialog, oxyServices, clientId, webAuthMode, startWebOAuthSignIn, startNativeOAuthSignIn } = useOxy();
|
|
194
198
|
const { isAuthenticated, isLoading } = useAuthStore(
|
|
195
199
|
useShallow((state) => ({ isAuthenticated: state.isAuthenticated, isLoading: state.isLoading }))
|
|
196
200
|
);
|
|
@@ -228,7 +232,7 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
228
232
|
if (cached && cached.clientId === clientId && cached.oxyServices === oxyServices) {
|
|
229
233
|
return cached.promise;
|
|
230
234
|
}
|
|
231
|
-
const promise = oxyServices.apps.getPublic(clientId).catch((error) => {
|
|
235
|
+
const promise = oxyServices.apps.getPublic(clientId, { cache: false }).catch((error) => {
|
|
232
236
|
// Only clear if this is still the live entry (a later resolve may have
|
|
233
237
|
// replaced it after a clientId/oxyServices change).
|
|
234
238
|
if (appResolutionRef.current?.promise === promise) {
|
|
@@ -308,6 +312,14 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
308
312
|
return;
|
|
309
313
|
}
|
|
310
314
|
|
|
315
|
+
if (nativeOAuthCompletion === 'sdk') {
|
|
316
|
+
if (onOAuthResult) { notifyNotConfigured(app.name); return; }
|
|
317
|
+
const result = await startNativeOAuthSignIn({ redirectUri: oauthRedirectUri });
|
|
318
|
+
if (result.status === 'failed') notifyFailed();
|
|
319
|
+
if (result.status === 'unsupported') notifyNotConfigured(app.name);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
311
323
|
// Native: open the in-app auth session, then hand the handshake to the
|
|
312
324
|
// RP so it can complete the token exchange from its deep-link callback.
|
|
313
325
|
const [pkce, state] = await Promise.all([generatePkcePair(), generateOAuthState()]);
|
|
@@ -332,6 +344,8 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
332
344
|
clientId,
|
|
333
345
|
oauthRedirectUri,
|
|
334
346
|
onOAuthResult,
|
|
347
|
+
nativeOAuthCompletion,
|
|
348
|
+
startNativeOAuthSignIn,
|
|
335
349
|
startOfficialSignIn,
|
|
336
350
|
startWebOAuthSignIn,
|
|
337
351
|
shouldPreOpenPopup,
|
|
@@ -357,7 +371,7 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
357
371
|
const resolving = resolvePublicApplication();
|
|
358
372
|
if (!resolving) {
|
|
359
373
|
closeOAuthPopup(popup);
|
|
360
|
-
|
|
374
|
+
notifyNotConfigured('this app');
|
|
361
375
|
return;
|
|
362
376
|
}
|
|
363
377
|
let app: PublicApplication;
|
|
@@ -373,7 +387,7 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
373
387
|
notifyFailed();
|
|
374
388
|
return;
|
|
375
389
|
}
|
|
376
|
-
if (app
|
|
390
|
+
if (classifyApplicationSessionLane(app) === 'oauth') {
|
|
377
391
|
await startThirdPartyOAuth(app, popup);
|
|
378
392
|
return;
|
|
379
393
|
}
|
|
@@ -392,6 +406,7 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
392
406
|
);
|
|
393
407
|
notifyFailed();
|
|
394
408
|
} finally {
|
|
409
|
+
appResolutionRef.current = null;
|
|
395
410
|
routingRef.current = false;
|
|
396
411
|
}
|
|
397
412
|
},
|
|
@@ -400,6 +415,7 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
|
|
|
400
415
|
startOfficialSignIn,
|
|
401
416
|
startThirdPartyOAuth,
|
|
402
417
|
clientId,
|
|
418
|
+
notifyNotConfigured,
|
|
403
419
|
notifyFailed,
|
|
404
420
|
],
|
|
405
421
|
);
|