@ceralive/modem-control 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) hide show
  1. package/README.md +341 -0
  2. package/dist/backend/at-lease.d.ts +59 -0
  3. package/dist/backend/at-lease.js +118 -0
  4. package/dist/backend/cell-info.d.ts +46 -0
  5. package/dist/backend/cell-info.js +124 -0
  6. package/dist/backend/constants.d.ts +24 -0
  7. package/{src/backend/constants.ts → dist/backend/constants.js} +4 -9
  8. package/dist/backend/device-classifier.d.ts +46 -0
  9. package/dist/backend/device-classifier.js +258 -0
  10. package/dist/backend/enrichment.d.ts +28 -0
  11. package/dist/backend/enrichment.js +59 -0
  12. package/dist/backend/features.d.ts +61 -0
  13. package/dist/backend/features.js +109 -0
  14. package/dist/backend/identity-ladder.d.ts +57 -0
  15. package/dist/backend/identity-ladder.js +158 -0
  16. package/dist/backend/identity-registry.d.ts +51 -0
  17. package/dist/backend/identity-registry.js +101 -0
  18. package/dist/backend/index.d.ts +30 -0
  19. package/dist/backend/index.js +35 -0
  20. package/dist/backend/lifecycle-interlock.d.ts +25 -0
  21. package/dist/backend/lifecycle-interlock.js +18 -0
  22. package/dist/backend/managed-objects.d.ts +39 -0
  23. package/dist/backend/managed-objects.js +82 -0
  24. package/dist/backend/mapping.d.ts +11 -0
  25. package/dist/backend/mapping.js +148 -0
  26. package/dist/backend/mm-backend.d.ts +44 -0
  27. package/dist/backend/mm-backend.js +154 -0
  28. package/dist/backend/mm-location.d.ts +21 -0
  29. package/dist/backend/mm-location.js +237 -0
  30. package/dist/backend/mm-mutations.d.ts +42 -0
  31. package/dist/backend/mm-mutations.js +259 -0
  32. package/dist/backend/modem-actor.d.ts +41 -0
  33. package/dist/backend/modem-actor.js +78 -0
  34. package/dist/backend/nm-auto-apn.d.ts +54 -0
  35. package/dist/backend/nm-auto-apn.js +124 -0
  36. package/dist/backend/nm-gsm-fields.d.ts +23 -0
  37. package/dist/backend/nm-gsm-fields.js +107 -0
  38. package/dist/backend/nmcli-nm-port.d.ts +28 -0
  39. package/dist/backend/nmcli-nm-port.js +173 -0
  40. package/dist/backend/nmcli-runner.d.ts +24 -0
  41. package/dist/backend/nmcli-runner.js +35 -0
  42. package/dist/backend/observer.d.ts +37 -0
  43. package/dist/backend/observer.js +219 -0
  44. package/dist/backend/power-contract.d.ts +49 -0
  45. package/dist/backend/power-contract.js +34 -0
  46. package/dist/backend/recovery-attribution.d.ts +32 -0
  47. package/dist/backend/recovery-attribution.js +57 -0
  48. package/dist/backend/recovery-budget.d.ts +45 -0
  49. package/dist/backend/recovery-budget.js +44 -0
  50. package/dist/backend/recovery-ladder.d.ts +94 -0
  51. package/dist/backend/recovery-ladder.js +116 -0
  52. package/dist/backend/router-ethernet.d.ts +19 -0
  53. package/dist/backend/router-ethernet.js +66 -0
  54. package/dist/backend/row-store.d.ts +13 -0
  55. package/dist/backend/row-store.js +82 -0
  56. package/dist/backend/signal-setup.d.ts +30 -0
  57. package/dist/backend/signal-setup.js +92 -0
  58. package/dist/backend/sim-unlock.d.ts +13 -0
  59. package/dist/backend/sim-unlock.js +153 -0
  60. package/dist/backend/transition-preconditions.d.ts +101 -0
  61. package/dist/backend/transition-preconditions.js +132 -0
  62. package/dist/backend/usage/accounting.d.ts +39 -0
  63. package/dist/backend/usage/accounting.js +73 -0
  64. package/dist/backend/usage/billing-cycle.d.ts +11 -0
  65. package/dist/backend/usage/billing-cycle.js +39 -0
  66. package/dist/backend/usage/boot-id.d.ts +6 -0
  67. package/{src/backend/usage/boot-id.ts → dist/backend/usage/boot-id.js} +7 -7
  68. package/dist/backend/usage/index.d.ts +8 -0
  69. package/dist/backend/usage/index.js +11 -0
  70. package/dist/backend/usage/policy-store.d.ts +50 -0
  71. package/dist/backend/usage/policy-store.js +161 -0
  72. package/dist/backend/usage/policy-write.d.ts +65 -0
  73. package/dist/backend/usage/policy-write.js +112 -0
  74. package/dist/backend/usage/proc-net-dev.d.ts +18 -0
  75. package/{src/backend/usage/proc-net-dev.ts → dist/backend/usage/proc-net-dev.js} +39 -47
  76. package/dist/backend/usage/sampler.d.ts +75 -0
  77. package/dist/backend/usage/sampler.js +211 -0
  78. package/dist/backend/usage/store.d.ts +38 -0
  79. package/dist/backend/usage/store.js +126 -0
  80. package/dist/backend/usb-device-snapshot.d.ts +31 -0
  81. package/dist/backend/usb-device-snapshot.js +1 -0
  82. package/dist/backend/usb-enumerator.d.ts +21 -0
  83. package/dist/backend/usb-enumerator.js +153 -0
  84. package/dist/backend/usb-mode-transition.d.ts +29 -0
  85. package/dist/backend/usb-mode-transition.js +216 -0
  86. package/dist/band/band-names.d.ts +42 -0
  87. package/dist/band/band-names.js +150 -0
  88. package/dist/band/certification.d.ts +84 -0
  89. package/dist/band/certification.js +127 -0
  90. package/dist/band/certified-bands.json +4 -0
  91. package/dist/band/index.d.ts +2 -0
  92. package/dist/band/index.js +8 -0
  93. package/dist/capability/detect.d.ts +52 -0
  94. package/dist/capability/detect.js +86 -0
  95. package/dist/capability/five-g-preference.d.ts +104 -0
  96. package/dist/capability/five-g-preference.js +171 -0
  97. package/dist/capability/index.d.ts +3 -0
  98. package/dist/capability/index.js +10 -0
  99. package/dist/capability/support-claim.d.ts +39 -0
  100. package/dist/capability/support-claim.js +81 -0
  101. package/dist/domain/brand.d.ts +10 -0
  102. package/dist/domain/brand.js +21 -0
  103. package/dist/domain/errors.d.ts +32 -0
  104. package/dist/domain/errors.js +48 -0
  105. package/dist/domain/generation.d.ts +8 -0
  106. package/dist/domain/generation.js +12 -0
  107. package/dist/domain/guards.d.ts +6 -0
  108. package/dist/domain/guards.js +127 -0
  109. package/dist/domain/identity.d.ts +109 -0
  110. package/dist/domain/identity.js +86 -0
  111. package/dist/domain/index.d.ts +14 -0
  112. package/dist/domain/index.js +18 -0
  113. package/dist/domain/mm-enums.d.ts +12 -0
  114. package/dist/domain/mm-enums.js +139 -0
  115. package/dist/domain/modem-presentation.d.ts +10 -0
  116. package/dist/domain/modem-presentation.js +39 -0
  117. package/dist/domain/observation.d.ts +39 -0
  118. package/dist/domain/observation.js +4 -0
  119. package/dist/domain/operation.d.ts +118 -0
  120. package/dist/domain/operation.js +85 -0
  121. package/dist/domain/physical-identity.d.ts +43 -0
  122. package/dist/domain/physical-identity.js +113 -0
  123. package/{src/domain/policy.ts → dist/domain/policy.d.ts} +34 -69
  124. package/dist/domain/policy.js +38 -0
  125. package/dist/domain/shadow-divergence.d.ts +27 -0
  126. package/dist/domain/shadow-divergence.js +70 -0
  127. package/dist/domain/snapshot.d.ts +53 -0
  128. package/dist/domain/snapshot.js +75 -0
  129. package/{src/domain/state.ts → dist/domain/state.d.ts} +23 -122
  130. package/dist/domain/state.js +38 -0
  131. package/dist/fcc/coverage.d.ts +63 -0
  132. package/dist/fcc/coverage.js +102 -0
  133. package/dist/fcc/index.d.ts +3 -0
  134. package/dist/fcc/index.js +12 -0
  135. package/dist/fcc/policy-store.d.ts +40 -0
  136. package/dist/fcc/policy-store.js +146 -0
  137. package/dist/fcc/policy-write.d.ts +32 -0
  138. package/dist/fcc/policy-write.js +37 -0
  139. package/dist/hardware/hilink-protocol.d.ts +39 -0
  140. package/dist/hardware/hilink-protocol.js +58 -0
  141. package/dist/hardware/index.d.ts +3 -0
  142. package/dist/hardware/index.js +15 -0
  143. package/dist/hardware/router-parsers.d.ts +89 -0
  144. package/dist/hardware/router-parsers.js +234 -0
  145. package/dist/index.d.ts +20 -0
  146. package/dist/index.js +27 -0
  147. package/dist/journal/codec.d.ts +44 -0
  148. package/dist/journal/codec.js +198 -0
  149. package/dist/journal/engine.d.ts +28 -0
  150. package/dist/journal/engine.js +68 -0
  151. package/dist/journal/entry.d.ts +74 -0
  152. package/dist/journal/entry.js +56 -0
  153. package/dist/journal/index.d.ts +6 -0
  154. package/dist/journal/index.js +6 -0
  155. package/dist/journal/legacy-ceraui.d.ts +73 -0
  156. package/dist/journal/legacy-ceraui.js +227 -0
  157. package/dist/journal/recovery.d.ts +58 -0
  158. package/dist/journal/recovery.js +117 -0
  159. package/dist/journal/store.d.ts +55 -0
  160. package/dist/journal/store.js +150 -0
  161. package/dist/location/fix-state.d.ts +53 -0
  162. package/dist/location/fix-state.js +75 -0
  163. package/dist/location/index.d.ts +2 -0
  164. package/dist/location/index.js +8 -0
  165. package/dist/location/nmea.d.ts +10 -0
  166. package/dist/location/nmea.js +89 -0
  167. package/dist/observations/envelope.d.ts +63 -0
  168. package/dist/observations/envelope.js +77 -0
  169. package/dist/observations/freshness.d.ts +28 -0
  170. package/dist/observations/freshness.js +76 -0
  171. package/dist/observations/index.d.ts +13 -0
  172. package/dist/observations/index.js +24 -0
  173. package/dist/observations/metric.d.ts +61 -0
  174. package/dist/observations/metric.js +73 -0
  175. package/dist/observations/model.d.ts +80 -0
  176. package/dist/observations/model.js +11 -0
  177. package/dist/observations/provenance.d.ts +94 -0
  178. package/dist/observations/provenance.js +67 -0
  179. package/dist/observations/raw.d.ts +42 -0
  180. package/dist/observations/raw.js +146 -0
  181. package/dist/observations/reading.d.ts +51 -0
  182. package/dist/observations/reading.js +65 -0
  183. package/dist/observations/sources/hilink.d.ts +10 -0
  184. package/dist/observations/sources/hilink.js +84 -0
  185. package/dist/observations/sources/modemmanager.d.ts +18 -0
  186. package/dist/observations/sources/modemmanager.js +239 -0
  187. package/dist/observations/sources/router-shared.d.ts +29 -0
  188. package/dist/observations/sources/router-shared.js +68 -0
  189. package/dist/observations/sources/ufi.d.ts +10 -0
  190. package/dist/observations/sources/ufi.js +111 -0
  191. package/dist/observations/sources/zte.d.ts +7 -0
  192. package/dist/observations/sources/zte.js +71 -0
  193. package/dist/observations/state-separation.d.ts +64 -0
  194. package/dist/observations/state-separation.js +52 -0
  195. package/dist/operation-ids.d.ts +2 -0
  196. package/dist/operation-ids.js +26 -0
  197. package/dist/operations/contracts.d.ts +72 -0
  198. package/dist/operations/contracts.js +1 -0
  199. package/dist/operations/index.d.ts +1 -0
  200. package/dist/operations/index.js +1 -0
  201. package/dist/operations/operation-engine.d.ts +16 -0
  202. package/dist/operations/operation-engine.js +194 -0
  203. package/dist/ports/index.d.ts +12 -0
  204. package/{src/ports/index.ts → dist/ports/index.js} +12 -8
  205. package/dist/ports/location.d.ts +87 -0
  206. package/dist/ports/location.js +36 -0
  207. package/dist/ports/modem-manager.d.ts +89 -0
  208. package/dist/ports/modem-manager.js +9 -0
  209. package/dist/ports/mutation-admission.d.ts +27 -0
  210. package/dist/ports/mutation-admission.js +9 -0
  211. package/dist/ports/network-manager.d.ts +68 -0
  212. package/dist/ports/network-manager.js +13 -0
  213. package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
  214. package/dist/ports/observation.js +7 -0
  215. package/dist/ports/ops.d.ts +44 -0
  216. package/dist/ports/ops.js +16 -0
  217. package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
  218. package/dist/ports/receipts.js +9 -0
  219. package/dist/ports/reconcile.d.ts +33 -0
  220. package/dist/ports/reconcile.js +200 -0
  221. package/dist/ports/resource-ownership.d.ts +29 -0
  222. package/dist/ports/resource-ownership.js +1 -0
  223. package/dist/ports/router.d.ts +19 -0
  224. package/dist/ports/router.js +7 -0
  225. package/dist/ports/sms.d.ts +64 -0
  226. package/dist/ports/sms.js +24 -0
  227. package/dist/ports/uhubctl.d.ts +6 -0
  228. package/dist/ports/uhubctl.js +1 -0
  229. package/dist/providers/contracts.d.ts +124 -0
  230. package/dist/providers/contracts.js +10 -0
  231. package/dist/providers/huawei-hilink/index.d.ts +2 -0
  232. package/dist/providers/huawei-hilink/index.js +2 -0
  233. package/dist/providers/huawei-hilink/operations.d.ts +20 -0
  234. package/dist/providers/huawei-hilink/operations.js +56 -0
  235. package/dist/providers/huawei-hilink/provider.d.ts +52 -0
  236. package/dist/providers/huawei-hilink/provider.js +76 -0
  237. package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
  238. package/dist/providers/huawei-hilink/runtime.js +171 -0
  239. package/dist/providers/huawei-hilink/session.d.ts +28 -0
  240. package/dist/providers/huawei-hilink/session.js +120 -0
  241. package/dist/providers/huawei-hilink/transport.d.ts +19 -0
  242. package/dist/providers/huawei-hilink/transport.js +1 -0
  243. package/dist/providers/index.d.ts +8 -0
  244. package/dist/providers/index.js +8 -0
  245. package/dist/providers/matcher.d.ts +3 -0
  246. package/dist/providers/matcher.js +205 -0
  247. package/dist/providers/modem-manager/errors.d.ts +7 -0
  248. package/dist/providers/modem-manager/errors.js +37 -0
  249. package/dist/providers/modem-manager/generic-operations.d.ts +10 -0
  250. package/dist/providers/modem-manager/generic-operations.js +209 -0
  251. package/dist/providers/modem-manager/index.d.ts +4 -0
  252. package/dist/providers/modem-manager/index.js +4 -0
  253. package/dist/providers/modem-manager/module-operations.d.ts +21 -0
  254. package/dist/providers/modem-manager/module-operations.js +118 -0
  255. package/dist/providers/modem-manager/provider.d.ts +41 -0
  256. package/dist/providers/modem-manager/provider.js +155 -0
  257. package/dist/providers/modem-manager/runtime-composition-operation.d.ts +32 -0
  258. package/dist/providers/modem-manager/runtime-composition-operation.js +151 -0
  259. package/dist/providers/modem-manager/snapshot.d.ts +5 -0
  260. package/dist/providers/modem-manager/snapshot.js +204 -0
  261. package/dist/providers/modem-manager/types.d.ts +137 -0
  262. package/dist/providers/modem-manager/types.js +1 -0
  263. package/dist/providers/network-manager/adapter.d.ts +71 -0
  264. package/dist/providers/network-manager/adapter.js +348 -0
  265. package/dist/providers/network-manager/index.d.ts +2 -0
  266. package/dist/providers/network-manager/index.js +2 -0
  267. package/dist/providers/network-manager/types.d.ts +171 -0
  268. package/dist/providers/network-manager/types.js +77 -0
  269. package/dist/providers/registry.d.ts +13 -0
  270. package/dist/providers/registry.js +33 -0
  271. package/dist/providers/ufi-himi/index.d.ts +6 -0
  272. package/dist/providers/ufi-himi/index.js +6 -0
  273. package/dist/providers/ufi-himi/operations.d.ts +41 -0
  274. package/dist/providers/ufi-himi/operations.js +66 -0
  275. package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
  276. package/dist/providers/ufi-himi/prohibitions.js +88 -0
  277. package/dist/providers/ufi-himi/provider.d.ts +41 -0
  278. package/dist/providers/ufi-himi/provider.js +204 -0
  279. package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
  280. package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
  281. package/dist/providers/ufi-himi/session.d.ts +46 -0
  282. package/dist/providers/ufi-himi/session.js +92 -0
  283. package/dist/providers/ufi-himi/transport.d.ts +29 -0
  284. package/dist/providers/ufi-himi/transport.js +25 -0
  285. package/dist/providers/zte-goform/index.d.ts +2 -0
  286. package/dist/providers/zte-goform/index.js +2 -0
  287. package/dist/providers/zte-goform/provider.d.ts +56 -0
  288. package/dist/providers/zte-goform/provider.js +101 -0
  289. package/dist/providers/zte-goform/session.d.ts +23 -0
  290. package/dist/providers/zte-goform/session.js +197 -0
  291. package/dist/providers/zte-goform/transport.d.ts +16 -0
  292. package/dist/providers/zte-goform/transport.js +1 -0
  293. package/dist/radio/band-truth.d.ts +50 -0
  294. package/dist/radio/band-truth.js +92 -0
  295. package/dist/radio/index.d.ts +3 -0
  296. package/dist/radio/index.js +10 -0
  297. package/dist/radio/mode-combinations.d.ts +88 -0
  298. package/dist/radio/mode-combinations.js +198 -0
  299. package/dist/radio/mode-truth.d.ts +67 -0
  300. package/dist/radio/mode-truth.js +112 -0
  301. package/dist/redact.d.ts +15 -0
  302. package/dist/redact.js +189 -0
  303. package/dist/safety/composition-root.d.ts +28 -0
  304. package/dist/safety/composition-root.js +65 -0
  305. package/dist/safety/flock-resource-ownership.d.ts +11 -0
  306. package/dist/safety/flock-resource-ownership.js +138 -0
  307. package/dist/safety/index.d.ts +2 -0
  308. package/dist/safety/index.js +2 -0
  309. package/dist/sms/dbus-messaging.d.ts +17 -0
  310. package/dist/sms/dbus-messaging.js +185 -0
  311. package/dist/sms/inbox-store.d.ts +10 -0
  312. package/dist/sms/inbox-store.js +82 -0
  313. package/dist/sms/index.d.ts +4 -0
  314. package/dist/sms/index.js +10 -0
  315. package/dist/sms/mmcli-parse.d.ts +54 -0
  316. package/dist/sms/mmcli-parse.js +224 -0
  317. package/dist/sms/normalize.d.ts +42 -0
  318. package/dist/sms/normalize.js +95 -0
  319. package/dist/testing/domain-fakes.d.ts +58 -0
  320. package/dist/testing/domain-fakes.js +98 -0
  321. package/dist/testing/index.d.ts +2 -0
  322. package/dist/testing/index.js +17 -0
  323. package/dist/testing/provider-fakes.d.ts +45 -0
  324. package/dist/testing/provider-fakes.js +64 -0
  325. package/dist/transport/calls.d.ts +9 -0
  326. package/dist/transport/calls.js +88 -0
  327. package/dist/transport/codec.d.ts +3 -0
  328. package/dist/transport/codec.js +207 -0
  329. package/dist/transport/dbus-native.d.ts +57 -0
  330. package/dist/transport/dbus-native.js +17 -0
  331. package/dist/transport/errors.d.ts +21 -0
  332. package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
  333. package/dist/transport/index.d.ts +4 -0
  334. package/dist/transport/index.js +9 -0
  335. package/dist/transport/signals.d.ts +15 -0
  336. package/dist/transport/signals.js +123 -0
  337. package/dist/transport/signature.d.ts +7 -0
  338. package/dist/transport/signature.js +94 -0
  339. package/dist/transport/transport.d.ts +2 -0
  340. package/dist/transport/transport.js +202 -0
  341. package/dist/transport/types.d.ts +61 -0
  342. package/dist/transport/types.js +19 -0
  343. package/dist/usb-mode/catalog-schema.d.ts +139 -0
  344. package/dist/usb-mode/catalog-schema.js +97 -0
  345. package/dist/usb-mode/catalog.d.ts +21 -0
  346. package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
  347. package/dist/usb-mode/certified-catalog.json +67 -0
  348. package/dist/usb-mode/index.d.ts +6 -0
  349. package/dist/usb-mode/index.js +16 -0
  350. package/dist/usb-mode/ingestion.d.ts +111 -0
  351. package/dist/usb-mode/ingestion.js +187 -0
  352. package/dist/usb-mode/promotion-review.d.ts +21 -0
  353. package/dist/usb-mode/promotion-review.js +87 -0
  354. package/dist/usb-mode/runtime-capability.d.ts +59 -0
  355. package/dist/usb-mode/runtime-capability.js +157 -0
  356. package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
  357. package/dist/usb-mode/usb-devices-parse.js +157 -0
  358. package/dist/ussd/calls.d.ts +32 -0
  359. package/dist/ussd/calls.js +96 -0
  360. package/dist/ussd/index.d.ts +5 -0
  361. package/dist/ussd/index.js +12 -0
  362. package/dist/ussd/mm-ussd.d.ts +37 -0
  363. package/dist/ussd/mm-ussd.js +205 -0
  364. package/dist/ussd/refusal.d.ts +53 -0
  365. package/dist/ussd/refusal.js +154 -0
  366. package/dist/ussd/registration.d.ts +20 -0
  367. package/dist/ussd/registration.js +101 -0
  368. package/dist/ussd/session.d.ts +106 -0
  369. package/dist/ussd/session.js +163 -0
  370. package/package.json +38 -4
  371. package/src/backend/at-lease.test.ts +0 -106
  372. package/src/backend/at-lease.ts +0 -158
  373. package/src/backend/cell-info.test.ts +0 -154
  374. package/src/backend/cell-info.ts +0 -160
  375. package/src/backend/device-classifier.test.ts +0 -168
  376. package/src/backend/device-classifier.ts +0 -248
  377. package/src/backend/enrichment.ts +0 -96
  378. package/src/backend/features.test.ts +0 -162
  379. package/src/backend/features.ts +0 -179
  380. package/src/backend/identity-ladder.test.ts +0 -117
  381. package/src/backend/identity-ladder.ts +0 -221
  382. package/src/backend/identity-registry.test.ts +0 -89
  383. package/src/backend/identity-registry.ts +0 -151
  384. package/src/backend/index.ts +0 -236
  385. package/src/backend/lifecycle-interlock.ts +0 -38
  386. package/src/backend/managed-objects.ts +0 -108
  387. package/src/backend/mapping.ts +0 -160
  388. package/src/backend/mm-backend.ts +0 -191
  389. package/src/backend/mm-mutations.ts +0 -228
  390. package/src/backend/modem-actor.test.ts +0 -95
  391. package/src/backend/modem-actor.ts +0 -112
  392. package/src/backend/nm-auto-apn.ts +0 -161
  393. package/src/backend/nm-gsm-fields.ts +0 -122
  394. package/src/backend/nmcli-nm-port.ts +0 -228
  395. package/src/backend/nmcli-runner.ts +0 -52
  396. package/src/backend/observer.ts +0 -297
  397. package/src/backend/power-contract.test.ts +0 -40
  398. package/src/backend/power-contract.ts +0 -83
  399. package/src/backend/recovery-attribution.test.ts +0 -102
  400. package/src/backend/recovery-attribution.ts +0 -86
  401. package/src/backend/recovery-budget.test.ts +0 -64
  402. package/src/backend/recovery-budget.ts +0 -84
  403. package/src/backend/recovery-ladder.test.ts +0 -257
  404. package/src/backend/recovery-ladder.ts +0 -249
  405. package/src/backend/router-ethernet.test.ts +0 -71
  406. package/src/backend/router-ethernet.ts +0 -90
  407. package/src/backend/row-store.ts +0 -105
  408. package/src/backend/signal-setup.ts +0 -112
  409. package/src/backend/sim-unlock.ts +0 -193
  410. package/src/backend/transition-preconditions.ts +0 -149
  411. package/src/backend/uhubctl-power-hook.test.ts +0 -274
  412. package/src/backend/uhubctl-power-hook.ts +0 -377
  413. package/src/backend/usage/accounting.test.ts +0 -147
  414. package/src/backend/usage/accounting.ts +0 -123
  415. package/src/backend/usage/billing-cycle.test.ts +0 -62
  416. package/src/backend/usage/billing-cycle.ts +0 -45
  417. package/src/backend/usage/index.ts +0 -60
  418. package/src/backend/usage/policy-store.test.ts +0 -164
  419. package/src/backend/usage/policy-store.ts +0 -216
  420. package/src/backend/usage/policy-write.test.ts +0 -198
  421. package/src/backend/usage/policy-write.ts +0 -207
  422. package/src/backend/usage/proc-net-dev.test.ts +0 -56
  423. package/src/backend/usage/sampler.test.ts +0 -327
  424. package/src/backend/usage/sampler.ts +0 -282
  425. package/src/backend/usage/store.test.ts +0 -148
  426. package/src/backend/usage/store.ts +0 -177
  427. package/src/backend/usb-enumerator.test.ts +0 -87
  428. package/src/backend/usb-enumerator.ts +0 -181
  429. package/src/backend/usb-mode-transition.test.ts +0 -323
  430. package/src/backend/usb-mode-transition.ts +0 -253
  431. package/src/domain/brand.ts +0 -29
  432. package/src/domain/errors.ts +0 -77
  433. package/src/domain/guards.test.ts +0 -218
  434. package/src/domain/guards.ts +0 -144
  435. package/src/domain/identity.test.ts +0 -83
  436. package/src/domain/identity.ts +0 -165
  437. package/src/domain/index.ts +0 -12
  438. package/src/domain/snapshot.test.ts +0 -266
  439. package/src/domain/snapshot.ts +0 -120
  440. package/src/index.test.ts +0 -6
  441. package/src/index.ts +0 -15
  442. package/src/ports/README.md +0 -61
  443. package/src/ports/forbidden-surface.test.ts +0 -241
  444. package/src/ports/modem-manager.ts +0 -72
  445. package/src/ports/network-manager.ts +0 -87
  446. package/src/ports/ops.ts +0 -60
  447. package/src/ports/ops.type-test.ts +0 -39
  448. package/src/ports/receipts.test.ts +0 -153
  449. package/src/ports/reconcile.test.ts +0 -152
  450. package/src/ports/reconcile.ts +0 -338
  451. package/src/ports/router.ts +0 -29
  452. package/src/redact.test.ts +0 -82
  453. package/src/redact.ts +0 -73
  454. package/src/transport/README.md +0 -65
  455. package/src/transport/calls.ts +0 -113
  456. package/src/transport/characterization.test.ts +0 -260
  457. package/src/transport/codec.test.ts +0 -118
  458. package/src/transport/codec.ts +0 -240
  459. package/src/transport/conformance-python.test.ts +0 -152
  460. package/src/transport/conformance-same-lib.test.ts +0 -115
  461. package/src/transport/dbus-native-lib.d.ts +0 -19
  462. package/src/transport/dbus-native.ts +0 -85
  463. package/src/transport/index.ts +0 -30
  464. package/src/transport/no-library-leak.test.ts +0 -61
  465. package/src/transport/reliability.test.ts +0 -173
  466. package/src/transport/signals.ts +0 -150
  467. package/src/transport/signature.ts +0 -110
  468. package/src/transport/test-support/fake-service.ts +0 -168
  469. package/src/transport/test-support/independent-producer.py +0 -110
  470. package/src/transport/test-support/private-bus.ts +0 -66
  471. package/src/transport/transport.ts +0 -250
  472. package/src/transport/types.ts +0 -118
  473. package/src/usb-mode/catalog-schema.test.ts +0 -181
  474. package/src/usb-mode/catalog-schema.ts +0 -113
  475. package/src/usb-mode/certified-catalog.json +0 -67
  476. package/src/usb-mode/index.ts +0 -58
  477. package/src/usb-mode/ingestion.test.ts +0 -268
  478. package/src/usb-mode/ingestion.ts +0 -297
  479. package/src/usb-mode/promotion-review.ts +0 -117
  480. package/src/usb-mode/usb-devices-parse.ts +0 -196
@@ -1,274 +0,0 @@
1
- // The uhubctl power hook — the refusals matter more than the happy path:
2
- // - a mapped key cycles the right port and reports `applied` only on re-enumeration
3
- // - an UNMAPPED key is `unsupported` with ZERO commands run
4
- // - a non-zero uhubctl exit is `failed` and never claims re-enumeration
5
- // - a modem that never comes back is `failed` with expected-vs-observed
6
- // - wiring this hook into the ladder does NOT arm it: recovery.enabled=false still
7
- // fires zero cycles
8
-
9
- import { describe, expect, test } from 'bun:test';
10
- import { epochMillis, runtimePath } from '../domain';
11
- import { ModemActor } from './modem-actor';
12
- import { RecoveryLadder, type RecoveryRequest, type RecoverySteps } from './recovery-ladder';
13
- import {
14
- createUhubctlPowerHook,
15
- parseUhubctlPortMap,
16
- type UhubctlPortMap,
17
- type UhubctlResult,
18
- type UhubctlRunner,
19
- type UsbEnumerationPoller,
20
- uhubctlCycleArgv,
21
- } from './uhubctl-power-hook';
22
-
23
- const STABLE_KEY = 'slot:a';
24
- const ID_PATH = 'platform-fc800000.usb-usb-0:1.4.1:1.2';
25
-
26
- const PORTS: UhubctlPortMap = {
27
- [STABLE_KEY]: { hubLocation: '1-1.4', port: 1 },
28
- };
29
-
30
- /** A runner that records every argv it was handed and returns a canned result. */
31
- function fakeRunner(result: UhubctlResult, calls: string[][]): UhubctlRunner {
32
- return {
33
- run(argv) {
34
- calls.push([...argv]);
35
- return result;
36
- },
37
- };
38
- }
39
-
40
- const OK: UhubctlResult = { stdout: 'Sent power off request\n', stderr: '', exitCode: 0 };
41
-
42
- /** A poller that walks a scripted sequence of ID_PATH observations. */
43
- function scriptedPoller(sequence: readonly (string | undefined)[]): UsbEnumerationPoller {
44
- let index = 0;
45
- return {
46
- idPathFor() {
47
- const value = sequence[Math.min(index, sequence.length - 1)];
48
- index += 1;
49
- return value;
50
- },
51
- };
52
- }
53
-
54
- /** A clock that advances a fixed step per read — makes the timeout loop deterministic. */
55
- function steppingClock(stepMs: number): () => number {
56
- let value = 0;
57
- return () => {
58
- const current = value;
59
- value += stepMs;
60
- return current;
61
- };
62
- }
63
-
64
- const context = { stableKey: STABLE_KEY, at: epochMillis(0) };
65
- const noSleep = (): Promise<void> => Promise.resolve();
66
-
67
- describe('uhubctl power hook — a mapped key cycles its port', () => {
68
- test('applied: the exact argv is run and the SAME ID_PATH comes back', async () => {
69
- const calls: string[][] = [];
70
- const hook = createUhubctlPowerHook({
71
- ports: PORTS,
72
- runner: fakeRunner(OK, calls),
73
- // Pre-cut observation, then absent, then the same path returns.
74
- poller: scriptedPoller([ID_PATH, undefined, ID_PATH]),
75
- sleep: noSleep,
76
- });
77
-
78
- expect(hook.capability.power).toBe('usb-hub-port-cycle');
79
-
80
- const result = await hook.cycle(context);
81
- expect(result.status).toBe('applied');
82
- expect(result.reason).toContain(ID_PATH);
83
- // Argv array, no shell, allowlisted flags — asserted byte-for-byte.
84
- expect(calls).toEqual([['-l', '1-1.4', '-p', '1', '-a', 'cycle', '-d', '3']]);
85
- });
86
-
87
- test('the argv builder refuses a token that is not allowlisted', () => {
88
- expect(uhubctlCycleArgv({ hubLocation: '1-1.4', port: 1 }, 3)).toEqual([
89
- '-l',
90
- '1-1.4',
91
- '-p',
92
- '1',
93
- '-a',
94
- 'cycle',
95
- '-d',
96
- '3',
97
- ]);
98
- // A mapping that evaded the schema cannot smuggle a flag into the argv.
99
- expect(() => uhubctlCycleArgv({ hubLocation: '--force', port: 1 }, 3)).toThrow('allowlisted');
100
- });
101
-
102
- test('the schema rejects a hub location that is not a bus-port path', () => {
103
- expect(() =>
104
- parseUhubctlPortMap('{"slot:a":{"hubLocation":"; rm -rf /","port":1}}', 'x'),
105
- ).toThrow('hubLocation');
106
- });
107
- });
108
-
109
- describe('uhubctl power hook — an unmapped stable key is unsupported', () => {
110
- test('no mapping ⇒ unsupported, and the runner is NEVER invoked', async () => {
111
- const calls: string[][] = [];
112
- const hook = createUhubctlPowerHook({
113
- ports: PORTS,
114
- runner: fakeRunner(OK, calls),
115
- poller: { idPathFor: () => Promise.reject(new Error('poller must not be consulted')) },
116
- sleep: noSleep,
117
- });
118
- const result = await hook.cycle({ stableKey: 'slot:unknown', at: epochMillis(0) });
119
- expect(result.status).toBe('unsupported');
120
- expect(result.reason).toContain('slot:unknown');
121
- expect(calls).toEqual([]);
122
- });
123
- });
124
-
125
- describe('uhubctl power hook — a failing cycle command fails', () => {
126
- test('a non-zero exit is failed, carries stderr, and never claims re-enumeration', async () => {
127
- const calls: string[][] = [];
128
- const hook = createUhubctlPowerHook({
129
- ports: PORTS,
130
- runner: fakeRunner(
131
- { stdout: '', stderr: 'No compatible devices detected!', exitCode: 1 },
132
- calls,
133
- ),
134
- poller: scriptedPoller([ID_PATH, ID_PATH]),
135
- sleep: noSleep,
136
- });
137
- const result = await hook.cycle(context);
138
- expect(result.status).toBe('failed');
139
- expect(result.reason).toContain('exited 1');
140
- expect(result.reason).toContain('No compatible devices detected!');
141
- expect(calls).toHaveLength(1);
142
- });
143
-
144
- test('a runner that throws is failed, not an unhandled rejection', async () => {
145
- const hook = createUhubctlPowerHook({
146
- ports: PORTS,
147
- runner: { run: () => Promise.reject(new Error('uhubctl: command not found')) },
148
- poller: scriptedPoller([ID_PATH]),
149
- sleep: noSleep,
150
- });
151
- const result = await hook.cycle(context);
152
- expect(result.status).toBe('failed');
153
- expect(result.reason).toContain('command not found');
154
- });
155
- });
156
-
157
- describe('uhubctl power hook — an enumeration timeout fails', () => {
158
- test('the modem never returning is failed with expected-vs-observed', async () => {
159
- const calls: string[][] = [];
160
- const hook = createUhubctlPowerHook({
161
- ports: PORTS,
162
- runner: fakeRunner(OK, calls),
163
- // Seen before the cut, then gone forever.
164
- poller: scriptedPoller([ID_PATH, undefined]),
165
- enumerationTimeoutMs: 1000,
166
- pollIntervalMs: 250,
167
- now: steppingClock(400),
168
- sleep: noSleep,
169
- });
170
- const result = await hook.cycle(context);
171
- expect(result.status).toBe('failed');
172
- expect(result.reason).toContain('did not re-enumerate within 1000ms');
173
- expect(result.reason).toContain(ID_PATH);
174
- expect(result.reason).toContain('no device');
175
- // The port WAS cycled — the failure is the postcondition, not the command.
176
- expect(calls).toHaveLength(1);
177
- });
178
-
179
- test('a DIFFERENT device appearing at that key is not accepted as recovery', async () => {
180
- const hook = createUhubctlPowerHook({
181
- ports: PORTS,
182
- runner: fakeRunner(OK, []),
183
- poller: scriptedPoller([ID_PATH, 'platform-fc800000.usb-usb-0:9.9:1.0']),
184
- enumerationTimeoutMs: 1000,
185
- now: steppingClock(400),
186
- sleep: noSleep,
187
- });
188
- const result = await hook.cycle(context);
189
- expect(result.status).toBe('failed');
190
- expect(result.reason).toContain('9.9');
191
- });
192
-
193
- test('an aborted signal ends the wait instead of hanging', async () => {
194
- const controller = new AbortController();
195
- controller.abort();
196
- const hook = createUhubctlPowerHook({
197
- ports: PORTS,
198
- runner: fakeRunner(OK, []),
199
- poller: scriptedPoller([ID_PATH, undefined]),
200
- signal: controller.signal,
201
- sleep: noSleep,
202
- });
203
- const result = await hook.cycle(context);
204
- expect(result.status).toBe('failed');
205
- expect(result.reason).toContain('cancelled');
206
- });
207
- });
208
-
209
- describe('uhubctl power hook — wiring it in does NOT arm recovery', () => {
210
- test('recovery.enabled=false fires ZERO uhubctl cycles even with a real hook installed', async () => {
211
- const calls: string[][] = [];
212
- const hook = createUhubctlPowerHook({
213
- ports: PORTS,
214
- runner: fakeRunner(OK, calls),
215
- poller: { idPathFor: () => Promise.reject(new Error('poller must not be consulted')) },
216
- sleep: noSleep,
217
- });
218
- const throwingSteps: RecoverySteps = {
219
- nmCycle: () => Promise.reject(new Error('nmCycle must not fire')),
220
- mmCycle: () => Promise.reject(new Error('mmCycle must not fire')),
221
- reset: () => Promise.reject(new Error('reset must not fire')),
222
- };
223
- const request: RecoveryRequest = {
224
- stableKey: STABLE_KEY,
225
- modem: runtimePath('/org/freedesktop/ModemManager1/Modem/0'),
226
- attribution: 'modem-fault',
227
- now: epochMillis(0),
228
- probeHealthy: () => Promise.resolve(false),
229
- };
230
- const ladder = new RecoveryLadder({
231
- actor: new ModemActor(),
232
- steps: throwingSteps,
233
- powerHook: hook,
234
- });
235
-
236
- const outcome = await ladder.run({ enabled: false }, request);
237
-
238
- expect(outcome.kind).toBe('disabled');
239
- expect(outcome.steps).toEqual([]);
240
- // The whole point: a REAL power hook is installed and still nothing ran.
241
- expect(calls).toEqual([]);
242
- });
243
-
244
- test('the same hook DOES cycle once recovery is explicitly enabled', async () => {
245
- const calls: string[][] = [];
246
- const hook = createUhubctlPowerHook({
247
- ports: PORTS,
248
- runner: fakeRunner(OK, calls),
249
- poller: scriptedPoller([ID_PATH, ID_PATH]),
250
- sleep: noSleep,
251
- });
252
- const ladder = new RecoveryLadder({
253
- actor: new ModemActor(),
254
- steps: {
255
- nmCycle: () => Promise.resolve({ status: 'failed', reason: 'x' }),
256
- mmCycle: () => Promise.resolve({ status: 'failed', reason: 'x' }),
257
- reset: () => Promise.resolve({ status: 'failed', reason: 'x' }),
258
- },
259
- powerHook: hook,
260
- });
261
- const outcome = await ladder.run(
262
- { enabled: true },
263
- {
264
- stableKey: STABLE_KEY,
265
- modem: runtimePath('/org/freedesktop/ModemManager1/Modem/0'),
266
- attribution: 'modem-fault',
267
- now: epochMillis(0),
268
- probeHealthy: () => Promise.resolve(false),
269
- },
270
- );
271
- expect(outcome.steps.find((s) => s.rung === 'powerCycle')?.status).toBe('applied');
272
- expect(calls).toHaveLength(1);
273
- });
274
- });
@@ -1,377 +0,0 @@
1
- // The `usb-hub-port-cycle` power hook — recovery ladder rung 4, backed by `uhubctl`.
2
- //
3
- // This is the FIRST real `PowerHook` implementation. It cuts VBUS on one port of a
4
- // per-port-power-switching (PPPS) USB hub, waits for the modem to come back on the
5
- // SAME physical topology path, and reports `applied` only when it actually did.
6
- //
7
- // FOUR safety properties, in the order they bite:
8
- //
9
- // 1. CONFIG-MAPPED, NEVER DISCOVERED. A stable key is power-cyclable only if an
10
- // operator wrote it into an explicitly-pathed config file (`readUhubctlPowerConfig`
11
- // takes the path as an argument — there is no default path, no search, no probe).
12
- // An unmapped key returns `unsupported` and touches nothing. Guessing which hub
13
- // port a modem is on and then cutting its power is exactly the failure mode that
14
- // would black out an unrelated device.
15
- // 2. ARGV ONLY, ALLOWLISTED. The command is built as an argv array and handed to an
16
- // injected runner — there is no shell, no string interpolation, no `sh -c`. Every
17
- // generated token is re-checked against `ALLOWED_ARGV` before the runner is
18
- // called, so even a config that somehow evaded the schema cannot smuggle a flag.
19
- // 3. BOUNDED + CANCELLABLE. The runner call is bounded by `commandTimeoutMs`, the
20
- // re-enumeration wait by `enumerationTimeoutMs`, and both observe an optional
21
- // `AbortSignal`. The worst case is `commandTimeoutMs + enumerationTimeoutMs`;
22
- // there is no path that waits forever.
23
- // 4. SERIALISED PER MODEM. Like every other disruptive op in this backend (see
24
- // `mm-mutations.ts`), a cycle runs through the shared per-modem `ModemActor`,
25
- // keyed on the STABLE key. Two overlapping cycles on one port would otherwise
26
- // interleave a power-on with a power-off and leave the port dark.
27
- //
28
- // PROOF OF SUCCESS IS RE-ENUMERATION, NOT EXIT CODE 0. `uhubctl` exiting 0 only means
29
- // the hub accepted the request. The hook records the modem's `ID_PATH` BEFORE the cut
30
- // and only reports `applied` once that same `ID_PATH` is observed again — a port that
31
- // powers back up with nothing on it is a `failed`, reported with expected-vs-observed.
32
- //
33
- // -----------------------------------------------------------------------------------
34
- // CAVEAT — STALE DEVICE FILES ON LINUX KERNELS BEFORE 6.0.
35
- //
36
- // uhubctl README, FAQ section `_USB devices are not removed after port power down on
37
- // Linux_` (github.com/mvp/uhubctl, README.md), verbatim:
38
- //
39
- // "After powering down USB port, udev does not get any event, so it keeps the device
40
- // files around. However, trying to access the device files will lead to an IO error.
41
- // This is Linux kernel issue and is fixed since uhubctl 2.5.0 for systems with Linux
42
- // kernel 6.0 or later. If you are still using Linux 5.x or older, you can use this
43
- // workaround for this issue:
44
- //
45
- // sudo uhubctl -a off -l ${location} -p ${port}
46
- // sudo udevadm trigger --action=remove /sys/bus/usb/devices/${location}.${port}/
47
- //
48
- // Device file will be removed by udev, but USB device will be still visible in
49
- // `lsusb`. Note that path /sys/bus/usb/devices/${location}.${port} will only exist if
50
- // device was detected on that port. When you turn power back on, device should
51
- // re-enumerate properly (no need to call `udevadm` again)."
52
- //
53
- // Why it matters HERE: during the dark window of a cycle, a pre-6.0 kernel leaves the
54
- // device files in place, so a presence check that asks "does the node still exist?"
55
- // reports the modem as present when it is electrically gone — and would let this hook
56
- // declare `applied` off a stale artefact rather than a real re-enumeration.
57
- //
58
- // THIS HOOK DOES NOT RUN `udevadm trigger --action=remove` ITSELF, deliberately: it is
59
- // a privileged host-wide udev mutation whose sysfs path only exists if a device was
60
- // detected there, and firing it from a recovery rung would make rung 4 mutate state
61
- // well outside the port it was mapped to. Instead the hook is built so the caveat
62
- // cannot corrupt its verdict — presence is resolved by the INJECTED
63
- // `UsbEnumerationPoller`, whose production implementation re-reads udev every call and
64
- // never caches (see `usb-enumerator.ts`, which re-runs `udevadm info --export-db` per
65
- // `enumerate()`), and the postcondition compares `ID_PATH`, not a device-node path. A
66
- // deployment pinned to a pre-6.0 kernel wires the `udevadm trigger --action=remove`
67
- // step into that poller or into a udev rule — one explicit, auditable place.
68
- //
69
- // Hardware note: the README's compatible-hub table lists `0BDA:0411` (Rosonway RSH-A10
70
- // / RSH-A16, Juiced Systems 6HUB-01) as per-port-power-switching capable — that is the
71
- // Realtek chipset on this project's bench board. `0bda:5411` is NOT on that list, so a
72
- // hub reporting that id may need `-f`, which this hook never passes.
73
- // -----------------------------------------------------------------------------------
74
-
75
- import { z } from 'zod';
76
- import { ModemActor } from './modem-actor';
77
- import type {
78
- PowerCapability,
79
- PowerCycleContext,
80
- PowerCycleResult,
81
- PowerHook,
82
- PreferredUsbMode,
83
- } from './power-contract';
84
-
85
- /**
86
- * A uhubctl hub location: `<bus>-<port>[.<port>…]` (e.g. `1-1`, `2-1.4`), or a bare
87
- * bus number for a root hub. This mirrors the Linux sysfs USB path and is the ONLY
88
- * shape accepted — a VID:PID selector or a `--` flag can never parse as one.
89
- */
90
- const HUB_LOCATION = /^[0-9]{1,3}(-[0-9]{1,3}(\.[0-9]{1,3})*)?$/;
91
-
92
- /** One mapped modem: which PPPS hub it hangs off, and which port on that hub. */
93
- export const uhubctlPortMappingSchema = z.strictObject({
94
- /** The hub's uhubctl location (`-l`), e.g. `1-1` or `2-1.4`. */
95
- hubLocation: z.string().regex(HUB_LOCATION, 'hubLocation must look like `1-1` or `2-1.4`'),
96
- /** The 1-based port number on that hub (`-p`). */
97
- port: z.number().int().min(1).max(255),
98
- });
99
- export type UhubctlPortMapping = z.infer<typeof uhubctlPortMappingSchema>;
100
-
101
- /**
102
- * The whole config file: a map from STABLE KEY to its hub/port mapping. `.strictObject`
103
- * on each entry means a typo'd or smuggled extra field is rejected rather than ignored.
104
- */
105
- export const uhubctlPortMapSchema = z.record(z.string().min(1), uhubctlPortMappingSchema);
106
- export type UhubctlPortMap = z.infer<typeof uhubctlPortMapSchema>;
107
-
108
- /**
109
- * Parse config text (JSON) into a validated port map. `path` is used only for the
110
- * error message, so a malformed file fails visibly with a named field.
111
- */
112
- export function parseUhubctlPortMap(text: string, path: string): UhubctlPortMap {
113
- let raw: unknown;
114
- try {
115
- raw = JSON.parse(text) as unknown;
116
- } catch (error) {
117
- throw new Error(`invalid uhubctl port map ${path}: ${describe(error)}`);
118
- }
119
- const result = uhubctlPortMapSchema.safeParse(raw);
120
- if (!result.success) {
121
- const issue = result.error.issues[0];
122
- const where = issue?.path.join('.') || '(root)';
123
- throw new Error(
124
- `invalid uhubctl port map ${path}: ${where}: ${issue?.message ?? 'schema mismatch'}`,
125
- );
126
- }
127
- return result.data;
128
- }
129
-
130
- /**
131
- * Read + validate a port map from an EXPLICIT path. There is intentionally no default
132
- * and no discovery: a caller that cannot name the file gets no power control.
133
- */
134
- export async function readUhubctlPortMap(path: string): Promise<UhubctlPortMap> {
135
- return parseUhubctlPortMap(await Bun.file(path).text(), path);
136
- }
137
-
138
- /** The result of one `uhubctl` invocation. */
139
- export interface UhubctlResult {
140
- readonly stdout: string;
141
- readonly stderr: string;
142
- readonly exitCode: number;
143
- }
144
-
145
- /**
146
- * A runner over `uhubctl` argv — structurally the same seam as `NmcliRunner`. Tests
147
- * inject a fake; the device injects `SpawnUhubctlRunner`. The hook NEVER spawns
148
- * directly, so the argv the tests assert against is byte-for-byte what runs on-device.
149
- */
150
- export interface UhubctlRunner {
151
- run(argv: readonly string[]): UhubctlResult | Promise<UhubctlResult>;
152
- }
153
-
154
- /** The device-exact runner: spawns the real `uhubctl` with the argv array verbatim. */
155
- export class SpawnUhubctlRunner implements UhubctlRunner {
156
- async run(argv: readonly string[]): Promise<UhubctlResult> {
157
- const proc = Bun.spawn(['uhubctl', ...argv], { stdout: 'pipe', stderr: 'pipe' });
158
- const [stdout, stderr, exitCode] = await Promise.all([
159
- new Response(proc.stdout).text(),
160
- new Response(proc.stderr).text(),
161
- proc.exited,
162
- ]);
163
- return { stdout, stderr, exitCode };
164
- }
165
- }
166
-
167
- /**
168
- * Resolves the modem's current udev `ID_PATH` (its physical topology UID) for a stable
169
- * key, or `undefined` when nothing is enumerated there. Injected so tests need no
170
- * hardware; the production implementation MUST re-read udev/sysfs every call (see the
171
- * pre-6.0 stale-devfile caveat at the top of this file).
172
- */
173
- export interface UsbEnumerationPoller {
174
- idPathFor(stableKey: string): string | undefined | Promise<string | undefined>;
175
- }
176
-
177
- const DEFAULT_ENUMERATION_TIMEOUT_MS = 30_000;
178
- const DEFAULT_COMMAND_TIMEOUT_MS = 15_000;
179
- const DEFAULT_POLL_INTERVAL_MS = 250;
180
- /** `uhubctl -d` — seconds the port stays dark before it is powered back on. */
181
- const DEFAULT_POWER_OFF_DELAY_SECONDS = 3;
182
-
183
- /** Construction dependencies. Everything that touches the system is injectable. */
184
- export interface UhubctlPowerHookDeps {
185
- /** The validated stable-key → hub/port map (see `readUhubctlPortMap`). */
186
- readonly ports: UhubctlPortMap;
187
- readonly runner: UhubctlRunner;
188
- readonly poller: UsbEnumerationPoller;
189
- /** Shared per-modem serialisation. Defaults to a private actor. */
190
- readonly actor?: ModemActor;
191
- readonly enumerationTimeoutMs?: number;
192
- readonly commandTimeoutMs?: number;
193
- readonly pollIntervalMs?: number;
194
- readonly powerOffDelaySeconds?: number;
195
- readonly preferredUsbMode?: PreferredUsbMode;
196
- /** Cancels an in-flight cycle — the hook resolves `failed`, it never hangs. */
197
- readonly signal?: AbortSignal;
198
- readonly sleep?: (ms: number) => Promise<void>;
199
- readonly now?: () => number;
200
- }
201
-
202
- /**
203
- * Every argv token this hook is permitted to emit. The flags are literals; the two
204
- * value slots are re-validated against the same shapes the schema enforced. Anything
205
- * else is a bug in this file and fails closed before the runner is called.
206
- */
207
- const ALLOWED_ARGV: readonly RegExp[] = [
208
- /^-l$/,
209
- /^-p$/,
210
- /^-a$/,
211
- /^-d$/,
212
- /^cycle$/,
213
- HUB_LOCATION,
214
- /^[0-9]{1,3}$/,
215
- ];
216
-
217
- /** Build the `uhubctl` argv for one mapping. Pure + exported so tests can assert it. */
218
- export function uhubctlCycleArgv(
219
- mapping: UhubctlPortMapping,
220
- powerOffDelaySeconds: number,
221
- ): readonly string[] {
222
- const argv = [
223
- '-l',
224
- mapping.hubLocation,
225
- '-p',
226
- String(mapping.port),
227
- '-a',
228
- 'cycle',
229
- '-d',
230
- String(powerOffDelaySeconds),
231
- ];
232
- for (const token of argv) {
233
- if (!ALLOWED_ARGV.some((allowed) => allowed.test(token))) {
234
- throw new Error(`refusing to run uhubctl: argv token '${token}' is not allowlisted`);
235
- }
236
- }
237
- return argv;
238
- }
239
-
240
- /** The `usb-hub-port-cycle` power hook. One instance serves every mapped modem. */
241
- export function createUhubctlPowerHook(deps: UhubctlPowerHookDeps): PowerHook {
242
- const enumerationTimeoutMs = deps.enumerationTimeoutMs ?? DEFAULT_ENUMERATION_TIMEOUT_MS;
243
- const commandTimeoutMs = deps.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
244
- const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
245
- const powerOffDelaySeconds = deps.powerOffDelaySeconds ?? DEFAULT_POWER_OFF_DELAY_SECONDS;
246
- const actor = deps.actor ?? new ModemActor();
247
- const now = deps.now ?? Date.now;
248
- const sleep = deps.sleep ?? defaultSleep;
249
-
250
- const capability: PowerCapability = {
251
- power: 'usb-hub-port-cycle',
252
- usbReset: true,
253
- enumerationTimeoutMs,
254
- ...(deps.preferredUsbMode !== undefined ? { preferredUsbMode: deps.preferredUsbMode } : {}),
255
- };
256
-
257
- const cancelled = (): PowerCycleResult | undefined =>
258
- deps.signal?.aborted === true
259
- ? { status: 'failed', reason: 'power cycle cancelled by the caller' }
260
- : undefined;
261
-
262
- async function awaitReenumeration(
263
- stableKey: string,
264
- expected: string | undefined,
265
- ): Promise<PowerCycleResult> {
266
- const deadline = now() + enumerationTimeoutMs;
267
- let observed: string | undefined;
268
- while (now() < deadline) {
269
- const abort = cancelled();
270
- if (abort !== undefined) {
271
- return abort;
272
- }
273
- observed = await deps.poller.idPathFor(stableKey);
274
- // A port cycle preserves the physical topology, so the SAME ID_PATH must
275
- // come back. If nothing was enumerated before the cut there is no path to
276
- // compare against — any device re-appearing at that key is the recovery.
277
- if (observed !== undefined && (expected === undefined || observed === expected)) {
278
- return {
279
- status: 'applied',
280
- reason: `port cycled; modem re-enumerated at ID_PATH '${observed}'`,
281
- };
282
- }
283
- await sleep(pollIntervalMs);
284
- }
285
- return {
286
- status: 'failed',
287
- reason:
288
- `modem did not re-enumerate within ${enumerationTimeoutMs}ms — expected ID_PATH ` +
289
- `${expected === undefined ? '(any device)' : `'${expected}'`}, observed ` +
290
- `${observed === undefined ? 'no device' : `'${observed}'`}`,
291
- };
292
- }
293
-
294
- async function cycleMapped(
295
- stableKey: string,
296
- mapping: UhubctlPortMapping,
297
- ): Promise<PowerCycleResult> {
298
- // Record the pre-cut topology path — the postcondition compares against it.
299
- const expected = await deps.poller.idPathFor(stableKey);
300
-
301
- let argv: readonly string[];
302
- try {
303
- argv = uhubctlCycleArgv(mapping, powerOffDelaySeconds);
304
- } catch (error) {
305
- return { status: 'failed', reason: describe(error) };
306
- }
307
-
308
- let result: UhubctlResult;
309
- try {
310
- result = await withTimeout(
311
- Promise.resolve(deps.runner.run(argv)),
312
- commandTimeoutMs,
313
- `uhubctl did not return within ${commandTimeoutMs}ms`,
314
- );
315
- } catch (error) {
316
- return { status: 'failed', reason: `uhubctl ${argv.join(' ')} failed: ${describe(error)}` };
317
- }
318
- if (result.exitCode !== 0) {
319
- return {
320
- status: 'failed',
321
- reason:
322
- `uhubctl ${argv.join(' ')} exited ${result.exitCode}: ` +
323
- `${result.stderr.trim() || result.stdout.trim() || '(no output)'}`,
324
- };
325
- }
326
-
327
- // Exit 0 only means the hub accepted the request — re-enumeration is the proof.
328
- return awaitReenumeration(stableKey, expected);
329
- }
330
-
331
- return {
332
- capability,
333
- cycle(context: PowerCycleContext): Promise<PowerCycleResult> {
334
- const { stableKey } = context;
335
- const mapping = deps.ports[stableKey];
336
- if (mapping === undefined) {
337
- // Refuse BEFORE the actor and before any I/O: an unmapped key must never
338
- // cut power to a port that was never declared to belong to it.
339
- return Promise.resolve({
340
- status: 'unsupported',
341
- reason: `no uhubctl hub/port mapping is configured for stable key '${stableKey}'`,
342
- });
343
- }
344
- const abort = cancelled();
345
- if (abort !== undefined) {
346
- return Promise.resolve(abort);
347
- }
348
- // Serialised on the STABLE key, like every other disruptive op (mm-mutations).
349
- return actor.run(stableKey, () => cycleMapped(stableKey, mapping));
350
- },
351
- };
352
- }
353
-
354
- function defaultSleep(ms: number): Promise<void> {
355
- return new Promise((resolve) => setTimeout(resolve, ms));
356
- }
357
-
358
- /** Bound a promise; rejects with `message` if it has not settled in `ms`. */
359
- async function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
360
- let timer: ReturnType<typeof setTimeout> | undefined;
361
- try {
362
- return await Promise.race([
363
- promise,
364
- new Promise<never>((_resolve, reject) => {
365
- timer = setTimeout(() => reject(new Error(message)), ms);
366
- }),
367
- ]);
368
- } finally {
369
- if (timer !== undefined) {
370
- clearTimeout(timer);
371
- }
372
- }
373
- }
374
-
375
- function describe(error: unknown): string {
376
- return error instanceof Error ? error.message : String(error);
377
- }