ping-openmls-sdk-react-native-macos 0.7.4 → 0.7.5

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.
Binary file
@@ -726,6 +726,8 @@ public func FfiConverterTypeMessageObserver_lower(_ value: MessageObserver) -> U
726
726
  public protocol MessagingClientProtocol: AnyObject {
727
727
  func addMembers(conversationId: ConversationId, entries: [KeyPackageEntry], nowMs: UInt64) async throws
728
728
 
729
+ func admitDeviceToChats(newDeviceId: DeviceId, entries: [AdmitChatEntry], nowMs: UInt64) async throws -> [AdmitChatOutcome]
730
+
729
731
  func buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Data, lastAppEvents: [CatchupAppEvent], nowMs: UInt64) async throws -> LinkingTicket
730
732
 
731
733
  func consumeLinkingTicket(ticket: LinkingTicket, nowMs: UInt64) async throws
@@ -845,6 +847,23 @@ open class MessagingClient:
845
847
  )
846
848
  }
847
849
 
850
+ open func admitDeviceToChats(newDeviceId: DeviceId, entries: [AdmitChatEntry], nowMs: UInt64) async throws -> [AdmitChatOutcome] {
851
+ return
852
+ try await uniffiRustCallAsync(
853
+ rustFutureFunc: {
854
+ uniffi_ping_ffi_fn_method_messagingclient_admit_device_to_chats(
855
+ self.uniffiClonePointer(),
856
+ FfiConverterTypeDeviceId.lower(newDeviceId), FfiConverterSequenceTypeAdmitChatEntry.lower(entries), FfiConverterUInt64.lower(nowMs)
857
+ )
858
+ },
859
+ pollFunc: ffi_ping_ffi_rust_future_poll_rust_buffer,
860
+ completeFunc: ffi_ping_ffi_rust_future_complete_rust_buffer,
861
+ freeFunc: ffi_ping_ffi_rust_future_free_rust_buffer,
862
+ liftFunc: FfiConverterSequenceTypeAdmitChatOutcome.lift,
863
+ errorHandler: FfiConverterTypePingError.lift
864
+ )
865
+ }
866
+
848
867
  open func buildLinkingTicket(newDeviceId: DeviceId, newDeviceKp: Data, lastAppEvents: [CatchupAppEvent], nowMs: UInt64) async throws -> LinkingTicket {
849
868
  return
850
869
  try await uniffiRustCallAsync(
@@ -1811,6 +1830,144 @@ public func FfiConverterTypeTransport_lower(_ value: Transport) -> UnsafeMutable
1811
1830
  return FfiConverterTypeTransport.lower(value)
1812
1831
  }
1813
1832
 
1833
+ public struct AdmitChatEntry {
1834
+ public var conversationId: ConversationId
1835
+ public var keyPackage: Data
1836
+
1837
+ /// Default memberwise initializers are never public by default, so we
1838
+ /// declare one manually.
1839
+ public init(conversationId: ConversationId, keyPackage: Data) {
1840
+ self.conversationId = conversationId
1841
+ self.keyPackage = keyPackage
1842
+ }
1843
+ }
1844
+
1845
+ extension AdmitChatEntry: Equatable, Hashable {
1846
+ public static func == (lhs: AdmitChatEntry, rhs: AdmitChatEntry) -> Bool {
1847
+ if lhs.conversationId != rhs.conversationId {
1848
+ return false
1849
+ }
1850
+ if lhs.keyPackage != rhs.keyPackage {
1851
+ return false
1852
+ }
1853
+ return true
1854
+ }
1855
+
1856
+ public func hash(into hasher: inout Hasher) {
1857
+ hasher.combine(conversationId)
1858
+ hasher.combine(keyPackage)
1859
+ }
1860
+ }
1861
+
1862
+ #if swift(>=5.8)
1863
+ @_documentation(visibility: private)
1864
+ #endif
1865
+ public struct FfiConverterTypeAdmitChatEntry: FfiConverterRustBuffer {
1866
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AdmitChatEntry {
1867
+ return
1868
+ try AdmitChatEntry(
1869
+ conversationId: FfiConverterTypeConversationId.read(from: &buf),
1870
+ keyPackage: FfiConverterData.read(from: &buf)
1871
+ )
1872
+ }
1873
+
1874
+ public static func write(_ value: AdmitChatEntry, into buf: inout [UInt8]) {
1875
+ FfiConverterTypeConversationId.write(value.conversationId, into: &buf)
1876
+ FfiConverterData.write(value.keyPackage, into: &buf)
1877
+ }
1878
+ }
1879
+
1880
+ #if swift(>=5.8)
1881
+ @_documentation(visibility: private)
1882
+ #endif
1883
+ public func FfiConverterTypeAdmitChatEntry_lift(_ buf: RustBuffer) throws -> AdmitChatEntry {
1884
+ return try FfiConverterTypeAdmitChatEntry.lift(buf)
1885
+ }
1886
+
1887
+ #if swift(>=5.8)
1888
+ @_documentation(visibility: private)
1889
+ #endif
1890
+ public func FfiConverterTypeAdmitChatEntry_lower(_ value: AdmitChatEntry) -> RustBuffer {
1891
+ return FfiConverterTypeAdmitChatEntry.lower(value)
1892
+ }
1893
+
1894
+ public struct AdmitChatOutcome {
1895
+ public var conversationId: ConversationId
1896
+ public var status: AdmitChatStatus
1897
+ public var reason: String?
1898
+ public var error: String?
1899
+
1900
+ /// Default memberwise initializers are never public by default, so we
1901
+ /// declare one manually.
1902
+ public init(conversationId: ConversationId, status: AdmitChatStatus, reason: String?, error: String?) {
1903
+ self.conversationId = conversationId
1904
+ self.status = status
1905
+ self.reason = reason
1906
+ self.error = error
1907
+ }
1908
+ }
1909
+
1910
+ extension AdmitChatOutcome: Equatable, Hashable {
1911
+ public static func == (lhs: AdmitChatOutcome, rhs: AdmitChatOutcome) -> Bool {
1912
+ if lhs.conversationId != rhs.conversationId {
1913
+ return false
1914
+ }
1915
+ if lhs.status != rhs.status {
1916
+ return false
1917
+ }
1918
+ if lhs.reason != rhs.reason {
1919
+ return false
1920
+ }
1921
+ if lhs.error != rhs.error {
1922
+ return false
1923
+ }
1924
+ return true
1925
+ }
1926
+
1927
+ public func hash(into hasher: inout Hasher) {
1928
+ hasher.combine(conversationId)
1929
+ hasher.combine(status)
1930
+ hasher.combine(reason)
1931
+ hasher.combine(error)
1932
+ }
1933
+ }
1934
+
1935
+ #if swift(>=5.8)
1936
+ @_documentation(visibility: private)
1937
+ #endif
1938
+ public struct FfiConverterTypeAdmitChatOutcome: FfiConverterRustBuffer {
1939
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AdmitChatOutcome {
1940
+ return
1941
+ try AdmitChatOutcome(
1942
+ conversationId: FfiConverterTypeConversationId.read(from: &buf),
1943
+ status: FfiConverterTypeAdmitChatStatus.read(from: &buf),
1944
+ reason: FfiConverterOptionString.read(from: &buf),
1945
+ error: FfiConverterOptionString.read(from: &buf)
1946
+ )
1947
+ }
1948
+
1949
+ public static func write(_ value: AdmitChatOutcome, into buf: inout [UInt8]) {
1950
+ FfiConverterTypeConversationId.write(value.conversationId, into: &buf)
1951
+ FfiConverterTypeAdmitChatStatus.write(value.status, into: &buf)
1952
+ FfiConverterOptionString.write(value.reason, into: &buf)
1953
+ FfiConverterOptionString.write(value.error, into: &buf)
1954
+ }
1955
+ }
1956
+
1957
+ #if swift(>=5.8)
1958
+ @_documentation(visibility: private)
1959
+ #endif
1960
+ public func FfiConverterTypeAdmitChatOutcome_lift(_ buf: RustBuffer) throws -> AdmitChatOutcome {
1961
+ return try FfiConverterTypeAdmitChatOutcome.lift(buf)
1962
+ }
1963
+
1964
+ #if swift(>=5.8)
1965
+ @_documentation(visibility: private)
1966
+ #endif
1967
+ public func FfiConverterTypeAdmitChatOutcome_lower(_ value: AdmitChatOutcome) -> RustBuffer {
1968
+ return FfiConverterTypeAdmitChatOutcome.lower(value)
1969
+ }
1970
+
1814
1971
  public struct CatchupAppEvent {
1815
1972
  public var conversationId: ConversationId
1816
1973
  public var appEventBytes: Data
@@ -2852,6 +3009,64 @@ public func FfiConverterTypeUserId_lower(_ value: UserId) -> RustBuffer {
2852
3009
  // Note that we don't yet support `indirect` for enums.
2853
3010
  // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.
2854
3011
 
3012
+ public enum AdmitChatStatus {
3013
+ case admitted
3014
+ case skipped
3015
+ case failed
3016
+ }
3017
+
3018
+ #if swift(>=5.8)
3019
+ @_documentation(visibility: private)
3020
+ #endif
3021
+ public struct FfiConverterTypeAdmitChatStatus: FfiConverterRustBuffer {
3022
+ typealias SwiftType = AdmitChatStatus
3023
+
3024
+ public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> AdmitChatStatus {
3025
+ let variant: Int32 = try readInt(&buf)
3026
+ switch variant {
3027
+ case 1: return .admitted
3028
+
3029
+ case 2: return .skipped
3030
+
3031
+ case 3: return .failed
3032
+
3033
+ default: throw UniffiInternalError.unexpectedEnumCase
3034
+ }
3035
+ }
3036
+
3037
+ public static func write(_ value: AdmitChatStatus, into buf: inout [UInt8]) {
3038
+ switch value {
3039
+ case .admitted:
3040
+ writeInt(&buf, Int32(1))
3041
+
3042
+ case .skipped:
3043
+ writeInt(&buf, Int32(2))
3044
+
3045
+ case .failed:
3046
+ writeInt(&buf, Int32(3))
3047
+ }
3048
+ }
3049
+ }
3050
+
3051
+ #if swift(>=5.8)
3052
+ @_documentation(visibility: private)
3053
+ #endif
3054
+ public func FfiConverterTypeAdmitChatStatus_lift(_ buf: RustBuffer) throws -> AdmitChatStatus {
3055
+ return try FfiConverterTypeAdmitChatStatus.lift(buf)
3056
+ }
3057
+
3058
+ #if swift(>=5.8)
3059
+ @_documentation(visibility: private)
3060
+ #endif
3061
+ public func FfiConverterTypeAdmitChatStatus_lower(_ value: AdmitChatStatus) -> RustBuffer {
3062
+ return FfiConverterTypeAdmitChatStatus.lower(value)
3063
+ }
3064
+
3065
+ extension AdmitChatStatus: Equatable, Hashable {}
3066
+
3067
+ // Note that we don't yet support `indirect` for enums.
3068
+ // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion.
3069
+
2855
3070
  public enum MessageKind {
2856
3071
  case application
2857
3072
  case commit
@@ -3150,6 +3365,56 @@ private struct FfiConverterSequenceString: FfiConverterRustBuffer {
3150
3365
  }
3151
3366
  }
3152
3367
 
3368
+ #if swift(>=5.8)
3369
+ @_documentation(visibility: private)
3370
+ #endif
3371
+ private struct FfiConverterSequenceTypeAdmitChatEntry: FfiConverterRustBuffer {
3372
+ typealias SwiftType = [AdmitChatEntry]
3373
+
3374
+ static func write(_ value: [AdmitChatEntry], into buf: inout [UInt8]) {
3375
+ let len = Int32(value.count)
3376
+ writeInt(&buf, len)
3377
+ for item in value {
3378
+ FfiConverterTypeAdmitChatEntry.write(item, into: &buf)
3379
+ }
3380
+ }
3381
+
3382
+ static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [AdmitChatEntry] {
3383
+ let len: Int32 = try readInt(&buf)
3384
+ var seq = [AdmitChatEntry]()
3385
+ seq.reserveCapacity(Int(len))
3386
+ for _ in 0 ..< len {
3387
+ try seq.append(FfiConverterTypeAdmitChatEntry.read(from: &buf))
3388
+ }
3389
+ return seq
3390
+ }
3391
+ }
3392
+
3393
+ #if swift(>=5.8)
3394
+ @_documentation(visibility: private)
3395
+ #endif
3396
+ private struct FfiConverterSequenceTypeAdmitChatOutcome: FfiConverterRustBuffer {
3397
+ typealias SwiftType = [AdmitChatOutcome]
3398
+
3399
+ static func write(_ value: [AdmitChatOutcome], into buf: inout [UInt8]) {
3400
+ let len = Int32(value.count)
3401
+ writeInt(&buf, len)
3402
+ for item in value {
3403
+ FfiConverterTypeAdmitChatOutcome.write(item, into: &buf)
3404
+ }
3405
+ }
3406
+
3407
+ static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [AdmitChatOutcome] {
3408
+ let len: Int32 = try readInt(&buf)
3409
+ var seq = [AdmitChatOutcome]()
3410
+ seq.reserveCapacity(Int(len))
3411
+ for _ in 0 ..< len {
3412
+ try seq.append(FfiConverterTypeAdmitChatOutcome.read(from: &buf))
3413
+ }
3414
+ return seq
3415
+ }
3416
+ }
3417
+
3153
3418
  #if swift(>=5.8)
3154
3419
  @_documentation(visibility: private)
3155
3420
  #endif
@@ -3532,6 +3797,9 @@ private var initializationResult: InitializationResult = {
3532
3797
  if uniffi_ping_ffi_checksum_method_messagingclient_add_members() != 2224 {
3533
3798
  return InitializationResult.apiChecksumMismatch
3534
3799
  }
3800
+ if uniffi_ping_ffi_checksum_method_messagingclient_admit_device_to_chats() != 22080 {
3801
+ return InitializationResult.apiChecksumMismatch
3802
+ }
3535
3803
  if uniffi_ping_ffi_checksum_method_messagingclient_build_linking_ticket() != 21631 {
3536
3804
  return InitializationResult.apiChecksumMismatch
3537
3805
  }
@@ -96,6 +96,16 @@ RCT_EXTERN_METHOD(addMembers: (NSArray *)conversationId
96
96
  resolver: (RCTPromiseResolveBlock)resolve
97
97
  rejecter: (RCTPromiseRejectBlock)reject)
98
98
 
99
+ // Post-link reconciliation: admit a freshly-linked device to every chat in
100
+ // `entries`. `entries` is an array of `{ conversationId: [Int], keyPackage: [Int] }`
101
+ // dicts. Returns an array of `{ conversationId: [Int], status: String,
102
+ // reason?: String, error?: String }`.
103
+ RCT_EXTERN_METHOD(admitDeviceToChats: (NSArray *)newDeviceId
104
+ entries: (NSArray *)entries
105
+ nowMs: (double)nowMs
106
+ resolver: (RCTPromiseResolveBlock)resolve
107
+ rejecter: (RCTPromiseRejectBlock)reject)
108
+
99
109
  RCT_EXTERN_METHOD(removeMembers: (NSArray *)conversationId
100
110
  leafIndexes: (NSArray *)leafIndexes
101
111
  nowMs: (double)nowMs
@@ -439,6 +439,40 @@ public final class PingNative: RCTEventEmitter {
439
439
  }
440
440
  }
441
441
 
442
+ /// Admit a freshly-linked device to every chat in `entries`. Mirrors the SDK's
443
+ /// `MessagingClient.admit_device_to_chats` — one Commit + Welcome per chat,
444
+ /// per-chat outcomes returned so a single bad KP doesn't strand the user on
445
+ /// the other chats. `entries` is `{ conversationId: [Int], keyPackage: [Int] }[]`.
446
+ /// Result is `{ conversationId: [Int], status: "admitted"|"skipped"|"failed",
447
+ /// reason?: String, error?: String }[]`.
448
+ @objc(admitDeviceToChats:entries:nowMs:resolver:rejecter:)
449
+ public func admitDeviceToChatsNative(
450
+ _ newDeviceIdBytes: NSArray,
451
+ entries entriesArr: NSArray,
452
+ nowMs: Double,
453
+ resolver resolve: @escaping RCTPromiseResolveBlock,
454
+ rejecter reject: @escaping RCTPromiseRejectBlock
455
+ ) {
456
+ Task {
457
+ guard let client = self.client else {
458
+ reject("NotInitialised", "MessagingClient not initialised", nil)
459
+ return
460
+ }
461
+ do {
462
+ let deviceId = try TypeBridge.decodeDeviceId(newDeviceIdBytes)
463
+ let entries = try TypeBridge.decodeAdmitChatEntryArray(entriesArr)
464
+ let outcomes = try await client.admitDeviceToChats(
465
+ newDeviceId: deviceId,
466
+ entries: entries,
467
+ nowMs: UInt64(nowMs)
468
+ )
469
+ resolve(TypeBridge.encodeAdmitChatOutcomeArray(outcomes))
470
+ } catch {
471
+ reject("AdmitDeviceToChatsFailed", String(describing: error), error)
472
+ }
473
+ }
474
+ }
475
+
442
476
  /// Remove members by leaf index. Leaf indexes come from `MlsGroup::members()` in
443
477
  /// stage 4e's `listConversations` result; for now they're caller-supplied integers.
444
478
  @objc(removeMembers:leafIndexes:nowMs:resolver:rejecter:)
@@ -318,6 +318,55 @@ enum TypeBridge {
318
318
  return try arr.map { try decodeKeyPackageEntry($0) }
319
319
  }
320
320
 
321
+ // MARK: - AdmitChat (post-link device admission)
322
+
323
+ /// Decode `{ conversationId: [Int], keyPackage: [Int] }` into UniFFI's
324
+ /// `AdmitChatEntry`. Used by `admitDeviceToChatsNative` — one entry per
325
+ /// chat the existing device is admitting the new device into.
326
+ static func decodeAdmitChatEntry(_ value: Any?) throws -> AdmitChatEntry {
327
+ guard let dict = value as? [String: Any] else {
328
+ throw BridgeError.decodeFailure("expected AdmitChatEntry dict")
329
+ }
330
+ let convId = try decodeConversationId(dict["conversationId"] ?? dict["conversation_id"])
331
+ let keyPackage = try decodeBytesOrThrow(
332
+ dict["keyPackage"] ?? dict["key_package"],
333
+ field: "keyPackage"
334
+ )
335
+ return AdmitChatEntry(conversationId: convId, keyPackage: keyPackage)
336
+ }
337
+
338
+ static func decodeAdmitChatEntryArray(_ value: Any?) throws -> [AdmitChatEntry] {
339
+ guard let arr = value as? [Any] else {
340
+ throw BridgeError.decodeFailure("expected AdmitChatEntry array")
341
+ }
342
+ return try arr.map { try decodeAdmitChatEntry($0) }
343
+ }
344
+
345
+ /// Encode an `AdmitChatStatus` as one of `"admitted" | "skipped" | "failed"`.
346
+ /// Matches the web SDK's `AdmitChatOutcome.status` discriminator so a host
347
+ /// app sharing logic across web/desktop can switch on a single string.
348
+ static func encodeAdmitChatStatus(_ status: AdmitChatStatus) -> String {
349
+ switch status {
350
+ case .admitted: return "admitted"
351
+ case .skipped: return "skipped"
352
+ case .failed: return "failed"
353
+ }
354
+ }
355
+
356
+ static func encodeAdmitChatOutcome(_ outcome: AdmitChatOutcome) -> [String: Any] {
357
+ var dict: [String: Any] = [
358
+ "conversationId": encodeConversationId(outcome.conversationId),
359
+ "status": encodeAdmitChatStatus(outcome.status),
360
+ ]
361
+ if let reason = outcome.reason { dict["reason"] = reason }
362
+ if let error = outcome.error { dict["error"] = error }
363
+ return dict
364
+ }
365
+
366
+ static func encodeAdmitChatOutcomeArray(_ outcomes: [AdmitChatOutcome]) -> [[String: Any]] {
367
+ return outcomes.map(encodeAdmitChatOutcome)
368
+ }
369
+
321
370
  // MARK: - [CR-13] CatchupAppEvent
322
371
 
323
372
  /// Decode `{ conversationId: [Int], appEventBytes: [Int] }` into UniFFI's
package/ios/pingFFI.h CHANGED
@@ -382,6 +382,11 @@ uint64_t uniffi_ping_ffi_fn_constructor_messagingclient_init(RustBuffer identity
382
382
  uint64_t uniffi_ping_ffi_fn_method_messagingclient_add_members(void*_Nonnull ptr, RustBuffer conversation_id, RustBuffer entries, uint64_t now_ms
383
383
  );
384
384
  #endif
385
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_ADMIT_DEVICE_TO_CHATS
386
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_ADMIT_DEVICE_TO_CHATS
387
+ uint64_t uniffi_ping_ffi_fn_method_messagingclient_admit_device_to_chats(void*_Nonnull ptr, RustBuffer new_device_id, RustBuffer entries, uint64_t now_ms
388
+ );
389
+ #endif
385
390
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_BUILD_LINKING_TICKET
386
391
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_BUILD_LINKING_TICKET
387
392
  uint64_t uniffi_ping_ffi_fn_method_messagingclient_build_linking_ticket(void*_Nonnull ptr, RustBuffer new_device_id, RustBuffer new_device_kp, RustBuffer last_app_events, uint64_t now_ms
@@ -883,6 +888,12 @@ uint16_t uniffi_ping_ffi_checksum_method_messageobserver_on_conversation_updated
883
888
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_ADD_MEMBERS
884
889
  uint16_t uniffi_ping_ffi_checksum_method_messagingclient_add_members(void
885
890
 
891
+ );
892
+ #endif
893
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_ADMIT_DEVICE_TO_CHATS
894
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_ADMIT_DEVICE_TO_CHATS
895
+ uint16_t uniffi_ping_ffi_checksum_method_messagingclient_admit_device_to_chats(void
896
+
886
897
  );
887
898
  #endif
888
899
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_BUILD_LINKING_TICKET
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ping-openmls-sdk-react-native-macos",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "Real MLS for React Native macOS apps — wraps the ping-openmls-sdk Rust core via UniFFI.",
5
5
  "homepage": "https://github.com/AMP-Media-Development/ping-openmls-sdk",
6
6
  "license": "Apache-2.0",
@@ -70,6 +70,24 @@ export interface CatchupAppEvent {
70
70
  appEventBytes: Uint8Array;
71
71
  }
72
72
 
73
+ /** One per-chat KP for `MessagingClient.admitDeviceToChats`. Hosts claim these
74
+ * from the auth-layer's per-account KP pool after the newly-linked device's
75
+ * bootstrap has uploaded its KP batch. */
76
+ export interface AdmitChatEntry {
77
+ conversationId: Uint8Array;
78
+ keyPackage: Uint8Array;
79
+ }
80
+
81
+ /** Per-chat result returned by `MessagingClient.admitDeviceToChats`. */
82
+ export interface AdmitChatOutcome {
83
+ conversationId: Uint8Array;
84
+ status: "admitted" | "skipped" | "failed";
85
+ /** Set when `status === "skipped"`. Diagnostic only. */
86
+ reason?: string;
87
+ /** Set when `status === "failed"`. The underlying MLS / transport error. */
88
+ error?: string;
89
+ }
90
+
73
91
  /**
74
92
  * High-level facade over the native MessagingClient. Methods marshal arguments + return
75
93
  * values across the JS↔Swift bridge; the actual MLS work happens in Rust.
@@ -87,6 +105,14 @@ export class MessagingClient {
87
105
  private constructor(
88
106
  private readonly disconnectStorage: () => void,
89
107
  private readonly disconnectTransport: () => void,
108
+ /**
109
+ * Keep a reference to the host transport so post-link reconciliation
110
+ * can prime its Welcome-recipient map before the native SDK emits
111
+ * Welcomes. The bridge connection (above) is what forwards `send` /
112
+ * `fetchSince` / `discoverDevices` calls — we keep this around purely
113
+ * for the host-side primitives (e.g. `setNextWelcomeRecipients`).
114
+ */
115
+ private readonly transport: Transport,
90
116
  ) {}
91
117
 
92
118
  /**
@@ -122,7 +148,7 @@ export class MessagingClient {
122
148
  throw e;
123
149
  }
124
150
 
125
- const client = new MessagingClient(disconnectStorage, disconnectTransport);
151
+ const client = new MessagingClient(disconnectStorage, disconnectTransport, cfg.transport);
126
152
 
127
153
  // Install the observer bridge so native can fire application-message and
128
154
  // conversation-updated events. Failure here is non-fatal — the client still works
@@ -347,6 +373,68 @@ export class MessagingClient {
347
373
  await NativePing.consumeLinkingTicket(encodeLinkingTicket(ticket), Date.now());
348
374
  }
349
375
 
376
+ /**
377
+ * Admit a freshly-linked device to every chat in `entries` — one Commit +
378
+ * Welcome per chat, with per-chat outcomes. Host calls this AFTER
379
+ * `consumeLinkingTicket` on the new device side (and after the new device
380
+ * has uploaded its KeyPackage batch so the existing device can claim per-chat
381
+ * KPs).
382
+ *
383
+ * Welcome routing: this method primes the host transport's
384
+ * `setNextWelcomeRecipients` for each chat BEFORE the native call so the
385
+ * BE's `POST /v1/messages?recipient=` query param lands correctly. Hosts
386
+ * that implement Transport.setNextWelcomeRecipients (PingTransport on web
387
+ * and desktop both do) need no further coordination. Hosts that don't see
388
+ * a silent no-op — Welcomes will still send, but Welcome routing depends
389
+ * entirely on the transport's existing recipient priming.
390
+ */
391
+ async admitDeviceToChats(
392
+ newDeviceId: Uint8Array,
393
+ entries: AdmitChatEntry[],
394
+ ): Promise<AdmitChatOutcome[]> {
395
+ // Pre-prime the transport's recipient map for every entry. Each entry's
396
+ // Welcome will consume its own slot on send; pre-priming the whole batch
397
+ // is safe because chat ids are unique within `entries`. We tolerate
398
+ // hosts without `setNextWelcomeRecipients` (e.g. tests) by treating the
399
+ // missing method as a no-op.
400
+ if (this.transport.setNextWelcomeRecipients) {
401
+ for (const entry of entries) {
402
+ try {
403
+ await this.transport.setNextWelcomeRecipients(
404
+ entry.conversationId,
405
+ [newDeviceId],
406
+ );
407
+ } catch {
408
+ // Priming is best-effort; the per-chat send will surface a 400
409
+ // from the BE if it actually needed the recipient.
410
+ }
411
+ }
412
+ }
413
+
414
+ const raw = await NativePing.admitDeviceToChats(
415
+ Array.from(newDeviceId),
416
+ entries.map((e) => ({
417
+ conversationId: Array.from(e.conversationId),
418
+ keyPackage: Array.from(e.keyPackage),
419
+ })),
420
+ Date.now(),
421
+ );
422
+ return raw.map((o: {
423
+ conversationId: number[];
424
+ status: "admitted" | "skipped" | "failed";
425
+ reason?: string;
426
+ error?: string;
427
+ }) => {
428
+ const outcome: AdmitChatOutcome = {
429
+ conversationId: Uint8Array.from(o.conversationId),
430
+ status: o.status,
431
+ };
432
+ if (o.reason !== undefined) outcome.reason = o.reason;
433
+ if (o.error !== undefined) outcome.error = o.error;
434
+ return outcome;
435
+ });
436
+ }
437
+
350
438
  /**
351
439
  * Revoke a device ([CR-2]).
352
440
  *
package/src/NativePing.ts CHANGED
@@ -117,6 +117,28 @@ export interface Spec extends TurboModule {
117
117
  nowMs: number,
118
118
  ): Promise<null>;
119
119
 
120
+ /**
121
+ * Admit a freshly-linked device to every chat in `entries`. The new device
122
+ * gets its own MLS leaf (so per-device PCS still works) and the BE-side
123
+ * `conversation_members` row is materialised as a side effect of each
124
+ * Welcome. Returns per-chat outcomes — `status` is one of `"admitted"`,
125
+ * `"skipped"`, `"failed"`. On `"failed"`, `error` carries the underlying
126
+ * MLS / transport error. On `"skipped"`, `reason` carries the diagnostic
127
+ * (e.g. `"device_group"` when a DG slipped into the entries).
128
+ */
129
+ admitDeviceToChats(
130
+ newDeviceId: number[],
131
+ entries: { conversationId: number[]; keyPackage: number[] }[],
132
+ nowMs: number,
133
+ ): Promise<
134
+ {
135
+ conversationId: number[];
136
+ status: "admitted" | "skipped" | "failed";
137
+ reason?: string;
138
+ error?: string;
139
+ }[]
140
+ >;
141
+
120
142
  /** Remove members by leaf index in the conversation's ratchet tree. */
121
143
  removeMembers(
122
144
  conversationId: number[],
@@ -26,6 +26,17 @@ export interface Transport {
26
26
  * handle.
27
27
  */
28
28
  subscribe?(onEvent: (envelopeJson: Record<string, unknown>) => void): { unsubscribe(): void };
29
+ /**
30
+ * Optional. Prime the host with recipient device ids for the NEXT Welcome on
31
+ * `conversationId`. Called by `MessagingClient.admitDeviceToChats` before
32
+ * each per-chat Welcome send so the BE's `POST /v1/messages?recipient=`
33
+ * query param lands correctly. Hosts that route Welcomes differently (or
34
+ * don't need per-recipient routing) may leave this unimplemented.
35
+ */
36
+ setNextWelcomeRecipients?(
37
+ conversationId: Uint8Array,
38
+ recipientDeviceIds: Uint8Array[],
39
+ ): Promise<void>;
29
40
  }
30
41
 
31
42
  interface TransportCallEvent {