@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.
Files changed (149) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/AndroidManifest.xml +6 -6
  3. package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionModule.kt +106 -41
  4. package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionProvider.kt +45 -32
  5. package/android/src/main/java/so/oxy/devicesession/OxyDeviceSessionStore.kt +22 -27
  6. package/android/src/main/java/so/oxy/identity/OxyIdentityModule.kt +89 -43
  7. package/android/src/main/java/so/oxy/security/OxyCallerPolicy.kt +97 -0
  8. package/android/src/main/java/so/oxy/session/OxyBackgroundSessionStore.kt +4 -10
  9. package/android/src/main/java/so/oxy/storage/OxyEncryptedPrefs.kt +17 -18
  10. package/ios/OxyIdentityModule.swift +13 -14
  11. package/lib/commonjs/ui/boot/runProviderColdBoot.js +16 -3
  12. package/lib/commonjs/ui/boot/runProviderColdBoot.js.map +1 -1
  13. package/lib/commonjs/ui/components/OxySignInButton.js +25 -7
  14. package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
  15. package/lib/commonjs/ui/context/OxyContext.js +198 -41
  16. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  17. package/lib/commonjs/ui/context/commitSessionFlow.js +5 -0
  18. package/lib/commonjs/ui/context/commitSessionFlow.js.map +1 -1
  19. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +56 -9
  20. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  21. package/lib/commonjs/ui/context/isolatedOAuthSession.js +52 -0
  22. package/lib/commonjs/ui/context/isolatedOAuthSession.js.map +1 -0
  23. package/lib/commonjs/ui/context/useOxyAccountGraph.js +7 -5
  24. package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useAuth.js +7 -3
  26. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  27. package/lib/commonjs/ui/oauth/applicationSessionLane.js +32 -0
  28. package/lib/commonjs/ui/oauth/applicationSessionLane.js.map +1 -0
  29. package/lib/commonjs/ui/oauth/completeOAuthCode.js +4 -2
  30. package/lib/commonjs/ui/oauth/completeOAuthCode.js.map +1 -1
  31. package/lib/commonjs/ui/oauth/nativeAuthTransport.js +119 -0
  32. package/lib/commonjs/ui/oauth/nativeAuthTransport.js.map +1 -0
  33. package/lib/commonjs/ui/session/createSessionClient.js +14 -2
  34. package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
  35. package/lib/commonjs/ui/session/deviceCredentialTracker.js +8 -0
  36. package/lib/commonjs/ui/session/deviceCredentialTracker.js.map +1 -1
  37. package/lib/commonjs/ui/session/tokenLossRecovery.js +1 -1
  38. package/lib/module/ui/boot/runProviderColdBoot.js +16 -3
  39. package/lib/module/ui/boot/runProviderColdBoot.js.map +1 -1
  40. package/lib/module/ui/components/OxySignInButton.js +25 -7
  41. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  42. package/lib/module/ui/context/OxyContext.js +200 -43
  43. package/lib/module/ui/context/OxyContext.js.map +1 -1
  44. package/lib/module/ui/context/commitSessionFlow.js +5 -0
  45. package/lib/module/ui/context/commitSessionFlow.js.map +1 -1
  46. package/lib/module/ui/context/hooks/useAuthOperations.js +56 -9
  47. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  48. package/lib/module/ui/context/isolatedOAuthSession.js +44 -0
  49. package/lib/module/ui/context/isolatedOAuthSession.js.map +1 -0
  50. package/lib/module/ui/context/useOxyAccountGraph.js +7 -5
  51. package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
  52. package/lib/module/ui/hooks/useAuth.js +7 -3
  53. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  54. package/lib/module/ui/oauth/applicationSessionLane.js +26 -0
  55. package/lib/module/ui/oauth/applicationSessionLane.js.map +1 -0
  56. package/lib/module/ui/oauth/completeOAuthCode.js +4 -2
  57. package/lib/module/ui/oauth/completeOAuthCode.js.map +1 -1
  58. package/lib/module/ui/oauth/nativeAuthTransport.js +115 -0
  59. package/lib/module/ui/oauth/nativeAuthTransport.js.map +1 -0
  60. package/lib/module/ui/session/createSessionClient.js +14 -2
  61. package/lib/module/ui/session/createSessionClient.js.map +1 -1
  62. package/lib/module/ui/session/deviceCredentialTracker.js +8 -0
  63. package/lib/module/ui/session/deviceCredentialTracker.js.map +1 -1
  64. package/lib/module/ui/session/tokenLossRecovery.js +1 -1
  65. package/lib/typescript/commonjs/index.d.ts +1 -0
  66. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts +9 -2
  68. package/lib/typescript/commonjs/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts +2 -0
  70. package/lib/typescript/commonjs/ui/components/OxySignInButton.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +3 -1
  72. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/ui/context/commitSessionFlow.d.ts +2 -0
  74. package/lib/typescript/commonjs/ui/context/commitSessionFlow.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +3 -2
  76. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/ui/context/isolatedOAuthSession.d.ts +19 -0
  78. package/lib/typescript/commonjs/ui/context/isolatedOAuthSession.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +3 -0
  80. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts +2 -1
  82. package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/oauth/applicationSessionLane.d.ts +6 -0
  85. package/lib/typescript/commonjs/ui/oauth/applicationSessionLane.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/ui/oauth/completeOAuthCode.d.ts +2 -2
  87. package/lib/typescript/commonjs/ui/oauth/nativeAuthTransport.d.ts +28 -0
  88. package/lib/typescript/commonjs/ui/oauth/nativeAuthTransport.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +1 -1
  90. package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/session/deviceCredentialTracker.d.ts.map +1 -1
  92. package/lib/typescript/module/index.d.ts +1 -0
  93. package/lib/typescript/module/index.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts +9 -2
  95. package/lib/typescript/module/ui/boot/runProviderColdBoot.d.ts.map +1 -1
  96. package/lib/typescript/module/ui/components/OxySignInButton.d.ts +2 -0
  97. package/lib/typescript/module/ui/components/OxySignInButton.d.ts.map +1 -1
  98. package/lib/typescript/module/ui/context/OxyContext.d.ts +3 -1
  99. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  100. package/lib/typescript/module/ui/context/commitSessionFlow.d.ts +2 -0
  101. package/lib/typescript/module/ui/context/commitSessionFlow.d.ts.map +1 -1
  102. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +3 -2
  103. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  104. package/lib/typescript/module/ui/context/isolatedOAuthSession.d.ts +19 -0
  105. package/lib/typescript/module/ui/context/isolatedOAuthSession.d.ts.map +1 -0
  106. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +3 -0
  107. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
  108. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts +2 -1
  109. package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
  110. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  111. package/lib/typescript/module/ui/oauth/applicationSessionLane.d.ts +6 -0
  112. package/lib/typescript/module/ui/oauth/applicationSessionLane.d.ts.map +1 -0
  113. package/lib/typescript/module/ui/oauth/completeOAuthCode.d.ts +2 -2
  114. package/lib/typescript/module/ui/oauth/nativeAuthTransport.d.ts +28 -0
  115. package/lib/typescript/module/ui/oauth/nativeAuthTransport.d.ts.map +1 -0
  116. package/lib/typescript/module/ui/session/createSessionClient.d.ts +1 -1
  117. package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
  118. package/lib/typescript/module/ui/session/deviceCredentialTracker.d.ts.map +1 -1
  119. package/package.json +30 -30
  120. package/plugins/withOxySharedPermissions.js +89 -0
  121. package/plugins/withSharedDeviceSessionProvider.js +26 -71
  122. package/src/index.ts +2 -0
  123. package/src/ui/boot/__tests__/coldBootNoIdpRedirect.test.ts +56 -2
  124. package/src/ui/boot/runProviderColdBoot.ts +24 -4
  125. package/src/ui/components/OxySignInButton.tsx +20 -4
  126. package/src/ui/context/OxyContext.tsx +171 -40
  127. package/src/ui/context/__tests__/commitSessionFlow.test.ts +13 -0
  128. package/src/ui/context/__tests__/isolatedOAuthSession.test.ts +35 -0
  129. package/src/ui/context/commitSessionFlow.ts +8 -0
  130. package/src/ui/context/hooks/useAuthOperations.ts +55 -9
  131. package/src/ui/context/isolatedOAuthSession.ts +48 -0
  132. package/src/ui/context/oxyContextTypes.ts +4 -0
  133. package/src/ui/context/useOxyAccountGraph.ts +8 -5
  134. package/src/ui/hooks/useAuth.ts +6 -2
  135. package/src/ui/oauth/__tests__/completeOAuthCode.test.ts +1 -1
  136. package/src/ui/oauth/__tests__/nativeAuthTransport.test.ts +57 -0
  137. package/src/ui/oauth/__tests__/oauthBearerCommit.http.test.ts +131 -0
  138. package/src/ui/oauth/applicationSessionLane.ts +26 -0
  139. package/src/ui/oauth/completeOAuthCode.ts +3 -3
  140. package/src/ui/oauth/nativeAuthTransport.ts +67 -0
  141. package/src/ui/session/__tests__/tokenLossRecovery.test.ts +2 -2
  142. package/src/ui/session/createSessionClient.ts +13 -1
  143. package/src/ui/session/deviceCredentialTracker.ts +7 -1
  144. package/src/ui/session/tokenLossRecovery.ts +1 -1
  145. package/android/src/main/java/so/oxy/identity/OxyIdentityProvider.kt +0 -77
  146. package/android/src/main/java/so/oxy/identity/OxyIdentityStore.kt +0 -66
  147. package/plugins/withSharedDeviceSessionReader.js +0 -57
  148. package/plugins/withSharedIdentityProvider.js +0 -83
  149. package/plugins/withSharedIdentityReader.js +0 -46
@@ -0,0 +1,67 @@
1
+ import type { OxyServices } from '@oxy.so/core';
2
+ import { openAuthorizeUrlNative } from '../components/oauthNavigation';
3
+ import { resolveApplicationSessionLane } from './applicationSessionLane';
4
+ import { completeOAuthCode } from './completeOAuthCode';
5
+ import { prepareAuthorizeRequest } from './oauthHandshake';
6
+ import type { OAuthSessionCommitInput } from './types';
7
+
8
+ export interface StartNativeOAuthSignInOptions {
9
+ /** Exact registered URI, including any fixed query. Never inferred. */
10
+ redirectUri: string;
11
+ }
12
+ export type NativeOAuthSignInResult =
13
+ | { status: 'signed-in' }
14
+ | { status: 'cancelled' }
15
+ | { status: 'unsupported'; reason: 'unsupported-platform' | 'missing-client-id' | 'identity-bound' | 'not-third-party' }
16
+ | { status: 'failed'; reason: 'invalid-redirect-uri' | 'client-resolution-failed' | 'native-callback-invalid' | 'state-mismatch' | 'exchange-failed' | 'already-in-progress' };
17
+ export interface NativeOAuthSignInContext {
18
+ platform: 'native' | 'unsupported';
19
+ oxyServices: OxyServices;
20
+ clientId: string | null;
21
+ authorizeBaseUrl?: string;
22
+ identityBound: boolean;
23
+ commitSession: (input: OAuthSessionCommitInput) => Promise<void>;
24
+ }
25
+ const attempts = new WeakSet<OxyServices>();
26
+
27
+ function isExactRedirectUri(value: string): boolean {
28
+ if (!value || value !== value.trim() || /\s/.test(value)) return false;
29
+ try {
30
+ const uri = new URL(value);
31
+ return !uri.username && !uri.password && !uri.hash &&
32
+ !['code', 'state', 'error', 'error_description'].some((key) => uri.searchParams.has(key));
33
+ } catch { return false; }
34
+ }
35
+
36
+ /** SDK owns the in-memory handshake, observed native callback and session commit. */
37
+ export async function startNativeOAuthSignIn(
38
+ context: NativeOAuthSignInContext,
39
+ options: StartNativeOAuthSignInOptions,
40
+ ): Promise<NativeOAuthSignInResult> {
41
+ if (context.platform !== 'native') return { status: 'unsupported', reason: 'unsupported-platform' };
42
+ if (!context.clientId) return { status: 'unsupported', reason: 'missing-client-id' };
43
+ if (context.identityBound) return { status: 'unsupported', reason: 'identity-bound' };
44
+ if (!isExactRedirectUri(options.redirectUri)) return { status: 'failed', reason: 'invalid-redirect-uri' };
45
+ if (attempts.has(context.oxyServices)) return { status: 'failed', reason: 'already-in-progress' };
46
+ attempts.add(context.oxyServices);
47
+ try {
48
+ let lane: 'device' | 'oauth';
49
+ try { lane = await resolveApplicationSessionLane(context.oxyServices, context.clientId); }
50
+ catch { return { status: 'failed', reason: 'client-resolution-failed' }; }
51
+ if (lane !== 'oauth') return { status: 'unsupported', reason: 'not-third-party' };
52
+ const prepared = await prepareAuthorizeRequest({ clientId: context.clientId, redirectUri: options.redirectUri, authorizeBaseUrl: context.authorizeBaseUrl });
53
+ const { redirectUrl } = await openAuthorizeUrlNative(prepared.authorizeUrl, options.redirectUri, { allowExternalFallback: false });
54
+ if (!redirectUrl) return { status: 'cancelled' };
55
+ const prefix = `${options.redirectUri}${options.redirectUri.includes('?') ? '&' : '?'}`;
56
+ if (!redirectUrl.startsWith(prefix) || redirectUrl.includes('#')) return { status: 'failed', reason: 'native-callback-invalid' };
57
+ const parameters = new URLSearchParams(redirectUrl.slice(prefix.length));
58
+ if ([...new URL(options.redirectUri).searchParams.keys()].some((key) => parameters.has(key))) return { status: 'failed', reason: 'native-callback-invalid' };
59
+ if (['code', 'state', 'error', 'error_description'].some((key) => parameters.getAll(key).length > 1)) return { status: 'failed', reason: 'native-callback-invalid' };
60
+ if (parameters.get('state') !== prepared.handshake.state) return { status: 'failed', reason: 'state-mismatch' };
61
+ const code = parameters.get('code');
62
+ if (parameters.has('error') || !code) return { status: 'failed', reason: 'native-callback-invalid' };
63
+ const completion = await completeOAuthCode({ oxyServices: context.oxyServices, clientId: context.clientId, code, returnedState: parameters.get('state'), handshake: prepared.handshake, redirectUri: options.redirectUri, commitSession: context.commitSession });
64
+ return completion.ok ? { status: 'signed-in' } : { status: 'failed', reason: completion.reason };
65
+ } catch { return { status: 'failed', reason: 'exchange-failed' }; }
66
+ finally { attempts.delete(context.oxyServices); }
67
+ }
@@ -102,7 +102,7 @@ describe('token loss recovery', () => {
102
102
  });
103
103
 
104
104
  it('re-establishes through the shared identity when the credential is gone, like the cold boot does', async () => {
105
- const h = harness([null, 'from-shared-key']);
105
+ const h = harness([null, 'from-commons-proof']);
106
106
  h.state.credential = false;
107
107
  h.state.keyed = true;
108
108
  const recovery = createTokenLossRecovery(h.deps);
@@ -111,7 +111,7 @@ describe('token loss recovery', () => {
111
111
  await runToCompletion(recovery);
112
112
 
113
113
  expect(h.signOutLocally).not.toHaveBeenCalled();
114
- expect(h.state.token).toBe('from-shared-key');
114
+ expect(h.state.token).toBe('from-commons-proof');
115
115
  });
116
116
 
117
117
  it('gives the key-based lane a bounded number of tries, then signs out', async () => {
@@ -40,15 +40,27 @@ export function createSessionClient(
40
40
  oxyServices: OxyServices,
41
41
  onUnauthenticated?: (origin: SessionStateOrigin) => void,
42
42
  getPinnedAccountId?: () => string | null,
43
+ isDeviceSessionAllowed: () => boolean = () => true,
44
+ onFullExplicitSignOut?: () => Promise<void>,
43
45
  ): {
44
46
  client: SessionClient;
45
47
  host: ReturnType<typeof createSessionClientHost>;
46
48
  } {
47
- const host = createSessionClientHost(oxyServices);
49
+ const baseHost = createSessionClientHost(oxyServices);
50
+ const host = {
51
+ ...baseHost,
52
+ getAccessToken: () => isDeviceSessionAllowed() ? baseHost.getAccessToken() : null,
53
+ setTokens: (token: string) => { if (isDeviceSessionAllowed()) baseHost.setTokens(token); },
54
+ makeRequest: async <T>(method: 'GET' | 'POST', url: string, data?: unknown, requestOptions?: { cache?: boolean }): Promise<T> => {
55
+ if (!isDeviceSessionAllowed()) throw new Error('An isolated OAuth session cannot access the shared device');
56
+ return baseHost.makeRequest<T>(method, url, data, requestOptions);
57
+ },
58
+ };
48
59
  const transport = createTokenTransport(oxyServices, getPinnedAccountId);
49
60
  const client = new SessionClient(host, {
50
61
  transport,
51
62
  onUnauthenticated,
63
+ onFullExplicitSignOut,
52
64
  getPinnedAccountId,
53
65
  });
54
66
  return { client, host };
@@ -7,7 +7,7 @@
7
7
  * every load, save and clear passes through, so the press can read it at once.
8
8
  * The cold boot loads the store on mount, long before anyone presses sign-in.
9
9
  */
10
- import type { AuthStateStore } from '@oxy.so/core/session';
10
+ import type { AuthStateStore, AuthStateWriteGuard, PersistedAuthState } from '@oxy.so/core/session';
11
11
 
12
12
  export interface HeldDeviceCredential {
13
13
  deviceId: string;
@@ -31,6 +31,7 @@ function credentialOf(state: { deviceId?: string; deviceSecret?: string } | null
31
31
  export function trackDeviceCredential(store: AuthStateStore): CredentialTrackingAuthStateStore {
32
32
  let held: HeldDeviceCredential | null | undefined;
33
33
  return {
34
+ ...store,
34
35
  load: async () => {
35
36
  const state = await store.load();
36
37
  held = credentialOf(state);
@@ -40,6 +41,11 @@ export function trackDeviceCredential(store: AuthStateStore): CredentialTracking
40
41
  held = credentialOf(state);
41
42
  return store.save(state);
42
43
  },
44
+ ...(store.saveIfCurrent ? {saveIfCurrent: async (state: PersistedAuthState, guard: AuthStateWriteGuard) => {
45
+ const committed = await store.saveIfCurrent?.(state, guard);
46
+ if (committed) held = credentialOf(state);
47
+ return committed === true;
48
+ }} : {}),
43
49
  clear: async () => {
44
50
  held = null;
45
51
  await store.clear();
@@ -21,7 +21,7 @@
21
21
  * this loop in a sign-out — so a real revocation still signs out.
22
22
  * - No credential, but a key-based lane can still re-establish (the native
23
23
  * shared identity, or an identity-bound client's own key) → try that a few
24
- * times, as the cold boot's shared-key step would, then sign out.
24
+ * times, as the cold boot's Commons-proof step would, then sign out.
25
25
  * - Neither → the session is over. Sign out now, as before.
26
26
  *
27
27
  * Framework-free and clock-injectable so the policy is unit tested without a
@@ -1,77 +0,0 @@
1
- package so.oxy.identity
2
-
3
- import android.content.ContentProvider
4
- import android.content.ContentValues
5
- import android.content.Context
6
- import android.content.pm.PackageManager
7
- import android.database.Cursor
8
- import android.net.Uri
9
- import android.os.Bundle
10
-
11
- /**
12
- * Cross-process read surface for the shared Oxy identity, hosted ONLY by Commons
13
- * (declared per-app by the `withSharedIdentityProvider` config plugin with a
14
- * `signature`-level permission, authority `${applicationId}.identity`).
15
- *
16
- * `call("getShared")` returns the keypair from Commons's own
17
- * EncryptedSharedPreferences — but ONLY after verifying the caller is signed
18
- * with the SAME certificate. That signature check is belt-and-suspenders on top
19
- * of the manifest's `signature` permission: even if the permission gate were
20
- * ever misconfigured, a differently-signed caller still gets null.
21
- *
22
- * All standard CRUD operations are no-ops; this provider exists solely for the
23
- * `call()` channel.
24
- */
25
- class OxyIdentityProvider : ContentProvider() {
26
- override fun onCreate(): Boolean = true
27
-
28
- override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
29
- if (method != METHOD_GET_SHARED) return null
30
- val ctx = context ?: return null
31
- if (!callerSignatureMatches(ctx)) return null
32
-
33
- val pair = runCatching { OxyIdentityStore.read(ctx) }.getOrNull() ?: return null
34
- return Bundle().apply {
35
- putString(OxyIdentityStore.KEY_PRIVATE, pair.first)
36
- putString(OxyIdentityStore.KEY_PUBLIC, pair.second)
37
- }
38
- }
39
-
40
- /**
41
- * True when the calling package shares this app's signing certificate. Uses
42
- * `checkSignatures` (deprecated but still the simplest correct cross-package
43
- * signature comparison; returns SIGNATURE_MATCH only for same-cert apps).
44
- */
45
- private fun callerSignatureMatches(ctx: Context): Boolean {
46
- val caller = callingPackage ?: return false
47
- return runCatching {
48
- @Suppress("DEPRECATION")
49
- ctx.packageManager.checkSignatures(caller, ctx.packageName) == PackageManager.SIGNATURE_MATCH
50
- }.getOrDefault(false)
51
- }
52
-
53
- override fun query(
54
- uri: Uri,
55
- projection: Array<out String>?,
56
- selection: String?,
57
- selectionArgs: Array<out String>?,
58
- sortOrder: String?
59
- ): Cursor? = null
60
-
61
- override fun getType(uri: Uri): String? = null
62
-
63
- override fun insert(uri: Uri, values: ContentValues?): Uri? = null
64
-
65
- override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0
66
-
67
- override fun update(
68
- uri: Uri,
69
- values: ContentValues?,
70
- selection: String?,
71
- selectionArgs: Array<out String>?
72
- ): Int = 0
73
-
74
- companion object {
75
- private const val METHOD_GET_SHARED = "getShared"
76
- }
77
- }
@@ -1,66 +0,0 @@
1
- package so.oxy.identity
2
-
3
- import android.content.Context
4
- import android.content.SharedPreferences
5
- import so.oxy.storage.OxyEncryptedPrefs
6
- import so.oxy.storage.RecoveryPolicy
7
-
8
- /**
9
- * Shared accessor for the hardware-backed EncryptedSharedPreferences that holds
10
- * the cross-app Oxy identity keypair.
11
- *
12
- * Used by BOTH [OxyIdentityModule] (the JS bridge / local read + write) and
13
- * [OxyIdentityProvider] (the cross-process read surface) so the store name and
14
- * the encryption scheme can never drift between the two halves.
15
- *
16
- * Opening the file — the single-memoized-instance rule and the two-stage keyset
17
- * self-heal, both load-bearing and non-obvious — lives in [OxyEncryptedPrefs],
18
- * which is shared with the background-session store. Read its documentation
19
- * before changing anything about how this file is opened.
20
- *
21
- * One consequence worth restating here: an open can legitimately return an EMPTY
22
- * file (the self-heal wipes a keyset it cannot read). That is safe for this slot
23
- * specifically because it is a DERIVED copy — Commons re-populates it from the
24
- * primary self-custody identity on the next boot (`migrateToSharedIdentity`), and
25
- * that primary lives in expo-secure-store under a different file and different
26
- * keystore aliases, untouched by any recovery here.
27
- */
28
- internal object OxyIdentityStore {
29
- const val PREFS_NAME = "oxy_shared_identity"
30
- const val KEY_PRIVATE = "priv"
31
- const val KEY_PUBLIC = "pub"
32
-
33
- /**
34
- * [RecoveryPolicy.RebuildFileOnly], like every other store (OxyHQ/oxy#1388).
35
- * This store used to regenerate the UID-shared androidx master key when its
36
- * keyset could not be rebuilt, which made every other Oxy app's encrypted prefs
37
- * unreadable. It never needs to: this slot is a DERIVED copy that Commons
38
- * re-populates from the primary identity on every launch
39
- * (`KeyManager.syncSharedIdentity`), so an empty or unavailable slot costs
40
- * nothing that a relaunch of Commons does not restore.
41
- */
42
- private fun prefs(context: Context): SharedPreferences =
43
- OxyEncryptedPrefs.open(context, PREFS_NAME, RecoveryPolicy.RebuildFileOnly)
44
-
45
- /** Read the stored keypair as (privateKey, publicKey), or null when absent/blank. */
46
- fun read(context: Context): Pair<String, String>? {
47
- val p = prefs(context)
48
- val priv = p.getString(KEY_PRIVATE, null) ?: return null
49
- val pub = p.getString(KEY_PUBLIC, null) ?: return null
50
- if (priv.isEmpty() || pub.isEmpty()) return null
51
- return priv to pub
52
- }
53
-
54
- fun write(context: Context, priv: String, pub: String) {
55
- // commit() (synchronous) so a cross-process reader that fires immediately
56
- // after the write is guaranteed to see the flushed value.
57
- prefs(context).edit()
58
- .putString(KEY_PRIVATE, priv)
59
- .putString(KEY_PUBLIC, pub)
60
- .commit()
61
- }
62
-
63
- fun clear(context: Context) {
64
- prefs(context).edit().clear().commit()
65
- }
66
- }
@@ -1,57 +0,0 @@
1
- /**
2
- * Config plugin: withSharedDeviceSessionReader (every other official Oxy app).
3
- *
4
- * A reader app JOINS the device's existing session — it adopts the shared
5
- * `deviceId` + `deviceSecret` a hub app published, so it lands signed in with no
6
- * QR and, crucially, without ever asking for Commons's private identity key.
7
- * This plugin wires the minimal Android side of `@oxy.so/services`:
8
- *
9
- * - Requests the `signature`-level permission
10
- * `so.oxy.shared.permission.READ_DEVICE_SESSION` (defined by the hub apps).
11
- * Because it is `signature`, it is granted only when this app is signed with
12
- * the SAME certificate as the hub — that is the entire trust boundary, and it
13
- * is what makes an incorrectly-signed app unable to read the slot.
14
- * - Adds `<queries>` entries for the hub authorities so package-visibility
15
- * filtering (Android 11+) never hides them from `ContentResolver.call`.
16
- *
17
- * The provider itself is declared only by the hub apps
18
- * (`withSharedDeviceSessionProvider`) — see the note there about why hosting is
19
- * restricted to apps inside the `so.oxy.shared` UID.
20
- *
21
- * Keep this list identical to `PROVIDER_AUTHORITIES` in
22
- * `android/src/main/java/so/oxy/devicesession/OxyDeviceSessionModule.kt`.
23
- */
24
- const { withAndroidManifest } = require('@expo/config-plugins');
25
-
26
- const READ_DEVICE_SESSION_PERMISSION = 'so.oxy.shared.permission.READ_DEVICE_SESSION';
27
- const PROVIDER_AUTHORITIES = [
28
- 'so.oxy.accounts.devicesession',
29
- 'so.oxy.accounts.dev.devicesession',
30
- 'so.oxy.commons.devicesession',
31
- 'so.oxy.commons.dev.devicesession',
32
- ];
33
-
34
- module.exports = function withSharedDeviceSessionReader(config) {
35
- return withAndroidManifest(config, (modConfig) => {
36
- const manifest = modConfig.modResults.manifest;
37
-
38
- manifest['uses-permission'] = manifest['uses-permission'] ?? [];
39
- if (!manifest['uses-permission'].some((p) => p.$['android:name'] === READ_DEVICE_SESSION_PERMISSION)) {
40
- manifest['uses-permission'].push({ $: { 'android:name': READ_DEVICE_SESSION_PERMISSION } });
41
- }
42
-
43
- manifest['queries'] = manifest['queries'] ?? [];
44
- if (manifest['queries'].length === 0) {
45
- manifest['queries'].push({});
46
- }
47
- const queries = manifest['queries'][0];
48
- queries.provider = queries.provider ?? [];
49
- for (const authority of PROVIDER_AUTHORITIES) {
50
- if (!queries.provider.some((p) => p.$['android:authorities'] === authority)) {
51
- queries.provider.push({ $: { 'android:authorities': authority } });
52
- }
53
- }
54
-
55
- return modConfig;
56
- });
57
- };
@@ -1,83 +0,0 @@
1
- /**
2
- * Config plugin: withSharedIdentityProvider (Commons only).
3
- *
4
- * Commons is the identity vault — the ONE app that HOSTS the cross-app shared
5
- * identity. This plugin wires the Android side of `@oxy.so/services`:
6
- *
7
- * - Defines a `signature`-level permission `so.oxy.shared.permission.READ_IDENTITY`.
8
- * `signature` means only apps signed with the SAME certificate (the shared
9
- * Oxy release keystore) can hold it — the trust boundary is the signing key,
10
- * not the deprecated `sharedUserId`.
11
- * - Requests that same permission (`<uses-permission>`) so Commons can also
12
- * read cross-authority (e.g. prod ⇆ dev variant) through the provider.
13
- * - Declares the `OxyIdentityProvider` at authority `${applicationId}.identity`
14
- * (AGP substitutes `${applicationId}` at build → `so.oxy.commons.identity`,
15
- * or `so.oxy.commons.dev.identity` for the dev variant), guarded by that
16
- * permission.
17
- * - Adds a `<queries>` entry for the provider authorities so package-visibility
18
- * filtering (Android 11+) never hides the sibling provider from the resolver.
19
- *
20
- * Reader apps (accounts, Mention, …) use the companion `withSharedIdentityReader`
21
- * plugin, which only requests the permission + queries — they never host the
22
- * provider.
23
- */
24
- const { withAndroidManifest } = require('@expo/config-plugins');
25
-
26
- const READ_IDENTITY_PERMISSION = 'so.oxy.shared.permission.READ_IDENTITY';
27
- const PROVIDER_CLASS = 'so.oxy.identity.OxyIdentityProvider';
28
- const PROVIDER_AUTHORITIES = ['so.oxy.commons.identity', 'so.oxy.commons.dev.identity'];
29
-
30
- module.exports = function withSharedIdentityProvider(config) {
31
- return withAndroidManifest(config, (modConfig) => {
32
- const manifest = modConfig.modResults.manifest;
33
-
34
- // 1. Define the signature-level permission.
35
- manifest['permission'] = manifest['permission'] ?? [];
36
- if (!manifest['permission'].some((p) => p.$['android:name'] === READ_IDENTITY_PERMISSION)) {
37
- manifest['permission'].push({
38
- $: {
39
- 'android:name': READ_IDENTITY_PERMISSION,
40
- 'android:protectionLevel': 'signature',
41
- },
42
- });
43
- }
44
-
45
- // 2. Request it (Commons reads cross-authority too).
46
- manifest['uses-permission'] = manifest['uses-permission'] ?? [];
47
- if (!manifest['uses-permission'].some((p) => p.$['android:name'] === READ_IDENTITY_PERMISSION)) {
48
- manifest['uses-permission'].push({ $: { 'android:name': READ_IDENTITY_PERMISSION } });
49
- }
50
-
51
- // 3. Make the sibling provider authorities visible under package filtering.
52
- manifest['queries'] = manifest['queries'] ?? [];
53
- if (manifest['queries'].length === 0) {
54
- manifest['queries'].push({});
55
- }
56
- const queries = manifest['queries'][0];
57
- queries.provider = queries.provider ?? [];
58
- for (const authority of PROVIDER_AUTHORITIES) {
59
- if (!queries.provider.some((p) => p.$['android:authorities'] === authority)) {
60
- queries.provider.push({ $: { 'android:authorities': authority } });
61
- }
62
- }
63
-
64
- // 4. Host the provider.
65
- const app = manifest.application?.[0];
66
- if (!app) {
67
- throw new Error('withSharedIdentityProvider: AndroidManifest has no <application>');
68
- }
69
- app.provider = app.provider ?? [];
70
- if (!app.provider.some((p) => p.$['android:name'] === PROVIDER_CLASS)) {
71
- app.provider.push({
72
- $: {
73
- 'android:name': PROVIDER_CLASS,
74
- 'android:authorities': '${applicationId}.identity',
75
- 'android:exported': 'true',
76
- 'android:permission': READ_IDENTITY_PERMISSION,
77
- },
78
- });
79
- }
80
-
81
- return modConfig;
82
- });
83
- };
@@ -1,46 +0,0 @@
1
- /**
2
- * Config plugin: withSharedIdentityReader (reader RPs — accounts, etc.).
3
- *
4
- * Reader apps do NOT host the shared identity — they only READ it from the
5
- * Commons-hosted `OxyIdentityProvider` to enable silent "Sign in with Oxy".
6
- * This plugin wires the minimal Android side of `@oxy.so/services`:
7
- *
8
- * - Requests the `signature`-level permission `so.oxy.shared.permission.READ_IDENTITY`
9
- * (defined by Commons). Because it is `signature`, it is only granted when
10
- * this app is signed with the SAME certificate as Commons (the shared Oxy
11
- * release keystore) — that is the entire trust boundary.
12
- * - Adds a `<queries>` entry for the Commons provider authorities so
13
- * package-visibility filtering (Android 11+) never hides the provider from
14
- * `ContentResolver.call`.
15
- *
16
- * The provider itself is declared only in Commons (`withSharedIdentityProvider`).
17
- */
18
- const { withAndroidManifest } = require('@expo/config-plugins');
19
-
20
- const READ_IDENTITY_PERMISSION = 'so.oxy.shared.permission.READ_IDENTITY';
21
- const PROVIDER_AUTHORITIES = ['so.oxy.commons.identity', 'so.oxy.commons.dev.identity'];
22
-
23
- module.exports = function withSharedIdentityReader(config) {
24
- return withAndroidManifest(config, (modConfig) => {
25
- const manifest = modConfig.modResults.manifest;
26
-
27
- manifest['uses-permission'] = manifest['uses-permission'] ?? [];
28
- if (!manifest['uses-permission'].some((p) => p.$['android:name'] === READ_IDENTITY_PERMISSION)) {
29
- manifest['uses-permission'].push({ $: { 'android:name': READ_IDENTITY_PERMISSION } });
30
- }
31
-
32
- manifest['queries'] = manifest['queries'] ?? [];
33
- if (manifest['queries'].length === 0) {
34
- manifest['queries'].push({});
35
- }
36
- const queries = manifest['queries'][0];
37
- queries.provider = queries.provider ?? [];
38
- for (const authority of PROVIDER_AUTHORITIES) {
39
- if (!queries.provider.some((p) => p.$['android:authorities'] === authority)) {
40
- queries.provider.push({ $: { 'android:authorities': authority } });
41
- }
42
- }
43
-
44
- return modConfig;
45
- });
46
- };