@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.
- package/CHANGELOG.md +122 -0
- package/LatchwayReactNative.podspec +33 -0
- package/NOTICE +7 -0
- package/README.md +293 -3
- package/SECURITY.md +69 -0
- package/android/build.gradle.kts +62 -0
- package/android/consumer-rules.pro +2 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +9 -0
- package/android/gradle.properties +4 -0
- package/android/gradlew +251 -0
- package/android/gradlew.bat +94 -0
- package/android/settings.gradle.kts +33 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/dev/latchway/reactnative/LatchwayReactNativePackage.kt +25 -0
- package/android/src/main/java/dev/latchway/reactnative/NativeLatchwayModule.kt +964 -0
- package/android/src/test/java/dev/latchway/reactnative/NativeLatchwayModuleTest.kt +521 -0
- package/babel.cjs +22 -0
- package/babel.d.cts +7 -0
- package/contract.lock +7 -0
- package/docs/architecture.md +145 -0
- package/docs/conformance.md +68 -0
- package/docs/langchain.md +135 -0
- package/docs/native-installation.md +172 -0
- package/docs/physical-device-evidence.md +559 -0
- package/docs/releasing.md +360 -0
- package/docs/security.md +55 -0
- package/ios/LatchwayNativeBridge.swift +1551 -0
- package/ios/RCTNativeLatchway.h +9 -0
- package/ios/RCTNativeLatchway.mm +175 -0
- package/lib/client.d.ts +29 -0
- package/lib/client.d.ts.map +1 -0
- package/lib/client.js +939 -0
- package/lib/client.js.map +1 -0
- package/lib/component-client.d.ts +15 -0
- package/lib/component-client.d.ts.map +1 -0
- package/lib/component-client.js +141 -0
- package/lib/component-client.js.map +1 -0
- package/lib/config.d.ts +25 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +261 -0
- package/lib/config.js.map +1 -0
- package/lib/coordinator.d.ts +19 -0
- package/lib/coordinator.d.ts.map +1 -0
- package/lib/coordinator.js +167 -0
- package/lib/coordinator.js.map +1 -0
- package/lib/errors.d.ts +5 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +201 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -0
- package/lib/native/NativeLatchway.d.ts +25 -0
- package/lib/native/NativeLatchway.d.ts.map +1 -0
- package/lib/native/NativeLatchway.js +3 -0
- package/lib/native/NativeLatchway.js.map +1 -0
- package/lib/native/bridge.d.ts +5 -0
- package/lib/native/bridge.d.ts.map +1 -0
- package/lib/native/bridge.js +17 -0
- package/lib/native/bridge.js.map +1 -0
- package/lib/native-output.d.ts +3 -0
- package/lib/native-output.d.ts.map +1 -0
- package/lib/native-output.js +43 -0
- package/lib/native-output.js.map +1 -0
- package/lib/polyfills.d.ts +8 -0
- package/lib/polyfills.d.ts.map +1 -0
- package/lib/polyfills.js +56 -0
- package/lib/polyfills.js.map +1 -0
- package/lib/request-id.d.ts +2 -0
- package/lib/request-id.d.ts.map +1 -0
- package/lib/request-id.js +5 -0
- package/lib/request-id.js.map +1 -0
- package/lib/runtime-symbols.d.ts +2 -0
- package/lib/runtime-symbols.d.ts.map +1 -0
- package/lib/runtime-symbols.js +9 -0
- package/lib/runtime-symbols.js.map +1 -0
- package/lib/testing.d.ts +7 -0
- package/lib/testing.d.ts.map +1 -0
- package/lib/testing.js +9 -0
- package/lib/testing.js.map +1 -0
- package/lib/types.d.ts +200 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +8 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +8 -0
- package/lib/version.js.map +1 -0
- package/package.json +152 -6
- package/react-native.config.cjs +7 -0
- package/release-compatibility.json +64 -0
- package/src/client.ts +1022 -0
- package/src/component-client.ts +158 -0
- package/src/config.ts +368 -0
- package/src/coordinator.ts +195 -0
- package/src/errors.ts +225 -0
- package/src/index.ts +53 -0
- package/src/native/NativeLatchway.ts +75 -0
- package/src/native/bridge.ts +23 -0
- package/src/native-output.ts +43 -0
- package/src/polyfills.ts +50 -0
- package/src/request-id.ts +5 -0
- package/src/runtime-symbols.ts +9 -0
- package/src/testing.ts +11 -0
- package/src/types.ts +241 -0
- package/src/version.ts +7 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows Keep a Changelog, and releases will follow Semantic
|
|
6
|
+
Versioning once package publication begins.
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.1.0] - 2026-09-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Explicit `@latchway/react-native/polyfills` bootstrap for Hermes async symbols,
|
|
15
|
+
streaming UTF-8 decoding, web streams, URL and abort compatibility. Complete
|
|
16
|
+
existing globals are retained; the ordinary SDK import does not install it.
|
|
17
|
+
- Optional `withLatchwayBabel` configuration helper for LangChain class and
|
|
18
|
+
export-namespace compatibility without depending on LangChain at runtime.
|
|
19
|
+
- Standalone npm-only LatchwayChat with Firebase authentication, temporary
|
|
20
|
+
multi-turn LangChain weather tools, direct fetch, cancellation and diagnostics.
|
|
21
|
+
- Isolated runtime regressions and a LangChain quickstart.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Metro now resolves the published built JavaScript entry without a custom
|
|
26
|
+
`.js`-to-TypeScript resolver. Native Codegen retains its original TS schema.
|
|
27
|
+
|
|
28
|
+
### Compatibility
|
|
29
|
+
|
|
30
|
+
- Shared client, iOS App Attest and Android SDK dependencies remain 1.0.0;
|
|
31
|
+
the native authentication/credential boundary and wire protocol are unchanged.
|
|
32
|
+
- Supported baseline remains React Native 0.82 / New Architecture. Android
|
|
33
|
+
physical Play Integrity evidence is deferred, not inferred from a build.
|
|
34
|
+
|
|
35
|
+
## [1.0.0] - 2026-09-01
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Added the root React Native installation-family component lifecycle:
|
|
40
|
+
descriptor-snapshotted prepare, replace, identity-free diagnostics, revoke,
|
|
41
|
+
and descriptor-bound family retirement, with Android reporting the iOS-only
|
|
42
|
+
operations explicitly unsupported.
|
|
43
|
+
- Added a Debug-only App Intent integration proof that uses an independently
|
|
44
|
+
keyed delegated `react_native_ios` session, fully consumes a Responses request,
|
|
45
|
+
and binds its bounded shared-Keychain receipt to the root's exact-run challenge;
|
|
46
|
+
the Release fixture remains fail-closed with no AppExtensions linkage.
|
|
47
|
+
- Physical React Native evidence now requires the authorization-first HTTP 403
|
|
48
|
+
`component_feature_not_granted` mapping for an ungranted feature instead of
|
|
49
|
+
the feature-enumerating HTTP 404 expectation.
|
|
50
|
+
- Added Metro-runnable OpenAI, Vercel AI OpenAI/Anthropic, and LangChain
|
|
51
|
+
example consumers plus
|
|
52
|
+
a 16-case shared-ID React Native framework conformance matrix covering
|
|
53
|
+
Responses, Chat, embeddings, streaming, tools, structured output, errors,
|
|
54
|
+
retry dispatches, cancellation, and credential isolation, plus a separate
|
|
55
|
+
RN-only explicit-refresh, Anthropic Messages, and opaque-route cases.
|
|
56
|
+
- Exposed the canonical non-secret `gatewayURL` and made `fetchFor` preserve
|
|
57
|
+
Latchway request correlation through the conventional `X-Request-ID` alias
|
|
58
|
+
without buffering response streams.
|
|
59
|
+
- Removed the obsolete React Native CLI `podspecPath` override; React Native
|
|
60
|
+
0.82 now discovers the root podspec without an invalid-config warning.
|
|
61
|
+
- Added iOS and Android production Metro bundles to the normal `pnpm check`
|
|
62
|
+
gate so Node-only framework imports fail before release.
|
|
63
|
+
|
|
64
|
+
- Replaced JavaScript-owned authorization envelopes and network dispatch with
|
|
65
|
+
native URLSession/OkHttp dispatch, opaque response handles, pull-streamed
|
|
66
|
+
response chunks, cancellation, exact origin/path enforcement, and strict
|
|
67
|
+
safe response metadata. The removed `authorize` API is an intentional
|
|
68
|
+
security-boundary break; `fetchFor(feature)` supplies framework adapters
|
|
69
|
+
without exposing reusable credentials.
|
|
70
|
+
- Advanced the exact JavaScript source pin to the reviewed final source commit
|
|
71
|
+
whose protected release evidence requires both Firebase App Check and
|
|
72
|
+
Cloudflare Turnstile.
|
|
73
|
+
- Synchronized the release to contract 1.0.0, current wire protocol 2,
|
|
74
|
+
and the canonical installation-family and component-attestation binding v2
|
|
75
|
+
fixtures while preserving wire 1 in the core compatibility window.
|
|
76
|
+
- Added an extension-process component client for iOS Action and SSO
|
|
77
|
+
extensions. It performs native App Attest step-up without a containing-app
|
|
78
|
+
identity callback and reports composite `delegated_direct_attested` trust;
|
|
79
|
+
Android reports direct component attestation as unsupported.
|
|
80
|
+
|
|
81
|
+
### Candidate baseline
|
|
82
|
+
|
|
83
|
+
#### Changed
|
|
84
|
+
|
|
85
|
+
- Native consumers now pin the reviewed JavaScript, iOS, Android, Kotlin,
|
|
86
|
+
React Native, and core source commits through a machine-verified release
|
|
87
|
+
compatibility lock.
|
|
88
|
+
- The example now includes complete React Native 0.82 New Architecture iOS and
|
|
89
|
+
Android hosts, including source-development overrides that are excluded from
|
|
90
|
+
published metadata.
|
|
91
|
+
- The example provider stack now pins the native-build-verified React Native
|
|
92
|
+
Firebase 25.1.0, Firebase Apple 12.15.0, and Firebase Android BoM 34.15.0 set,
|
|
93
|
+
and repository installs use pnpm's hoisted linker for CocoaPods framework
|
|
94
|
+
compatibility.
|
|
95
|
+
- The version 1 release candidate uses the exact reviewed JavaScript, iOS,
|
|
96
|
+
Android, and core source commits recorded by the synchronized compatibility
|
|
97
|
+
lock.
|
|
98
|
+
- The Firebase example now selects the Firebase identity provider explicitly
|
|
99
|
+
and uses the currently supported OpenAI Responses route and request shape.
|
|
100
|
+
- Automatic bodyless retries now require exact canonical, correlated
|
|
101
|
+
pre-dispatch Problem documents and unambiguous nonce semantics; ambiguous or
|
|
102
|
+
duplicate response metadata fails closed.
|
|
103
|
+
- Provider credential headers are comprehensively stripped and decoded
|
|
104
|
+
credential-like query names fail before identity acquisition or dispatch.
|
|
105
|
+
- Native `operation_indeterminate` failures preserve only a canonical
|
|
106
|
+
reconciliation identifier and reject missing, conflicting, or forbidden
|
|
107
|
+
operation metadata.
|
|
108
|
+
|
|
109
|
+
### Added
|
|
110
|
+
|
|
111
|
+
- Tag-triggered npm trusted publication with provenance, immutable release
|
|
112
|
+
package verification, draft GitHub releases, and published native consumer
|
|
113
|
+
gates.
|
|
114
|
+
- Full contract-bundle verification and clean packed-package consumer gates.
|
|
115
|
+
- Handwritten React Native client API with exact-origin authenticated fetch,
|
|
116
|
+
safe DPoP-nonce/session retries, cancellation, quota, and diagnostics.
|
|
117
|
+
- TurboModule bridges backed by the Latchway iOS and Android SDKs for native
|
|
118
|
+
keys, attestation, DPoP, secure sessions, and installation revocation.
|
|
119
|
+
- Canonical contract-vector tests, native-boundary checks, deterministic
|
|
120
|
+
package verification, CI, security documentation, and an example app.
|
|
121
|
+
- Protocol-specific framework feature bindings; the primary example and
|
|
122
|
+
physical-device path use the setup wizard's OpenAI Responses feature.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |spec|
|
|
6
|
+
spec.name = "LatchwayReactNative"
|
|
7
|
+
spec.version = package["version"]
|
|
8
|
+
spec.summary = package["description"]
|
|
9
|
+
spec.homepage = package["homepage"]
|
|
10
|
+
spec.license = { type: "Apache-2.0", file: "LICENSE" }
|
|
11
|
+
spec.author = { "Latchway Contributors" => "security@latchway.dev" }
|
|
12
|
+
spec.source = { git: "https://github.com/Latchway/latchway-react-native-sdk.git", tag: "v#{spec.version}" }
|
|
13
|
+
spec.platforms = { ios: "15.0" }
|
|
14
|
+
spec.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
15
|
+
# React Native's generated TurboModule spec is a C++ header. It is an
|
|
16
|
+
# implementation detail discovered by class name at runtime and must not be
|
|
17
|
+
# exposed through the framework umbrella module, where Clang may scan it as
|
|
18
|
+
# plain Objective-C and fail to resolve C++ standard-library headers.
|
|
19
|
+
spec.private_header_files = "ios/RCTNativeLatchway.h"
|
|
20
|
+
spec.requires_arc = true
|
|
21
|
+
spec.swift_version = "6.0"
|
|
22
|
+
spec.pod_target_xcconfig = {
|
|
23
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
|
|
24
|
+
"DEFINES_MODULE" => "YES",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
spec.dependency "Latchway/AppAttest", "1.0.0"
|
|
28
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
29
|
+
install_modules_dependencies(spec)
|
|
30
|
+
else
|
|
31
|
+
spec.dependency "React-Core"
|
|
32
|
+
end
|
|
33
|
+
end
|
package/NOTICE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Latchway React Native SDK
|
|
2
|
+
Copyright 2026 Latchway contributors
|
|
3
|
+
|
|
4
|
+
This product includes software developed by Latchway contributors.
|
|
5
|
+
|
|
6
|
+
The Code of Conduct is adapted from Contributor Covenant version 2.1,
|
|
7
|
+
licensed under the Creative Commons Attribution 4.0 International License.
|
package/README.md
CHANGED
|
@@ -1,5 +1,295 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Latchway React Native SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@latchway/react-native` gives iOS and Android applications one fetch-shaped API for a self-hosted Latchway gateway. The JavaScript layer never accepts an upstream AI-provider key. P-256 installation keys, DPoP signing, refresh-token storage, and platform attestation stay in the native Latchway SDKs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Version 1.1.0 adds an optional [LangChain quickstart](docs/langchain.md), runtime
|
|
6
|
+
bootstrap and Babel helper, with standard Metro resolution. Native SDK
|
|
7
|
+
dependencies remain 1.0.0 from CocoaPods and Maven Central.
|
|
8
|
+
|
|
9
|
+
For a standalone npm-only consumer with Firebase login, LangChain weather tools,
|
|
10
|
+
streaming chat and direct-fetch Settings, see
|
|
11
|
+
[LatchwayChat](Examples/LatchwayChat/README.md). The existing `example/` remains
|
|
12
|
+
the separate workspace/conformance application.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- React Native 0.82.x with the New Architecture enabled
|
|
17
|
+
- iOS 15 or newer, an App Attest-capable application entitlement, and `Latchway/AppAttest` 1.0.0
|
|
18
|
+
- Android API 24 or newer, Play Integrity configured for the application, and the `dev.latchway` 1.0.0 artifacts
|
|
19
|
+
- Node 24.19.0 and pnpm 10.15.0 for repository development
|
|
20
|
+
|
|
21
|
+
The repository example additionally pins React Native Firebase 25.1.0, Firebase
|
|
22
|
+
Apple SDK 12.15.0, and Firebase Android BoM 34.15.0. These are example identity
|
|
23
|
+
provider dependencies, not runtime dependencies of the published Latchway
|
|
24
|
+
package. Repository installs use pnpm's hoisted linker because React Native's
|
|
25
|
+
CocoaPods static-framework exclusions require conventional `node_modules`
|
|
26
|
+
paths.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createLatchwayClient } from "@latchway/react-native";
|
|
32
|
+
|
|
33
|
+
const latchway = createLatchwayClient({
|
|
34
|
+
baseURL: "https://gateway.example.com",
|
|
35
|
+
// Generated by the Latchway Admin API; names/slugs are not accepted.
|
|
36
|
+
applicationID: "app_01J00000000000000000000000",
|
|
37
|
+
environment: "production",
|
|
38
|
+
getIdentityToken: async () => {
|
|
39
|
+
const token = await applicationIdentity.currentToken();
|
|
40
|
+
if (token === undefined) throw new Error("The user must sign in before calling Latchway.");
|
|
41
|
+
return token;
|
|
42
|
+
},
|
|
43
|
+
apple: {
|
|
44
|
+
rootKeychainAccessGroup: applicationConfiguration.rootPrivateKeychainAccessGroup,
|
|
45
|
+
legacySharedKeychainAccessGroups: applicationConfiguration.extensionSharedKeychainAccessGroups,
|
|
46
|
+
},
|
|
47
|
+
android: {
|
|
48
|
+
playIntegrityCloudProjectNumber: applicationConfiguration.googleCloudProjectNumber,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const response = await latchway.fetch("/v1/responses", {
|
|
53
|
+
method: "POST",
|
|
54
|
+
latchwayFeature: "habit_assistant",
|
|
55
|
+
headers: { "Content-Type": "application/json" },
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model: "assistant-default",
|
|
58
|
+
input: "Plan tomorrow",
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// OpenAI-compatible clients, Vercel OpenAI/Anthropic providers, and LangChain
|
|
63
|
+
// adapters receive a normal fetch function permanently scoped to one feature.
|
|
64
|
+
const habitAssistantFetch = latchway.fetchFor("habit_assistant");
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`gatewayURL` exposes only the canonical non-secret gateway origin for framework
|
|
68
|
+
constructors that require a base URL. `fetchFor` keeps the feature fixed,
|
|
69
|
+
preserves the pull-driven response stream, and aliases the canonical
|
|
70
|
+
`X-Latchway-Request-ID` as `X-Request-ID` for provider SDK error correlation.
|
|
71
|
+
It never uses or replaces global `fetch`.
|
|
72
|
+
|
|
73
|
+
The example contains runnable consumers for the exact locally tested versions
|
|
74
|
+
of the official OpenAI JavaScript SDK (7.8.0), Vercel AI SDK (7.0.85 with
|
|
75
|
+
`@ai-sdk/openai` 4.0.52 and `@ai-sdk/anthropic` 4.0.46), and LangChain
|
|
76
|
+
JavaScript (`@langchain/openai` 1.5.10):
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import {
|
|
80
|
+
createFrameworkConsumers,
|
|
81
|
+
runFrameworkConsumerSmoke,
|
|
82
|
+
} from "./framework-consumers";
|
|
83
|
+
|
|
84
|
+
const frameworks = createFrameworkConsumers(latchway, {
|
|
85
|
+
responses: "habit_responses",
|
|
86
|
+
chat: "habit_chat",
|
|
87
|
+
embeddings: "habit_embeddings",
|
|
88
|
+
anthropic: "habit_anthropic",
|
|
89
|
+
});
|
|
90
|
+
const result = await runFrameworkConsumerSmoke(frameworks, "Plan tomorrow");
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
These are compatibility consumers, not another LLM abstraction and not a
|
|
94
|
+
blanket support claim for every package feature. They execute Responses, Chat
|
|
95
|
+
Completions, embeddings, Anthropic Messages, and SSE through native networking.
|
|
96
|
+
Each identifier above must name a separately configured Latchway feature whose
|
|
97
|
+
protocol matches that request family; one feature cannot represent multiple
|
|
98
|
+
protocols.
|
|
99
|
+
Local conformance also covers tools, JSON-schema request preservation, errors,
|
|
100
|
+
framework retry dispatches, explicit refresh, restricted opaque routes, and
|
|
101
|
+
cancellation. Automatic pre-dispatch session recovery remains a native/device
|
|
102
|
+
evidence claim. Audio, images, uploads, Realtime, browser-only middleware,
|
|
103
|
+
streaming request bodies, and any framework path that does not honor the
|
|
104
|
+
injected fetch remain unsupported here. The core
|
|
105
|
+
`compatibility/frameworks.yaml` registry is the canonical release-status and
|
|
106
|
+
version source; `react-native-fetch` remains experimental until hosted and
|
|
107
|
+
physical-device gates pass.
|
|
108
|
+
|
|
109
|
+
The official `@anthropic-ai/sdk` 0.120.0 package is not claimed as a React
|
|
110
|
+
Native consumer. Its credential-chain module contains Node filesystem imports
|
|
111
|
+
that Metro resolves even when a static constructor placeholder is supplied.
|
|
112
|
+
The runnable Anthropic Messages path therefore uses the custom-fetch seam in
|
|
113
|
+
`@ai-sdk/anthropic`; no Node-module shim or provider credential is bundled.
|
|
114
|
+
|
|
115
|
+
`applicationID` is the generated application resource ID returned by the
|
|
116
|
+
Admin API, not an app name, package/bundle identifier, or user-chosen slug.
|
|
117
|
+
On iOS, `apple.rootKeychainAccessGroup` is required, fully resolved, and must
|
|
118
|
+
be the first access group in the signed root application. Put every explicit
|
|
119
|
+
extension-shared group in `legacySharedKeychainAccessGroups`; native code scans
|
|
120
|
+
only exact root-record coordinates and surfaces `storage_unavailable` if stale
|
|
121
|
+
implicit root state requires an explicit migration.
|
|
122
|
+
|
|
123
|
+
Call `dispose()` when the owning application scope is destroyed. Disposal drops the in-memory native client; secure installation state remains available to later instances. `refresh()` explicitly rotates session credentials without exposing them. `revokeCurrentInstallation()` removes only the root installation and leaves independently provisioned family components addressable.
|
|
124
|
+
|
|
125
|
+
The root application manages the complete native iOS component lifecycle with
|
|
126
|
+
public descriptors:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import type { ReactNativeIOSComponent } from "@latchway/react-native";
|
|
130
|
+
|
|
131
|
+
const appIntent: ReactNativeIOSComponent = {
|
|
132
|
+
definitionID: "habit_app_intent",
|
|
133
|
+
kind: "app_intent_extension",
|
|
134
|
+
keychainAccessGroup: "ABCDE12345.com.example.app.app-intent",
|
|
135
|
+
requestedFeatures: ["habit_assistant"],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const [prepared] = await latchway.prepareComponents([appIntent]);
|
|
139
|
+
const replaced = await latchway.replaceComponent(appIntent);
|
|
140
|
+
const localState = await latchway.componentDiagnostics(appIntent);
|
|
141
|
+
await latchway.revokeComponent(appIntent);
|
|
142
|
+
|
|
143
|
+
// On sign-out, native code retires every durably registered component.
|
|
144
|
+
await latchway.revokeCurrentInstallationFamily();
|
|
145
|
+
|
|
146
|
+
// The descriptor overload remains available for pre-registry legacy state.
|
|
147
|
+
await latchway.revokeCurrentInstallationFamily([appIntent]);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Each descriptor contains exactly a definition ID, kind, fully resolved Keychain
|
|
151
|
+
access group, and requested feature IDs. Its access group must be signed into
|
|
152
|
+
both targets and listed in `apple.legacySharedKeychainAccessGroups`.
|
|
153
|
+
Preparation, replacement, component revocation, and family retirement acquire
|
|
154
|
+
the root identity only transiently inside native code. Root-side
|
|
155
|
+
`componentDiagnostics` reads redacted local state without acquiring identity.
|
|
156
|
+
Inputs are normalized before asynchronous work, and all component credentials,
|
|
157
|
+
keys, grants, and sessions remain native. Before component-local state can be
|
|
158
|
+
created, the iOS SDK records its validated, non-secret Keychain coordinate in
|
|
159
|
+
the root application's private Keychain group. No-argument family retirement
|
|
160
|
+
uses that durable registry across launches, removes entries only after both the
|
|
161
|
+
component credential and key are erased, and retains failed entries for retry.
|
|
162
|
+
The optional descriptor overload can additionally retire legacy component
|
|
163
|
+
state that predates the registry.
|
|
164
|
+
|
|
165
|
+
An independently executing iOS action or SSO extension whose containing app
|
|
166
|
+
has already provisioned its component descriptor can inspect its isolated,
|
|
167
|
+
independently keyed delegated-session state without moving credentials through
|
|
168
|
+
JavaScript:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { createLatchwayComponentClient } from "@latchway/react-native";
|
|
172
|
+
|
|
173
|
+
const component = {
|
|
174
|
+
definitionID: "action_extension",
|
|
175
|
+
kind: "action_extension",
|
|
176
|
+
keychainAccessGroup: "ABCDE12345.com.example.app.action-extension",
|
|
177
|
+
requestedFeatures: ["habit_assistant"],
|
|
178
|
+
} as const;
|
|
179
|
+
|
|
180
|
+
// Construct this only in the JavaScript runtime hosted by the signed .appex.
|
|
181
|
+
// It is deliberately separate from the containing app's root client.
|
|
182
|
+
const componentClient = createLatchwayComponentClient({
|
|
183
|
+
baseURL: "https://gateway.example.com",
|
|
184
|
+
applicationID: "app_01J00000000000000000000000",
|
|
185
|
+
environment: "production",
|
|
186
|
+
component,
|
|
187
|
+
apple: {
|
|
188
|
+
rootKeychainAccessGroup: "ABCDE12345.com.example.app",
|
|
189
|
+
legacySharedKeychainAccessGroups: [component.keychainAccessGroup],
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const componentState = await componentClient.diagnostics();
|
|
194
|
+
// This is diagnostics only; RN v1 exposes no component request operation.
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The access group must be fully resolved and present in both signed entitlement
|
|
198
|
+
sets; build-setting expressions such as `$(AppIdentifierPrefix)` are rejected.
|
|
199
|
+
Configuration fails unless the current process is an iOS `.appex`. The root
|
|
200
|
+
application may establish App Attest only for itself; it must not attest for an
|
|
201
|
+
extension, and an iOS application extension cannot call
|
|
202
|
+
`DCAppAttestService.generateKey`. The extension client therefore constructs no
|
|
203
|
+
App Attest provider. It has no identity callback or root API and retains only
|
|
204
|
+
component-key and delegated-session isolation inside the pinned iOS SDK. The
|
|
205
|
+
legacy `establishDirectAttestation()` entry point and direct-attestation trust
|
|
206
|
+
source decoders remain for wire/API compatibility, but invocation fails closed
|
|
207
|
+
with `attestation_unsupported`; their presence is not a claim that iOS can
|
|
208
|
+
produce that state. Only redacted component diagnostics return. Android also
|
|
209
|
+
fails closed for direct component attestation.
|
|
210
|
+
|
|
211
|
+
The example's `AppIntents.appex` does not host a React Native JavaScript runtime.
|
|
212
|
+
In Debug only, its separate CocoaPods target links `Latchway/AppExtensions`
|
|
213
|
+
(Swift module `Latchway`) and constructs a native `LatchwayExtensionClient` with
|
|
214
|
+
runtime `react_native_ios`. After the root prepares the descriptor, the intent
|
|
215
|
+
publishes a nonsecret exact-run challenge in the shared Keychain immediately
|
|
216
|
+
before its waiting marker. The intent captures that challenge before creating
|
|
217
|
+
its client, proves an independently keyed delegated session, fully consumes one
|
|
218
|
+
successful bounded Responses body, rechecks that the challenge is still current,
|
|
219
|
+
and then echoes it in a bounded shared-Keychain receipt. Resume accepts only the
|
|
220
|
+
native-captured exact run and deletes both challenge and receipt. The receipt
|
|
221
|
+
contains only the nonsecret `dev_<32hex>` run nonce, status booleans, and a
|
|
222
|
+
timestamp; it contains no component, installation, or user IDs, tokens, proofs,
|
|
223
|
+
request body, or response body. The intent never receives the root identity or
|
|
224
|
+
root-private Keychain state.
|
|
225
|
+
|
|
226
|
+
The Release fixture has no AppExtensions dependency or executable Latchway
|
|
227
|
+
client path and its intent fails closed with an unsupported error. Both variants
|
|
228
|
+
retain private-first/shared-second root entitlements and a shared-only extension
|
|
229
|
+
entitlement. The local Debug intent is integration proof only and does not
|
|
230
|
+
broaden or replace the protected Release physical-evidence claim.
|
|
231
|
+
|
|
232
|
+
Equivalent clients in one JavaScript runtime share one native client and contract compatibility check. Native SDK actors/mutexes own session establishment and refresh single-flight. JavaScript never clones or replays an authenticated request; any bounded pre-dispatch retry is exclusively a native transport decision. Android installs the locked Latchway OkHttp interceptor, origin guard, and authenticator. iOS uses the locked feature transport, whose private redirect-rejecting URL session classifies at most 64 KiB of one canonical pre-dispatch rejection before its single safe retry.
|
|
233
|
+
|
|
234
|
+
The fetch surface is intentionally bounded. It dispatches only to the configured origin. The structured paths `/v1/responses`, `/v1/chat/completions`, `/v1/embeddings`, and `/v1/messages` are POST-only. Opaque integrations may use GET, POST, PUT, PATCH, or DELETE only below `/proxy/{feature}/<safe-relative-path>` with an exact feature match and no query; empty segments, traversal, encoded separators, backslashes, and absolute-URL-shaped suffixes fail closed. Fragments and credential-shaped query names fail before identity acquisition. Request bodies are buffered to cross the New Architecture bridge and are limited to 8 MiB. Response bodies remain pull-streamed in chunks with cancellation and backpressure, while native retains the network task and credential-bearing request. Redirects are refused, and JavaScript receives only status, a strict safe-header allowlist, an opaque response handle, and response bytes.
|
|
235
|
+
|
|
236
|
+
The compatible subset guarantees request method, headers, body, `AbortSignal`, response status/headers, and a pull-driven `ReadableStream`. It does not provide browser cookie or cache modes, service-worker behavior, redirect following, streaming request uploads, response trailers, or native `Response.url`/`redirected` metadata. Frameworks must expose a custom-fetch hook and use one of the allowed data-plane paths; browser-only framework features remain unsupported in React Native. A provider SDK's required placeholder authorization header is discarded, never forwarded. Consult the released framework compatibility registry before claiming support for a specific OpenAI, Vercel AI, or LangChain version.
|
|
237
|
+
|
|
238
|
+
`errorFromResponse` is re-exported for explicit conversion of a returned problem response. Every resulting `LatchwayError.documentationURL` uses `https://docs.latchway.dev/errors/<hyphenated-code>`, and a server-originated native failure is accepted only when its documentation URL matches its code exactly. An `operation_indeterminate` error includes a required canonical `operationID`; preserve it with the request ID and reconcile the operation before deciding whether to retry.
|
|
239
|
+
|
|
240
|
+
## Security boundary
|
|
241
|
+
|
|
242
|
+
The root client's only application credential sent into the TurboModule is the external identity JWT returned by `getIdentityToken`, and it is retained only for the duration of a native operation that requires identity. Root component descriptors and extension-client descriptors contain only definition ID, kind, fully resolved Keychain access group, and requested feature IDs. Root-side and extension-side component diagnostics do not acquire identity. The separate component client cannot acquire the containing app's native root lease. The bridge does not accept App Attest objects, Play Integrity tokens, `client_data_hash`, request hashes, DPoP private keys, access tokens, or refresh tokens as inputs. Native code attaches Authorization and DPoP, sends the request, and owns the response stream. Authorization, DPoP, access tokens, refresh tokens, private keys, attestation evidence, and reusable credentials never return to JavaScript.
|
|
243
|
+
|
|
244
|
+
Caller-supplied `Authorization`, `DPoP`, cookies, API-key headers, transport-owned headers, and Latchway protocol headers are removed before the request crosses the bridge. This permits SDKs that require a placeholder API-key option without forwarding that placeholder or a real provider credential. Provider-credential query names are rejected, including percent-encoded and case-varied names. Insecure HTTP is limited to explicitly enabled loopback conformance.
|
|
245
|
+
|
|
246
|
+
See [native installation](docs/native-installation.md), [security details](docs/security.md), and [architecture](docs/architecture.md).
|
|
247
|
+
Release ordering and immutable publication gates are in [releasing](docs/releasing.md).
|
|
248
|
+
|
|
249
|
+
## Development
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
pnpm install --frozen-lockfile
|
|
253
|
+
pnpm check
|
|
254
|
+
pnpm verify:compatibility
|
|
255
|
+
pnpm pack:check
|
|
256
|
+
pnpm verify:reproducible
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
For a release-candidate checkout, `pnpm verify:compatibility --sources` also
|
|
260
|
+
requires the exact core, JavaScript, iOS, and Android commits recorded in
|
|
261
|
+
`release-compatibility.json`. `pnpm verify:bundle -- /path/to/latchway-contract-<version>.tar.gz`
|
|
262
|
+
verifies the complete immutable contract archive, and `pnpm consumer:check`
|
|
263
|
+
installs the packed JavaScript and React Native archives in a clean temporary
|
|
264
|
+
consumer before compiling it.
|
|
265
|
+
|
|
266
|
+
`pnpm codegen:check` parses the handwritten TurboModule spec and regenerates both platform surfaces in a disposable directory. Node tests use the explicit `@latchway/react-native/testing` bridge; production applications must never install a test bridge.
|
|
267
|
+
|
|
268
|
+
`pnpm example:bundle:check` creates production Metro bundles for both iOS and
|
|
269
|
+
Android in a disposable directory. It is part of `pnpm check` so framework
|
|
270
|
+
packages that typecheck but import Node-only modules fail the normal source
|
|
271
|
+
gate.
|
|
272
|
+
|
|
273
|
+
The example in [`example`](example/README.md) demonstrates Firebase Authentication, environment-supplied deployment configuration, raw fetch, real framework consumers, quota, diagnostics, and lifecycle cleanup without storing or logging credentials.
|
|
274
|
+
|
|
275
|
+
## Contract lock
|
|
276
|
+
|
|
277
|
+
The final version 1 source candidate consumes released contract checkpoint `1.0.0`,
|
|
278
|
+
current wire protocol `2` (with wire `1` retained in the core compatibility
|
|
279
|
+
window), core commit `d260e3d7485e9e1487b5e03922b79c7089d94ce2`, and
|
|
280
|
+
bundle SHA-256
|
|
281
|
+
`4866aec1ff70e78d70f07847448161c2b59970fe102d95393b051444536d29a4`.
|
|
282
|
+
Core plus all four SDK locks and fixtures are synchronized. This is source
|
|
283
|
+
compatibility evidence, not a claim that the npm package or native dependencies
|
|
284
|
+
have been published. All gates read `release-compatibility.json` and
|
|
285
|
+
`contract.lock`, so later compatible releases do not require rewriting CI.
|
|
286
|
+
`pnpm verify:contracts` checks the active lock and vendored canonical fixtures
|
|
287
|
+
byte-for-byte, including the installation-family and component-attestation
|
|
288
|
+
binding v2 fixtures. The
|
|
289
|
+
promotion-dispatched release workflow still refuses
|
|
290
|
+
publication until exact native registry, provenance, physical-device, and
|
|
291
|
+
immutable-release evidence passes.
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Release status
|
|
4
|
+
|
|
5
|
+
Latchway React Native SDK is implemented but remains pre-release. Do not depend
|
|
6
|
+
on an unpublished checkout as a supported production security boundary; use a
|
|
7
|
+
published version whose native compatibility entry is recorded.
|
|
8
|
+
|
|
9
|
+
Security fixes will target supported releases once the support matrix is
|
|
10
|
+
established. The unreleased branch may change without compatibility guarantees.
|
|
11
|
+
|
|
12
|
+
## Reporting a vulnerability
|
|
13
|
+
|
|
14
|
+
Use GitHub private vulnerability reporting for this repository. If that feature
|
|
15
|
+
is unavailable, contact the Latchway organization maintainers privately before
|
|
16
|
+
sharing details. Do not open a public issue for a suspected vulnerability.
|
|
17
|
+
|
|
18
|
+
Do not include live identity tokens, refresh tokens, DPoP proofs, attestation
|
|
19
|
+
evidence, private keys, signing assets, provider credentials, or user data in a
|
|
20
|
+
report. Revoke exposed credentials before continuing.
|
|
21
|
+
|
|
22
|
+
A useful report includes:
|
|
23
|
+
|
|
24
|
+
- The affected revision or released version
|
|
25
|
+
- The React Native, iOS or Android, and device versions
|
|
26
|
+
- Reproduction steps using synthetic or redacted data
|
|
27
|
+
- The expected and observed security behavior
|
|
28
|
+
- Impact, prerequisites, and any known mitigations
|
|
29
|
+
|
|
30
|
+
## Security-sensitive scope
|
|
31
|
+
|
|
32
|
+
Treat changes to TurboModule boundaries, native-to-JavaScript data exposure,
|
|
33
|
+
installation state, native SDK version resolution, request retry/replay
|
|
34
|
+
behavior, token redaction, and diagnostics as security-sensitive. Hardware keys,
|
|
35
|
+
attestation, DPoP signing, refresh-token storage, and installation state belong
|
|
36
|
+
to the native SDKs and must not be reimplemented in JavaScript or C++ here.
|
|
37
|
+
|
|
38
|
+
The application-owned external identity token crosses the TurboModule boundary
|
|
39
|
+
only for the duration of a native operation. Private keys, refresh tokens, raw
|
|
40
|
+
attestation evidence, attestation challenge bindings, and native session state
|
|
41
|
+
must never enter JavaScript. Access tokens and DPoP proofs also remain native:
|
|
42
|
+
the iOS URL session or Android OkHttp client attaches them, dispatches the
|
|
43
|
+
exact-origin request, rejects redirects, and retains the response handle.
|
|
44
|
+
JavaScript receives only safe response metadata and pull-streamed response
|
|
45
|
+
bytes. There is no public or TurboModule authorization-envelope API.
|
|
46
|
+
|
|
47
|
+
The SDK must never accept an upstream AI-provider secret. Latchway server or
|
|
48
|
+
native-SDK vulnerabilities should be reported to their owning repository, with
|
|
49
|
+
a cross-reference here when bridge behavior is involved.
|
|
50
|
+
|
|
51
|
+
## Dependency vulnerability gate
|
|
52
|
+
|
|
53
|
+
CI and release jobs bind `package.json`, `pnpm-lock.yaml`, and the scanner policy
|
|
54
|
+
scripts to the exact candidate commit before scanning. They install the
|
|
55
|
+
checksum-and-size-pinned OSV-Scanner 2.4.0 binary, download the public npm
|
|
56
|
+
advisory database, and then match dependencies locally with network resolution
|
|
57
|
+
disabled. No package inventory or repository path is sent to OSV.dev. Native
|
|
58
|
+
SDK dependency graphs are scanned in their owning iOS and Android repositories.
|
|
59
|
+
|
|
60
|
+
`pnpm security:dependencies` reproduces the gate. Critical, high, or
|
|
61
|
+
unknown-severity findings fail the candidate; lower-severity findings remain
|
|
62
|
+
visible for routine remediation. Scanner errors, malformed output, an empty
|
|
63
|
+
inventory, or a missing offline database also fail closed.
|
|
64
|
+
|
|
65
|
+
## Disclosure
|
|
66
|
+
|
|
67
|
+
Allow maintainers a reasonable opportunity to investigate and coordinate a fix
|
|
68
|
+
before public disclosure. Good-faith research that avoids privacy violations,
|
|
69
|
+
service disruption, and access beyond what is necessary is welcome.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
2
|
+
|
|
3
|
+
plugins {
|
|
4
|
+
id("com.android.library")
|
|
5
|
+
id("org.jetbrains.kotlin.android")
|
|
6
|
+
id("com.facebook.react")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
kotlin {
|
|
10
|
+
compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
group = "dev.latchway"
|
|
14
|
+
version = "1.0.0"
|
|
15
|
+
|
|
16
|
+
react {
|
|
17
|
+
root.set(file(".."))
|
|
18
|
+
reactNativeDir.set(file("../node_modules/react-native"))
|
|
19
|
+
codegenDir.set(file("../node_modules/@react-native/codegen"))
|
|
20
|
+
libraryName.set("LatchwayReactNativeSpec")
|
|
21
|
+
codegenJavaPackageName.set("dev.latchway.reactnative")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
android {
|
|
25
|
+
namespace = "dev.latchway.reactnative"
|
|
26
|
+
// The exact native 1.0.0 AAR metadata requires API 37 consumers.
|
|
27
|
+
compileSdk = 37
|
|
28
|
+
|
|
29
|
+
defaultConfig {
|
|
30
|
+
minSdk = 24
|
|
31
|
+
consumerProguardFiles("consumer-rules.pro")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
compileOptions {
|
|
35
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
36
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
lint {
|
|
40
|
+
abortOnError = true
|
|
41
|
+
warningsAsErrors = true
|
|
42
|
+
// These versions are compatibility inputs, not opportunistic upgrades.
|
|
43
|
+
// scripts/verify-compatibility.mjs enforces the reviewed exact lock.
|
|
44
|
+
disable += setOf("AndroidGradlePluginVersion", "NewerVersionAvailable")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
testOptions {
|
|
48
|
+
unitTests.isIncludeAndroidResources = true
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dependencies {
|
|
53
|
+
// A real host's React plugin forces this to the installed 0.82.x patch. The
|
|
54
|
+
// exact baseline keeps standalone package-consumer builds deterministic.
|
|
55
|
+
implementation("com.facebook.react:react-android:0.82.0")
|
|
56
|
+
implementation("dev.latchway:latchway-okhttp:1.0.0")
|
|
57
|
+
implementation("dev.latchway:latchway-play-integrity:1.0.0")
|
|
58
|
+
|
|
59
|
+
testImplementation("junit:junit:4.13.2")
|
|
60
|
+
testImplementation("org.robolectric:robolectric:4.14.1")
|
|
61
|
+
testImplementation("com.squareup.okhttp3:mockwebserver:5.3.0")
|
|
62
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
distributionBase=GRADLE_USER_HOME
|
|
2
|
+
distributionPath=wrapper/dists
|
|
3
|
+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
|
4
|
+
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
|
|
5
|
+
networkTimeout=10000
|
|
6
|
+
retries=0
|
|
7
|
+
validateDistributionUrl=true
|
|
8
|
+
zipStoreBase=GRADLE_USER_HOME
|
|
9
|
+
zipStorePath=wrapper/dists
|