react-native-audio-browser 0.2.0-next.2 → 0.2.0-next.21

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.
Files changed (660) hide show
  1. package/README.md +35 -21
  2. package/android/build.gradle +35 -12
  3. package/android/gradle.properties +2 -2
  4. package/android/src/main/AndroidManifest.xml +3 -1
  5. package/android/src/main/java/com/audiobrowser/AudioBrowser.kt +389 -164
  6. package/android/src/main/java/com/audiobrowser/AudioBrowserLifecycleModule.kt +38 -0
  7. package/android/src/main/java/com/audiobrowser/AudioBrowserPackage.kt +18 -2
  8. package/android/src/main/java/com/audiobrowser/ServiceBinding.kt +48 -0
  9. package/android/src/main/java/com/audiobrowser/browser/BrowserManager.kt +315 -213
  10. package/android/src/main/java/com/audiobrowser/browser/BrowserUrlResolution.kt +24 -5
  11. package/android/src/main/java/com/audiobrowser/browser/InertStyleDiagnostic.kt +267 -0
  12. package/android/src/main/java/com/audiobrowser/browser/JsonModels.kt +97 -58
  13. package/android/src/main/java/com/audiobrowser/browser/SectionScope.kt +70 -29
  14. package/android/src/main/java/com/audiobrowser/browser/StyleResolver.kt +60 -0
  15. package/android/src/main/java/com/audiobrowser/extension/ResolvedTrackExt.kt +20 -7
  16. package/android/src/main/java/com/audiobrowser/extension/TappedTrack.kt +20 -0
  17. package/android/src/main/java/com/audiobrowser/extension/TrackIdentity.kt +15 -0
  18. package/android/src/main/java/com/audiobrowser/http/HttpClient.kt +20 -3
  19. package/android/src/main/java/com/audiobrowser/model/IcyTitle.kt +24 -0
  20. package/android/src/main/java/com/audiobrowser/model/PlaybackMetadata.kt +10 -6
  21. package/android/src/main/java/com/audiobrowser/model/PlayerUpdateOptions.kt +12 -12
  22. package/android/src/main/java/com/audiobrowser/player/CapabilityControls.kt +111 -58
  23. package/android/src/main/java/com/audiobrowser/player/CrossOriginHeaderStripper.kt +77 -0
  24. package/android/src/main/java/com/audiobrowser/player/MediaFactory.kt +28 -10
  25. package/android/src/main/java/com/audiobrowser/player/MediaSessionCallback.kt +509 -230
  26. package/android/src/main/java/com/audiobrowser/player/MediaSessionCommandManager.kt +218 -252
  27. package/android/src/main/java/com/audiobrowser/player/NowPlayingUpdater.kt +21 -25
  28. package/android/src/main/java/com/audiobrowser/player/PlaybackStateStore.kt +113 -78
  29. package/android/src/main/java/com/audiobrowser/player/Player.kt +81 -38
  30. package/android/src/main/java/com/audiobrowser/player/PlayerCapabilitiesExt.kt +2 -15
  31. package/android/src/main/java/com/audiobrowser/player/PlayerListener.kt +1 -1
  32. package/android/src/main/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicy.kt +2 -1
  33. package/android/src/main/java/com/audiobrowser/player/ShuffleOrderLedBy.kt +30 -0
  34. package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +67 -11
  35. package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +168 -0
  36. package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
  37. package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +35 -7
  38. package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +278 -59
  39. package/android/src/main/java/com/audiobrowser/util/ArtworkVariant.kt +7 -9
  40. package/android/src/main/java/com/audiobrowser/util/BrowserPathHelper.kt +56 -40
  41. package/android/src/main/java/com/audiobrowser/util/MediaExtrasBuilder.kt +28 -23
  42. package/android/src/main/java/com/audiobrowser/util/StyleWire.kt +108 -0
  43. package/android/src/main/java/com/audiobrowser/util/TrackFactory.kt +56 -52
  44. package/android/src/main/res/values/strings.xml +25 -5
  45. package/android/src/test/java/com/audiobrowser/ServiceBindingTest.kt +87 -0
  46. package/android/src/test/java/com/audiobrowser/TestFixtures.kt +119 -33
  47. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerFavoritesTest.kt +153 -0
  48. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +15 -0
  49. package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +346 -8
  50. package/android/src/test/java/com/audiobrowser/browser/BrowserUrlResolutionTest.kt +38 -0
  51. package/android/src/test/java/com/audiobrowser/browser/InertStyleDiagnosticTest.kt +277 -0
  52. package/android/src/test/java/com/audiobrowser/browser/JsonArtworkTest.kt +3 -2
  53. package/android/src/test/java/com/audiobrowser/browser/JsonStyleTest.kt +147 -0
  54. package/android/src/test/java/com/audiobrowser/browser/SectionScopeTest.kt +128 -55
  55. package/android/src/test/java/com/audiobrowser/browser/StyleResolverTest.kt +154 -0
  56. package/android/src/test/java/com/audiobrowser/extension/TappedTrackTest.kt +43 -0
  57. package/android/src/test/java/com/audiobrowser/extension/TrackIdentityTest.kt +41 -0
  58. package/android/src/test/java/com/audiobrowser/model/IcyTitleTest.kt +49 -0
  59. package/android/src/test/java/com/audiobrowser/player/BrowseTilesTest.kt +385 -0
  60. package/android/src/test/java/com/audiobrowser/player/CapabilityControlsTest.kt +101 -58
  61. package/android/src/test/java/com/audiobrowser/player/CrossOriginHeaderStripperTest.kt +94 -0
  62. package/android/src/test/java/com/audiobrowser/player/MediaSessionSlotMappingTest.kt +45 -0
  63. package/android/src/test/java/com/audiobrowser/player/NowPlayingUpdaterTest.kt +42 -2
  64. package/android/src/test/java/com/audiobrowser/player/PlaybackErrorClassifierTest.kt +3 -4
  65. package/android/src/test/java/com/audiobrowser/player/PlaybackStateStoreTrackJsonTest.kt +55 -0
  66. package/android/src/test/java/com/audiobrowser/player/RetryLoadErrorHandlingPolicyTest.kt +3 -3
  67. package/android/src/test/java/com/audiobrowser/player/ShuffleOrderLedByTest.kt +79 -0
  68. package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +197 -7
  69. package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +325 -4
  70. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
  71. package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherTest.kt +83 -0
  72. package/android/src/test/java/com/audiobrowser/util/BrowserPathHelperTest.kt +41 -16
  73. package/android/src/test/java/com/audiobrowser/util/MediaExtrasBuilderTest.kt +162 -0
  74. package/android/src/test/java/com/audiobrowser/util/TrackFactoryMediaIdTest.kt +7 -7
  75. package/ios/Browser/BrowserConfig.swift +102 -5
  76. package/ios/Browser/BrowserManager+URLResolution.swift +18 -14
  77. package/ios/Browser/BrowserManager.swift +230 -205
  78. package/ios/Browser/BrowserPathHelper.swift +44 -44
  79. package/ios/Browser/InertStyleDiagnostic.swift +279 -0
  80. package/ios/Browser/JsonModels.swift +264 -106
  81. package/ios/Browser/SearchDrillIn.swift +7 -7
  82. package/ios/Browser/SectionPresentation.swift +64 -0
  83. package/ios/Browser/SectionScope.swift +58 -24
  84. package/ios/Browser/StyleResolver.swift +58 -0
  85. package/ios/Browser/TabBarEntries.swift +9 -3
  86. package/ios/Browser/TrackIdTemplate.swift +37 -0
  87. package/ios/CarPlay/CarPlayArtworkResolver.swift +8 -7
  88. package/ios/CarPlay/CarPlayController.swift +227 -85
  89. package/ios/CarPlay/CarPlayImageLoader.swift +42 -20
  90. package/ios/CarPlay/CarPlayListItemFactory.swift +289 -84
  91. package/ios/CarPlay/CarPlayNowPlayingManager.swift +15 -12
  92. package/ios/CarPlay/RNABMediaAddHandler.swift +6 -6
  93. package/ios/CarPlay/RNABMediaAffinityHandler.swift +2 -2
  94. package/ios/CarPlay/RNABMediaIntentHandler.swift +16 -16
  95. package/ios/Extension/Capability+RemoteCommand.swift +1 -1
  96. package/ios/Extension/PlayerCapabilities+Favorite.swift +1 -15
  97. package/ios/Extension/ResolvedTrack+Copying.swift +13 -15
  98. package/ios/Extension/Section+Copying.swift +29 -0
  99. package/ios/Extension/String+IcyCharset.swift +21 -0
  100. package/ios/Extension/TimedMetadata+AVFoundation.swift +6 -4
  101. package/ios/Extension/Track+Copying.swift +8 -12
  102. package/ios/Extension/Track+Identity.swift +18 -0
  103. package/ios/Extension/UIApplication+ActiveScene.swift +27 -0
  104. package/ios/Http/HttpClient.swift +8 -2
  105. package/ios/HybridAudioBrowser.swift +253 -100
  106. package/ios/Model/NitroTypeStubs.swift +125 -23
  107. package/ios/Model/PlayerUpdateOptions.swift +7 -0
  108. package/ios/Model/RemoteCommand.swift +18 -0
  109. package/ios/Model/TrackPlayerError.swift +2 -2
  110. package/ios/Option/SessionCategories.swift +3 -2
  111. package/ios/Player/MediaIntentCriteria.swift +17 -9
  112. package/ios/Player/MediaLoader.swift +33 -9
  113. package/ios/Player/NowPlayingUpdater.swift +54 -6
  114. package/ios/Player/PlaybackCoordinator.swift +13 -6
  115. package/ios/Player/PlaybackErrorHandler.swift +4 -4
  116. package/ios/Player/PlaybackTimer.swift +8 -2
  117. package/ios/Player/QueueManager.swift +46 -13
  118. package/ios/Player/RetryManager.swift +80 -18
  119. package/ios/Player/SeekCompletionHandler.swift +3 -1
  120. package/ios/Player/ShuffleOrder.swift +33 -0
  121. package/ios/Player/SleepTimerHandling.swift +13 -0
  122. package/ios/RemoteCommand/RemoteCommandCallbacks.swift +47 -0
  123. package/ios/RemoteCommand/RemoteCommandController.swift +62 -24
  124. package/ios/Tests/BrowserPathHelperTests.swift +50 -20
  125. package/ios/Tests/CarPlayArtworkResolverTests.swift +24 -41
  126. package/ios/Tests/InertStyleDiagnosticTests.swift +200 -0
  127. package/ios/Tests/JsonModelsTests.swift +150 -1
  128. package/ios/Tests/LRUCacheTests.swift +111 -0
  129. package/ios/Tests/LoadSeekCoordinatorTests.swift +3 -1
  130. package/ios/Tests/MediaIntentCriteriaTests.swift +2 -2
  131. package/ios/Tests/PlaybackCoordinatorTests.swift +1 -1
  132. package/ios/Tests/PlaybackErrorHandlerTests.swift +3 -1
  133. package/ios/Tests/PlaybackStateStoreTests.swift +4 -3
  134. package/ios/Tests/PlayerTimeObserverTests.swift +37 -0
  135. package/ios/Tests/QueueManagerTests.swift +122 -10
  136. package/ios/Tests/RemoteCommandControllerTests.swift +172 -0
  137. package/ios/Tests/RemoteCommandTests.swift +67 -0
  138. package/ios/Tests/ResolvedTrackCopyingTests.swift +39 -35
  139. package/ios/Tests/RetryManagerTests.swift +89 -0
  140. package/ios/Tests/SearchDrillInTests.swift +16 -16
  141. package/ios/Tests/SectionPresentationTests.swift +90 -0
  142. package/ios/Tests/SectionScopeTests.swift +87 -55
  143. package/ios/Tests/StyleResolverTests.swift +127 -0
  144. package/ios/Tests/Support/TestTypes.swift +8 -1
  145. package/ios/Tests/TabBarEntriesTests.swift +19 -19
  146. package/ios/Tests/TimedMetadataAVFoundationTests.swift +42 -0
  147. package/ios/Tests/TrackIdTemplateTests.swift +67 -0
  148. package/ios/Tests/TrackIdentityTests.swift +20 -0
  149. package/ios/Tests/TrackSelectorTests.swift +168 -33
  150. package/ios/TrackPlayer.swift +63 -35
  151. package/ios/TrackPlayerCallbacks.swift +5 -38
  152. package/ios/TrackSelector.swift +45 -30
  153. package/ios/Util/ArtworkImageFetcher.swift +16 -1
  154. package/ios/Util/LRUCache.swift +4 -1
  155. package/ios/Util/NetworkMonitor.swift +43 -26
  156. package/ios/Util/SFSymbolRenderer.swift +24 -2
  157. package/ios/Util/SVGProcessor.swift +9 -2
  158. package/lib/commonjs/AudioBrowser.js +8 -8
  159. package/lib/commonjs/AudioBrowser.js.map +1 -1
  160. package/lib/commonjs/CLAUDE.md +52 -0
  161. package/lib/commonjs/features/browser-config.js +12 -8
  162. package/lib/commonjs/features/browser-config.js.map +1 -1
  163. package/lib/commonjs/features/browser.js +11 -3
  164. package/lib/commonjs/features/browser.js.map +1 -1
  165. package/lib/commonjs/features/carConnection.js +1 -1
  166. package/lib/commonjs/features/equalizer.js +1 -1
  167. package/lib/commonjs/features/errors.js +3 -3
  168. package/lib/commonjs/features/favorites.js +14 -11
  169. package/lib/commonjs/features/favorites.js.map +1 -1
  170. package/lib/commonjs/features/gate.js +16 -1
  171. package/lib/commonjs/features/gate.js.map +1 -1
  172. package/lib/commonjs/features/metadata.js +20 -3
  173. package/lib/commonjs/features/metadata.js.map +1 -1
  174. package/lib/commonjs/features/network.js +1 -1
  175. package/lib/commonjs/features/nowPlaying.js +1 -1
  176. package/lib/commonjs/features/output.js +1 -1
  177. package/lib/commonjs/features/playback/controls.js +4 -2
  178. package/lib/commonjs/features/playback/controls.js.map +1 -1
  179. package/lib/commonjs/features/playback/playWhenReady.js +1 -1
  180. package/lib/commonjs/features/playback/playing.js +1 -1
  181. package/lib/commonjs/features/playback/progress.js +3 -2
  182. package/lib/commonjs/features/playback/progress.js.map +1 -1
  183. package/lib/commonjs/features/playback/rate.js +8 -1
  184. package/lib/commonjs/features/playback/rate.js.map +1 -1
  185. package/lib/commonjs/features/playback/state.js +3 -4
  186. package/lib/commonjs/features/playback/state.js.map +1 -1
  187. package/lib/commonjs/features/playback/trackPlaybackTime.js +0 -6
  188. package/lib/commonjs/features/playback/trackPlaybackTime.js.map +1 -1
  189. package/lib/commonjs/features/playback/volume.js +1 -1
  190. package/lib/commonjs/features/player/options.js +48 -34
  191. package/lib/commonjs/features/player/options.js.map +1 -1
  192. package/lib/commonjs/features/player/setup.js +18 -9
  193. package/lib/commonjs/features/player/setup.js.map +1 -1
  194. package/lib/commonjs/features/player/validateOptions.js +23 -0
  195. package/lib/commonjs/features/player/validateOptions.js.map +1 -0
  196. package/lib/commonjs/features/queue/activeTrack.js +15 -4
  197. package/lib/commonjs/features/queue/activeTrack.js.map +1 -1
  198. package/lib/commonjs/features/queue/queue.js +13 -6
  199. package/lib/commonjs/features/queue/queue.js.map +1 -1
  200. package/lib/commonjs/features/queue/repeatMode.js +1 -1
  201. package/lib/commonjs/features/queue/shuffle.js +1 -1
  202. package/lib/commonjs/features/remoteControls.js +13 -92
  203. package/lib/commonjs/features/remoteControls.js.map +1 -1
  204. package/lib/commonjs/features/sleepTimer.js +13 -1
  205. package/lib/commonjs/features/sleepTimer.js.map +1 -1
  206. package/lib/commonjs/test-utils/reactNativeStub.js +65 -0
  207. package/lib/commonjs/test-utils/reactNativeStub.js.map +1 -0
  208. package/lib/commonjs/utils/LazyNativeEmitter.js +28 -14
  209. package/lib/commonjs/utils/LazyNativeEmitter.js.map +1 -1
  210. package/lib/commonjs/utils/NativeUpdatedValue.js +31 -13
  211. package/lib/commonjs/utils/NativeUpdatedValue.js.map +1 -1
  212. package/lib/commonjs/utils/getTrackIdentity.js +24 -0
  213. package/lib/commonjs/utils/getTrackIdentity.js.map +1 -0
  214. package/lib/commonjs/utils/useNativeValueRefreshedBy.js +33 -0
  215. package/lib/commonjs/utils/useNativeValueRefreshedBy.js.map +1 -0
  216. package/lib/commonjs/web/NativeAudioBrowser.js +200 -75
  217. package/lib/commonjs/web/NativeAudioBrowser.js.map +1 -1
  218. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js +37 -0
  219. package/lib/commonjs/web/TrackPlayer/LoadAttempt.js.map +1 -0
  220. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  221. package/lib/commonjs/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  222. package/lib/commonjs/web/TrackPlayer/Player.js +254 -39
  223. package/lib/commonjs/web/TrackPlayer/Player.js.map +1 -1
  224. package/lib/commonjs/web/TrackPlayer/QueueManager.js +14 -0
  225. package/lib/commonjs/web/TrackPlayer/QueueManager.js.map +1 -1
  226. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js +33 -12
  227. package/lib/commonjs/web/TrackPlayer/QueuePlayer.js.map +1 -1
  228. package/lib/commonjs/web/browser/BrowserManager.js +175 -100
  229. package/lib/commonjs/web/browser/BrowserManager.js.map +1 -1
  230. package/lib/commonjs/web/browser/FavoriteManager.js +30 -21
  231. package/lib/commonjs/web/browser/FavoriteManager.js.map +1 -1
  232. package/lib/commonjs/web/browser/SearchManager.js +18 -23
  233. package/lib/commonjs/web/browser/SearchManager.js.map +1 -1
  234. package/lib/commonjs/web/browser/assertBrowsePageShape.js +2 -2
  235. package/lib/commonjs/web/browser/assertBrowsePageShape.js.map +1 -1
  236. package/lib/commonjs/web/browser/classifyTrackNavigation.js +9 -9
  237. package/lib/commonjs/web/browser/classifyTrackNavigation.js.map +1 -1
  238. package/lib/commonjs/web/browser/sections.js +89 -0
  239. package/lib/commonjs/web/browser/sections.js.map +1 -0
  240. package/lib/commonjs/web/http/RequestConfigBuilder.js +110 -27
  241. package/lib/commonjs/web/http/RequestConfigBuilder.js.map +1 -1
  242. package/lib/commonjs/web/http/followMediaRedirect.js +219 -0
  243. package/lib/commonjs/web/http/followMediaRedirect.js.map +1 -0
  244. package/lib/commonjs/web/player/NowPlayingManager.js +4 -3
  245. package/lib/commonjs/web/player/NowPlayingManager.js.map +1 -1
  246. package/lib/commonjs/web/util/BrowserPathHelper.js +56 -11
  247. package/lib/commonjs/web/util/BrowserPathHelper.js.map +1 -1
  248. package/lib/module/AudioBrowser.js +2 -1
  249. package/lib/module/AudioBrowser.js.map +1 -1
  250. package/lib/module/CLAUDE.md +52 -0
  251. package/lib/module/features/browser-config.js +12 -8
  252. package/lib/module/features/browser-config.js.map +1 -1
  253. package/lib/module/features/browser.js +11 -3
  254. package/lib/module/features/browser.js.map +1 -1
  255. package/lib/module/features/carConnection.js +1 -1
  256. package/lib/module/features/equalizer.js +1 -1
  257. package/lib/module/features/errors.js +3 -3
  258. package/lib/module/features/favorites.js +14 -11
  259. package/lib/module/features/favorites.js.map +1 -1
  260. package/lib/module/features/gate.js +16 -1
  261. package/lib/module/features/gate.js.map +1 -1
  262. package/lib/module/features/metadata.js +20 -3
  263. package/lib/module/features/metadata.js.map +1 -1
  264. package/lib/module/features/network.js +1 -1
  265. package/lib/module/features/nowPlaying.js +1 -1
  266. package/lib/module/features/output.js +1 -1
  267. package/lib/module/features/playback/controls.js +4 -2
  268. package/lib/module/features/playback/controls.js.map +1 -1
  269. package/lib/module/features/playback/playWhenReady.js +1 -1
  270. package/lib/module/features/playback/playing.js +1 -1
  271. package/lib/module/features/playback/progress.js +3 -2
  272. package/lib/module/features/playback/progress.js.map +1 -1
  273. package/lib/module/features/playback/rate.js +8 -1
  274. package/lib/module/features/playback/rate.js.map +1 -1
  275. package/lib/module/features/playback/state.js +3 -4
  276. package/lib/module/features/playback/state.js.map +1 -1
  277. package/lib/module/features/playback/trackPlaybackTime.js +0 -5
  278. package/lib/module/features/playback/trackPlaybackTime.js.map +1 -1
  279. package/lib/module/features/playback/volume.js +1 -1
  280. package/lib/module/features/player/options.js +47 -32
  281. package/lib/module/features/player/options.js.map +1 -1
  282. package/lib/module/features/player/setup.js +17 -8
  283. package/lib/module/features/player/setup.js.map +1 -1
  284. package/lib/module/features/player/validateOptions.js +19 -0
  285. package/lib/module/features/player/validateOptions.js.map +1 -0
  286. package/lib/module/features/queue/activeTrack.js +15 -4
  287. package/lib/module/features/queue/activeTrack.js.map +1 -1
  288. package/lib/module/features/queue/queue.js +13 -6
  289. package/lib/module/features/queue/queue.js.map +1 -1
  290. package/lib/module/features/queue/repeatMode.js +1 -1
  291. package/lib/module/features/queue/shuffle.js +1 -1
  292. package/lib/module/features/remoteControls.js +13 -92
  293. package/lib/module/features/remoteControls.js.map +1 -1
  294. package/lib/module/features/sleepTimer.js +13 -1
  295. package/lib/module/features/sleepTimer.js.map +1 -1
  296. package/lib/module/test-utils/reactNativeStub.js +58 -0
  297. package/lib/module/test-utils/reactNativeStub.js.map +1 -0
  298. package/lib/module/utils/LazyNativeEmitter.js +28 -14
  299. package/lib/module/utils/LazyNativeEmitter.js.map +1 -1
  300. package/lib/module/utils/NativeUpdatedValue.js +31 -13
  301. package/lib/module/utils/NativeUpdatedValue.js.map +1 -1
  302. package/lib/module/utils/getTrackIdentity.js +20 -0
  303. package/lib/module/utils/getTrackIdentity.js.map +1 -0
  304. package/lib/module/utils/useNativeValueRefreshedBy.js +29 -0
  305. package/lib/module/utils/useNativeValueRefreshedBy.js.map +1 -0
  306. package/lib/module/web/NativeAudioBrowser.js +200 -75
  307. package/lib/module/web/NativeAudioBrowser.js.map +1 -1
  308. package/lib/module/web/TrackPlayer/LoadAttempt.js +32 -0
  309. package/lib/module/web/TrackPlayer/LoadAttempt.js.map +1 -0
  310. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js +18 -2
  311. package/lib/module/web/TrackPlayer/PlaybackStateMachine.js.map +1 -1
  312. package/lib/module/web/TrackPlayer/Player.js +254 -39
  313. package/lib/module/web/TrackPlayer/Player.js.map +1 -1
  314. package/lib/module/web/TrackPlayer/QueueManager.js +14 -0
  315. package/lib/module/web/TrackPlayer/QueueManager.js.map +1 -1
  316. package/lib/module/web/TrackPlayer/QueuePlayer.js +33 -12
  317. package/lib/module/web/TrackPlayer/QueuePlayer.js.map +1 -1
  318. package/lib/module/web/browser/BrowserManager.js +175 -100
  319. package/lib/module/web/browser/BrowserManager.js.map +1 -1
  320. package/lib/module/web/browser/FavoriteManager.js +31 -21
  321. package/lib/module/web/browser/FavoriteManager.js.map +1 -1
  322. package/lib/module/web/browser/SearchManager.js +18 -23
  323. package/lib/module/web/browser/SearchManager.js.map +1 -1
  324. package/lib/module/web/browser/assertBrowsePageShape.js +2 -2
  325. package/lib/module/web/browser/assertBrowsePageShape.js.map +1 -1
  326. package/lib/module/web/browser/classifyTrackNavigation.js +9 -9
  327. package/lib/module/web/browser/classifyTrackNavigation.js.map +1 -1
  328. package/lib/module/web/browser/sections.js +83 -0
  329. package/lib/module/web/browser/sections.js.map +1 -0
  330. package/lib/module/web/http/RequestConfigBuilder.js +110 -27
  331. package/lib/module/web/http/RequestConfigBuilder.js.map +1 -1
  332. package/lib/module/web/http/followMediaRedirect.js +215 -0
  333. package/lib/module/web/http/followMediaRedirect.js.map +1 -0
  334. package/lib/module/web/player/NowPlayingManager.js +4 -3
  335. package/lib/module/web/player/NowPlayingManager.js.map +1 -1
  336. package/lib/module/web/util/BrowserPathHelper.js +56 -11
  337. package/lib/module/web/util/BrowserPathHelper.js.map +1 -1
  338. package/lib/typescript/package.json +1 -0
  339. package/lib/typescript/src/AudioBrowser.d.ts +1 -1
  340. package/lib/typescript/src/AudioBrowser.d.ts.map +1 -1
  341. package/lib/typescript/src/features/browser-config.d.ts.map +1 -1
  342. package/lib/typescript/src/features/browser.d.ts +10 -2
  343. package/lib/typescript/src/features/browser.d.ts.map +1 -1
  344. package/lib/typescript/src/features/carConnection.d.ts +1 -1
  345. package/lib/typescript/src/features/equalizer.d.ts +1 -1
  346. package/lib/typescript/src/features/errors.d.ts +3 -3
  347. package/lib/typescript/src/features/favorites.d.ts +14 -11
  348. package/lib/typescript/src/features/favorites.d.ts.map +1 -1
  349. package/lib/typescript/src/features/gate.d.ts +18 -2
  350. package/lib/typescript/src/features/gate.d.ts.map +1 -1
  351. package/lib/typescript/src/features/metadata.d.ts +20 -3
  352. package/lib/typescript/src/features/metadata.d.ts.map +1 -1
  353. package/lib/typescript/src/features/network.d.ts +1 -1
  354. package/lib/typescript/src/features/nowPlaying.d.ts +1 -1
  355. package/lib/typescript/src/features/output.d.ts +1 -1
  356. package/lib/typescript/src/features/playback/controls.d.ts +4 -2
  357. package/lib/typescript/src/features/playback/controls.d.ts.map +1 -1
  358. package/lib/typescript/src/features/playback/playWhenReady.d.ts +1 -1
  359. package/lib/typescript/src/features/playback/playing.d.ts +1 -1
  360. package/lib/typescript/src/features/playback/progress.d.ts +1 -1
  361. package/lib/typescript/src/features/playback/progress.d.ts.map +1 -1
  362. package/lib/typescript/src/features/playback/rate.d.ts +8 -1
  363. package/lib/typescript/src/features/playback/rate.d.ts.map +1 -1
  364. package/lib/typescript/src/features/playback/state.d.ts +3 -4
  365. package/lib/typescript/src/features/playback/state.d.ts.map +1 -1
  366. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts +1 -1
  367. package/lib/typescript/src/features/playback/trackPlaybackTime.d.ts.map +1 -1
  368. package/lib/typescript/src/features/playback/volume.d.ts +1 -1
  369. package/lib/typescript/src/features/player/options.d.ts +108 -56
  370. package/lib/typescript/src/features/player/options.d.ts.map +1 -1
  371. package/lib/typescript/src/features/player/setup.d.ts +14 -4
  372. package/lib/typescript/src/features/player/setup.d.ts.map +1 -1
  373. package/lib/typescript/src/features/player/validateOptions.d.ts +2 -0
  374. package/lib/typescript/src/features/player/validateOptions.d.ts.map +1 -0
  375. package/lib/typescript/src/features/queue/activeTrack.d.ts +9 -2
  376. package/lib/typescript/src/features/queue/activeTrack.d.ts.map +1 -1
  377. package/lib/typescript/src/features/queue/queue.d.ts +7 -4
  378. package/lib/typescript/src/features/queue/queue.d.ts.map +1 -1
  379. package/lib/typescript/src/features/queue/repeatMode.d.ts +1 -1
  380. package/lib/typescript/src/features/queue/shuffle.d.ts +1 -1
  381. package/lib/typescript/src/features/remoteControls.d.ts +23 -22
  382. package/lib/typescript/src/features/remoteControls.d.ts.map +1 -1
  383. package/lib/typescript/src/features/sleepTimer.d.ts +1 -1
  384. package/lib/typescript/src/features/sleepTimer.d.ts.map +1 -1
  385. package/lib/typescript/src/test-utils/reactNativeStub.d.ts +36 -0
  386. package/lib/typescript/src/test-utils/reactNativeStub.d.ts.map +1 -0
  387. package/lib/typescript/src/types/browser-native.d.ts +3 -2
  388. package/lib/typescript/src/types/browser-native.d.ts.map +1 -1
  389. package/lib/typescript/src/types/browser-nodes.d.ts +380 -126
  390. package/lib/typescript/src/types/browser-nodes.d.ts.map +1 -1
  391. package/lib/typescript/src/types/browser.d.ts +82 -57
  392. package/lib/typescript/src/types/browser.d.ts.map +1 -1
  393. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts +19 -16
  394. package/lib/typescript/src/utils/LazyNativeEmitter.d.ts.map +1 -1
  395. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts +24 -19
  396. package/lib/typescript/src/utils/NativeUpdatedValue.d.ts.map +1 -1
  397. package/lib/typescript/src/utils/getTrackIdentity.d.ts +18 -0
  398. package/lib/typescript/src/utils/getTrackIdentity.d.ts.map +1 -0
  399. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts +21 -0
  400. package/lib/typescript/src/utils/useNativeValueRefreshedBy.d.ts.map +1 -0
  401. package/nitrogen/generated/android/AudioBrowser+autolinking.cmake +1 -2
  402. package/nitrogen/generated/android/AudioBrowserOnLoad.cpp +2 -0
  403. package/nitrogen/generated/android/c++/JAndroidOptions.hpp +10 -10
  404. package/nitrogen/generated/android/c++/{JFavoritesMatchMode.hpp → JArtworkRendering.hpp} +15 -15
  405. package/nitrogen/generated/android/c++/JArtworkRequestConfig.hpp +8 -3
  406. package/nitrogen/generated/android/c++/JBrowseResult.hpp +16 -4
  407. package/nitrogen/generated/android/c++/JCarPlayPlayingIndicatorLocation.hpp +58 -0
  408. package/nitrogen/generated/android/c++/JCardImage.hpp +58 -0
  409. package/nitrogen/generated/android/c++/JFavoriteChangedEvent.hpp +8 -3
  410. package/nitrogen/generated/android/c++/JFormatNowPlayingParams.hpp +8 -3
  411. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_RequestConfig___Track.hpp +8 -3
  412. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_NowPlayingUpdate____FormatNowPlayingParams.hpp +8 -3
  413. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__optional_std__string____Track.hpp +8 -3
  414. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_RequestConfig_____Track.hpp +8 -3
  415. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__vector_Track______SearchParams.hpp +8 -2
  416. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_void_____TrackLoadEvent.hpp +8 -2
  417. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__string__.hpp +102 -0
  418. package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError__std__shared_ptr_Promise_std__variant_ResolvedTrack__BrowseError_______BrowserSourceCallbackParam.hpp +16 -4
  419. package/nitrogen/generated/android/c++/JFunc_void_FavoriteChangedEvent.hpp +8 -3
  420. package/nitrogen/generated/android/c++/JFunc_void_Options.hpp +7 -10
  421. package/nitrogen/generated/android/c++/JFunc_void_PlaybackActiveTrackChangedEvent.hpp +8 -3
  422. package/nitrogen/generated/android/c++/JFunc_void_std__optional_ResolvedTrack_.hpp +15 -3
  423. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Track_.hpp +8 -2
  424. package/nitrogen/generated/android/c++/JGridTile.hpp +61 -0
  425. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.cpp +45 -25
  426. package/nitrogen/generated/android/c++/JHybridAudioBrowserSpec.hpp +1 -1
  427. package/nitrogen/generated/android/c++/JIOSOptions.hpp +9 -3
  428. package/nitrogen/generated/android/c++/JImageShape.hpp +58 -0
  429. package/nitrogen/generated/android/c++/JMediaRequestConfig.hpp +8 -3
  430. package/nitrogen/generated/android/c++/JNativeBrowserConfiguration.hpp +36 -11
  431. package/nitrogen/generated/android/c++/JNativeRouteEntry.hpp +14 -2
  432. package/nitrogen/generated/android/c++/JNativeSetupPlayerOptions.hpp +15 -12
  433. package/nitrogen/generated/android/c++/JNativeUpdateOptions.hpp +7 -10
  434. package/nitrogen/generated/android/c++/JNitroAndroidUpdateOptions.hpp +10 -10
  435. package/nitrogen/generated/android/c++/JNitroIOSUpdateOptions.hpp +9 -3
  436. package/nitrogen/generated/android/c++/JOptions.hpp +7 -10
  437. package/nitrogen/generated/android/c++/JPlaybackActiveTrackChangedEvent.hpp +8 -3
  438. package/nitrogen/generated/android/c++/JPlayerCapabilities.hpp +5 -11
  439. package/nitrogen/generated/android/c++/{JNotificationButton.hpp → JRemoteButton.hpp} +19 -19
  440. package/nitrogen/generated/android/c++/JRemoteButtonLayout.hpp +86 -0
  441. package/nitrogen/generated/android/c++/JResolvedTrack.hpp +57 -49
  442. package/nitrogen/generated/android/c++/JSection.hpp +116 -0
  443. package/nitrogen/generated/android/c++/JSectionStyle.hpp +96 -0
  444. package/nitrogen/generated/android/c++/JStyleDisplay.hpp +58 -0
  445. package/nitrogen/generated/android/c++/JTrack.hpp +25 -46
  446. package/nitrogen/generated/android/c++/JTrackLoadEvent.hpp +8 -2
  447. package/nitrogen/generated/android/c++/JTrackStyle.hpp +46 -18
  448. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.cpp +26 -0
  449. package/nitrogen/generated/android/c++/JVariant_NullType_RemoteButtonLayout.hpp +75 -0
  450. package/nitrogen/generated/android/c++/JVariant_ResolvedTrack_BrowseError_Promise_BrowseResult_.hpp +16 -4
  451. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/AndroidOptions.kt +3 -3
  452. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoritesMatchMode.kt → ArtworkRendering.kt} +5 -5
  453. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CarPlayPlayingIndicatorLocation.kt +23 -0
  454. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/CardImage.kt +23 -0
  455. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Func_std__shared_ptr_Promise_std__string__.kt +80 -0
  456. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/GridTile.kt +24 -0
  457. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/HybridAudioBrowserSpec.kt +1 -1
  458. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/IOSOptions.kt +6 -3
  459. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageShape.kt +23 -0
  460. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NativeBrowserConfiguration.kt +8 -5
  461. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroAndroidUpdateOptions.kt +3 -3
  462. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NitroIOSUpdateOptions.kt +6 -3
  463. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/PlayerCapabilities.kt +2 -2
  464. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{NotificationButton.kt → RemoteButton.kt} +3 -3
  465. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/RemoteButtonLayout.kt +44 -0
  466. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ResolvedTrack.kt +12 -15
  467. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{FavoriteConfig.kt → Section.kt} +15 -6
  468. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/SectionStyle.kt +59 -0
  469. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/StyleDisplay.kt +23 -0
  470. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Track.kt +6 -12
  471. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/TrackStyle.kt +35 -5
  472. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/{Variant_NullType_NotificationButtonLayout.kt → Variant_NullType_RemoteButtonLayout.kt} +9 -9
  473. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.cpp +9 -0
  474. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Bridge.hpp +244 -133
  475. package/nitrogen/generated/ios/AudioBrowser-Swift-Cxx-Umbrella.hpp +31 -16
  476. package/nitrogen/generated/ios/c++/HybridAudioBrowserSpecSwift.hpp +33 -18
  477. package/nitrogen/generated/ios/swift/AndroidOptions.swift +12 -12
  478. package/nitrogen/generated/ios/swift/ArtworkRendering.swift +40 -0
  479. package/nitrogen/generated/ios/swift/CarPlayPlayingIndicatorLocation.swift +40 -0
  480. package/nitrogen/generated/ios/swift/CardImage.swift +40 -0
  481. package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__string__.swift +54 -0
  482. package/nitrogen/generated/ios/swift/GridTile.swift +44 -0
  483. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec.swift +1 -1
  484. package/nitrogen/generated/ios/swift/HybridAudioBrowserSpec_cxx.swift +2 -2
  485. package/nitrogen/generated/ios/swift/IOSOptions.swift +7 -2
  486. package/nitrogen/generated/ios/swift/ImageShape.swift +40 -0
  487. package/nitrogen/generated/ios/swift/NativeBrowserConfiguration.swift +52 -5
  488. package/nitrogen/generated/ios/swift/NitroAndroidUpdateOptions.swift +12 -12
  489. package/nitrogen/generated/ios/swift/NitroIOSUpdateOptions.swift +12 -1
  490. package/nitrogen/generated/ios/swift/PlayerCapabilities.swift +8 -27
  491. package/nitrogen/generated/ios/swift/{NotificationButton.swift → RemoteButton.swift} +6 -6
  492. package/nitrogen/generated/ios/swift/RemoteButtonLayout.swift +57 -0
  493. package/nitrogen/generated/ios/swift/ResolvedTrack.swift +62 -73
  494. package/nitrogen/generated/ios/swift/Section.swift +82 -0
  495. package/nitrogen/generated/ios/swift/SectionStyle.swift +133 -0
  496. package/nitrogen/generated/ios/swift/StyleDisplay.swift +40 -0
  497. package/nitrogen/generated/ios/swift/Track.swift +28 -63
  498. package/nitrogen/generated/ios/swift/TrackStyle.swift +86 -22
  499. package/nitrogen/generated/ios/swift/{Variant_NullType_NotificationButtonLayout.swift → Variant_NullType_RemoteButtonLayout.swift} +3 -3
  500. package/nitrogen/generated/shared/c++/AndroidOptions.hpp +8 -8
  501. package/nitrogen/generated/shared/c++/{FavoritesMatchMode.hpp → ArtworkRendering.hpp} +17 -17
  502. package/nitrogen/generated/shared/c++/CarPlayPlayingIndicatorLocation.hpp +76 -0
  503. package/nitrogen/generated/shared/c++/CardImage.hpp +76 -0
  504. package/nitrogen/generated/shared/c++/GridTile.hpp +80 -0
  505. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.cpp +1 -1
  506. package/nitrogen/generated/shared/c++/HybridAudioBrowserSpec.hpp +1 -1
  507. package/nitrogen/generated/shared/c++/IOSOptions.hpp +9 -2
  508. package/nitrogen/generated/shared/c++/ImageShape.hpp +76 -0
  509. package/nitrogen/generated/shared/c++/NativeBrowserConfiguration.hpp +10 -6
  510. package/nitrogen/generated/shared/c++/NitroAndroidUpdateOptions.hpp +8 -8
  511. package/nitrogen/generated/shared/c++/NitroIOSUpdateOptions.hpp +9 -2
  512. package/nitrogen/generated/shared/c++/PlayerCapabilities.hpp +6 -9
  513. package/nitrogen/generated/shared/c++/{NotificationButton.hpp → RemoteButton.hpp} +19 -19
  514. package/nitrogen/generated/shared/c++/RemoteButtonLayout.hpp +94 -0
  515. package/nitrogen/generated/shared/c++/ResolvedTrack.hpp +33 -37
  516. package/nitrogen/generated/shared/c++/Section.hpp +102 -0
  517. package/nitrogen/generated/shared/c++/SectionStyle.hpp +126 -0
  518. package/nitrogen/generated/shared/c++/StyleDisplay.hpp +76 -0
  519. package/nitrogen/generated/shared/c++/Track.hpp +18 -30
  520. package/nitrogen/generated/shared/c++/TrackStyle.hpp +73 -34
  521. package/package.json +24 -11
  522. package/src/AudioBrowser.ts +2 -1
  523. package/src/CLAUDE.md +52 -0
  524. package/src/features/battery.test.ts +79 -0
  525. package/src/features/browser-config.test.ts +21 -6
  526. package/src/features/browser-config.ts +12 -8
  527. package/src/features/browser.test.ts +1 -1
  528. package/src/features/browser.ts +11 -3
  529. package/src/features/carConnection.ts +1 -1
  530. package/src/features/equalizer.ts +1 -1
  531. package/src/features/errors.ts +3 -3
  532. package/src/features/favorites.ts +14 -11
  533. package/src/features/gate.ts +16 -1
  534. package/src/features/hook-wiring.test.ts +302 -0
  535. package/src/features/metadata.ts +20 -3
  536. package/src/features/network.ts +1 -1
  537. package/src/features/nowPlaying.ts +1 -1
  538. package/src/features/output.ts +1 -1
  539. package/src/features/playback/controls.ts +4 -2
  540. package/src/features/playback/playWhenReady.ts +1 -1
  541. package/src/features/playback/playing.ts +1 -1
  542. package/src/features/playback/progress.test.ts +169 -0
  543. package/src/features/playback/progress.ts +3 -2
  544. package/src/features/playback/rate.ts +8 -1
  545. package/src/features/playback/state.ts +3 -4
  546. package/src/features/playback/trackPlaybackTime.test.ts +16 -6
  547. package/src/features/playback/trackPlaybackTime.ts +1 -6
  548. package/src/features/playback/volume.ts +1 -1
  549. package/src/features/player/options.ts +126 -76
  550. package/src/features/player/setup.test.ts +24 -7
  551. package/src/features/player/setup.ts +22 -10
  552. package/src/features/player/validateOptions.ts +21 -0
  553. package/src/features/queue/activeTrack.ts +15 -4
  554. package/src/features/queue/queue.ts +13 -6
  555. package/src/features/queue/repeatMode.ts +1 -1
  556. package/src/features/queue/shuffle.ts +1 -1
  557. package/src/features/remoteControls.ts +13 -92
  558. package/src/features/sleepTimer.test.ts +172 -0
  559. package/src/features/sleepTimer.ts +9 -1
  560. package/src/nitro-stub-drift.test.ts +277 -0
  561. package/src/public-surface.test.ts +147 -0
  562. package/src/specs/audio-browser.nitro.ts +5 -4
  563. package/src/style-resolution.test.ts +210 -0
  564. package/src/test-utils/reactNativeStub.ts +60 -0
  565. package/src/types/browser-native.ts +7 -3
  566. package/src/types/browser-nodes.ts +396 -134
  567. package/src/types/browser.ts +85 -59
  568. package/src/utils/LazyNativeEmitter.ts +28 -15
  569. package/src/utils/NativeUpdatedValue.ts +29 -13
  570. package/src/utils/getTrackIdentity.ts +20 -0
  571. package/src/utils/useDebug.test.ts +182 -0
  572. package/src/utils/useNativeUpdatedValue.test.ts +103 -0
  573. package/src/utils/useNativeValueRefreshedBy.ts +38 -0
  574. package/src/web/NativeAudioBrowser.playWhenReady.test.ts +232 -2
  575. package/src/web/NativeAudioBrowser.ts +239 -82
  576. package/src/web/TrackPlayer/LoadAttempt.ts +29 -0
  577. package/src/web/TrackPlayer/PlaybackStateMachine.test.ts +60 -13
  578. package/src/web/TrackPlayer/PlaybackStateMachine.ts +22 -6
  579. package/src/web/TrackPlayer/Player.dispatch.test.ts +5 -3
  580. package/src/web/TrackPlayer/Player.load.test.ts +137 -1
  581. package/src/web/TrackPlayer/Player.ts +284 -37
  582. package/src/web/TrackPlayer/QueueManager.test.ts +29 -0
  583. package/src/web/TrackPlayer/QueueManager.ts +14 -0
  584. package/src/web/TrackPlayer/QueuePlayer.ts +33 -14
  585. package/src/web/__tests__/lifecycle.test.ts +345 -0
  586. package/src/web/__tests__/lifecycleHarness.ts +351 -0
  587. package/src/web/__tests__/loadEdgeCases.test.ts +246 -0
  588. package/src/web/__tests__/loadSupersession.test.ts +175 -0
  589. package/src/web/__tests__/retry.test.ts +169 -0
  590. package/src/web/browser/BrowserManager.ts +209 -111
  591. package/src/web/browser/FavoriteManager.test.ts +84 -0
  592. package/src/web/browser/FavoriteManager.ts +28 -23
  593. package/src/web/browser/SearchManager.test.ts +75 -0
  594. package/src/web/browser/SearchManager.ts +25 -28
  595. package/src/web/browser/__tests__/artwork-resolution.test.ts +117 -0
  596. package/src/web/browser/__tests__/layer-resolver.test.ts +35 -0
  597. package/src/web/browser/__tests__/search-artwork.test.ts +46 -0
  598. package/src/web/browser/__tests__/search-navigation.test.ts +76 -0
  599. package/src/web/browser/assertBrowsePageShape.test.ts +1 -1
  600. package/src/web/browser/assertBrowsePageShape.ts +5 -3
  601. package/src/web/browser/classifyTrackNavigation.test.ts +10 -8
  602. package/src/web/browser/classifyTrackNavigation.ts +9 -9
  603. package/src/web/browser/parseSearchResponse.test.ts +8 -2
  604. package/src/web/browser/sections.test.ts +91 -0
  605. package/src/web/browser/sections.ts +72 -0
  606. package/src/web/http/RequestConfigBuilder.test.ts +241 -1
  607. package/src/web/http/RequestConfigBuilder.ts +140 -33
  608. package/src/web/http/followMediaRedirect.test.ts +289 -0
  609. package/src/web/http/followMediaRedirect.ts +253 -0
  610. package/src/web/player/NowPlayingManager.ts +4 -3
  611. package/src/web/util/BrowserPathHelper.test.ts +52 -0
  612. package/src/web/util/BrowserPathHelper.ts +61 -13
  613. package/website/guide/android-auto.md +114 -0
  614. package/website/guide/android-certificates.md +409 -0
  615. package/website/guide/artwork.md +255 -0
  616. package/website/guide/audio-output.md +135 -0
  617. package/website/guide/automotive.md +86 -0
  618. package/website/guide/basic-usage.md +151 -0
  619. package/website/guide/battery.md +115 -0
  620. package/website/guide/browser.md +581 -0
  621. package/website/guide/carplay.md +620 -0
  622. package/website/guide/configuration.md +173 -0
  623. package/website/guide/equalizer.md +231 -0
  624. package/website/guide/errors.md +310 -0
  625. package/website/guide/favorites.md +154 -0
  626. package/website/guide/gate.md +143 -0
  627. package/website/guide/getting-started.md +46 -0
  628. package/website/guide/hooks.md +238 -0
  629. package/website/guide/live-streams.md +125 -0
  630. package/website/guide/metadata.md +239 -0
  631. package/website/guide/migrating-from-track-player.md +244 -0
  632. package/website/guide/native-callback-fetch.md +127 -0
  633. package/website/guide/network.md +86 -0
  634. package/website/guide/now-playing.md +175 -0
  635. package/website/guide/playback.md +328 -0
  636. package/website/guide/queue.md +300 -0
  637. package/website/guide/remote-controls.md +256 -0
  638. package/website/guide/search.md +255 -0
  639. package/website/guide/sleep-timer.md +268 -0
  640. package/website/guide/track.md +247 -0
  641. package/ios/CarPlay/ImageRowItem+CarPlay.swift +0 -19
  642. package/ios/Model/MediaURL.swift +0 -67
  643. package/nitrogen/generated/android/c++/JFavoriteConfig.hpp +0 -58
  644. package/nitrogen/generated/android/c++/JImageRowItem.hpp +0 -105
  645. package/nitrogen/generated/android/c++/JNotificationButtonLayout.hpp +0 -94
  646. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.cpp +0 -26
  647. package/nitrogen/generated/android/c++/JVariant_Boolean_FavoriteConfig.hpp +0 -71
  648. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.cpp +0 -26
  649. package/nitrogen/generated/android/c++/JVariant_NullType_NotificationButtonLayout.hpp +0 -75
  650. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/ImageRowItem.kt +0 -68
  651. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/NotificationButtonLayout.kt +0 -50
  652. package/nitrogen/generated/android/kotlin/com/margelo/nitro/audiobrowser/Variant_Boolean_FavoriteConfig.kt +0 -53
  653. package/nitrogen/generated/ios/swift/FavoriteConfig.swift +0 -29
  654. package/nitrogen/generated/ios/swift/FavoritesMatchMode.swift +0 -40
  655. package/nitrogen/generated/ios/swift/ImageRowItem.swift +0 -195
  656. package/nitrogen/generated/ios/swift/NotificationButtonLayout.swift +0 -92
  657. package/nitrogen/generated/ios/swift/Variant_Bool_FavoriteConfig.swift +0 -18
  658. package/nitrogen/generated/shared/c++/FavoriteConfig.hpp +0 -84
  659. package/nitrogen/generated/shared/c++/ImageRowItem.hpp +0 -129
  660. package/nitrogen/generated/shared/c++/NotificationButtonLayout.hpp +0 -102
@@ -1,7 +1,10 @@
1
1
  package com.audiobrowser.tls
2
2
 
3
+ import java.net.Socket
3
4
  import java.security.cert.CertificateException
4
5
  import java.security.cert.X509Certificate
6
+ import javax.net.ssl.SSLEngine
7
+ import javax.net.ssl.X509ExtendedTrustManager
5
8
  import javax.net.ssl.X509TrustManager
6
9
  import org.junit.Assert.assertEquals
7
10
  import org.junit.Assert.assertThrows
@@ -31,8 +34,8 @@ class AiaChasingTrustManagerTest {
31
34
  val r13 = CertFixtures.cert("letsencrypt-r13.pem")
32
35
  val delegate = FakeDelegate(trustsChainOfSize = 2)
33
36
  val tm =
34
- AiaChasingTrustManager(delegate) {
35
- if (it == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
37
+ AiaChasingTrustManager(delegate) { url, _ ->
38
+ if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
36
39
  }
37
40
 
38
41
  // Must not throw: the leaf-only chain is rejected, AIA fills in R13, the [leaf, R13] chain
@@ -46,7 +49,7 @@ class AiaChasingTrustManagerTest {
46
49
  fun `rethrows the original failure when no intermediate can be fetched`() {
47
50
  val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
48
51
  val delegate = FakeDelegate(trustsChainOfSize = 2)
49
- val tm = AiaChasingTrustManager(delegate) { emptyList() }
52
+ val tm = AiaChasingTrustManager(delegate) { _, _ -> emptyList() }
50
53
 
51
54
  assertThrows(CertificateException::class.java) { tm.checkServerTrusted(arrayOf(leaf), "RSA") }
52
55
 
@@ -60,7 +63,7 @@ class AiaChasingTrustManagerTest {
60
63
  val delegate = FakeDelegate(trustsChainOfSize = 1)
61
64
  var fetches = 0
62
65
  val tm =
63
- AiaChasingTrustManager(delegate) {
66
+ AiaChasingTrustManager(delegate) { _, _ ->
64
67
  fetches++
65
68
  emptyList()
66
69
  }
@@ -70,4 +73,322 @@ class AiaChasingTrustManagerTest {
70
73
  assertEquals(0, fetches)
71
74
  assertEquals(listOf(1), delegate.checkedChainSizes)
72
75
  }
76
+
77
+ // -- the API 24+ variant, which is what the platform actually calls into --
78
+
79
+ /**
80
+ * Stands in for Android's `RootTrustManager` under a per-domain network security config: the
81
+ * hostname-aware overloads work, and the two-argument one refuses to answer at all.
82
+ */
83
+ private class FakeDomainConfigDelegate(private val trustsChainOfSize: Int) :
84
+ X509ExtendedTrustManager() {
85
+ val hostnameAwareChecks = mutableListOf<Int>()
86
+
87
+ override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
88
+ throw CertificateException(
89
+ "Domain specific configurations require that hostname aware " +
90
+ "checkServerTrusted(X509Certificate[], String, String) is used"
91
+ )
92
+
93
+ override fun checkServerTrusted(
94
+ chain: Array<X509Certificate>,
95
+ authType: String,
96
+ socket: Socket?,
97
+ ) {
98
+ hostnameAwareChecks.add(chain.size)
99
+ if (chain.size != trustsChainOfSize) {
100
+ throw CertificateException("Trust anchor for certification path not found.")
101
+ }
102
+ }
103
+
104
+ override fun checkServerTrusted(
105
+ chain: Array<X509Certificate>,
106
+ authType: String,
107
+ engine: SSLEngine?,
108
+ ) = checkServerTrusted(chain, authType, null as Socket?)
109
+
110
+ override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
111
+
112
+ override fun checkClientTrusted(
113
+ chain: Array<X509Certificate>,
114
+ authType: String,
115
+ socket: Socket?,
116
+ ) = Unit
117
+
118
+ override fun checkClientTrusted(
119
+ chain: Array<X509Certificate>,
120
+ authType: String,
121
+ engine: SSLEngine?,
122
+ ) = Unit
123
+
124
+ override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
125
+ }
126
+
127
+ @Test
128
+ fun `a complete chain rejected for some other reason is not chased`() {
129
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
130
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
131
+ val root = CertFixtures.cert("isrg-root-x1.pem")
132
+ // Trusts nothing — an expired chain, a hostname mismatch, a CT refusal all look like this:
133
+ // the delegate rejects a chain that is not missing anything.
134
+ val delegate =
135
+ object : X509TrustManager {
136
+ override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
137
+ throw CertificateException("certificate expired")
138
+
139
+ override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
140
+
141
+ override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf(root)
142
+ }
143
+ var fetches = 0
144
+ val tm =
145
+ AiaChasingTrustManager(delegate) { _, _ ->
146
+ fetches++
147
+ emptyList()
148
+ }
149
+
150
+ assertThrows(CertificateException::class.java) {
151
+ tm.checkServerTrusted(arrayOf(leaf, r13), "RSA")
152
+ }
153
+
154
+ // Chasing could not have helped, so it must not have reached out to a host named by a
155
+ // certificate that just failed validation.
156
+ assertEquals(0, fetches)
157
+ }
158
+
159
+ @Test
160
+ fun `the original failure is kept when the completed chain fails too`() {
161
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
162
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
163
+ // Trusts no chain of any size, so the retry fails as well.
164
+ val delegate = FakeDelegate(trustsChainOfSize = -1)
165
+ val tm = AiaChasingTrustManager(delegate) { _, _ -> listOf(r13) }
166
+
167
+ val thrown =
168
+ assertThrows(CertificateException::class.java) { tm.checkServerTrusted(arrayOf(leaf), "RSA") }
169
+
170
+ assertEquals(1, thrown.suppressed.size)
171
+ }
172
+
173
+ @Test
174
+ fun `client checks are forwarded, not routed through the server path`() {
175
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
176
+ var clientChecks = 0
177
+ val delegate =
178
+ object : X509ExtendedTrustManager() {
179
+ override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
180
+ throw CertificateException("server path must not be used for a client check")
181
+
182
+ override fun checkServerTrusted(
183
+ chain: Array<X509Certificate>,
184
+ authType: String,
185
+ socket: Socket?,
186
+ ) = throw CertificateException("server path must not be used for a client check")
187
+
188
+ override fun checkServerTrusted(
189
+ chain: Array<X509Certificate>,
190
+ authType: String,
191
+ engine: SSLEngine?,
192
+ ) = throw CertificateException("server path must not be used for a client check")
193
+
194
+ override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
195
+ clientChecks++
196
+ }
197
+
198
+ override fun checkClientTrusted(
199
+ chain: Array<X509Certificate>,
200
+ authType: String,
201
+ socket: Socket?,
202
+ ) {
203
+ clientChecks++
204
+ }
205
+
206
+ override fun checkClientTrusted(
207
+ chain: Array<X509Certificate>,
208
+ authType: String,
209
+ engine: SSLEngine?,
210
+ ) {
211
+ clientChecks++
212
+ }
213
+
214
+ override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
215
+ }
216
+ val tm = AiaChasingExtendedTrustManager(delegate) { _, _ -> emptyList() }
217
+
218
+ tm.checkClientTrusted(arrayOf(leaf), "RSA")
219
+ tm.checkClientTrusted(arrayOf(leaf), "RSA", null as Socket?)
220
+ tm.checkClientTrusted(arrayOf(leaf), "RSA", null as SSLEngine?)
221
+
222
+ assertEquals(3, clientChecks)
223
+ }
224
+
225
+ /** A delegate carrying the Android-specific hostname-aware method, as `RootTrustManager` does. */
226
+ @Suppress("unused")
227
+ private class HostAwareDelegate(private val trustsChainOfSize: Int) : X509TrustManager {
228
+ val hosts = mutableListOf<String>()
229
+
230
+ fun checkServerTrusted(
231
+ chain: Array<X509Certificate>,
232
+ authType: String,
233
+ host: String,
234
+ ): List<X509Certificate> {
235
+ hosts.add(host)
236
+ if (chain.size != trustsChainOfSize) {
237
+ throw CertificateException("Trust anchor for certification path not found.")
238
+ }
239
+ return chain.toList()
240
+ }
241
+
242
+ override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
243
+ throw CertificateException("the host-aware overload should have been used")
244
+
245
+ override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
246
+
247
+ override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
248
+ }
249
+
250
+ @Test
251
+ fun `the X509TrustManagerExtensions signature is present and forwards the host`() {
252
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
253
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
254
+ val delegate = HostAwareDelegate(trustsChainOfSize = 2)
255
+ val tm =
256
+ AiaChasingExtendedTrustManager(delegate) { url, _ ->
257
+ if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
258
+ }
259
+
260
+ // The method X509TrustManagerExtensions looks up reflectively must exist on the wrapper...
261
+ val method =
262
+ tm.javaClass.getMethod(
263
+ "checkServerTrusted",
264
+ Array<X509Certificate>::class.java,
265
+ String::class.java,
266
+ String::class.java,
267
+ )
268
+
269
+ // ...and must reach the delegate's own host-aware method, so pinning still sees the host.
270
+ val accepted = tm.checkServerTrusted(arrayOf(leaf), "RSA", "ca.example")
271
+
272
+ assertEquals(listOf(leaf, r13), accepted)
273
+ assertEquals(listOf("ca.example", "ca.example"), delegate.hosts)
274
+ assertEquals(List::class.java, method.returnType)
275
+ }
276
+
277
+ @Test
278
+ fun `the host-aware signature falls back to the extended overloads when the delegate lacks it`() {
279
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
280
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
281
+ // An extended delegate without the Android-specific String-host method, whose two-argument
282
+ // form refuses outright — the domain-config case. The fallback must route through its
283
+ // extended overloads, and must report the AIA-completed chain that actually validated.
284
+ val delegate = FakeDomainConfigDelegate(trustsChainOfSize = 2)
285
+ val tm =
286
+ AiaChasingExtendedTrustManager(delegate) { url, _ ->
287
+ if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
288
+ }
289
+
290
+ val accepted = tm.checkServerTrusted(arrayOf(leaf), "RSA", "ca.example")
291
+
292
+ assertEquals(listOf(leaf, r13), accepted)
293
+ assertEquals(listOf(1, 2), delegate.hostnameAwareChecks)
294
+ }
295
+
296
+ /** A host-aware delegate that validates fine but reports an empty accepted chain. */
297
+ @Suppress("unused")
298
+ private class SilentHostAwareDelegate(private val trustsChainOfSize: Int) : X509TrustManager {
299
+ fun checkServerTrusted(
300
+ chain: Array<X509Certificate>,
301
+ authType: String,
302
+ host: String,
303
+ ): List<X509Certificate> {
304
+ if (chain.size != trustsChainOfSize) {
305
+ throw CertificateException("Trust anchor for certification path not found.")
306
+ }
307
+ return emptyList()
308
+ }
309
+
310
+ override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
311
+ throw CertificateException("the host-aware overload should have been used")
312
+
313
+ override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
314
+
315
+ override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
316
+ }
317
+
318
+ @Test
319
+ fun `a delegate reporting an empty accepted chain still yields the chain that validated`() {
320
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
321
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
322
+ val delegate = SilentHostAwareDelegate(trustsChainOfSize = 2)
323
+ val tm =
324
+ AiaChasingExtendedTrustManager(delegate) { url, _ ->
325
+ if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
326
+ }
327
+
328
+ // An empty list from the delegate on success must not be passed through: the checked chain
329
+ // is what it accepted, and a pinning consumer given [] would fail a valid connection.
330
+ val accepted = tm.checkServerTrusted(arrayOf(leaf), "RSA", "ca.example")
331
+
332
+ assertEquals(listOf(leaf, r13), accepted)
333
+ }
334
+
335
+ @Test
336
+ fun `forwards to the hostname-aware overload the platform requires`() {
337
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
338
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
339
+ val delegate = FakeDomainConfigDelegate(trustsChainOfSize = 2)
340
+ val tm =
341
+ AiaChasingExtendedTrustManager(delegate) { url, _ ->
342
+ if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
343
+ }
344
+
345
+ // The socket overload must reach the delegate's socket overload. Routing it through the
346
+ // two-argument form instead would throw the domain-config refusal, which is what a plain
347
+ // X509TrustManager wrapper causes for every connection in the process.
348
+ tm.checkServerTrusted(arrayOf(leaf), "RSA", null as Socket?)
349
+
350
+ assertEquals(listOf(1, 2), delegate.hostnameAwareChecks)
351
+ }
352
+
353
+ @Test
354
+ fun `the engine overload is hostname-aware too`() {
355
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
356
+ val delegate = FakeDomainConfigDelegate(trustsChainOfSize = 1)
357
+
358
+ AiaChasingExtendedTrustManager(delegate) { _, _ -> emptyList() }
359
+ .checkServerTrusted(arrayOf(leaf), "RSA", null as SSLEngine?)
360
+
361
+ assertEquals(listOf(1), delegate.hostnameAwareChecks)
362
+ }
363
+
364
+ @Test
365
+ fun `a delegate without extended overloads still works`() {
366
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
367
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
368
+ val delegate = FakeDelegate(trustsChainOfSize = 2)
369
+ val tm =
370
+ AiaChasingExtendedTrustManager(delegate) { url, _ ->
371
+ if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
372
+ }
373
+
374
+ tm.checkServerTrusted(arrayOf(leaf), "RSA", null as Socket?)
375
+
376
+ assertEquals(listOf(1, 2), delegate.checkedChainSizes)
377
+ }
378
+
379
+ @Test
380
+ fun `the extended variant does not weaken trust`() {
381
+ val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
382
+ val r13 = CertFixtures.cert("letsencrypt-r13.pem")
383
+ // Trusts nothing, whatever the chain.
384
+ val delegate = FakeDomainConfigDelegate(trustsChainOfSize = -1)
385
+ val tm = AiaChasingExtendedTrustManager(delegate) { _, _ -> listOf(r13) }
386
+
387
+ assertThrows(CertificateException::class.java) {
388
+ tm.checkServerTrusted(arrayOf(leaf), "RSA", null as Socket?)
389
+ }
390
+
391
+ // The completed chain was offered to the delegate and refused, rather than assumed good.
392
+ assertEquals(listOf(1, 2), delegate.hostnameAwareChecks)
393
+ }
73
394
  }
@@ -0,0 +1,322 @@
1
+ package com.audiobrowser.tls
2
+
3
+ import java.net.InetAddress
4
+ import java.net.ServerSocket
5
+ import java.security.cert.X509Certificate
6
+ import java.util.concurrent.CountDownLatch
7
+ import java.util.concurrent.TimeUnit
8
+ import java.util.concurrent.atomic.AtomicInteger
9
+ import java.util.concurrent.atomic.AtomicReference
10
+ import javax.security.auth.x500.X500Principal
11
+ import kotlin.concurrent.thread
12
+ import org.junit.After
13
+ import org.junit.Assert.assertEquals
14
+ import org.junit.Assert.assertFalse
15
+ import org.junit.Assert.assertTrue
16
+ import org.junit.Test
17
+ import org.mockito.Mockito.mock
18
+ import org.mockito.Mockito.`when`
19
+
20
+ /**
21
+ * Exercises [CachingCertificateFetcher.fetch] against a real loopback server.
22
+ *
23
+ * The safety helpers have their own unit tests, but a predicate that is never consulted protects
24
+ * nothing: these cover the *wiring*, so that removing the `isSafeUrl` guard or swapping
25
+ * `readCapped` back for a plain read-to-EOF fails a test rather than passing silently.
26
+ */
27
+ class CachingCertificateFetcherCallSiteTest {
28
+
29
+ private var server: ServerSocket? = null
30
+
31
+ @After
32
+ fun tearDown() {
33
+ server?.close()
34
+ }
35
+
36
+ /**
37
+ * Serves [response] to every connection, recording the request line of the first. [handled]
38
+ * counts accepted connections.
39
+ */
40
+ private fun startServer(
41
+ response: ByteArray = "HTTP/1.0 404 Not Found\r\n\r\n".toByteArray(),
42
+ handled: AtomicInteger = AtomicInteger(),
43
+ firstRequest: AtomicReference<String> = AtomicReference(),
44
+ ): Int {
45
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
46
+ server = socket
47
+ thread(isDaemon = true) {
48
+ while (!socket.isClosed) {
49
+ try {
50
+ socket.accept().use { client ->
51
+ handled.incrementAndGet()
52
+ val request = ByteArray(1024)
53
+ val read = client.getInputStream().read(request)
54
+ firstRequest.compareAndSet(null, String(request, 0, maxOf(read, 0)))
55
+ client.getOutputStream().apply {
56
+ write(response)
57
+ flush()
58
+ }
59
+ }
60
+ } catch (e: Exception) {
61
+ return@thread // socket closed, or the client hung up
62
+ }
63
+ }
64
+ }
65
+ return socket.localPort
66
+ }
67
+
68
+ @Test
69
+ fun `a URL carrying CRLF never reaches the network`() {
70
+ val handled = AtomicInteger()
71
+ val port = startServer(handled = handled)
72
+
73
+ // Without the isSafeUrl guard this connects and emits "SET foo bar" as its own request line.
74
+ val certs = CachingCertificateFetcher().fetch("http://127.0.0.1:$port/\r\nSET foo bar\r\n")
75
+
76
+ assertTrue(certs.isEmpty())
77
+ assertEquals("the request must not be sent at all", 0, handled.get())
78
+ }
79
+
80
+ @Test
81
+ fun `a well-formed URL does reach the network, so the guard is not just refusing everything`() {
82
+ val handled = AtomicInteger()
83
+ val request = AtomicReference<String>()
84
+ val port = startServer(handled = handled, firstRequest = request)
85
+
86
+ CachingCertificateFetcher().fetch("http://127.0.0.1:$port/issuer.crt")
87
+
88
+ assertEquals(1, handled.get())
89
+ assertTrue("request line was: ${request.get()}", request.get().startsWith("GET /issuer.crt "))
90
+ }
91
+
92
+ @Test
93
+ fun `a non-default port is carried in the Host header`() {
94
+ val request = AtomicReference<String>()
95
+ val port = startServer(firstRequest = request)
96
+
97
+ CachingCertificateFetcher().fetch("http://127.0.0.1:$port/issuer.crt")
98
+
99
+ assertTrue(
100
+ "headers were: ${request.get()}",
101
+ request.get().contains("Host: 127.0.0.1:$port\r\n"),
102
+ )
103
+ }
104
+
105
+ @Test(timeout = 30_000)
106
+ fun `an oversized response is abandoned rather than read to EOF`() {
107
+ // Asserting only that the fetch fails would prove nothing — 4 MiB crosses loopback in
108
+ // milliseconds and would fail to parse either way. What has to be true is that the client
109
+ // stopped reading, which shows up as the server's write not draining.
110
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
111
+ server = socket
112
+ val written = AtomicInteger()
113
+ val finished = CountDownLatch(1)
114
+ thread(isDaemon = true) {
115
+ runCatching {
116
+ socket.accept().use { client ->
117
+ client.getInputStream().read(ByteArray(1024))
118
+ val out = client.getOutputStream()
119
+ out.write("HTTP/1.0 200 OK\r\n\r\n".toByteArray())
120
+ val chunk = ByteArray(64 * 1024)
121
+ // 64 MiB — far past the 1 MiB cap. Once the client stops reading and closes, this
122
+ // throws a broken pipe, which is the signal we want.
123
+ repeat(1024) {
124
+ out.write(chunk)
125
+ written.addAndGet(chunk.size)
126
+ }
127
+ }
128
+ }
129
+ finished.countDown()
130
+ }
131
+
132
+ val certs = CachingCertificateFetcher().fetch("http://127.0.0.1:${socket.localPort}/big.crt")
133
+
134
+ assertTrue(certs.isEmpty())
135
+ assertTrue(finished.await(20, TimeUnit.SECONDS))
136
+ // Socket buffers mean the server gets somewhat past the cap before the write fails, but it
137
+ // must be nowhere near the 64 MiB it wanted to send.
138
+ assertTrue(
139
+ "server wrote ${written.get()} bytes; the read was not cut off",
140
+ written.get() < 16 shl 20,
141
+ )
142
+ }
143
+
144
+ @Test(timeout = 30_000)
145
+ fun `a real certificate is fetched, parsed and then served from cache`() {
146
+ // The success path, end to end over a socket: every other wire test serves a failure, so a
147
+ // broken body offset or a broken cache write would not show up anywhere.
148
+ val der = CertFixtures.bytes("letsencrypt-r13.der")
149
+ val handled = AtomicInteger()
150
+ val port =
151
+ startServer(
152
+ response =
153
+ ("HTTP/1.0 200 OK\r\nContent-Type: application/pkix-cert\r\n\r\n").toByteArray() + der,
154
+ handled = handled,
155
+ )
156
+ val fetcher = CachingCertificateFetcher()
157
+ val url = "http://127.0.0.1:$port/r13.der"
158
+
159
+ val certs = fetcher.fetch(url)
160
+
161
+ assertEquals(1, certs.size)
162
+ assertEquals("CN=R13,O=Let's Encrypt,C=US", certs.first().subjectX500Principal.name)
163
+
164
+ // Second call must be served from the cache, not the network.
165
+ assertEquals(certs, fetcher.fetch(url))
166
+ assertEquals("a successful fetch must be cached", 1, handled.get())
167
+ }
168
+
169
+ /**
170
+ * A mock certificate whose AIA extension lists [url] as its one CA-Issuers location, so
171
+ * [AiaCertChaser.completeChain] chases it with this fetcher — the real wiring, end to end.
172
+ */
173
+ private fun certPointingAt(url: String): X509Certificate {
174
+ // Short-form DER lengths only; a loopback URL is far under 128 bytes.
175
+ fun der(tag: Int, content: ByteArray) =
176
+ byteArrayOf(tag.toByte(), content.size.toByte()) + content
177
+ val caIssuersOid = byteArrayOf(0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02)
178
+ val aia =
179
+ der(
180
+ 0x04,
181
+ der(
182
+ 0x30,
183
+ der(0x30, der(0x06, caIssuersOid) + der(0x86, url.toByteArray(Charsets.US_ASCII))),
184
+ ),
185
+ )
186
+ return mock(X509Certificate::class.java).also {
187
+ `when`(it.getExtensionValue("1.3.6.1.5.5.7.1.1")).thenReturn(aia)
188
+ // Distinct principals, or the chaser reads the mock as a self-signed root and never fetches.
189
+ `when`(it.subjectX500Principal).thenReturn(X500Principal("CN=leaf"))
190
+ `when`(it.issuerX500Principal).thenReturn(X500Principal("CN=issuer"))
191
+ }
192
+ }
193
+
194
+ @Test(timeout = 30_000)
195
+ fun `a drip-feeding server is cut off by the chase deadline, not only the per-fetch caps`() {
196
+ // One byte every 100 ms: each read lands well inside the socket timeout, so per-read limits
197
+ // never fire, and the per-fetch read budget alone would let this run for its full 10 s. The
198
+ // walk's 1 s budget must cut the fetch *itself* — not merely decline to start the next one.
199
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
200
+ server = socket
201
+ thread(isDaemon = true) {
202
+ runCatching {
203
+ socket.accept().use { client ->
204
+ client.getInputStream().read(ByteArray(1024))
205
+ val out = client.getOutputStream()
206
+ out.write("HTTP/1.0 200 OK\r\n\r\n".toByteArray())
207
+ out.flush()
208
+ repeat(300) { // 30 s of drip on offer; the client must not stay for it
209
+ out.write(0)
210
+ out.flush()
211
+ Thread.sleep(100)
212
+ }
213
+ }
214
+ }
215
+ }
216
+ val fetcher = CachingCertificateFetcher()
217
+ val cert = certPointingAt("http://127.0.0.1:${socket.localPort}/drip.crt")
218
+
219
+ val started = System.nanoTime()
220
+ val completed =
221
+ AiaCertChaser.completeChain(listOf(cert), budgetMs = 1_000) { url, deadline ->
222
+ fetcher.fetch(url, deadline)
223
+ }
224
+ val elapsedMs = (System.nanoTime() - started) / 1_000_000
225
+
226
+ assertEquals(listOf(cert), completed)
227
+ // Well past the 1 s budget to absorb scheduling noise, but far short of the 10 s the
228
+ // per-fetch read budget alone would have allowed.
229
+ assertTrue("chase ran for $elapsedMs ms", elapsedMs < 8_000)
230
+ }
231
+
232
+ @Test(timeout = 30_000)
233
+ fun `the chase deadline spans redirect hops, not just fetch starts`() {
234
+ // Every hop stalls 1 s and then redirects back to itself. Each hop sits comfortably inside
235
+ // the per-operation timeouts, so only a deadline threaded through the redirect recursion
236
+ // can end the fetch — without one it walks all 5 redirects, ~6 s here.
237
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
238
+ server = socket
239
+ thread(isDaemon = true) {
240
+ while (!socket.isClosed) {
241
+ try {
242
+ socket.accept().use { client ->
243
+ client.getInputStream().read(ByteArray(1024))
244
+ Thread.sleep(1_000)
245
+ client.getOutputStream().apply {
246
+ write("HTTP/1.0 302 Found\r\nLocation: /again.crt\r\n\r\n".toByteArray())
247
+ flush()
248
+ }
249
+ }
250
+ } catch (e: Exception) {
251
+ return@thread
252
+ }
253
+ }
254
+ }
255
+ val fetcher = CachingCertificateFetcher()
256
+
257
+ val started = System.nanoTime()
258
+ val certs =
259
+ fetcher.fetch(
260
+ "http://127.0.0.1:${socket.localPort}/loop.crt",
261
+ deadlineNanos = System.nanoTime() + 1_500 * 1_000_000L,
262
+ )
263
+ val elapsedMs = (System.nanoTime() - started) / 1_000_000
264
+
265
+ assertTrue(certs.isEmpty())
266
+ assertTrue("fetch ran for $elapsedMs ms across redirect hops", elapsedMs < 4_000)
267
+ }
268
+
269
+ @Test(timeout = 30_000)
270
+ fun `a server that accepts and never answers is bounded by the read timeout`() {
271
+ val socket = ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"))
272
+ server = socket
273
+ val accepted = CountDownLatch(1)
274
+ // Accept, then hold the connection open saying nothing at all.
275
+ thread(isDaemon = true) {
276
+ runCatching {
277
+ val client = socket.accept()
278
+ accepted.countDown()
279
+ Thread.sleep(60_000)
280
+ client.close()
281
+ }
282
+ }
283
+
284
+ val certs =
285
+ CachingCertificateFetcher(connectTimeoutMs = 2_000, readTimeoutMs = 1_000)
286
+ .fetch("http://127.0.0.1:${socket.localPort}/hangs.crt")
287
+
288
+ assertTrue(accepted.await(10, TimeUnit.SECONDS))
289
+ assertTrue(certs.isEmpty())
290
+ }
291
+
292
+ @Test
293
+ fun `a non-http scheme is never fetched`() {
294
+ val handled = AtomicInteger()
295
+ val port = startServer(handled = handled)
296
+
297
+ assertTrue(CachingCertificateFetcher().fetch("ftp://127.0.0.1:$port/issuer.crt").isEmpty())
298
+ assertTrue(CachingCertificateFetcher().fetch("file:///etc/hosts").isEmpty())
299
+ assertEquals(0, handled.get())
300
+ }
301
+
302
+ @Test
303
+ fun `a failed fetch is not cached, so it stays retryable`() {
304
+ val handled = AtomicInteger()
305
+ val port = startServer(handled = handled)
306
+ val fetcher = CachingCertificateFetcher()
307
+ val url = "http://127.0.0.1:$port/issuer.crt"
308
+
309
+ fetcher.fetch(url)
310
+ fetcher.fetch(url)
311
+
312
+ assertEquals("both attempts must hit the network", 2, handled.get())
313
+ }
314
+
315
+ @Test
316
+ fun `the cache is bounded`() {
317
+ // Far more distinct URLs than MAX_CACHE_ENTRIES; nothing is cached here (every fetch fails),
318
+ // but the bound is what stops an attacker-chosen URL space from growing without limit.
319
+ assertFalse(CachingCertificateFetcher.MAX_CACHE_ENTRIES <= 0)
320
+ assertTrue(CachingCertificateFetcher.MAX_CACHE_ENTRIES <= 128)
321
+ }
322
+ }