@umutcansu/react-native-pinvault 0.0.0-stage → 2.3.2
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/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +386 -2
- package/RNPinVault.podspec +43 -0
- package/android/build.gradle +68 -0
- package/android/consumer-rules.pro +25 -0
- package/android/src/main/AndroidManifest.xml +16 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ConfigParser.kt +300 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/JsEnvironmentGuard.kt +53 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/NativeSecurity.kt +239 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultModule.kt +410 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultNetworking.kt +296 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultPackage.kt +25 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinnedFetch.kt +131 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ResultMapper.kt +188 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/StrictJson.kt +225 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/VaultTokenStore.kt +63 -0
- package/ios/Core/ConfigParser.swift +276 -0
- package/ios/Core/JSEnvironmentGuard.swift +129 -0
- package/ios/Core/NativeSecurity.swift +197 -0
- package/ios/Core/PinnedFetch.swift +119 -0
- package/ios/Core/ReactNetworking.swift +172 -0
- package/ios/Core/ResultMapper.swift +163 -0
- package/ios/Core/StrictJSON.swift +214 -0
- package/ios/Core/VaultTokenStore.swift +66 -0
- package/ios/PinVaultBridge.swift +412 -0
- package/ios/PinVaultReactNetworking.swift +47 -0
- package/ios/RNPinVault.h +7 -0
- package/ios/RNPinVault.mm +293 -0
- package/ios/RNPinVaultURLRequestHandler.mm +92 -0
- package/lib/module/NativePinVault.js +13 -0
- package/lib/module/NativePinVault.js.map +1 -0
- package/lib/module/index.js +312 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativePinVault.d.ts +50 -0
- package/lib/typescript/src/NativePinVault.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +150 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +346 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +128 -4
- package/src/NativePinVault.ts +72 -0
- package/src/index.ts +385 -0
- package/src/types.ts +351 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
package io.github.umutcansu.pinvault.reactnative
|
|
2
|
+
|
|
3
|
+
import android.content.pm.ApplicationInfo
|
|
4
|
+
import android.provider.Settings
|
|
5
|
+
import androidx.fragment.app.FragmentActivity
|
|
6
|
+
import com.facebook.react.bridge.Arguments
|
|
7
|
+
import com.facebook.react.bridge.Promise
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
|
+
import com.facebook.react.bridge.WritableArray
|
|
10
|
+
import com.facebook.react.bridge.WritableMap
|
|
11
|
+
import io.github.umutcansu.pinvault.PinVault
|
|
12
|
+
import io.github.umutcansu.pinvault.api.PinVaultConnectionListener
|
|
13
|
+
import io.github.umutcansu.pinvault.model.VaultFileStatus
|
|
14
|
+
import kotlinx.coroutines.CoroutineScope
|
|
15
|
+
import kotlinx.coroutines.Dispatchers
|
|
16
|
+
import kotlinx.coroutines.SupervisorJob
|
|
17
|
+
import kotlinx.coroutines.cancel
|
|
18
|
+
import kotlinx.coroutines.launch
|
|
19
|
+
import kotlinx.coroutines.sync.Mutex
|
|
20
|
+
import kotlinx.coroutines.sync.withLock
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The TurboModule (`RNPinVault`). Every method runs off the JS thread and
|
|
24
|
+
* answers through its promise; every structured input is parsed strictly
|
|
25
|
+
* ([ConfigParser], [PinnedFetch]); every result is mapped by [ResultMapper].
|
|
26
|
+
* Nothing here pins, decrypts or stores anything itself: it calls the
|
|
27
|
+
* `PinVault` object of the Android library.
|
|
28
|
+
*/
|
|
29
|
+
class PinVaultModule(reactContext: ReactApplicationContext) : NativePinVaultSpec(reactContext) {
|
|
30
|
+
|
|
31
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
32
|
+
private val app get() = reactApplicationContext.applicationContext
|
|
33
|
+
private val mapper = ResultMapper(tokens)
|
|
34
|
+
|
|
35
|
+
init {
|
|
36
|
+
current = this
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun getName(): String = NAME
|
|
40
|
+
|
|
41
|
+
override fun invalidate() {
|
|
42
|
+
scope.cancel()
|
|
43
|
+
super.invalidate()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
private fun run(promise: Promise, block: suspend () -> Any?) {
|
|
49
|
+
scope.launch {
|
|
50
|
+
try {
|
|
51
|
+
promise.resolve(toBridge(block()))
|
|
52
|
+
} catch (_: AlreadyAnswered) {
|
|
53
|
+
// The block rejected the promise itself.
|
|
54
|
+
} catch (e: BridgeInputException) {
|
|
55
|
+
promise.reject(E_INVALID_ARGUMENT, tokens.redact(e.message))
|
|
56
|
+
} catch (e: IllegalStateException) {
|
|
57
|
+
// The library throws this before init.
|
|
58
|
+
promise.reject(E_NOT_STARTED, tokens.redact(e.message), exceptionInfo(e))
|
|
59
|
+
} catch (e: Exception) {
|
|
60
|
+
promise.reject(E_NATIVE, tokens.redact("${e.javaClass.simpleName}: ${e.message}"), exceptionInfo(e))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A getter that, like iOS, answers an empty value before `start()` instead of failing. */
|
|
66
|
+
private fun <T> started(default: T, block: () -> T): T =
|
|
67
|
+
if (!startedInProcess) default else try { block() } catch (_: IllegalStateException) { default }
|
|
68
|
+
|
|
69
|
+
private fun exceptionInfo(e: Throwable): WritableMap = Arguments.createMap().apply {
|
|
70
|
+
putString("exceptionName", e.javaClass.simpleName)
|
|
71
|
+
putString("exceptionMessage", tokens.redact(e.message))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private fun emitGuard(requestId: String, operation: String) {
|
|
75
|
+
emitOnGuardRequest(Arguments.createMap().apply {
|
|
76
|
+
putString("requestId", requestId)
|
|
77
|
+
putString("operation", operation)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The library outlives a JS reload: events and guard questions go to the live module.
|
|
82
|
+
private val connectionListener = PinVaultConnectionListener { event ->
|
|
83
|
+
try {
|
|
84
|
+
val module = current ?: return@PinVaultConnectionListener
|
|
85
|
+
module.emitOnConnectionEvent(toBridge(module.mapper.event(event)) as WritableMap)
|
|
86
|
+
} catch (_: Exception) {
|
|
87
|
+
// A JS runtime that is going away; telemetry must never break a handshake.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Start / config ───────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
override fun start(configJson: String, promise: Promise) {
|
|
94
|
+
scope.launch {
|
|
95
|
+
startLock.withLock {
|
|
96
|
+
var newGuard: JsEnvironmentGuard? = null
|
|
97
|
+
val parsed = try {
|
|
98
|
+
ConfigParser.parse(
|
|
99
|
+
configJson, tokens,
|
|
100
|
+
guardFactory = { timeout ->
|
|
101
|
+
JsEnvironmentGuard(timeout) { id, op -> current?.emitGuard(id, op) }.also { newGuard = it }
|
|
102
|
+
},
|
|
103
|
+
listener = connectionListener,
|
|
104
|
+
native = NativeSecurity.load(app),
|
|
105
|
+
release = NativeSecurity.isReleaseBuild(app),
|
|
106
|
+
)
|
|
107
|
+
} catch (e: IllegalArgumentException) {
|
|
108
|
+
// The guard of the running config stays: a refused config changes nothing.
|
|
109
|
+
promise.reject(E_INVALID_CONFIG, tokens.redact(e.message))
|
|
110
|
+
return@launch
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
if (!parsed.pinGlobalNetworking && PinVaultNetworking.isInstalled) {
|
|
114
|
+
promise.reject(
|
|
115
|
+
E_INVALID_CONFIG,
|
|
116
|
+
"config.android.pinGlobalNetworking: false, but React Native's networking is already pinned " +
|
|
117
|
+
"(the plugin's content provider, or PinVaultNetworking.install); opt out natively: " +
|
|
118
|
+
"remove the provider in the app's manifest (tools:node=\"remove\", README \"Networking\")",
|
|
119
|
+
)
|
|
120
|
+
return@launch
|
|
121
|
+
}
|
|
122
|
+
if (parsed.pinGlobalNetworking) PinVaultNetworking.install(app)
|
|
123
|
+
val hooks = PinVaultNetworking.warnIfNotPinned("PinVault.start")
|
|
124
|
+
if (parsed.networking.requirePinned && !hooks.pinned) {
|
|
125
|
+
promise.reject(E_NETWORKING_NOT_PINNED, "requirePinnedReactNativeNetworking: ${hooks.describe()}")
|
|
126
|
+
return@launch
|
|
127
|
+
}
|
|
128
|
+
guard = newGuard
|
|
129
|
+
// A second start applies the new config: the library keeps the
|
|
130
|
+
// first one otherwise (the samples restart the same way).
|
|
131
|
+
if (startedInProcess) {
|
|
132
|
+
PinVaultNetworking.pinning.deactivate()
|
|
133
|
+
PinVault.reset()
|
|
134
|
+
}
|
|
135
|
+
PinVaultNetworking.pinning.options = parsed.networking
|
|
136
|
+
val result = PinVault.init(app, parsed.config)
|
|
137
|
+
startedInProcess = true
|
|
138
|
+
if (PinVaultNetworking.isInstalled) {
|
|
139
|
+
try {
|
|
140
|
+
PinVaultNetworking.pinning.activate(parsed.networking)
|
|
141
|
+
} catch (_: IllegalStateException) {
|
|
142
|
+
// Setup failed (InitResult.Failed): RN's https stays refused.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
promise.resolve(toBridge(mapper.init(result)))
|
|
146
|
+
} catch (e: Exception) {
|
|
147
|
+
promise.reject(E_NATIVE, tokens.redact("${e.javaClass.simpleName}: ${e.message}"), exceptionInfo(e))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
override fun updateNow(promise: Promise) = run(promise) { mapper.update(PinVault.updateNow()) }
|
|
154
|
+
|
|
155
|
+
override fun currentVersion(promise: Promise) = run(promise) { started(0) { PinVault.currentVersion() } }
|
|
156
|
+
|
|
157
|
+
override fun hostPinVersions(promise: Promise) = run(promise) { started(emptyMap()) { PinVault.hostPinVersions() } }
|
|
158
|
+
|
|
159
|
+
override fun pinsForHost(hostname: String, promise: Promise) = run(promise) {
|
|
160
|
+
mapOf("pins" to started(null) { PinVault.pinsForHost(hostname) })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
override fun signingStatus(configApiId: String?, promise: Promise) = run(promise) {
|
|
164
|
+
started(null) { PinVault.signingStatus(configApiId) }?.let(mapper::signing)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override fun isForceUpdate(promise: Promise) = run(promise) { started(false) { PinVault.isForceUpdate() } }
|
|
168
|
+
|
|
169
|
+
override fun reset(promise: Promise) = run(promise) {
|
|
170
|
+
started(Unit) { PinVault.reset() }
|
|
171
|
+
null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
override fun schedulePeriodicUpdates(intervalHours: Double?, promise: Promise) {
|
|
175
|
+
if (!startedInProcess) return promise.resolve(false)
|
|
176
|
+
val hours = intervalHours?.toLong()
|
|
177
|
+
if (hours != null && (hours < 1 || hours > 24L * 30)) {
|
|
178
|
+
return promise.reject(E_INVALID_ARGUMENT, "intervalHours: must be between 1 and 720")
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
val callback: (Boolean) -> Unit = { promise.resolve(it) }
|
|
182
|
+
if (hours == null) PinVault.schedulePeriodicUpdates(onScheduled = callback)
|
|
183
|
+
else PinVault.schedulePeriodicUpdates(hours, callback)
|
|
184
|
+
} catch (e: Exception) {
|
|
185
|
+
promise.reject(E_NATIVE, tokens.redact(e.message), exceptionInfo(e))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
override fun cancelPeriodicUpdates(promise: Promise) = run(promise) {
|
|
190
|
+
started(Unit) { PinVault.cancelPeriodicUpdates() }
|
|
191
|
+
null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
override fun enableDebugLogging(promise: Promise) = run(promise) {
|
|
195
|
+
val debuggable = (app.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
|
196
|
+
if (debuggable) PinVault.enableDebugLogging()
|
|
197
|
+
debuggable
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Pinned HTTP ──────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
override fun fetch(requestJson: String, promise: Promise) {
|
|
203
|
+
scope.launch {
|
|
204
|
+
// RN's own fetch is pinned only while both hooks are PinVault's: say so when another library replaced one.
|
|
205
|
+
if (PinVaultNetworking.isInstalled) PinVaultNetworking.warnIfNotPinned("PinVault.fetch")
|
|
206
|
+
val request = try {
|
|
207
|
+
PinnedFetch.parse(requestJson)
|
|
208
|
+
} catch (e: IllegalArgumentException) {
|
|
209
|
+
promise.reject(E_INVALID_ARGUMENT, tokens.redact(e.message))
|
|
210
|
+
return@launch
|
|
211
|
+
}
|
|
212
|
+
val client = try {
|
|
213
|
+
if (request.settings != null) PinVault.getClient(request.settings) else PinVault.getClient()
|
|
214
|
+
} catch (e: IllegalStateException) {
|
|
215
|
+
promise.reject(E_NOT_STARTED, "PinVault has not started: call start() first")
|
|
216
|
+
return@launch
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
promise.resolve(toBridge(PinnedFetch.execute(client, request)))
|
|
220
|
+
} catch (e: Exception) {
|
|
221
|
+
val name = e.javaClass.simpleName
|
|
222
|
+
promise.reject(E_FETCH, tokens.redact("$name: ${e.message}"), exceptionInfo(e))
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Enrollment ───────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
override fun deviceId(promise: Promise) = run(promise) {
|
|
230
|
+
@Suppress("HardwareIds")
|
|
231
|
+
Settings.Secure.getString(app.contentResolver, Settings.Secure.ANDROID_ID)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
override fun enrollForResult(token: String, label: String?, promise: Promise) = run(promise) {
|
|
235
|
+
if (token.isEmpty() || token.length > 4096) throw BridgeInputException("token: must be 1 to 4096 characters")
|
|
236
|
+
tokens.withTransientSecretSuspend(token) { mapper.enrollment(PinVault.enrollForResult(app, token, label)) }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
override fun autoEnrollForResult(promise: Promise) = run(promise) { mapper.enrollment(PinVault.autoEnrollForResult(app)) }
|
|
240
|
+
|
|
241
|
+
override fun checkPendingEnrollment(promise: Promise) = run(promise) { mapper.enrollment(PinVault.checkPendingEnrollment(app)) }
|
|
242
|
+
|
|
243
|
+
override fun isEnrolled(label: String?, promise: Promise) = run(promise) { PinVault.isEnrolled(app, label) }
|
|
244
|
+
|
|
245
|
+
override fun isEnrollmentPending(label: String?, promise: Promise) = run(promise) { PinVault.isEnrollmentPending(app, label) }
|
|
246
|
+
|
|
247
|
+
override fun enrollmentVerificationCode(label: String?, promise: Promise) = run(promise) {
|
|
248
|
+
PinVault.enrollmentVerificationCode(app, label)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
override fun enrolledClientCN(label: String?, promise: Promise) = run(promise) { PinVault.enrolledClientCN(app, label) }
|
|
252
|
+
|
|
253
|
+
override fun enrolledClientNotAfter(label: String?, promise: Promise) = run(promise) {
|
|
254
|
+
PinVault.enrolledClientNotAfter(app, label)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
override fun unenroll(label: String?, wipeVaultFiles: Boolean, promise: Promise) = run(promise) {
|
|
258
|
+
if (wipeVaultFiles) PinVault.unenroll(app, label, true) else PinVault.unenroll(app, label)
|
|
259
|
+
null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
override fun identityKeySecurityLevel(label: String?, promise: Promise) = run(promise) {
|
|
263
|
+
PinVault.identityKeySecurityLevel(label)?.name
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Vault files ──────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
override fun setVaultToken(key: String, token: String?, promise: Promise) = run(promise) {
|
|
269
|
+
checkKey(key)
|
|
270
|
+
if (token != null && token.length > 4096) throw BridgeInputException("token: longer than 4096 characters")
|
|
271
|
+
tokens.put(key, token)
|
|
272
|
+
null
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
override fun clearVaultTokens(promise: Promise) = run(promise) { tokens.clear() }
|
|
276
|
+
|
|
277
|
+
override fun fetchFile(key: String, token: String?, promise: Promise) = run(promise) {
|
|
278
|
+
checkKey(key)
|
|
279
|
+
if (token != null) {
|
|
280
|
+
if (token.length > 4096) throw BridgeInputException("token: longer than 4096 characters")
|
|
281
|
+
tokens.put(key, token)
|
|
282
|
+
}
|
|
283
|
+
mapper.vaultFile(PinVault.fetchFile(key))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
override fun loadFile(key: String, encoding: String, promise: Promise) = run(promise) {
|
|
287
|
+
checkKey(key)
|
|
288
|
+
val enc = ResultMapper.checkEncoding(encoding)
|
|
289
|
+
started(null) { PinVault.loadFile(key) }?.let { ResultMapper.encode(it, enc) }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
override fun fileStatus(key: String, promise: Promise) = run(promise) {
|
|
293
|
+
checkKey(key)
|
|
294
|
+
started(VaultFileStatus.NOT_STORED) { PinVault.fileStatus(key) }.name
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
override fun unlockFile(key: String, promptJson: String, promise: Promise) = run(promise) {
|
|
298
|
+
checkKey(key)
|
|
299
|
+
val (prompt, encoding) = ConfigParser.unlockPrompt(promptJson)
|
|
300
|
+
val activity = reactApplicationContext.currentActivity as? FragmentActivity
|
|
301
|
+
?: return@run rejectNow(promise, E_NO_ACTIVITY, "unlockFile needs a FragmentActivity in the foreground")
|
|
302
|
+
// The library shows BiometricPrompt on the main thread itself.
|
|
303
|
+
mapper.unlock(PinVault.unlockFile(activity, key, prompt), encoding)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
override fun isFileLocked(key: String, promise: Promise) = run(promise) { checkKey(key); started(false) { PinVault.isFileLocked(key) } }
|
|
307
|
+
|
|
308
|
+
override fun hasFile(key: String, promise: Promise) = run(promise) { checkKey(key); started(false) { PinVault.hasFile(key) } }
|
|
309
|
+
|
|
310
|
+
override fun fileVersion(key: String, promise: Promise) = run(promise) { checkKey(key); started(0) { PinVault.fileVersion(key) } }
|
|
311
|
+
|
|
312
|
+
override fun clearFile(key: String, promise: Promise) = run(promise) {
|
|
313
|
+
checkKey(key)
|
|
314
|
+
started(Unit) { PinVault.clearFile(key) }
|
|
315
|
+
null
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
override fun syncAllFiles(promise: Promise) = run(promise) {
|
|
319
|
+
PinVault.syncAllFiles().mapValues { (_, r) -> mapper.vaultFile(r) }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Attestation ──────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
override fun attestNow(configApiId: String?, promise: Promise) = run(promise) { mapper.attestation(PinVault.attestNow(configApiId)) }
|
|
325
|
+
|
|
326
|
+
override fun fetchAttestationToken(host: String?, promise: Promise) = run(promise) {
|
|
327
|
+
mapper.attestationToken(PinVault.fetchAttestationToken(host))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
override fun attestationStatus(configApiId: String?, promise: Promise) = run(promise) {
|
|
331
|
+
mapper.attestation(PinVault.attestationStatus(configApiId))
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── environmentGuard ─────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
override fun answerGuard(requestId: String, allowed: Boolean) {
|
|
337
|
+
guard?.answer(requestId, allowed)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private fun checkKey(key: String) {
|
|
341
|
+
if (key.isEmpty() || key.length > 128) throw BridgeInputException("key: must be 1 to 128 characters")
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private fun rejectNow(promise: Promise, code: String, message: String): Nothing {
|
|
345
|
+
promise.reject(code, message)
|
|
346
|
+
throw AlreadyAnswered()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Thrown after the promise was already rejected; [run] must not answer again. */
|
|
350
|
+
private class AlreadyAnswered : RuntimeException()
|
|
351
|
+
|
|
352
|
+
companion object {
|
|
353
|
+
const val NAME = NativePinVaultSpec.NAME
|
|
354
|
+
|
|
355
|
+
const val E_INVALID_CONFIG = "E_INVALID_CONFIG"
|
|
356
|
+
const val E_INVALID_ARGUMENT = "E_INVALID_ARGUMENT"
|
|
357
|
+
const val E_NOT_STARTED = "E_NOT_STARTED"
|
|
358
|
+
const val E_FETCH = "E_FETCH"
|
|
359
|
+
const val E_NO_ACTIVITY = "E_NO_ACTIVITY"
|
|
360
|
+
const val E_NATIVE = "E_NATIVE"
|
|
361
|
+
const val E_NETWORKING_NOT_PINNED = "E_NETWORKING_NOT_PINNED"
|
|
362
|
+
|
|
363
|
+
/** Process-wide: the library is a process singleton, a JS reload makes a new module. */
|
|
364
|
+
@Volatile private var current: PinVaultModule? = null
|
|
365
|
+
@Volatile private var startedInProcess = false
|
|
366
|
+
@Volatile private var guard: JsEnvironmentGuard? = null
|
|
367
|
+
private val startLock = Mutex()
|
|
368
|
+
private val tokens = VaultTokenStore()
|
|
369
|
+
|
|
370
|
+
/** Plain values → bridge values (maps, lists, numbers as doubles). */
|
|
371
|
+
internal fun toBridge(value: Any?): Any? = when (value) {
|
|
372
|
+
null, is Unit -> null
|
|
373
|
+
is String, is Boolean -> value
|
|
374
|
+
is Int -> value
|
|
375
|
+
is Long -> value.toDouble()
|
|
376
|
+
is Number -> value.toDouble()
|
|
377
|
+
is Map<*, *> -> Arguments.createMap().also { map ->
|
|
378
|
+
value.forEach { (k, v) -> putInto(map, k as String, toBridge(v)) }
|
|
379
|
+
}
|
|
380
|
+
is List<*> -> Arguments.createArray().also { array -> value.forEach { pushInto(array, toBridge(it)) } }
|
|
381
|
+
else -> value.toString()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private fun putInto(map: WritableMap, key: String, v: Any?) {
|
|
385
|
+
when (v) {
|
|
386
|
+
null -> map.putNull(key)
|
|
387
|
+
is String -> map.putString(key, v)
|
|
388
|
+
is Boolean -> map.putBoolean(key, v)
|
|
389
|
+
is Int -> map.putInt(key, v)
|
|
390
|
+
is Double -> map.putDouble(key, v)
|
|
391
|
+
is WritableMap -> map.putMap(key, v)
|
|
392
|
+
is WritableArray -> map.putArray(key, v)
|
|
393
|
+
else -> map.putString(key, v.toString())
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private fun pushInto(array: WritableArray, v: Any?) {
|
|
398
|
+
when (v) {
|
|
399
|
+
null -> array.pushNull()
|
|
400
|
+
is String -> array.pushString(v)
|
|
401
|
+
is Boolean -> array.pushBoolean(v)
|
|
402
|
+
is Int -> array.pushInt(v)
|
|
403
|
+
is Double -> array.pushDouble(v)
|
|
404
|
+
is WritableMap -> array.pushMap(v)
|
|
405
|
+
is WritableArray -> array.pushArray(v)
|
|
406
|
+
else -> array.pushString(v.toString())
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|