pulse-updates 1.0.11 → 1.0.13

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.
@@ -339,10 +339,10 @@ final class PulseAppLauncher {
339
339
  // Download file
340
340
  let data = try Data(contentsOf: remoteUrl)
341
341
 
342
- // Verify hash if expected
342
+ // Verify hash if expected (hex, matching the manifest/DB encoding).
343
343
  if let expectedHash = asset.expectedHash {
344
- let actualHash = PulseCrypto.sha256Base64(data)
345
- if actualHash != expectedHash {
344
+ let actualHash = PulseCrypto.sha256Hex(data)
345
+ if actualHash != expectedHash.lowercased() {
346
346
  self.launcherQueue.async {
347
347
  completion(false, PulseLauncherError.hashMismatch(expected: expectedHash, actual: actualHash))
348
348
  }
@@ -578,15 +578,20 @@ struct PulseDefaultSelectionPolicy: PulseSelectionPolicy {
578
578
 
579
579
  /// Select the best update to launch
580
580
  /// Priority: 1) Ready updates (newest by commitTime), 2) Embedded updates (fallback)
581
+ /// Excludes updates that failed to launch and were never confirmed good
582
+ /// (failedLaunchCount > 0 && successfulLaunchCount == 0) so a bad update can't be re-selected.
581
583
  func selectUpdateToLaunch(from updates: [PulseUpdate]) -> PulseUpdate? {
584
+ // Exclude updates that failed to launch and were never confirmed good.
585
+ let healthy = updates.filter { !($0.failedLaunchCount > 0 && $0.successfulLaunchCount == 0) }
586
+
582
587
  // First, try to find the newest ready (downloaded) update
583
- let readyUpdates = updates.filter { $0.status == .ready }
588
+ let readyUpdates = healthy.filter { $0.status == .ready }
584
589
  if let newest = readyUpdates.sorted(by: { $0.commitTime > $1.commitTime }).first {
585
590
  return newest
586
591
  }
587
592
 
588
- // Fall back to embedded update
589
- return updates.first { $0.status == .embedded }
593
+ // Fall back to embedded update (embedded is always launchable).
594
+ return healthy.first { $0.status == .embedded } ?? updates.first { $0.status == .embedded }
590
595
  }
591
596
 
592
597
  /// Select updates to delete - keeps launched update and one backup
@@ -628,12 +633,14 @@ struct PulseDefaultSelectionPolicy: PulseSelectionPolicy {
628
633
  // MARK: - Crypto Helper
629
634
 
630
635
  struct PulseCrypto {
631
- static func sha256Base64(_ data: Data) -> String {
636
+ /// Lowercase hex SHA-256. Manifests and the asset DB store hashes as hex, so all
637
+ /// integrity comparisons must use hex (not base64).
638
+ static func sha256Hex(_ data: Data) -> String {
632
639
  var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
633
640
  data.withUnsafeBytes {
634
641
  _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
635
642
  }
636
- return Data(hash).base64EncodedString()
643
+ return hash.map { String(format: "%02x", $0) }.joined()
637
644
  }
638
645
  }
639
646
 
@@ -4,6 +4,7 @@
4
4
 
5
5
  import Foundation
6
6
  import Network
7
+ import CryptoKit
7
8
 
8
9
  // MARK: - Asset Path Resolution
9
10
 
@@ -110,6 +111,24 @@ public final class PulseController {
110
111
  /// Remote load status for error recovery
111
112
  public private(set) var remoteLoadStatus: PulseRemoteLoadStatus = .idle
112
113
 
114
+ /// Stable, anonymous per-install device id (persisted in the pulse dir). Used to bucket staged
115
+ /// rollouts server-side via the Pulse-Device-Id header and to attribute telemetry events.
116
+ private var _deviceId: String?
117
+ public var deviceId: String {
118
+ if let id = _deviceId { return id }
119
+ let file = directory.appendingPathComponent("device_id")
120
+ if let data = try? Data(contentsOf: file),
121
+ let s = String(data: data, encoding: .utf8) {
122
+ let trimmed = s.trimmingCharacters(in: .whitespacesAndNewlines)
123
+ if !trimmed.isEmpty { _deviceId = trimmed; return trimmed }
124
+ }
125
+ let id = UUID().uuidString
126
+ do { try id.data(using: .utf8)?.write(to: file) }
127
+ catch { pulseLog("deviceId: persist failed (\(error.localizedDescription)); a new id will be used next launch") }
128
+ _deviceId = id
129
+ return id
130
+ }
131
+
113
132
  // MARK: - Private Properties
114
133
 
115
134
  private var database: PulseDatabase?
@@ -164,7 +183,8 @@ public final class PulseController {
164
183
  launchWaitMs: (info?["PulseUpdatesLaunchWaitMs"] as? Int) ?? 0,
165
184
  channel: info?["PulseUpdatesChannel"] as? String,
166
185
  signingKeyId: info?["PulseUpdatesSigningKeyId"] as? String,
167
- signingPublicKey: info?["PulseUpdatesSigningPublicKey"] as? String
186
+ signingPublicKey: info?["PulseUpdatesSigningPublicKey"] as? String,
187
+ requireSignature: (info?["PulseUpdatesRequireSignature"] as? Bool) ?? PulseUpdatesConfig.defaultRequireSignature
168
188
  )
169
189
  }
170
190
 
@@ -229,6 +249,20 @@ public final class PulseController {
229
249
 
230
250
  // Synchronously select the best update for launch
231
251
  selectBestUpdateSync()
252
+
253
+ // Wire crash-recovery on the getBundleURL launch path. This path never calls start(), so
254
+ // without this the RCT fatal handler / RCTContentDidAppear observer would never install and
255
+ // failed/successful launch recording (and rollback) would never run.
256
+ if errorRecovery == nil {
257
+ errorRecovery = PulseErrorRecovery()
258
+ errorRecovery?.delegate = self
259
+ errorRecovery?.startMonitoring()
260
+ }
261
+
262
+ // Seed launch-in-progress tracking for the selected update before the first root view renders.
263
+ if let updateId = launchedUpdate?.updateId {
264
+ try? database?.recordLaunchStart(updateId: updateId)
265
+ }
232
266
  }
233
267
 
234
268
  /// Synchronously select the best update (used for initial launch)
@@ -268,6 +302,16 @@ public final class PulseController {
268
302
  let exists = FileManager.default.fileExists(atPath: bundlePath.path)
269
303
  pulseLog("selectBestUpdateSync: bundlePath exists=\(exists)")
270
304
 
305
+ // Re-verify the on-disk bundle against its content-addressed hash before handing it
306
+ // to React Native. File existence alone is not enough — a truncated/tampered/partial
307
+ // bundle must not be launched.
308
+ if exists, !bundleFileMatchesHash(at: bundlePath, expectedHash: bundleHash) {
309
+ pulseLogError("selectBestUpdateSync: bundle hash mismatch for \(selectedUpdate.updateId), marking failed and falling back to embedded")
310
+ try? database.setStatus(.failed, forUpdateId: selectedUpdate.updateId)
311
+ launchEmbedded()
312
+ return
313
+ }
314
+
271
315
  if exists {
272
316
  launchAssetUrl = bundlePath
273
317
  isEmbeddedLaunch = false
@@ -303,6 +347,22 @@ public final class PulseController {
303
347
  }
304
348
  }
305
349
 
350
+ /// Re-hash an on-disk bundle file and compare against its expected content-addressed hash
351
+ /// (lowercased hex SHA-256). Returns true only when the file exists and its digest matches.
352
+ private func bundleFileMatchesHash(at fileUrl: URL, expectedHash: String) -> Bool {
353
+ guard let handle = try? FileHandle(forReadingFrom: fileUrl) else { return false }
354
+ defer { try? handle.close() }
355
+
356
+ var hasher = SHA256()
357
+ while true {
358
+ let chunk = try? handle.read(upToCount: 1024 * 1024)
359
+ guard let data = chunk, !data.isEmpty else { break }
360
+ hasher.update(data: data)
361
+ }
362
+ let actual = hasher.finalize().map { String(format: "%02x", $0) }.joined()
363
+ return actual == expectedHash.lowercased()
364
+ }
365
+
306
366
  /// Build localAssets map for an OTA update (downloaded + embedded fallbacks)
307
367
  /// Store BOTH hash AND path-based keys for maximum compatibility:
308
368
  /// - hash: for SHA256 hash lookup (how expo-asset looks up)
@@ -570,6 +630,7 @@ public final class PulseController {
570
630
  }
571
631
 
572
632
  self.lastCheckManifest = otaManifest
633
+ self.reportEvent("check", updateId: otaManifest.updateId)
573
634
  pulseLog("checkForUpdate: cached manifest for fetch (ota=\(otaCommitTime) > embedded=\(embeddedCommitTime))")
574
635
  }
575
636
  completion(result)
@@ -628,6 +689,12 @@ public final class PulseController {
628
689
  self.pendingFetchCallbacks.removeAll()
629
690
  self.fetchLock.unlock()
630
691
 
692
+ // A freshly downloaded update marks this device "served" on the server, which is what
693
+ // gates its launch outcomes into the crash-rate auto-rollback.
694
+ if case .success(let fetchResult) = result, fetchResult.isNew {
695
+ self.reportEvent("download", updateId: fetchResult.manifest?.updateId)
696
+ }
697
+
631
698
  // Call original completion
632
699
  completion(result)
633
700
 
@@ -654,12 +721,44 @@ public final class PulseController {
654
721
  public func markAppReady() {
655
722
  if let updateId = launchedUpdate?.updateId {
656
723
  try? database?.recordSuccessfulLaunch(updateId: updateId)
724
+ if !isEmbeddedLaunch { reportEvent("launch_success", updateId: updateId) }
657
725
  }
658
726
 
659
727
  // Run reaper after successful launch
660
728
  runReaper()
661
729
  }
662
730
 
731
+ /// Fire-and-forget device telemetry to the server's /pulse/events endpoint (async URLSession, so
732
+ /// it never blocks). Best-effort: failures are logged, never silently swallowed, and never affect
733
+ /// the app. The server marks a device "served" on the "download" event — only served devices count
734
+ /// toward crash-rate auto-rollback — and uses launch_success/launch_failure to drive that rate.
735
+ /// appSlug + base are derived from the manifest URL (.../pulse/manifest/{appSlug}).
736
+ private func reportEvent(_ type: String, updateId: String?) {
737
+ guard let updateId = updateId, !updateId.isEmpty, let config = config else { return }
738
+ let marker = "/pulse/manifest/"
739
+ guard let range = config.updateUrl.range(of: marker) else {
740
+ pulseLog("reportEvent(\(type)): updateUrl '\(config.updateUrl)' has no \(marker) — skipping")
741
+ return
742
+ }
743
+ let base = String(config.updateUrl[..<range.lowerBound])
744
+ let appSlug = String(config.updateUrl[range.upperBound...].prefix(while: { $0 != "/" && $0 != "?" }))
745
+ guard let eventsUrl = URL(string: "\(base)/pulse/events") else { return }
746
+ let body: [String: Any] = ["appSlug": appSlug, "updateId": updateId, "deviceId": deviceId, "type": type]
747
+ guard let httpBody = try? JSONSerialization.data(withJSONObject: body) else { return }
748
+ var req = URLRequest(url: eventsUrl)
749
+ req.httpMethod = "POST"
750
+ req.setValue("application/json", forHTTPHeaderField: "Content-Type")
751
+ req.timeoutInterval = 5
752
+ req.httpBody = httpBody
753
+ URLSession.shared.dataTask(with: req) { _, response, error in
754
+ if let error = error {
755
+ pulseLog("reportEvent(\(type)) failed: \(error.localizedDescription)")
756
+ } else if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
757
+ pulseLog("reportEvent(\(type)) -> HTTP \(http.statusCode)")
758
+ }
759
+ }.resume()
760
+ }
761
+
663
762
  /// Report a fatal error for error recovery handling
664
763
  public func reportError(_ error: NSError) {
665
764
  errorRecovery?.handle(error: error)
@@ -869,7 +968,30 @@ public final class PulseController {
869
968
 
870
969
  private func rollbackToPreviousUpdate() {
871
970
  pulseLog("Rolling back to previous update")
872
- // Implementation depends on error recovery strategy
971
+
972
+ // Mark the currently launched update as failed so the selection policy excludes it
973
+ // (failedLaunchCount > 0 && successfulLaunchCount == 0) on the next selection.
974
+ if let updateId = launchedUpdate?.updateId, !(launchedUpdate?.isEmbedded ?? true) {
975
+ try? database?.recordFailedLaunch(updateId: updateId)
976
+ try? database?.setStatus(.failed, forUpdateId: updateId)
977
+ reportEvent("launch_failure", updateId: updateId)
978
+ pulseLog("rollbackToPreviousUpdate: marked \(updateId) failed")
979
+ }
980
+
981
+ // Clear the current selection and re-select the best remaining update (previous-good, else embedded).
982
+ launchAssetUrl = nil
983
+ launchedUpdate = nil
984
+ launchedManifestJson = nil
985
+ isEmbeddedLaunch = true
986
+ localAssets = [:]
987
+
988
+ selectBestUpdateSync()
989
+ if launchAssetUrl == nil {
990
+ launchEmbedded()
991
+ }
992
+
993
+ // Ask React Native to reload with the rolled-back bundle.
994
+ NotificationCenter.default.post(name: NSNotification.Name("PulseUpdatesReloadRequest"), object: nil)
873
995
  }
874
996
 
875
997
  private func runReaper() {
@@ -1061,8 +1183,31 @@ final class PulseRemoteLoader {
1061
1183
  return
1062
1184
  }
1063
1185
 
1064
- // Verify signature if required
1065
- if config.signingPublicKey != nil {
1186
+ // Signature gate (fail-closed in release).
1187
+ // If signatures are required but no key material is configured, or the
1188
+ // manifest is unsigned, REFUSE the update so the app keeps running the
1189
+ // embedded/known-good bundle instead of applying an unverified one.
1190
+ let hasKeyMaterial = config.signingPublicKey != nil && config.signingKeyId != nil
1191
+ if config.requireSignature {
1192
+ guard hasKeyMaterial else {
1193
+ pulseLogError("REFUSING remote update \(manifest.updateId): requireSignature is enabled but signingPublicKey/signingKeyId are not configured. Falling back to the embedded bundle. Configure PulseUpdatesSigningPublicKey and PulseUpdatesSigningKeyId, or set PulseUpdatesRequireSignature=NO to intentionally allow unsigned updates.")
1194
+ completion(.failure(PulseUpdatesError.signatureRequiredButMissing))
1195
+ return
1196
+ }
1197
+ guard manifest.signature != nil else {
1198
+ pulseLogError("REFUSING remote update \(manifest.updateId): requireSignature is enabled but the manifest carries no signature. Falling back to the embedded bundle.")
1199
+ completion(.failure(PulseUpdatesError.signatureRequiredButMissing))
1200
+ return
1201
+ }
1202
+ guard verifyManifestSignature(manifest: manifest, manifestJson: data, config: config) else {
1203
+ completion(.failure(PulseUpdatesError.signatureVerificationFailed))
1204
+ return
1205
+ }
1206
+ } else if hasKeyMaterial, manifest.signature != nil {
1207
+ // Signatures not strictly required (e.g. DEBUG), but key material is
1208
+ // present, so still verify WHEN a signature is provided. An unsigned
1209
+ // manifest is allowed through here (verifyManifestSignature would reject
1210
+ // a nil signature, which would wrongly block unsigned DEBUG updates).
1066
1211
  guard verifyManifestSignature(manifest: manifest, manifestJson: data, config: config) else {
1067
1212
  completion(.failure(PulseUpdatesError.signatureVerificationFailed))
1068
1213
  return
@@ -1141,6 +1286,9 @@ final class PulseRemoteLoader {
1141
1286
  request.setValue("2", forHTTPHeaderField: "Pulse-Protocol-Version")
1142
1287
  request.setValue("ios", forHTTPHeaderField: "X-Pulse-Platform")
1143
1288
  request.setValue(config.runtimeVersion, forHTTPHeaderField: "X-Pulse-Runtime-Version")
1289
+ // Stable device id so the server can bucket this device into staged rollouts (without it,
1290
+ // a device is held back from any partial rollout until it reaches 100%).
1291
+ request.setValue(PulseController.shared.deviceId, forHTTPHeaderField: "Pulse-Device-Id")
1144
1292
 
1145
1293
  if let channel = config.channel {
1146
1294
  request.setValue(channel, forHTTPHeaderField: "X-Pulse-Channel-Name")
@@ -1482,8 +1630,14 @@ final class PulseRemoteLoader {
1482
1630
  case "\r": result += "\\r"
1483
1631
  case "\t": result += "\\t"
1484
1632
  default:
1485
- if char.asciiValue ?? 0 < 32 {
1486
- result += String(format: "\\u%04x", char.asciiValue ?? 0)
1633
+ // Only real control characters are \u-escaped. `asciiValue` is nil for every
1634
+ // non-ASCII character, so `?? 0` collapsed each of them to 0 and rewrote it
1635
+ // as \u0000: the canonical form no longer matched what the server signed and
1636
+ // the manifest was refused ("Signature verification failed"). A single em
1637
+ // dash in a release message was enough to block an app's updates entirely.
1638
+ // Android's escapeJson never had this — it tests char.code < 32.
1639
+ if let ascii = char.asciiValue, ascii < 32 {
1640
+ result += String(format: "\\u%04x", ascii)
1487
1641
  } else {
1488
1642
  result.append(char)
1489
1643
  }
@@ -14,6 +14,10 @@ public struct PulseUpdatesConfig {
14
14
  public let channel: String?
15
15
  public let signingKeyId: String?
16
16
  public let signingPublicKey: String?
17
+ /// When true, remote updates MUST carry a valid Ed25519 signature and the
18
+ /// app MUST be configured with signingKeyId/signingPublicKey, otherwise the
19
+ /// update is refused (fail-closed). Defaults to true in release builds.
20
+ public let requireSignature: Bool
17
21
  public let scopeKey: String
18
22
 
19
23
  public init(
@@ -24,7 +28,8 @@ public struct PulseUpdatesConfig {
24
28
  launchWaitMs: Int = 0,
25
29
  channel: String? = nil,
26
30
  signingKeyId: String? = nil,
27
- signingPublicKey: String? = nil
31
+ signingPublicKey: String? = nil,
32
+ requireSignature: Bool = PulseUpdatesConfig.defaultRequireSignature
28
33
  ) {
29
34
  self.enabled = enabled
30
35
  self.updateUrl = updateUrl
@@ -34,9 +39,21 @@ public struct PulseUpdatesConfig {
34
39
  self.channel = channel
35
40
  self.signingKeyId = signingKeyId
36
41
  self.signingPublicKey = signingPublicKey
42
+ self.requireSignature = requireSignature
37
43
  // Use update URL host as scope key (like Expo)
38
44
  self.scopeKey = URL(string: updateUrl)?.host ?? "default"
39
45
  }
46
+
47
+ /// Default for requireSignature: fail-closed in release, fail-open only in DEBUG.
48
+ /// Callers that don't pass requireSignature (e.g. the JS-driven configure bridge)
49
+ /// inherit this release-secure default automatically.
50
+ public static var defaultRequireSignature: Bool {
51
+ #if DEBUG
52
+ return false
53
+ #else
54
+ return true
55
+ #endif
56
+ }
40
57
  }
41
58
 
42
59
  // MARK: - Manifest Models
@@ -214,6 +231,7 @@ public enum PulseUpdatesError: Error, LocalizedError {
214
231
  case networkError(underlying: Error)
215
232
  case invalidManifest(reason: String)
216
233
  case signatureVerificationFailed
234
+ case signatureRequiredButMissing
217
235
  case hashMismatch(expected: String, actual: String)
218
236
  case downloadFailed(underlying: Error)
219
237
  case noUpdateAvailable
@@ -233,6 +251,8 @@ public enum PulseUpdatesError: Error, LocalizedError {
233
251
  return "Invalid manifest: \(reason)"
234
252
  case .signatureVerificationFailed:
235
253
  return "Signature verification failed"
254
+ case .signatureRequiredButMissing:
255
+ return "Signature required but missing: the SDK is configured to require signed updates but no signing key is configured or the manifest is unsigned"
236
256
  case .hashMismatch(let expected, let actual):
237
257
  return "Hash mismatch: expected \(expected), got \(actual)"
238
258
  case .downloadFailed(let error):
@@ -47,6 +47,16 @@ export interface PulseUpdatesConfig {
47
47
  channel?: string;
48
48
  signingKeyId?: string;
49
49
  signingPublicKey?: string;
50
+ /**
51
+ * When true, manifests must carry a valid Ed25519 signature verifying against
52
+ * `signingPublicKey`; unsigned or unverifiable manifests are rejected (fail-closed).
53
+ *
54
+ * NATIVE-CONFIG ONLY: this is read from the Info.plist / AndroidManifest
55
+ * `PulseUpdatesRequireSignature` meta-data (defaults to `true` in release) and is NOT
56
+ * accepted by `configure()` — setting it here has no effect. Configure signing via the
57
+ * native config so a release can never silently flip to refuse-all from JS.
58
+ */
59
+ requireSignature?: boolean;
50
60
  }
51
61
  export interface PulseUpdatesState {
52
62
  isEnabled: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,CAAC,EAAE,cAAc,CAAC;IAG3B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,aAAa,EAAE,IAAI,GAAG,IAAI,CAAC;IAC3B,eAAe,EAAE,aAAa,GAAG,IAAI,CAAC;IACtC,gBAAgB,EAAE,aAAa,GAAG,IAAI,CAAC;IACvC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC5C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,SAAS,CAAC,EAAE,cAAc,CAAC;IAG3B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,UAAU,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,aAAa,EAAE,IAAI,GAAG,IAAI,CAAC;IAC3B,eAAe,EAAE,aAAa,GAAG,IAAI,CAAC;IACtC,gBAAgB,EAAE,aAAa,GAAG,IAAI,CAAC;IACvC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC5C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-updates",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "OTA updates for React Native - lightweight alternative to expo-updates",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -26,7 +26,10 @@
26
26
  "build": "bob build",
27
27
  "prepare": "bob build",
28
28
  "test": "node --test tests/*.test.js",
29
- "publish-update": "node scripts/publish.js publish"
29
+ "publish-update": "node scripts/publish.mjs publish"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
30
33
  },
31
34
  "keywords": [
32
35
  "react-native",
@@ -36,19 +39,18 @@
36
39
  ],
37
40
  "repository": {
38
41
  "type": "git",
39
- "url": "https://git.spicysparks.com/SpicySparks/Pulse-Client.git"
42
+ "url": "https://github.com/pulse-updates/pulse-updates.git"
40
43
  },
41
- "author": "Lyra",
44
+ "author": "Pulse Updates",
42
45
  "license": "MIT",
43
46
  "peerDependencies": {
44
- "react": "*",
45
- "react-native": "*"
47
+ "react": ">=18.2.0",
48
+ "react-native": ">=0.76.0"
46
49
  },
47
50
  "devDependencies": {
48
51
  "@types/react": "^18.2.0",
49
- "@types/react-native": "^0.72.0",
50
52
  "react": "^18.2.0",
51
- "react-native": "^0.73.0",
53
+ "react-native": "^0.83.0",
52
54
  "react-native-builder-bob": "^0.23.0",
53
55
  "typescript": "^5.0.0"
54
56
  },