@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
package/README.md ADDED
@@ -0,0 +1,317 @@
1
+ # `@ceralive/modem-control`
2
+
3
+ Cellular modem control for CeraLive: the frozen v1.1 domain contracts, the provider
4
+ registry and evidence-scored matcher, the ModemManager D-Bus backend, the
5
+ NetworkManager adapter, the desired-state reconciler, the USB composition-mode model,
6
+ the data-usage sampler, and the gated capability modules.
7
+
8
+ **This package is a LIBRARY.** It ships no `bin`, no systemd unit, no shebang, and
9
+ nothing that opens a listening socket — it is imported by a controller, it is not one.
10
+ That claim is checked against the real `bun pm pack` output, not against the source
11
+ tree; see [Shape gate](#shape-gate).
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install @ceralive/modem-control # or: bun add @ceralive/modem-control
17
+ ```
18
+
19
+ ESM only (`"type": "module"`). Node 26 and Bun 1.3 are the runtimes the published
20
+ tarball is exercised against on every CI run.
21
+
22
+ ## Public entry points
23
+
24
+ Seven specifiers, and nothing else. Every other module is internal and reachable only
25
+ through the root entry, so an internal reorganisation is not a breaking change.
26
+
27
+ | Specifier | What it carries |
28
+ |-----------|-----------------|
29
+ | `@ceralive/modem-control` | Everything below plus the ports, backend, reconciler, redaction, SMS, USSD, location and FCC modules |
30
+ | `@ceralive/modem-control/domain` | Frozen v1.1 contracts: `PhysicalModemId`, `DeviceGeneration`, `ObservationEnvelope`, `OperationDescriptor` / `OperationResult` |
31
+ | `@ceralive/modem-control/providers` | `ProviderDefinition`, the registry, and the evidence-scored matcher |
32
+ | `@ceralive/modem-control/capabilities` | The five-state support-claim taxonomy and per-modem capability detection |
33
+ | `@ceralive/modem-control/hardware` | The per-SKU hardware model: USB composition modes + certified catalog, and the band vocabulary + band-lock certification |
34
+ | `@ceralive/modem-control/transport` | The D-Bus transport seam (no underlying-library type is re-exported) |
35
+ | `@ceralive/modem-control/testing` | Public **contract fakes** for consumers' own tests |
36
+
37
+ The `./hardware` surface also owns the transport-free response parsers migrated
38
+ from CeraUI: SIM-presence evidence plus normalized Huawei HiLink, ZTE goform,
39
+ and Qualcomm UFI/HIMI signal, detail, and capability reads. They accept response
40
+ bodies only; HTTP sessions, interface binding, retries, caches, and writes remain
41
+ consumer-owned.
42
+
43
+ The root entry also carries the **observation layer** built on those parsers. It turns a raw
44
+ per-vendor payload into one `ObservationEnvelope<NormalizedModemObservation>` in which every
45
+ metric records which source produced it and when, and in which a missing value carries a
46
+ reason that says whether the source *cannot* report it (`unsupported`) or merely *did not*
47
+ on this read. `fresh`, `stale`, `unavailable` and `unknown` are four distinct shapes rather
48
+ than a value plus a flag: stale keeps its value, unavailable carries none and never ages into
49
+ stale, and unknown says which field is missing and why. Nothing the provider sent is
50
+ discarded — every provider-native field is retained verbatim in a typed diagnostics block,
51
+ with `unmapped` derived rather than declared. Desired, applied and observed state stay in
52
+ three separate slots.
53
+
54
+ The migration surface is broader than response parsing but remains pure: the root and
55
+ existing `./domain`, `./capabilities`, and `./hardware` entries expose portable physical
56
+ identity/link-id derivation, modem presentation rules, ModemManager enum decoding,
57
+ USB-network classification and labels, capability-module selection, and shadow-backend
58
+ divergence folding. Every helper consumes caller-supplied values or snapshots; none discovers
59
+ devices, opens a transport, persists state, or performs a modem write.
60
+
61
+ USB snapshots retain the udev `P:` record as an absolute `sysfsPath`, allowing consumers to
62
+ correlate ModemManager `Device`/`Physdev` paths to the most-specific USB parent without relying
63
+ on a network-interface name.
64
+
65
+ ### Typed ModemManager provider
66
+
67
+ `createModemManagerProvider({ transport })` returns the concrete `ModemManagerProvider` and its
68
+ provider-registry `definition`. Matching is based on the live ObjectManager tree, not a certified
69
+ model allowlist, so unknown future modems retain generic mode, signal, SIM, and power controls when
70
+ their runtime interfaces/properties advertise them. Its normalized `observe` result uses the root
71
+ observation envelope; its lifecycle `start` / `observe` / `stop` methods reuse the epoch-scoped
72
+ signal observer.
73
+
74
+ ### Huawei HiLink provider
75
+
76
+ `createHuaweiHiLinkDefinition()` exposes exact E3372H firmware profiles for password types 3 and
77
+ 4. Firmware plus `SesTokInfo` evidence chooses one profile, and `state-login` must confirm that
78
+ profile's password type before the provider makes its only login attempt. Every request is bound to
79
+ the injected network interface with redirects disabled. Mode and mobile-data writes acquire
80
+ `router-session`, probe their own capability, and require a new authenticated readback before they
81
+ can report `applied`; no Wi-Fi write exists. Credentials and session material remain private to the
82
+ provider runtime. See [`../docs/HUAWEI-HILINK-PROVIDER.md`](../docs/HUAWEI-HILINK-PROVIDER.md).
83
+
84
+ ### ZTE goform provider
85
+
86
+ `createZteGoformDefinition()` exposes the incompatible `mf79u-legacy` and
87
+ `mf266-salted` authentication profiles without fallback between them. MF79U sends one
88
+ browser-shaped form login with a base64 password; MF266 performs the `LD` challenge,
89
+ salted SHA-256 login, then derives `AD` from version data and `RD`. Session material stays
90
+ in memory. Unknown ZTE firmware is fingerprinted into a read-only telemetry profile, and
91
+ Wi-Fi writes are absent from every operation surface. See
92
+ [`../docs/MF79U-DIAGNOSIS.md`](../docs/MF79U-DIAGNOSIS.md).
93
+
94
+ ### UFI / HIMI provider — read-only by construction
95
+
96
+ `createUfiHimiDefinition()` normalizes the Qualcomm UFI/HIMI telemetry over the vendor's
97
+ single `POST /himiapi/json` endpoint. Because that API puts its verb in the request
98
+ body's `cmdid` rather than in the HTTP method, read-only is enforced as a **frozen
99
+ command vocabulary** — seven `get*` reads plus `login` — so a write command cannot be
100
+ expressed at all. `operations()` returns `ProviderReadOperations` entries verbatim and
101
+ exposes zero write descriptors.
102
+
103
+ The prohibited Qualcomm operations — NV, EFS, identity and calibration writes, firmware
104
+ flashing, EDL automation, blind driver/interface retries, DIAG writes, the DIAG info
105
+ probe, and shell transport fallback — are inert table entries with **no implementation
106
+ anywhere**. `planUfiOperation()` answers each with a typed reason and takes no transport
107
+ parameter, so the refusal provably precedes any device contact; the same ids driven
108
+ through `OperationEngine` are refused before execution too.
109
+
110
+ `05c6:9024` is evidence of an RNDIS+ADB composition, not a permission. `05c6:9091` is a
111
+ firmware-chosen product id and is **not** proof of DIAG — only an interface descriptor is,
112
+ and production access stays `prohibited` regardless. The supervised, read-only, bench-only
113
+ probe is documented in [`../docs/UFI-DIAG-PROBE.md`](../docs/UFI-DIAG-PROBE.md).
114
+
115
+ ### NetworkManager adapter — saved vs applied
116
+
117
+ `new NetworkManagerAdapter({ port })` is the bearer/APN authority, and the only one. It holds
118
+ three separate slots per NM connection: the **desired** profile (what an operator asked for,
119
+ recorded from the request), the **applied** bearer (what NM actually put into force, recorded
120
+ from NM's readback, together with the interface it landed on), and the **observed** device
121
+ state. `observe()` folds one complete NM readout and reports a typed applied-state LOSS —
122
+ `interface-absent`, `interface-detached`, `connection-replaced`, or `activation-failed` — while
123
+ leaving the desired profile untouched, so a modem that re-enumerates costs you the bearer and
124
+ never the configuration. A device still settling is reported `pending` rather than lost, and a
125
+ readout from a superseded generation is refused rather than folded late.
126
+
127
+ It composes the existing `NmcliNmPort` rather than replacing it, performs no radio, band, SIM
128
+ or power operation, keys every slot by NM's connection UUID rather than by a physical modem
129
+ identity, mirrors no credential into a state slot, and has no profile-delete path.
130
+
131
+ The operation surface composes the existing radio/band backend, GPS location adapter and bounded
132
+ fix-state machine, read-only SMS port, USSD session adapter, and FCC coverage catalog. Generic band
133
+ reads are always runtime-driven; disruptive band writes additionally require a certification
134
+ catalog entry for the device's SKU. It contains no bearer/APN authority and no command-line
135
+ fallback. The provider never invokes `mmcli`, `qmicli`, or `mbimcli`—those remain operator
136
+ diagnostics only.
137
+
138
+ ### Radio capability truth — the modem's own catalog, unedited
139
+
140
+ The root export also carries the mode/band **capability truth** layer. `SupportedModes`
141
+ and `CurrentModes` are decoded without loss: a combination whose preferred mask is 0
142
+ reads `preferred: 'none'` — a value, not a missing field — and reaches
143
+ `descriptor.constraints.values` exactly as the modem stated it. A mode bit this build
144
+ does not name round-trips as `mode-bit-<n>`, its combination is classified
145
+ `unknown-combination`, and it stays **offered**; a catalog member that is not a `(uu)`
146
+ pair is retained in `undecodable` rather than dropped, so decoded plus undecodable is
147
+ always the member count the modem sent. A selection the modem never advertised is
148
+ refused, never rounded to the nearest one.
149
+
150
+ `modes` and `bands` are typed write operations with **required readback**: the daemon
151
+ accepting `SetCurrentModes` / `SetCurrentBands` only proves the call was accepted, so
152
+ the applied value is re-read and compared before either reports success. `bands`
153
+ additionally carries `mutationImpact: 'disruptive'`, a `band-certification-present`
154
+ live precondition, and an availability that is `refused` with
155
+ `band-certification-required` unless the device's SKU resolves to an entry in the
156
+ band-lock certification catalog — which ships empty, so that is today's answer for every
157
+ device. Supply `bandSku` to `createModemManagerProvider` to resolve one;
158
+ ModemManager exposes no USB `vid:pid`, so the package cannot build a `BandSku` alone.
159
+
160
+ Both operations expose `describe(context)` alongside their static `descriptor`, because
161
+ a static descriptor cannot carry a device's own catalog or its certification state.
162
+
163
+ ### SIM presence is evidence, never inference
164
+
165
+ `readSimPresence` returns the presence together with the `SimPresenceEvidence` that
166
+ decided it, and `absent` is reachable through exactly one evidence kind — ModemManager's
167
+ own `StateFailedReason: sim-missing`. A blank `Sim` object path proves nothing (MM
168
+ reports `/` while a modem initializes and while a slot switch is in flight) and reads
169
+ `unknown`. `ModemManagerSimState.present` is positive evidence only: `false` is not a
170
+ claim of absence. The Huawei, ZTE and UFI sources still claim no presence at all and now
171
+ NAME the vendor code they left undecoded, which stays verbatim in the diagnostics block.
172
+
173
+ `Modem.CurrentModes` and `Modem.SignalQuality` are retained as the D-Bus structs they
174
+ are, so the preferred mode and the measurement-recency flag survive normalization;
175
+ `NormalizedSignal.qualityRecent` claims that flag, and the router sources answer
176
+ `unsupported` for it.
177
+
178
+ ### Mutation safety ports
179
+
180
+ The root export includes `MutationAdmissionPort`, `ResourceOwnershipPort`,
181
+ `ModemManagerInhibitPort`, and `UhubctlPort`. Admission remains consumer-owned: a required
182
+ mutation without an injected admission port is refused as `admission-port-missing`; this
183
+ package does not know or infer why the consumer refused it.
184
+
185
+ File stores, router sessions, and USB-hub access use acquire-or-refuse exclusive ownership.
186
+ `createFlockResourceOwnershipPort({ lockPath })` is the Linux adapter: non-blocking `flock`,
187
+ holder PID/start-time metadata, and clean release when the holder process dies. The lock path
188
+ is mandatory input; `DEFAULT_MODEM_CONTROL_LOCK_PATH` is only a conventional value callers
189
+ may select. There is no pass-through ownership implementation.
190
+
191
+ One `createModemControlCompositionRoot()` may be live per process. A second construction
192
+ throws, and `actorFor(physicalModemId)` shares one actor for that modem across all callers in
193
+ the root. `UhubctlPort` has no control-package implementation; an embedding process must
194
+ inject one and own its executable policy.
195
+
196
+ ### Descriptor-gated operation engine
197
+
198
+ `createOperationEngine()` executes `OperationDescriptor` contracts through that composition
199
+ root. Every mutation enters the root's shared physical-modem actor before its live preconditions
200
+ and admission are checked. Writes are therefore single-flight per physical modem, and a mutation
201
+ that waited in the queue cannot reuse facts checked before it waited. Reads do not occupy the
202
+ write queue; the engine retries only a failed read whose descriptor explicitly says
203
+ `idempotent-read`, once.
204
+
205
+ A stale-generation completion or a timed-out/dropped write reply is classified by the frozen
206
+ domain helper as `unknown-outcome`. The engine then closes a per-`PhysicalModemId` mutation gate:
207
+ subsequent mutations are refused as `reconciliation-required` without calling the provider.
208
+ `engine.reconcile()` uses the same actor and reopens the gate only when reconciliation finishes in
209
+ the requested current generation. Required readback, rollback, and journal hooks are checked before
210
+ execution and fired according to the descriptor; an unknown outcome is never treated as a definite
211
+ failure that is safe to roll back.
212
+
213
+ ### Transaction journal — the path comes from you
214
+
215
+ `createFileJournalStore({ path })` and `createJournalEngine({ store })` are the durable
216
+ half of that reconciliation gate. The engine satisfies the operation engine's
217
+ `OperationJournalHook`, so it can be handed straight to an `OperationExecution` as its
218
+ `journal`, and `engine.recover()` reads the file back after a restart and reports which
219
+ operations were still `pending` and which ended `unknown-outcome` — the set a controller
220
+ must reconcile before it mutates those modems again.
221
+
222
+ **The path is required and this package has no default for it.** Where a journal lives is
223
+ a property of the system embedding this library, not of the library, so there is no
224
+ fallback location to accidentally write to. An empty path is refused with
225
+ `JournalPathError`.
226
+
227
+ The store is append-only and never truncates itself. A record it cannot decode is returned
228
+ as typed damage alongside every record that *did* decode — including the ones after it — so
229
+ a single corrupt line can never take the rest of the journal with it. Call
230
+ `assertJournalIntact(recovery)` to escalate that damage to a `JournalRecoveryError` once
231
+ you have seen what survived. Neither an operation's input nor its returned value is ever
232
+ written to disk.
233
+
234
+ `readLegacyCeraUiJournal({ dir })` reads an older per-modem snapshot journal into the same
235
+ recovery model, so a consumer migrating onto this package can enumerate outstanding work
236
+ from files written before it existed. It only ever reads.
237
+
238
+ ### `./testing` is the contract-fakes surface
239
+
240
+ A consumer writing tests against this package needs valid instances of the domain and
241
+ provider contracts. Hand-rolling them is how a consumer's fixtures come to disagree
242
+ with the package — a hand-written `OperationResult` literal quietly stops matching what
243
+ `classifyOperationCompletion` actually returns. Every fake in `./testing` is built
244
+ through this package's own constructors and classifiers, so it cannot express a shape
245
+ the domain refuses.
246
+
247
+ ```ts
248
+ import { createProviderMatcher, createProviderRegistry } from '@ceralive/modem-control/providers';
249
+ import { fakeProviderDefinition, fakeProviderMatchRequest } from '@ceralive/modem-control/testing';
250
+
251
+ const registry = createProviderRegistry();
252
+ registry.register(fakeProviderDefinition({ observation: { registered: true } }));
253
+
254
+ const result = await createProviderMatcher(registry).match(fakeProviderMatchRequest());
255
+ ```
256
+
257
+ `./testing` is pure data and functions — no bus, no daemon, no process, no filesystem.
258
+ It is **not** the repository's `test-support/` directory, which holds the heavy
259
+ internals this package's own tests use (an MM-faithful fake D-Bus service on a private
260
+ session bus, a stateful `nmcli` harness, and the provider-matching conformance corpus).
261
+ Those are unpublished and are not a reusable surface.
262
+
263
+ ### Provider-matching conformance matrix
264
+
265
+ `src/providers/conformance-matrix.test.ts` runs 20 cases — nine fleet profiles plus
266
+ eleven ambiguity / malformed / auth-expired / lockout / unknown-firmware /
267
+ wrong-interface / wrong-transport cases — with the ModemManager, Huawei HiLink, ZTE
268
+ goform and UFI/HIMI providers **all registered at once**, asserting the exact provider,
269
+ profile, writability and evidence score per case. A companion suite asserts the exact
270
+ per-firmware HTTP transcript (method, path, form/JSON/XML body, header order, cookie,
271
+ and request count), and a third is a **software upper-bound fixture at 16 concurrently
272
+ attached modems** — a fixture result, not a hardware claim; the bench-verified fleet
273
+ size remains 8. See [`../docs/PROVIDER-MATCHING.md`](../docs/PROVIDER-MATCHING.md).
274
+
275
+ ## Build
276
+
277
+ ```sh
278
+ bun run build # tsc -> dist/ (ESM + .d.ts), then fully specify every relative specifier
279
+ bun run verify:tarball # pack and assert the published artifact's shape
280
+ bun run verify:consumers # install the tarball into standalone Node 26 + Bun projects and import every subpath
281
+ ```
282
+
283
+ `dist/` mirrors `src/` one-to-one rather than being bundled. Bundling with code
284
+ splitting produced an entry whose `export { … }` list named symbols the file never
285
+ imported — accepted by one loader, a `SyntaxError` in another. Bundling *without*
286
+ splitting instead gives each subpath its own copy of the shared modules, which silently
287
+ breaks `instanceof` across two subpaths of the same package. A 1:1 emit has exactly one
288
+ instance of every module.
289
+
290
+ Because `tsc` never rewrites a specifier and this package's sources are written for
291
+ bundler resolution, `scripts/build.ts` rewrites each emitted `./x` into `./x.js` or
292
+ `./x/index.js` — resolved against the emit itself — and fails the build if one
293
+ extensionless specifier survives.
294
+
295
+ `prepack` runs the build, so `npm pack` / `bun pm pack` can never publish a stale `dist/`.
296
+
297
+ ## Shape gate
298
+
299
+ `scripts/tarball-shape.ts` runs six rules over the extracted tarball, driven from
300
+ `bun test` (`scripts/tarball-shape.test.ts`) and from the CLI
301
+ (`scripts/assert-tarball-shape.ts`):
302
+
303
+ 1. no raw source ships — the published surface is built output;
304
+ 2. `dist/` actually contains JavaScript and declarations;
305
+ 3. every declared public entry is in the exports map **and** its files are packed;
306
+ 4. no subpath beyond the declared set — internal barrels stay internal;
307
+ 5. no export target, `main` or `types` points outside `./dist/`;
308
+ 6. the library-only proof: no `bin`, no systemd unit, no shebang, no listening-socket
309
+ construct.
310
+
311
+ The declared entries live in `scripts/entries.ts` and the test additionally spells the
312
+ seven specifiers out as a literal, so a subpath cannot be dropped without a reviewable
313
+ change to the public contract.
314
+
315
+ ## License
316
+
317
+ AGPL-3.0
@@ -0,0 +1,57 @@
1
+ import { type EpochMillis } from '../domain/index.js';
2
+ /** The baseline allowlist — identify only. Catalog commands are unioned in per SKU. */
3
+ export declare const AT_BASELINE_ALLOWLIST: ReadonlySet<string>;
4
+ /** Union the baseline allowlist with a catalog entry's declared transition commands. */
5
+ export declare function computeAtAllowlist(commands: Iterable<string>): ReadonlySet<string>;
6
+ /** An AT command's response. `ok` (an `OK` terminator) is NEVER transition-success alone. */
7
+ export interface AtResponse {
8
+ readonly ok: boolean;
9
+ readonly raw: string;
10
+ }
11
+ /** The raw AT transport — a serial write, injected so tests need no hardware. */
12
+ export interface AtCommandSender {
13
+ send(command: string): Promise<AtResponse>;
14
+ }
15
+ /** One audited AT attempt. Recorded only after passing through `redact`. */
16
+ export interface AtAuditEntry {
17
+ readonly command: string;
18
+ readonly outcome: 'sent' | 'rejected' | 'timeout' | 'error';
19
+ readonly at: EpochMillis;
20
+ readonly ok?: boolean;
21
+ readonly reason?: string;
22
+ readonly context?: Record<string, unknown>;
23
+ }
24
+ /** Where audit entries go — receives a REDACTED copy of each `AtAuditEntry`. */
25
+ export interface AtAuditSink {
26
+ record(entry: unknown): void;
27
+ }
28
+ /** Thrown when a command outside the allowlist is attempted — the sender is never called. */
29
+ export declare class AtCommandNotAllowedError extends Error {
30
+ constructor(command: string);
31
+ }
32
+ /** Thrown when a command exceeds the watchdog timeout. */
33
+ export declare class AtCommandTimeoutError extends Error {
34
+ constructor(command: string, timeoutMs: number);
35
+ }
36
+ /** Construction dependencies for an `AtCommandLease`. */
37
+ export interface AtCommandLeaseDeps {
38
+ readonly sender: AtCommandSender;
39
+ readonly allowlist: ReadonlySet<string>;
40
+ readonly audit?: AtAuditSink;
41
+ readonly now?: () => EpochMillis;
42
+ readonly timeoutMs?: number;
43
+ /** Fired when a command exceeds the timeout — the transition wires force-uninhibit here. */
44
+ readonly onWatchdog?: (command: string) => void | Promise<void>;
45
+ }
46
+ /**
47
+ * A held AT-command lease. `run` enforces the allowlist, bounds the send with a
48
+ * watchdog, and audits every attempt (redacted). The allowlist is fixed at
49
+ * construction from `computeAtAllowlist(entry)`, so a lease can only ever emit the
50
+ * commands one certified SKU permits.
51
+ */
52
+ export declare class AtCommandLease {
53
+ #private;
54
+ constructor(deps: AtCommandLeaseDeps);
55
+ /** Send one AT command through the lease. `context` is redacted into the audit entry. */
56
+ run(command: string, context?: Record<string, unknown>): Promise<AtResponse>;
57
+ }
@@ -0,0 +1,109 @@
1
+ // The AT-command lease baseline — the ONLY channel raw AT commands may travel.
2
+ //
3
+ // Three non-negotiable safety properties (draft §rounds 5/6, §84 raw-AT lease):
4
+ // 1. ALLOWLIST — only `ATI` (identify) plus the exact commands a certified catalog
5
+ // entry declares may ever be sent. Anything else is rejected BEFORE the sender
6
+ // is touched. There is no escape hatch.
7
+ // 2. WATCHDOG — a command that does not return within the timeout fires the
8
+ // `onWatchdog` hook (the transition wires this to force-uninhibit) and rejects,
9
+ // so a hung AT write can never wedge the transaction forever.
10
+ // 3. AUDIT + REDACTION — every attempt is recorded through A2.2's `redact` (never a
11
+ // reimplementation), so an identifier that lands in an audit entry's context is
12
+ // stripped before it is stored.
13
+ import { epochMillis } from '../domain/index.js';
14
+ import { redact } from '../redact.js';
15
+ /** The baseline allowlist — identify only. Catalog commands are unioned in per SKU. */
16
+ export const AT_BASELINE_ALLOWLIST = new Set(['ATI']);
17
+ /** Union the baseline allowlist with a catalog entry's declared transition commands. */
18
+ export function computeAtAllowlist(commands) {
19
+ return new Set([...AT_BASELINE_ALLOWLIST, ...commands]);
20
+ }
21
+ /** Thrown when a command outside the allowlist is attempted — the sender is never called. */
22
+ export class AtCommandNotAllowedError extends Error {
23
+ constructor(command) {
24
+ super(`AT command not in allowlist: ${command}`);
25
+ this.name = 'AtCommandNotAllowedError';
26
+ Object.setPrototypeOf(this, AtCommandNotAllowedError.prototype);
27
+ }
28
+ }
29
+ /** Thrown when a command exceeds the watchdog timeout. */
30
+ export class AtCommandTimeoutError extends Error {
31
+ constructor(command, timeoutMs) {
32
+ super(`AT command timed out after ${timeoutMs}ms: ${command}`);
33
+ this.name = 'AtCommandTimeoutError';
34
+ Object.setPrototypeOf(this, AtCommandTimeoutError.prototype);
35
+ }
36
+ }
37
+ const DEFAULT_AT_TIMEOUT_MS = 10_000;
38
+ /**
39
+ * A held AT-command lease. `run` enforces the allowlist, bounds the send with a
40
+ * watchdog, and audits every attempt (redacted). The allowlist is fixed at
41
+ * construction from `computeAtAllowlist(entry)`, so a lease can only ever emit the
42
+ * commands one certified SKU permits.
43
+ */
44
+ export class AtCommandLease {
45
+ #sender;
46
+ #allowlist;
47
+ #audit;
48
+ #now;
49
+ #timeoutMs;
50
+ #onWatchdog;
51
+ constructor(deps) {
52
+ this.#sender = deps.sender;
53
+ this.#allowlist = deps.allowlist;
54
+ this.#audit = deps.audit;
55
+ this.#now = deps.now ?? (() => epochMillis(Date.now()));
56
+ this.#timeoutMs = deps.timeoutMs ?? DEFAULT_AT_TIMEOUT_MS;
57
+ this.#onWatchdog = deps.onWatchdog;
58
+ }
59
+ /** Send one AT command through the lease. `context` is redacted into the audit entry. */
60
+ async run(command, context) {
61
+ const ctx = context !== undefined ? { context } : {};
62
+ if (!this.#allowlist.has(command)) {
63
+ this.#record({
64
+ command,
65
+ outcome: 'rejected',
66
+ at: this.#now(),
67
+ reason: 'not in allowlist',
68
+ ...ctx,
69
+ });
70
+ throw new AtCommandNotAllowedError(command);
71
+ }
72
+ try {
73
+ const response = await this.#sendWithWatchdog(command);
74
+ this.#record({ command, outcome: 'sent', at: this.#now(), ok: response.ok, ...ctx });
75
+ return response;
76
+ }
77
+ catch (error) {
78
+ const timedOut = error instanceof AtCommandTimeoutError;
79
+ if (timedOut) {
80
+ await this.#onWatchdog?.(command);
81
+ }
82
+ this.#record({
83
+ command,
84
+ outcome: timedOut ? 'timeout' : 'error',
85
+ at: this.#now(),
86
+ reason: error instanceof Error ? error.message : String(error),
87
+ ...ctx,
88
+ });
89
+ throw error;
90
+ }
91
+ }
92
+ async #sendWithWatchdog(command) {
93
+ let timer;
94
+ const watchdog = new Promise((_, reject) => {
95
+ timer = setTimeout(() => reject(new AtCommandTimeoutError(command, this.#timeoutMs)), this.#timeoutMs);
96
+ });
97
+ try {
98
+ return await Promise.race([this.#sender.send(command), watchdog]);
99
+ }
100
+ finally {
101
+ if (timer !== undefined) {
102
+ clearTimeout(timer);
103
+ }
104
+ }
105
+ }
106
+ #record(entry) {
107
+ this.#audit?.record(redact(entry));
108
+ }
109
+ }
@@ -0,0 +1,46 @@
1
+ import type { EpochMillis } from '../domain/index.js';
2
+ import type { DecodedProps } from './managed-objects.js';
3
+ /** Where a batch of cell readings came from, and when it was observed. */
4
+ export interface CellInfoProvenance {
5
+ /** A human/source tag, e.g. the modem path or `'Modem.GetCellInfo'`. */
6
+ readonly source: string;
7
+ readonly observedAt: EpochMillis;
8
+ }
9
+ /** One normalized cell reading — only the pinned fields, plus provenance. */
10
+ export interface CellReading {
11
+ /** `true` when this dict is marked as the serving cell. */
12
+ readonly serving: boolean;
13
+ /** The cell identifier, when supplied (serving-cell tiebreak key). */
14
+ readonly cellId?: string;
15
+ /** Physical cell id — from the `physical-ci` key ONLY. */
16
+ readonly pci?: number;
17
+ readonly rsrp?: number;
18
+ readonly rsrq?: number;
19
+ /** NR SINR — from the `sinr` key ONLY (a `snr`-keyed dict is ignored). */
20
+ readonly sinr?: number;
21
+ /** Radio band — present ONLY when the source supplied it directly. */
22
+ readonly band?: string;
23
+ readonly source: string;
24
+ readonly observedAt: EpochMillis;
25
+ }
26
+ /** Normalize ONE cell's `a{sv}` dict into a `CellReading`, pinning the known keys. */
27
+ export declare function normalizeCellReading(cell: DecodedProps, provenance: CellInfoProvenance): CellReading;
28
+ /** Normalize a whole `GetCellInfo` reply (`aa{sv}` → cell dicts) into readings. */
29
+ export declare function normalizeCellInfo(cells: readonly DecodedProps[], provenance: CellInfoProvenance): readonly CellReading[];
30
+ /**
31
+ * Serving-cell TOTAL order — a strict, permutation-invariant ranking of readings.
32
+ * Returns < 0 when `a` outranks `b` (should sort first / is the better serving cell):
33
+ *
34
+ * 1. a `serving`-marked cell outranks a non-serving one;
35
+ * 2. then the HIGHER `rsrp` outranks the lower — a cell with NO `rsrp` sorts LAST;
36
+ * 3. ties break lexicographically by `cell-id` (a cell with none sorts last);
37
+ * 4. final deterministic tiebreaks (`pci`, then a stable serialization) guarantee
38
+ * the order is total even for otherwise-identical distinct cells, so the winner
39
+ * never depends on input order.
40
+ */
41
+ export declare function compareServing(a: CellReading, b: CellReading): number;
42
+ /**
43
+ * Select the serving cell under the total order above. Permutation-invariant: the
44
+ * same set of readings always yields the same winner regardless of their order.
45
+ */
46
+ export declare function selectServingCell(cells: readonly CellReading[]): CellReading | undefined;
@@ -0,0 +1,124 @@
1
+ // Cell-info normalization — a decoded ModemManager cell dict → a stable reading.
2
+ //
3
+ // `Modem.GetCellInfo` returns `aa{sv}`: one `a{sv}` dict per visible cell. The keys
4
+ // vary by RAT and MM version, so this module pins EXACTLY the mappings the rest of
5
+ // the stack depends on and ignores everything else:
6
+ //
7
+ // - `physical-ci` → `pci` (the real MM key; never guessed from anything else)
8
+ // - `rsrp`, `rsrq` pass through as numbers
9
+ // - `sinr` the REAL NR SINR key. A dict carrying `snr` (the
10
+ // WRONG name) is IGNORED — `sinr` stays undefined.
11
+ // - `cell-id` the cell identifier (serving-cell tiebreak)
12
+ // - `band` surfaced ONLY when the source supplies it directly;
13
+ // never inferred from earfcn / frequency / anything.
14
+ // - `serving` / `cell-type` whether this is the serving cell.
15
+ //
16
+ // Every reading also carries `source` + `observedAt` provenance, so a consumer can
17
+ // tell a fresh reading from a cached one and know where it came from. Pure — no I/O.
18
+ import { numberProp, stringProp } from './managed-objects.js';
19
+ /** Read a `serving` flag: an explicit `serving` bool, else a `cell-type` naming it. */
20
+ function readServing(cell) {
21
+ const flag = cell.find(([key]) => key === 'serving')?.[1]?.value;
22
+ if (typeof flag === 'boolean') {
23
+ return flag;
24
+ }
25
+ const cellType = stringProp(cell, 'cell-type');
26
+ return cellType?.toLowerCase().includes('serving') ?? false;
27
+ }
28
+ /** Normalize ONE cell's `a{sv}` dict into a `CellReading`, pinning the known keys. */
29
+ export function normalizeCellReading(cell, provenance) {
30
+ const cellId = stringProp(cell, 'cell-id');
31
+ const pci = numberProp(cell, 'physical-ci');
32
+ const rsrp = numberProp(cell, 'rsrp');
33
+ const rsrq = numberProp(cell, 'rsrq');
34
+ // `sinr` ONLY — a dict carrying `snr` must never populate this field.
35
+ const sinr = numberProp(cell, 'sinr');
36
+ // `band` ONLY when directly supplied; never inferred.
37
+ const band = stringProp(cell, 'band');
38
+ return {
39
+ serving: readServing(cell),
40
+ ...(cellId !== undefined ? { cellId } : {}),
41
+ ...(pci !== undefined ? { pci } : {}),
42
+ ...(rsrp !== undefined ? { rsrp } : {}),
43
+ ...(rsrq !== undefined ? { rsrq } : {}),
44
+ ...(sinr !== undefined ? { sinr } : {}),
45
+ ...(band !== undefined ? { band } : {}),
46
+ source: provenance.source,
47
+ observedAt: provenance.observedAt,
48
+ };
49
+ }
50
+ /** Normalize a whole `GetCellInfo` reply (`aa{sv}` → cell dicts) into readings. */
51
+ export function normalizeCellInfo(cells, provenance) {
52
+ return cells.map((cell) => normalizeCellReading(cell, provenance));
53
+ }
54
+ /**
55
+ * Serving-cell TOTAL order — a strict, permutation-invariant ranking of readings.
56
+ * Returns < 0 when `a` outranks `b` (should sort first / is the better serving cell):
57
+ *
58
+ * 1. a `serving`-marked cell outranks a non-serving one;
59
+ * 2. then the HIGHER `rsrp` outranks the lower — a cell with NO `rsrp` sorts LAST;
60
+ * 3. ties break lexicographically by `cell-id` (a cell with none sorts last);
61
+ * 4. final deterministic tiebreaks (`pci`, then a stable serialization) guarantee
62
+ * the order is total even for otherwise-identical distinct cells, so the winner
63
+ * never depends on input order.
64
+ */
65
+ export function compareServing(a, b) {
66
+ if (a.serving !== b.serving) {
67
+ return a.serving ? -1 : 1;
68
+ }
69
+ const rsrpRank = compareOptionalDesc(a.rsrp, b.rsrp);
70
+ if (rsrpRank !== 0) {
71
+ return rsrpRank;
72
+ }
73
+ const cellRank = compareOptionalAsc(a.cellId, b.cellId);
74
+ if (cellRank !== 0) {
75
+ return cellRank;
76
+ }
77
+ const pciRank = compareOptionalAsc(a.pci, b.pci);
78
+ if (pciRank !== 0) {
79
+ return pciRank;
80
+ }
81
+ return stableTag(a) < stableTag(b) ? -1 : stableTag(a) > stableTag(b) ? 1 : 0;
82
+ }
83
+ /** Higher value first; `undefined` last. */
84
+ function compareOptionalDesc(a, b) {
85
+ if (a === b)
86
+ return 0;
87
+ if (a === undefined)
88
+ return 1;
89
+ if (b === undefined)
90
+ return -1;
91
+ return b - a;
92
+ }
93
+ /** Lower value first; `undefined` last. Works for numbers and strings. */
94
+ function compareOptionalAsc(a, b) {
95
+ if (a === b)
96
+ return 0;
97
+ if (a === undefined)
98
+ return 1;
99
+ if (b === undefined)
100
+ return -1;
101
+ return a < b ? -1 : 1;
102
+ }
103
+ /** A stable, order-independent fingerprint used only as the final total-order tiebreak. */
104
+ function stableTag(reading) {
105
+ return JSON.stringify([
106
+ reading.serving,
107
+ reading.cellId ?? null,
108
+ reading.pci ?? null,
109
+ reading.rsrp ?? null,
110
+ reading.rsrq ?? null,
111
+ reading.sinr ?? null,
112
+ reading.band ?? null,
113
+ ]);
114
+ }
115
+ /**
116
+ * Select the serving cell under the total order above. Permutation-invariant: the
117
+ * same set of readings always yields the same winner regardless of their order.
118
+ */
119
+ export function selectServingCell(cells) {
120
+ if (cells.length === 0) {
121
+ return undefined;
122
+ }
123
+ return cells.reduce((best, cell) => (compareServing(cell, best) < 0 ? cell : best));
124
+ }