@oxyhq/services 19.1.2 → 19.2.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 +21 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/so/oxy/identity/OxyIdentityModule.kt +84 -0
- package/android/src/main/java/so/oxy/identity/OxyIdentityProvider.kt +77 -0
- package/android/src/main/java/so/oxy/identity/OxyIdentityStore.kt +76 -0
- package/expo-module.config.json +9 -0
- package/ios/OxyIdentity.podspec +28 -0
- package/ios/OxyIdentityModule.swift +33 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +53 -50
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/commitSessionFlow.js +91 -0
- package/lib/commonjs/ui/context/commitSessionFlow.js.map +1 -0
- package/lib/commonjs/ui/hooks/useOxyEvent.js +28 -0
- package/lib/commonjs/ui/hooks/useOxyEvent.js.map +1 -0
- package/lib/commonjs/ui/screens/FileManagementScreen.js +3 -9
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/session/createSessionClient.js +12 -8
- package/lib/commonjs/ui/session/createSessionClient.js.map +1 -1
- package/lib/commonjs/ui/session/tokenTransport.js +22 -32
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +54 -51
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/commitSessionFlow.js +87 -0
- package/lib/module/ui/context/commitSessionFlow.js.map +1 -0
- package/lib/module/ui/hooks/useOxyEvent.js +25 -0
- package/lib/module/ui/hooks/useOxyEvent.js.map +1 -0
- package/lib/module/ui/screens/FileManagementScreen.js +3 -9
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/session/createSessionClient.js +12 -8
- package/lib/module/ui/session/createSessionClient.js.map +1 -1
- package/lib/module/ui/session/tokenTransport.js +23 -33
- package/lib/module/ui/session/tokenTransport.js.map +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/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/commitSessionFlow.d.ts +39 -0
- package/lib/typescript/commonjs/ui/context/commitSessionFlow.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +3 -1
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useOxyEvent.d.ts +8 -0
- package/lib/typescript/commonjs/ui/hooks/useOxyEvent.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts +12 -8
- package/lib/typescript/commonjs/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +11 -9
- package/lib/typescript/commonjs/ui/session/tokenTransport.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/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/commitSessionFlow.d.ts +39 -0
- package/lib/typescript/module/ui/context/commitSessionFlow.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +3 -1
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useOxyEvent.d.ts +8 -0
- package/lib/typescript/module/ui/hooks/useOxyEvent.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/createSessionClient.d.ts +12 -8
- package/lib/typescript/module/ui/session/createSessionClient.d.ts.map +1 -1
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +11 -9
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -1
- package/package.json +29 -17
- package/plugins/withSharedIdentityProvider.js +83 -0
- package/plugins/withSharedIdentityReader.js +46 -0
- package/src/index.ts +1 -0
- package/src/ui/context/OxyContext.tsx +51 -49
- package/src/ui/context/__tests__/commitSessionFlow.test.ts +192 -0
- package/src/ui/context/commitSessionFlow.ts +115 -0
- package/src/ui/context/oxyContextTypes.ts +3 -1
- package/src/ui/hooks/useOxyEvent.ts +22 -0
- package/src/ui/screens/FileManagementScreen.tsx +3 -9
- package/src/ui/session/__tests__/createSessionClient.test.ts +5 -5
- package/src/ui/session/__tests__/tokenTransport.test.ts +43 -60
- package/src/ui/session/createSessionClient.ts +13 -10
- package/src/ui/session/tokenTransport.ts +22 -41
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'expo-module-gradle-plugin'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
group = 'so.oxy.identity'
|
|
7
|
+
version = '0.1.0'
|
|
8
|
+
|
|
9
|
+
android {
|
|
10
|
+
namespace "so.oxy.identity"
|
|
11
|
+
defaultConfig {
|
|
12
|
+
versionCode 1
|
|
13
|
+
versionName "0.1.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
dependencies {
|
|
18
|
+
// Hardware-backed EncryptedSharedPreferences (MasterKey AES256_GCM) for the
|
|
19
|
+
// cross-app shared identity keypair.
|
|
20
|
+
implementation "androidx.security:security-crypto:1.1.0-alpha06"
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<manifest>
|
|
2
|
+
<!--
|
|
3
|
+
Intentionally empty. The signature-protected <permission> and the
|
|
4
|
+
<provider> that hosts the shared identity are declared per-app by the
|
|
5
|
+
Commons config plugin (`withSharedIdentityProvider.js`), NOT here — only
|
|
6
|
+
Commons (the identity vault) should host the provider, and its authority is
|
|
7
|
+
the app's own ${applicationId}.identity. Reader apps add only the matching
|
|
8
|
+
<uses-permission> via `withSharedIdentityReader.js`.
|
|
9
|
+
-->
|
|
10
|
+
</manifest>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package so.oxy.identity
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import android.os.Bundle
|
|
6
|
+
import expo.modules.kotlin.exception.Exceptions
|
|
7
|
+
import expo.modules.kotlin.modules.Module
|
|
8
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* JS bridge for the cross-app shared Oxy identity.
|
|
12
|
+
*
|
|
13
|
+
* - `putShared` persists the keypair into THIS app's own hardware-backed
|
|
14
|
+
* EncryptedSharedPreferences (only Commons calls this).
|
|
15
|
+
* - `getShared` resolves the keypair from the LOCAL store first (Commons reading
|
|
16
|
+
* itself), then from the Commons ContentProvider — prod authority, then the
|
|
17
|
+
* dev-variant authority. The `signature` permission on the provider means only
|
|
18
|
+
* apps signed with the shared Oxy release key can resolve it.
|
|
19
|
+
* - `hasShared` / `clearShared` are the check + local teardown helpers.
|
|
20
|
+
*
|
|
21
|
+
* Every failure resolves to null / no-op so the JS layer degrades to the app's
|
|
22
|
+
* normal interactive sign-in path.
|
|
23
|
+
*/
|
|
24
|
+
class OxyIdentityModule : Module() {
|
|
25
|
+
private val context: Context
|
|
26
|
+
get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
|
|
27
|
+
|
|
28
|
+
override fun definition() = ModuleDefinition {
|
|
29
|
+
Name("OxyIdentity")
|
|
30
|
+
|
|
31
|
+
AsyncFunction("getShared") {
|
|
32
|
+
readShared()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
AsyncFunction("putShared") { privateKey: String, publicKey: String ->
|
|
36
|
+
OxyIdentityStore.write(context, privateKey, publicKey)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
AsyncFunction("hasShared") {
|
|
40
|
+
readShared() != null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
AsyncFunction("clearShared") {
|
|
44
|
+
OxyIdentityStore.clear(context)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Local EncryptedSharedPreferences first, then the Commons provider (prod
|
|
50
|
+
* authority, then dev). Returns null on any failure.
|
|
51
|
+
*/
|
|
52
|
+
private fun readShared(): Map<String, String>? {
|
|
53
|
+
runCatching { OxyIdentityStore.read(context) }.getOrNull()?.let { (priv, pub) ->
|
|
54
|
+
return mapOf("privateKey" to priv, "publicKey" to pub)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (authority in PROVIDER_AUTHORITIES) {
|
|
58
|
+
val result = runCatching { callProvider(authority) }.getOrNull()
|
|
59
|
+
if (result != null) return result
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private fun callProvider(authority: String): Map<String, String>? {
|
|
65
|
+
val uri = Uri.parse("content://$authority")
|
|
66
|
+
val bundle: Bundle = context.contentResolver.call(uri, METHOD_GET_SHARED, null, null)
|
|
67
|
+
?: return null
|
|
68
|
+
val priv = bundle.getString(OxyIdentityStore.KEY_PRIVATE) ?: return null
|
|
69
|
+
val pub = bundle.getString(OxyIdentityStore.KEY_PUBLIC) ?: return null
|
|
70
|
+
if (priv.isEmpty() || pub.isEmpty()) return null
|
|
71
|
+
return mapOf("privateKey" to priv, "publicKey" to pub)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
companion object {
|
|
75
|
+
private const val METHOD_GET_SHARED = "getShared"
|
|
76
|
+
|
|
77
|
+
// Commons hosts the provider at "${applicationId}.identity". Try the prod
|
|
78
|
+
// app id first, then the dev variant ("so.oxy.commons.dev").
|
|
79
|
+
private val PROVIDER_AUTHORITIES = listOf(
|
|
80
|
+
"so.oxy.commons.identity",
|
|
81
|
+
"so.oxy.commons.dev.identity"
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package so.oxy.identity
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import androidx.security.crypto.EncryptedSharedPreferences
|
|
6
|
+
import androidx.security.crypto.MasterKey
|
|
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 AES256 encryption scheme can never drift between the two halves.
|
|
15
|
+
*
|
|
16
|
+
* ## Single memoized instance (CRITICAL)
|
|
17
|
+
*
|
|
18
|
+
* `EncryptedSharedPreferences.create()` must be called AT MOST ONCE per file per
|
|
19
|
+
* process. It is NOT safe to re-instantiate: when a second instance is created
|
|
20
|
+
* for the same file while another is live (e.g. the JS write thread and a Binder
|
|
21
|
+
* thread serving [OxyIdentityProvider.call] concurrently), Tink's keyset load
|
|
22
|
+
* races and the next decrypt throws `AEADBadTagException` — which silently turned
|
|
23
|
+
* every cross-app read into "no shared identity". So the instance is created once,
|
|
24
|
+
* lazily, under a lock, keyed on the process-global application context, and
|
|
25
|
+
* reused for all reads/writes/provider calls.
|
|
26
|
+
*/
|
|
27
|
+
internal object OxyIdentityStore {
|
|
28
|
+
const val PREFS_NAME = "oxy_shared_identity"
|
|
29
|
+
const val KEY_PRIVATE = "priv"
|
|
30
|
+
const val KEY_PUBLIC = "pub"
|
|
31
|
+
|
|
32
|
+
@Volatile private var cachedPrefs: SharedPreferences? = null
|
|
33
|
+
|
|
34
|
+
private fun prefs(context: Context): SharedPreferences {
|
|
35
|
+
cachedPrefs?.let { return it }
|
|
36
|
+
return synchronized(this) {
|
|
37
|
+
cachedPrefs ?: run {
|
|
38
|
+
val appContext = context.applicationContext
|
|
39
|
+
val masterKey = MasterKey.Builder(appContext)
|
|
40
|
+
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
|
41
|
+
.build()
|
|
42
|
+
val created = EncryptedSharedPreferences.create(
|
|
43
|
+
appContext,
|
|
44
|
+
PREFS_NAME,
|
|
45
|
+
masterKey,
|
|
46
|
+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
47
|
+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
48
|
+
)
|
|
49
|
+
cachedPrefs = created
|
|
50
|
+
created
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Read the stored keypair as (privateKey, publicKey), or null when absent/blank. */
|
|
56
|
+
fun read(context: Context): Pair<String, String>? {
|
|
57
|
+
val p = prefs(context)
|
|
58
|
+
val priv = p.getString(KEY_PRIVATE, null) ?: return null
|
|
59
|
+
val pub = p.getString(KEY_PUBLIC, null) ?: return null
|
|
60
|
+
if (priv.isEmpty() || pub.isEmpty()) return null
|
|
61
|
+
return priv to pub
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fun write(context: Context, priv: String, pub: String) {
|
|
65
|
+
// commit() (synchronous) so a cross-process reader that fires immediately
|
|
66
|
+
// after the write is guaranteed to see the flushed value.
|
|
67
|
+
prefs(context).edit()
|
|
68
|
+
.putString(KEY_PRIVATE, priv)
|
|
69
|
+
.putString(KEY_PUBLIC, pub)
|
|
70
|
+
.commit()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fun clear(context: Context) {
|
|
74
|
+
prefs(context).edit().clear().commit()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'OxyIdentity'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = {
|
|
14
|
+
:ios => '16.4',
|
|
15
|
+
:tvos => '16.4'
|
|
16
|
+
}
|
|
17
|
+
s.swift_version = '5.9'
|
|
18
|
+
s.source = { git: 'https://github.com/oxyhq/sdk.git' }
|
|
19
|
+
s.static_framework = true
|
|
20
|
+
|
|
21
|
+
s.dependency 'ExpoModulesCore'
|
|
22
|
+
|
|
23
|
+
s.source_files = "**/*.{h,m,mm,swift}"
|
|
24
|
+
s.pod_target_xcconfig = {
|
|
25
|
+
'DEFINES_MODULE' => 'YES',
|
|
26
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
27
|
+
}
|
|
28
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* iOS no-op implementation of the shared Oxy identity bridge.
|
|
5
|
+
*
|
|
6
|
+
* On Apple platforms the cross-app identity share is handled directly by
|
|
7
|
+
* `@oxyhq/core`'s `KeyManager` via the Keychain Access Group
|
|
8
|
+
* (`group.so.oxy.shared`) — there is no ContentProvider equivalent to wrap. So
|
|
9
|
+
* every function here resolves to `nil` / no-op, which makes the JS
|
|
10
|
+
* `loadSharedIdentityBridge()` seam a pass-through on iOS: `KeyManager`'s iOS
|
|
11
|
+
* branches keep using `expo-secure-store` with the keychain group untouched.
|
|
12
|
+
*/
|
|
13
|
+
public class OxyIdentityModule: Module {
|
|
14
|
+
public func definition() -> ModuleDefinition {
|
|
15
|
+
Name("OxyIdentity")
|
|
16
|
+
|
|
17
|
+
AsyncFunction("getShared") { () -> [String: String]? in
|
|
18
|
+
return nil
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
AsyncFunction("putShared") { (_ privateKey: String, _ publicKey: String) in
|
|
22
|
+
// No-op on iOS: the keychain-access-group path in KeyManager owns writes.
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
AsyncFunction("hasShared") { () -> Bool in
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
AsyncFunction("clearShared") { () in
|
|
30
|
+
// No-op on iOS.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/lib/commonjs/index.js
CHANGED
|
@@ -441,6 +441,12 @@ Object.defineProperty(exports, "useOxy", {
|
|
|
441
441
|
return _OxyContext.useOxy;
|
|
442
442
|
}
|
|
443
443
|
});
|
|
444
|
+
Object.defineProperty(exports, "useOxyEvent", {
|
|
445
|
+
enumerable: true,
|
|
446
|
+
get: function () {
|
|
447
|
+
return _useOxyEvent.useOxyEvent;
|
|
448
|
+
}
|
|
449
|
+
});
|
|
444
450
|
Object.defineProperty(exports, "usePrivacySettings", {
|
|
445
451
|
enumerable: true,
|
|
446
452
|
get: function () {
|
|
@@ -645,6 +651,7 @@ var _mutationKeys = require("./ui/hooks/mutations/mutationKeys.js");
|
|
|
645
651
|
var _queryKeys = require("./ui/hooks/queries/queryKeys.js");
|
|
646
652
|
var _useMutationStatus = require("./ui/hooks/useMutationStatus.js");
|
|
647
653
|
var _useOnlineStatus = require("./ui/hooks/useOnlineStatus.js");
|
|
654
|
+
var _useOxyEvent = require("./ui/hooks/useOxyEvent.js");
|
|
648
655
|
var _errorHandlers = require("./ui/utils/errorHandlers.js");
|
|
649
656
|
var _useFileFiltering = require("./ui/hooks/useFileFiltering.js");
|
|
650
657
|
var _OxySignInButton = _interopRequireDefault(require("./ui/components/OxySignInButton.js"));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_core","require","_reactNative","_OxyProvider","_interopRequireDefault","_OxyContext","_useAuth","_FontLoader","_authStore","_accountStore","_assetStore","_useAssets","_useFileDownloadUrl","_useFollow","_useStorage","_useAccountQueries","_useServicesQueries","_useSecurityQueries","_usePaymentQueries","_useAccountMutations","_useServicesMutations","_mutationFactory","_mutationKeys","_queryKeys","_useMutationStatus","_useOnlineStatus","_errorHandlers","_useFileFiltering","_OxySignInButton","_OxyAuthPrompt","_OxyOAuthCallback","_OxyConsentScreen","_RequireOxyAuth","_FollowButton","_OxyPayButton","_LogoIcon","_LogoText","_ProfileButton","_useSwitchableAccounts","_ProfileScreen","_ManageAccountScreen","_NotificationsScreen","_PreferencesScreen","_ConnectedAppsScreen","_CreateAccountScreen","_AccountMembersScreen","_AccountSettingsScreen","_bottomSheetManager","_accountDialogManager","e","__esModule","default","setPlatformOS","Platform","OS"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"names":["_core","require","_reactNative","_OxyProvider","_interopRequireDefault","_OxyContext","_useAuth","_FontLoader","_authStore","_accountStore","_assetStore","_useAssets","_useFileDownloadUrl","_useFollow","_useStorage","_useAccountQueries","_useServicesQueries","_useSecurityQueries","_usePaymentQueries","_useAccountMutations","_useServicesMutations","_mutationFactory","_mutationKeys","_queryKeys","_useMutationStatus","_useOnlineStatus","_useOxyEvent","_errorHandlers","_useFileFiltering","_OxySignInButton","_OxyAuthPrompt","_OxyOAuthCallback","_OxyConsentScreen","_RequireOxyAuth","_FollowButton","_OxyPayButton","_LogoIcon","_LogoText","_ProfileButton","_useSwitchableAccounts","_ProfileScreen","_ManageAccountScreen","_NotificationsScreen","_PreferencesScreen","_ConnectedAppsScreen","_CreateAccountScreen","_AccountMembersScreen","_AccountSettingsScreen","_bottomSheetManager","_accountDialogManager","e","__esModule","default","setPlatformOS","Platform","OS"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -26,6 +26,7 @@ var _useAvatarPicker = require("../hooks/useAvatarPicker.js");
|
|
|
26
26
|
var _accountStore = require("../stores/accountStore.js");
|
|
27
27
|
var _index = require("../session/index.js");
|
|
28
28
|
var _oxyContextHelpers = require("./oxyContextHelpers.js");
|
|
29
|
+
var _commitSessionFlow = require("./commitSessionFlow.js");
|
|
29
30
|
var _useOxyAccountGraph = require("./useOxyAccountGraph.js");
|
|
30
31
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
31
32
|
const OxyContext = /*#__PURE__*/(0, _react.createContext)(null);
|
|
@@ -240,8 +241,16 @@ const OxyProvider = ({
|
|
|
240
241
|
// the one-shot join redirect.
|
|
241
242
|
const sessionClientPairRef = (0, _react.useRef)(null);
|
|
242
243
|
if (!sessionClientPairRef.current) {
|
|
243
|
-
sessionClientPairRef.current = (0, _index.createSessionClient)(oxyServices,
|
|
244
|
-
|
|
244
|
+
sessionClientPairRef.current = (0, _index.createSessionClient)(oxyServices, origin => {
|
|
245
|
+
// Erase the DURABLE device credential only on a `request`-origin verdict
|
|
246
|
+
// (a REST sign-out / revocation). A `push`-origin empty state is a socket
|
|
247
|
+
// broadcast that may be a transient reconnect artifact — clearing the
|
|
248
|
+
// credential on it would strand the user signed out with no way back, so
|
|
249
|
+
// we clear only the local UI session and keep the credential (a dead one
|
|
250
|
+
// re-mints to `no_active_session` and resolves signed-out on next boot).
|
|
251
|
+
if (origin === 'request') {
|
|
252
|
+
(0, _useAuthOperations.clearPersistedAuthSafe)(authStore, logger);
|
|
253
|
+
}
|
|
245
254
|
void clearSessionStateRef.current().catch(clearError => {
|
|
246
255
|
logger('Failed to clear local state on remote sign-out', clearError);
|
|
247
256
|
});
|
|
@@ -261,8 +270,12 @@ const OxyProvider = ({
|
|
|
261
270
|
return;
|
|
262
271
|
}
|
|
263
272
|
if (state.accounts.length === 0) {
|
|
273
|
+
// Clear the LOCAL UI session on any applied empty state, but NEVER the
|
|
274
|
+
// durable device credential here: `syncFromClient` runs for socket-pushed
|
|
275
|
+
// (possibly transient) states too, and wiping the credential on one would
|
|
276
|
+
// strand the user signed out. The credential wipe is gated on a
|
|
277
|
+
// `request`-origin verdict in `onUnauthenticated` above.
|
|
264
278
|
sessionClientHost.setCurrentAccountId(null);
|
|
265
|
-
(0, _useAuthOperations.clearPersistedAuthSafe)(authStore, logger);
|
|
266
279
|
await clearSessionState();
|
|
267
280
|
return;
|
|
268
281
|
}
|
|
@@ -289,7 +302,7 @@ const OxyProvider = ({
|
|
|
289
302
|
loginSuccess(activeUser);
|
|
290
303
|
}
|
|
291
304
|
sessionClientHost.setCurrentAccountId(latest.activeAccountId);
|
|
292
|
-
}, [oxyServices, sessionClient, sessionClientHost,
|
|
305
|
+
}, [oxyServices, sessionClient, sessionClientHost, updateSessions, setActiveSessionId, loginSuccess, clearSessionState, logger]);
|
|
293
306
|
const syncFromClientRef = (0, _react.useRef)(syncFromClient);
|
|
294
307
|
syncFromClientRef.current = syncFromClient;
|
|
295
308
|
const syncDeviceCredentialToHost = (0, _react.useCallback)(async () => {
|
|
@@ -476,45 +489,27 @@ const OxyProvider = ({
|
|
|
476
489
|
setActiveSessionId(input.sessionId);
|
|
477
490
|
}
|
|
478
491
|
|
|
479
|
-
// Register into the device set
|
|
480
|
-
//
|
|
481
|
-
// the
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
// back to the minimal shape if the profile fetch fails.
|
|
501
|
-
let fullUser = null;
|
|
502
|
-
try {
|
|
503
|
-
fullUser = await oxyServices.getCurrentUser();
|
|
504
|
-
} catch (profileError) {
|
|
505
|
-
if (__DEV__) {
|
|
506
|
-
_core.logger.debug('Failed to fetch full user on commit; using minimal fallback', {
|
|
507
|
-
component: 'OxyContext',
|
|
508
|
-
method: 'commitSession'
|
|
509
|
-
}, profileError);
|
|
510
|
-
}
|
|
511
|
-
fullUser = input.user ?? null;
|
|
512
|
-
}
|
|
513
|
-
if (fullUser) {
|
|
514
|
-
loginSuccess(fullUser);
|
|
515
|
-
onAuthStateChangeRef.current?.(fullUser);
|
|
516
|
-
}
|
|
517
|
-
markAuthResolvedRef.current();
|
|
492
|
+
// Register into the device set, hydrate the full user, and flip the
|
|
493
|
+
// auth-resolution gate. A deliberate sign-in ACTIVATES the account and
|
|
494
|
+
// BLOCKS on the reconcile (register + socket + sessions projection) before
|
|
495
|
+
// resolving — unchanged ordering. The cold boot resolves auth from the
|
|
496
|
+
// profile fetch FIRST and reconciles the device set in the background, so
|
|
497
|
+
// first paint is not held behind those extra round-trips. Reconcile is
|
|
498
|
+
// best-effort — a failure never fails the sign-in (cold boot re-registers
|
|
499
|
+
// on the next load).
|
|
500
|
+
await (0, _commitSessionFlow.commitDeviceSetAndResolve)({
|
|
501
|
+
activate: options.activate,
|
|
502
|
+
userId: input.userId,
|
|
503
|
+
fallbackUser: input.user ?? null,
|
|
504
|
+
registerAndActivate: userId => sessionClient.registerAndActivate(userId),
|
|
505
|
+
addCurrentAccount: () => sessionClient.addCurrentAccount(),
|
|
506
|
+
startSocket: () => sessionClient.start(),
|
|
507
|
+
syncFromClient,
|
|
508
|
+
getCurrentUser: () => oxyServices.getCurrentUser(),
|
|
509
|
+
loginSuccess,
|
|
510
|
+
onAuthStateChange: onAuthStateChangeRef.current,
|
|
511
|
+
markAuthResolved: markAuthResolvedRef.current
|
|
512
|
+
});
|
|
518
513
|
if (options.activate && options.hubSync && hubSync && (0, _isWebBrowser.isWebBrowser)()) {
|
|
519
514
|
try {
|
|
520
515
|
await (0, _core.syncHubAfterSignIn)(oxyServices, {
|
|
@@ -577,7 +572,13 @@ const OxyProvider = ({
|
|
|
577
572
|
return;
|
|
578
573
|
}
|
|
579
574
|
void _reactNative.Linking.openURL(url);
|
|
580
|
-
}
|
|
575
|
+
},
|
|
576
|
+
// Native-only probe so the controller can detect an installed Commons and
|
|
577
|
+
// deep-link straight into its approve screen (keeping the QR/polling live
|
|
578
|
+
// as fallback). `undefined` on web mirrors the `openUrl` split — the
|
|
579
|
+
// controller short-circuits, so `redirectToAuthorize` never runs for the
|
|
580
|
+
// `oxycommons://` payload.
|
|
581
|
+
canOpenApp: (0, _isWebBrowser.isWebBrowser)() ? undefined : url => _reactNative.Linking.canOpenURL(url)
|
|
581
582
|
});
|
|
582
583
|
}
|
|
583
584
|
const accountDialogController = accountDialogControllerRef.current;
|
|
@@ -708,10 +709,10 @@ const OxyProvider = ({
|
|
|
708
709
|
if (document.visibilityState !== 'visible') return;
|
|
709
710
|
const reconcile = async () => {
|
|
710
711
|
if (!oxyServices.getAccessToken() && sessionClientHost.getDeviceCredential()) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
712
|
+
// Route through the ONE shared single-flight the scheduler/preflight/401
|
|
713
|
+
// use — never a private mint lane — so a tab-focus reconcile can't
|
|
714
|
+
// double-rotate the device secret against them.
|
|
715
|
+
await oxyServices.httpService.refreshAccessToken('preflight');
|
|
715
716
|
}
|
|
716
717
|
if (!oxyServices.getAccessToken() && !sessionClientHost.getDeviceCredential()) {
|
|
717
718
|
return;
|
|
@@ -723,7 +724,7 @@ const OxyProvider = ({
|
|
|
723
724
|
};
|
|
724
725
|
document.addEventListener('visibilitychange', onVisibility);
|
|
725
726
|
return () => document.removeEventListener('visibilitychange', onVisibility);
|
|
726
|
-
}, [oxyServices, sessionClient, sessionClientHost,
|
|
727
|
+
}, [oxyServices, sessionClient, sessionClientHost, syncFromClient]);
|
|
727
728
|
|
|
728
729
|
// Exposed `refreshSessions`: re-bootstrap the server-authoritative device
|
|
729
730
|
// state and reproject — the manual counterpart to the realtime socket.
|
|
@@ -819,6 +820,7 @@ const OxyProvider = ({
|
|
|
819
820
|
storageKeyPrefix,
|
|
820
821
|
clientId,
|
|
821
822
|
oxyServices,
|
|
823
|
+
sessionClient,
|
|
822
824
|
useFollow: useFollowHook,
|
|
823
825
|
showBottomSheet: showBottomSheetForContext,
|
|
824
826
|
openAvatarPicker,
|
|
@@ -830,7 +832,7 @@ const OxyProvider = ({
|
|
|
830
832
|
switchToAccount,
|
|
831
833
|
refreshAccounts,
|
|
832
834
|
createAccount: createAccountFn
|
|
833
|
-
}), [user, sessions, activeSessionId, isAuthenticated, isLoading, tokenReady, hasAccessToken, canUsePrivateApi, isPrivateApiPending, authResolved, storage, error, currentLanguage, currentLanguageMetadata, currentLanguageName, currentNativeLanguageName, hasIdentity, getPublicKey, signIn, signInWithPassword, completeTwoFactorSignIn, revokeSuspiciousSignIn, handleWebSession, logout, logoutAll, switchSessionForContext, refreshSessionsForContext, setLanguage, getDeviceSessions, logoutAllDeviceSessions, updateDeviceName, clearSessionState, clearAllAccountData, storageKeyPrefix, clientId, oxyServices, useFollowHook, showBottomSheetForContext, openAvatarPicker, accountDialogController, accountDialogOpen, openAccountDialog, closeAccountDialog, accounts, switchToAccount, refreshAccounts, createAccountFn]);
|
|
835
|
+
}), [user, sessions, activeSessionId, isAuthenticated, isLoading, tokenReady, hasAccessToken, canUsePrivateApi, isPrivateApiPending, authResolved, storage, error, currentLanguage, currentLanguageMetadata, currentLanguageName, currentNativeLanguageName, hasIdentity, getPublicKey, signIn, signInWithPassword, completeTwoFactorSignIn, revokeSuspiciousSignIn, handleWebSession, logout, logoutAll, switchSessionForContext, refreshSessionsForContext, setLanguage, getDeviceSessions, logoutAllDeviceSessions, updateDeviceName, clearSessionState, clearAllAccountData, storageKeyPrefix, clientId, oxyServices, sessionClient, useFollowHook, showBottomSheetForContext, openAvatarPicker, accountDialogController, accountDialogOpen, openAccountDialog, closeAccountDialog, accounts, switchToAccount, refreshAccounts, createAccountFn]);
|
|
834
836
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(OxyContext.Provider, {
|
|
835
837
|
value: contextValue,
|
|
836
838
|
children: children
|
|
@@ -886,6 +888,7 @@ const LOADING_STATE = {
|
|
|
886
888
|
storageKeyPrefix: 'oxy_session',
|
|
887
889
|
clientId: null,
|
|
888
890
|
oxyServices: LOADING_STATE_OXY_SERVICES,
|
|
891
|
+
sessionClient: null,
|
|
889
892
|
openAvatarPicker: () => {},
|
|
890
893
|
accountDialogController: null,
|
|
891
894
|
isAccountDialogOpen: false,
|