@latchway/react-native 0.0.0-bootstrap.0 → 1.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 (108) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/LatchwayReactNative.podspec +33 -0
  3. package/NOTICE +7 -0
  4. package/README.md +293 -3
  5. package/SECURITY.md +69 -0
  6. package/android/build.gradle.kts +62 -0
  7. package/android/consumer-rules.pro +2 -0
  8. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  9. package/android/gradle/wrapper/gradle-wrapper.properties +9 -0
  10. package/android/gradle.properties +4 -0
  11. package/android/gradlew +251 -0
  12. package/android/gradlew.bat +94 -0
  13. package/android/settings.gradle.kts +33 -0
  14. package/android/src/main/AndroidManifest.xml +3 -0
  15. package/android/src/main/java/dev/latchway/reactnative/LatchwayReactNativePackage.kt +25 -0
  16. package/android/src/main/java/dev/latchway/reactnative/NativeLatchwayModule.kt +964 -0
  17. package/android/src/test/java/dev/latchway/reactnative/NativeLatchwayModuleTest.kt +521 -0
  18. package/babel.cjs +22 -0
  19. package/babel.d.cts +7 -0
  20. package/contract.lock +7 -0
  21. package/docs/architecture.md +145 -0
  22. package/docs/conformance.md +68 -0
  23. package/docs/langchain.md +135 -0
  24. package/docs/native-installation.md +172 -0
  25. package/docs/physical-device-evidence.md +559 -0
  26. package/docs/releasing.md +360 -0
  27. package/docs/security.md +55 -0
  28. package/ios/LatchwayNativeBridge.swift +1551 -0
  29. package/ios/RCTNativeLatchway.h +9 -0
  30. package/ios/RCTNativeLatchway.mm +175 -0
  31. package/lib/client.d.ts +29 -0
  32. package/lib/client.d.ts.map +1 -0
  33. package/lib/client.js +939 -0
  34. package/lib/client.js.map +1 -0
  35. package/lib/component-client.d.ts +15 -0
  36. package/lib/component-client.d.ts.map +1 -0
  37. package/lib/component-client.js +141 -0
  38. package/lib/component-client.js.map +1 -0
  39. package/lib/config.d.ts +25 -0
  40. package/lib/config.d.ts.map +1 -0
  41. package/lib/config.js +261 -0
  42. package/lib/config.js.map +1 -0
  43. package/lib/coordinator.d.ts +19 -0
  44. package/lib/coordinator.d.ts.map +1 -0
  45. package/lib/coordinator.js +167 -0
  46. package/lib/coordinator.js.map +1 -0
  47. package/lib/errors.d.ts +5 -0
  48. package/lib/errors.d.ts.map +1 -0
  49. package/lib/errors.js +201 -0
  50. package/lib/errors.js.map +1 -0
  51. package/lib/index.d.ts +8 -0
  52. package/lib/index.d.ts.map +1 -0
  53. package/lib/index.js +13 -0
  54. package/lib/index.js.map +1 -0
  55. package/lib/native/NativeLatchway.d.ts +25 -0
  56. package/lib/native/NativeLatchway.d.ts.map +1 -0
  57. package/lib/native/NativeLatchway.js +3 -0
  58. package/lib/native/NativeLatchway.js.map +1 -0
  59. package/lib/native/bridge.d.ts +5 -0
  60. package/lib/native/bridge.d.ts.map +1 -0
  61. package/lib/native/bridge.js +17 -0
  62. package/lib/native/bridge.js.map +1 -0
  63. package/lib/native-output.d.ts +3 -0
  64. package/lib/native-output.d.ts.map +1 -0
  65. package/lib/native-output.js +43 -0
  66. package/lib/native-output.js.map +1 -0
  67. package/lib/polyfills.d.ts +8 -0
  68. package/lib/polyfills.d.ts.map +1 -0
  69. package/lib/polyfills.js +56 -0
  70. package/lib/polyfills.js.map +1 -0
  71. package/lib/request-id.d.ts +2 -0
  72. package/lib/request-id.d.ts.map +1 -0
  73. package/lib/request-id.js +5 -0
  74. package/lib/request-id.js.map +1 -0
  75. package/lib/runtime-symbols.d.ts +2 -0
  76. package/lib/runtime-symbols.d.ts.map +1 -0
  77. package/lib/runtime-symbols.js +9 -0
  78. package/lib/runtime-symbols.js.map +1 -0
  79. package/lib/testing.d.ts +7 -0
  80. package/lib/testing.d.ts.map +1 -0
  81. package/lib/testing.js +9 -0
  82. package/lib/testing.js.map +1 -0
  83. package/lib/types.d.ts +200 -0
  84. package/lib/types.d.ts.map +1 -0
  85. package/lib/types.js +2 -0
  86. package/lib/types.js.map +1 -0
  87. package/lib/version.d.ts +8 -0
  88. package/lib/version.d.ts.map +1 -0
  89. package/lib/version.js +8 -0
  90. package/lib/version.js.map +1 -0
  91. package/package.json +152 -6
  92. package/react-native.config.cjs +7 -0
  93. package/release-compatibility.json +64 -0
  94. package/src/client.ts +1022 -0
  95. package/src/component-client.ts +158 -0
  96. package/src/config.ts +368 -0
  97. package/src/coordinator.ts +195 -0
  98. package/src/errors.ts +225 -0
  99. package/src/index.ts +53 -0
  100. package/src/native/NativeLatchway.ts +75 -0
  101. package/src/native/bridge.ts +23 -0
  102. package/src/native-output.ts +43 -0
  103. package/src/polyfills.ts +50 -0
  104. package/src/request-id.ts +5 -0
  105. package/src/runtime-symbols.ts +9 -0
  106. package/src/testing.ts +11 -0
  107. package/src/types.ts +241 -0
  108. package/src/version.ts +7 -0
@@ -0,0 +1,521 @@
1
+ package dev.latchway.reactnative
2
+
3
+ import com.facebook.react.bridge.Promise
4
+ import com.facebook.react.bridge.BridgeReactContext
5
+ import com.facebook.react.bridge.JavaOnlyMap
6
+ import com.facebook.react.bridge.ReactApplicationContext
7
+ import com.facebook.react.bridge.WritableMap
8
+ import dev.latchway.core.LATCHWAY_CONTRACT_VERSION
9
+ import dev.latchway.core.LATCHWAY_PROTOCOL_VERSION
10
+ import dev.latchway.core.LatchwayErrorCode
11
+ import dev.latchway.core.LatchwayException
12
+ import dev.latchway.okhttp.LATCHWAY_REACT_NATIVE_FRAMEWORK_ID
13
+ import dev.latchway.okhttp.LATCHWAY_REACT_NATIVE_FRAMEWORK_VERSION
14
+ import okhttp3.Authenticator
15
+ import okhttp3.MediaType.Companion.toMediaType
16
+ import okhttp3.OkHttpClient
17
+ import okhttp3.Protocol
18
+ import okhttp3.Request
19
+ import okhttp3.Response
20
+ import okhttp3.ResponseBody.Companion.toResponseBody
21
+ import okhttp3.mockwebserver.MockResponse
22
+ import okhttp3.mockwebserver.MockWebServer
23
+ import org.json.JSONArray
24
+ import org.json.JSONObject
25
+ import org.junit.After
26
+ import org.junit.Assert.assertEquals
27
+ import org.junit.Assert.assertFalse
28
+ import org.junit.Assert.assertNotEquals
29
+ import org.junit.Assert.assertNull
30
+ import org.junit.Assert.assertThrows
31
+ import org.junit.Assert.assertTrue
32
+ import org.junit.Test
33
+ import org.junit.runner.RunWith
34
+ import org.robolectric.RobolectricTestRunner
35
+ import org.robolectric.RuntimeEnvironment
36
+ import org.robolectric.annotation.Config
37
+ import okio.Buffer
38
+ import java.lang.reflect.Proxy
39
+ import java.nio.charset.StandardCharsets
40
+ import java.util.ArrayDeque
41
+ import java.util.Base64
42
+ import java.util.concurrent.CopyOnWriteArrayList
43
+ import java.util.concurrent.CountDownLatch
44
+ import java.util.concurrent.TimeUnit
45
+ import java.util.concurrent.atomic.AtomicInteger
46
+
47
+ @RunWith(RobolectricTestRunner::class)
48
+ @Config(manifest = Config.NONE, sdk = [35])
49
+ public class NativeLatchwayModuleTest {
50
+ private val fixtures = mutableListOf<ModuleFixture>()
51
+
52
+ @After
53
+ public fun closeFixtures() {
54
+ fixtures.reversed().forEach(ModuleFixture::close)
55
+ fixtures.clear()
56
+ }
57
+
58
+ @Test
59
+ public fun fwAuth101And102GeneratedSpecForwardsBootstrapAndDpopDispatchToNativeSdk() {
60
+ // FW-AUTH-101 / FW-AUTH-102: this is compiled bridge evidence. Pair it
61
+ // with the exact-AAR session/DPoP tests for the native cryptography.
62
+ val fake = FakeNativeClientOperations()
63
+ val fixture = configuredFixture(fake)
64
+
65
+ val response = fixture.startRequest(
66
+ identityToken = "external-identity-bootstrap",
67
+ requestJSON = nativeRequest(fixture.baseURL, "bootstrap through generated spec"),
68
+ )
69
+ val metadata = JSONObject(response.resolvedString())
70
+
71
+ assertEquals(200, metadata.getInt("status"))
72
+ assertEquals(1, fake.bootstrapCalls.get())
73
+ assertEquals(listOf("external-identity-bootstrap"), fake.networkIdentityTokens)
74
+ assertEquals(1, fake.protectedRequests.size)
75
+ val protected = fake.protectedRequests.single()
76
+ assertTrue(protected.header("Authorization")?.startsWith("DPoP ") == true)
77
+ assertEquals(2, protected.header("DPoP")?.count { it == '.' })
78
+ val protectedBody = Buffer()
79
+ requireNotNull(protected.body).writeTo(protectedBody)
80
+ assertEquals("bootstrap through generated spec", protectedBody.readUtf8())
81
+ assertTokenCleared(fake)
82
+ fixture.closeResponse(metadata.getString("responseID")).assertResolved()
83
+ }
84
+
85
+ @Test
86
+ public fun fwAuth103AndBeh104OneGeneratedSpecCallKeepsSafeRetryAndFreshProofNative() {
87
+ // FW-AUTH-103 / FW-BEH-104: the compiled TurboModule boundary owns one
88
+ // operation while OkHttp performs both attempts. Pair with the exact
89
+ // Android AAR proof test to establish real DPoP generation.
90
+ MockWebServer().use { server ->
91
+ server.enqueue(MockResponse()
92
+ .setResponseCode(401)
93
+ .setHeader("WWW-Authenticate", "DPoP error=\"use_dpop_nonce\", dpop_nonce=\"nonce-2\"")
94
+ .setBody("{}"))
95
+ server.enqueue(MockResponse().setResponseCode(200).setBody("{}"))
96
+ server.start()
97
+
98
+ val proofSequence = AtomicInteger()
99
+ val nativeRefreshes = AtomicInteger()
100
+ val identities = CopyOnWriteArrayList<String>()
101
+ val fake = FakeNativeClientOperations { operations ->
102
+ val authorization = "DPoP native-access-token"
103
+ nativeApplicationClientBuilder()
104
+ .addInterceptor { chain ->
105
+ identities += operations.currentIdentity()
106
+ val request = chain.request().newBuilder()
107
+ .header("Authorization", authorization)
108
+ .header("DPoP", semanticProof(proofSequence.incrementAndGet()))
109
+ .build()
110
+ chain.proceed(request)
111
+ }
112
+ .authenticator(Authenticator { _, response ->
113
+ identities += operations.currentIdentity()
114
+ nativeRefreshes.incrementAndGet()
115
+ response.request.newBuilder()
116
+ .header("Authorization", authorization)
117
+ .header("DPoP", semanticProof(proofSequence.incrementAndGet()))
118
+ .build()
119
+ })
120
+ .build()
121
+ }
122
+ val fixture = configuredFixture(fake, server.url("/").toString().removeSuffix("/"))
123
+
124
+ val response = fixture.startRequest(
125
+ identityToken = "external-identity-for-native-retry",
126
+ requestJSON = nativeRequest(fixture.baseURL, "one replayable body"),
127
+ )
128
+ val metadata = JSONObject(response.resolvedString())
129
+ val first = requireNotNull(server.takeRequest(5, TimeUnit.SECONDS))
130
+ val second = requireNotNull(server.takeRequest(5, TimeUnit.SECONDS))
131
+
132
+ assertEquals(200, metadata.getInt("status"))
133
+ assertEquals(1, nativeRefreshes.get())
134
+ assertEquals(2, server.requestCount)
135
+ assertEquals(first.body.readUtf8(), second.body.readUtf8())
136
+ assertEquals(first.headers["Authorization"], second.headers["Authorization"])
137
+ assertNotEquals(first.headers["DPoP"], second.headers["DPoP"])
138
+ assertEquals(
139
+ listOf("external-identity-for-native-retry", "external-identity-for-native-retry"),
140
+ identities,
141
+ )
142
+ assertTokenCleared(fake)
143
+ fixture.closeResponse(metadata.getString("responseID")).assertResolved()
144
+ }
145
+ }
146
+
147
+ @Test
148
+ public fun fwAuth104GeneratedSpecClearsRejectedIdentityBeforeReauthentication() {
149
+ // FW-AUTH-104
150
+ val fake = FakeNativeClientOperations()
151
+ fake.refreshFailures += LatchwayException(
152
+ code = LatchwayErrorCode.IDENTITY_REAUTHENTICATION_REQUIRED,
153
+ safeMessage = "synthetic native reauthentication request",
154
+ )
155
+ val fixture = configuredFixture(fake)
156
+
157
+ val rejected = fixture.refresh("external-identity-stale")
158
+ assertEquals("identity_reauthentication_required", rejected.rejection().code)
159
+ assertTokenCleared(fake)
160
+
161
+ fixture.refresh("external-identity-fresh").assertResolved()
162
+ assertEquals(
163
+ listOf("external-identity-stale", "external-identity-fresh"),
164
+ fake.refreshIdentityTokens,
165
+ )
166
+ assertTokenCleared(fake)
167
+ }
168
+
169
+ @Test
170
+ public fun fwAuth105GeneratedSpecDelegatesFamilyRetirementAndSurfacesTerminalState() {
171
+ // FW-AUTH-105: terminal-state enforcement is implemented by the exact
172
+ // native SDK; this compiled test proves the generated bridge forwards it.
173
+ val fake = FakeNativeClientOperations()
174
+ val fixture = configuredFixture(fake)
175
+
176
+ fixture.revokeFamily("external-identity-family-owner").assertResolved()
177
+ assertEquals(1, fake.familyRevocations.get())
178
+ assertTokenCleared(fake)
179
+
180
+ val rejected = fixture.quota("external-identity-after-family-retirement")
181
+ assertEquals("installation_family_revoked", rejected.rejection().code)
182
+ assertTrue(fake.protectedRequests.isEmpty())
183
+ assertTokenCleared(fake)
184
+ }
185
+
186
+ @Test
187
+ public fun fwAuth106GeneratedSpecSurfacesNativeComponentRevocationWithoutDispatch() {
188
+ // FW-AUTH-106: the Android SDK owns component state. The React Native
189
+ // TurboModule must preserve its closed terminal failure unchanged.
190
+ val fake = FakeNativeClientOperations().apply { componentRevoked = true }
191
+ val fixture = configuredFixture(fake)
192
+
193
+ val rejected = fixture.quota("external-identity-component-owner")
194
+
195
+ assertEquals("component_revoked", rejected.rejection().code)
196
+ assertTrue(fake.protectedRequests.isEmpty())
197
+ assertTokenCleared(fake)
198
+ }
199
+
200
+ @Test
201
+ public fun fwSec103ProductionResponseTargetRevalidationRejectsCrossOriginRedirectResult() {
202
+ // FW-SEC-103
203
+ val fake = FakeNativeClientOperations { operations ->
204
+ nativeApplicationClientBuilder()
205
+ .addInterceptor { chain ->
206
+ operations.redirectResponseAttempts.incrementAndGet()
207
+ val redirected = chain.request().newBuilder()
208
+ .url("https://redirect-attacker.invalid/v1/responses")
209
+ .build()
210
+ Response.Builder()
211
+ .request(redirected)
212
+ .protocol(Protocol.HTTP_1_1)
213
+ .code(200)
214
+ .message("OK")
215
+ .body("{}".toResponseBody(JSON_MEDIA_TYPE))
216
+ .build()
217
+ }
218
+ .build()
219
+ }
220
+ val fixture = configuredFixture(fake)
221
+ val policyClient = nativeApplicationClientBuilder().build()
222
+ try {
223
+ assertFalse(policyClient.followRedirects)
224
+ assertFalse(policyClient.followSslRedirects)
225
+ } finally {
226
+ policyClient.closeCompletely()
227
+ }
228
+
229
+ val rejected = fixture.startRequest(
230
+ identityToken = "external-identity-redirect-check",
231
+ requestJSON = nativeRequest(fixture.baseURL, "redirect must be revalidated"),
232
+ )
233
+
234
+ assertEquals("request_invalid", rejected.rejection().code)
235
+ assertEquals(1, fake.redirectResponseAttempts.get())
236
+ assertTokenCleared(fake)
237
+ }
238
+
239
+ private fun configuredFixture(
240
+ fake: FakeNativeClientOperations,
241
+ baseURL: String = "https://gateway.example.test",
242
+ ): ModuleFixture {
243
+ val reactContext: ReactApplicationContext = BridgeReactContext(RuntimeEnvironment.getApplication())
244
+ val factory = NativeClientFactory { configuration, _, _, tokenProvider, _ ->
245
+ fake.configuration = configuration
246
+ fake.tokenProvider = tokenProvider
247
+ fake
248
+ }
249
+ val module = NativeLatchwayModule(reactContext, factory) { JavaOnlyMap() }
250
+ val fixture = ModuleFixture(module, fake, baseURL)
251
+ fixtures += fixture
252
+
253
+ val configured = fixture.configure()
254
+ val compatibility = JSONObject(configured.resolvedString())
255
+ assertEquals("react_native_android", compatibility.getString("platform"))
256
+ assertEquals(LATCHWAY_CONTRACT_VERSION, compatibility.getString("contractVersion"))
257
+ assertEquals(LATCHWAY_PROTOCOL_VERSION, compatibility.getInt("protocolVersion"))
258
+ assertEquals(LATCHWAY_REACT_NATIVE_FRAMEWORK_ID, fake.configuration?.frameworkID)
259
+ assertEquals(LATCHWAY_REACT_NATIVE_FRAMEWORK_VERSION, fake.configuration?.frameworkVersion)
260
+ return fixture
261
+ }
262
+
263
+ private fun assertTokenCleared(fake: FakeNativeClientOperations) {
264
+ assertThrows(IllegalStateException::class.java) { fake.currentIdentity() }
265
+ }
266
+ }
267
+
268
+ private class ModuleFixture(
269
+ private val module: NativeLatchwayModule,
270
+ private val operations: FakeNativeClientOperations,
271
+ val baseURL: String,
272
+ ) {
273
+ private val operationSequence = AtomicInteger()
274
+ private var closed = false
275
+
276
+ fun configure(): RecordingPromise = RecordingPromise().also { promise ->
277
+ module.configure(CLIENT_ID, nativeConfiguration(baseURL), promise.value)
278
+ promise.await()
279
+ }
280
+
281
+ fun startRequest(identityToken: String, requestJSON: String): RecordingPromise =
282
+ RecordingPromise().also { promise ->
283
+ module.startRequest(
284
+ CLIENT_ID,
285
+ operationID("request"),
286
+ identityToken,
287
+ requestJSON,
288
+ promise.value,
289
+ )
290
+ promise.await()
291
+ }
292
+
293
+ fun refresh(identityToken: String): RecordingPromise = RecordingPromise().also { promise ->
294
+ module.refresh(CLIENT_ID, operationID("refresh"), identityToken, promise.value)
295
+ promise.await()
296
+ }
297
+
298
+ fun revokeFamily(identityToken: String): RecordingPromise = RecordingPromise().also { promise ->
299
+ module.revokeFamily(CLIENT_ID, operationID("family"), identityToken, promise.value)
300
+ promise.await()
301
+ }
302
+
303
+ fun quota(identityToken: String): RecordingPromise = RecordingPromise().also { promise ->
304
+ module.quota(CLIENT_ID, operationID("quota"), identityToken, "assistant", promise.value)
305
+ promise.await()
306
+ }
307
+
308
+ fun closeResponse(responseID: String): RecordingPromise = RecordingPromise().also { promise ->
309
+ module.closeResponse(CLIENT_ID, responseID, promise.value)
310
+ promise.await()
311
+ }
312
+
313
+ fun close() {
314
+ if (closed) return
315
+ closed = true
316
+ val disposed = RecordingPromise()
317
+ module.dispose(CLIENT_ID, disposed.value)
318
+ disposed.await()
319
+ module.invalidate()
320
+ if (!operations.closed) operations.close()
321
+ }
322
+
323
+ private fun operationID(kind: String): String =
324
+ "rn-android-$kind-${operationSequence.incrementAndGet()}"
325
+ }
326
+
327
+ private class FakeNativeClientOperations(
328
+ createApplicationClient: (FakeNativeClientOperations) -> OkHttpClient = { operations ->
329
+ nativeApplicationClientBuilder()
330
+ .addInterceptor { chain -> operations.defaultDispatch(chain.request()) }
331
+ .build()
332
+ },
333
+ ) : NativeClientOperations {
334
+ lateinit var tokenProvider: TransientIdentityTokenProvider
335
+ var configuration: NativeConfiguration? = null
336
+ var familyRevoked: Boolean = false
337
+ var componentRevoked: Boolean = false
338
+ var closed: Boolean = false
339
+ val bootstrapCalls = AtomicInteger()
340
+ val familyRevocations = AtomicInteger()
341
+ val redirectResponseAttempts = AtomicInteger()
342
+ val networkIdentityTokens = CopyOnWriteArrayList<String>()
343
+ val refreshIdentityTokens = CopyOnWriteArrayList<String>()
344
+ val protectedRequests = CopyOnWriteArrayList<Request>()
345
+ val refreshFailures = ArrayDeque<Throwable>()
346
+
347
+ override val applicationClient: OkHttpClient by lazy { createApplicationClient(this) }
348
+
349
+ override suspend fun refresh() {
350
+ refreshIdentityTokens += currentIdentity()
351
+ refreshFailures.pollFirst()?.let { throw it }
352
+ }
353
+
354
+ override suspend fun quota(feature: String): String {
355
+ if (familyRevoked) {
356
+ throw LatchwayException(
357
+ code = LatchwayErrorCode.INSTALLATION_FAMILY_REVOKED,
358
+ safeMessage = "synthetic native family terminal state",
359
+ )
360
+ }
361
+ if (componentRevoked) {
362
+ throw LatchwayException(
363
+ code = LatchwayErrorCode.COMPONENT_REVOKED,
364
+ safeMessage = "synthetic native component terminal state",
365
+ )
366
+ }
367
+ return JSONObject()
368
+ .put("feature", feature)
369
+ .put("observed_at", "2026-09-02T00:00:00Z")
370
+ .put("limits", JSONArray())
371
+ .toString()
372
+ }
373
+
374
+ override suspend fun diagnostics(): String = JSONObject()
375
+ .put("contractVersion", LATCHWAY_CONTRACT_VERSION)
376
+ .put("protocolVersion", LATCHWAY_PROTOCOL_VERSION)
377
+ .toString()
378
+
379
+ override suspend fun revokeCurrentInstallation() = Unit
380
+
381
+ override suspend fun revokeCurrentInstallationFamily() {
382
+ currentIdentity()
383
+ familyRevocations.incrementAndGet()
384
+ familyRevoked = true
385
+ }
386
+
387
+ fun currentIdentity(): String = tokenProvider.current()
388
+
389
+ override fun close() {
390
+ if (closed) return
391
+ closed = true
392
+ applicationClient.closeCompletely()
393
+ }
394
+
395
+ private fun defaultDispatch(request: Request): Response {
396
+ if (familyRevoked) {
397
+ throw LatchwayException(
398
+ code = LatchwayErrorCode.INSTALLATION_FAMILY_REVOKED,
399
+ safeMessage = "synthetic native family terminal state",
400
+ )
401
+ }
402
+ if (componentRevoked) {
403
+ throw LatchwayException(
404
+ code = LatchwayErrorCode.COMPONENT_REVOKED,
405
+ safeMessage = "synthetic native component terminal state",
406
+ )
407
+ }
408
+ networkIdentityTokens += currentIdentity()
409
+ bootstrapCalls.compareAndSet(0, 1)
410
+ val protected = request.newBuilder()
411
+ .header("Authorization", "DPoP native-access-token")
412
+ .header("DPoP", semanticProof(protectedRequests.size + 1))
413
+ .build()
414
+ protectedRequests += protected
415
+ return Response.Builder()
416
+ .request(protected)
417
+ .protocol(Protocol.HTTP_1_1)
418
+ .code(200)
419
+ .message("OK")
420
+ .body("{}".toResponseBody(JSON_MEDIA_TYPE))
421
+ .build()
422
+ }
423
+ }
424
+
425
+ private class RecordingPromise {
426
+ private val terminal = CountDownLatch(1)
427
+ @Volatile private var resolved: Any? = UNSET
428
+ @Volatile private var rejected: NativeRejection? = null
429
+
430
+ val value: Promise = Proxy.newProxyInstance(
431
+ Promise::class.java.classLoader,
432
+ arrayOf(Promise::class.java),
433
+ ) { _, method, arguments ->
434
+ when (method.name) {
435
+ "resolve" -> {
436
+ resolved = arguments?.firstOrNull()
437
+ terminal.countDown()
438
+ }
439
+ "reject" -> {
440
+ val values = arguments.orEmpty()
441
+ rejected = NativeRejection(
442
+ code = values.firstOrNull() as? String ?: "unspecified",
443
+ message = values.drop(1).filterIsInstance<String>().firstOrNull(),
444
+ userInfo = values.filterIsInstance<WritableMap>().firstOrNull(),
445
+ )
446
+ terminal.countDown()
447
+ }
448
+ }
449
+ null
450
+ } as Promise
451
+
452
+ fun await(): RecordingPromise {
453
+ assertTrue("native promise did not settle", terminal.await(10, TimeUnit.SECONDS))
454
+ return this
455
+ }
456
+
457
+ fun assertResolved() {
458
+ assertNull(rejected)
459
+ assertTrue(resolved !== UNSET)
460
+ }
461
+
462
+ fun resolvedString(): String {
463
+ assertResolved()
464
+ return resolved as String
465
+ }
466
+
467
+ fun rejection(): NativeRejection {
468
+ assertTrue(resolved === UNSET)
469
+ return requireNotNull(rejected)
470
+ }
471
+ }
472
+
473
+ private data class NativeRejection(
474
+ val code: String,
475
+ val message: String?,
476
+ val userInfo: WritableMap?,
477
+ )
478
+
479
+ private fun nativeConfiguration(baseURL: String): String = JSONObject()
480
+ .put("baseURL", baseURL)
481
+ .put("applicationID", "app_react_native_android_test")
482
+ .put("environment", "development")
483
+ .put("identityProvider", "mock_oidc")
484
+ .put("appVersion", "1.0.0-test")
485
+ .put("sdkVersion", "1.0.0")
486
+ .put("frameworkID", LATCHWAY_REACT_NATIVE_FRAMEWORK_ID)
487
+ .put("frameworkVersion", LATCHWAY_REACT_NATIVE_FRAMEWORK_VERSION)
488
+ .put("contractVersion", LATCHWAY_CONTRACT_VERSION)
489
+ .put("protocolVersion", LATCHWAY_PROTOCOL_VERSION)
490
+ .put("allowInsecureLoopback", baseURL.startsWith("http://127.0.0.1:"))
491
+ .put("apple", JSONObject()
492
+ .put("appAttestEnabled", true)
493
+ .put("softwareKeyFallbackPolicy", "forbidden"))
494
+ .put("android", JSONObject()
495
+ .put("playIntegrityCloudProjectNumber", "123456789")
496
+ .put("keyPolicy", "software_allowed"))
497
+ .toString()
498
+
499
+ private fun nativeRequest(baseURL: String, body: String): String = JSONObject()
500
+ .put("url", "$baseURL/v1/responses")
501
+ .put("method", "POST")
502
+ .put("feature", "assistant")
503
+ .put("headers", JSONArray().put(JSONArray().put("content-type").put("application/json")))
504
+ .put(
505
+ "bodyBase64",
506
+ Base64.getEncoder().encodeToString(body.toByteArray(StandardCharsets.UTF_8)),
507
+ )
508
+ .toString()
509
+
510
+ private fun semanticProof(sequence: Int): String =
511
+ "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0In0.eyJqdGkiOiJqdGktJHNlcXVlbmNlIn0.signature-$sequence"
512
+
513
+ private fun OkHttpClient.closeCompletely() {
514
+ dispatcher.cancelAll()
515
+ connectionPool.evictAll()
516
+ dispatcher.executorService.shutdown()
517
+ }
518
+
519
+ private val JSON_MEDIA_TYPE = "application/json".toMediaType()
520
+ private const val CLIENT_ID = "rn-android-compiled-test-client"
521
+ private val UNSET = Any()
package/babel.cjs ADDED
@@ -0,0 +1,22 @@
1
+ /** Opt-in compiler compatibility for LangChain on the supported RN baseline. */
2
+ function withLatchwayBabel(config = {}) {
3
+ if (config.assumptions?.noClassCalls === false) {
4
+ throw new Error("LangChain on React Native requires assumptions.noClassCalls=true. Remove the conflicting assumption.");
5
+ }
6
+ const namespacePlugin = require.resolve("@babel/plugin-transform-export-namespace-from");
7
+ const plugins = config.plugins ?? [];
8
+ const hasNamespacePlugin = plugins.some((entry) => {
9
+ const plugin = Array.isArray(entry) ? entry[0] : entry;
10
+ return plugin === namespacePlugin || plugin === "@babel/plugin-transform-export-namespace-from";
11
+ });
12
+ return {
13
+ ...config,
14
+ // LangChain's Symbol.hasInstance reads fields initialized by super(). The
15
+ // Babel pre-constructor instanceof assertion runs too early. Classes must
16
+ // still be called with new; LangChain's own type checks remain unchanged.
17
+ assumptions: { ...config.assumptions, noClassCalls: true },
18
+ plugins: hasNamespacePlugin ? [...plugins] : [namespacePlugin, ...plugins],
19
+ };
20
+ }
21
+
22
+ module.exports = { withLatchwayBabel };
package/babel.d.cts ADDED
@@ -0,0 +1,7 @@
1
+ interface BabelConfiguration {
2
+ assumptions?: Record<string, boolean>;
3
+ plugins?: unknown[];
4
+ [key: string]: unknown;
5
+ }
6
+ /** Preserves existing configuration and adds explicit LangChain compatibility. */
7
+ export function withLatchwayBabel(config?: BabelConfiguration): BabelConfiguration;
package/contract.lock ADDED
@@ -0,0 +1,7 @@
1
+ contract_version: 1.0.0
2
+ wire_protocol: 2
3
+ core_release: v1.0.0
4
+ core_commit: d260e3d7485e9e1487b5e03922b79c7089d94ce2
5
+ bundle_sha256: "4866aec1ff70e78d70f07847448161c2b59970fe102d95393b051444536d29a4"
6
+ minimum_server_version: 1.0.0
7
+ maximum_tested_server_version: 1.0.x