@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.
- package/README.md +341 -0
- package/dist/backend/at-lease.d.ts +59 -0
- package/dist/backend/at-lease.js +118 -0
- package/dist/backend/cell-info.d.ts +46 -0
- package/dist/backend/cell-info.js +124 -0
- package/dist/backend/constants.d.ts +24 -0
- package/{src/backend/constants.ts → dist/backend/constants.js} +4 -9
- package/dist/backend/device-classifier.d.ts +46 -0
- package/dist/backend/device-classifier.js +258 -0
- package/dist/backend/enrichment.d.ts +28 -0
- package/dist/backend/enrichment.js +59 -0
- package/dist/backend/features.d.ts +61 -0
- package/dist/backend/features.js +109 -0
- package/dist/backend/identity-ladder.d.ts +57 -0
- package/dist/backend/identity-ladder.js +158 -0
- package/dist/backend/identity-registry.d.ts +51 -0
- package/dist/backend/identity-registry.js +101 -0
- package/dist/backend/index.d.ts +30 -0
- package/dist/backend/index.js +35 -0
- package/dist/backend/lifecycle-interlock.d.ts +25 -0
- package/dist/backend/lifecycle-interlock.js +18 -0
- package/dist/backend/managed-objects.d.ts +39 -0
- package/dist/backend/managed-objects.js +82 -0
- package/dist/backend/mapping.d.ts +11 -0
- package/dist/backend/mapping.js +148 -0
- package/dist/backend/mm-backend.d.ts +44 -0
- package/dist/backend/mm-backend.js +154 -0
- package/dist/backend/mm-location.d.ts +21 -0
- package/dist/backend/mm-location.js +237 -0
- package/dist/backend/mm-mutations.d.ts +42 -0
- package/dist/backend/mm-mutations.js +259 -0
- package/dist/backend/modem-actor.d.ts +41 -0
- package/dist/backend/modem-actor.js +78 -0
- package/dist/backend/nm-auto-apn.d.ts +54 -0
- package/dist/backend/nm-auto-apn.js +124 -0
- package/dist/backend/nm-gsm-fields.d.ts +23 -0
- package/dist/backend/nm-gsm-fields.js +107 -0
- package/dist/backend/nmcli-nm-port.d.ts +28 -0
- package/dist/backend/nmcli-nm-port.js +173 -0
- package/dist/backend/nmcli-runner.d.ts +24 -0
- package/dist/backend/nmcli-runner.js +35 -0
- package/dist/backend/observer.d.ts +37 -0
- package/dist/backend/observer.js +219 -0
- package/dist/backend/power-contract.d.ts +49 -0
- package/dist/backend/power-contract.js +34 -0
- package/dist/backend/recovery-attribution.d.ts +32 -0
- package/dist/backend/recovery-attribution.js +57 -0
- package/dist/backend/recovery-budget.d.ts +45 -0
- package/dist/backend/recovery-budget.js +44 -0
- package/dist/backend/recovery-ladder.d.ts +94 -0
- package/dist/backend/recovery-ladder.js +116 -0
- package/dist/backend/router-ethernet.d.ts +19 -0
- package/dist/backend/router-ethernet.js +66 -0
- package/dist/backend/row-store.d.ts +13 -0
- package/dist/backend/row-store.js +82 -0
- package/dist/backend/signal-setup.d.ts +30 -0
- package/dist/backend/signal-setup.js +92 -0
- package/dist/backend/sim-unlock.d.ts +13 -0
- package/dist/backend/sim-unlock.js +153 -0
- package/dist/backend/transition-preconditions.d.ts +101 -0
- package/dist/backend/transition-preconditions.js +132 -0
- package/dist/backend/usage/accounting.d.ts +39 -0
- package/dist/backend/usage/accounting.js +73 -0
- package/dist/backend/usage/billing-cycle.d.ts +11 -0
- package/dist/backend/usage/billing-cycle.js +39 -0
- package/dist/backend/usage/boot-id.d.ts +6 -0
- package/{src/backend/usage/boot-id.ts → dist/backend/usage/boot-id.js} +7 -7
- package/dist/backend/usage/index.d.ts +8 -0
- package/dist/backend/usage/index.js +11 -0
- package/dist/backend/usage/policy-store.d.ts +50 -0
- package/dist/backend/usage/policy-store.js +161 -0
- package/dist/backend/usage/policy-write.d.ts +65 -0
- package/dist/backend/usage/policy-write.js +112 -0
- package/dist/backend/usage/proc-net-dev.d.ts +18 -0
- package/{src/backend/usage/proc-net-dev.ts → dist/backend/usage/proc-net-dev.js} +39 -47
- package/dist/backend/usage/sampler.d.ts +75 -0
- package/dist/backend/usage/sampler.js +211 -0
- package/dist/backend/usage/store.d.ts +38 -0
- package/dist/backend/usage/store.js +126 -0
- package/dist/backend/usb-device-snapshot.d.ts +31 -0
- package/dist/backend/usb-device-snapshot.js +1 -0
- package/dist/backend/usb-enumerator.d.ts +21 -0
- package/dist/backend/usb-enumerator.js +153 -0
- package/dist/backend/usb-mode-transition.d.ts +29 -0
- package/dist/backend/usb-mode-transition.js +216 -0
- package/dist/band/band-names.d.ts +42 -0
- package/dist/band/band-names.js +150 -0
- package/dist/band/certification.d.ts +84 -0
- package/dist/band/certification.js +127 -0
- package/dist/band/certified-bands.json +4 -0
- package/dist/band/index.d.ts +2 -0
- package/dist/band/index.js +8 -0
- package/dist/capability/detect.d.ts +52 -0
- package/dist/capability/detect.js +86 -0
- package/dist/capability/five-g-preference.d.ts +104 -0
- package/dist/capability/five-g-preference.js +171 -0
- package/dist/capability/index.d.ts +3 -0
- package/dist/capability/index.js +10 -0
- package/dist/capability/support-claim.d.ts +39 -0
- package/dist/capability/support-claim.js +81 -0
- package/dist/domain/brand.d.ts +10 -0
- package/dist/domain/brand.js +21 -0
- package/dist/domain/errors.d.ts +32 -0
- package/dist/domain/errors.js +48 -0
- package/dist/domain/generation.d.ts +8 -0
- package/dist/domain/generation.js +12 -0
- package/dist/domain/guards.d.ts +6 -0
- package/dist/domain/guards.js +127 -0
- package/dist/domain/identity.d.ts +109 -0
- package/dist/domain/identity.js +86 -0
- package/dist/domain/index.d.ts +14 -0
- package/dist/domain/index.js +18 -0
- package/dist/domain/mm-enums.d.ts +12 -0
- package/dist/domain/mm-enums.js +139 -0
- package/dist/domain/modem-presentation.d.ts +10 -0
- package/dist/domain/modem-presentation.js +39 -0
- package/dist/domain/observation.d.ts +39 -0
- package/dist/domain/observation.js +4 -0
- package/dist/domain/operation.d.ts +118 -0
- package/dist/domain/operation.js +85 -0
- package/dist/domain/physical-identity.d.ts +43 -0
- package/dist/domain/physical-identity.js +113 -0
- package/{src/domain/policy.ts → dist/domain/policy.d.ts} +34 -69
- package/dist/domain/policy.js +38 -0
- package/dist/domain/shadow-divergence.d.ts +27 -0
- package/dist/domain/shadow-divergence.js +70 -0
- package/dist/domain/snapshot.d.ts +53 -0
- package/dist/domain/snapshot.js +75 -0
- package/{src/domain/state.ts → dist/domain/state.d.ts} +23 -122
- package/dist/domain/state.js +38 -0
- package/dist/fcc/coverage.d.ts +63 -0
- package/dist/fcc/coverage.js +102 -0
- package/dist/fcc/index.d.ts +3 -0
- package/dist/fcc/index.js +12 -0
- package/dist/fcc/policy-store.d.ts +40 -0
- package/dist/fcc/policy-store.js +146 -0
- package/dist/fcc/policy-write.d.ts +32 -0
- package/dist/fcc/policy-write.js +37 -0
- package/dist/hardware/hilink-protocol.d.ts +39 -0
- package/dist/hardware/hilink-protocol.js +58 -0
- package/dist/hardware/index.d.ts +3 -0
- package/dist/hardware/index.js +15 -0
- package/dist/hardware/router-parsers.d.ts +89 -0
- package/dist/hardware/router-parsers.js +234 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +27 -0
- package/dist/journal/codec.d.ts +44 -0
- package/dist/journal/codec.js +198 -0
- package/dist/journal/engine.d.ts +28 -0
- package/dist/journal/engine.js +68 -0
- package/dist/journal/entry.d.ts +74 -0
- package/dist/journal/entry.js +56 -0
- package/dist/journal/index.d.ts +6 -0
- package/dist/journal/index.js +6 -0
- package/dist/journal/legacy-ceraui.d.ts +73 -0
- package/dist/journal/legacy-ceraui.js +227 -0
- package/dist/journal/recovery.d.ts +58 -0
- package/dist/journal/recovery.js +117 -0
- package/dist/journal/store.d.ts +55 -0
- package/dist/journal/store.js +150 -0
- package/dist/location/fix-state.d.ts +53 -0
- package/dist/location/fix-state.js +75 -0
- package/dist/location/index.d.ts +2 -0
- package/dist/location/index.js +8 -0
- package/dist/location/nmea.d.ts +10 -0
- package/dist/location/nmea.js +89 -0
- package/dist/observations/envelope.d.ts +63 -0
- package/dist/observations/envelope.js +77 -0
- package/dist/observations/freshness.d.ts +28 -0
- package/dist/observations/freshness.js +76 -0
- package/dist/observations/index.d.ts +13 -0
- package/dist/observations/index.js +24 -0
- package/dist/observations/metric.d.ts +61 -0
- package/dist/observations/metric.js +73 -0
- package/dist/observations/model.d.ts +80 -0
- package/dist/observations/model.js +11 -0
- package/dist/observations/provenance.d.ts +94 -0
- package/dist/observations/provenance.js +67 -0
- package/dist/observations/raw.d.ts +42 -0
- package/dist/observations/raw.js +146 -0
- package/dist/observations/reading.d.ts +51 -0
- package/dist/observations/reading.js +65 -0
- package/dist/observations/sources/hilink.d.ts +10 -0
- package/dist/observations/sources/hilink.js +84 -0
- package/dist/observations/sources/modemmanager.d.ts +18 -0
- package/dist/observations/sources/modemmanager.js +239 -0
- package/dist/observations/sources/router-shared.d.ts +29 -0
- package/dist/observations/sources/router-shared.js +68 -0
- package/dist/observations/sources/ufi.d.ts +10 -0
- package/dist/observations/sources/ufi.js +111 -0
- package/dist/observations/sources/zte.d.ts +7 -0
- package/dist/observations/sources/zte.js +71 -0
- package/dist/observations/state-separation.d.ts +64 -0
- package/dist/observations/state-separation.js +52 -0
- package/dist/operation-ids.d.ts +2 -0
- package/dist/operation-ids.js +26 -0
- package/dist/operations/contracts.d.ts +72 -0
- package/dist/operations/contracts.js +1 -0
- package/dist/operations/index.d.ts +1 -0
- package/dist/operations/index.js +1 -0
- package/dist/operations/operation-engine.d.ts +16 -0
- package/dist/operations/operation-engine.js +194 -0
- package/dist/ports/index.d.ts +12 -0
- package/{src/ports/index.ts → dist/ports/index.js} +12 -8
- package/dist/ports/location.d.ts +87 -0
- package/dist/ports/location.js +36 -0
- package/dist/ports/modem-manager.d.ts +89 -0
- package/dist/ports/modem-manager.js +9 -0
- package/dist/ports/mutation-admission.d.ts +27 -0
- package/dist/ports/mutation-admission.js +9 -0
- package/dist/ports/network-manager.d.ts +68 -0
- package/dist/ports/network-manager.js +13 -0
- package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
- package/dist/ports/observation.js +7 -0
- package/dist/ports/ops.d.ts +44 -0
- package/dist/ports/ops.js +16 -0
- package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
- package/dist/ports/receipts.js +9 -0
- package/dist/ports/reconcile.d.ts +33 -0
- package/dist/ports/reconcile.js +200 -0
- package/dist/ports/resource-ownership.d.ts +29 -0
- package/dist/ports/resource-ownership.js +1 -0
- package/dist/ports/router.d.ts +19 -0
- package/dist/ports/router.js +7 -0
- package/dist/ports/sms.d.ts +64 -0
- package/dist/ports/sms.js +24 -0
- package/dist/ports/uhubctl.d.ts +6 -0
- package/dist/ports/uhubctl.js +1 -0
- package/dist/providers/contracts.d.ts +124 -0
- package/dist/providers/contracts.js +10 -0
- package/dist/providers/huawei-hilink/index.d.ts +2 -0
- package/dist/providers/huawei-hilink/index.js +2 -0
- package/dist/providers/huawei-hilink/operations.d.ts +20 -0
- package/dist/providers/huawei-hilink/operations.js +56 -0
- package/dist/providers/huawei-hilink/provider.d.ts +52 -0
- package/dist/providers/huawei-hilink/provider.js +76 -0
- package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
- package/dist/providers/huawei-hilink/runtime.js +171 -0
- package/dist/providers/huawei-hilink/session.d.ts +28 -0
- package/dist/providers/huawei-hilink/session.js +120 -0
- package/dist/providers/huawei-hilink/transport.d.ts +19 -0
- package/dist/providers/huawei-hilink/transport.js +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/matcher.d.ts +3 -0
- package/dist/providers/matcher.js +205 -0
- package/dist/providers/modem-manager/errors.d.ts +7 -0
- package/dist/providers/modem-manager/errors.js +37 -0
- package/dist/providers/modem-manager/generic-operations.d.ts +10 -0
- package/dist/providers/modem-manager/generic-operations.js +209 -0
- package/dist/providers/modem-manager/index.d.ts +4 -0
- package/dist/providers/modem-manager/index.js +4 -0
- package/dist/providers/modem-manager/module-operations.d.ts +21 -0
- package/dist/providers/modem-manager/module-operations.js +118 -0
- package/dist/providers/modem-manager/provider.d.ts +41 -0
- package/dist/providers/modem-manager/provider.js +155 -0
- package/dist/providers/modem-manager/runtime-composition-operation.d.ts +32 -0
- package/dist/providers/modem-manager/runtime-composition-operation.js +151 -0
- package/dist/providers/modem-manager/snapshot.d.ts +5 -0
- package/dist/providers/modem-manager/snapshot.js +204 -0
- package/dist/providers/modem-manager/types.d.ts +137 -0
- package/dist/providers/modem-manager/types.js +1 -0
- package/dist/providers/network-manager/adapter.d.ts +71 -0
- package/dist/providers/network-manager/adapter.js +348 -0
- package/dist/providers/network-manager/index.d.ts +2 -0
- package/dist/providers/network-manager/index.js +2 -0
- package/dist/providers/network-manager/types.d.ts +171 -0
- package/dist/providers/network-manager/types.js +77 -0
- package/dist/providers/registry.d.ts +13 -0
- package/dist/providers/registry.js +33 -0
- package/dist/providers/ufi-himi/index.d.ts +6 -0
- package/dist/providers/ufi-himi/index.js +6 -0
- package/dist/providers/ufi-himi/operations.d.ts +41 -0
- package/dist/providers/ufi-himi/operations.js +66 -0
- package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
- package/dist/providers/ufi-himi/prohibitions.js +88 -0
- package/dist/providers/ufi-himi/provider.d.ts +41 -0
- package/dist/providers/ufi-himi/provider.js +204 -0
- package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
- package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
- package/dist/providers/ufi-himi/session.d.ts +46 -0
- package/dist/providers/ufi-himi/session.js +92 -0
- package/dist/providers/ufi-himi/transport.d.ts +29 -0
- package/dist/providers/ufi-himi/transport.js +25 -0
- package/dist/providers/zte-goform/index.d.ts +2 -0
- package/dist/providers/zte-goform/index.js +2 -0
- package/dist/providers/zte-goform/provider.d.ts +56 -0
- package/dist/providers/zte-goform/provider.js +101 -0
- package/dist/providers/zte-goform/session.d.ts +23 -0
- package/dist/providers/zte-goform/session.js +197 -0
- package/dist/providers/zte-goform/transport.d.ts +16 -0
- package/dist/providers/zte-goform/transport.js +1 -0
- package/dist/radio/band-truth.d.ts +50 -0
- package/dist/radio/band-truth.js +92 -0
- package/dist/radio/index.d.ts +3 -0
- package/dist/radio/index.js +10 -0
- package/dist/radio/mode-combinations.d.ts +88 -0
- package/dist/radio/mode-combinations.js +198 -0
- package/dist/radio/mode-truth.d.ts +67 -0
- package/dist/radio/mode-truth.js +112 -0
- package/dist/redact.d.ts +15 -0
- package/dist/redact.js +189 -0
- package/dist/safety/composition-root.d.ts +28 -0
- package/dist/safety/composition-root.js +65 -0
- package/dist/safety/flock-resource-ownership.d.ts +11 -0
- package/dist/safety/flock-resource-ownership.js +138 -0
- package/dist/safety/index.d.ts +2 -0
- package/dist/safety/index.js +2 -0
- package/dist/sms/dbus-messaging.d.ts +17 -0
- package/dist/sms/dbus-messaging.js +185 -0
- package/dist/sms/inbox-store.d.ts +10 -0
- package/dist/sms/inbox-store.js +82 -0
- package/dist/sms/index.d.ts +4 -0
- package/dist/sms/index.js +10 -0
- package/dist/sms/mmcli-parse.d.ts +54 -0
- package/dist/sms/mmcli-parse.js +224 -0
- package/dist/sms/normalize.d.ts +42 -0
- package/dist/sms/normalize.js +95 -0
- package/dist/testing/domain-fakes.d.ts +58 -0
- package/dist/testing/domain-fakes.js +98 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/provider-fakes.d.ts +45 -0
- package/dist/testing/provider-fakes.js +64 -0
- package/dist/transport/calls.d.ts +9 -0
- package/dist/transport/calls.js +88 -0
- package/dist/transport/codec.d.ts +3 -0
- package/dist/transport/codec.js +207 -0
- package/dist/transport/dbus-native.d.ts +57 -0
- package/dist/transport/dbus-native.js +17 -0
- package/dist/transport/errors.d.ts +21 -0
- package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
- package/dist/transport/index.d.ts +4 -0
- package/dist/transport/index.js +9 -0
- package/dist/transport/signals.d.ts +15 -0
- package/dist/transport/signals.js +123 -0
- package/dist/transport/signature.d.ts +7 -0
- package/dist/transport/signature.js +94 -0
- package/dist/transport/transport.d.ts +2 -0
- package/dist/transport/transport.js +202 -0
- package/dist/transport/types.d.ts +61 -0
- package/dist/transport/types.js +19 -0
- package/dist/usb-mode/catalog-schema.d.ts +139 -0
- package/dist/usb-mode/catalog-schema.js +97 -0
- package/dist/usb-mode/catalog.d.ts +21 -0
- package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
- package/dist/usb-mode/certified-catalog.json +67 -0
- package/dist/usb-mode/index.d.ts +6 -0
- package/dist/usb-mode/index.js +16 -0
- package/dist/usb-mode/ingestion.d.ts +111 -0
- package/dist/usb-mode/ingestion.js +187 -0
- package/dist/usb-mode/promotion-review.d.ts +21 -0
- package/dist/usb-mode/promotion-review.js +87 -0
- package/dist/usb-mode/runtime-capability.d.ts +59 -0
- package/dist/usb-mode/runtime-capability.js +157 -0
- package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
- package/dist/usb-mode/usb-devices-parse.js +157 -0
- package/dist/ussd/calls.d.ts +32 -0
- package/dist/ussd/calls.js +96 -0
- package/dist/ussd/index.d.ts +5 -0
- package/dist/ussd/index.js +12 -0
- package/dist/ussd/mm-ussd.d.ts +37 -0
- package/dist/ussd/mm-ussd.js +205 -0
- package/dist/ussd/refusal.d.ts +53 -0
- package/dist/ussd/refusal.js +154 -0
- package/dist/ussd/registration.d.ts +20 -0
- package/dist/ussd/registration.js +101 -0
- package/dist/ussd/session.d.ts +106 -0
- package/dist/ussd/session.js +163 -0
- package/package.json +38 -4
- package/src/backend/at-lease.test.ts +0 -106
- package/src/backend/at-lease.ts +0 -158
- package/src/backend/cell-info.test.ts +0 -154
- package/src/backend/cell-info.ts +0 -160
- package/src/backend/device-classifier.test.ts +0 -168
- package/src/backend/device-classifier.ts +0 -248
- package/src/backend/enrichment.ts +0 -96
- package/src/backend/features.test.ts +0 -162
- package/src/backend/features.ts +0 -179
- package/src/backend/identity-ladder.test.ts +0 -117
- package/src/backend/identity-ladder.ts +0 -221
- package/src/backend/identity-registry.test.ts +0 -89
- package/src/backend/identity-registry.ts +0 -151
- package/src/backend/index.ts +0 -236
- package/src/backend/lifecycle-interlock.ts +0 -38
- package/src/backend/managed-objects.ts +0 -108
- package/src/backend/mapping.ts +0 -160
- package/src/backend/mm-backend.ts +0 -191
- package/src/backend/mm-mutations.ts +0 -228
- package/src/backend/modem-actor.test.ts +0 -95
- package/src/backend/modem-actor.ts +0 -112
- package/src/backend/nm-auto-apn.ts +0 -161
- package/src/backend/nm-gsm-fields.ts +0 -122
- package/src/backend/nmcli-nm-port.ts +0 -228
- package/src/backend/nmcli-runner.ts +0 -52
- package/src/backend/observer.ts +0 -297
- package/src/backend/power-contract.test.ts +0 -40
- package/src/backend/power-contract.ts +0 -83
- package/src/backend/recovery-attribution.test.ts +0 -102
- package/src/backend/recovery-attribution.ts +0 -86
- package/src/backend/recovery-budget.test.ts +0 -64
- package/src/backend/recovery-budget.ts +0 -84
- package/src/backend/recovery-ladder.test.ts +0 -257
- package/src/backend/recovery-ladder.ts +0 -249
- package/src/backend/router-ethernet.test.ts +0 -71
- package/src/backend/router-ethernet.ts +0 -90
- package/src/backend/row-store.ts +0 -105
- package/src/backend/signal-setup.ts +0 -112
- package/src/backend/sim-unlock.ts +0 -193
- package/src/backend/transition-preconditions.ts +0 -149
- package/src/backend/uhubctl-power-hook.test.ts +0 -274
- package/src/backend/uhubctl-power-hook.ts +0 -377
- package/src/backend/usage/accounting.test.ts +0 -147
- package/src/backend/usage/accounting.ts +0 -123
- package/src/backend/usage/billing-cycle.test.ts +0 -62
- package/src/backend/usage/billing-cycle.ts +0 -45
- package/src/backend/usage/index.ts +0 -60
- package/src/backend/usage/policy-store.test.ts +0 -164
- package/src/backend/usage/policy-store.ts +0 -216
- package/src/backend/usage/policy-write.test.ts +0 -198
- package/src/backend/usage/policy-write.ts +0 -207
- package/src/backend/usage/proc-net-dev.test.ts +0 -56
- package/src/backend/usage/sampler.test.ts +0 -327
- package/src/backend/usage/sampler.ts +0 -282
- package/src/backend/usage/store.test.ts +0 -148
- package/src/backend/usage/store.ts +0 -177
- package/src/backend/usb-enumerator.test.ts +0 -87
- package/src/backend/usb-enumerator.ts +0 -181
- package/src/backend/usb-mode-transition.test.ts +0 -323
- package/src/backend/usb-mode-transition.ts +0 -253
- package/src/domain/brand.ts +0 -29
- package/src/domain/errors.ts +0 -77
- package/src/domain/guards.test.ts +0 -218
- package/src/domain/guards.ts +0 -144
- package/src/domain/identity.test.ts +0 -83
- package/src/domain/identity.ts +0 -165
- package/src/domain/index.ts +0 -12
- package/src/domain/snapshot.test.ts +0 -266
- package/src/domain/snapshot.ts +0 -120
- package/src/index.test.ts +0 -6
- package/src/index.ts +0 -15
- package/src/ports/README.md +0 -61
- package/src/ports/forbidden-surface.test.ts +0 -241
- package/src/ports/modem-manager.ts +0 -72
- package/src/ports/network-manager.ts +0 -87
- package/src/ports/ops.ts +0 -60
- package/src/ports/ops.type-test.ts +0 -39
- package/src/ports/receipts.test.ts +0 -153
- package/src/ports/reconcile.test.ts +0 -152
- package/src/ports/reconcile.ts +0 -338
- package/src/ports/router.ts +0 -29
- package/src/redact.test.ts +0 -82
- package/src/redact.ts +0 -73
- package/src/transport/README.md +0 -65
- package/src/transport/calls.ts +0 -113
- package/src/transport/characterization.test.ts +0 -260
- package/src/transport/codec.test.ts +0 -118
- package/src/transport/codec.ts +0 -240
- package/src/transport/conformance-python.test.ts +0 -152
- package/src/transport/conformance-same-lib.test.ts +0 -115
- package/src/transport/dbus-native-lib.d.ts +0 -19
- package/src/transport/dbus-native.ts +0 -85
- package/src/transport/index.ts +0 -30
- package/src/transport/no-library-leak.test.ts +0 -61
- package/src/transport/reliability.test.ts +0 -173
- package/src/transport/signals.ts +0 -150
- package/src/transport/signature.ts +0 -110
- package/src/transport/test-support/fake-service.ts +0 -168
- package/src/transport/test-support/independent-producer.py +0 -110
- package/src/transport/test-support/private-bus.ts +0 -66
- package/src/transport/transport.ts +0 -250
- package/src/transport/types.ts +0 -118
- package/src/usb-mode/catalog-schema.test.ts +0 -181
- package/src/usb-mode/catalog-schema.ts +0 -113
- package/src/usb-mode/certified-catalog.json +0 -67
- package/src/usb-mode/index.ts +0 -58
- package/src/usb-mode/ingestion.test.ts +0 -268
- package/src/usb-mode/ingestion.ts +0 -297
- package/src/usb-mode/promotion-review.ts +0 -117
- 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.
|
|
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
|
-
".":
|
|
18
|
-
|
|
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
|
-
"
|
|
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
|
-
});
|
package/src/backend/at-lease.ts
DELETED
|
@@ -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
|
-
});
|