@ceralive/modem-control 0.2.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -0
- package/dist/backend/at-lease.d.ts +57 -0
- package/dist/backend/at-lease.js +109 -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 +76 -0
- package/dist/backend/transition-preconditions.js +64 -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 +196 -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 +19 -0
- package/dist/index.js +26 -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 +47 -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/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 +8 -0
- package/dist/providers/modem-manager/generic-operations.js +207 -0
- package/dist/providers/modem-manager/index.d.ts +3 -0
- package/dist/providers/modem-manager/index.js +3 -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 +39 -0
- package/dist/providers/modem-manager/provider.js +152 -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 +135 -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 +163 -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 +51 -0
- package/dist/providers/zte-goform/provider.js +101 -0
- package/dist/providers/zte-goform/session.d.ts +17 -0
- package/dist/providers/zte-goform/session.js +133 -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 +78 -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 +151 -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 +5 -0
- package/dist/usb-mode/index.js +15 -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/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 +84 -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 -221
- 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/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 -37
- package/src/backend/usage/proc-net-dev.test.ts +0 -56
- package/src/backend/usage/sampler.test.ts +0 -219
- package/src/backend/usage/sampler.ts +0 -228
- 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 -27
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Rendering the REVIEW ARTIFACT for a catalog promotion — the PR-comment template.
|
|
2
|
+
//
|
|
3
|
+
// Catalog additions are human-reviewed commits (Phase-A rule). This module renders what
|
|
4
|
+
// a reviewer reads: the proposed entry, the classifier fixture derived from the same
|
|
5
|
+
// bundle, and a checklist whose boxes a machine cannot tick. It renders a REFUSAL with
|
|
6
|
+
// equal prominence — a refused promotion produces a comment that says so, never silence,
|
|
7
|
+
// because a silently-absent comment is indistinguishable from a forgotten run.
|
|
8
|
+
//
|
|
9
|
+
// Nothing here writes a file or opens a PR. The output is text.
|
|
10
|
+
function refusalBlock(what, refusal) {
|
|
11
|
+
return [
|
|
12
|
+
`### ❌ ${what} — REFUSED`,
|
|
13
|
+
'',
|
|
14
|
+
`**Reason:** \`${refusal.reason}\``,
|
|
15
|
+
'',
|
|
16
|
+
`> ${refusal.detail}`,
|
|
17
|
+
'',
|
|
18
|
+
'This is a typed refusal from the ingestion seam, not a review opinion. Fix the',
|
|
19
|
+
'capture and re-run the runbook; do not hand-author the artifact around it.',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
22
|
+
function entryBlock(entry) {
|
|
23
|
+
return [
|
|
24
|
+
'### Proposed `certified-catalog.json` entry',
|
|
25
|
+
'',
|
|
26
|
+
'```json',
|
|
27
|
+
JSON.stringify(entry, null, 2),
|
|
28
|
+
'```',
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
31
|
+
function fixtureBlock(fixture) {
|
|
32
|
+
const { snapshot, provenance } = fixture;
|
|
33
|
+
const syntheticNote = provenance.synthetic
|
|
34
|
+
? '> ⚠️ Derived from a **synthetic** bundle — valid as test data, never as certification evidence.'
|
|
35
|
+
: `> Derived from a real capture, bundle sha256 \`${provenance.bundleSha256}\`.`;
|
|
36
|
+
return [
|
|
37
|
+
'### Proposed classifier fixture (`control/src/backend/device-classifier.test.ts`)',
|
|
38
|
+
'',
|
|
39
|
+
syntheticNote,
|
|
40
|
+
'',
|
|
41
|
+
'```ts',
|
|
42
|
+
`const FIXTURE: UsbDeviceSnapshot = ${JSON.stringify(snapshot, null, 2)};`,
|
|
43
|
+
'```',
|
|
44
|
+
].join('\n');
|
|
45
|
+
}
|
|
46
|
+
function checklistBlock(context, entry) {
|
|
47
|
+
const transitions = entry.permittedTransitions.length;
|
|
48
|
+
return [
|
|
49
|
+
'### Reviewer checklist (every box is a human judgement)',
|
|
50
|
+
'',
|
|
51
|
+
`- [ ] The bundle at \`${context.evidencePath}\` was captured by **${context.runbook}** on real hardware, and its \`CERTIFY OK\` line reads \`synthetic=false\`.`,
|
|
52
|
+
'- [ ] The bundle sha256 in the entry matches the sha256 the capture printed — recomputed, not copied from this comment.',
|
|
53
|
+
`- [ ] \`canonicalMode: "${entry.canonicalMode}"\` is the mode the device was actually observed in, not the mode it was expected to be in.`,
|
|
54
|
+
transitions === 0
|
|
55
|
+
? '- [ ] `permittedTransitions: []` is correct for this stage — a stage-1 entry never declares a transition.'
|
|
56
|
+
: '- [ ] The declared transition was OBSERVED end to end: the AT command executed, the port dropped if `expectsPortDrop`, and the device re-enumerated presenting `expectedDescriptors`.',
|
|
57
|
+
'- [ ] No claim in `docs/MODEM-SUPPORT-MATRIX.md` is being changed by this commit without its own evidence.',
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render the review comment for a promotion request. Always returns a comment: a
|
|
62
|
+
* refusal renders a refusal block, so a run that produced nothing promotable still
|
|
63
|
+
* leaves a visible, auditable trace.
|
|
64
|
+
*/
|
|
65
|
+
export function renderPromotionReview(request) {
|
|
66
|
+
const { context, entry, fixture } = request;
|
|
67
|
+
const parts = [
|
|
68
|
+
`## Catalog promotion review — ${context.runbook}`,
|
|
69
|
+
'',
|
|
70
|
+
`Evidence: \`${context.evidencePath}\``,
|
|
71
|
+
'',
|
|
72
|
+
'Generated by the `control/src/usb-mode/` ingestion seam. **This comment promotes',
|
|
73
|
+
'nothing** — the promotion is the human-reviewed commit that follows it.',
|
|
74
|
+
'',
|
|
75
|
+
];
|
|
76
|
+
parts.push(entry.ok ? entryBlock(entry.value) : refusalBlock('Catalog entry', entry));
|
|
77
|
+
parts.push('');
|
|
78
|
+
parts.push(fixture.ok ? fixtureBlock(fixture.value) : refusalBlock('Classifier fixture', fixture));
|
|
79
|
+
parts.push('');
|
|
80
|
+
if (entry.ok) {
|
|
81
|
+
parts.push(checklistBlock(context, entry.value));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
parts.push('### No checklist', '', 'The catalog entry was refused, so there is nothing to review. A checklist here', 'would invite a reviewer to approve an artifact that does not exist.');
|
|
85
|
+
}
|
|
86
|
+
return `${parts.join('\n')}\n`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** One interface line of a `usb-devices` record. */
|
|
2
|
+
export interface ParsedUsbInterface {
|
|
3
|
+
readonly interfaceClass: number;
|
|
4
|
+
readonly interfaceSubClass: number;
|
|
5
|
+
readonly interfaceProtocol: number;
|
|
6
|
+
/** The bound kernel driver, omitted when `usb-devices` reports `(none)`. */
|
|
7
|
+
readonly driver?: string;
|
|
8
|
+
}
|
|
9
|
+
/** One device block of a `usb-devices` capture. */
|
|
10
|
+
export interface ParsedUsbDevice {
|
|
11
|
+
/** Lowercase hex `xxxx:xxxx`, exactly the catalog's `vidPid` discriminator shape. */
|
|
12
|
+
readonly vidPid: string;
|
|
13
|
+
/** The `D:` line's `Cls=` byte — the device-descriptor `bDeviceClass`. */
|
|
14
|
+
readonly bDeviceClass: number;
|
|
15
|
+
readonly manufacturer?: string;
|
|
16
|
+
readonly product?: string;
|
|
17
|
+
readonly interfaces: readonly ParsedUsbInterface[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse `usb-devices` output into one record per device. Pure and total: malformed
|
|
21
|
+
* lines are skipped rather than guessed at, and a block with no `P:` line yields no
|
|
22
|
+
* record (it has no identity, so inventing one would be a lie).
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseUsbDevices(text: string): ParsedUsbDevice[];
|
|
25
|
+
/**
|
|
26
|
+
* Find the single device matching `vidPid` in a parsed capture. Returns `undefined`
|
|
27
|
+
* when there is NO match, and — deliberately — also when there is more than one: a
|
|
28
|
+
* duplicate VID:PID (this bench has two identical Huawei HiLink units) makes the
|
|
29
|
+
* selection ambiguous, and an ambiguous selection must refuse rather than pick the
|
|
30
|
+
* first. The caller turns that into a typed refusal.
|
|
31
|
+
*/
|
|
32
|
+
export declare function selectUniqueDevice(devices: readonly ParsedUsbDevice[], vidPid: string): {
|
|
33
|
+
readonly device: ParsedUsbDevice;
|
|
34
|
+
} | {
|
|
35
|
+
readonly ambiguousMatches: number;
|
|
36
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Parsing `usb-devices` text — the ONLY per-interface descriptor source inside a
|
|
2
|
+
// certification bundle.
|
|
3
|
+
//
|
|
4
|
+
// A base certification bundle (`certify <slot>` with no `--transition`) carries no
|
|
5
|
+
// structured descriptors at all: it holds `lsusb -v` and `usb-devices` as raw text plus
|
|
6
|
+
// the slot's udev property map. Authoring a classifier fixture or a catalog entry from
|
|
7
|
+
// such a bundle therefore requires reading the descriptors back out of that text, and
|
|
8
|
+
// `usb-devices` is the right half to read — it is line-oriented, one fixed-width record
|
|
9
|
+
// per device, and it names each interface's BOUND KERNEL DRIVER, which `lsusb -v` does
|
|
10
|
+
// not. The driver is not optional detail here: `classifyDevice` decides `mm-managed` vs
|
|
11
|
+
// `router-mode` partly on `qmi_wwan` / `cdc_ether` / `option` bindings.
|
|
12
|
+
//
|
|
13
|
+
// The parser is pure and total: unparseable lines are SKIPPED, never guessed at, and a
|
|
14
|
+
// device that yields no interfaces still yields a record (callers decide whether an
|
|
15
|
+
// interface-less device is usable — this file never makes that judgement).
|
|
16
|
+
//
|
|
17
|
+
// Record shape (`usb-devices`, one blank-line-separated block per device):
|
|
18
|
+
// T: Bus=04 Lev=03 Prnt=03 Port=03 Cnt=01 Dev#= 7 Spd=480 MxCh= 0
|
|
19
|
+
// D: Ver= 2.00 Cls=00(>ifc ) Sub=00 Prot=00 MxPS=64 #Cfgs= 1
|
|
20
|
+
// P: Vendor=2c7c ProdID=0801 Rev=05.04
|
|
21
|
+
// S: Manufacturer=Quectel
|
|
22
|
+
// S: Product=RM530N-GL
|
|
23
|
+
// I: If#= 4 Alt= 0 #EPs= 3 Cls=ff(vend.) Sub=ff Prot=ff Driver=qmi_wwan
|
|
24
|
+
/** Read `Key=value` from a `usb-devices` line; `undefined` when the key is absent. */
|
|
25
|
+
function field(line, key) {
|
|
26
|
+
// Values are whitespace-delimited and may be preceded by padding spaces (`Dev#= 7`).
|
|
27
|
+
// `Cls=ff(vend.)` carries a trailing gloss, stripped by the hex/number parsers below.
|
|
28
|
+
const match = new RegExp(`${key}=\\s*(\\S+)`).exec(line);
|
|
29
|
+
return match?.[1];
|
|
30
|
+
}
|
|
31
|
+
/** Parse a hex byte field, tolerating `usb-devices`' `ff(vend.)` gloss suffix. */
|
|
32
|
+
function hexByte(line, key) {
|
|
33
|
+
const raw = field(line, key);
|
|
34
|
+
if (raw === undefined) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const digits = /^[0-9a-fA-F]{1,2}/.exec(raw)?.[0];
|
|
38
|
+
if (digits === undefined) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const value = Number.parseInt(digits, 16);
|
|
42
|
+
return Number.isNaN(value) ? undefined : value;
|
|
43
|
+
}
|
|
44
|
+
/** Parse an `S: Manufacturer=…` style line into its `[key, value]` pair. */
|
|
45
|
+
function stringField(line) {
|
|
46
|
+
const eq = line.indexOf('=');
|
|
47
|
+
if (eq < 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const key = line.slice(3, eq).trim();
|
|
51
|
+
const value = line.slice(eq + 1).trim();
|
|
52
|
+
return key === '' || value === '' ? undefined : [key, value];
|
|
53
|
+
}
|
|
54
|
+
function emptyAccumulator() {
|
|
55
|
+
return { interfaces: [] };
|
|
56
|
+
}
|
|
57
|
+
function finish(acc, out) {
|
|
58
|
+
// A record with no `P:` line is not a device — never synthesise an identity for it.
|
|
59
|
+
if (acc.vidPid === undefined) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
out.push({
|
|
63
|
+
vidPid: acc.vidPid,
|
|
64
|
+
bDeviceClass: acc.bDeviceClass ?? 0,
|
|
65
|
+
interfaces: acc.interfaces,
|
|
66
|
+
...(acc.manufacturer !== undefined ? { manufacturer: acc.manufacturer } : {}),
|
|
67
|
+
...(acc.product !== undefined ? { product: acc.product } : {}),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function applyProductLine(line, acc) {
|
|
71
|
+
const vendor = field(line, 'Vendor');
|
|
72
|
+
const product = field(line, 'ProdID');
|
|
73
|
+
if (vendor !== undefined && product !== undefined) {
|
|
74
|
+
acc.vidPid = `${vendor.toLowerCase()}:${product.toLowerCase()}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function applyStringLine(line, acc) {
|
|
78
|
+
const pair = stringField(line);
|
|
79
|
+
if (pair === undefined) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const [key, value] = pair;
|
|
83
|
+
if (key === 'Manufacturer') {
|
|
84
|
+
acc.manufacturer = value;
|
|
85
|
+
}
|
|
86
|
+
else if (key === 'Product') {
|
|
87
|
+
acc.product = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function applyInterfaceLine(line, acc) {
|
|
91
|
+
const interfaceClass = hexByte(line, 'Cls');
|
|
92
|
+
const interfaceSubClass = hexByte(line, 'Sub');
|
|
93
|
+
const interfaceProtocol = hexByte(line, 'Prot');
|
|
94
|
+
if (interfaceClass === undefined ||
|
|
95
|
+
interfaceSubClass === undefined ||
|
|
96
|
+
interfaceProtocol === undefined) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const driver = field(line, 'Driver');
|
|
100
|
+
acc.interfaces.push({
|
|
101
|
+
interfaceClass,
|
|
102
|
+
interfaceSubClass,
|
|
103
|
+
interfaceProtocol,
|
|
104
|
+
...(driver !== undefined && driver !== '(none)' ? { driver } : {}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse `usb-devices` output into one record per device. Pure and total: malformed
|
|
109
|
+
* lines are skipped rather than guessed at, and a block with no `P:` line yields no
|
|
110
|
+
* record (it has no identity, so inventing one would be a lie).
|
|
111
|
+
*/
|
|
112
|
+
export function parseUsbDevices(text) {
|
|
113
|
+
const out = [];
|
|
114
|
+
let acc = emptyAccumulator();
|
|
115
|
+
for (const line of text.split('\n')) {
|
|
116
|
+
// A `T:` line opens a new device record; `usb-devices` also blank-line-separates
|
|
117
|
+
// them, but the topology line is the reliable delimiter (blank lines are optional
|
|
118
|
+
// in some kernels' output).
|
|
119
|
+
if (line.startsWith('T:')) {
|
|
120
|
+
finish(acc, out);
|
|
121
|
+
acc = emptyAccumulator();
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (line.startsWith('D:')) {
|
|
125
|
+
const deviceClass = hexByte(line, 'Cls');
|
|
126
|
+
if (deviceClass !== undefined) {
|
|
127
|
+
acc.bDeviceClass = deviceClass;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (line.startsWith('P:')) {
|
|
131
|
+
applyProductLine(line, acc);
|
|
132
|
+
}
|
|
133
|
+
else if (line.startsWith('S:')) {
|
|
134
|
+
applyStringLine(line, acc);
|
|
135
|
+
}
|
|
136
|
+
else if (line.startsWith('I:')) {
|
|
137
|
+
applyInterfaceLine(line, acc);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finish(acc, out);
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Find the single device matching `vidPid` in a parsed capture. Returns `undefined`
|
|
145
|
+
* when there is NO match, and — deliberately — also when there is more than one: a
|
|
146
|
+
* duplicate VID:PID (this bench has two identical Huawei HiLink units) makes the
|
|
147
|
+
* selection ambiguous, and an ambiguous selection must refuse rather than pick the
|
|
148
|
+
* first. The caller turns that into a typed refusal.
|
|
149
|
+
*/
|
|
150
|
+
export function selectUniqueDevice(devices, vidPid) {
|
|
151
|
+
const matches = devices.filter((d) => d.vidPid === vidPid);
|
|
152
|
+
const only = matches[0];
|
|
153
|
+
if (matches.length === 1 && only !== undefined) {
|
|
154
|
+
return { device: only };
|
|
155
|
+
}
|
|
156
|
+
return { ambiguousMatches: matches.length };
|
|
157
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DbusTransport } from '../transport/index.js';
|
|
2
|
+
import type { UssdRepliedState } from './session.js';
|
|
3
|
+
/** `MMModem3gppUssdSessionState`. */
|
|
4
|
+
export declare const USSD_STATE_UNKNOWN = 0;
|
|
5
|
+
export declare const USSD_STATE_IDLE = 1;
|
|
6
|
+
export declare const USSD_STATE_ACTIVE = 2;
|
|
7
|
+
export declare const USSD_STATE_USER_RESPONSE = 3;
|
|
8
|
+
/**
|
|
9
|
+
* Decode MM's post-call session state. `UNKNOWN` folds onto `released` with the
|
|
10
|
+
* rest: an unreadable state must close the session rather than leave the operator
|
|
11
|
+
* looking at a dialogue nothing can advance.
|
|
12
|
+
*/
|
|
13
|
+
export declare function decodeRepliedState(state: number): UssdRepliedState;
|
|
14
|
+
export interface UssdCallTarget {
|
|
15
|
+
readonly transport: DbusTransport;
|
|
16
|
+
readonly destination: string;
|
|
17
|
+
readonly modem: string;
|
|
18
|
+
readonly timeoutMs: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function callInitiate(target: UssdCallTarget, ussdCommand: string): Promise<string>;
|
|
21
|
+
export declare function callRespond(target: UssdCallTarget, ussdResponse: string): Promise<string>;
|
|
22
|
+
export declare function callCancel(target: UssdCallTarget): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Read the post-call session state.
|
|
25
|
+
*
|
|
26
|
+
* A targeted `Properties.Get` rather than a `GetManagedObjects` sweep: this runs
|
|
27
|
+
* after every network round-trip, and the whole-tree read is the most expensive
|
|
28
|
+
* call in the package. `unknown` on anything unreadable — the caller treats that
|
|
29
|
+
* as "the network released the session", which is the conservative direction (it
|
|
30
|
+
* closes a session rather than leaving one the operator cannot see).
|
|
31
|
+
*/
|
|
32
|
+
export declare function readUssdState(target: UssdCallTarget): Promise<number>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The four D-Bus calls the USSD adapter makes, and nothing else.
|
|
2
|
+
//
|
|
3
|
+
// Split from the adapter so the session machinery above can be read without the
|
|
4
|
+
// marshalling below, and so a call's shape (interface, member, signature) is
|
|
5
|
+
// stated once in one place.
|
|
6
|
+
//
|
|
7
|
+
// CARRIER TEXT DISCIPLINE: `Initiate` and `Respond` both take and return operator
|
|
8
|
+
// text, and neither the command nor the reply may ever reach a log line. Nothing
|
|
9
|
+
// in this file logs, and every error raised here is re-thrown untouched so the
|
|
10
|
+
// classifier — not a string built around the payload — decides what the caller is
|
|
11
|
+
// told.
|
|
12
|
+
import { MODEM3GPP_USSD_IFACE, PROPERTIES_IFACE } from '../backend/constants.js';
|
|
13
|
+
/** `MMModem3gppUssdSessionState`. */
|
|
14
|
+
export const USSD_STATE_UNKNOWN = 0;
|
|
15
|
+
export const USSD_STATE_IDLE = 1;
|
|
16
|
+
export const USSD_STATE_ACTIVE = 2;
|
|
17
|
+
export const USSD_STATE_USER_RESPONSE = 3;
|
|
18
|
+
/**
|
|
19
|
+
* Decode MM's post-call session state. `UNKNOWN` folds onto `released` with the
|
|
20
|
+
* rest: an unreadable state must close the session rather than leave the operator
|
|
21
|
+
* looking at a dialogue nothing can advance.
|
|
22
|
+
*/
|
|
23
|
+
export function decodeRepliedState(state) {
|
|
24
|
+
if (state === USSD_STATE_USER_RESPONSE) {
|
|
25
|
+
return 'awaiting-reply';
|
|
26
|
+
}
|
|
27
|
+
return state === USSD_STATE_ACTIVE ? 'active' : 'released';
|
|
28
|
+
}
|
|
29
|
+
function firstString(body) {
|
|
30
|
+
const value = body[0];
|
|
31
|
+
return typeof value === 'string' ? value : '';
|
|
32
|
+
}
|
|
33
|
+
export async function callInitiate(target, ussdCommand) {
|
|
34
|
+
const reply = await target.transport.callMethod({
|
|
35
|
+
destination: target.destination,
|
|
36
|
+
path: target.modem,
|
|
37
|
+
interface: MODEM3GPP_USSD_IFACE,
|
|
38
|
+
member: 'Initiate',
|
|
39
|
+
signature: 's',
|
|
40
|
+
args: [ussdCommand],
|
|
41
|
+
timeoutMs: target.timeoutMs,
|
|
42
|
+
});
|
|
43
|
+
return firstString(reply.body);
|
|
44
|
+
}
|
|
45
|
+
export async function callRespond(target, ussdResponse) {
|
|
46
|
+
const reply = await target.transport.callMethod({
|
|
47
|
+
destination: target.destination,
|
|
48
|
+
path: target.modem,
|
|
49
|
+
interface: MODEM3GPP_USSD_IFACE,
|
|
50
|
+
member: 'Respond',
|
|
51
|
+
signature: 's',
|
|
52
|
+
args: [ussdResponse],
|
|
53
|
+
timeoutMs: target.timeoutMs,
|
|
54
|
+
});
|
|
55
|
+
return firstString(reply.body);
|
|
56
|
+
}
|
|
57
|
+
export async function callCancel(target) {
|
|
58
|
+
await target.transport.callMethod({
|
|
59
|
+
destination: target.destination,
|
|
60
|
+
path: target.modem,
|
|
61
|
+
interface: MODEM3GPP_USSD_IFACE,
|
|
62
|
+
member: 'Cancel',
|
|
63
|
+
timeoutMs: target.timeoutMs,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read the post-call session state.
|
|
68
|
+
*
|
|
69
|
+
* A targeted `Properties.Get` rather than a `GetManagedObjects` sweep: this runs
|
|
70
|
+
* after every network round-trip, and the whole-tree read is the most expensive
|
|
71
|
+
* call in the package. `unknown` on anything unreadable — the caller treats that
|
|
72
|
+
* as "the network released the session", which is the conservative direction (it
|
|
73
|
+
* closes a session rather than leaving one the operator cannot see).
|
|
74
|
+
*/
|
|
75
|
+
export async function readUssdState(target) {
|
|
76
|
+
try {
|
|
77
|
+
const reply = await target.transport.callMethod({
|
|
78
|
+
destination: target.destination,
|
|
79
|
+
path: target.modem,
|
|
80
|
+
interface: PROPERTIES_IFACE,
|
|
81
|
+
member: 'Get',
|
|
82
|
+
signature: 'ss',
|
|
83
|
+
args: [MODEM3GPP_USSD_IFACE, 'State'],
|
|
84
|
+
timeoutMs: target.timeoutMs,
|
|
85
|
+
});
|
|
86
|
+
const wrapped = reply.body[0];
|
|
87
|
+
if (typeof wrapped === 'number') {
|
|
88
|
+
return wrapped;
|
|
89
|
+
}
|
|
90
|
+
const inner = wrapped?.value;
|
|
91
|
+
return typeof inner === 'number' ? inner : USSD_STATE_UNKNOWN;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return USSD_STATE_UNKNOWN;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { callCancel, callInitiate, callRespond, decodeRepliedState, readUssdState, USSD_STATE_ACTIVE, USSD_STATE_IDLE, USSD_STATE_UNKNOWN, USSD_STATE_USER_RESPONSE, type UssdCallTarget, } from './calls.js';
|
|
2
|
+
export { MmUssd, type MmUssdDeps, type UssdScheduler, type UssdTimerHandle, type UssdVerbResult, } from './mm-ussd.js';
|
|
3
|
+
export { classifyUssdFailure, isPacketSwitchedOnly, USSD_REFUSAL_REASONS, type UssdRefusalReason, type UssdRegistrationFacts, } from './refusal.js';
|
|
4
|
+
export { decodeAccessTechnologies, readUssdRegistrationFacts, registrationFactsFromTree, UNKNOWN_REGISTRATION, } from './registration.js';
|
|
5
|
+
export { IDLE_SESSION, isUssdSessionOpen, reduceUssdSession, USSD_SESSION_OUTCOMES, USSD_SESSION_STATES, type UssdRepliedState, type UssdSessionEvent, type UssdSessionOutcome, type UssdSessionSnapshot, type UssdSessionState, type UssdTransition, } from './session.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The gated USSD capability module — session state machine, refusal taxonomy, and
|
|
2
|
+
// the ModemManager `Modem3gpp.Ussd` adapter.
|
|
3
|
+
//
|
|
4
|
+
// GATED: nothing here runs unless the operator has enabled the `ussd` capability
|
|
5
|
+
// module and the modem positively advertises the interface (`../capability`).
|
|
6
|
+
// LEASE-ONLY: a USSD session cannot re-register the radio, so it takes the
|
|
7
|
+
// per-modem mutation lease and is NOT journaled.
|
|
8
|
+
export { callCancel, callInitiate, callRespond, decodeRepliedState, readUssdState, USSD_STATE_ACTIVE, USSD_STATE_IDLE, USSD_STATE_UNKNOWN, USSD_STATE_USER_RESPONSE, } from './calls.js';
|
|
9
|
+
export { MmUssd, } from './mm-ussd.js';
|
|
10
|
+
export { classifyUssdFailure, isPacketSwitchedOnly, USSD_REFUSAL_REASONS, } from './refusal.js';
|
|
11
|
+
export { decodeAccessTechnologies, readUssdRegistrationFacts, registrationFactsFromTree, UNKNOWN_REGISTRATION, } from './registration.js';
|
|
12
|
+
export { IDLE_SESSION, isUssdSessionOpen, reduceUssdSession, USSD_SESSION_OUTCOMES, USSD_SESSION_STATES, } from './session.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ModemActor } from '../backend/modem-actor.js';
|
|
2
|
+
import type { ModemRef } from '../ports/index.js';
|
|
3
|
+
import type { DbusTransport } from '../transport/index.js';
|
|
4
|
+
import { type UssdRefusalReason } from './refusal.js';
|
|
5
|
+
import { type UssdSessionSnapshot } from './session.js';
|
|
6
|
+
export interface UssdTimerHandle {
|
|
7
|
+
cancel(): void;
|
|
8
|
+
}
|
|
9
|
+
export type UssdScheduler = (delayMs: number, run: () => void) => UssdTimerHandle;
|
|
10
|
+
export interface UssdVerbResult {
|
|
11
|
+
readonly ok: boolean;
|
|
12
|
+
readonly snapshot: UssdSessionSnapshot;
|
|
13
|
+
/** Carrier text. Redacted by key everywhere it is serialized. */
|
|
14
|
+
readonly ussdReply?: string;
|
|
15
|
+
readonly refusal?: UssdRefusalReason;
|
|
16
|
+
}
|
|
17
|
+
export interface MmUssdDeps {
|
|
18
|
+
readonly transport: DbusTransport;
|
|
19
|
+
readonly actor: ModemActor;
|
|
20
|
+
readonly destination?: string;
|
|
21
|
+
readonly resolveStableKey: (modem: ModemRef) => string;
|
|
22
|
+
readonly callTimeoutMs?: number;
|
|
23
|
+
readonly sessionIdleTimeoutMs?: number;
|
|
24
|
+
readonly scheduler?: UssdScheduler;
|
|
25
|
+
/** Notified on every stored-state change, so a UI can follow a session. */
|
|
26
|
+
readonly onSessionChange?: (stableKey: string, snapshot: UssdSessionSnapshot) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare class MmUssd {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(deps: MmUssdDeps);
|
|
31
|
+
snapshot(modem: ModemRef): UssdSessionSnapshot;
|
|
32
|
+
initiate(modem: ModemRef, ussdCommand: string): Promise<UssdVerbResult>;
|
|
33
|
+
respond(modem: ModemRef, ussdResponse: string): Promise<UssdVerbResult>;
|
|
34
|
+
cancel(modem: ModemRef): Promise<UssdVerbResult>;
|
|
35
|
+
/** Drop every timer. A live session on the modem is NOT cancelled by this. */
|
|
36
|
+
stop(): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// The ModemManager USSD adapter — `Modem3gpp.Ussd` driven by the pure session
|
|
2
|
+
// machine in `./session`.
|
|
3
|
+
//
|
|
4
|
+
// Three properties are load-bearing and none of them is obvious from the D-Bus
|
|
5
|
+
// API alone:
|
|
6
|
+
//
|
|
7
|
+
// 1. **Every verb runs through the shared per-modem `ModemActor`.** A USSD
|
|
8
|
+
// session is a single network-side resource per subscriber, so two verbs
|
|
9
|
+
// interleaving on one modem is exactly the double-dialogue the network
|
|
10
|
+
// answers busy. The actor is keyed on the STABLE key, so the serialization
|
|
11
|
+
// survives a replug like every other disruptive op in this package.
|
|
12
|
+
//
|
|
13
|
+
// 2. **An unanswered session is closed at a bound, and the CANCEL is attempted.**
|
|
14
|
+
// A session nobody responds to stays open NETWORK-side; letting it expire in
|
|
15
|
+
// silence would leave the next `Initiate` failing busy for reasons the
|
|
16
|
+
// operator cannot see. The bound closes our machine and best-effort releases
|
|
17
|
+
// the network's — best-effort because a modem that did not answer the last
|
|
18
|
+
// call may not answer this one either, and a timeout must still terminate.
|
|
19
|
+
//
|
|
20
|
+
// 3. **A `closed` session resets the STORED state to idle, while the CALLER is
|
|
21
|
+
// told `closed`.** The machine is deliberately terminal so a finished session
|
|
22
|
+
// cannot be resurrected; the adapter's map is per-modem and long-lived, so it
|
|
23
|
+
// starts each new dialogue from a fresh machine.
|
|
24
|
+
//
|
|
25
|
+
// The carrier's text rides `ussdReply` and NOTHING here logs it. That field name
|
|
26
|
+
// is not cosmetic: `../redact` is key-based, so the name IS what guarantees the
|
|
27
|
+
// value is masked in every receipt, bundle, and log line it can reach.
|
|
28
|
+
import { MM_BUS_NAME } from '../backend/constants.js';
|
|
29
|
+
import { callCancel, callInitiate, callRespond, decodeRepliedState, readUssdState, } from './calls.js';
|
|
30
|
+
import { classifyUssdFailure } from './refusal.js';
|
|
31
|
+
import { readUssdRegistrationFacts } from './registration.js';
|
|
32
|
+
import { IDLE_SESSION, reduceUssdSession, } from './session.js';
|
|
33
|
+
/** A network round-trip. USSD legitimately takes tens of seconds. */
|
|
34
|
+
const DEFAULT_CALL_TIMEOUT_MS = 45_000;
|
|
35
|
+
/** How long a session may sit awaiting an operator response before it is closed. */
|
|
36
|
+
const DEFAULT_SESSION_IDLE_TIMEOUT_MS = 120_000;
|
|
37
|
+
const defaultScheduler = (delayMs, run) => {
|
|
38
|
+
const timer = setTimeout(run, delayMs);
|
|
39
|
+
timer.unref?.();
|
|
40
|
+
return {
|
|
41
|
+
cancel: () => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export class MmUssd {
|
|
47
|
+
#deps;
|
|
48
|
+
#destination;
|
|
49
|
+
#callTimeoutMs;
|
|
50
|
+
#idleTimeoutMs;
|
|
51
|
+
#scheduler;
|
|
52
|
+
#sessions = new Map();
|
|
53
|
+
#timers = new Map();
|
|
54
|
+
constructor(deps) {
|
|
55
|
+
this.#deps = deps;
|
|
56
|
+
this.#destination = deps.destination ?? MM_BUS_NAME;
|
|
57
|
+
this.#callTimeoutMs = deps.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
|
|
58
|
+
this.#idleTimeoutMs = deps.sessionIdleTimeoutMs ?? DEFAULT_SESSION_IDLE_TIMEOUT_MS;
|
|
59
|
+
this.#scheduler = deps.scheduler ?? defaultScheduler;
|
|
60
|
+
}
|
|
61
|
+
snapshot(modem) {
|
|
62
|
+
return this.#sessions.get(this.#deps.resolveStableKey(modem)) ?? IDLE_SESSION;
|
|
63
|
+
}
|
|
64
|
+
initiate(modem, ussdCommand) {
|
|
65
|
+
return this.#runVerb(modem, { kind: 'initiate' }, (target) => callInitiate(target, ussdCommand));
|
|
66
|
+
}
|
|
67
|
+
respond(modem, ussdResponse) {
|
|
68
|
+
return this.#runVerb(modem, { kind: 'respond' }, (target) => callRespond(target, ussdResponse));
|
|
69
|
+
}
|
|
70
|
+
cancel(modem) {
|
|
71
|
+
const key = this.#deps.resolveStableKey(modem);
|
|
72
|
+
return this.#deps.actor.run(key, async () => {
|
|
73
|
+
const gate = this.#gate(key, { kind: 'cancel' });
|
|
74
|
+
if (!gate.ok) {
|
|
75
|
+
return gate.result;
|
|
76
|
+
}
|
|
77
|
+
const target = this.#target(modem);
|
|
78
|
+
try {
|
|
79
|
+
await callCancel(target);
|
|
80
|
+
return this.#settle(key, { kind: 'cancelled' });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return this.#fail(key, modem, error);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Drop every timer. A live session on the modem is NOT cancelled by this. */
|
|
88
|
+
stop() {
|
|
89
|
+
for (const timer of this.#timers.values()) {
|
|
90
|
+
timer.cancel();
|
|
91
|
+
}
|
|
92
|
+
this.#timers.clear();
|
|
93
|
+
}
|
|
94
|
+
async #runVerb(modem, event, dispatch) {
|
|
95
|
+
const key = this.#deps.resolveStableKey(modem);
|
|
96
|
+
return this.#deps.actor.run(key, async () => {
|
|
97
|
+
const gate = this.#gate(key, event);
|
|
98
|
+
if (!gate.ok) {
|
|
99
|
+
return gate.result;
|
|
100
|
+
}
|
|
101
|
+
const target = this.#target(modem);
|
|
102
|
+
try {
|
|
103
|
+
const ussdReply = await dispatch(target);
|
|
104
|
+
const sessionState = decodeRepliedState(await readUssdState(target));
|
|
105
|
+
const settled = this.#settle(key, { kind: 'replied', sessionState });
|
|
106
|
+
// A session the network kept open — whether or not it asked a
|
|
107
|
+
// question — is what the idle bound exists to release.
|
|
108
|
+
if (sessionState !== 'released') {
|
|
109
|
+
this.#armIdleTimeout(key, modem);
|
|
110
|
+
}
|
|
111
|
+
return { ...settled, ussdReply };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return this.#fail(key, modem, error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Apply the operator's verb to the machine. A refusal is returned WITHOUT
|
|
120
|
+
* touching the stored state or dialling the bus — a doomed verb must not
|
|
121
|
+
* disturb a live session.
|
|
122
|
+
*/
|
|
123
|
+
#gate(key, event) {
|
|
124
|
+
const current = this.#sessions.get(key) ?? IDLE_SESSION;
|
|
125
|
+
const transition = reduceUssdSession(current, event);
|
|
126
|
+
if (!transition.ok) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
result: { ok: false, snapshot: current, refusal: transition.refusal },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
this.#store(key, transition.snapshot);
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
135
|
+
#settle(key, event) {
|
|
136
|
+
const current = this.#sessions.get(key) ?? IDLE_SESSION;
|
|
137
|
+
const transition = reduceUssdSession(current, event);
|
|
138
|
+
if (!transition.ok) {
|
|
139
|
+
return { ok: false, snapshot: current, refusal: transition.refusal };
|
|
140
|
+
}
|
|
141
|
+
this.#store(key, transition.snapshot);
|
|
142
|
+
const closedRefusal = transition.snapshot.refusal;
|
|
143
|
+
return {
|
|
144
|
+
ok: closedRefusal === undefined,
|
|
145
|
+
snapshot: transition.snapshot,
|
|
146
|
+
...(closedRefusal === undefined ? {} : { refusal: closedRefusal }),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async #fail(key, modem, error) {
|
|
150
|
+
const registration = await readUssdRegistrationFacts(this.#deps.transport, this.#destination, modem);
|
|
151
|
+
return this.#settle(key, { kind: 'failed', reason: classifyUssdFailure(error, registration) });
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A session reaching `closed` is REPORTED as closed and STORED as idle, so the
|
|
155
|
+
* next `initiate` starts from a fresh machine rather than the terminal one.
|
|
156
|
+
*/
|
|
157
|
+
#store(key, snapshot) {
|
|
158
|
+
this.#clearTimer(key);
|
|
159
|
+
if (snapshot.state === 'closed') {
|
|
160
|
+
this.#sessions.delete(key);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.#sessions.set(key, snapshot);
|
|
164
|
+
}
|
|
165
|
+
this.#deps.onSessionChange?.(key, snapshot);
|
|
166
|
+
}
|
|
167
|
+
#armIdleTimeout(key, modem) {
|
|
168
|
+
this.#clearTimer(key);
|
|
169
|
+
this.#timers.set(key, this.#scheduler(this.#idleTimeoutMs, () => {
|
|
170
|
+
void this.#expire(key, modem);
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
#clearTimer(key) {
|
|
174
|
+
this.#timers.get(key)?.cancel();
|
|
175
|
+
this.#timers.delete(key);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The bound elapsed. The machine closes `timed-out` FIRST — that outcome is
|
|
179
|
+
* the operator's answer whether or not the release lands — and the modem-side
|
|
180
|
+
* cancel is attempted afterwards, best-effort.
|
|
181
|
+
*/
|
|
182
|
+
async #expire(key, modem) {
|
|
183
|
+
await this.#deps.actor.run(key, async () => {
|
|
184
|
+
if (!this.#sessions.has(key)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
this.#settle(key, { kind: 'timeout' });
|
|
188
|
+
try {
|
|
189
|
+
await callCancel(this.#target(modem));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// The modem that did not answer the dialogue may not answer this
|
|
193
|
+
// either; the session is already closed on our side.
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
#target(modem) {
|
|
198
|
+
return {
|
|
199
|
+
transport: this.#deps.transport,
|
|
200
|
+
destination: this.#destination,
|
|
201
|
+
modem,
|
|
202
|
+
timeoutMs: this.#callTimeoutMs,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|