ping-openmls-sdk-react-native-macos 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ // Bridge that implements UniFFI's `MessageObserver` protocol by emitting RN events. The
2
+ // Rust core invokes `onApplicationMessage` / `onConversationUpdated` synchronously from
3
+ // its async tasks; we just forward to JS via the event emitter — no continuations needed
4
+ // because the Rust side doesn't wait for a return value.
5
+ //
6
+ // Same direction as Storage/Transport (Rust → JS) but simpler: fire-and-forget instead
7
+ // of round-trip.
8
+ //
9
+ // Spec: docs/RN_NATIVE_BINDINGS.md (stage 4e).
10
+
11
+ import Foundation
12
+
13
+ public typealias ObserverEventEmitter = (_ name: String, _ body: [String: Any]) -> Void
14
+
15
+ public final class JSObserverBridge: MessageObserver, @unchecked Sendable {
16
+
17
+ private let emit: ObserverEventEmitter
18
+
19
+ public init(emit: @escaping ObserverEventEmitter) {
20
+ self.emit = emit
21
+ }
22
+
23
+ // MARK: - UniFFI MessageObserver protocol conformance
24
+
25
+ public func onApplicationMessage(msg: IncomingMessage) {
26
+ emit("PingApplicationMessage", TypeBridge.encodeIncomingMessage(msg))
27
+ }
28
+
29
+ public func onConversationUpdated(id: ConversationId, newEpoch: UInt64) {
30
+ emit("PingConversationUpdated", [
31
+ "conversation_id": TypeBridge.encodeConversationId(id),
32
+ "new_epoch": Int(newEpoch),
33
+ ])
34
+ }
35
+ }
@@ -0,0 +1,135 @@
1
+ // Bridge that implements UniFFI's `Storage` protocol by marshalling each call across
2
+ // the Swift→JS→Swift boundary via RN's RCTEventEmitter. The Rust core (via UniFFI)
3
+ // invokes our methods asynchronously; we suspend on a CheckedContinuation, fire an
4
+ // event to JS, and the JS-side handler calls `PingNative.resolveStorageCall(id, value)`
5
+ // to wake us up.
6
+ //
7
+ // Stage 4a wires this class to the actual UniFFI `Storage` protocol — it can now be
8
+ // passed to `MessagingClient.init(storage: ...)` once stage 4b lands.
9
+ //
10
+ // Spec: docs/RN_NATIVE_BINDINGS.md (stage 4a).
11
+
12
+ import Foundation
13
+
14
+ /// Callback emitted by the bridge when it needs JS to handle a storage call. The
15
+ /// implementation is expected to dispatch an RN event with `body = {id, method, args}`.
16
+ public typealias StorageEventEmitter = (_ id: String, _ method: String, _ args: [String: Any]) -> Void
17
+
18
+ /// JS-backed implementation of UniFFI's `Storage` protocol. Each call is a JS event
19
+ /// round-trip; the Swift-side continuation is resumed from `PingNative` when the JS
20
+ /// handler reports back.
21
+ public final class JSStorageBridge: Storage, @unchecked Sendable {
22
+
23
+ private let emit: StorageEventEmitter
24
+
25
+ /// Pending Swift continuations keyed by call id. Mutations guarded by `lock`.
26
+ private var pending: [String: PendingCall] = [:]
27
+ private let lock = NSLock()
28
+
29
+ public init(emit: @escaping StorageEventEmitter) {
30
+ self.emit = emit
31
+ }
32
+
33
+ // MARK: - UniFFI Storage protocol conformance
34
+
35
+ public func get(namespace: String, key: String) async throws -> Data? {
36
+ return try await call(method: "get",
37
+ args: ["namespace": namespace, "key": key],
38
+ decode: { TypeBridge.decodeBytes($0) })
39
+ }
40
+
41
+ public func put(namespace: String, key: String, value: Data) async throws {
42
+ let _: Bool = try await call(method: "put",
43
+ args: ["namespace": namespace,
44
+ "key": key,
45
+ "value": TypeBridge.encodeBytes(value)],
46
+ decode: { _ in true })
47
+ }
48
+
49
+ public func delete(namespace: String, key: String) async throws {
50
+ let _: Bool = try await call(method: "delete",
51
+ args: ["namespace": namespace, "key": key],
52
+ decode: { _ in true })
53
+ }
54
+
55
+ public func listKeys(namespace: String, prefix: String) async throws -> [String] {
56
+ return try await call(method: "listKeys",
57
+ args: ["namespace": namespace, "prefix": prefix],
58
+ decode: { value in (value as? [String]) ?? [] })
59
+ }
60
+
61
+ // MARK: - JS → Swift resumption hooks (called from PingNative)
62
+
63
+ public func resolve(id: String, value: Any?) {
64
+ lock.lock()
65
+ let call = pending.removeValue(forKey: id)
66
+ lock.unlock()
67
+ call?.resume(.success(value as Any))
68
+ }
69
+
70
+ public func reject(id: String, message: String) {
71
+ lock.lock()
72
+ let call = pending.removeValue(forKey: id)
73
+ lock.unlock()
74
+ // Rust expects a `PingError` for storage failures. UniFFI 0.28 generates flat
75
+ // error variants with a labeled `message: String` associated value; we forward
76
+ // the JS-side message verbatim so it shows up in Rust-side logs.
77
+ call?.resume(.failure(PingError.Storage(message: message)))
78
+ }
79
+
80
+ // MARK: - Internals
81
+
82
+ private func call<T>(method: String,
83
+ args: [String: Any],
84
+ decode: @escaping (Any?) -> T) async throws -> T {
85
+ let id = UUID().uuidString
86
+ return try await withCheckedThrowingContinuation { continuation in
87
+ lock.lock()
88
+ pending[id] = PendingCall(continuation: ContinuationBox(decode: decode,
89
+ inner: continuation))
90
+ lock.unlock()
91
+ // Fire the event AFTER storing the continuation so a synchronously-resolving
92
+ // JS handler (unlikely but possible) doesn't race us.
93
+ emit(id, method, args)
94
+ }
95
+ }
96
+
97
+ /// Erased pending call — the bridge must hold heterogeneously-typed continuations.
98
+ private struct PendingCall {
99
+ let continuation: AnyContinuation
100
+ func resume(_ result: Result<Any?, Error>) {
101
+ continuation.resume(result)
102
+ }
103
+ }
104
+
105
+ private protocol AnyContinuation {
106
+ func resume(_ result: Result<Any?, Error>)
107
+ }
108
+
109
+ private struct ContinuationBox<T>: AnyContinuation {
110
+ let decode: (Any?) -> T
111
+ let inner: CheckedContinuation<T, Error>
112
+
113
+ func resume(_ result: Result<Any?, Error>) {
114
+ switch result {
115
+ case .success(let value):
116
+ inner.resume(returning: decode(value))
117
+ case .failure(let error):
118
+ inner.resume(throwing: error)
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ /// Bridge-internal errors. Rust expects `PingError` from Storage/Transport methods;
125
+ /// `BridgeError` is used for cases the bridge itself produces (decode failures) before
126
+ /// converting to `PingError.Codec`.
127
+ public enum BridgeError: Error, CustomStringConvertible {
128
+ case decodeFailure(String)
129
+
130
+ public var description: String {
131
+ switch self {
132
+ case .decodeFailure(let m): return "bridge decode failure: \(m)"
133
+ }
134
+ }
135
+ }
@@ -0,0 +1,126 @@
1
+ // Bridge that implements UniFFI's `Transport` protocol by marshalling each call across
2
+ // the Swift→JS→Swift boundary. Same pattern as `JSStorageBridge`; isolated in its own
3
+ // class so the two bridges' pending-call tables don't collide on UUIDs.
4
+ //
5
+ // Stage 4a wires this to the real UniFFI `Transport` protocol. Stage 4b will pass an
6
+ // instance to `MessagingClient.init(transport: ...)`.
7
+ //
8
+ // Spec: docs/RN_NATIVE_BINDINGS.md (stage 4a).
9
+
10
+ import Foundation
11
+
12
+ public typealias TransportEventEmitter = (_ id: String, _ method: String, _ args: [String: Any]) -> Void
13
+
14
+ /// JS-backed implementation of UniFFI's `Transport` protocol.
15
+ public final class JSTransportBridge: Transport, @unchecked Sendable {
16
+
17
+ private let emit: TransportEventEmitter
18
+
19
+ private var pending: [String: PendingCall] = [:]
20
+ private let lock = NSLock()
21
+
22
+ public init(emit: @escaping TransportEventEmitter) {
23
+ self.emit = emit
24
+ }
25
+
26
+ // MARK: - UniFFI Transport protocol conformance
27
+
28
+ public func send(envelope: MessageEnvelope) async throws {
29
+ let _: Bool = try await call(method: "send",
30
+ args: ["envelope": TypeBridge.encodeEnvelope(envelope)],
31
+ decode: { _ in true })
32
+ }
33
+
34
+ public func fetchSince(conversationId: ConversationId,
35
+ cursorToken: Data,
36
+ limit: UInt32) async throws -> [MessageEnvelope] {
37
+ // Args match the JS-side `Transport.fetchSince(conversationIdHex, cursorBase64, limit)`
38
+ // signature exactly — hex + base64 strings are smaller across the bridge than byte
39
+ // arrays and align with what the relay's `/sync` endpoint already speaks.
40
+ let convHex = conversationId.value.map { String(format: "%02x", $0) }.joined()
41
+ let cursorB64 = cursorToken.base64EncodedString()
42
+ return try await call(method: "fetchSince",
43
+ args: [
44
+ "conversationIdHex": convHex,
45
+ "cursorBase64": cursorB64,
46
+ "limit": Int(limit),
47
+ ],
48
+ decode: { value in
49
+ // Best-effort decode; if JS returns malformed envelopes we return empty
50
+ // and let the SDK retry rather than throwing (resync is cheaper than crash).
51
+ guard let arr = value as? [[String: Any]] else { return [] }
52
+ return arr.compactMap { dict in
53
+ try? TypeBridge.decodeEnvelope(dict)
54
+ }
55
+ })
56
+ }
57
+
58
+ public func discoverDevices(userId: UserId) async throws -> [DiscoveredDevice] {
59
+ let userHex = userId.value.map { String(format: "%02x", $0) }.joined()
60
+ return try await call(method: "discoverDevices",
61
+ args: ["userIdHex": userHex],
62
+ decode: { value in
63
+ guard let arr = value as? [[String: Any]] else { return [] }
64
+ return arr.compactMap { dict in
65
+ try? TypeBridge.decodeDiscoveredDevice(dict)
66
+ }
67
+ })
68
+ }
69
+
70
+ // MARK: - JS → Swift resumption hooks
71
+
72
+ public func resolve(id: String, value: Any?) {
73
+ lock.lock()
74
+ let call = pending.removeValue(forKey: id)
75
+ lock.unlock()
76
+ call?.resume(.success(value as Any))
77
+ }
78
+
79
+ public func reject(id: String, message: String) {
80
+ lock.lock()
81
+ let call = pending.removeValue(forKey: id)
82
+ lock.unlock()
83
+ // See JSStorageBridge.reject — UniFFI 0.28 cases carry a labeled `message: String`.
84
+ call?.resume(.failure(PingError.Transport(message: message)))
85
+ }
86
+
87
+ // MARK: - Internals (mirror of JSStorageBridge — kept inlined for clarity over DRY)
88
+
89
+ private func call<T>(method: String,
90
+ args: [String: Any],
91
+ decode: @escaping (Any?) -> T) async throws -> T {
92
+ let id = UUID().uuidString
93
+ return try await withCheckedThrowingContinuation { continuation in
94
+ lock.lock()
95
+ pending[id] = PendingCall(continuation: ContinuationBox(decode: decode,
96
+ inner: continuation))
97
+ lock.unlock()
98
+ emit(id, method, args)
99
+ }
100
+ }
101
+
102
+ private struct PendingCall {
103
+ let continuation: AnyContinuation
104
+ func resume(_ result: Result<Any?, Error>) {
105
+ continuation.resume(result)
106
+ }
107
+ }
108
+
109
+ private protocol AnyContinuation {
110
+ func resume(_ result: Result<Any?, Error>)
111
+ }
112
+
113
+ private struct ContinuationBox<T>: AnyContinuation {
114
+ let decode: (Any?) -> T
115
+ let inner: CheckedContinuation<T, Error>
116
+
117
+ func resume(_ result: Result<Any?, Error>) {
118
+ switch result {
119
+ case .success(let value):
120
+ inner.resume(returning: decode(value))
121
+ case .failure(let error):
122
+ inner.resume(throwing: error)
123
+ }
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,143 @@
1
+ // Objective-C bridge that registers the `PingNative` Swift class with React Native's
2
+ // module dispatcher. Required because RCT_EXTERN_MODULE / RCT_EXTERN_METHOD are
3
+ // Objective-C macros — the Swift side stays pure Swift via @objc declarations.
4
+ //
5
+ // Adding a new method: declare it with RCT_EXTERN_METHOD here AND with @objc
6
+ // (`@objc(name:resolver:rejecter:)`) on the Swift side.
7
+ //
8
+ // Spec: docs/RN_NATIVE_BINDINGS.md (stage 2).
9
+
10
+ #import <React/RCTBridgeModule.h>
11
+ #import <React/RCTEventEmitter.h>
12
+
13
+ // PingNative inherits from RCTEventEmitter (stage 3) so it can dispatch
14
+ // "PingStorageCall" / "PingTransportCall" events to JS for the bridge marshalling.
15
+ @interface RCT_EXTERN_MODULE(PingNative, RCTEventEmitter)
16
+
17
+ // Stage 2 smoke test.
18
+ RCT_EXTERN_METHOD(getWireContractVersion: (RCTPromiseResolveBlock)resolve
19
+ rejecter: (RCTPromiseRejectBlock)reject)
20
+
21
+ // Stage 3: storage round-trip self-test.
22
+ RCT_EXTERN_METHOD(testStorageRoundtrip: (RCTPromiseResolveBlock)resolve
23
+ rejecter: (RCTPromiseRejectBlock)reject)
24
+
25
+ // Stage 3: JS-side resume hooks for the storage bridge.
26
+ RCT_EXTERN_METHOD(resolveStorageCall: (NSString *)id
27
+ value: (id)value
28
+ resolver: (RCTPromiseResolveBlock)resolve
29
+ rejecter: (RCTPromiseRejectBlock)reject)
30
+
31
+ RCT_EXTERN_METHOD(rejectStorageCall: (NSString *)id
32
+ message: (NSString *)message
33
+ resolver: (RCTPromiseResolveBlock)resolve
34
+ rejecter: (RCTPromiseRejectBlock)reject)
35
+
36
+ // Stage 3: JS-side resume hooks for the transport bridge.
37
+ RCT_EXTERN_METHOD(resolveTransportCall: (NSString *)id
38
+ value: (id)value
39
+ resolver: (RCTPromiseResolveBlock)resolve
40
+ rejecter: (RCTPromiseRejectBlock)reject)
41
+
42
+ RCT_EXTERN_METHOD(rejectTransportCall: (NSString *)id
43
+ message: (NSString *)message
44
+ resolver: (RCTPromiseResolveBlock)resolve
45
+ rejecter: (RCTPromiseRejectBlock)reject)
46
+
47
+ // Stage 4b: identity helper (UDL addition).
48
+ RCT_EXTERN_METHOD(generateIdentityExport: (RCTPromiseResolveBlock)resolve
49
+ rejecter: (RCTPromiseRejectBlock)reject)
50
+
51
+ // Stage 4b: MessagingClient lifecycle.
52
+ RCT_EXTERN_METHOD(initClient: (NSString *)identityB64
53
+ deviceLabel: (NSString *)deviceLabel
54
+ nowMs: (double)nowMs
55
+ resolver: (RCTPromiseResolveBlock)resolve
56
+ rejecter: (RCTPromiseRejectBlock)reject)
57
+
58
+ RCT_EXTERN_METHOD(getUserId: (RCTPromiseResolveBlock)resolve
59
+ rejecter: (RCTPromiseRejectBlock)reject)
60
+
61
+ RCT_EXTERN_METHOD(getDeviceId: (RCTPromiseResolveBlock)resolve
62
+ rejecter: (RCTPromiseRejectBlock)reject)
63
+
64
+ // Stage 4c: messaging methods.
65
+ RCT_EXTERN_METHOD(freshKeyPackage: (RCTPromiseResolveBlock)resolve
66
+ rejecter: (RCTPromiseRejectBlock)reject)
67
+
68
+ RCT_EXTERN_METHOD(createConversation: (NSString *)name
69
+ nowMs: (double)nowMs
70
+ resolver: (RCTPromiseResolveBlock)resolve
71
+ rejecter: (RCTPromiseRejectBlock)reject)
72
+
73
+ RCT_EXTERN_METHOD(joinConversation: (NSDictionary *)welcome
74
+ nowMs: (double)nowMs
75
+ resolver: (RCTPromiseResolveBlock)resolve
76
+ rejecter: (RCTPromiseRejectBlock)reject)
77
+
78
+ RCT_EXTERN_METHOD(sendMessage: (NSArray *)conversationId
79
+ plaintext: (NSArray *)plaintext
80
+ nowMs: (double)nowMs
81
+ resolver: (RCTPromiseResolveBlock)resolve
82
+ rejecter: (RCTPromiseRejectBlock)reject)
83
+
84
+ RCT_EXTERN_METHOD(addMembers: (NSArray *)conversationId
85
+ keyPackages: (NSArray *)keyPackages
86
+ nowMs: (double)nowMs
87
+ resolver: (RCTPromiseResolveBlock)resolve
88
+ rejecter: (RCTPromiseRejectBlock)reject)
89
+
90
+ RCT_EXTERN_METHOD(removeMembers: (NSArray *)conversationId
91
+ leafIndexes: (NSArray *)leafIndexes
92
+ nowMs: (double)nowMs
93
+ resolver: (RCTPromiseResolveBlock)resolve
94
+ rejecter: (RCTPromiseRejectBlock)reject)
95
+
96
+ // Stage 4d: sync methods.
97
+ RCT_EXTERN_METHOD(processEnvelope: (NSDictionary *)envelope
98
+ nowMs: (double)nowMs
99
+ resolver: (RCTPromiseResolveBlock)resolve
100
+ rejecter: (RCTPromiseRejectBlock)reject)
101
+
102
+ RCT_EXTERN_METHOD(syncConversations: (double)nowMs
103
+ resolver: (RCTPromiseResolveBlock)resolve
104
+ rejecter: (RCTPromiseRejectBlock)reject)
105
+
106
+ // Stage 4e: discovery + observer.
107
+ RCT_EXTERN_METHOD(listConversations: (RCTPromiseResolveBlock)resolve
108
+ rejecter: (RCTPromiseRejectBlock)reject)
109
+
110
+ RCT_EXTERN_METHOD(listDevices: (RCTPromiseResolveBlock)resolve
111
+ rejecter: (RCTPromiseRejectBlock)reject)
112
+
113
+ RCT_EXTERN_METHOD(setObserver: (RCTPromiseResolveBlock)resolve
114
+ rejecter: (RCTPromiseRejectBlock)reject)
115
+
116
+ // Stage 4f: linking + revocation.
117
+ RCT_EXTERN_METHOD(buildLinkingTicket: (NSArray *)newDeviceId
118
+ newDeviceKp: (NSArray *)newDeviceKp
119
+ nowMs: (double)nowMs
120
+ resolver: (RCTPromiseResolveBlock)resolve
121
+ rejecter: (RCTPromiseRejectBlock)reject)
122
+
123
+ RCT_EXTERN_METHOD(consumeLinkingTicket: (NSDictionary *)ticket
124
+ nowMs: (double)nowMs
125
+ resolver: (RCTPromiseResolveBlock)resolve
126
+ rejecter: (RCTPromiseRejectBlock)reject)
127
+
128
+ RCT_EXTERN_METHOD(revokeDevice: (NSArray *)deviceId
129
+ nowMs: (double)nowMs
130
+ resolver: (RCTPromiseResolveBlock)resolve
131
+ rejecter: (RCTPromiseRejectBlock)reject)
132
+
133
+ // Stage 5 polish: macOS clipboard helper.
134
+ RCT_EXTERN_METHOD(setClipboard: (NSString *)text
135
+ resolver: (RCTPromiseResolveBlock)resolve
136
+ rejecter: (RCTPromiseRejectBlock)reject)
137
+
138
+ + (BOOL)requiresMainQueueSetup
139
+ {
140
+ return NO;
141
+ }
142
+
143
+ @end