ping-openmls-sdk-react-native-macos 0.2.2 → 0.3.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.
@@ -211,11 +211,13 @@ public final class PingNative: RCTEventEmitter {
211
211
  /// `nowMs` is the wall-clock at the call site (Hermes can't pass UInt64 across the
212
212
  /// bridge precisely so we accept Double and truncate; valid for the next ~285,000
213
213
  /// years of Unix time).
214
- @objc(initClient:deviceLabel:nowMs:resolver:rejecter:)
214
+ @objc(initClient:deviceLabel:nowMs:sqlitePath:sqliteEncryptionKeyB64:resolver:rejecter:)
215
215
  public func initClient(
216
216
  _ identityB64: String,
217
217
  deviceLabel: String,
218
218
  nowMs: Double,
219
+ sqlitePath: String?,
220
+ sqliteEncryptionKeyB64: String?,
219
221
  resolver resolve: @escaping RCTPromiseResolveBlock,
220
222
  rejecter reject: @escaping RCTPromiseRejectBlock
221
223
  ) {
@@ -225,6 +227,17 @@ public final class PingNative: RCTEventEmitter {
225
227
  reject("InvalidIdentity", "identity bytes failed base64 decode", nil)
226
228
  return
227
229
  }
230
+ // [CR-4] Optional SQLCipher key. Decode if supplied; reject if it
231
+ // decodes but isn't 32 bytes (host bug — better loud than silent).
232
+ var sqliteKey: Data? = nil
233
+ if let keyB64 = sqliteEncryptionKeyB64,
234
+ let keyData = Data(base64Encoded: keyB64) {
235
+ guard keyData.count == 32 else {
236
+ reject("InvalidSqliteKey", "sqliteEncryptionKey must be 32 bytes, got \(keyData.count)", nil)
237
+ return
238
+ }
239
+ sqliteKey = keyData
240
+ }
228
241
  // UniFFI's `[Name=init]` constructor generates a static factory named
229
242
  // `init` (backquoted because Swift reserves the bare identifier for
230
243
  // designated initializers).
@@ -233,7 +246,9 @@ public final class PingNative: RCTEventEmitter {
233
246
  deviceLabel: deviceLabel,
234
247
  storage: self.storageBridge,
235
248
  transport: self.transportBridge,
236
- nowMs: UInt64(nowMs)
249
+ nowMs: UInt64(nowMs),
250
+ sqlitePath: sqlitePath,
251
+ sqliteEncryptionKey: sqliteKey
237
252
  )
238
253
  self.client = c
239
254
  resolve(true)
@@ -373,12 +388,13 @@ public final class PingNative: RCTEventEmitter {
373
388
  }
374
389
  }
375
390
 
376
- /// Add members to an existing conversation. `keyPackages` is an array of byte arrays
377
- /// (each one a peer's KeyPackage from `freshKeyPackage`).
378
- @objc(addMembers:keyPackages:nowMs:resolver:rejecter:)
391
+ /// Add members ([CR-2]). `entries` is an array of `{ deviceId: [Int], keyPackage:
392
+ /// [Int] }` dicts the device-id pairing lets the SDK persist a per-conversation
393
+ /// device→leaf map that `revokeDeviceNative` later consumes.
394
+ @objc(addMembers:entries:nowMs:resolver:rejecter:)
379
395
  public func addMembersNative(
380
396
  _ conversationIdBytes: NSArray,
381
- keyPackages keyPackagesArr: NSArray,
397
+ entries entriesArr: NSArray,
382
398
  nowMs: Double,
383
399
  resolver resolve: @escaping RCTPromiseResolveBlock,
384
400
  rejecter reject: @escaping RCTPromiseRejectBlock
@@ -390,13 +406,10 @@ public final class PingNative: RCTEventEmitter {
390
406
  }
391
407
  do {
392
408
  let convId = try TypeBridge.decodeConversationId(conversationIdBytes)
393
- var kps: [Data] = []
394
- for kp in keyPackagesArr {
395
- kps.append(try TypeBridge.decodeBytesOrThrow(kp, field: "keyPackage"))
396
- }
409
+ let entries = try TypeBridge.decodeKeyPackageEntryArray(entriesArr)
397
410
  try await client.addMembers(
398
411
  conversationId: convId,
399
- keyPackages: kps,
412
+ entries: entries,
400
413
  nowMs: UInt64(nowMs)
401
414
  )
402
415
  resolve(NSNull())
@@ -555,10 +568,15 @@ public final class PingNative: RCTEventEmitter {
555
568
  /// Build a linking ticket on the existing device (E) for a new device (N) that
556
569
  /// just published its KeyPackage. The ticket bundles a Welcome to E's DeviceGroup
557
570
  /// plus a catch-up snapshot. N consumes the ticket via `consumeLinkingTicket`.
558
- @objc(buildLinkingTicket:newDeviceKp:nowMs:resolver:rejecter:)
571
+ /// Build a linking ticket ([CR-13] populates `catchup_snapshot` from
572
+ /// `lastAppEvents`). `lastAppEvents` is an array of `{ conversationId: [Int],
573
+ /// appEventBytes: [Int] }` — host-supplied "what you missed" data the new
574
+ /// device renders before sync catches up. Empty array suppresses catchup.
575
+ @objc(buildLinkingTicket:newDeviceKp:lastAppEvents:nowMs:resolver:rejecter:)
559
576
  public func buildLinkingTicketNative(
560
577
  _ newDeviceIdBytes: NSArray,
561
578
  newDeviceKp newDeviceKpBytes: NSArray,
579
+ lastAppEvents lastAppEventsArr: NSArray,
562
580
  nowMs: Double,
563
581
  resolver resolve: @escaping RCTPromiseResolveBlock,
564
582
  rejecter reject: @escaping RCTPromiseRejectBlock
@@ -571,9 +589,11 @@ public final class PingNative: RCTEventEmitter {
571
589
  do {
572
590
  let newDeviceId = try TypeBridge.decodeDeviceId(newDeviceIdBytes)
573
591
  let newKp = try TypeBridge.decodeBytesOrThrow(newDeviceKpBytes, field: "newDeviceKp")
592
+ let events = try TypeBridge.decodeCatchupAppEventArray(lastAppEventsArr)
574
593
  let ticket = try await client.buildLinkingTicket(
575
594
  newDeviceId: newDeviceId,
576
595
  newDeviceKp: newKp,
596
+ lastAppEvents: events,
577
597
  nowMs: UInt64(nowMs)
578
598
  )
579
599
  resolve(TypeBridge.encodeLinkingTicket(ticket))
@@ -610,9 +630,9 @@ public final class PingNative: RCTEventEmitter {
610
630
  }
611
631
  }
612
632
 
613
- /// Revoke a device issues Remove proposals in the DeviceGroup and every external
614
- /// conversation the device participates in. Post-revocation, the target device
615
- /// can't decrypt new traffic (PCS).
633
+ /// Revoke a device ([CR-2]). Returns the array of Commit envelopes the SDK
634
+ /// produced — one per conversation the device was a locally-known leaf in.
635
+ /// Empty array means the device wasn't locally known (scope limit per CR-2).
616
636
  @objc(revokeDevice:nowMs:resolver:rejecter:)
617
637
  public func revokeDeviceNative(
618
638
  _ deviceIdBytes: NSArray,
@@ -627,14 +647,96 @@ public final class PingNative: RCTEventEmitter {
627
647
  }
628
648
  do {
629
649
  let deviceId = try TypeBridge.decodeDeviceId(deviceIdBytes)
630
- try await client.revokeDevice(deviceId: deviceId, nowMs: UInt64(nowMs))
631
- resolve(NSNull())
650
+ let envs = try await client.revokeDevice(deviceId: deviceId, nowMs: UInt64(nowMs))
651
+ resolve(TypeBridge.encodeEnvelopeArray(envs))
632
652
  } catch {
633
653
  reject("RevokeFailed", String(describing: error), error)
634
654
  }
635
655
  }
636
656
  }
637
657
 
658
+ // MARK: - CR-8 / CR-7 helpers
659
+
660
+ /// Export a derived secret from a conversation's MLS exporter ([CR-8]).
661
+ /// Returned bytes are a secret — the JS side is responsible for wiping them.
662
+ @objc(exportConversationSecret:label:context:length:resolver:rejecter:)
663
+ public func exportConversationSecretNative(
664
+ _ conversationIdBytes: NSArray,
665
+ label: String,
666
+ context contextBytes: NSArray,
667
+ length: NSNumber,
668
+ resolver resolve: @escaping RCTPromiseResolveBlock,
669
+ rejecter reject: @escaping RCTPromiseRejectBlock
670
+ ) {
671
+ guard let client = self.client else {
672
+ reject("NotInitialised", "MessagingClient not initialised", nil)
673
+ return
674
+ }
675
+ do {
676
+ let convId = try TypeBridge.decodeConversationId(conversationIdBytes)
677
+ let context = try TypeBridge.decodeBytesOrThrow(contextBytes, field: "context")
678
+ let bytes = try client.exportConversationSecret(
679
+ conversationId: convId,
680
+ label: label,
681
+ context: context,
682
+ length: length.uint32Value
683
+ )
684
+ resolve(TypeBridge.encodeBytes(bytes))
685
+ } catch {
686
+ reject("ExportSecretFailed", String(describing: error), error)
687
+ }
688
+ }
689
+
690
+ /// Export a `GroupStateSnapshot` for one conversation ([CR-7]).
691
+ @objc(exportConversationStateSnapshot:nowMs:resolver:rejecter:)
692
+ public func exportConversationStateSnapshotNative(
693
+ _ conversationIdBytes: NSArray,
694
+ nowMs: Double,
695
+ resolver resolve: @escaping RCTPromiseResolveBlock,
696
+ rejecter reject: @escaping RCTPromiseRejectBlock
697
+ ) {
698
+ guard let client = self.client else {
699
+ reject("NotInitialised", "MessagingClient not initialised", nil)
700
+ return
701
+ }
702
+ do {
703
+ let convId = try TypeBridge.decodeConversationId(conversationIdBytes)
704
+ let bytes = try client.exportConversationStateSnapshot(
705
+ conversationId: convId,
706
+ nowMs: UInt64(nowMs)
707
+ )
708
+ resolve(TypeBridge.encodeBytes(bytes))
709
+ } catch {
710
+ reject("ExportStateSnapshotFailed", String(describing: error), error)
711
+ }
712
+ }
713
+
714
+ /// Import a `GroupStateSnapshot` from another device ([CR-7]).
715
+ @objc(importStateSnapshot:nowMs:resolver:rejecter:)
716
+ public func importStateSnapshotNative(
717
+ _ snapshotBytes: NSArray,
718
+ nowMs: Double,
719
+ resolver resolve: @escaping RCTPromiseResolveBlock,
720
+ rejecter reject: @escaping RCTPromiseRejectBlock
721
+ ) {
722
+ Task {
723
+ guard let client = self.client else {
724
+ reject("NotInitialised", "MessagingClient not initialised", nil)
725
+ return
726
+ }
727
+ do {
728
+ let bytes = try TypeBridge.decodeBytesOrThrow(snapshotBytes, field: "snapshotBytes")
729
+ let convId = try await client.importStateSnapshot(
730
+ snapshotBytes: bytes,
731
+ nowMs: UInt64(nowMs)
732
+ )
733
+ resolve(TypeBridge.encodeConversationId(convId))
734
+ } catch {
735
+ reject("ImportStateSnapshotFailed", String(describing: error), error)
736
+ }
737
+ }
738
+ }
739
+
638
740
  // MARK: - macOS clipboard helper (stage 5 polish)
639
741
 
640
742
  /// Write a string to the macOS pasteboard. Hermes doesn't ship `navigator.clipboard`
@@ -294,4 +294,50 @@ enum TypeBridge {
294
294
  catchupSnapshot: snapshot
295
295
  )
296
296
  }
297
+
298
+ // MARK: - [CR-2] KeyPackageEntry
299
+
300
+ /// Decode `{ deviceId: [Int], keyPackage: [Int] }` into UniFFI's `KeyPackageEntry`.
301
+ /// Used by `addMembersNative` — each entry pairs a device with its KeyPackage.
302
+ static func decodeKeyPackageEntry(_ value: Any?) throws -> KeyPackageEntry {
303
+ guard let dict = value as? [String: Any] else {
304
+ throw BridgeError.decodeFailure("expected KeyPackageEntry dict")
305
+ }
306
+ let deviceId = try decodeDeviceId(dict["deviceId"] ?? dict["device_id"])
307
+ let keyPackage = try decodeBytesOrThrow(
308
+ dict["keyPackage"] ?? dict["key_package"],
309
+ field: "keyPackage"
310
+ )
311
+ return KeyPackageEntry(deviceId: deviceId, keyPackage: keyPackage)
312
+ }
313
+
314
+ static func decodeKeyPackageEntryArray(_ value: Any?) throws -> [KeyPackageEntry] {
315
+ guard let arr = value as? [Any] else {
316
+ throw BridgeError.decodeFailure("expected KeyPackageEntry array")
317
+ }
318
+ return try arr.map { try decodeKeyPackageEntry($0) }
319
+ }
320
+
321
+ // MARK: - [CR-13] CatchupAppEvent
322
+
323
+ /// Decode `{ conversationId: [Int], appEventBytes: [Int] }` into UniFFI's
324
+ /// `CatchupAppEvent`. Used by `buildLinkingTicketNative`.
325
+ static func decodeCatchupAppEvent(_ value: Any?) throws -> CatchupAppEvent {
326
+ guard let dict = value as? [String: Any] else {
327
+ throw BridgeError.decodeFailure("expected CatchupAppEvent dict")
328
+ }
329
+ let convId = try decodeConversationId(dict["conversationId"] ?? dict["conversation_id"])
330
+ let bytes = try decodeBytesOrThrow(
331
+ dict["appEventBytes"] ?? dict["app_event_bytes"],
332
+ field: "appEventBytes"
333
+ )
334
+ return CatchupAppEvent(conversationId: convId, appEventBytes: bytes)
335
+ }
336
+
337
+ static func decodeCatchupAppEventArray(_ value: Any?) throws -> [CatchupAppEvent] {
338
+ guard let arr = value as? [Any] else {
339
+ throw BridgeError.decodeFailure("expected CatchupAppEvent array")
340
+ }
341
+ return try arr.map { try decodeCatchupAppEvent($0) }
342
+ }
297
343
  }
package/ios/pingFFI.h CHANGED
@@ -374,17 +374,17 @@ void uniffi_ping_ffi_fn_free_messagingclient(void*_Nonnull ptr, RustCallStatus *
374
374
  #endif
375
375
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_CONSTRUCTOR_MESSAGINGCLIENT_INIT
376
376
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_CONSTRUCTOR_MESSAGINGCLIENT_INIT
377
- uint64_t uniffi_ping_ffi_fn_constructor_messagingclient_init(RustBuffer identity_export, RustBuffer device_label, void*_Nonnull storage, void*_Nonnull transport, uint64_t now_ms
377
+ uint64_t uniffi_ping_ffi_fn_constructor_messagingclient_init(RustBuffer identity_export, RustBuffer device_label, void*_Nonnull storage, void*_Nonnull transport, uint64_t now_ms, RustBuffer sqlite_path, RustBuffer sqlite_encryption_key
378
378
  );
379
379
  #endif
380
380
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_ADD_MEMBERS
381
381
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_ADD_MEMBERS
382
- uint64_t uniffi_ping_ffi_fn_method_messagingclient_add_members(void*_Nonnull ptr, RustBuffer conversation_id, RustBuffer key_packages, uint64_t now_ms
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
385
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_BUILD_LINKING_TICKET
386
386
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_BUILD_LINKING_TICKET
387
- uint64_t uniffi_ping_ffi_fn_method_messagingclient_build_linking_ticket(void*_Nonnull ptr, RustBuffer new_device_id, RustBuffer new_device_kp, uint64_t now_ms
387
+ 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
388
388
  );
389
389
  #endif
390
390
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_CONSUME_LINKING_TICKET
@@ -407,11 +407,26 @@ RustBuffer uniffi_ping_ffi_fn_method_messagingclient_device_id(void*_Nonnull ptr
407
407
  RustBuffer uniffi_ping_ffi_fn_method_messagingclient_device_info(void*_Nonnull ptr, uint64_t now_ms, RustCallStatus *_Nonnull out_status
408
408
  );
409
409
  #endif
410
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_EXPORT_CONVERSATION_SECRET
411
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_EXPORT_CONVERSATION_SECRET
412
+ RustBuffer uniffi_ping_ffi_fn_method_messagingclient_export_conversation_secret(void*_Nonnull ptr, RustBuffer conversation_id, RustBuffer label, RustBuffer context, uint32_t length, RustCallStatus *_Nonnull out_status
413
+ );
414
+ #endif
415
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_EXPORT_CONVERSATION_STATE_SNAPSHOT
416
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_EXPORT_CONVERSATION_STATE_SNAPSHOT
417
+ RustBuffer uniffi_ping_ffi_fn_method_messagingclient_export_conversation_state_snapshot(void*_Nonnull ptr, RustBuffer conversation_id, uint64_t now_ms, RustCallStatus *_Nonnull out_status
418
+ );
419
+ #endif
410
420
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_FRESH_KEY_PACKAGE
411
421
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_FRESH_KEY_PACKAGE
412
422
  RustBuffer uniffi_ping_ffi_fn_method_messagingclient_fresh_key_package(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status
413
423
  );
414
424
  #endif
425
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_IMPORT_STATE_SNAPSHOT
426
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_IMPORT_STATE_SNAPSHOT
427
+ uint64_t uniffi_ping_ffi_fn_method_messagingclient_import_state_snapshot(void*_Nonnull ptr, RustBuffer snapshot_bytes, uint64_t now_ms
428
+ );
429
+ #endif
415
430
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_JOIN_CONVERSATION
416
431
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_METHOD_MESSAGINGCLIENT_JOIN_CONVERSATION
417
432
  uint64_t uniffi_ping_ffi_fn_method_messagingclient_join_conversation(void*_Nonnull ptr, RustBuffer welcome, uint64_t now_ms
@@ -527,10 +542,25 @@ uint64_t uniffi_ping_ffi_fn_method_transport_fetch_since(void*_Nonnull ptr, Rust
527
542
  uint64_t uniffi_ping_ffi_fn_method_transport_send(void*_Nonnull ptr, RustBuffer envelope
528
543
  );
529
544
  #endif
545
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_FUNC_DECODE_CATCHUP_SNAPSHOT
546
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_FUNC_DECODE_CATCHUP_SNAPSHOT
547
+ RustBuffer uniffi_ping_ffi_fn_func_decode_catchup_snapshot(RustBuffer snapshot_bytes, RustCallStatus *_Nonnull out_status
548
+ );
549
+ #endif
530
550
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_FUNC_GENERATE_IDENTITY_EXPORT
531
551
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_FUNC_GENERATE_IDENTITY_EXPORT
532
552
  RustBuffer uniffi_ping_ffi_fn_func_generate_identity_export(RustCallStatus *_Nonnull out_status
533
553
 
554
+ );
555
+ #endif
556
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_FUNC_OPEN_LINKING_TICKET
557
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_FUNC_OPEN_LINKING_TICKET
558
+ RustBuffer uniffi_ping_ffi_fn_func_open_linking_ticket(RustBuffer sealed, RustBuffer new_device_priv, RustCallStatus *_Nonnull out_status
559
+ );
560
+ #endif
561
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_FUNC_SEAL_LINKING_TICKET
562
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_FN_FUNC_SEAL_LINKING_TICKET
563
+ RustBuffer uniffi_ping_ffi_fn_func_seal_linking_ticket(RustBuffer ticket, RustBuffer new_device_pub, RustCallStatus *_Nonnull out_status
534
564
  );
535
565
  #endif
536
566
  #ifndef UNIFFI_FFIDEF_FFI_PING_FFI_RUSTBUFFER_ALLOC
@@ -811,12 +841,30 @@ void ffi_ping_ffi_rust_future_free_void(uint64_t handle
811
841
  #ifndef UNIFFI_FFIDEF_FFI_PING_FFI_RUST_FUTURE_COMPLETE_VOID
812
842
  #define UNIFFI_FFIDEF_FFI_PING_FFI_RUST_FUTURE_COMPLETE_VOID
813
843
  void ffi_ping_ffi_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
844
+ );
845
+ #endif
846
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_FUNC_DECODE_CATCHUP_SNAPSHOT
847
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_FUNC_DECODE_CATCHUP_SNAPSHOT
848
+ uint16_t uniffi_ping_ffi_checksum_func_decode_catchup_snapshot(void
849
+
814
850
  );
815
851
  #endif
816
852
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_FUNC_GENERATE_IDENTITY_EXPORT
817
853
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_FUNC_GENERATE_IDENTITY_EXPORT
818
854
  uint16_t uniffi_ping_ffi_checksum_func_generate_identity_export(void
819
855
 
856
+ );
857
+ #endif
858
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_FUNC_OPEN_LINKING_TICKET
859
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_FUNC_OPEN_LINKING_TICKET
860
+ uint16_t uniffi_ping_ffi_checksum_func_open_linking_ticket(void
861
+
862
+ );
863
+ #endif
864
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_FUNC_SEAL_LINKING_TICKET
865
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_FUNC_SEAL_LINKING_TICKET
866
+ uint16_t uniffi_ping_ffi_checksum_func_seal_linking_ticket(void
867
+
820
868
  );
821
869
  #endif
822
870
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGEOBSERVER_ON_APPLICATION_MESSAGE
@@ -865,12 +913,30 @@ uint16_t uniffi_ping_ffi_checksum_method_messagingclient_device_id(void
865
913
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_DEVICE_INFO
866
914
  uint16_t uniffi_ping_ffi_checksum_method_messagingclient_device_info(void
867
915
 
916
+ );
917
+ #endif
918
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_EXPORT_CONVERSATION_SECRET
919
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_EXPORT_CONVERSATION_SECRET
920
+ uint16_t uniffi_ping_ffi_checksum_method_messagingclient_export_conversation_secret(void
921
+
922
+ );
923
+ #endif
924
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_EXPORT_CONVERSATION_STATE_SNAPSHOT
925
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_EXPORT_CONVERSATION_STATE_SNAPSHOT
926
+ uint16_t uniffi_ping_ffi_checksum_method_messagingclient_export_conversation_state_snapshot(void
927
+
868
928
  );
869
929
  #endif
870
930
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_FRESH_KEY_PACKAGE
871
931
  #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_FRESH_KEY_PACKAGE
872
932
  uint16_t uniffi_ping_ffi_checksum_method_messagingclient_fresh_key_package(void
873
933
 
934
+ );
935
+ #endif
936
+ #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_IMPORT_STATE_SNAPSHOT
937
+ #define UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_IMPORT_STATE_SNAPSHOT
938
+ uint16_t uniffi_ping_ffi_checksum_method_messagingclient_import_state_snapshot(void
939
+
874
940
  );
875
941
  #endif
876
942
  #ifndef UNIFFI_FFIDEF_UNIFFI_PING_FFI_CHECKSUM_METHOD_MESSAGINGCLIENT_JOIN_CONVERSATION
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ping-openmls-sdk-react-native-macos",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
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",
@@ -24,6 +24,31 @@ export interface ClientConfig {
24
24
  storage: Storage;
25
25
  /** Caller's transport backend. Wired into the native bridge during `init`. */
26
26
  transport: Transport;
27
+ /**
28
+ * [CR-4] Absolute path to a host-managed SQLite file the SDK creates and owns
29
+ * for persistent MLS state. Pass when the host needs cold-restart decryption
30
+ * (iOS NSE, Android Service push wake, similar). The parent directory MUST exist.
31
+ * When omitted, the SDK uses the in-memory provider — fine for tests, not for
32
+ * NSE-style flows.
33
+ */
34
+ sqlitePath?: string;
35
+ /**
36
+ * [CR-4] 32-byte SQLCipher key. Ignored when `sqlitePath` is omitted. Source
37
+ * from your OS keyring (Keychain on iOS, Keystore on Android, etc.).
38
+ */
39
+ sqliteEncryptionKey?: Uint8Array;
40
+ }
41
+
42
+ /** [CR-2] One `(deviceId, keyPackage)` pair for `Conversation.addMembers`. */
43
+ export interface KeyPackageEntry {
44
+ deviceId: Uint8Array;
45
+ keyPackage: Uint8Array;
46
+ }
47
+
48
+ /** [CR-13] One `(conversationId, lastAppEventBytes)` pair for `buildLinkingTicket`. */
49
+ export interface CatchupAppEvent {
50
+ conversationId: Uint8Array;
51
+ appEventBytes: Uint8Array;
27
52
  }
28
53
 
29
54
  /**
@@ -57,7 +82,16 @@ export class MessagingClient {
57
82
 
58
83
  try {
59
84
  const identityB64 = bytesToBase64(cfg.identityExport);
60
- await NativePing.initClient(identityB64, cfg.deviceLabel, Date.now());
85
+ const sqliteKeyB64 = cfg.sqliteEncryptionKey
86
+ ? bytesToBase64(cfg.sqliteEncryptionKey)
87
+ : null;
88
+ await NativePing.initClient(
89
+ identityB64,
90
+ cfg.deviceLabel,
91
+ Date.now(),
92
+ cfg.sqlitePath ?? null,
93
+ sqliteKeyB64,
94
+ );
61
95
  } catch (e) {
62
96
  // Init failed — disconnect the bridges so we don't leak event listeners.
63
97
  disconnectStorage();
@@ -264,14 +298,22 @@ export class MessagingClient {
264
298
  * KeyPackage (`newDeviceKp`); this builds an HPKE-wrapped Welcome to the user's
265
299
  * DeviceGroup plus a catch-up snapshot. The new device consumes via
266
300
  * `consumeLinkingTicket`.
301
+ *
302
+ * [CR-13] `lastAppEvents` is host-supplied "what you missed" data the new device
303
+ * will render before sync catches up. Pass `[]` to suppress catchup data.
267
304
  */
268
305
  async buildLinkingTicket(
269
306
  newDeviceId: Uint8Array,
270
307
  newDeviceKp: Uint8Array,
308
+ lastAppEvents: CatchupAppEvent[] = [],
271
309
  ): Promise<LinkingTicket> {
272
310
  const raw = await NativePing.buildLinkingTicket(
273
311
  Array.from(newDeviceId),
274
312
  Array.from(newDeviceKp),
313
+ lastAppEvents.map((e) => ({
314
+ conversationId: Array.from(e.conversationId),
315
+ appEventBytes: Array.from(e.appEventBytes),
316
+ })),
275
317
  Date.now(),
276
318
  );
277
319
  return decodeLinkingTicket(raw);
@@ -282,9 +324,67 @@ export class MessagingClient {
282
324
  await NativePing.consumeLinkingTicket(encodeLinkingTicket(ticket), Date.now());
283
325
  }
284
326
 
285
- /** Revoke a device — issues Remove proposals across the user's groups. */
286
- async revokeDevice(deviceId: Uint8Array): Promise<void> {
287
- await NativePing.revokeDevice(Array.from(deviceId), Date.now());
327
+ /**
328
+ * Revoke a device ([CR-2]).
329
+ *
330
+ * Returns one Commit envelope per conversation the device was a locally-known
331
+ * leaf in. The SDK has already broadcast each via `transport.send`; the returned
332
+ * list is for any additional host-side handling. Empty array means the device
333
+ * wasn't locally known anywhere (CR-2 scope limit).
334
+ */
335
+ async revokeDevice(deviceId: Uint8Array): Promise<Array<Record<string, unknown>>> {
336
+ return await NativePing.revokeDevice(Array.from(deviceId), Date.now());
337
+ }
338
+
339
+ /**
340
+ * Export a derived secret from a conversation's MLS exporter ([CR-8]).
341
+ *
342
+ * Used to seed the ephemeral channel, call-media keys, and call-ephemeral
343
+ * framer keys. Returned bytes are a secret — never log; clear the typed array
344
+ * (`u8.fill(0)`) after use.
345
+ */
346
+ async exportConversationSecret(
347
+ conversation: Uint8Array,
348
+ label: string,
349
+ context: Uint8Array,
350
+ length: number,
351
+ ): Promise<Uint8Array> {
352
+ const arr = await NativePing.exportConversationSecret(
353
+ Array.from(conversation),
354
+ label,
355
+ Array.from(context),
356
+ length,
357
+ );
358
+ return Uint8Array.from(arr);
359
+ }
360
+
361
+ /**
362
+ * Export a portable MLS state snapshot for one conversation ([CR-7]).
363
+ *
364
+ * Bytes can be embedded in a recovery blob or shipped through a linking ticket
365
+ * so another device of the same user identity can re-attach via
366
+ * [importStateSnapshot]. Contains past epoch secrets — treat as a secret.
367
+ */
368
+ async exportConversationStateSnapshot(
369
+ conversation: Uint8Array,
370
+ ): Promise<Uint8Array> {
371
+ const arr = await NativePing.exportConversationStateSnapshot(
372
+ Array.from(conversation),
373
+ Date.now(),
374
+ );
375
+ return Uint8Array.from(arr);
376
+ }
377
+
378
+ /**
379
+ * Import a `GroupStateSnapshot` from another device of the same user identity
380
+ * ([CR-7]). On success the conversation appears in [listConversations].
381
+ */
382
+ async importStateSnapshot(snapshotBytes: Uint8Array): Promise<Uint8Array> {
383
+ const arr = await NativePing.importStateSnapshot(
384
+ Array.from(snapshotBytes),
385
+ Date.now(),
386
+ );
387
+ return Uint8Array.from(arr);
288
388
  }
289
389
 
290
390
  /** Detach storage + transport + observer listeners. Call when you're done. */
@@ -423,11 +523,21 @@ export class Conversation {
423
523
  );
424
524
  }
425
525
 
426
- /** Add members by their KeyPackage bytes (typically obtained via the relay's directory). */
427
- async addMembers(keyPackages: Uint8Array[]): Promise<void> {
526
+ /**
527
+ * Add members by `(deviceId, keyPackage)` pairs ([CR-2]).
528
+ *
529
+ * Hosts typically pull these straight from `transport.discoverDevices(userId)`.
530
+ * The pairing lets the SDK persist a per-conversation device→leaf map so
531
+ * `MessagingClient.revokeDevice` can later locate the leaves without a fresh
532
+ * directory lookup.
533
+ */
534
+ async addMembers(entries: KeyPackageEntry[]): Promise<void> {
428
535
  await NativePing.addMembers(
429
536
  Array.from(this.id),
430
- keyPackages.map((kp) => Array.from(kp)),
537
+ entries.map((e) => ({
538
+ deviceId: Array.from(e.deviceId),
539
+ keyPackage: Array.from(e.keyPackage),
540
+ })),
431
541
  Date.now(),
432
542
  );
433
543
  }