@ceralive/modem-control 0.2.0 → 1.1.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 (464) hide show
  1. package/README.md +317 -0
  2. package/dist/backend/at-lease.d.ts +57 -0
  3. package/dist/backend/at-lease.js +109 -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 +76 -0
  61. package/dist/backend/transition-preconditions.js +64 -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 +196 -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 +19 -0
  146. package/dist/index.js +26 -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 +47 -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/operations/contracts.d.ts +72 -0
  196. package/dist/operations/contracts.js +1 -0
  197. package/dist/operations/index.d.ts +1 -0
  198. package/dist/operations/index.js +1 -0
  199. package/dist/operations/operation-engine.d.ts +16 -0
  200. package/dist/operations/operation-engine.js +194 -0
  201. package/dist/ports/index.d.ts +12 -0
  202. package/{src/ports/index.ts → dist/ports/index.js} +12 -8
  203. package/dist/ports/location.d.ts +87 -0
  204. package/dist/ports/location.js +36 -0
  205. package/dist/ports/modem-manager.d.ts +89 -0
  206. package/dist/ports/modem-manager.js +9 -0
  207. package/dist/ports/mutation-admission.d.ts +27 -0
  208. package/dist/ports/mutation-admission.js +9 -0
  209. package/dist/ports/network-manager.d.ts +68 -0
  210. package/dist/ports/network-manager.js +13 -0
  211. package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
  212. package/dist/ports/observation.js +7 -0
  213. package/dist/ports/ops.d.ts +44 -0
  214. package/dist/ports/ops.js +16 -0
  215. package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
  216. package/dist/ports/receipts.js +9 -0
  217. package/dist/ports/reconcile.d.ts +33 -0
  218. package/dist/ports/reconcile.js +200 -0
  219. package/dist/ports/resource-ownership.d.ts +29 -0
  220. package/dist/ports/resource-ownership.js +1 -0
  221. package/dist/ports/router.d.ts +19 -0
  222. package/dist/ports/router.js +7 -0
  223. package/dist/ports/sms.d.ts +64 -0
  224. package/dist/ports/sms.js +24 -0
  225. package/dist/ports/uhubctl.d.ts +6 -0
  226. package/dist/ports/uhubctl.js +1 -0
  227. package/dist/providers/contracts.d.ts +124 -0
  228. package/dist/providers/contracts.js +10 -0
  229. package/dist/providers/huawei-hilink/index.d.ts +2 -0
  230. package/dist/providers/huawei-hilink/index.js +2 -0
  231. package/dist/providers/huawei-hilink/operations.d.ts +20 -0
  232. package/dist/providers/huawei-hilink/operations.js +56 -0
  233. package/dist/providers/huawei-hilink/provider.d.ts +52 -0
  234. package/dist/providers/huawei-hilink/provider.js +76 -0
  235. package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
  236. package/dist/providers/huawei-hilink/runtime.js +171 -0
  237. package/dist/providers/huawei-hilink/session.d.ts +28 -0
  238. package/dist/providers/huawei-hilink/session.js +120 -0
  239. package/dist/providers/huawei-hilink/transport.d.ts +19 -0
  240. package/dist/providers/huawei-hilink/transport.js +1 -0
  241. package/dist/providers/index.d.ts +8 -0
  242. package/dist/providers/index.js +8 -0
  243. package/dist/providers/matcher.d.ts +3 -0
  244. package/dist/providers/matcher.js +205 -0
  245. package/dist/providers/modem-manager/errors.d.ts +7 -0
  246. package/dist/providers/modem-manager/errors.js +37 -0
  247. package/dist/providers/modem-manager/generic-operations.d.ts +8 -0
  248. package/dist/providers/modem-manager/generic-operations.js +207 -0
  249. package/dist/providers/modem-manager/index.d.ts +3 -0
  250. package/dist/providers/modem-manager/index.js +3 -0
  251. package/dist/providers/modem-manager/module-operations.d.ts +21 -0
  252. package/dist/providers/modem-manager/module-operations.js +118 -0
  253. package/dist/providers/modem-manager/provider.d.ts +39 -0
  254. package/dist/providers/modem-manager/provider.js +152 -0
  255. package/dist/providers/modem-manager/snapshot.d.ts +5 -0
  256. package/dist/providers/modem-manager/snapshot.js +204 -0
  257. package/dist/providers/modem-manager/types.d.ts +135 -0
  258. package/dist/providers/modem-manager/types.js +1 -0
  259. package/dist/providers/network-manager/adapter.d.ts +71 -0
  260. package/dist/providers/network-manager/adapter.js +348 -0
  261. package/dist/providers/network-manager/index.d.ts +2 -0
  262. package/dist/providers/network-manager/index.js +2 -0
  263. package/dist/providers/network-manager/types.d.ts +163 -0
  264. package/dist/providers/network-manager/types.js +77 -0
  265. package/dist/providers/registry.d.ts +13 -0
  266. package/dist/providers/registry.js +33 -0
  267. package/dist/providers/ufi-himi/index.d.ts +6 -0
  268. package/dist/providers/ufi-himi/index.js +6 -0
  269. package/dist/providers/ufi-himi/operations.d.ts +41 -0
  270. package/dist/providers/ufi-himi/operations.js +66 -0
  271. package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
  272. package/dist/providers/ufi-himi/prohibitions.js +88 -0
  273. package/dist/providers/ufi-himi/provider.d.ts +41 -0
  274. package/dist/providers/ufi-himi/provider.js +204 -0
  275. package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
  276. package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
  277. package/dist/providers/ufi-himi/session.d.ts +46 -0
  278. package/dist/providers/ufi-himi/session.js +92 -0
  279. package/dist/providers/ufi-himi/transport.d.ts +29 -0
  280. package/dist/providers/ufi-himi/transport.js +25 -0
  281. package/dist/providers/zte-goform/index.d.ts +2 -0
  282. package/dist/providers/zte-goform/index.js +2 -0
  283. package/dist/providers/zte-goform/provider.d.ts +51 -0
  284. package/dist/providers/zte-goform/provider.js +101 -0
  285. package/dist/providers/zte-goform/session.d.ts +17 -0
  286. package/dist/providers/zte-goform/session.js +133 -0
  287. package/dist/providers/zte-goform/transport.d.ts +16 -0
  288. package/dist/providers/zte-goform/transport.js +1 -0
  289. package/dist/radio/band-truth.d.ts +50 -0
  290. package/dist/radio/band-truth.js +92 -0
  291. package/dist/radio/index.d.ts +3 -0
  292. package/dist/radio/index.js +10 -0
  293. package/dist/radio/mode-combinations.d.ts +78 -0
  294. package/dist/radio/mode-combinations.js +198 -0
  295. package/dist/radio/mode-truth.d.ts +67 -0
  296. package/dist/radio/mode-truth.js +112 -0
  297. package/dist/redact.d.ts +15 -0
  298. package/dist/redact.js +189 -0
  299. package/dist/safety/composition-root.d.ts +28 -0
  300. package/dist/safety/composition-root.js +65 -0
  301. package/dist/safety/flock-resource-ownership.d.ts +11 -0
  302. package/dist/safety/flock-resource-ownership.js +151 -0
  303. package/dist/safety/index.d.ts +2 -0
  304. package/dist/safety/index.js +2 -0
  305. package/dist/sms/dbus-messaging.d.ts +17 -0
  306. package/dist/sms/dbus-messaging.js +185 -0
  307. package/dist/sms/inbox-store.d.ts +10 -0
  308. package/dist/sms/inbox-store.js +82 -0
  309. package/dist/sms/index.d.ts +4 -0
  310. package/dist/sms/index.js +10 -0
  311. package/dist/sms/mmcli-parse.d.ts +54 -0
  312. package/dist/sms/mmcli-parse.js +224 -0
  313. package/dist/sms/normalize.d.ts +42 -0
  314. package/dist/sms/normalize.js +95 -0
  315. package/dist/testing/domain-fakes.d.ts +58 -0
  316. package/dist/testing/domain-fakes.js +98 -0
  317. package/dist/testing/index.d.ts +2 -0
  318. package/dist/testing/index.js +17 -0
  319. package/dist/testing/provider-fakes.d.ts +45 -0
  320. package/dist/testing/provider-fakes.js +64 -0
  321. package/dist/transport/calls.d.ts +9 -0
  322. package/dist/transport/calls.js +88 -0
  323. package/dist/transport/codec.d.ts +3 -0
  324. package/dist/transport/codec.js +207 -0
  325. package/dist/transport/dbus-native.d.ts +57 -0
  326. package/dist/transport/dbus-native.js +17 -0
  327. package/dist/transport/errors.d.ts +21 -0
  328. package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
  329. package/dist/transport/index.d.ts +4 -0
  330. package/dist/transport/index.js +9 -0
  331. package/dist/transport/signals.d.ts +15 -0
  332. package/dist/transport/signals.js +123 -0
  333. package/dist/transport/signature.d.ts +7 -0
  334. package/dist/transport/signature.js +94 -0
  335. package/dist/transport/transport.d.ts +2 -0
  336. package/dist/transport/transport.js +202 -0
  337. package/dist/transport/types.d.ts +61 -0
  338. package/dist/transport/types.js +19 -0
  339. package/dist/usb-mode/catalog-schema.d.ts +139 -0
  340. package/dist/usb-mode/catalog-schema.js +97 -0
  341. package/dist/usb-mode/catalog.d.ts +21 -0
  342. package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
  343. package/dist/usb-mode/certified-catalog.json +67 -0
  344. package/dist/usb-mode/index.d.ts +5 -0
  345. package/dist/usb-mode/index.js +15 -0
  346. package/dist/usb-mode/ingestion.d.ts +111 -0
  347. package/dist/usb-mode/ingestion.js +187 -0
  348. package/dist/usb-mode/promotion-review.d.ts +21 -0
  349. package/dist/usb-mode/promotion-review.js +87 -0
  350. package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
  351. package/dist/usb-mode/usb-devices-parse.js +157 -0
  352. package/dist/ussd/calls.d.ts +32 -0
  353. package/dist/ussd/calls.js +96 -0
  354. package/dist/ussd/index.d.ts +5 -0
  355. package/dist/ussd/index.js +12 -0
  356. package/dist/ussd/mm-ussd.d.ts +37 -0
  357. package/dist/ussd/mm-ussd.js +205 -0
  358. package/dist/ussd/refusal.d.ts +53 -0
  359. package/dist/ussd/refusal.js +154 -0
  360. package/dist/ussd/registration.d.ts +20 -0
  361. package/dist/ussd/registration.js +101 -0
  362. package/dist/ussd/session.d.ts +84 -0
  363. package/dist/ussd/session.js +163 -0
  364. package/package.json +38 -4
  365. package/src/backend/at-lease.test.ts +0 -106
  366. package/src/backend/at-lease.ts +0 -158
  367. package/src/backend/cell-info.test.ts +0 -154
  368. package/src/backend/cell-info.ts +0 -160
  369. package/src/backend/device-classifier.test.ts +0 -168
  370. package/src/backend/device-classifier.ts +0 -248
  371. package/src/backend/enrichment.ts +0 -96
  372. package/src/backend/features.test.ts +0 -162
  373. package/src/backend/features.ts +0 -179
  374. package/src/backend/identity-ladder.test.ts +0 -117
  375. package/src/backend/identity-ladder.ts +0 -221
  376. package/src/backend/identity-registry.test.ts +0 -89
  377. package/src/backend/identity-registry.ts +0 -151
  378. package/src/backend/index.ts +0 -221
  379. package/src/backend/lifecycle-interlock.ts +0 -38
  380. package/src/backend/managed-objects.ts +0 -108
  381. package/src/backend/mapping.ts +0 -160
  382. package/src/backend/mm-backend.ts +0 -191
  383. package/src/backend/mm-mutations.ts +0 -228
  384. package/src/backend/modem-actor.test.ts +0 -95
  385. package/src/backend/modem-actor.ts +0 -112
  386. package/src/backend/nm-auto-apn.ts +0 -161
  387. package/src/backend/nm-gsm-fields.ts +0 -122
  388. package/src/backend/nmcli-nm-port.ts +0 -228
  389. package/src/backend/nmcli-runner.ts +0 -52
  390. package/src/backend/observer.ts +0 -297
  391. package/src/backend/power-contract.test.ts +0 -40
  392. package/src/backend/power-contract.ts +0 -83
  393. package/src/backend/recovery-attribution.test.ts +0 -102
  394. package/src/backend/recovery-attribution.ts +0 -86
  395. package/src/backend/recovery-budget.test.ts +0 -64
  396. package/src/backend/recovery-budget.ts +0 -84
  397. package/src/backend/recovery-ladder.test.ts +0 -257
  398. package/src/backend/recovery-ladder.ts +0 -249
  399. package/src/backend/router-ethernet.test.ts +0 -71
  400. package/src/backend/router-ethernet.ts +0 -90
  401. package/src/backend/row-store.ts +0 -105
  402. package/src/backend/signal-setup.ts +0 -112
  403. package/src/backend/sim-unlock.ts +0 -193
  404. package/src/backend/transition-preconditions.ts +0 -149
  405. package/src/backend/usage/accounting.test.ts +0 -147
  406. package/src/backend/usage/accounting.ts +0 -123
  407. package/src/backend/usage/billing-cycle.test.ts +0 -62
  408. package/src/backend/usage/billing-cycle.ts +0 -45
  409. package/src/backend/usage/index.ts +0 -37
  410. package/src/backend/usage/proc-net-dev.test.ts +0 -56
  411. package/src/backend/usage/sampler.test.ts +0 -219
  412. package/src/backend/usage/sampler.ts +0 -228
  413. package/src/backend/usage/store.test.ts +0 -148
  414. package/src/backend/usage/store.ts +0 -177
  415. package/src/backend/usb-enumerator.test.ts +0 -87
  416. package/src/backend/usb-enumerator.ts +0 -181
  417. package/src/backend/usb-mode-transition.test.ts +0 -323
  418. package/src/backend/usb-mode-transition.ts +0 -253
  419. package/src/domain/brand.ts +0 -29
  420. package/src/domain/errors.ts +0 -77
  421. package/src/domain/guards.test.ts +0 -218
  422. package/src/domain/guards.ts +0 -144
  423. package/src/domain/identity.test.ts +0 -83
  424. package/src/domain/identity.ts +0 -165
  425. package/src/domain/index.ts +0 -12
  426. package/src/domain/snapshot.test.ts +0 -266
  427. package/src/domain/snapshot.ts +0 -120
  428. package/src/index.test.ts +0 -6
  429. package/src/index.ts +0 -15
  430. package/src/ports/README.md +0 -61
  431. package/src/ports/forbidden-surface.test.ts +0 -241
  432. package/src/ports/modem-manager.ts +0 -72
  433. package/src/ports/network-manager.ts +0 -87
  434. package/src/ports/ops.ts +0 -60
  435. package/src/ports/ops.type-test.ts +0 -39
  436. package/src/ports/receipts.test.ts +0 -153
  437. package/src/ports/reconcile.test.ts +0 -152
  438. package/src/ports/reconcile.ts +0 -338
  439. package/src/ports/router.ts +0 -29
  440. package/src/redact.test.ts +0 -82
  441. package/src/redact.ts +0 -73
  442. package/src/transport/README.md +0 -65
  443. package/src/transport/calls.ts +0 -113
  444. package/src/transport/characterization.test.ts +0 -260
  445. package/src/transport/codec.test.ts +0 -118
  446. package/src/transport/codec.ts +0 -240
  447. package/src/transport/conformance-python.test.ts +0 -152
  448. package/src/transport/conformance-same-lib.test.ts +0 -115
  449. package/src/transport/dbus-native-lib.d.ts +0 -19
  450. package/src/transport/dbus-native.ts +0 -85
  451. package/src/transport/index.ts +0 -30
  452. package/src/transport/no-library-leak.test.ts +0 -61
  453. package/src/transport/reliability.test.ts +0 -173
  454. package/src/transport/signals.ts +0 -150
  455. package/src/transport/signature.ts +0 -110
  456. package/src/transport/test-support/fake-service.ts +0 -168
  457. package/src/transport/test-support/independent-producer.py +0 -110
  458. package/src/transport/test-support/private-bus.ts +0 -66
  459. package/src/transport/transport.ts +0 -250
  460. package/src/transport/types.ts +0 -118
  461. package/src/usb-mode/catalog-schema.test.ts +0 -181
  462. package/src/usb-mode/catalog-schema.ts +0 -113
  463. package/src/usb-mode/certified-catalog.json +0 -67
  464. package/src/usb-mode/index.ts +0 -27
@@ -0,0 +1,107 @@
1
+ // Pure nmcli GSM-connection argv builders — the FULL nine-field write parity today's
2
+ // CeraUI carried (modem-registration.ts `sanitizeModemConfigForNetworkManager` +
3
+ // `addConnectionForModem`), rebuilt from scratch for the device-exact adapter so the
4
+ // greenfield port loses nothing the wrap-first path wrote.
5
+ //
6
+ // The nine fields and their exact NM conventions (`gsm.*` + `connection.*` setting
7
+ // semantics) are the contract A4.1 must not regress:
8
+ // gsm.apn / gsm.username / gsm.password — creds ("" when unset)
9
+ // gsm.password-flags — "4" NOT_REQUIRED (no password),
10
+ // "0" NONE / system-stored (password set)
11
+ // gsm.home-only — "yes" ⇒ roaming DISABLED
12
+ // gsm.network-id — manual operator PLMN, only while roaming
13
+ // gsm.auto-config — "yes" ⇒ Auto-APN (creds MUST be empty)
14
+ // connection.autoconnect — always "yes"
15
+ // connection.autoconnect-retries — "2"
16
+ // Empty-string fallback; Bun runtime limitation with empty CLI args: a bare
17
+ // `undefined` reaching `Bun.spawn` argv drops the slot and shifts every following
18
+ // token, so every optional nmcli value is coerced to "" first — the same `value || ""`
19
+ // convention CeraUI documented on gsm.apn / gsm.username / gsm.password.
20
+ export const cliArg = (value) => value || '';
21
+ /** NM secret-flags: no password ⇒ NOT_REQUIRED ("4"); password set ⇒ NONE ("0"). */
22
+ export const passwordFlags = (password) => (password ? '0' : '4');
23
+ /** `gsm.network-id` — the manual operator id, and only while roaming (home-only off). */
24
+ const networkIdField = (input) => input.homeOnly ? '' : cliArg(input.networkId);
25
+ /**
26
+ * The nine `gsm.*` + `connection.*` key/value pairs for a profile, in stable order.
27
+ * With `autoConfig` set the creds are forced empty (NM rejects `auto-config yes` while
28
+ * any credential is present — nm-setting-gsm.c:440-447), so an Auto-APN create is always
29
+ * NM-valid by construction.
30
+ */
31
+ export function gsmFieldPairs(input) {
32
+ const auto = input.autoConfig;
33
+ return [
34
+ ['gsm.apn', auto ? '' : cliArg(input.apn)],
35
+ ['gsm.username', auto ? '' : cliArg(input.username)],
36
+ ['gsm.password', auto ? '' : cliArg(input.password)],
37
+ ['gsm.password-flags', passwordFlags(auto ? undefined : input.password)],
38
+ ['gsm.home-only', input.homeOnly ? 'yes' : 'no'],
39
+ ['gsm.network-id', networkIdField(input)],
40
+ ['gsm.auto-config', auto ? 'yes' : 'no'],
41
+ ['connection.autoconnect', 'yes'],
42
+ ['connection.autoconnect-retries', '2'],
43
+ ];
44
+ }
45
+ /** Flatten key/value pairs into the alternating argv nmcli expects. */
46
+ export function flattenPairs(pairs) {
47
+ return pairs.flat();
48
+ }
49
+ /** `connection add type gsm con-name <name> <nine fields>` — device-exact create argv. */
50
+ export function createGsmArgs(input) {
51
+ return [
52
+ 'connection',
53
+ 'add',
54
+ 'type',
55
+ 'gsm',
56
+ 'con-name',
57
+ input.connectionName,
58
+ ...flattenPairs(gsmFieldPairs(input)),
59
+ ];
60
+ }
61
+ /**
62
+ * The key/value pairs for a `connection modify`. Roaming (home-only) and the manual
63
+ * operator id move together — turning roaming off clears `gsm.network-id`, honoring the
64
+ * `gsm.network-id = roaming ? id : ""` invariant in the SAME modify.
65
+ */
66
+ export function patchPairs(patch) {
67
+ const pairs = [];
68
+ if (patch.connectionName !== undefined) {
69
+ pairs.push('connection.id', patch.connectionName);
70
+ }
71
+ if (patch.apn !== undefined) {
72
+ pairs.push('gsm.apn', cliArg(patch.apn));
73
+ }
74
+ if (patch.username !== undefined) {
75
+ pairs.push('gsm.username', cliArg(patch.username));
76
+ }
77
+ if (patch.password !== undefined) {
78
+ pairs.push('gsm.password', cliArg(patch.password), 'gsm.password-flags', passwordFlags(patch.password));
79
+ }
80
+ if (patch.autoConfig !== undefined) {
81
+ pairs.push('gsm.auto-config', patch.autoConfig ? 'yes' : 'no');
82
+ }
83
+ if (patch.homeOnly !== undefined) {
84
+ pairs.push('gsm.home-only', patch.homeOnly ? 'yes' : 'no');
85
+ pairs.push('gsm.network-id', patch.homeOnly ? '' : cliArg(patch.networkId));
86
+ }
87
+ else if (patch.networkId !== undefined) {
88
+ pairs.push('gsm.network-id', cliArg(patch.networkId));
89
+ }
90
+ return pairs;
91
+ }
92
+ /** Map an nmcli terse readback back onto a `GsmProfile` (inverse of the write parity). */
93
+ export function buildProfile(id, settings) {
94
+ const username = settings.get('gsm.username') ?? '';
95
+ const password = settings.get('gsm.password') ?? '';
96
+ const networkId = settings.get('gsm.network-id') ?? '';
97
+ return {
98
+ connectionId: id,
99
+ connectionName: settings.get('connection.id') ?? '',
100
+ apn: settings.get('gsm.apn') ?? '',
101
+ homeOnly: settings.get('gsm.home-only') === 'yes',
102
+ autoConfig: settings.get('gsm.auto-config') === 'yes',
103
+ ...(username !== '' ? { username } : {}),
104
+ ...(password !== '' ? { password } : {}),
105
+ ...(networkId !== '' ? { networkId } : {}),
106
+ };
107
+ }
@@ -0,0 +1,28 @@
1
+ import { type ConnectionId, type DeviceIfname, type GsmProfile, type GsmProfileInput, type GsmProfilePatch, type NetworkManagerPort, type QuiesceLease, type Receipt } from '../ports/index.js';
2
+ import { type AutoApnTransitionResult, type ManualApn } from './nm-auto-apn.js';
3
+ import { type NmcliRunner } from './nmcli-runner.js';
4
+ export interface NmcliNmPortOptions {
5
+ readonly runner: NmcliRunner;
6
+ /** Resolved once at boot by the capability probe; default `true`. */
7
+ readonly autoApnCapable?: boolean;
8
+ readonly leaseTtlMs?: number;
9
+ readonly now?: () => number;
10
+ }
11
+ export declare class NmcliNmPort implements NetworkManagerPort {
12
+ #private;
13
+ constructor(options: NmcliNmPortOptions);
14
+ createGsmProfile(profile: GsmProfileInput): Promise<GsmProfile>;
15
+ readGsmProfile(id: ConnectionId): Promise<GsmProfile | undefined>;
16
+ updateGsmProfile(id: ConnectionId, patch: GsmProfilePatch): Promise<GsmProfile>;
17
+ deleteGsmProfile(id: ConnectionId): Promise<void>;
18
+ activate(id: ConnectionId, ifname: DeviceIfname): Promise<Receipt>;
19
+ deactivate(id: ConnectionId, ifname: DeviceIfname): Promise<Receipt>;
20
+ acquireQuiesceLease(id: ConnectionId, ifname: DeviceIfname): Promise<QuiesceLease>;
21
+ releaseQuiesceLease(lease: QuiesceLease): Promise<void>;
22
+ /** Reactivate every lease held past the TTL — the abandoned-lease watchdog. */
23
+ sweepExpiredLeases(now?: number): Promise<void>;
24
+ /** Flip a profile to Auto-APN via ONE atomic modify, then reactivate + classify. */
25
+ transitionToAuto(id: ConnectionId, ifname: DeviceIfname): Promise<AutoApnTransitionResult>;
26
+ /** Flip a profile to manual APN via ONE atomic modify, then reactivate + classify. */
27
+ transitionToManual(id: ConnectionId, ifname: DeviceIfname, creds: ManualApn): Promise<AutoApnTransitionResult>;
28
+ }
@@ -0,0 +1,173 @@
1
+ // The device-exact NetworkManager adapter: a FRESH nmcli-based `NetworkManagerPort`
2
+ // (no reused CeraUI code) with the full nine-field GSM write parity, verify-then-
3
+ // device-disconnect deactivation (never id-only `connection down`), abandoned-lease
4
+ // quiesce, and atomic Auto-APN transitions.
5
+ import { epochMillis } from '../domain/index.js';
6
+ import { connectionId, receipt, } from '../ports/index.js';
7
+ import { AUTO_APN_ADVISORY, classifyActivation, toAutoArgs, toManualArgs, } from './nm-auto-apn.js';
8
+ import { buildProfile, createGsmArgs, patchPairs } from './nm-gsm-fields.js';
9
+ import { parseTerse, runNmcli } from './nmcli-runner.js';
10
+ const READBACK_FIELDS = [
11
+ 'connection.id',
12
+ 'gsm.apn',
13
+ 'gsm.username',
14
+ 'gsm.password',
15
+ 'gsm.password-flags',
16
+ 'gsm.home-only',
17
+ 'gsm.network-id',
18
+ 'gsm.auto-config',
19
+ 'connection.autoconnect',
20
+ 'connection.autoconnect-retries',
21
+ ];
22
+ /** Default abandoned-lease TTL: a held quiesce lease older than this auto-releases. */
23
+ const DEFAULT_LEASE_TTL_MS = 60_000;
24
+ export class NmcliNmPort {
25
+ #runner;
26
+ #autoApnCapable;
27
+ #leaseTtlMs;
28
+ #now;
29
+ #leases = new Set();
30
+ constructor(options) {
31
+ this.#runner = options.runner;
32
+ this.#autoApnCapable = options.autoApnCapable ?? true;
33
+ this.#leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
34
+ this.#now = options.now ?? Date.now;
35
+ }
36
+ async createGsmProfile(profile) {
37
+ const result = await runNmcli(this.#runner, createGsmArgs(profile));
38
+ const uuid = /\(([^)]+)\) successfully added/.exec(result.stdout)?.[1];
39
+ if (uuid === undefined) {
40
+ throw new Error(`nmcli connection add failed: ${result.stderr || result.stdout}`);
41
+ }
42
+ return this.#requireProfile(connectionId(uuid));
43
+ }
44
+ async readGsmProfile(id) {
45
+ const result = await runNmcli(this.#runner, [
46
+ '-t',
47
+ '-f',
48
+ READBACK_FIELDS.join(','),
49
+ 'connection',
50
+ 'show',
51
+ id,
52
+ ]);
53
+ if (result.exitCode !== 0) {
54
+ return undefined;
55
+ }
56
+ return buildProfile(id, parseTerse(result.stdout));
57
+ }
58
+ async updateGsmProfile(id, patch) {
59
+ const result = await runNmcli(this.#runner, ['connection', 'modify', id, ...patchPairs(patch)]);
60
+ if (result.exitCode !== 0) {
61
+ throw new Error(`nmcli connection modify failed: ${result.stderr}`);
62
+ }
63
+ return this.#requireProfile(id);
64
+ }
65
+ async deleteGsmProfile(id) {
66
+ await runNmcli(this.#runner, ['connection', 'delete', id]);
67
+ }
68
+ async activate(id, ifname) {
69
+ const result = await runNmcli(this.#runner, ['connection', 'up', id, 'ifname', ifname]);
70
+ return activationReceipt(result, `activated ${id} on ${ifname}`);
71
+ }
72
+ async deactivate(id, ifname) {
73
+ if ((await this.#activeUuidOn(ifname)) !== id) {
74
+ return receipt('enabled', 'applied', `${id} not active on ${ifname}; nothing to deactivate`);
75
+ }
76
+ const result = await runNmcli(this.#runner, ['device', 'disconnect', ifname]);
77
+ return activationReceipt(result, `deactivated ${id} on ${ifname}`);
78
+ }
79
+ async acquireQuiesceLease(id, ifname) {
80
+ if ((await this.#activeUuidOn(ifname)) === id) {
81
+ await runNmcli(this.#runner, ['device', 'disconnect', ifname]);
82
+ }
83
+ const lease = {
84
+ connectionId: id,
85
+ deviceIfname: ifname,
86
+ acquiredAt: epochMillis(this.#now()),
87
+ };
88
+ this.#leases.add(lease);
89
+ return lease;
90
+ }
91
+ async releaseQuiesceLease(lease) {
92
+ if (this.#leases.delete(lease)) {
93
+ await this.#reactivate(lease);
94
+ }
95
+ }
96
+ /** Reactivate every lease held past the TTL — the abandoned-lease watchdog. */
97
+ async sweepExpiredLeases(now = this.#now()) {
98
+ for (const lease of [...this.#leases]) {
99
+ if (now - lease.acquiredAt >= this.#leaseTtlMs) {
100
+ this.#leases.delete(lease);
101
+ await this.#reactivate(lease);
102
+ }
103
+ }
104
+ }
105
+ /** Flip a profile to Auto-APN via ONE atomic modify, then reactivate + classify. */
106
+ async transitionToAuto(id, ifname) {
107
+ if (!this.#autoApnCapable) {
108
+ return {
109
+ receipt: receipt('connection', 'unsupported', 'Auto-APN requires NetworkManager >= 1.22'),
110
+ advisory: AUTO_APN_ADVISORY,
111
+ };
112
+ }
113
+ const modify = await runNmcli(this.#runner, toAutoArgs(id));
114
+ if (modify.exitCode !== 0) {
115
+ return {
116
+ receipt: receipt('connection', 'failed', modify.stderr || 'auto-config modify rejected'),
117
+ };
118
+ }
119
+ return this.#reactivateAndClassify(id, ifname, true);
120
+ }
121
+ /** Flip a profile to manual APN via ONE atomic modify, then reactivate + classify. */
122
+ async transitionToManual(id, ifname, creds) {
123
+ const modify = await runNmcli(this.#runner, toManualArgs(id, creds));
124
+ if (modify.exitCode !== 0) {
125
+ return {
126
+ receipt: receipt('connection', 'failed', modify.stderr || 'manual-apn modify rejected'),
127
+ };
128
+ }
129
+ return this.#reactivateAndClassify(id, ifname, false);
130
+ }
131
+ async #reactivateAndClassify(id, ifname, underAuto) {
132
+ const up = await runNmcli(this.#runner, ['connection', 'up', id, 'ifname', ifname]);
133
+ return classifyActivation(up, { underAuto });
134
+ }
135
+ async #reactivate(lease) {
136
+ await runNmcli(this.#runner, [
137
+ 'connection',
138
+ 'up',
139
+ lease.connectionId,
140
+ 'ifname',
141
+ lease.deviceIfname,
142
+ ]);
143
+ }
144
+ async #activeUuidOn(ifname) {
145
+ const result = await runNmcli(this.#runner, [
146
+ '-t',
147
+ '-f',
148
+ 'UUID,DEVICE',
149
+ 'connection',
150
+ 'show',
151
+ '--active',
152
+ ]);
153
+ for (const line of result.stdout.split('\n')) {
154
+ const [uuid, device] = line.split(':');
155
+ if (device === ifname && uuid) {
156
+ return uuid;
157
+ }
158
+ }
159
+ return undefined;
160
+ }
161
+ async #requireProfile(id) {
162
+ const profile = await this.readGsmProfile(id);
163
+ if (profile === undefined) {
164
+ throw new Error(`nmcli: profile ${id} did not read back`);
165
+ }
166
+ return profile;
167
+ }
168
+ }
169
+ function activationReceipt(result, appliedReason) {
170
+ return result.exitCode === 0
171
+ ? receipt('enabled', 'applied', appliedReason)
172
+ : receipt('enabled', 'failed', result.stderr || 'nmcli returned a non-zero exit code');
173
+ }
@@ -0,0 +1,24 @@
1
+ export interface NmcliResult {
2
+ readonly stdout: string;
3
+ readonly stderr: string;
4
+ readonly exitCode: number;
5
+ }
6
+ /**
7
+ * A runner over nmcli argv. `run` may be synchronous (the stateful test double) or
8
+ * asynchronous (the real `Bun.spawn` adapter) — the port awaits either way.
9
+ */
10
+ export interface NmcliRunner {
11
+ run(argv: readonly string[]): NmcliResult | Promise<NmcliResult>;
12
+ }
13
+ /** Await a runner's result whether it returned synchronously or as a promise. */
14
+ export declare function runNmcli(runner: NmcliRunner, argv: readonly string[]): Promise<NmcliResult>;
15
+ /**
16
+ * The device-exact runner: spawns the real `nmcli` via Bun. Every argv the port
17
+ * builds is passed verbatim, so what the tests assert against the stateful stub is
18
+ * byte-for-byte what runs on the device.
19
+ */
20
+ export declare class SpawnNmcliRunner implements NmcliRunner {
21
+ run(argv: readonly string[]): Promise<NmcliResult>;
22
+ }
23
+ /** Parse nmcli terse (`-t`) `key:value` lines into a map, splitting on the first `:`. */
24
+ export declare function parseTerse(stdout: string): Map<string, string>;
@@ -0,0 +1,35 @@
1
+ // The nmcli invocation seam. The shipping adapter spawns the real `nmcli`; tests
2
+ // inject A2.3's stateful in-memory runner (structurally the same `run(argv)`). The
3
+ // port never spawns directly — it only ever talks to an injected `NmcliRunner`, so
4
+ // the exact code path exercised under test is the one that runs on-device.
5
+ /** Await a runner's result whether it returned synchronously or as a promise. */
6
+ export async function runNmcli(runner, argv) {
7
+ return runner.run(argv);
8
+ }
9
+ /**
10
+ * The device-exact runner: spawns the real `nmcli` via Bun. Every argv the port
11
+ * builds is passed verbatim, so what the tests assert against the stateful stub is
12
+ * byte-for-byte what runs on the device.
13
+ */
14
+ export class SpawnNmcliRunner {
15
+ async run(argv) {
16
+ const proc = Bun.spawn(['nmcli', ...argv], { stdout: 'pipe', stderr: 'pipe' });
17
+ const [stdout, stderr, exitCode] = await Promise.all([
18
+ new Response(proc.stdout).text(),
19
+ new Response(proc.stderr).text(),
20
+ proc.exited,
21
+ ]);
22
+ return { stdout, stderr, exitCode };
23
+ }
24
+ }
25
+ /** Parse nmcli terse (`-t`) `key:value` lines into a map, splitting on the first `:`. */
26
+ export function parseTerse(stdout) {
27
+ const settings = new Map();
28
+ for (const line of stdout.split('\n')) {
29
+ const separator = line.indexOf(':');
30
+ if (separator >= 0) {
31
+ settings.set(line.slice(0, separator), line.slice(separator + 1));
32
+ }
33
+ }
34
+ return settings;
35
+ }
@@ -0,0 +1,37 @@
1
+ import type { ModemObservationPort, ObservationList, ObservationListener, Unsubscribe } from '../ports/index.js';
2
+ import type { DbusTransport } from '../transport/index.js';
3
+ import { type DecodedManagedObjects } from './managed-objects.js';
4
+ /**
5
+ * A current-epoch authoritative snapshot, delivered to `onEpochRefresh` AFTER the
6
+ * epoch guard passes. `epoch` is the owning MM unique bus name; `tree` is the decoded
7
+ * `GetManagedObjects` payload the snapshot was reconciled from. The Signal.Setup
8
+ * manager (A3.3) hooks this to (re-)apply cadence per modem per epoch, and the D-Bus
9
+ * backend uses it to refresh its path→stable-key map.
10
+ */
11
+ export interface EpochRefreshEvent {
12
+ readonly epoch: string;
13
+ readonly tree: DecodedManagedObjects;
14
+ }
15
+ export interface MmDbusObserverOptions {
16
+ /** The transport to talk D-Bus over (A2.4). The observer connects it on `start()`. */
17
+ readonly transport: DbusTransport;
18
+ /** MM bus name override (defaults to `org.freedesktop.ModemManager1`). */
19
+ readonly destination?: string;
20
+ /**
21
+ * Called after EVERY successful current-epoch authoritative snapshot (start,
22
+ * hot-plug, epoch change, property change) — never for a superseded epoch. The
23
+ * hook fires whether or not any row changed, so a consumer always sees the live
24
+ * epoch + tree. It must not throw; a throw is swallowed so it can never break the
25
+ * observer's refresh loop.
26
+ */
27
+ readonly onEpochRefresh?: (event: EpochRefreshEvent) => void;
28
+ }
29
+ export declare class MmDbusObserver implements ModemObservationPort {
30
+ #private;
31
+ constructor(options: MmDbusObserverOptions);
32
+ start(): Promise<ObservationList>;
33
+ observe(listener: ObservationListener): Unsubscribe;
34
+ stop(): Promise<void>;
35
+ }
36
+ /** Construct an epoch-scoped ModemManager observer over an A2.4 transport. */
37
+ export declare function createMmDbusObserver(options: MmDbusObserverOptions): MmDbusObserver;
@@ -0,0 +1,219 @@
1
+ // The epoch-scoped ModemManager observer.
2
+ //
3
+ // `MmDbusObserver` implements the read-only `ModemObservationPort` over the A2.4
4
+ // transport, tested against the A2.3 fake. `start()` connects, subscribes to the four
5
+ // lifecycle signals, THEN takes the first authoritative `GetManagedObjects` snapshot —
6
+ // reconciling any signal that raced in between.
7
+ //
8
+ // SAFETY-CRITICAL — epoch-scoped removal (draft §Oracle round-3 #5). An "epoch" is one
9
+ // continuous ownership period of the MM bus name, tracked via `NameOwnerChanged`. A
10
+ // modem is REMOVED only when it is missing from a CURRENT-epoch authoritative snapshot.
11
+ // Owner loss, bus disconnect, and any signal whose `sender` is not the current owner
12
+ // (an OLD-epoch straggler) never remove a modem — they only ever mark it
13
+ // `sourceUnavailable`. The false-removal class is dead by construction: even the
14
+ // `ObservationList` failure arm retains its rows. Row bookkeeping lives in
15
+ // `ObservationRowStore`; this file owns epoch tracking, subscriptions, and refresh.
16
+ import { DBUS_DESTINATION, DBUS_IFACE, DBUS_PATH, MM_BUS_NAME, MM_ROOT_PATH, OBJECT_MANAGER_IFACE, PROPERTIES_IFACE, } from './constants.js';
17
+ import { asManagedObjects } from './managed-objects.js';
18
+ import { ObservationRowStore } from './row-store.js';
19
+ export class MmDbusObserver {
20
+ #transport;
21
+ #destination;
22
+ #onEpochRefresh;
23
+ #store = new ObservationRowStore();
24
+ #listeners = new Set();
25
+ #subscriptions = [];
26
+ #currentOwner;
27
+ #started = false;
28
+ #stopped = false;
29
+ #priming = false;
30
+ #refreshDuringPrime = false;
31
+ #refreshing = false;
32
+ #refreshQueued = false;
33
+ #onDisconnected = () => this.#handleSourceGone('source-unavailable');
34
+ #onReconnected = () => {
35
+ void this.#adoptCurrentOwner();
36
+ };
37
+ constructor(options) {
38
+ this.#transport = options.transport;
39
+ this.#destination = options.destination ?? MM_BUS_NAME;
40
+ this.#onEpochRefresh = options.onEpochRefresh;
41
+ }
42
+ async start() {
43
+ if (this.#started) {
44
+ return this.#store.list();
45
+ }
46
+ this.#started = true;
47
+ this.#priming = true;
48
+ await this.#transport.connect();
49
+ this.#transport.on('disconnected', this.#onDisconnected);
50
+ this.#transport.on('reconnected', this.#onReconnected);
51
+ await this.#subscribeAll();
52
+ await this.#adoptCurrentOwner();
53
+ this.#priming = false;
54
+ if (this.#refreshDuringPrime) {
55
+ this.#refreshDuringPrime = false;
56
+ this.#scheduleRefresh();
57
+ }
58
+ return this.#store.list();
59
+ }
60
+ observe(listener) {
61
+ this.#listeners.add(listener);
62
+ return () => {
63
+ this.#listeners.delete(listener);
64
+ };
65
+ }
66
+ async stop() {
67
+ if (this.#stopped) {
68
+ return;
69
+ }
70
+ this.#stopped = true;
71
+ this.#transport.off('disconnected', this.#onDisconnected);
72
+ this.#transport.off('reconnected', this.#onReconnected);
73
+ const subs = this.#subscriptions.splice(0);
74
+ await Promise.all(subs.map((sub) => sub.unsubscribe().catch(() => undefined)));
75
+ this.#listeners.clear();
76
+ }
77
+ // ── signal subscription ────────────────────────────────────────────────────────
78
+ async #subscribeAll() {
79
+ const om = { interface: OBJECT_MANAGER_IFACE, path: MM_ROOT_PATH };
80
+ this.#subscriptions.push(await this.#transport.subscribeSignal({ ...om, member: 'InterfacesAdded' }, (event) => this.#onObjectSignal(event)), await this.#transport.subscribeSignal({ ...om, member: 'InterfacesRemoved' }, (event) => this.#onObjectSignal(event)), await this.#transport.subscribeSignal({ interface: PROPERTIES_IFACE, member: 'PropertiesChanged' }, (event) => this.#onObjectSignal(event)), await this.#transport.subscribeSignal({ interface: DBUS_IFACE, member: 'NameOwnerChanged' }, (event) => this.#onNameOwnerChanged(event)));
81
+ }
82
+ // ── epoch tracking ───────────────────────────────────────────────────────────
83
+ async #adoptCurrentOwner() {
84
+ const owner = await this.#queryOwner();
85
+ if (owner === undefined) {
86
+ this.#handleSourceGone('source-unavailable');
87
+ return;
88
+ }
89
+ this.#currentOwner = owner;
90
+ await this.#runRefresh(owner);
91
+ }
92
+ async #queryOwner() {
93
+ try {
94
+ const reply = await this.#transport.callMethod({
95
+ destination: DBUS_DESTINATION,
96
+ path: DBUS_PATH,
97
+ interface: DBUS_IFACE,
98
+ member: 'GetNameOwner',
99
+ signature: 's',
100
+ args: [MM_BUS_NAME],
101
+ });
102
+ const owner = reply.body[0];
103
+ return typeof owner === 'string' && owner.length > 0 ? owner : undefined;
104
+ }
105
+ catch {
106
+ // NameHasNoOwner (or a transient failure) → no current epoch yet.
107
+ return undefined;
108
+ }
109
+ }
110
+ #onNameOwnerChanged(event) {
111
+ if (event.body[0] !== MM_BUS_NAME) {
112
+ return;
113
+ }
114
+ const newOwner = typeof event.body[2] === 'string' ? event.body[2] : '';
115
+ if (newOwner.length === 0) {
116
+ // Owner lost — stale, never a removal.
117
+ this.#handleSourceGone('source-unavailable');
118
+ return;
119
+ }
120
+ if (newOwner === this.#currentOwner) {
121
+ return;
122
+ }
123
+ // New epoch: everything goes stale until the fresh snapshot restores it.
124
+ if (this.#store.markUnavailable('source-unavailable')) {
125
+ this.#emit();
126
+ }
127
+ this.#currentOwner = newOwner;
128
+ this.#scheduleRefresh();
129
+ }
130
+ #onObjectSignal(event) {
131
+ // Epoch guard: a signal from anyone but the current owner is an OLD-epoch
132
+ // straggler and must never drive a removal (draft §Oracle round-3 #5).
133
+ if (this.#currentOwner === undefined || event.sender !== this.#currentOwner) {
134
+ return;
135
+ }
136
+ if (this.#priming) {
137
+ this.#refreshDuringPrime = true;
138
+ return;
139
+ }
140
+ this.#scheduleRefresh();
141
+ }
142
+ // ── authoritative refresh ──────────────────────────────────────────────────────
143
+ #scheduleRefresh() {
144
+ const owner = this.#currentOwner;
145
+ if (owner === undefined || this.#stopped) {
146
+ return;
147
+ }
148
+ if (this.#refreshing) {
149
+ this.#refreshQueued = true;
150
+ return;
151
+ }
152
+ void this.#runRefresh(owner);
153
+ }
154
+ async #runRefresh(epochOwner) {
155
+ this.#refreshing = true;
156
+ try {
157
+ const reply = await this.#transport.callMethod({
158
+ destination: this.#destination,
159
+ path: MM_ROOT_PATH,
160
+ interface: OBJECT_MANAGER_IFACE,
161
+ member: 'GetManagedObjects',
162
+ });
163
+ // Late reply from a superseded epoch — discard (draft §Oracle round-3 #5).
164
+ if (this.#currentOwner !== epochOwner || this.#stopped) {
165
+ return;
166
+ }
167
+ const tree = asManagedObjects(reply.body[0]);
168
+ const rowsChanged = this.#store.reconcile(tree);
169
+ const healthChanged = this.#store.markHealthy();
170
+ this.#notifyEpochRefresh(epochOwner, tree);
171
+ if (rowsChanged || healthChanged) {
172
+ this.#emit();
173
+ }
174
+ }
175
+ catch {
176
+ if (this.#currentOwner === epochOwner && !this.#stopped) {
177
+ this.#markUnavailable('bus-error');
178
+ }
179
+ }
180
+ finally {
181
+ this.#refreshing = false;
182
+ if (this.#refreshQueued && !this.#stopped) {
183
+ this.#refreshQueued = false;
184
+ this.#scheduleRefresh();
185
+ }
186
+ }
187
+ }
188
+ // ── source-unavailable transitions ─────────────────────────────────────────────
189
+ #handleSourceGone(reason) {
190
+ this.#currentOwner = undefined;
191
+ this.#markUnavailable(reason);
192
+ }
193
+ #markUnavailable(reason) {
194
+ if (this.#store.markUnavailable(reason)) {
195
+ this.#emit();
196
+ }
197
+ }
198
+ #notifyEpochRefresh(epoch, tree) {
199
+ if (this.#onEpochRefresh === undefined) {
200
+ return;
201
+ }
202
+ try {
203
+ this.#onEpochRefresh({ epoch, tree });
204
+ }
205
+ catch {
206
+ // A consumer's hook must never break the observer's refresh loop.
207
+ }
208
+ }
209
+ #emit() {
210
+ const list = this.#store.list();
211
+ for (const listener of [...this.#listeners]) {
212
+ listener(list);
213
+ }
214
+ }
215
+ }
216
+ /** Construct an epoch-scoped ModemManager observer over an A2.4 transport. */
217
+ export function createMmDbusObserver(options) {
218
+ return new MmDbusObserver(options);
219
+ }
@@ -0,0 +1,49 @@
1
+ import type { EpochMillis } from '../domain/index.js';
2
+ /** How a board can power-cycle a modem. Only `none` is implemented in Phase A. */
3
+ export type PowerCapabilityKind = 'none' | 'gpio-cut' | 'pwrkey-pulse' | 'usb-hub-port-cycle';
4
+ /**
5
+ * The USB data mode a modem should re-enumerate into after a power cycle. A4.2 owns
6
+ * the certified USB-mode catalog; the power contract only records the operator's
7
+ * preferred post-cycle enumeration mode.
8
+ */
9
+ export type PreferredUsbMode = 'qmi' | 'mbim' | 'ecm-ncm' | 'rndis' | 'router-ethernet';
10
+ /** A board's modem power-control capability description. */
11
+ export interface PowerCapability {
12
+ readonly power: PowerCapabilityKind;
13
+ /** The board can pulse a USB-level reset on the modem's port. */
14
+ readonly usbReset?: boolean;
15
+ /** How long to wait for the modem to re-enumerate after a cycle. */
16
+ readonly enumerationTimeoutMs: number;
17
+ /** Which USB mode to prefer when the modem re-enumerates (A4.2 catalog vocabulary). */
18
+ readonly preferredUsbMode?: PreferredUsbMode;
19
+ }
20
+ /** The Phase-A capability: no board power control exists — a no-op. */
21
+ export declare const NONE_POWER_CAPABILITY: PowerCapability;
22
+ /** Outcome of asking the power hook to cycle a modem. */
23
+ export interface PowerCycleResult {
24
+ readonly status: 'applied' | 'unsupported' | 'failed';
25
+ readonly reason: string;
26
+ }
27
+ /** The minimal context handed to the power hook. */
28
+ export interface PowerCycleContext {
29
+ readonly stableKey: string;
30
+ readonly at: EpochMillis;
31
+ }
32
+ /**
33
+ * The pluggable power-cycle hook (recovery ladder rung 4). Phase A ships only the
34
+ * `none` no-op; a real GPIO / PWRKEY / USB-hub implementation is hardware-gated and
35
+ * injected later without changing the ladder.
36
+ */
37
+ export interface PowerHook {
38
+ readonly capability: PowerCapability;
39
+ cycle(context: PowerCycleContext): Promise<PowerCycleResult>;
40
+ }
41
+ /**
42
+ * Build a Phase-A power hook for `capability`. EVERY capability returns
43
+ * `unsupported`: `none` because there is nothing to cut, and every real mechanism
44
+ * because its hardware driver is not implemented yet — the contract field exists so
45
+ * a board can DECLARE the capability, but rung 4 never actuates in Phase A.
46
+ */
47
+ export declare function unsupportedPowerHook(capability: PowerCapability): PowerHook;
48
+ /** The Phase-A default power hook: describes `none` and always returns `unsupported`. */
49
+ export declare const NONE_POWER_HOOK: PowerHook;