@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
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
// The transition transaction — the safety-critical core. This proves:
|
|
2
|
-
// - the happy path walks all TEN steps in order and yields a NEW ifname on the SAME
|
|
3
|
-
// stableKey;
|
|
4
|
-
// - the THREE-TIER negative matrix: TIER A entry refusals fire ZERO actor/lease/AT
|
|
5
|
-
// calls; TIER B an in-actor race is caught after exactly one actor entry with zero
|
|
6
|
-
// lease/AT; TIER C a postcondition mismatch fails degraded, never reactivates, and
|
|
7
|
-
// still releases the interlock via finally;
|
|
8
|
-
// - a crash mid-transaction trips the watchdog, force-uninhibits, and returns
|
|
9
|
-
// degraded rather than hanging forever.
|
|
10
|
-
|
|
11
|
-
import { describe, expect, test } from 'bun:test';
|
|
12
|
-
import { epochMillis } from '../domain';
|
|
13
|
-
import { connectionId, deviceIfname, type NetworkManagerPort, receipt } from '../ports';
|
|
14
|
-
import type { UsbDeviceSnapshot } from './device-classifier';
|
|
15
|
-
import { ModemActor } from './modem-actor';
|
|
16
|
-
import type { TransitionInterlock, UsbModeTransitionRequest } from './transition-preconditions';
|
|
17
|
-
import { UsbModeTransition, type UsbModeTransitionDeps } from './usb-mode-transition';
|
|
18
|
-
|
|
19
|
-
const CACHED_UID = 'pci-0000:00-usb-0:1';
|
|
20
|
-
const SKU = {
|
|
21
|
-
vidPid: '2c7c:0125',
|
|
22
|
-
model: 'CERALIVE-SYNTHETIC-TEST-SKU',
|
|
23
|
-
firmwarePrefix: 'SYNTHETICFW01',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const TEN_STEPS = [
|
|
27
|
-
'nm-quiesce',
|
|
28
|
-
'inhibit',
|
|
29
|
-
'at-command',
|
|
30
|
-
'await-port-drop',
|
|
31
|
-
'uninhibit',
|
|
32
|
-
'await-reenumeration',
|
|
33
|
-
'postcondition',
|
|
34
|
-
'resolve-ifname',
|
|
35
|
-
'reactivate',
|
|
36
|
-
'release-interlock',
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
const OLD_QMI: UsbDeviceSnapshot = {
|
|
40
|
-
vendorId: '2c7c',
|
|
41
|
-
productId: '0125',
|
|
42
|
-
bDeviceClass: 0,
|
|
43
|
-
physicalUid: CACHED_UID,
|
|
44
|
-
ifname: 'wwan0',
|
|
45
|
-
interfaces: [
|
|
46
|
-
{ interfaceClass: 0xff, interfaceSubClass: 0xff, interfaceProtocol: 0xff, driver: 'qmi_wwan' },
|
|
47
|
-
],
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/** Re-enumerated as MBIM (target), same physical UID, NEW ifname — matches the catalog. */
|
|
51
|
-
const NEW_MBIM: UsbDeviceSnapshot = {
|
|
52
|
-
vendorId: '2c7c',
|
|
53
|
-
productId: '0126',
|
|
54
|
-
bDeviceClass: 0,
|
|
55
|
-
physicalUid: CACHED_UID,
|
|
56
|
-
ifname: 'wwan1',
|
|
57
|
-
interfaces: [
|
|
58
|
-
{ interfaceClass: 0x02, interfaceSubClass: 0x0e, interfaceProtocol: 0x00, driver: 'cdc_mbim' },
|
|
59
|
-
{ interfaceClass: 0x0a, interfaceSubClass: 0x00, interfaceProtocol: 0x02, driver: 'cdc_mbim' },
|
|
60
|
-
],
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/** Re-enumerated STILL as QMI — the postcondition-mismatch device (same UID). */
|
|
64
|
-
const STILL_QMI: UsbDeviceSnapshot = { ...OLD_QMI, ifname: 'wwan0' };
|
|
65
|
-
|
|
66
|
-
interface SpyLog {
|
|
67
|
-
readonly calls: string[];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function makeNm(log: SpyLog): NetworkManagerPort {
|
|
71
|
-
const rcpt = () => receipt('connection', 'applied', 'ok');
|
|
72
|
-
return {
|
|
73
|
-
createGsmProfile: () => Promise.reject(new Error('unused')),
|
|
74
|
-
readGsmProfile: () => Promise.resolve(undefined),
|
|
75
|
-
updateGsmProfile: () => Promise.reject(new Error('unused')),
|
|
76
|
-
deleteGsmProfile: () => Promise.resolve(),
|
|
77
|
-
activate: (_id, ifname) => {
|
|
78
|
-
log.calls.push(`nm.activate:${ifname}`);
|
|
79
|
-
return Promise.resolve(rcpt());
|
|
80
|
-
},
|
|
81
|
-
deactivate: () => Promise.resolve(rcpt()),
|
|
82
|
-
acquireQuiesceLease: (id, ifname) => {
|
|
83
|
-
log.calls.push('nm.acquireQuiesceLease');
|
|
84
|
-
return Promise.resolve({
|
|
85
|
-
connectionId: id,
|
|
86
|
-
deviceIfname: ifname,
|
|
87
|
-
acquiredAt: epochMillis(0),
|
|
88
|
-
});
|
|
89
|
-
},
|
|
90
|
-
releaseQuiesceLease: () => {
|
|
91
|
-
log.calls.push('nm.releaseQuiesceLease');
|
|
92
|
-
return Promise.resolve();
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function makeMm(log: SpyLog): Pick<UsbModeTransitionDeps['modemManager'], 'inhibit' | 'uninhibit'> {
|
|
98
|
-
return {
|
|
99
|
-
inhibit: (uid) => {
|
|
100
|
-
log.calls.push(`mm.inhibit:${uid}`);
|
|
101
|
-
return Promise.resolve({ uid, acquiredAt: epochMillis(0) });
|
|
102
|
-
},
|
|
103
|
-
uninhibit: () => {
|
|
104
|
-
log.calls.push('mm.uninhibit');
|
|
105
|
-
return Promise.resolve();
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function okSender(log: SpyLog): UsbModeTransitionDeps['atSender'] {
|
|
111
|
-
return {
|
|
112
|
-
send: (command) => {
|
|
113
|
-
log.calls.push(`at.send:${command}`);
|
|
114
|
-
return Promise.resolve({ ok: true, raw: 'OK' });
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function scriptedEnumerate(
|
|
120
|
-
frames: UsbDeviceSnapshot[][],
|
|
121
|
-
): () => Promise<readonly UsbDeviceSnapshot[]> {
|
|
122
|
-
let i = 0;
|
|
123
|
-
return () => {
|
|
124
|
-
const frame = frames[Math.min(i, frames.length - 1)] ?? [];
|
|
125
|
-
i += 1;
|
|
126
|
-
return Promise.resolve(frame);
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function makeRequest(overrides: Partial<UsbModeTransitionRequest> = {}): UsbModeTransitionRequest {
|
|
131
|
-
return {
|
|
132
|
-
stableKey: 'slot:test',
|
|
133
|
-
sku: SKU,
|
|
134
|
-
fromMode: 'qmi',
|
|
135
|
-
toMode: 'mbim',
|
|
136
|
-
connectionId: connectionId('uuid-1'),
|
|
137
|
-
deviceIfname: deviceIfname('wwan0'),
|
|
138
|
-
cachedPhysicalUid: CACHED_UID,
|
|
139
|
-
inhibitUid: CACHED_UID,
|
|
140
|
-
confirm: true,
|
|
141
|
-
maintenance: true,
|
|
142
|
-
now: epochMillis(0),
|
|
143
|
-
probeReadiness: () => Promise.resolve({ identityConfidence: 'high' }),
|
|
144
|
-
...overrides,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function makeTransition(
|
|
149
|
-
log: SpyLog,
|
|
150
|
-
overrides: Partial<UsbModeTransitionDeps> = {},
|
|
151
|
-
): UsbModeTransition {
|
|
152
|
-
return new UsbModeTransition({
|
|
153
|
-
actor: new ModemActor(),
|
|
154
|
-
nm: makeNm(log),
|
|
155
|
-
modemManager: makeMm(log),
|
|
156
|
-
atSender: okSender(log),
|
|
157
|
-
enumerate: scriptedEnumerate([[OLD_QMI], [], [NEW_MBIM]]),
|
|
158
|
-
reenumerationTimeoutMs: 500,
|
|
159
|
-
pollIntervalMs: 1,
|
|
160
|
-
watchdogMs: 200,
|
|
161
|
-
...overrides,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
describe('UsbModeTransition — the happy path walks all ten steps', () => {
|
|
166
|
-
test('a certified qmi→mbim transition succeeds with a NEW ifname on the same stableKey', async () => {
|
|
167
|
-
const log: SpyLog = { calls: [] };
|
|
168
|
-
const transition = makeTransition(log);
|
|
169
|
-
const request = makeRequest();
|
|
170
|
-
const outcome = await transition.execute(request);
|
|
171
|
-
|
|
172
|
-
expect(outcome.status).toBe('succeeded');
|
|
173
|
-
if (outcome.status !== 'succeeded') {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
// The row survives with the SAME stableKey but a NEW ifname.
|
|
177
|
-
expect(request.stableKey).toBe('slot:test');
|
|
178
|
-
expect(String(outcome.newIfname)).toBe('wwan1');
|
|
179
|
-
expect(String(request.deviceIfname)).toBe('wwan0');
|
|
180
|
-
// All ten goal steps, in order.
|
|
181
|
-
expect(outcome.steps.filter((s) => TEN_STEPS.includes(s))).toEqual(TEN_STEPS);
|
|
182
|
-
// The catalog command was sent exactly once; reactivation targeted the new ifname.
|
|
183
|
-
expect(log.calls.filter((c) => c.startsWith('at.send:'))).toEqual([
|
|
184
|
-
'at.send:AT+QCFG="usbnet",2',
|
|
185
|
-
]);
|
|
186
|
-
expect(log.calls).toContain('nm.activate:wwan1');
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
/** No nm/mm/at side-effecting call ran, and the actor was never entered. */
|
|
191
|
-
function expectZeroSideEffects(log: SpyLog, steps: readonly string[]): void {
|
|
192
|
-
expect(log.calls).toEqual([]);
|
|
193
|
-
expect(steps).not.toContain('actor-enter');
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
describe('UsbModeTransition — TIER A: entry refusals fire ZERO actor/lease/AT calls', () => {
|
|
197
|
-
const cases: Array<[string, Partial<UsbModeTransitionRequest>]> = [
|
|
198
|
-
['unconfirmed (confirm:false)', { confirm: false }],
|
|
199
|
-
['uncertified SKU (no catalog entry)', { sku: { ...SKU, firmwarePrefix: 'UNKNOWNFW' } }],
|
|
200
|
-
[
|
|
201
|
-
'non-permitted transition (mbim→ecm-ncm not certified)',
|
|
202
|
-
{ fromMode: 'mbim', toMode: 'ecm-ncm' },
|
|
203
|
-
],
|
|
204
|
-
[
|
|
205
|
-
'ambiguous / low-confidence identity',
|
|
206
|
-
{ probeReadiness: () => Promise.resolve({ identityConfidence: 'low' }) },
|
|
207
|
-
],
|
|
208
|
-
['missing maintenance flag', { maintenance: false }],
|
|
209
|
-
];
|
|
210
|
-
for (const [name, overrides] of cases) {
|
|
211
|
-
test(name, async () => {
|
|
212
|
-
const log: SpyLog = { calls: [] };
|
|
213
|
-
const transition = makeTransition(log);
|
|
214
|
-
const outcome = await transition.execute(makeRequest(overrides));
|
|
215
|
-
expect(outcome.status).toBe('refused');
|
|
216
|
-
if (outcome.status === 'refused') {
|
|
217
|
-
expect(outcome.stage).toBe('entry');
|
|
218
|
-
}
|
|
219
|
-
expectZeroSideEffects(log, outcome.steps);
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
test('interlock already held → refused at entry, hold never acquired', async () => {
|
|
224
|
-
const log: SpyLog = { calls: [] };
|
|
225
|
-
const heldInterlock: TransitionInterlock = {
|
|
226
|
-
canDisrupt: () => Promise.resolve({ allow: false, reason: 'a stream is admitted' }),
|
|
227
|
-
hold: () => Promise.reject(new Error('hold must not be acquired')),
|
|
228
|
-
};
|
|
229
|
-
const transition = makeTransition(log, { interlock: heldInterlock });
|
|
230
|
-
const outcome = await transition.execute(makeRequest());
|
|
231
|
-
expect(outcome.status).toBe('refused');
|
|
232
|
-
expectZeroSideEffects(log, outcome.steps);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
describe('UsbModeTransition — TIER B: an in-actor race is caught with zero lease/AT', () => {
|
|
237
|
-
test('valid at entry, invalid in-actor → one actor entry, zero lease/AT calls', async () => {
|
|
238
|
-
const log: SpyLog = { calls: [] };
|
|
239
|
-
let probes = 0;
|
|
240
|
-
const transition = makeTransition(log, {});
|
|
241
|
-
const request = makeRequest({
|
|
242
|
-
// High at entry, low by the time the actor runs (a duplicate IMEI appeared).
|
|
243
|
-
probeReadiness: () => {
|
|
244
|
-
probes += 1;
|
|
245
|
-
return Promise.resolve({ identityConfidence: probes === 1 ? 'high' : 'low' });
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
const outcome = await transition.execute(request);
|
|
249
|
-
|
|
250
|
-
expect(outcome.status).toBe('refused');
|
|
251
|
-
if (outcome.status === 'refused') {
|
|
252
|
-
expect(outcome.stage).toBe('in-actor');
|
|
253
|
-
}
|
|
254
|
-
// Exactly one actor entry.
|
|
255
|
-
expect(outcome.steps.filter((s) => s === 'actor-enter')).toEqual(['actor-enter']);
|
|
256
|
-
// Zero lease/AT (and zero nm/mm) calls fired.
|
|
257
|
-
expect(log.calls).toEqual([]);
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
describe('UsbModeTransition — TIER C: a postcondition mismatch fails degraded', () => {
|
|
262
|
-
test('AT returned OK but the device stayed qmi → FAILED+degraded, NO reactivation, interlock released', async () => {
|
|
263
|
-
const log: SpyLog = { calls: [] };
|
|
264
|
-
let holdReleases = 0;
|
|
265
|
-
const interlock: TransitionInterlock = {
|
|
266
|
-
canDisrupt: () => Promise.resolve({ allow: true }),
|
|
267
|
-
hold: () =>
|
|
268
|
-
Promise.resolve({
|
|
269
|
-
release: () => {
|
|
270
|
-
holdReleases += 1;
|
|
271
|
-
return Promise.resolve();
|
|
272
|
-
},
|
|
273
|
-
}),
|
|
274
|
-
};
|
|
275
|
-
const transition = makeTransition(log, {
|
|
276
|
-
interlock,
|
|
277
|
-
// Re-enumerates STILL as qmi — the switch did not take, even though AT said OK.
|
|
278
|
-
enumerate: scriptedEnumerate([[OLD_QMI], [], [STILL_QMI]]),
|
|
279
|
-
});
|
|
280
|
-
const outcome = await transition.execute(makeRequest());
|
|
281
|
-
|
|
282
|
-
expect(outcome.status).toBe('failed');
|
|
283
|
-
if (outcome.status === 'failed') {
|
|
284
|
-
expect(outcome.degraded).toBe(true);
|
|
285
|
-
expect(outcome.reason).toContain('postcondition mismatch');
|
|
286
|
-
}
|
|
287
|
-
// The AT command WAS sent (OK ignored) but reactivation NEVER happened.
|
|
288
|
-
expect(log.calls.filter((c) => c.startsWith('at.send:'))).toHaveLength(1);
|
|
289
|
-
expect(log.calls.some((c) => c.startsWith('nm.activate:'))).toBe(false);
|
|
290
|
-
expect(outcome.steps).not.toContain('reactivate');
|
|
291
|
-
// The interlock hook was released via finally, on the failure path.
|
|
292
|
-
expect(outcome.steps).toContain('release-interlock');
|
|
293
|
-
expect(holdReleases).toBe(1);
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
describe('UsbModeTransition — crash mid-transaction trips the watchdog', () => {
|
|
298
|
-
test('a hung AT command force-uninhibits, reprobes, and returns degraded (never hangs)', async () => {
|
|
299
|
-
const log: SpyLog = { calls: [] };
|
|
300
|
-
const hangingSender: UsbModeTransitionDeps['atSender'] = {
|
|
301
|
-
send: (command) => {
|
|
302
|
-
log.calls.push(`at.send:${command}`);
|
|
303
|
-
return new Promise(() => undefined);
|
|
304
|
-
},
|
|
305
|
-
};
|
|
306
|
-
const transition = makeTransition(log, {
|
|
307
|
-
atSender: hangingSender,
|
|
308
|
-
enumerate: scriptedEnumerate([[OLD_QMI]]),
|
|
309
|
-
watchdogMs: 30,
|
|
310
|
-
});
|
|
311
|
-
const outcome = await transition.execute(makeRequest());
|
|
312
|
-
|
|
313
|
-
expect(outcome.status).toBe('failed');
|
|
314
|
-
if (outcome.status === 'failed') {
|
|
315
|
-
expect(outcome.degraded).toBe(true);
|
|
316
|
-
expect(outcome.reason).toContain('timed out');
|
|
317
|
-
}
|
|
318
|
-
// The watchdog force-uninhibited the modem and released the interlock.
|
|
319
|
-
expect(outcome.steps).toContain('force-uninhibit');
|
|
320
|
-
expect(outcome.steps).toContain('release-interlock');
|
|
321
|
-
expect(log.calls).toContain('mm.uninhibit');
|
|
322
|
-
});
|
|
323
|
-
});
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
// The certified USB-mode transition transaction.
|
|
2
|
-
//
|
|
3
|
-
// A mode switch is destructive: the modem physically re-enumerates, its interface
|
|
4
|
-
// name changes, and MM briefly loses sight of it. This transaction runs the switch
|
|
5
|
-
// through A3.3's shared per-modem `ModemActor` (keyed on stableKey, so it serialises
|
|
6
|
-
// behind every other disruptive op) in a FIXED order:
|
|
7
|
-
//
|
|
8
|
-
// 1 NM-quiesce → 2 inhibit-by-cached-UID → 3 AT command → 4 expected port-drop →
|
|
9
|
-
// 5 uninhibit → 6 await SAME physical UID → 7 POSTCONDITION → 8 resolve new ifname →
|
|
10
|
-
// 9 reactivate (uuid, newIfname) → 10 release interlock (finally, always).
|
|
11
|
-
//
|
|
12
|
-
// THE POSTCONDITION IS THE ONLY PROOF OF SUCCESS. An AT `OK` proves nothing — only a
|
|
13
|
-
// re-enumerated device whose descriptors AND observed mode equal the catalog target
|
|
14
|
-
// counts. On a postcondition MISMATCH the whole transaction fails `degraded`, does NOT
|
|
15
|
-
// reactivate, and still releases the interlock via `finally`. A hung command trips the
|
|
16
|
-
// AT watchdog, which force-uninhibits so the system reprobes rather than wedging.
|
|
17
|
-
|
|
18
|
-
import type { DeviceIfname, InhibitLease, ModemManagerPort, NetworkManagerPort } from '../ports';
|
|
19
|
-
import { deviceIfname } from '../ports';
|
|
20
|
-
import { CERTIFIED_CATALOG, type CertifiedCatalog, type PermittedTransition } from '../usb-mode';
|
|
21
|
-
import {
|
|
22
|
-
type AtAuditSink,
|
|
23
|
-
AtCommandLease,
|
|
24
|
-
type AtCommandSender,
|
|
25
|
-
computeAtAllowlist,
|
|
26
|
-
} from './at-lease';
|
|
27
|
-
import { descriptorsMatch, detectUsbMode, type UsbDeviceSnapshot } from './device-classifier';
|
|
28
|
-
import type { ModemActor } from './modem-actor';
|
|
29
|
-
import {
|
|
30
|
-
ALLOW_ALL_TRANSITION_INTERLOCK,
|
|
31
|
-
checkTransitionPreconditions,
|
|
32
|
-
type TransitionInterlock,
|
|
33
|
-
type UsbModeTransitionOutcome,
|
|
34
|
-
type UsbModeTransitionRequest,
|
|
35
|
-
} from './transition-preconditions';
|
|
36
|
-
|
|
37
|
-
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
|
-
|
|
39
|
-
const DEFAULT_WATCHDOG_MS = 30_000;
|
|
40
|
-
const DEFAULT_REENUM_TIMEOUT_MS = 60_000;
|
|
41
|
-
const DEFAULT_POLL_INTERVAL_MS = 250;
|
|
42
|
-
|
|
43
|
-
/** Construction dependencies for the transition. Everything I/O is injectable. */
|
|
44
|
-
export interface UsbModeTransitionDeps {
|
|
45
|
-
readonly actor: ModemActor;
|
|
46
|
-
readonly nm: NetworkManagerPort;
|
|
47
|
-
readonly modemManager: Pick<ModemManagerPort, 'inhibit' | 'uninhibit'>;
|
|
48
|
-
readonly atSender: AtCommandSender;
|
|
49
|
-
/** Refresh-triggered USB enumeration (e.g. `createUsbEnumerator().enumerate`). */
|
|
50
|
-
readonly enumerate: () => Promise<readonly UsbDeviceSnapshot[]>;
|
|
51
|
-
readonly interlock?: TransitionInterlock;
|
|
52
|
-
readonly catalog?: CertifiedCatalog;
|
|
53
|
-
readonly audit?: AtAuditSink;
|
|
54
|
-
readonly resolveIfname?: (device: UsbDeviceSnapshot) => DeviceIfname | undefined;
|
|
55
|
-
readonly watchdogMs?: number;
|
|
56
|
-
readonly reenumerationTimeoutMs?: number;
|
|
57
|
-
readonly pollIntervalMs?: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function defaultResolveIfname(device: UsbDeviceSnapshot): DeviceIfname | undefined {
|
|
61
|
-
return device.ifname !== undefined && device.ifname !== ''
|
|
62
|
-
? deviceIfname(device.ifname)
|
|
63
|
-
: undefined;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** The USB-mode transition transaction. One instance is reusable across requests. */
|
|
67
|
-
export class UsbModeTransition {
|
|
68
|
-
readonly #actor: ModemActor;
|
|
69
|
-
readonly #nm: NetworkManagerPort;
|
|
70
|
-
readonly #modemManager: Pick<ModemManagerPort, 'inhibit' | 'uninhibit'>;
|
|
71
|
-
readonly #atSender: AtCommandSender;
|
|
72
|
-
readonly #enumerate: () => Promise<readonly UsbDeviceSnapshot[]>;
|
|
73
|
-
readonly #interlock: TransitionInterlock;
|
|
74
|
-
readonly #catalog: CertifiedCatalog;
|
|
75
|
-
readonly #audit: AtAuditSink | undefined;
|
|
76
|
-
readonly #resolveIfname: (device: UsbDeviceSnapshot) => DeviceIfname | undefined;
|
|
77
|
-
readonly #watchdogMs: number;
|
|
78
|
-
readonly #reenumMs: number;
|
|
79
|
-
readonly #pollMs: number;
|
|
80
|
-
|
|
81
|
-
constructor(deps: UsbModeTransitionDeps) {
|
|
82
|
-
this.#actor = deps.actor;
|
|
83
|
-
this.#nm = deps.nm;
|
|
84
|
-
this.#modemManager = deps.modemManager;
|
|
85
|
-
this.#atSender = deps.atSender;
|
|
86
|
-
this.#enumerate = deps.enumerate;
|
|
87
|
-
this.#interlock = deps.interlock ?? ALLOW_ALL_TRANSITION_INTERLOCK;
|
|
88
|
-
this.#catalog = deps.catalog ?? CERTIFIED_CATALOG;
|
|
89
|
-
this.#audit = deps.audit;
|
|
90
|
-
this.#resolveIfname = deps.resolveIfname ?? defaultResolveIfname;
|
|
91
|
-
this.#watchdogMs = deps.watchdogMs ?? DEFAULT_WATCHDOG_MS;
|
|
92
|
-
this.#reenumMs = deps.reenumerationTimeoutMs ?? DEFAULT_REENUM_TIMEOUT_MS;
|
|
93
|
-
this.#pollMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Run one transition. Preconditions are checked at entry, then again in-actor. */
|
|
97
|
-
async execute(request: UsbModeTransitionRequest): Promise<UsbModeTransitionOutcome> {
|
|
98
|
-
const steps: string[] = [];
|
|
99
|
-
// ENTRY check — a doomed request NEVER enters the actor (TIER A: zero calls).
|
|
100
|
-
const entry = await checkTransitionPreconditions(request, this.#catalog, this.#interlock);
|
|
101
|
-
if (!entry.ok) {
|
|
102
|
-
return { status: 'refused', stage: 'entry', reason: entry.reason, steps };
|
|
103
|
-
}
|
|
104
|
-
return this.#actor.run(request.stableKey, () => this.#inActor(request, steps));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async #inActor(
|
|
108
|
-
request: UsbModeTransitionRequest,
|
|
109
|
-
steps: string[],
|
|
110
|
-
): Promise<UsbModeTransitionOutcome> {
|
|
111
|
-
steps.push('actor-enter');
|
|
112
|
-
// IN-ACTOR re-check — catches a race that closed a gate while queued (TIER B).
|
|
113
|
-
const recheck = await checkTransitionPreconditions(request, this.#catalog, this.#interlock);
|
|
114
|
-
if (!recheck.ok) {
|
|
115
|
-
return { status: 'refused', stage: 'in-actor', reason: recheck.reason, steps };
|
|
116
|
-
}
|
|
117
|
-
const hold = await this.#interlock.hold({ stableKey: request.stableKey });
|
|
118
|
-
try {
|
|
119
|
-
return await this.#runTransaction(
|
|
120
|
-
request,
|
|
121
|
-
recheck.entry.permittedTransitions,
|
|
122
|
-
recheck.transition,
|
|
123
|
-
steps,
|
|
124
|
-
);
|
|
125
|
-
} finally {
|
|
126
|
-
steps.push('release-interlock');
|
|
127
|
-
await hold.release().catch(() => undefined);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async #runTransaction(
|
|
132
|
-
request: UsbModeTransitionRequest,
|
|
133
|
-
allCommands: readonly PermittedTransition[],
|
|
134
|
-
transition: PermittedTransition,
|
|
135
|
-
steps: string[],
|
|
136
|
-
): Promise<UsbModeTransitionOutcome> {
|
|
137
|
-
let inhibit: InhibitLease | undefined;
|
|
138
|
-
let reactivated = false;
|
|
139
|
-
const forceUninhibit = async (): Promise<void> => {
|
|
140
|
-
if (inhibit === undefined) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
const held = inhibit;
|
|
144
|
-
inhibit = undefined;
|
|
145
|
-
steps.push('force-uninhibit');
|
|
146
|
-
await this.#modemManager.uninhibit(held).catch(() => undefined);
|
|
147
|
-
};
|
|
148
|
-
const lease = new AtCommandLease({
|
|
149
|
-
sender: this.#atSender,
|
|
150
|
-
allowlist: computeAtAllowlist(allCommands.map((t) => t.atCommand)),
|
|
151
|
-
timeoutMs: this.#watchdogMs,
|
|
152
|
-
onWatchdog: forceUninhibit,
|
|
153
|
-
...(this.#audit !== undefined ? { audit: this.#audit } : {}),
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
steps.push('nm-quiesce');
|
|
157
|
-
const quiesce = await this.#nm.acquireQuiesceLease(request.connectionId, request.deviceIfname);
|
|
158
|
-
try {
|
|
159
|
-
steps.push('inhibit');
|
|
160
|
-
inhibit = await this.#modemManager.inhibit(request.inhibitUid);
|
|
161
|
-
|
|
162
|
-
// AT `OK` is IGNORED for success — only the postcondition below decides.
|
|
163
|
-
steps.push('at-command');
|
|
164
|
-
await lease.run(transition.atCommand, { inhibitUid: request.inhibitUid });
|
|
165
|
-
|
|
166
|
-
steps.push('await-port-drop');
|
|
167
|
-
await this.#awaitPortDrop(request.cachedPhysicalUid);
|
|
168
|
-
|
|
169
|
-
steps.push('uninhibit');
|
|
170
|
-
if (inhibit !== undefined) {
|
|
171
|
-
const held = inhibit;
|
|
172
|
-
inhibit = undefined;
|
|
173
|
-
await this.#modemManager.uninhibit(held);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
steps.push('await-reenumeration');
|
|
177
|
-
const device = await this.#awaitReenumeration(request.cachedPhysicalUid);
|
|
178
|
-
|
|
179
|
-
steps.push('postcondition');
|
|
180
|
-
const observedMode = detectUsbMode(device);
|
|
181
|
-
const descriptorsOk = descriptorsMatch(device, transition.expectedDescriptors);
|
|
182
|
-
if (observedMode !== request.toMode || !descriptorsOk) {
|
|
183
|
-
return {
|
|
184
|
-
status: 'failed',
|
|
185
|
-
degraded: true,
|
|
186
|
-
reason: `postcondition mismatch: observed ${observedMode ?? 'unknown'} vs target ${request.toMode}; descriptors ${descriptorsOk ? 'ok' : 'mismatch'}`,
|
|
187
|
-
steps,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
steps.push('resolve-ifname');
|
|
192
|
-
const newIfname = this.#resolveIfname(device);
|
|
193
|
-
if (newIfname === undefined) {
|
|
194
|
-
return { status: 'failed', degraded: true, reason: 'could not resolve new ifname', steps };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
steps.push('reactivate');
|
|
198
|
-
await this.#nm.activate(request.connectionId, newIfname);
|
|
199
|
-
reactivated = true;
|
|
200
|
-
return { status: 'succeeded', newIfname, steps };
|
|
201
|
-
} catch (error) {
|
|
202
|
-
await forceUninhibit();
|
|
203
|
-
await this.#reprobe();
|
|
204
|
-
return {
|
|
205
|
-
status: 'failed',
|
|
206
|
-
degraded: true,
|
|
207
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
208
|
-
steps,
|
|
209
|
-
};
|
|
210
|
-
} finally {
|
|
211
|
-
await forceUninhibit();
|
|
212
|
-
if (!reactivated) {
|
|
213
|
-
// Failure path: restore the old connection. On success the new-ifname
|
|
214
|
-
// activation supersedes the quiesce lease (old ifname is gone).
|
|
215
|
-
steps.push('release-quiesce');
|
|
216
|
-
await this.#nm.releaseQuiesceLease(quiesce).catch(() => undefined);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async #awaitPortDrop(uid: string): Promise<void> {
|
|
222
|
-
const deadline = Date.now() + this.#reenumMs;
|
|
223
|
-
while (Date.now() < deadline) {
|
|
224
|
-
const devices = await this.#enumerate();
|
|
225
|
-
if (!devices.some((d) => d.physicalUid === uid)) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
await sleep(this.#pollMs);
|
|
229
|
-
}
|
|
230
|
-
throw new Error(`control port did not drop within ${this.#reenumMs}ms (uid ${uid})`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async #awaitReenumeration(uid: string): Promise<UsbDeviceSnapshot> {
|
|
234
|
-
const deadline = Date.now() + this.#reenumMs;
|
|
235
|
-
while (Date.now() < deadline) {
|
|
236
|
-
const devices = await this.#enumerate();
|
|
237
|
-
const device = devices.find((d) => d.physicalUid === uid);
|
|
238
|
-
if (device !== undefined) {
|
|
239
|
-
return device;
|
|
240
|
-
}
|
|
241
|
-
await sleep(this.#pollMs);
|
|
242
|
-
}
|
|
243
|
-
throw new Error(`device did not re-enumerate within ${this.#reenumMs}ms (uid ${uid})`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Best-effort state re-read after a crash — the transaction still fails degraded. */
|
|
247
|
-
async #reprobe(): Promise<void> {
|
|
248
|
-
await this.#enumerate().then(
|
|
249
|
-
() => undefined,
|
|
250
|
-
() => undefined,
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
}
|
package/src/domain/brand.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// Nominal (branded) primitive types for the domain layer.
|
|
2
|
-
//
|
|
3
|
-
// Branding stops the four identity strings — and the two counter numbers — from
|
|
4
|
-
// being interchangeable at the type level: a `LogicalSlotId` can never be passed
|
|
5
|
-
// where a `SubscriptionId` is expected, even though both are strings at runtime.
|
|
6
|
-
// Downstream waves (A2.2 ports, A3.x D-Bus backend) depend on this distinction.
|
|
7
|
-
|
|
8
|
-
import { DomainError } from './errors';
|
|
9
|
-
|
|
10
|
-
declare const brand: unique symbol;
|
|
11
|
-
|
|
12
|
-
/** A primitive `T` tagged with a compile-time-only brand `B`. Erased at runtime. */
|
|
13
|
-
export type Brand<T, B extends string> = T & { readonly [brand]: B };
|
|
14
|
-
|
|
15
|
-
/** Assert a value is a non-empty string, throwing a typed error otherwise. */
|
|
16
|
-
export function nonEmptyString(value: string, label: string): string {
|
|
17
|
-
if (value.length === 0) {
|
|
18
|
-
throw new DomainError(`${label} must be a non-empty string`);
|
|
19
|
-
}
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Assert a value is a non-negative safe integer, throwing a typed error otherwise. */
|
|
24
|
-
export function nonNegativeInteger(value: number, label: string): number {
|
|
25
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
26
|
-
throw new DomainError(`${label} must be a non-negative safe integer, got ${value}`);
|
|
27
|
-
}
|
|
28
|
-
return value;
|
|
29
|
-
}
|
package/src/domain/errors.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// Typed errors for the domain layer.
|
|
2
|
-
//
|
|
3
|
-
// The domain never fails silently: an impossible state combination, a refused
|
|
4
|
-
// policy binding, or a non-monotonic revision each raise a distinct, catchable
|
|
5
|
-
// error class carrying a machine-readable reason. QA and callers discriminate on
|
|
6
|
-
// the class and the `code`, never on a message string.
|
|
7
|
-
|
|
8
|
-
/** Base class for every domain-layer error. Callers can catch this to trap all. */
|
|
9
|
-
export class DomainError extends Error {
|
|
10
|
-
override readonly name: string = 'DomainError';
|
|
11
|
-
|
|
12
|
-
constructor(message: string) {
|
|
13
|
-
super(message);
|
|
14
|
-
// Restore the prototype chain across the ES5 target transpile so
|
|
15
|
-
// `instanceof` works on subclasses (standard TS extends-Error guard).
|
|
16
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Every distinct impossible cross-dimension combination the guards reject.
|
|
22
|
-
* One code per rule so a failing construction names exactly what it violated.
|
|
23
|
-
*/
|
|
24
|
-
export type ImpossibleStateCode =
|
|
25
|
-
| 'registered-while-absent'
|
|
26
|
-
| 'registered-radio-off'
|
|
27
|
-
| 'registered-empty-rat-set'
|
|
28
|
-
| 'active-state-while-absent'
|
|
29
|
-
| 'radio-off-while-active'
|
|
30
|
-
| 'nm-activated-while-absent'
|
|
31
|
-
| 'nm-activated-without-interface'
|
|
32
|
-
| 'nm-activated-without-mm-connected'
|
|
33
|
-
| 'multiple-active-sim-slots'
|
|
34
|
-
| 'locked-sim-in-empty-slot'
|
|
35
|
-
| 'mm-locked-without-sim-lock'
|
|
36
|
-
| 'data-interface-name-without-presence'
|
|
37
|
-
| 'recovery-attempts-negative'
|
|
38
|
-
| 'recovery-cooldown-stage-mismatch'
|
|
39
|
-
| 'recovery-idle-with-attempts';
|
|
40
|
-
|
|
41
|
-
/** A snapshot was constructed (or transitioned into) a physically impossible state. */
|
|
42
|
-
export class ImpossibleStateError extends DomainError {
|
|
43
|
-
override readonly name = 'ImpossibleStateError';
|
|
44
|
-
readonly code: ImpossibleStateCode;
|
|
45
|
-
|
|
46
|
-
constructor(code: ImpossibleStateCode, detail: string) {
|
|
47
|
-
super(`impossible cellular state [${code}]: ${detail}`);
|
|
48
|
-
this.code = code;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* A durable policy binding was attempted against an identity that is not allowed
|
|
54
|
-
* to carry one — today only low-confidence (ambiguous) equipment identities.
|
|
55
|
-
*/
|
|
56
|
-
export class PolicyBindingRefusedError extends DomainError {
|
|
57
|
-
override readonly name = 'PolicyBindingRefusedError';
|
|
58
|
-
readonly reason: 'ambiguous-identity';
|
|
59
|
-
|
|
60
|
-
constructor(detail: string) {
|
|
61
|
-
super(`durable policy binding refused: ${detail}`);
|
|
62
|
-
this.reason = 'ambiguous-identity';
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** A snapshot transition tried to keep or lower the monotonic revision. */
|
|
67
|
-
export class RevisionMonotonicityError extends DomainError {
|
|
68
|
-
override readonly name = 'RevisionMonotonicityError';
|
|
69
|
-
readonly previous: number;
|
|
70
|
-
readonly next: number;
|
|
71
|
-
|
|
72
|
-
constructor(previous: number, next: number) {
|
|
73
|
-
super(`revision must strictly increase: ${previous} -> ${next}`);
|
|
74
|
-
this.previous = previous;
|
|
75
|
-
this.next = next;
|
|
76
|
-
}
|
|
77
|
-
}
|