@ceralive/modem-control 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) hide show
  1. package/README.md +341 -0
  2. package/dist/backend/at-lease.d.ts +59 -0
  3. package/dist/backend/at-lease.js +118 -0
  4. package/dist/backend/cell-info.d.ts +46 -0
  5. package/dist/backend/cell-info.js +124 -0
  6. package/dist/backend/constants.d.ts +24 -0
  7. package/{src/backend/constants.ts → dist/backend/constants.js} +4 -9
  8. package/dist/backend/device-classifier.d.ts +46 -0
  9. package/dist/backend/device-classifier.js +258 -0
  10. package/dist/backend/enrichment.d.ts +28 -0
  11. package/dist/backend/enrichment.js +59 -0
  12. package/dist/backend/features.d.ts +61 -0
  13. package/dist/backend/features.js +109 -0
  14. package/dist/backend/identity-ladder.d.ts +57 -0
  15. package/dist/backend/identity-ladder.js +158 -0
  16. package/dist/backend/identity-registry.d.ts +51 -0
  17. package/dist/backend/identity-registry.js +101 -0
  18. package/dist/backend/index.d.ts +30 -0
  19. package/dist/backend/index.js +35 -0
  20. package/dist/backend/lifecycle-interlock.d.ts +25 -0
  21. package/dist/backend/lifecycle-interlock.js +18 -0
  22. package/dist/backend/managed-objects.d.ts +39 -0
  23. package/dist/backend/managed-objects.js +82 -0
  24. package/dist/backend/mapping.d.ts +11 -0
  25. package/dist/backend/mapping.js +148 -0
  26. package/dist/backend/mm-backend.d.ts +44 -0
  27. package/dist/backend/mm-backend.js +154 -0
  28. package/dist/backend/mm-location.d.ts +21 -0
  29. package/dist/backend/mm-location.js +237 -0
  30. package/dist/backend/mm-mutations.d.ts +42 -0
  31. package/dist/backend/mm-mutations.js +259 -0
  32. package/dist/backend/modem-actor.d.ts +41 -0
  33. package/dist/backend/modem-actor.js +78 -0
  34. package/dist/backend/nm-auto-apn.d.ts +54 -0
  35. package/dist/backend/nm-auto-apn.js +124 -0
  36. package/dist/backend/nm-gsm-fields.d.ts +23 -0
  37. package/dist/backend/nm-gsm-fields.js +107 -0
  38. package/dist/backend/nmcli-nm-port.d.ts +28 -0
  39. package/dist/backend/nmcli-nm-port.js +173 -0
  40. package/dist/backend/nmcli-runner.d.ts +24 -0
  41. package/dist/backend/nmcli-runner.js +35 -0
  42. package/dist/backend/observer.d.ts +37 -0
  43. package/dist/backend/observer.js +219 -0
  44. package/dist/backend/power-contract.d.ts +49 -0
  45. package/dist/backend/power-contract.js +34 -0
  46. package/dist/backend/recovery-attribution.d.ts +32 -0
  47. package/dist/backend/recovery-attribution.js +57 -0
  48. package/dist/backend/recovery-budget.d.ts +45 -0
  49. package/dist/backend/recovery-budget.js +44 -0
  50. package/dist/backend/recovery-ladder.d.ts +94 -0
  51. package/dist/backend/recovery-ladder.js +116 -0
  52. package/dist/backend/router-ethernet.d.ts +19 -0
  53. package/dist/backend/router-ethernet.js +66 -0
  54. package/dist/backend/row-store.d.ts +13 -0
  55. package/dist/backend/row-store.js +82 -0
  56. package/dist/backend/signal-setup.d.ts +30 -0
  57. package/dist/backend/signal-setup.js +92 -0
  58. package/dist/backend/sim-unlock.d.ts +13 -0
  59. package/dist/backend/sim-unlock.js +153 -0
  60. package/dist/backend/transition-preconditions.d.ts +101 -0
  61. package/dist/backend/transition-preconditions.js +132 -0
  62. package/dist/backend/usage/accounting.d.ts +39 -0
  63. package/dist/backend/usage/accounting.js +73 -0
  64. package/dist/backend/usage/billing-cycle.d.ts +11 -0
  65. package/dist/backend/usage/billing-cycle.js +39 -0
  66. package/dist/backend/usage/boot-id.d.ts +6 -0
  67. package/{src/backend/usage/boot-id.ts → dist/backend/usage/boot-id.js} +7 -7
  68. package/dist/backend/usage/index.d.ts +8 -0
  69. package/dist/backend/usage/index.js +11 -0
  70. package/dist/backend/usage/policy-store.d.ts +50 -0
  71. package/dist/backend/usage/policy-store.js +161 -0
  72. package/dist/backend/usage/policy-write.d.ts +65 -0
  73. package/dist/backend/usage/policy-write.js +112 -0
  74. package/dist/backend/usage/proc-net-dev.d.ts +18 -0
  75. package/{src/backend/usage/proc-net-dev.ts → dist/backend/usage/proc-net-dev.js} +39 -47
  76. package/dist/backend/usage/sampler.d.ts +75 -0
  77. package/dist/backend/usage/sampler.js +211 -0
  78. package/dist/backend/usage/store.d.ts +38 -0
  79. package/dist/backend/usage/store.js +126 -0
  80. package/dist/backend/usb-device-snapshot.d.ts +31 -0
  81. package/dist/backend/usb-device-snapshot.js +1 -0
  82. package/dist/backend/usb-enumerator.d.ts +21 -0
  83. package/dist/backend/usb-enumerator.js +153 -0
  84. package/dist/backend/usb-mode-transition.d.ts +29 -0
  85. package/dist/backend/usb-mode-transition.js +216 -0
  86. package/dist/band/band-names.d.ts +42 -0
  87. package/dist/band/band-names.js +150 -0
  88. package/dist/band/certification.d.ts +84 -0
  89. package/dist/band/certification.js +127 -0
  90. package/dist/band/certified-bands.json +4 -0
  91. package/dist/band/index.d.ts +2 -0
  92. package/dist/band/index.js +8 -0
  93. package/dist/capability/detect.d.ts +52 -0
  94. package/dist/capability/detect.js +86 -0
  95. package/dist/capability/five-g-preference.d.ts +104 -0
  96. package/dist/capability/five-g-preference.js +171 -0
  97. package/dist/capability/index.d.ts +3 -0
  98. package/dist/capability/index.js +10 -0
  99. package/dist/capability/support-claim.d.ts +39 -0
  100. package/dist/capability/support-claim.js +81 -0
  101. package/dist/domain/brand.d.ts +10 -0
  102. package/dist/domain/brand.js +21 -0
  103. package/dist/domain/errors.d.ts +32 -0
  104. package/dist/domain/errors.js +48 -0
  105. package/dist/domain/generation.d.ts +8 -0
  106. package/dist/domain/generation.js +12 -0
  107. package/dist/domain/guards.d.ts +6 -0
  108. package/dist/domain/guards.js +127 -0
  109. package/dist/domain/identity.d.ts +109 -0
  110. package/dist/domain/identity.js +86 -0
  111. package/dist/domain/index.d.ts +14 -0
  112. package/dist/domain/index.js +18 -0
  113. package/dist/domain/mm-enums.d.ts +12 -0
  114. package/dist/domain/mm-enums.js +139 -0
  115. package/dist/domain/modem-presentation.d.ts +10 -0
  116. package/dist/domain/modem-presentation.js +39 -0
  117. package/dist/domain/observation.d.ts +39 -0
  118. package/dist/domain/observation.js +4 -0
  119. package/dist/domain/operation.d.ts +118 -0
  120. package/dist/domain/operation.js +85 -0
  121. package/dist/domain/physical-identity.d.ts +43 -0
  122. package/dist/domain/physical-identity.js +113 -0
  123. package/{src/domain/policy.ts → dist/domain/policy.d.ts} +34 -69
  124. package/dist/domain/policy.js +38 -0
  125. package/dist/domain/shadow-divergence.d.ts +27 -0
  126. package/dist/domain/shadow-divergence.js +70 -0
  127. package/dist/domain/snapshot.d.ts +53 -0
  128. package/dist/domain/snapshot.js +75 -0
  129. package/{src/domain/state.ts → dist/domain/state.d.ts} +23 -122
  130. package/dist/domain/state.js +38 -0
  131. package/dist/fcc/coverage.d.ts +63 -0
  132. package/dist/fcc/coverage.js +102 -0
  133. package/dist/fcc/index.d.ts +3 -0
  134. package/dist/fcc/index.js +12 -0
  135. package/dist/fcc/policy-store.d.ts +40 -0
  136. package/dist/fcc/policy-store.js +146 -0
  137. package/dist/fcc/policy-write.d.ts +32 -0
  138. package/dist/fcc/policy-write.js +37 -0
  139. package/dist/hardware/hilink-protocol.d.ts +39 -0
  140. package/dist/hardware/hilink-protocol.js +58 -0
  141. package/dist/hardware/index.d.ts +3 -0
  142. package/dist/hardware/index.js +15 -0
  143. package/dist/hardware/router-parsers.d.ts +89 -0
  144. package/dist/hardware/router-parsers.js +234 -0
  145. package/dist/index.d.ts +20 -0
  146. package/dist/index.js +27 -0
  147. package/dist/journal/codec.d.ts +44 -0
  148. package/dist/journal/codec.js +198 -0
  149. package/dist/journal/engine.d.ts +28 -0
  150. package/dist/journal/engine.js +68 -0
  151. package/dist/journal/entry.d.ts +74 -0
  152. package/dist/journal/entry.js +56 -0
  153. package/dist/journal/index.d.ts +6 -0
  154. package/dist/journal/index.js +6 -0
  155. package/dist/journal/legacy-ceraui.d.ts +73 -0
  156. package/dist/journal/legacy-ceraui.js +227 -0
  157. package/dist/journal/recovery.d.ts +58 -0
  158. package/dist/journal/recovery.js +117 -0
  159. package/dist/journal/store.d.ts +55 -0
  160. package/dist/journal/store.js +150 -0
  161. package/dist/location/fix-state.d.ts +53 -0
  162. package/dist/location/fix-state.js +75 -0
  163. package/dist/location/index.d.ts +2 -0
  164. package/dist/location/index.js +8 -0
  165. package/dist/location/nmea.d.ts +10 -0
  166. package/dist/location/nmea.js +89 -0
  167. package/dist/observations/envelope.d.ts +63 -0
  168. package/dist/observations/envelope.js +77 -0
  169. package/dist/observations/freshness.d.ts +28 -0
  170. package/dist/observations/freshness.js +76 -0
  171. package/dist/observations/index.d.ts +13 -0
  172. package/dist/observations/index.js +24 -0
  173. package/dist/observations/metric.d.ts +61 -0
  174. package/dist/observations/metric.js +73 -0
  175. package/dist/observations/model.d.ts +80 -0
  176. package/dist/observations/model.js +11 -0
  177. package/dist/observations/provenance.d.ts +94 -0
  178. package/dist/observations/provenance.js +67 -0
  179. package/dist/observations/raw.d.ts +42 -0
  180. package/dist/observations/raw.js +146 -0
  181. package/dist/observations/reading.d.ts +51 -0
  182. package/dist/observations/reading.js +65 -0
  183. package/dist/observations/sources/hilink.d.ts +10 -0
  184. package/dist/observations/sources/hilink.js +84 -0
  185. package/dist/observations/sources/modemmanager.d.ts +18 -0
  186. package/dist/observations/sources/modemmanager.js +239 -0
  187. package/dist/observations/sources/router-shared.d.ts +29 -0
  188. package/dist/observations/sources/router-shared.js +68 -0
  189. package/dist/observations/sources/ufi.d.ts +10 -0
  190. package/dist/observations/sources/ufi.js +111 -0
  191. package/dist/observations/sources/zte.d.ts +7 -0
  192. package/dist/observations/sources/zte.js +71 -0
  193. package/dist/observations/state-separation.d.ts +64 -0
  194. package/dist/observations/state-separation.js +52 -0
  195. package/dist/operation-ids.d.ts +2 -0
  196. package/dist/operation-ids.js +26 -0
  197. package/dist/operations/contracts.d.ts +72 -0
  198. package/dist/operations/contracts.js +1 -0
  199. package/dist/operations/index.d.ts +1 -0
  200. package/dist/operations/index.js +1 -0
  201. package/dist/operations/operation-engine.d.ts +16 -0
  202. package/dist/operations/operation-engine.js +194 -0
  203. package/dist/ports/index.d.ts +12 -0
  204. package/{src/ports/index.ts → dist/ports/index.js} +12 -8
  205. package/dist/ports/location.d.ts +87 -0
  206. package/dist/ports/location.js +36 -0
  207. package/dist/ports/modem-manager.d.ts +89 -0
  208. package/dist/ports/modem-manager.js +9 -0
  209. package/dist/ports/mutation-admission.d.ts +27 -0
  210. package/dist/ports/mutation-admission.js +9 -0
  211. package/dist/ports/network-manager.d.ts +68 -0
  212. package/dist/ports/network-manager.js +13 -0
  213. package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
  214. package/dist/ports/observation.js +7 -0
  215. package/dist/ports/ops.d.ts +44 -0
  216. package/dist/ports/ops.js +16 -0
  217. package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
  218. package/dist/ports/receipts.js +9 -0
  219. package/dist/ports/reconcile.d.ts +33 -0
  220. package/dist/ports/reconcile.js +200 -0
  221. package/dist/ports/resource-ownership.d.ts +29 -0
  222. package/dist/ports/resource-ownership.js +1 -0
  223. package/dist/ports/router.d.ts +19 -0
  224. package/dist/ports/router.js +7 -0
  225. package/dist/ports/sms.d.ts +64 -0
  226. package/dist/ports/sms.js +24 -0
  227. package/dist/ports/uhubctl.d.ts +6 -0
  228. package/dist/ports/uhubctl.js +1 -0
  229. package/dist/providers/contracts.d.ts +124 -0
  230. package/dist/providers/contracts.js +10 -0
  231. package/dist/providers/huawei-hilink/index.d.ts +2 -0
  232. package/dist/providers/huawei-hilink/index.js +2 -0
  233. package/dist/providers/huawei-hilink/operations.d.ts +20 -0
  234. package/dist/providers/huawei-hilink/operations.js +56 -0
  235. package/dist/providers/huawei-hilink/provider.d.ts +52 -0
  236. package/dist/providers/huawei-hilink/provider.js +76 -0
  237. package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
  238. package/dist/providers/huawei-hilink/runtime.js +171 -0
  239. package/dist/providers/huawei-hilink/session.d.ts +28 -0
  240. package/dist/providers/huawei-hilink/session.js +120 -0
  241. package/dist/providers/huawei-hilink/transport.d.ts +19 -0
  242. package/dist/providers/huawei-hilink/transport.js +1 -0
  243. package/dist/providers/index.d.ts +8 -0
  244. package/dist/providers/index.js +8 -0
  245. package/dist/providers/matcher.d.ts +3 -0
  246. package/dist/providers/matcher.js +205 -0
  247. package/dist/providers/modem-manager/errors.d.ts +7 -0
  248. package/dist/providers/modem-manager/errors.js +37 -0
  249. package/dist/providers/modem-manager/generic-operations.d.ts +10 -0
  250. package/dist/providers/modem-manager/generic-operations.js +209 -0
  251. package/dist/providers/modem-manager/index.d.ts +4 -0
  252. package/dist/providers/modem-manager/index.js +4 -0
  253. package/dist/providers/modem-manager/module-operations.d.ts +21 -0
  254. package/dist/providers/modem-manager/module-operations.js +118 -0
  255. package/dist/providers/modem-manager/provider.d.ts +41 -0
  256. package/dist/providers/modem-manager/provider.js +155 -0
  257. package/dist/providers/modem-manager/runtime-composition-operation.d.ts +32 -0
  258. package/dist/providers/modem-manager/runtime-composition-operation.js +151 -0
  259. package/dist/providers/modem-manager/snapshot.d.ts +5 -0
  260. package/dist/providers/modem-manager/snapshot.js +204 -0
  261. package/dist/providers/modem-manager/types.d.ts +137 -0
  262. package/dist/providers/modem-manager/types.js +1 -0
  263. package/dist/providers/network-manager/adapter.d.ts +71 -0
  264. package/dist/providers/network-manager/adapter.js +348 -0
  265. package/dist/providers/network-manager/index.d.ts +2 -0
  266. package/dist/providers/network-manager/index.js +2 -0
  267. package/dist/providers/network-manager/types.d.ts +171 -0
  268. package/dist/providers/network-manager/types.js +77 -0
  269. package/dist/providers/registry.d.ts +13 -0
  270. package/dist/providers/registry.js +33 -0
  271. package/dist/providers/ufi-himi/index.d.ts +6 -0
  272. package/dist/providers/ufi-himi/index.js +6 -0
  273. package/dist/providers/ufi-himi/operations.d.ts +41 -0
  274. package/dist/providers/ufi-himi/operations.js +66 -0
  275. package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
  276. package/dist/providers/ufi-himi/prohibitions.js +88 -0
  277. package/dist/providers/ufi-himi/provider.d.ts +41 -0
  278. package/dist/providers/ufi-himi/provider.js +204 -0
  279. package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
  280. package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
  281. package/dist/providers/ufi-himi/session.d.ts +46 -0
  282. package/dist/providers/ufi-himi/session.js +92 -0
  283. package/dist/providers/ufi-himi/transport.d.ts +29 -0
  284. package/dist/providers/ufi-himi/transport.js +25 -0
  285. package/dist/providers/zte-goform/index.d.ts +2 -0
  286. package/dist/providers/zte-goform/index.js +2 -0
  287. package/dist/providers/zte-goform/provider.d.ts +56 -0
  288. package/dist/providers/zte-goform/provider.js +101 -0
  289. package/dist/providers/zte-goform/session.d.ts +23 -0
  290. package/dist/providers/zte-goform/session.js +197 -0
  291. package/dist/providers/zte-goform/transport.d.ts +16 -0
  292. package/dist/providers/zte-goform/transport.js +1 -0
  293. package/dist/radio/band-truth.d.ts +50 -0
  294. package/dist/radio/band-truth.js +92 -0
  295. package/dist/radio/index.d.ts +3 -0
  296. package/dist/radio/index.js +10 -0
  297. package/dist/radio/mode-combinations.d.ts +88 -0
  298. package/dist/radio/mode-combinations.js +198 -0
  299. package/dist/radio/mode-truth.d.ts +67 -0
  300. package/dist/radio/mode-truth.js +112 -0
  301. package/dist/redact.d.ts +15 -0
  302. package/dist/redact.js +189 -0
  303. package/dist/safety/composition-root.d.ts +28 -0
  304. package/dist/safety/composition-root.js +65 -0
  305. package/dist/safety/flock-resource-ownership.d.ts +11 -0
  306. package/dist/safety/flock-resource-ownership.js +138 -0
  307. package/dist/safety/index.d.ts +2 -0
  308. package/dist/safety/index.js +2 -0
  309. package/dist/sms/dbus-messaging.d.ts +17 -0
  310. package/dist/sms/dbus-messaging.js +185 -0
  311. package/dist/sms/inbox-store.d.ts +10 -0
  312. package/dist/sms/inbox-store.js +82 -0
  313. package/dist/sms/index.d.ts +4 -0
  314. package/dist/sms/index.js +10 -0
  315. package/dist/sms/mmcli-parse.d.ts +54 -0
  316. package/dist/sms/mmcli-parse.js +224 -0
  317. package/dist/sms/normalize.d.ts +42 -0
  318. package/dist/sms/normalize.js +95 -0
  319. package/dist/testing/domain-fakes.d.ts +58 -0
  320. package/dist/testing/domain-fakes.js +98 -0
  321. package/dist/testing/index.d.ts +2 -0
  322. package/dist/testing/index.js +17 -0
  323. package/dist/testing/provider-fakes.d.ts +45 -0
  324. package/dist/testing/provider-fakes.js +64 -0
  325. package/dist/transport/calls.d.ts +9 -0
  326. package/dist/transport/calls.js +88 -0
  327. package/dist/transport/codec.d.ts +3 -0
  328. package/dist/transport/codec.js +207 -0
  329. package/dist/transport/dbus-native.d.ts +57 -0
  330. package/dist/transport/dbus-native.js +17 -0
  331. package/dist/transport/errors.d.ts +21 -0
  332. package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
  333. package/dist/transport/index.d.ts +4 -0
  334. package/dist/transport/index.js +9 -0
  335. package/dist/transport/signals.d.ts +15 -0
  336. package/dist/transport/signals.js +123 -0
  337. package/dist/transport/signature.d.ts +7 -0
  338. package/dist/transport/signature.js +94 -0
  339. package/dist/transport/transport.d.ts +2 -0
  340. package/dist/transport/transport.js +202 -0
  341. package/dist/transport/types.d.ts +61 -0
  342. package/dist/transport/types.js +19 -0
  343. package/dist/usb-mode/catalog-schema.d.ts +139 -0
  344. package/dist/usb-mode/catalog-schema.js +97 -0
  345. package/dist/usb-mode/catalog.d.ts +21 -0
  346. package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
  347. package/dist/usb-mode/certified-catalog.json +67 -0
  348. package/dist/usb-mode/index.d.ts +6 -0
  349. package/dist/usb-mode/index.js +16 -0
  350. package/dist/usb-mode/ingestion.d.ts +111 -0
  351. package/dist/usb-mode/ingestion.js +187 -0
  352. package/dist/usb-mode/promotion-review.d.ts +21 -0
  353. package/dist/usb-mode/promotion-review.js +87 -0
  354. package/dist/usb-mode/runtime-capability.d.ts +59 -0
  355. package/dist/usb-mode/runtime-capability.js +157 -0
  356. package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
  357. package/dist/usb-mode/usb-devices-parse.js +157 -0
  358. package/dist/ussd/calls.d.ts +32 -0
  359. package/dist/ussd/calls.js +96 -0
  360. package/dist/ussd/index.d.ts +5 -0
  361. package/dist/ussd/index.js +12 -0
  362. package/dist/ussd/mm-ussd.d.ts +37 -0
  363. package/dist/ussd/mm-ussd.js +205 -0
  364. package/dist/ussd/refusal.d.ts +53 -0
  365. package/dist/ussd/refusal.js +154 -0
  366. package/dist/ussd/registration.d.ts +20 -0
  367. package/dist/ussd/registration.js +101 -0
  368. package/dist/ussd/session.d.ts +106 -0
  369. package/dist/ussd/session.js +163 -0
  370. package/package.json +38 -4
  371. package/src/backend/at-lease.test.ts +0 -106
  372. package/src/backend/at-lease.ts +0 -158
  373. package/src/backend/cell-info.test.ts +0 -154
  374. package/src/backend/cell-info.ts +0 -160
  375. package/src/backend/device-classifier.test.ts +0 -168
  376. package/src/backend/device-classifier.ts +0 -248
  377. package/src/backend/enrichment.ts +0 -96
  378. package/src/backend/features.test.ts +0 -162
  379. package/src/backend/features.ts +0 -179
  380. package/src/backend/identity-ladder.test.ts +0 -117
  381. package/src/backend/identity-ladder.ts +0 -221
  382. package/src/backend/identity-registry.test.ts +0 -89
  383. package/src/backend/identity-registry.ts +0 -151
  384. package/src/backend/index.ts +0 -236
  385. package/src/backend/lifecycle-interlock.ts +0 -38
  386. package/src/backend/managed-objects.ts +0 -108
  387. package/src/backend/mapping.ts +0 -160
  388. package/src/backend/mm-backend.ts +0 -191
  389. package/src/backend/mm-mutations.ts +0 -228
  390. package/src/backend/modem-actor.test.ts +0 -95
  391. package/src/backend/modem-actor.ts +0 -112
  392. package/src/backend/nm-auto-apn.ts +0 -161
  393. package/src/backend/nm-gsm-fields.ts +0 -122
  394. package/src/backend/nmcli-nm-port.ts +0 -228
  395. package/src/backend/nmcli-runner.ts +0 -52
  396. package/src/backend/observer.ts +0 -297
  397. package/src/backend/power-contract.test.ts +0 -40
  398. package/src/backend/power-contract.ts +0 -83
  399. package/src/backend/recovery-attribution.test.ts +0 -102
  400. package/src/backend/recovery-attribution.ts +0 -86
  401. package/src/backend/recovery-budget.test.ts +0 -64
  402. package/src/backend/recovery-budget.ts +0 -84
  403. package/src/backend/recovery-ladder.test.ts +0 -257
  404. package/src/backend/recovery-ladder.ts +0 -249
  405. package/src/backend/router-ethernet.test.ts +0 -71
  406. package/src/backend/router-ethernet.ts +0 -90
  407. package/src/backend/row-store.ts +0 -105
  408. package/src/backend/signal-setup.ts +0 -112
  409. package/src/backend/sim-unlock.ts +0 -193
  410. package/src/backend/transition-preconditions.ts +0 -149
  411. package/src/backend/uhubctl-power-hook.test.ts +0 -274
  412. package/src/backend/uhubctl-power-hook.ts +0 -377
  413. package/src/backend/usage/accounting.test.ts +0 -147
  414. package/src/backend/usage/accounting.ts +0 -123
  415. package/src/backend/usage/billing-cycle.test.ts +0 -62
  416. package/src/backend/usage/billing-cycle.ts +0 -45
  417. package/src/backend/usage/index.ts +0 -60
  418. package/src/backend/usage/policy-store.test.ts +0 -164
  419. package/src/backend/usage/policy-store.ts +0 -216
  420. package/src/backend/usage/policy-write.test.ts +0 -198
  421. package/src/backend/usage/policy-write.ts +0 -207
  422. package/src/backend/usage/proc-net-dev.test.ts +0 -56
  423. package/src/backend/usage/sampler.test.ts +0 -327
  424. package/src/backend/usage/sampler.ts +0 -282
  425. package/src/backend/usage/store.test.ts +0 -148
  426. package/src/backend/usage/store.ts +0 -177
  427. package/src/backend/usb-enumerator.test.ts +0 -87
  428. package/src/backend/usb-enumerator.ts +0 -181
  429. package/src/backend/usb-mode-transition.test.ts +0 -323
  430. package/src/backend/usb-mode-transition.ts +0 -253
  431. package/src/domain/brand.ts +0 -29
  432. package/src/domain/errors.ts +0 -77
  433. package/src/domain/guards.test.ts +0 -218
  434. package/src/domain/guards.ts +0 -144
  435. package/src/domain/identity.test.ts +0 -83
  436. package/src/domain/identity.ts +0 -165
  437. package/src/domain/index.ts +0 -12
  438. package/src/domain/snapshot.test.ts +0 -266
  439. package/src/domain/snapshot.ts +0 -120
  440. package/src/index.test.ts +0 -6
  441. package/src/index.ts +0 -15
  442. package/src/ports/README.md +0 -61
  443. package/src/ports/forbidden-surface.test.ts +0 -241
  444. package/src/ports/modem-manager.ts +0 -72
  445. package/src/ports/network-manager.ts +0 -87
  446. package/src/ports/ops.ts +0 -60
  447. package/src/ports/ops.type-test.ts +0 -39
  448. package/src/ports/receipts.test.ts +0 -153
  449. package/src/ports/reconcile.test.ts +0 -152
  450. package/src/ports/reconcile.ts +0 -338
  451. package/src/ports/router.ts +0 -29
  452. package/src/redact.test.ts +0 -82
  453. package/src/redact.ts +0 -73
  454. package/src/transport/README.md +0 -65
  455. package/src/transport/calls.ts +0 -113
  456. package/src/transport/characterization.test.ts +0 -260
  457. package/src/transport/codec.test.ts +0 -118
  458. package/src/transport/codec.ts +0 -240
  459. package/src/transport/conformance-python.test.ts +0 -152
  460. package/src/transport/conformance-same-lib.test.ts +0 -115
  461. package/src/transport/dbus-native-lib.d.ts +0 -19
  462. package/src/transport/dbus-native.ts +0 -85
  463. package/src/transport/index.ts +0 -30
  464. package/src/transport/no-library-leak.test.ts +0 -61
  465. package/src/transport/reliability.test.ts +0 -173
  466. package/src/transport/signals.ts +0 -150
  467. package/src/transport/signature.ts +0 -110
  468. package/src/transport/test-support/fake-service.ts +0 -168
  469. package/src/transport/test-support/independent-producer.py +0 -110
  470. package/src/transport/test-support/private-bus.ts +0 -66
  471. package/src/transport/transport.ts +0 -250
  472. package/src/transport/types.ts +0 -118
  473. package/src/usb-mode/catalog-schema.test.ts +0 -181
  474. package/src/usb-mode/catalog-schema.ts +0 -113
  475. package/src/usb-mode/certified-catalog.json +0 -67
  476. package/src/usb-mode/index.ts +0 -58
  477. package/src/usb-mode/ingestion.test.ts +0 -268
  478. package/src/usb-mode/ingestion.ts +0 -297
  479. package/src/usb-mode/promotion-review.ts +0 -117
  480. package/src/usb-mode/usb-devices-parse.ts +0 -196
@@ -0,0 +1,163 @@
1
+ // The USSD session state machine — pure, total, and the only place a session's
2
+ // legality is decided.
3
+ //
4
+ // USSD is a SESSION protocol, not a request/response one: `Initiate` opens a
5
+ // dialogue the network may keep open pending a `Respond`, and a session that is
6
+ // neither responded to nor cancelled stays open on the NETWORK side, consuming a
7
+ // scarce per-subscriber slot and blocking the next `Initiate` with a busy error.
8
+ // So "which verb is legal right now" is a real question with a real wrong answer,
9
+ // and answering it inside the D-Bus adapter would make it untestable without a
10
+ // bus. It lives here instead, as data.
11
+ //
12
+ // Everything the machine can be told is an EVENT and every answer is a
13
+ // TRANSITION — an illegal verb is REFUSED with a typed reason, never thrown and
14
+ // never silently ignored. A refusal at an RPC boundary must name what the caller
15
+ // can do about it; a throw becomes an opaque failure and a silent no-op becomes a
16
+ // UI that spins forever.
17
+ //
18
+ // The machine carries NO carrier text. The reply an operator sees is threaded by
19
+ // the adapter and redacted at every log boundary (`../redact`); keeping it out of
20
+ // the state entirely means a state snapshot can never leak one.
21
+ /**
22
+ * Session states.
23
+ *
24
+ * Three of them (`idle`, `active`, `awaiting-reply`) mirror MM's own
25
+ * `MMModem3gppUssdSessionState`; the rest are LOCAL in-flight states, because MM
26
+ * has no state for "we dispatched a call and the reply has not landed". Without
27
+ * them a second `initiate` racing the first would be judged against `idle` and
28
+ * allowed through, which is exactly the double-open the network answers busy.
29
+ */
30
+ export const USSD_SESSION_STATES = [
31
+ /** No session. MM `IDLE`. */
32
+ 'idle',
33
+ /** `Initiate` dispatched, reply outstanding. Local. */
34
+ 'initiating',
35
+ /** Network answered and the session is open with nothing pending. MM `ACTIVE`. */
36
+ 'active',
37
+ /** Network asked a question; a `Respond` is required. MM `USER_RESPONSE`. */
38
+ 'awaiting-reply',
39
+ /** `Respond` dispatched, reply outstanding. Local. */
40
+ 'responding',
41
+ /** `Cancel` dispatched, confirmation outstanding. Local. */
42
+ 'cancelling',
43
+ /** Terminal for this session object. A new session starts from a new machine. */
44
+ 'closed',
45
+ ];
46
+ /** How a session that reached `closed` got there. */
47
+ export const USSD_SESSION_OUTCOMES = [
48
+ /** The network completed the dialogue and released the session. */
49
+ 'completed',
50
+ /** The operator cancelled it. */
51
+ 'cancelled',
52
+ /** No answer within the bound; the machine closed it locally. */
53
+ 'timed-out',
54
+ /** The network or the modem refused. `refusal` names which. */
55
+ 'failed',
56
+ ];
57
+ export const IDLE_SESSION = { state: 'idle' };
58
+ /** States in which an operator verb may be dispatched at all. */
59
+ const ACCEPTS_INITIATE = new Set(['idle']);
60
+ const ACCEPTS_RESPOND = new Set([
61
+ 'awaiting-reply',
62
+ ]);
63
+ const ACCEPTS_CANCEL = new Set([
64
+ 'initiating',
65
+ 'active',
66
+ 'awaiting-reply',
67
+ 'responding',
68
+ ]);
69
+ /** States with a call in flight — the only ones a network answer may land on. */
70
+ const IN_FLIGHT = new Set([
71
+ 'initiating',
72
+ 'responding',
73
+ ]);
74
+ function open(state) {
75
+ return { ok: true, snapshot: { state } };
76
+ }
77
+ function close(outcome, refusal) {
78
+ return {
79
+ ok: true,
80
+ snapshot: { state: 'closed', outcome, ...(refusal === undefined ? {} : { refusal }) },
81
+ };
82
+ }
83
+ function refuse(refusal) {
84
+ return { ok: false, refusal };
85
+ }
86
+ /**
87
+ * Apply one event. TOTAL: every (state, event) pair has an answer, and an answer
88
+ * is either a new snapshot or a typed refusal that leaves the machine untouched.
89
+ *
90
+ * A `closed` machine accepts NOTHING — not even another `cancel`. Re-opening a
91
+ * terminal session would hide the fact that the previous one ended, and the
92
+ * cost of a fresh machine is one object.
93
+ */
94
+ export function reduceUssdSession(snapshot, event) {
95
+ const state = snapshot.state;
96
+ if (state === 'closed') {
97
+ return refuse('no-session');
98
+ }
99
+ // Every event except `initiate` describes something happening TO a session,
100
+ // and an idle machine has none for them to happen to.
101
+ const sessionOpen = state !== 'idle';
102
+ switch (event.kind) {
103
+ case 'initiate':
104
+ // A session already in flight or open is the busy case the network
105
+ // itself would answer — refused locally so no second dialogue is opened.
106
+ return ACCEPTS_INITIATE.has(state) ? open('initiating') : refuse('session-busy');
107
+ case 'respond':
108
+ // Responding to a session that never asked a question is not a busy
109
+ // device; it is the wrong verb, and `invalid-state` says so.
110
+ return ACCEPTS_RESPOND.has(state) ? open('responding') : refuse('invalid-state');
111
+ case 'cancel':
112
+ // The two refusals are different operator facts: nothing to close, vs a
113
+ // cancel that is already in flight.
114
+ if (ACCEPTS_CANCEL.has(state)) {
115
+ return open('cancelling');
116
+ }
117
+ return refuse(sessionOpen ? 'invalid-state' : 'no-session');
118
+ case 'replied':
119
+ // A network answer that lands on a state with no call in flight is
120
+ // evidence of a lost reply or a duplicate; it is refused rather than
121
+ // used to resurrect a session the machine already moved past.
122
+ if (!IN_FLIGHT.has(state)) {
123
+ return refuse('invalid-state');
124
+ }
125
+ // The three-way answer is the dialogue-vs-one-shot distinction, and
126
+ // collapsing `active` into `released` is what would leave a session MM
127
+ // still considers open dangling on the network side.
128
+ if (event.sessionState === 'released') {
129
+ return close('completed');
130
+ }
131
+ return open(event.sessionState);
132
+ case 'cancelled':
133
+ // Only a machine that asked to cancel may be closed by one, so a stray
134
+ // confirmation cannot tear down a live dialogue.
135
+ return state === 'cancelling' ? close('cancelled') : refuse('invalid-state');
136
+ case 'network-released':
137
+ // The network is authoritative about its own session, so this lands
138
+ // from anywhere the session is still open — including mid-call, where
139
+ // it is the honest end of a dialogue whose reply will not come. During
140
+ // a cancel it is reported as `cancelled`: the operator asked for the
141
+ // session to end and it ended.
142
+ if (!sessionOpen) {
143
+ return refuse('no-session');
144
+ }
145
+ return close(state === 'cancelling' ? 'cancelled' : 'completed');
146
+ case 'timeout':
147
+ // The bound closes rather than reverting: after an unanswered call the
148
+ // network's own view is unknown, and pretending we are back at `idle`
149
+ // would let the next `initiate` walk into a busy error with no
150
+ // explanation.
151
+ return sessionOpen ? close('timed-out') : refuse('no-session');
152
+ case 'failed':
153
+ return sessionOpen ? close('failed', event.reason) : refuse('no-session');
154
+ default: {
155
+ const unreachable = event;
156
+ return unreachable;
157
+ }
158
+ }
159
+ }
160
+ /** True while the session still holds a network dialogue open. */
161
+ export function isUssdSessionOpen(snapshot) {
162
+ return snapshot.state !== 'idle' && snapshot.state !== 'closed';
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ceralive/modem-control",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Cellular modem control for CeraLive — ModemManager D-Bus backend, NetworkManager adapter, desired-state reconciler, USB composition-mode model, data-usage sampler.",
6
6
  "license": "AGPL-3.0",
@@ -13,14 +13,48 @@
13
13
  "registry": "https://registry.npmjs.org/",
14
14
  "access": "public"
15
15
  },
16
+ "sideEffects": false,
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
16
19
  "exports": {
17
- ".": "./src/index.ts",
18
- "./transport": "./src/transport/index.ts"
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ },
24
+ "./transport": {
25
+ "types": "./dist/transport/index.d.ts",
26
+ "import": "./dist/transport/index.js"
27
+ },
28
+ "./domain": {
29
+ "types": "./dist/domain/index.d.ts",
30
+ "import": "./dist/domain/index.js"
31
+ },
32
+ "./providers": {
33
+ "types": "./dist/providers/index.d.ts",
34
+ "import": "./dist/providers/index.js"
35
+ },
36
+ "./capabilities": {
37
+ "types": "./dist/capability/index.d.ts",
38
+ "import": "./dist/capability/index.js"
39
+ },
40
+ "./hardware": {
41
+ "types": "./dist/hardware/index.d.ts",
42
+ "import": "./dist/hardware/index.js"
43
+ },
44
+ "./testing": {
45
+ "types": "./dist/testing/index.d.ts",
46
+ "import": "./dist/testing/index.js"
47
+ },
48
+ "./package.json": "./package.json"
19
49
  },
20
50
  "files": [
21
- "src"
51
+ "dist"
22
52
  ],
23
53
  "scripts": {
54
+ "build": "bun run scripts/build.ts",
55
+ "prepack": "bun run build",
56
+ "verify:consumers": "bun run scripts/verify-consumers.ts",
57
+ "verify:tarball": "bun run scripts/assert-tarball-shape.ts",
24
58
  "test": "bun test",
25
59
  "typecheck": "tsc --noEmit"
26
60
  },
@@ -1,106 +0,0 @@
1
- // The AT lease's three guarantees: the allowlist rejects anything but ATI + catalog
2
- // commands (and never touches the sender when it does); the watchdog fires + rejects
3
- // a hung command; every attempt is audited through `redact`, so a secret in the
4
- // context is stripped before it is stored.
5
-
6
- import { describe, expect, test } from 'bun:test';
7
- import {
8
- AT_BASELINE_ALLOWLIST,
9
- AtCommandLease,
10
- AtCommandNotAllowedError,
11
- type AtCommandSender,
12
- AtCommandTimeoutError,
13
- type AtResponse,
14
- computeAtAllowlist,
15
- } from './at-lease';
16
-
17
- const CATALOG_COMMAND = 'AT+QCFG="usbnet",2';
18
-
19
- function recordingSender(): { sender: AtCommandSender; sent: string[] } {
20
- const sent: string[] = [];
21
- return {
22
- sent,
23
- sender: {
24
- send(command: string): Promise<AtResponse> {
25
- sent.push(command);
26
- return Promise.resolve({ ok: true, raw: 'OK' });
27
- },
28
- },
29
- };
30
- }
31
-
32
- describe('AtCommandLease — allowlist', () => {
33
- test('the baseline allowlist is exactly {ATI}; catalog commands union in', () => {
34
- expect([...AT_BASELINE_ALLOWLIST]).toEqual(['ATI']);
35
- const allowlist = computeAtAllowlist([CATALOG_COMMAND]);
36
- expect(allowlist.has('ATI')).toBe(true);
37
- expect(allowlist.has(CATALOG_COMMAND)).toBe(true);
38
- });
39
-
40
- test('ATI and the catalog command are allowed', async () => {
41
- const { sender, sent } = recordingSender();
42
- const lease = new AtCommandLease({ sender, allowlist: computeAtAllowlist([CATALOG_COMMAND]) });
43
- await lease.run('ATI');
44
- await lease.run(CATALOG_COMMAND);
45
- expect(sent).toEqual(['ATI', CATALOG_COMMAND]);
46
- });
47
-
48
- test('a command outside the allowlist is rejected WITHOUT touching the sender', async () => {
49
- const { sender, sent } = recordingSender();
50
- const lease = new AtCommandLease({ sender, allowlist: computeAtAllowlist([CATALOG_COMMAND]) });
51
- await expect(lease.run('AT+DANGEROUS')).rejects.toBeInstanceOf(AtCommandNotAllowedError);
52
- expect(sent).toEqual([]);
53
- });
54
- });
55
-
56
- describe('AtCommandLease — watchdog', () => {
57
- test('a hung command fires the watchdog and rejects with a timeout', async () => {
58
- const hangingSender: AtCommandSender = { send: () => new Promise<AtResponse>(() => undefined) };
59
- const watchdogHits: string[] = [];
60
- const lease = new AtCommandLease({
61
- sender: hangingSender,
62
- allowlist: computeAtAllowlist([]),
63
- timeoutMs: 20,
64
- onWatchdog: (command) => {
65
- watchdogHits.push(command);
66
- },
67
- });
68
- await expect(lease.run('ATI')).rejects.toBeInstanceOf(AtCommandTimeoutError);
69
- expect(watchdogHits).toEqual(['ATI']);
70
- });
71
- });
72
-
73
- describe('AtCommandLease — audit + redaction', () => {
74
- test('a sensitive value in the audit context is redacted before recording', async () => {
75
- const { sender } = recordingSender();
76
- const entries: unknown[] = [];
77
- const lease = new AtCommandLease({
78
- sender,
79
- allowlist: computeAtAllowlist([]),
80
- audit: { record: (entry) => entries.push(entry) },
81
- });
82
- await lease.run('ATI', { subscriptionId: 'SECRET-ICCID', note: 'keep-me' });
83
- expect(entries).toHaveLength(1);
84
- const recorded = entries[0] as {
85
- command: string;
86
- outcome: string;
87
- context: Record<string, unknown>;
88
- };
89
- expect(recorded.command).toBe('ATI');
90
- expect(recorded.outcome).toBe('sent');
91
- expect(recorded.context.subscriptionId).toBe('[redacted]');
92
- expect(recorded.context.note).toBe('keep-me');
93
- });
94
-
95
- test('a rejected command is audited with outcome "rejected"', async () => {
96
- const { sender } = recordingSender();
97
- const entries: Array<{ outcome: string }> = [];
98
- const lease = new AtCommandLease({
99
- sender,
100
- allowlist: computeAtAllowlist([]),
101
- audit: { record: (entry) => entries.push(entry as { outcome: string }) },
102
- });
103
- await expect(lease.run('AT+NOPE')).rejects.toBeInstanceOf(AtCommandNotAllowedError);
104
- expect(entries[0]?.outcome).toBe('rejected');
105
- });
106
- });
@@ -1,158 +0,0 @@
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
-
14
- import { type EpochMillis, epochMillis } from '../domain';
15
- import { redact } from '../redact';
16
-
17
- /** The baseline allowlist — identify only. Catalog commands are unioned in per SKU. */
18
- export const AT_BASELINE_ALLOWLIST: ReadonlySet<string> = new Set(['ATI']);
19
-
20
- /** Union the baseline allowlist with a catalog entry's declared transition commands. */
21
- export function computeAtAllowlist(commands: Iterable<string>): ReadonlySet<string> {
22
- return new Set<string>([...AT_BASELINE_ALLOWLIST, ...commands]);
23
- }
24
-
25
- /** An AT command's response. `ok` (an `OK` terminator) is NEVER transition-success alone. */
26
- export interface AtResponse {
27
- readonly ok: boolean;
28
- readonly raw: string;
29
- }
30
-
31
- /** The raw AT transport — a serial write, injected so tests need no hardware. */
32
- export interface AtCommandSender {
33
- send(command: string): Promise<AtResponse>;
34
- }
35
-
36
- /** One audited AT attempt. Recorded only after passing through `redact`. */
37
- export interface AtAuditEntry {
38
- readonly command: string;
39
- readonly outcome: 'sent' | 'rejected' | 'timeout' | 'error';
40
- readonly at: EpochMillis;
41
- readonly ok?: boolean;
42
- readonly reason?: string;
43
- readonly context?: Record<string, unknown>;
44
- }
45
-
46
- /** Where audit entries go — receives a REDACTED copy of each `AtAuditEntry`. */
47
- export interface AtAuditSink {
48
- record(entry: unknown): void;
49
- }
50
-
51
- /** Thrown when a command outside the allowlist is attempted — the sender is never called. */
52
- export class AtCommandNotAllowedError extends Error {
53
- constructor(command: string) {
54
- super(`AT command not in allowlist: ${command}`);
55
- this.name = 'AtCommandNotAllowedError';
56
- Object.setPrototypeOf(this, AtCommandNotAllowedError.prototype);
57
- }
58
- }
59
-
60
- /** Thrown when a command exceeds the watchdog timeout. */
61
- export class AtCommandTimeoutError extends Error {
62
- constructor(command: string, timeoutMs: number) {
63
- super(`AT command timed out after ${timeoutMs}ms: ${command}`);
64
- this.name = 'AtCommandTimeoutError';
65
- Object.setPrototypeOf(this, AtCommandTimeoutError.prototype);
66
- }
67
- }
68
-
69
- /** Construction dependencies for an `AtCommandLease`. */
70
- export interface AtCommandLeaseDeps {
71
- readonly sender: AtCommandSender;
72
- readonly allowlist: ReadonlySet<string>;
73
- readonly audit?: AtAuditSink;
74
- readonly now?: () => EpochMillis;
75
- readonly timeoutMs?: number;
76
- /** Fired when a command exceeds the timeout — the transition wires force-uninhibit here. */
77
- readonly onWatchdog?: (command: string) => void | Promise<void>;
78
- }
79
-
80
- const DEFAULT_AT_TIMEOUT_MS = 10_000;
81
-
82
- /**
83
- * A held AT-command lease. `run` enforces the allowlist, bounds the send with a
84
- * watchdog, and audits every attempt (redacted). The allowlist is fixed at
85
- * construction from `computeAtAllowlist(entry)`, so a lease can only ever emit the
86
- * commands one certified SKU permits.
87
- */
88
- export class AtCommandLease {
89
- readonly #sender: AtCommandSender;
90
- readonly #allowlist: ReadonlySet<string>;
91
- readonly #audit: AtAuditSink | undefined;
92
- readonly #now: () => EpochMillis;
93
- readonly #timeoutMs: number;
94
- readonly #onWatchdog: ((command: string) => void | Promise<void>) | undefined;
95
-
96
- constructor(deps: AtCommandLeaseDeps) {
97
- this.#sender = deps.sender;
98
- this.#allowlist = deps.allowlist;
99
- this.#audit = deps.audit;
100
- this.#now = deps.now ?? ((): EpochMillis => epochMillis(Date.now()));
101
- this.#timeoutMs = deps.timeoutMs ?? DEFAULT_AT_TIMEOUT_MS;
102
- this.#onWatchdog = deps.onWatchdog;
103
- }
104
-
105
- /** Send one AT command through the lease. `context` is redacted into the audit entry. */
106
- async run(command: string, context?: Record<string, unknown>): Promise<AtResponse> {
107
- const ctx = context !== undefined ? { context } : {};
108
- if (!this.#allowlist.has(command)) {
109
- this.#record({
110
- command,
111
- outcome: 'rejected',
112
- at: this.#now(),
113
- reason: 'not in allowlist',
114
- ...ctx,
115
- });
116
- throw new AtCommandNotAllowedError(command);
117
- }
118
- try {
119
- const response = await this.#sendWithWatchdog(command);
120
- this.#record({ command, outcome: 'sent', at: this.#now(), ok: response.ok, ...ctx });
121
- return response;
122
- } catch (error) {
123
- const timedOut = error instanceof AtCommandTimeoutError;
124
- if (timedOut) {
125
- await this.#onWatchdog?.(command);
126
- }
127
- this.#record({
128
- command,
129
- outcome: timedOut ? 'timeout' : 'error',
130
- at: this.#now(),
131
- reason: error instanceof Error ? error.message : String(error),
132
- ...ctx,
133
- });
134
- throw error;
135
- }
136
- }
137
-
138
- async #sendWithWatchdog(command: string): Promise<AtResponse> {
139
- let timer: ReturnType<typeof setTimeout> | undefined;
140
- const watchdog = new Promise<never>((_, reject) => {
141
- timer = setTimeout(
142
- () => reject(new AtCommandTimeoutError(command, this.#timeoutMs)),
143
- this.#timeoutMs,
144
- );
145
- });
146
- try {
147
- return await Promise.race([this.#sender.send(command), watchdog]);
148
- } finally {
149
- if (timer !== undefined) {
150
- clearTimeout(timer);
151
- }
152
- }
153
- }
154
-
155
- #record(entry: AtAuditEntry): void {
156
- this.#audit?.record(redact(entry));
157
- }
158
- }
@@ -1,154 +0,0 @@
1
- // Cell-info normalization — pure fixtures, no bus.
2
- //
3
- // Locks the review-corrected mappings (draft round 10/11): the real NR key is `sinr`
4
- // (a `snr`-carrying dict is IGNORED), `physical-ci` → `pci`, `rsrp`/`rsrq` pass
5
- // through, `band` only when directly supplied, provenance is always carried, and the
6
- // serving-cell TOTAL order is permutation-invariant.
7
-
8
- import { describe, expect, test } from 'bun:test';
9
- import { epochMillis } from '../domain';
10
- import type { DbusVariant } from '../transport';
11
- import { type CellReading, normalizeCellReading, selectServingCell } from './cell-info';
12
- import type { DecodedProps } from './managed-objects';
13
-
14
- type Scalar = number | string | boolean;
15
-
16
- function v(value: Scalar): DbusVariant {
17
- const signature = typeof value === 'number' ? 'i' : typeof value === 'boolean' ? 'b' : 's';
18
- return { signature, value };
19
- }
20
-
21
- function cell(record: Record<string, Scalar>): DecodedProps {
22
- return Object.entries(record).map(([key, value]) => [key, v(value)] as const);
23
- }
24
-
25
- const PROVENANCE = {
26
- source: '/org/freedesktop/ModemManager1/Modem/0',
27
- observedAt: epochMillis(1000),
28
- };
29
-
30
- const read = (record: Record<string, Scalar>): CellReading =>
31
- normalizeCellReading(cell(record), PROVENANCE);
32
-
33
- /** Every fixed permutation of a small array — exhaustive, deterministic shuffle. */
34
- function permutations<T>(items: readonly T[]): T[][] {
35
- if (items.length <= 1) {
36
- return [[...items]];
37
- }
38
- const result: T[][] = [];
39
- items.forEach((item, index) => {
40
- const rest = [...items.slice(0, index), ...items.slice(index + 1)];
41
- for (const tail of permutations(rest)) {
42
- result.push([item, ...tail]);
43
- }
44
- });
45
- return result;
46
- }
47
-
48
- describe('normalizeCellReading — pinned key mappings', () => {
49
- test('the real NR key is `sinr`', () => {
50
- expect(read({ sinr: 12 }).sinr).toBe(12);
51
- });
52
-
53
- test('a `snr`-carrying dict is IGNORED — sinr stays undefined', () => {
54
- const reading = read({ snr: 9, rsrp: -95 });
55
- expect(reading.sinr).toBeUndefined();
56
- expect(reading.rsrp).toBe(-95);
57
- });
58
-
59
- test('`sinr` wins even when `snr` is also present (wrong key never leaks in)', () => {
60
- expect(read({ snr: 9, sinr: 12 }).sinr).toBe(12);
61
- });
62
-
63
- test('`physical-ci` maps to `pci`; rsrp/rsrq pass through', () => {
64
- const reading = read({ 'physical-ci': 42, rsrp: -95, rsrq: -10 });
65
- expect(reading.pci).toBe(42);
66
- expect(reading.rsrp).toBe(-95);
67
- expect(reading.rsrq).toBe(-10);
68
- });
69
-
70
- test('`band` is surfaced ONLY when directly supplied', () => {
71
- expect(read({ rsrp: -90 }).band).toBeUndefined();
72
- expect(read({ rsrp: -90, band: 'n78' }).band).toBe('n78');
73
- });
74
-
75
- test('every reading carries `source` + `observedAt` provenance', () => {
76
- const reading = read({ 'cell-id': 'A', rsrp: -80 });
77
- expect(reading.source).toBe(PROVENANCE.source);
78
- expect(reading.observedAt).toBe(PROVENANCE.observedAt);
79
- });
80
-
81
- test('serving flag reads a `serving` bool OR a serving `cell-type`', () => {
82
- expect(read({ serving: true }).serving).toBe(true);
83
- expect(read({ 'cell-type': 'lte-serving' }).serving).toBe(true);
84
- expect(read({ 'cell-type': 'lte-neighbor' }).serving).toBe(false);
85
- });
86
- });
87
-
88
- describe('selectServingCell — TOTAL order', () => {
89
- test('a serving-marked cell wins even with a LOWER rsrp', () => {
90
- const marked = read({ 'cell-id': 'A', serving: true, rsrp: -110 });
91
- const strong = read({ 'cell-id': 'B', rsrp: -70 });
92
- expect(selectServingCell([strong, marked])?.cellId).toBe('A');
93
- });
94
-
95
- test('with no serving mark, the HIGHEST rsrp wins', () => {
96
- const cells = [
97
- read({ 'cell-id': 'A', rsrp: -95 }),
98
- read({ 'cell-id': 'B', rsrp: -70 }),
99
- read({ 'cell-id': 'C', rsrp: -110 }),
100
- ];
101
- expect(selectServingCell(cells)?.cellId).toBe('B');
102
- });
103
-
104
- test('cells lacking rsrp sort LAST', () => {
105
- const withRsrp = read({ 'cell-id': 'A', rsrp: -120 });
106
- const noRsrp = read({ 'cell-id': 'B' });
107
- expect(selectServingCell([noRsrp, withRsrp])?.cellId).toBe('A');
108
- });
109
-
110
- test('an rsrp tie breaks lexicographically by cell-id', () => {
111
- const cells = [
112
- read({ 'cell-id': 'zeta', rsrp: -80 }),
113
- read({ 'cell-id': 'alpha', rsrp: -80 }),
114
- read({ 'cell-id': 'mike', rsrp: -80 }),
115
- ];
116
- expect(selectServingCell(cells)?.cellId).toBe('alpha');
117
- });
118
-
119
- test('empty input has no serving cell', () => {
120
- expect(selectServingCell([])).toBeUndefined();
121
- });
122
- });
123
-
124
- describe('selectServingCell — permutation invariance (MANDATORY)', () => {
125
- test('every permutation of a mixed cell set picks the SAME serving cell', () => {
126
- const cells = [
127
- read({ 'cell-id': 'A', rsrp: -95, 'physical-ci': 1 }),
128
- read({ 'cell-id': 'B', serving: true, rsrp: -108, 'physical-ci': 2 }),
129
- read({ 'cell-id': 'C', rsrp: -70, 'physical-ci': 3 }),
130
- read({ 'cell-id': 'D', 'physical-ci': 4 }),
131
- read({ 'cell-id': 'E', rsrp: -88, 'physical-ci': 5 }),
132
- ];
133
- const perms = permutations(cells);
134
- expect(perms.length).toBe(120);
135
- // The serving-marked cell B wins regardless of order.
136
- for (const perm of perms) {
137
- expect(selectServingCell(perm)?.cellId).toBe('B');
138
- }
139
- });
140
-
141
- test('without a serving mark, the highest-rsrp winner is permutation-invariant', () => {
142
- const cells = [
143
- read({ 'cell-id': 'A', rsrp: -95 }),
144
- read({ 'cell-id': 'B', rsrp: -70 }),
145
- read({ 'cell-id': 'C', rsrp: -110 }),
146
- read({ 'cell-id': 'D', rsrp: -70 }),
147
- read({ 'cell-id': 'E' }),
148
- ];
149
- // Tie at -70 between B and D → lexicographic → B.
150
- for (const perm of permutations(cells)) {
151
- expect(selectServingCell(perm)?.cellId).toBe('B');
152
- }
153
- });
154
- });