ping-openmls-sdk-react-native-macos 0.2.3 → 0.6.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,14 @@ 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:deviceSigningSecretKeyB64: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?,
221
+ deviceSigningSecretKeyB64: String?,
219
222
  resolver resolve: @escaping RCTPromiseResolveBlock,
220
223
  rejecter reject: @escaping RCTPromiseRejectBlock
221
224
  ) {
@@ -225,6 +228,35 @@ public final class PingNative: RCTEventEmitter {
225
228
  reject("InvalidIdentity", "identity bytes failed base64 decode", nil)
226
229
  return
227
230
  }
231
+ // [CR-4] Optional SQLCipher key. Decode if supplied; reject if it
232
+ // decodes but isn't 32 bytes (host bug — better loud than silent).
233
+ var sqliteKey: Data? = nil
234
+ if let keyB64 = sqliteEncryptionKeyB64,
235
+ let keyData = Data(base64Encoded: keyB64) {
236
+ guard keyData.count == 32 else {
237
+ reject("InvalidSqliteKey", "sqliteEncryptionKey must be 32 bytes, got \(keyData.count)", nil)
238
+ return
239
+ }
240
+ sqliteKey = keyData
241
+ }
242
+ // Optional Ed25519 device signing secret. Same 32-byte
243
+ // shape as the SQLCipher key; loud-reject on mismatched
244
+ // length so callers find the bug at init time rather
245
+ // than via a server-side `sender_device_mismatch`
246
+ // hours later.
247
+ var deviceSigningSecret: Data? = nil
248
+ if let secretB64 = deviceSigningSecretKeyB64,
249
+ let secretData = Data(base64Encoded: secretB64) {
250
+ guard secretData.count == 32 else {
251
+ reject(
252
+ "InvalidDeviceSigningKey",
253
+ "deviceSigningSecretKey must be 32 bytes, got \(secretData.count)",
254
+ nil
255
+ )
256
+ return
257
+ }
258
+ deviceSigningSecret = secretData
259
+ }
228
260
  // UniFFI's `[Name=init]` constructor generates a static factory named
229
261
  // `init` (backquoted because Swift reserves the bare identifier for
230
262
  // designated initializers).
@@ -233,7 +265,10 @@ public final class PingNative: RCTEventEmitter {
233
265
  deviceLabel: deviceLabel,
234
266
  storage: self.storageBridge,
235
267
  transport: self.transportBridge,
236
- nowMs: UInt64(nowMs)
268
+ nowMs: UInt64(nowMs),
269
+ sqlitePath: sqlitePath,
270
+ sqliteEncryptionKey: sqliteKey,
271
+ deviceSigningSecretKey: deviceSigningSecret
237
272
  )
238
273
  self.client = c
239
274
  resolve(true)
@@ -373,12 +408,13 @@ public final class PingNative: RCTEventEmitter {
373
408
  }
374
409
  }
375
410
 
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:)
411
+ /// Add members ([CR-2]). `entries` is an array of `{ deviceId: [Int], keyPackage:
412
+ /// [Int] }` dicts the device-id pairing lets the SDK persist a per-conversation
413
+ /// device→leaf map that `revokeDeviceNative` later consumes.
414
+ @objc(addMembers:entries:nowMs:resolver:rejecter:)
379
415
  public func addMembersNative(
380
416
  _ conversationIdBytes: NSArray,
381
- keyPackages keyPackagesArr: NSArray,
417
+ entries entriesArr: NSArray,
382
418
  nowMs: Double,
383
419
  resolver resolve: @escaping RCTPromiseResolveBlock,
384
420
  rejecter reject: @escaping RCTPromiseRejectBlock
@@ -390,13 +426,10 @@ public final class PingNative: RCTEventEmitter {
390
426
  }
391
427
  do {
392
428
  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
- }
429
+ let entries = try TypeBridge.decodeKeyPackageEntryArray(entriesArr)
397
430
  try await client.addMembers(
398
431
  conversationId: convId,
399
- keyPackages: kps,
432
+ entries: entries,
400
433
  nowMs: UInt64(nowMs)
401
434
  )
402
435
  resolve(NSNull())
@@ -555,10 +588,15 @@ public final class PingNative: RCTEventEmitter {
555
588
  /// Build a linking ticket on the existing device (E) for a new device (N) that
556
589
  /// just published its KeyPackage. The ticket bundles a Welcome to E's DeviceGroup
557
590
  /// plus a catch-up snapshot. N consumes the ticket via `consumeLinkingTicket`.
558
- @objc(buildLinkingTicket:newDeviceKp:nowMs:resolver:rejecter:)
591
+ /// Build a linking ticket ([CR-13] populates `catchup_snapshot` from
592
+ /// `lastAppEvents`). `lastAppEvents` is an array of `{ conversationId: [Int],
593
+ /// appEventBytes: [Int] }` — host-supplied "what you missed" data the new
594
+ /// device renders before sync catches up. Empty array suppresses catchup.
595
+ @objc(buildLinkingTicket:newDeviceKp:lastAppEvents:nowMs:resolver:rejecter:)
559
596
  public func buildLinkingTicketNative(
560
597
  _ newDeviceIdBytes: NSArray,
561
598
  newDeviceKp newDeviceKpBytes: NSArray,
599
+ lastAppEvents lastAppEventsArr: NSArray,
562
600
  nowMs: Double,
563
601
  resolver resolve: @escaping RCTPromiseResolveBlock,
564
602
  rejecter reject: @escaping RCTPromiseRejectBlock
@@ -571,9 +609,11 @@ public final class PingNative: RCTEventEmitter {
571
609
  do {
572
610
  let newDeviceId = try TypeBridge.decodeDeviceId(newDeviceIdBytes)
573
611
  let newKp = try TypeBridge.decodeBytesOrThrow(newDeviceKpBytes, field: "newDeviceKp")
612
+ let events = try TypeBridge.decodeCatchupAppEventArray(lastAppEventsArr)
574
613
  let ticket = try await client.buildLinkingTicket(
575
614
  newDeviceId: newDeviceId,
576
615
  newDeviceKp: newKp,
616
+ lastAppEvents: events,
577
617
  nowMs: UInt64(nowMs)
578
618
  )
579
619
  resolve(TypeBridge.encodeLinkingTicket(ticket))
@@ -610,9 +650,9 @@ public final class PingNative: RCTEventEmitter {
610
650
  }
611
651
  }
612
652
 
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).
653
+ /// Revoke a device ([CR-2]). Returns the array of Commit envelopes the SDK
654
+ /// produced — one per conversation the device was a locally-known leaf in.
655
+ /// Empty array means the device wasn't locally known (scope limit per CR-2).
616
656
  @objc(revokeDevice:nowMs:resolver:rejecter:)
617
657
  public func revokeDeviceNative(
618
658
  _ deviceIdBytes: NSArray,
@@ -627,14 +667,96 @@ public final class PingNative: RCTEventEmitter {
627
667
  }
628
668
  do {
629
669
  let deviceId = try TypeBridge.decodeDeviceId(deviceIdBytes)
630
- try await client.revokeDevice(deviceId: deviceId, nowMs: UInt64(nowMs))
631
- resolve(NSNull())
670
+ let envs = try await client.revokeDevice(deviceId: deviceId, nowMs: UInt64(nowMs))
671
+ resolve(TypeBridge.encodeEnvelopeArray(envs))
632
672
  } catch {
633
673
  reject("RevokeFailed", String(describing: error), error)
634
674
  }
635
675
  }
636
676
  }
637
677
 
678
+ // MARK: - CR-8 / CR-7 helpers
679
+
680
+ /// Export a derived secret from a conversation's MLS exporter ([CR-8]).
681
+ /// Returned bytes are a secret — the JS side is responsible for wiping them.
682
+ @objc(exportConversationSecret:label:context:length:resolver:rejecter:)
683
+ public func exportConversationSecretNative(
684
+ _ conversationIdBytes: NSArray,
685
+ label: String,
686
+ context contextBytes: NSArray,
687
+ length: NSNumber,
688
+ resolver resolve: @escaping RCTPromiseResolveBlock,
689
+ rejecter reject: @escaping RCTPromiseRejectBlock
690
+ ) {
691
+ guard let client = self.client else {
692
+ reject("NotInitialised", "MessagingClient not initialised", nil)
693
+ return
694
+ }
695
+ do {
696
+ let convId = try TypeBridge.decodeConversationId(conversationIdBytes)
697
+ let context = try TypeBridge.decodeBytesOrThrow(contextBytes, field: "context")
698
+ let bytes = try client.exportConversationSecret(
699
+ conversationId: convId,
700
+ label: label,
701
+ context: context,
702
+ length: length.uint32Value
703
+ )
704
+ resolve(TypeBridge.encodeBytes(bytes))
705
+ } catch {
706
+ reject("ExportSecretFailed", String(describing: error), error)
707
+ }
708
+ }
709
+
710
+ /// Export a `GroupStateSnapshot` for one conversation ([CR-7]).
711
+ @objc(exportConversationStateSnapshot:nowMs:resolver:rejecter:)
712
+ public func exportConversationStateSnapshotNative(
713
+ _ conversationIdBytes: NSArray,
714
+ nowMs: Double,
715
+ resolver resolve: @escaping RCTPromiseResolveBlock,
716
+ rejecter reject: @escaping RCTPromiseRejectBlock
717
+ ) {
718
+ guard let client = self.client else {
719
+ reject("NotInitialised", "MessagingClient not initialised", nil)
720
+ return
721
+ }
722
+ do {
723
+ let convId = try TypeBridge.decodeConversationId(conversationIdBytes)
724
+ let bytes = try client.exportConversationStateSnapshot(
725
+ conversationId: convId,
726
+ nowMs: UInt64(nowMs)
727
+ )
728
+ resolve(TypeBridge.encodeBytes(bytes))
729
+ } catch {
730
+ reject("ExportStateSnapshotFailed", String(describing: error), error)
731
+ }
732
+ }
733
+
734
+ /// Import a `GroupStateSnapshot` from another device ([CR-7]).
735
+ @objc(importStateSnapshot:nowMs:resolver:rejecter:)
736
+ public func importStateSnapshotNative(
737
+ _ snapshotBytes: NSArray,
738
+ nowMs: Double,
739
+ resolver resolve: @escaping RCTPromiseResolveBlock,
740
+ rejecter reject: @escaping RCTPromiseRejectBlock
741
+ ) {
742
+ Task {
743
+ guard let client = self.client else {
744
+ reject("NotInitialised", "MessagingClient not initialised", nil)
745
+ return
746
+ }
747
+ do {
748
+ let bytes = try TypeBridge.decodeBytesOrThrow(snapshotBytes, field: "snapshotBytes")
749
+ let convId = try await client.importStateSnapshot(
750
+ snapshotBytes: bytes,
751
+ nowMs: UInt64(nowMs)
752
+ )
753
+ resolve(TypeBridge.encodeConversationId(convId))
754
+ } catch {
755
+ reject("ImportStateSnapshotFailed", String(describing: error), error)
756
+ }
757
+ }
758
+ }
759
+
638
760
  // MARK: - macOS clipboard helper (stage 5 polish)
639
761
 
640
762
  /// 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, RustBuffer device_signing_secret_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.3",
3
+ "version": "0.6.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,50 @@ 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
+ * Optional 32-byte Ed25519 secret key the SDK adopts as its device
42
+ * signing key on FIRST init. When supplied, `deviceId()` returns
43
+ * `SHA-256(public_key_of(secret))` — deterministic from what the
44
+ * caller passes.
45
+ *
46
+ * Use case: align the SDK's `device_id` (which the SDK stamps into
47
+ * every envelope's `sender_device` field) with an externally-
48
+ * computed device id — typically `SHA-256(device_signing_pubkey)`
49
+ * in the host's auth layer, where the JWT carries that same value
50
+ * as its `device_id` claim. Without this alignment, a server that
51
+ * validates `envelope.sender_device == jwt.device_id` rejects every
52
+ * send with `sender_device_mismatch`.
53
+ *
54
+ * Ignored on re-init when a `LocalDevice` is already persisted in
55
+ * storage — the on-disk identity is authoritative for stability
56
+ * across restarts.
57
+ */
58
+ deviceSigningSecretKey?: Uint8Array;
59
+ }
60
+
61
+ /** [CR-2] One `(deviceId, keyPackage)` pair for `Conversation.addMembers`. */
62
+ export interface KeyPackageEntry {
63
+ deviceId: Uint8Array;
64
+ keyPackage: Uint8Array;
65
+ }
66
+
67
+ /** [CR-13] One `(conversationId, lastAppEventBytes)` pair for `buildLinkingTicket`. */
68
+ export interface CatchupAppEvent {
69
+ conversationId: Uint8Array;
70
+ appEventBytes: Uint8Array;
27
71
  }
28
72
 
29
73
  /**
@@ -57,7 +101,20 @@ export class MessagingClient {
57
101
 
58
102
  try {
59
103
  const identityB64 = bytesToBase64(cfg.identityExport);
60
- await NativePing.initClient(identityB64, cfg.deviceLabel, Date.now());
104
+ const sqliteKeyB64 = cfg.sqliteEncryptionKey
105
+ ? bytesToBase64(cfg.sqliteEncryptionKey)
106
+ : null;
107
+ const signingSecretB64 = cfg.deviceSigningSecretKey
108
+ ? bytesToBase64(cfg.deviceSigningSecretKey)
109
+ : null;
110
+ await NativePing.initClient(
111
+ identityB64,
112
+ cfg.deviceLabel,
113
+ Date.now(),
114
+ cfg.sqlitePath ?? null,
115
+ sqliteKeyB64,
116
+ signingSecretB64,
117
+ );
61
118
  } catch (e) {
62
119
  // Init failed — disconnect the bridges so we don't leak event listeners.
63
120
  disconnectStorage();
@@ -264,14 +321,22 @@ export class MessagingClient {
264
321
  * KeyPackage (`newDeviceKp`); this builds an HPKE-wrapped Welcome to the user's
265
322
  * DeviceGroup plus a catch-up snapshot. The new device consumes via
266
323
  * `consumeLinkingTicket`.
324
+ *
325
+ * [CR-13] `lastAppEvents` is host-supplied "what you missed" data the new device
326
+ * will render before sync catches up. Pass `[]` to suppress catchup data.
267
327
  */
268
328
  async buildLinkingTicket(
269
329
  newDeviceId: Uint8Array,
270
330
  newDeviceKp: Uint8Array,
331
+ lastAppEvents: CatchupAppEvent[] = [],
271
332
  ): Promise<LinkingTicket> {
272
333
  const raw = await NativePing.buildLinkingTicket(
273
334
  Array.from(newDeviceId),
274
335
  Array.from(newDeviceKp),
336
+ lastAppEvents.map((e) => ({
337
+ conversationId: Array.from(e.conversationId),
338
+ appEventBytes: Array.from(e.appEventBytes),
339
+ })),
275
340
  Date.now(),
276
341
  );
277
342
  return decodeLinkingTicket(raw);
@@ -282,9 +347,67 @@ export class MessagingClient {
282
347
  await NativePing.consumeLinkingTicket(encodeLinkingTicket(ticket), Date.now());
283
348
  }
284
349
 
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());
350
+ /**
351
+ * Revoke a device ([CR-2]).
352
+ *
353
+ * Returns one Commit envelope per conversation the device was a locally-known
354
+ * leaf in. The SDK has already broadcast each via `transport.send`; the returned
355
+ * list is for any additional host-side handling. Empty array means the device
356
+ * wasn't locally known anywhere (CR-2 scope limit).
357
+ */
358
+ async revokeDevice(deviceId: Uint8Array): Promise<Array<Record<string, unknown>>> {
359
+ return await NativePing.revokeDevice(Array.from(deviceId), Date.now());
360
+ }
361
+
362
+ /**
363
+ * Export a derived secret from a conversation's MLS exporter ([CR-8]).
364
+ *
365
+ * Used to seed the ephemeral channel, call-media keys, and call-ephemeral
366
+ * framer keys. Returned bytes are a secret — never log; clear the typed array
367
+ * (`u8.fill(0)`) after use.
368
+ */
369
+ async exportConversationSecret(
370
+ conversation: Uint8Array,
371
+ label: string,
372
+ context: Uint8Array,
373
+ length: number,
374
+ ): Promise<Uint8Array> {
375
+ const arr = await NativePing.exportConversationSecret(
376
+ Array.from(conversation),
377
+ label,
378
+ Array.from(context),
379
+ length,
380
+ );
381
+ return Uint8Array.from(arr);
382
+ }
383
+
384
+ /**
385
+ * Export a portable MLS state snapshot for one conversation ([CR-7]).
386
+ *
387
+ * Bytes can be embedded in a recovery blob or shipped through a linking ticket
388
+ * so another device of the same user identity can re-attach via
389
+ * [importStateSnapshot]. Contains past epoch secrets — treat as a secret.
390
+ */
391
+ async exportConversationStateSnapshot(
392
+ conversation: Uint8Array,
393
+ ): Promise<Uint8Array> {
394
+ const arr = await NativePing.exportConversationStateSnapshot(
395
+ Array.from(conversation),
396
+ Date.now(),
397
+ );
398
+ return Uint8Array.from(arr);
399
+ }
400
+
401
+ /**
402
+ * Import a `GroupStateSnapshot` from another device of the same user identity
403
+ * ([CR-7]). On success the conversation appears in [listConversations].
404
+ */
405
+ async importStateSnapshot(snapshotBytes: Uint8Array): Promise<Uint8Array> {
406
+ const arr = await NativePing.importStateSnapshot(
407
+ Array.from(snapshotBytes),
408
+ Date.now(),
409
+ );
410
+ return Uint8Array.from(arr);
288
411
  }
289
412
 
290
413
  /** Detach storage + transport + observer listeners. Call when you're done. */
@@ -423,11 +546,21 @@ export class Conversation {
423
546
  );
424
547
  }
425
548
 
426
- /** Add members by their KeyPackage bytes (typically obtained via the relay's directory). */
427
- async addMembers(keyPackages: Uint8Array[]): Promise<void> {
549
+ /**
550
+ * Add members by `(deviceId, keyPackage)` pairs ([CR-2]).
551
+ *
552
+ * Hosts typically pull these straight from `transport.discoverDevices(userId)`.
553
+ * The pairing lets the SDK persist a per-conversation device→leaf map so
554
+ * `MessagingClient.revokeDevice` can later locate the leaves without a fresh
555
+ * directory lookup.
556
+ */
557
+ async addMembers(entries: KeyPackageEntry[]): Promise<void> {
428
558
  await NativePing.addMembers(
429
559
  Array.from(this.id),
430
- keyPackages.map((kp) => Array.from(kp)),
560
+ entries.map((e) => ({
561
+ deviceId: Array.from(e.deviceId),
562
+ keyPackage: Array.from(e.keyPackage),
563
+ })),
431
564
  Date.now(),
432
565
  );
433
566
  }