@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,107 @@
|
|
|
1
|
+
// Pure nmcli GSM-connection argv builders — the FULL nine-field write parity today's
|
|
2
|
+
// CeraUI carried (modem-registration.ts `sanitizeModemConfigForNetworkManager` +
|
|
3
|
+
// `addConnectionForModem`), rebuilt from scratch for the device-exact adapter so the
|
|
4
|
+
// greenfield port loses nothing the wrap-first path wrote.
|
|
5
|
+
//
|
|
6
|
+
// The nine fields and their exact NM conventions (`gsm.*` + `connection.*` setting
|
|
7
|
+
// semantics) are the contract A4.1 must not regress:
|
|
8
|
+
// gsm.apn / gsm.username / gsm.password — creds ("" when unset)
|
|
9
|
+
// gsm.password-flags — "4" NOT_REQUIRED (no password),
|
|
10
|
+
// "0" NONE / system-stored (password set)
|
|
11
|
+
// gsm.home-only — "yes" ⇒ roaming DISABLED
|
|
12
|
+
// gsm.network-id — manual operator PLMN, only while roaming
|
|
13
|
+
// gsm.auto-config — "yes" ⇒ Auto-APN (creds MUST be empty)
|
|
14
|
+
// connection.autoconnect — always "yes"
|
|
15
|
+
// connection.autoconnect-retries — "2"
|
|
16
|
+
// Empty-string fallback; Bun runtime limitation with empty CLI args: a bare
|
|
17
|
+
// `undefined` reaching `Bun.spawn` argv drops the slot and shifts every following
|
|
18
|
+
// token, so every optional nmcli value is coerced to "" first — the same `value || ""`
|
|
19
|
+
// convention CeraUI documented on gsm.apn / gsm.username / gsm.password.
|
|
20
|
+
export const cliArg = (value) => value || '';
|
|
21
|
+
/** NM secret-flags: no password ⇒ NOT_REQUIRED ("4"); password set ⇒ NONE ("0"). */
|
|
22
|
+
export const passwordFlags = (password) => (password ? '0' : '4');
|
|
23
|
+
/** `gsm.network-id` — the manual operator id, and only while roaming (home-only off). */
|
|
24
|
+
const networkIdField = (input) => input.homeOnly ? '' : cliArg(input.networkId);
|
|
25
|
+
/**
|
|
26
|
+
* The nine `gsm.*` + `connection.*` key/value pairs for a profile, in stable order.
|
|
27
|
+
* With `autoConfig` set the creds are forced empty (NM rejects `auto-config yes` while
|
|
28
|
+
* any credential is present — nm-setting-gsm.c:440-447), so an Auto-APN create is always
|
|
29
|
+
* NM-valid by construction.
|
|
30
|
+
*/
|
|
31
|
+
export function gsmFieldPairs(input) {
|
|
32
|
+
const auto = input.autoConfig;
|
|
33
|
+
return [
|
|
34
|
+
['gsm.apn', auto ? '' : cliArg(input.apn)],
|
|
35
|
+
['gsm.username', auto ? '' : cliArg(input.username)],
|
|
36
|
+
['gsm.password', auto ? '' : cliArg(input.password)],
|
|
37
|
+
['gsm.password-flags', passwordFlags(auto ? undefined : input.password)],
|
|
38
|
+
['gsm.home-only', input.homeOnly ? 'yes' : 'no'],
|
|
39
|
+
['gsm.network-id', networkIdField(input)],
|
|
40
|
+
['gsm.auto-config', auto ? 'yes' : 'no'],
|
|
41
|
+
['connection.autoconnect', 'yes'],
|
|
42
|
+
['connection.autoconnect-retries', '2'],
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
/** Flatten key/value pairs into the alternating argv nmcli expects. */
|
|
46
|
+
export function flattenPairs(pairs) {
|
|
47
|
+
return pairs.flat();
|
|
48
|
+
}
|
|
49
|
+
/** `connection add type gsm con-name <name> <nine fields>` — device-exact create argv. */
|
|
50
|
+
export function createGsmArgs(input) {
|
|
51
|
+
return [
|
|
52
|
+
'connection',
|
|
53
|
+
'add',
|
|
54
|
+
'type',
|
|
55
|
+
'gsm',
|
|
56
|
+
'con-name',
|
|
57
|
+
input.connectionName,
|
|
58
|
+
...flattenPairs(gsmFieldPairs(input)),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The key/value pairs for a `connection modify`. Roaming (home-only) and the manual
|
|
63
|
+
* operator id move together — turning roaming off clears `gsm.network-id`, honoring the
|
|
64
|
+
* `gsm.network-id = roaming ? id : ""` invariant in the SAME modify.
|
|
65
|
+
*/
|
|
66
|
+
export function patchPairs(patch) {
|
|
67
|
+
const pairs = [];
|
|
68
|
+
if (patch.connectionName !== undefined) {
|
|
69
|
+
pairs.push('connection.id', patch.connectionName);
|
|
70
|
+
}
|
|
71
|
+
if (patch.apn !== undefined) {
|
|
72
|
+
pairs.push('gsm.apn', cliArg(patch.apn));
|
|
73
|
+
}
|
|
74
|
+
if (patch.username !== undefined) {
|
|
75
|
+
pairs.push('gsm.username', cliArg(patch.username));
|
|
76
|
+
}
|
|
77
|
+
if (patch.password !== undefined) {
|
|
78
|
+
pairs.push('gsm.password', cliArg(patch.password), 'gsm.password-flags', passwordFlags(patch.password));
|
|
79
|
+
}
|
|
80
|
+
if (patch.autoConfig !== undefined) {
|
|
81
|
+
pairs.push('gsm.auto-config', patch.autoConfig ? 'yes' : 'no');
|
|
82
|
+
}
|
|
83
|
+
if (patch.homeOnly !== undefined) {
|
|
84
|
+
pairs.push('gsm.home-only', patch.homeOnly ? 'yes' : 'no');
|
|
85
|
+
pairs.push('gsm.network-id', patch.homeOnly ? '' : cliArg(patch.networkId));
|
|
86
|
+
}
|
|
87
|
+
else if (patch.networkId !== undefined) {
|
|
88
|
+
pairs.push('gsm.network-id', cliArg(patch.networkId));
|
|
89
|
+
}
|
|
90
|
+
return pairs;
|
|
91
|
+
}
|
|
92
|
+
/** Map an nmcli terse readback back onto a `GsmProfile` (inverse of the write parity). */
|
|
93
|
+
export function buildProfile(id, settings) {
|
|
94
|
+
const username = settings.get('gsm.username') ?? '';
|
|
95
|
+
const password = settings.get('gsm.password') ?? '';
|
|
96
|
+
const networkId = settings.get('gsm.network-id') ?? '';
|
|
97
|
+
return {
|
|
98
|
+
connectionId: id,
|
|
99
|
+
connectionName: settings.get('connection.id') ?? '',
|
|
100
|
+
apn: settings.get('gsm.apn') ?? '',
|
|
101
|
+
homeOnly: settings.get('gsm.home-only') === 'yes',
|
|
102
|
+
autoConfig: settings.get('gsm.auto-config') === 'yes',
|
|
103
|
+
...(username !== '' ? { username } : {}),
|
|
104
|
+
...(password !== '' ? { password } : {}),
|
|
105
|
+
...(networkId !== '' ? { networkId } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ConnectionId, type DeviceIfname, type GsmProfile, type GsmProfileInput, type GsmProfilePatch, type NetworkManagerPort, type QuiesceLease, type Receipt } from '../ports/index.js';
|
|
2
|
+
import { type AutoApnTransitionResult, type ManualApn } from './nm-auto-apn.js';
|
|
3
|
+
import { type NmcliRunner } from './nmcli-runner.js';
|
|
4
|
+
export interface NmcliNmPortOptions {
|
|
5
|
+
readonly runner: NmcliRunner;
|
|
6
|
+
/** Resolved once at boot by the capability probe; default `true`. */
|
|
7
|
+
readonly autoApnCapable?: boolean;
|
|
8
|
+
readonly leaseTtlMs?: number;
|
|
9
|
+
readonly now?: () => number;
|
|
10
|
+
}
|
|
11
|
+
export declare class NmcliNmPort implements NetworkManagerPort {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(options: NmcliNmPortOptions);
|
|
14
|
+
createGsmProfile(profile: GsmProfileInput): Promise<GsmProfile>;
|
|
15
|
+
readGsmProfile(id: ConnectionId): Promise<GsmProfile | undefined>;
|
|
16
|
+
updateGsmProfile(id: ConnectionId, patch: GsmProfilePatch): Promise<GsmProfile>;
|
|
17
|
+
deleteGsmProfile(id: ConnectionId): Promise<void>;
|
|
18
|
+
activate(id: ConnectionId, ifname: DeviceIfname): Promise<Receipt>;
|
|
19
|
+
deactivate(id: ConnectionId, ifname: DeviceIfname): Promise<Receipt>;
|
|
20
|
+
acquireQuiesceLease(id: ConnectionId, ifname: DeviceIfname): Promise<QuiesceLease>;
|
|
21
|
+
releaseQuiesceLease(lease: QuiesceLease): Promise<void>;
|
|
22
|
+
/** Reactivate every lease held past the TTL — the abandoned-lease watchdog. */
|
|
23
|
+
sweepExpiredLeases(now?: number): Promise<void>;
|
|
24
|
+
/** Flip a profile to Auto-APN via ONE atomic modify, then reactivate + classify. */
|
|
25
|
+
transitionToAuto(id: ConnectionId, ifname: DeviceIfname): Promise<AutoApnTransitionResult>;
|
|
26
|
+
/** Flip a profile to manual APN via ONE atomic modify, then reactivate + classify. */
|
|
27
|
+
transitionToManual(id: ConnectionId, ifname: DeviceIfname, creds: ManualApn): Promise<AutoApnTransitionResult>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The device-exact NetworkManager adapter: a FRESH nmcli-based `NetworkManagerPort`
|
|
2
|
+
// (no reused CeraUI code) with the full nine-field GSM write parity, verify-then-
|
|
3
|
+
// device-disconnect deactivation (never id-only `connection down`), abandoned-lease
|
|
4
|
+
// quiesce, and atomic Auto-APN transitions.
|
|
5
|
+
import { epochMillis } from '../domain/index.js';
|
|
6
|
+
import { connectionId, receipt, } from '../ports/index.js';
|
|
7
|
+
import { AUTO_APN_ADVISORY, classifyActivation, toAutoArgs, toManualArgs, } from './nm-auto-apn.js';
|
|
8
|
+
import { buildProfile, createGsmArgs, patchPairs } from './nm-gsm-fields.js';
|
|
9
|
+
import { parseTerse, runNmcli } from './nmcli-runner.js';
|
|
10
|
+
const READBACK_FIELDS = [
|
|
11
|
+
'connection.id',
|
|
12
|
+
'gsm.apn',
|
|
13
|
+
'gsm.username',
|
|
14
|
+
'gsm.password',
|
|
15
|
+
'gsm.password-flags',
|
|
16
|
+
'gsm.home-only',
|
|
17
|
+
'gsm.network-id',
|
|
18
|
+
'gsm.auto-config',
|
|
19
|
+
'connection.autoconnect',
|
|
20
|
+
'connection.autoconnect-retries',
|
|
21
|
+
];
|
|
22
|
+
/** Default abandoned-lease TTL: a held quiesce lease older than this auto-releases. */
|
|
23
|
+
const DEFAULT_LEASE_TTL_MS = 60_000;
|
|
24
|
+
export class NmcliNmPort {
|
|
25
|
+
#runner;
|
|
26
|
+
#autoApnCapable;
|
|
27
|
+
#leaseTtlMs;
|
|
28
|
+
#now;
|
|
29
|
+
#leases = new Set();
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.#runner = options.runner;
|
|
32
|
+
this.#autoApnCapable = options.autoApnCapable ?? true;
|
|
33
|
+
this.#leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
34
|
+
this.#now = options.now ?? Date.now;
|
|
35
|
+
}
|
|
36
|
+
async createGsmProfile(profile) {
|
|
37
|
+
const result = await runNmcli(this.#runner, createGsmArgs(profile));
|
|
38
|
+
const uuid = /\(([^)]+)\) successfully added/.exec(result.stdout)?.[1];
|
|
39
|
+
if (uuid === undefined) {
|
|
40
|
+
throw new Error(`nmcli connection add failed: ${result.stderr || result.stdout}`);
|
|
41
|
+
}
|
|
42
|
+
return this.#requireProfile(connectionId(uuid));
|
|
43
|
+
}
|
|
44
|
+
async readGsmProfile(id) {
|
|
45
|
+
const result = await runNmcli(this.#runner, [
|
|
46
|
+
'-t',
|
|
47
|
+
'-f',
|
|
48
|
+
READBACK_FIELDS.join(','),
|
|
49
|
+
'connection',
|
|
50
|
+
'show',
|
|
51
|
+
id,
|
|
52
|
+
]);
|
|
53
|
+
if (result.exitCode !== 0) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return buildProfile(id, parseTerse(result.stdout));
|
|
57
|
+
}
|
|
58
|
+
async updateGsmProfile(id, patch) {
|
|
59
|
+
const result = await runNmcli(this.#runner, ['connection', 'modify', id, ...patchPairs(patch)]);
|
|
60
|
+
if (result.exitCode !== 0) {
|
|
61
|
+
throw new Error(`nmcli connection modify failed: ${result.stderr}`);
|
|
62
|
+
}
|
|
63
|
+
return this.#requireProfile(id);
|
|
64
|
+
}
|
|
65
|
+
async deleteGsmProfile(id) {
|
|
66
|
+
await runNmcli(this.#runner, ['connection', 'delete', id]);
|
|
67
|
+
}
|
|
68
|
+
async activate(id, ifname) {
|
|
69
|
+
const result = await runNmcli(this.#runner, ['connection', 'up', id, 'ifname', ifname]);
|
|
70
|
+
return activationReceipt(result, `activated ${id} on ${ifname}`);
|
|
71
|
+
}
|
|
72
|
+
async deactivate(id, ifname) {
|
|
73
|
+
if ((await this.#activeUuidOn(ifname)) !== id) {
|
|
74
|
+
return receipt('enabled', 'applied', `${id} not active on ${ifname}; nothing to deactivate`);
|
|
75
|
+
}
|
|
76
|
+
const result = await runNmcli(this.#runner, ['device', 'disconnect', ifname]);
|
|
77
|
+
return activationReceipt(result, `deactivated ${id} on ${ifname}`);
|
|
78
|
+
}
|
|
79
|
+
async acquireQuiesceLease(id, ifname) {
|
|
80
|
+
if ((await this.#activeUuidOn(ifname)) === id) {
|
|
81
|
+
await runNmcli(this.#runner, ['device', 'disconnect', ifname]);
|
|
82
|
+
}
|
|
83
|
+
const lease = {
|
|
84
|
+
connectionId: id,
|
|
85
|
+
deviceIfname: ifname,
|
|
86
|
+
acquiredAt: epochMillis(this.#now()),
|
|
87
|
+
};
|
|
88
|
+
this.#leases.add(lease);
|
|
89
|
+
return lease;
|
|
90
|
+
}
|
|
91
|
+
async releaseQuiesceLease(lease) {
|
|
92
|
+
if (this.#leases.delete(lease)) {
|
|
93
|
+
await this.#reactivate(lease);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Reactivate every lease held past the TTL — the abandoned-lease watchdog. */
|
|
97
|
+
async sweepExpiredLeases(now = this.#now()) {
|
|
98
|
+
for (const lease of [...this.#leases]) {
|
|
99
|
+
if (now - lease.acquiredAt >= this.#leaseTtlMs) {
|
|
100
|
+
this.#leases.delete(lease);
|
|
101
|
+
await this.#reactivate(lease);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Flip a profile to Auto-APN via ONE atomic modify, then reactivate + classify. */
|
|
106
|
+
async transitionToAuto(id, ifname) {
|
|
107
|
+
if (!this.#autoApnCapable) {
|
|
108
|
+
return {
|
|
109
|
+
receipt: receipt('connection', 'unsupported', 'Auto-APN requires NetworkManager >= 1.22'),
|
|
110
|
+
advisory: AUTO_APN_ADVISORY,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const modify = await runNmcli(this.#runner, toAutoArgs(id));
|
|
114
|
+
if (modify.exitCode !== 0) {
|
|
115
|
+
return {
|
|
116
|
+
receipt: receipt('connection', 'failed', modify.stderr || 'auto-config modify rejected'),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return this.#reactivateAndClassify(id, ifname, true);
|
|
120
|
+
}
|
|
121
|
+
/** Flip a profile to manual APN via ONE atomic modify, then reactivate + classify. */
|
|
122
|
+
async transitionToManual(id, ifname, creds) {
|
|
123
|
+
const modify = await runNmcli(this.#runner, toManualArgs(id, creds));
|
|
124
|
+
if (modify.exitCode !== 0) {
|
|
125
|
+
return {
|
|
126
|
+
receipt: receipt('connection', 'failed', modify.stderr || 'manual-apn modify rejected'),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return this.#reactivateAndClassify(id, ifname, false);
|
|
130
|
+
}
|
|
131
|
+
async #reactivateAndClassify(id, ifname, underAuto) {
|
|
132
|
+
const up = await runNmcli(this.#runner, ['connection', 'up', id, 'ifname', ifname]);
|
|
133
|
+
return classifyActivation(up, { underAuto });
|
|
134
|
+
}
|
|
135
|
+
async #reactivate(lease) {
|
|
136
|
+
await runNmcli(this.#runner, [
|
|
137
|
+
'connection',
|
|
138
|
+
'up',
|
|
139
|
+
lease.connectionId,
|
|
140
|
+
'ifname',
|
|
141
|
+
lease.deviceIfname,
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
async #activeUuidOn(ifname) {
|
|
145
|
+
const result = await runNmcli(this.#runner, [
|
|
146
|
+
'-t',
|
|
147
|
+
'-f',
|
|
148
|
+
'UUID,DEVICE',
|
|
149
|
+
'connection',
|
|
150
|
+
'show',
|
|
151
|
+
'--active',
|
|
152
|
+
]);
|
|
153
|
+
for (const line of result.stdout.split('\n')) {
|
|
154
|
+
const [uuid, device] = line.split(':');
|
|
155
|
+
if (device === ifname && uuid) {
|
|
156
|
+
return uuid;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
async #requireProfile(id) {
|
|
162
|
+
const profile = await this.readGsmProfile(id);
|
|
163
|
+
if (profile === undefined) {
|
|
164
|
+
throw new Error(`nmcli: profile ${id} did not read back`);
|
|
165
|
+
}
|
|
166
|
+
return profile;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function activationReceipt(result, appliedReason) {
|
|
170
|
+
return result.exitCode === 0
|
|
171
|
+
? receipt('enabled', 'applied', appliedReason)
|
|
172
|
+
: receipt('enabled', 'failed', result.stderr || 'nmcli returned a non-zero exit code');
|
|
173
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface NmcliResult {
|
|
2
|
+
readonly stdout: string;
|
|
3
|
+
readonly stderr: string;
|
|
4
|
+
readonly exitCode: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A runner over nmcli argv. `run` may be synchronous (the stateful test double) or
|
|
8
|
+
* asynchronous (the real `Bun.spawn` adapter) — the port awaits either way.
|
|
9
|
+
*/
|
|
10
|
+
export interface NmcliRunner {
|
|
11
|
+
run(argv: readonly string[]): NmcliResult | Promise<NmcliResult>;
|
|
12
|
+
}
|
|
13
|
+
/** Await a runner's result whether it returned synchronously or as a promise. */
|
|
14
|
+
export declare function runNmcli(runner: NmcliRunner, argv: readonly string[]): Promise<NmcliResult>;
|
|
15
|
+
/**
|
|
16
|
+
* The device-exact runner: spawns the real `nmcli` via Bun. Every argv the port
|
|
17
|
+
* builds is passed verbatim, so what the tests assert against the stateful stub is
|
|
18
|
+
* byte-for-byte what runs on the device.
|
|
19
|
+
*/
|
|
20
|
+
export declare class SpawnNmcliRunner implements NmcliRunner {
|
|
21
|
+
run(argv: readonly string[]): Promise<NmcliResult>;
|
|
22
|
+
}
|
|
23
|
+
/** Parse nmcli terse (`-t`) `key:value` lines into a map, splitting on the first `:`. */
|
|
24
|
+
export declare function parseTerse(stdout: string): Map<string, string>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The nmcli invocation seam. The shipping adapter spawns the real `nmcli`; tests
|
|
2
|
+
// inject A2.3's stateful in-memory runner (structurally the same `run(argv)`). The
|
|
3
|
+
// port never spawns directly — it only ever talks to an injected `NmcliRunner`, so
|
|
4
|
+
// the exact code path exercised under test is the one that runs on-device.
|
|
5
|
+
/** Await a runner's result whether it returned synchronously or as a promise. */
|
|
6
|
+
export async function runNmcli(runner, argv) {
|
|
7
|
+
return runner.run(argv);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The device-exact runner: spawns the real `nmcli` via Bun. Every argv the port
|
|
11
|
+
* builds is passed verbatim, so what the tests assert against the stateful stub is
|
|
12
|
+
* byte-for-byte what runs on the device.
|
|
13
|
+
*/
|
|
14
|
+
export class SpawnNmcliRunner {
|
|
15
|
+
async run(argv) {
|
|
16
|
+
const proc = Bun.spawn(['nmcli', ...argv], { stdout: 'pipe', stderr: 'pipe' });
|
|
17
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
18
|
+
new Response(proc.stdout).text(),
|
|
19
|
+
new Response(proc.stderr).text(),
|
|
20
|
+
proc.exited,
|
|
21
|
+
]);
|
|
22
|
+
return { stdout, stderr, exitCode };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Parse nmcli terse (`-t`) `key:value` lines into a map, splitting on the first `:`. */
|
|
26
|
+
export function parseTerse(stdout) {
|
|
27
|
+
const settings = new Map();
|
|
28
|
+
for (const line of stdout.split('\n')) {
|
|
29
|
+
const separator = line.indexOf(':');
|
|
30
|
+
if (separator >= 0) {
|
|
31
|
+
settings.set(line.slice(0, separator), line.slice(separator + 1));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return settings;
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ModemObservationPort, ObservationList, ObservationListener, Unsubscribe } from '../ports/index.js';
|
|
2
|
+
import type { DbusTransport } from '../transport/index.js';
|
|
3
|
+
import { type DecodedManagedObjects } from './managed-objects.js';
|
|
4
|
+
/**
|
|
5
|
+
* A current-epoch authoritative snapshot, delivered to `onEpochRefresh` AFTER the
|
|
6
|
+
* epoch guard passes. `epoch` is the owning MM unique bus name; `tree` is the decoded
|
|
7
|
+
* `GetManagedObjects` payload the snapshot was reconciled from. The Signal.Setup
|
|
8
|
+
* manager (A3.3) hooks this to (re-)apply cadence per modem per epoch, and the D-Bus
|
|
9
|
+
* backend uses it to refresh its path→stable-key map.
|
|
10
|
+
*/
|
|
11
|
+
export interface EpochRefreshEvent {
|
|
12
|
+
readonly epoch: string;
|
|
13
|
+
readonly tree: DecodedManagedObjects;
|
|
14
|
+
}
|
|
15
|
+
export interface MmDbusObserverOptions {
|
|
16
|
+
/** The transport to talk D-Bus over (A2.4). The observer connects it on `start()`. */
|
|
17
|
+
readonly transport: DbusTransport;
|
|
18
|
+
/** MM bus name override (defaults to `org.freedesktop.ModemManager1`). */
|
|
19
|
+
readonly destination?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Called after EVERY successful current-epoch authoritative snapshot (start,
|
|
22
|
+
* hot-plug, epoch change, property change) — never for a superseded epoch. The
|
|
23
|
+
* hook fires whether or not any row changed, so a consumer always sees the live
|
|
24
|
+
* epoch + tree. It must not throw; a throw is swallowed so it can never break the
|
|
25
|
+
* observer's refresh loop.
|
|
26
|
+
*/
|
|
27
|
+
readonly onEpochRefresh?: (event: EpochRefreshEvent) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare class MmDbusObserver implements ModemObservationPort {
|
|
30
|
+
#private;
|
|
31
|
+
constructor(options: MmDbusObserverOptions);
|
|
32
|
+
start(): Promise<ObservationList>;
|
|
33
|
+
observe(listener: ObservationListener): Unsubscribe;
|
|
34
|
+
stop(): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Construct an epoch-scoped ModemManager observer over an A2.4 transport. */
|
|
37
|
+
export declare function createMmDbusObserver(options: MmDbusObserverOptions): MmDbusObserver;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// The epoch-scoped ModemManager observer.
|
|
2
|
+
//
|
|
3
|
+
// `MmDbusObserver` implements the read-only `ModemObservationPort` over the A2.4
|
|
4
|
+
// transport, tested against the A2.3 fake. `start()` connects, subscribes to the four
|
|
5
|
+
// lifecycle signals, THEN takes the first authoritative `GetManagedObjects` snapshot —
|
|
6
|
+
// reconciling any signal that raced in between.
|
|
7
|
+
//
|
|
8
|
+
// SAFETY-CRITICAL — epoch-scoped removal (draft §Oracle round-3 #5). An "epoch" is one
|
|
9
|
+
// continuous ownership period of the MM bus name, tracked via `NameOwnerChanged`. A
|
|
10
|
+
// modem is REMOVED only when it is missing from a CURRENT-epoch authoritative snapshot.
|
|
11
|
+
// Owner loss, bus disconnect, and any signal whose `sender` is not the current owner
|
|
12
|
+
// (an OLD-epoch straggler) never remove a modem — they only ever mark it
|
|
13
|
+
// `sourceUnavailable`. The false-removal class is dead by construction: even the
|
|
14
|
+
// `ObservationList` failure arm retains its rows. Row bookkeeping lives in
|
|
15
|
+
// `ObservationRowStore`; this file owns epoch tracking, subscriptions, and refresh.
|
|
16
|
+
import { DBUS_DESTINATION, DBUS_IFACE, DBUS_PATH, MM_BUS_NAME, MM_ROOT_PATH, OBJECT_MANAGER_IFACE, PROPERTIES_IFACE, } from './constants.js';
|
|
17
|
+
import { asManagedObjects } from './managed-objects.js';
|
|
18
|
+
import { ObservationRowStore } from './row-store.js';
|
|
19
|
+
export class MmDbusObserver {
|
|
20
|
+
#transport;
|
|
21
|
+
#destination;
|
|
22
|
+
#onEpochRefresh;
|
|
23
|
+
#store = new ObservationRowStore();
|
|
24
|
+
#listeners = new Set();
|
|
25
|
+
#subscriptions = [];
|
|
26
|
+
#currentOwner;
|
|
27
|
+
#started = false;
|
|
28
|
+
#stopped = false;
|
|
29
|
+
#priming = false;
|
|
30
|
+
#refreshDuringPrime = false;
|
|
31
|
+
#refreshing = false;
|
|
32
|
+
#refreshQueued = false;
|
|
33
|
+
#onDisconnected = () => this.#handleSourceGone('source-unavailable');
|
|
34
|
+
#onReconnected = () => {
|
|
35
|
+
void this.#adoptCurrentOwner();
|
|
36
|
+
};
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.#transport = options.transport;
|
|
39
|
+
this.#destination = options.destination ?? MM_BUS_NAME;
|
|
40
|
+
this.#onEpochRefresh = options.onEpochRefresh;
|
|
41
|
+
}
|
|
42
|
+
async start() {
|
|
43
|
+
if (this.#started) {
|
|
44
|
+
return this.#store.list();
|
|
45
|
+
}
|
|
46
|
+
this.#started = true;
|
|
47
|
+
this.#priming = true;
|
|
48
|
+
await this.#transport.connect();
|
|
49
|
+
this.#transport.on('disconnected', this.#onDisconnected);
|
|
50
|
+
this.#transport.on('reconnected', this.#onReconnected);
|
|
51
|
+
await this.#subscribeAll();
|
|
52
|
+
await this.#adoptCurrentOwner();
|
|
53
|
+
this.#priming = false;
|
|
54
|
+
if (this.#refreshDuringPrime) {
|
|
55
|
+
this.#refreshDuringPrime = false;
|
|
56
|
+
this.#scheduleRefresh();
|
|
57
|
+
}
|
|
58
|
+
return this.#store.list();
|
|
59
|
+
}
|
|
60
|
+
observe(listener) {
|
|
61
|
+
this.#listeners.add(listener);
|
|
62
|
+
return () => {
|
|
63
|
+
this.#listeners.delete(listener);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async stop() {
|
|
67
|
+
if (this.#stopped) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.#stopped = true;
|
|
71
|
+
this.#transport.off('disconnected', this.#onDisconnected);
|
|
72
|
+
this.#transport.off('reconnected', this.#onReconnected);
|
|
73
|
+
const subs = this.#subscriptions.splice(0);
|
|
74
|
+
await Promise.all(subs.map((sub) => sub.unsubscribe().catch(() => undefined)));
|
|
75
|
+
this.#listeners.clear();
|
|
76
|
+
}
|
|
77
|
+
// ── signal subscription ────────────────────────────────────────────────────────
|
|
78
|
+
async #subscribeAll() {
|
|
79
|
+
const om = { interface: OBJECT_MANAGER_IFACE, path: MM_ROOT_PATH };
|
|
80
|
+
this.#subscriptions.push(await this.#transport.subscribeSignal({ ...om, member: 'InterfacesAdded' }, (event) => this.#onObjectSignal(event)), await this.#transport.subscribeSignal({ ...om, member: 'InterfacesRemoved' }, (event) => this.#onObjectSignal(event)), await this.#transport.subscribeSignal({ interface: PROPERTIES_IFACE, member: 'PropertiesChanged' }, (event) => this.#onObjectSignal(event)), await this.#transport.subscribeSignal({ interface: DBUS_IFACE, member: 'NameOwnerChanged' }, (event) => this.#onNameOwnerChanged(event)));
|
|
81
|
+
}
|
|
82
|
+
// ── epoch tracking ───────────────────────────────────────────────────────────
|
|
83
|
+
async #adoptCurrentOwner() {
|
|
84
|
+
const owner = await this.#queryOwner();
|
|
85
|
+
if (owner === undefined) {
|
|
86
|
+
this.#handleSourceGone('source-unavailable');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.#currentOwner = owner;
|
|
90
|
+
await this.#runRefresh(owner);
|
|
91
|
+
}
|
|
92
|
+
async #queryOwner() {
|
|
93
|
+
try {
|
|
94
|
+
const reply = await this.#transport.callMethod({
|
|
95
|
+
destination: DBUS_DESTINATION,
|
|
96
|
+
path: DBUS_PATH,
|
|
97
|
+
interface: DBUS_IFACE,
|
|
98
|
+
member: 'GetNameOwner',
|
|
99
|
+
signature: 's',
|
|
100
|
+
args: [MM_BUS_NAME],
|
|
101
|
+
});
|
|
102
|
+
const owner = reply.body[0];
|
|
103
|
+
return typeof owner === 'string' && owner.length > 0 ? owner : undefined;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// NameHasNoOwner (or a transient failure) → no current epoch yet.
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
#onNameOwnerChanged(event) {
|
|
111
|
+
if (event.body[0] !== MM_BUS_NAME) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const newOwner = typeof event.body[2] === 'string' ? event.body[2] : '';
|
|
115
|
+
if (newOwner.length === 0) {
|
|
116
|
+
// Owner lost — stale, never a removal.
|
|
117
|
+
this.#handleSourceGone('source-unavailable');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (newOwner === this.#currentOwner) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// New epoch: everything goes stale until the fresh snapshot restores it.
|
|
124
|
+
if (this.#store.markUnavailable('source-unavailable')) {
|
|
125
|
+
this.#emit();
|
|
126
|
+
}
|
|
127
|
+
this.#currentOwner = newOwner;
|
|
128
|
+
this.#scheduleRefresh();
|
|
129
|
+
}
|
|
130
|
+
#onObjectSignal(event) {
|
|
131
|
+
// Epoch guard: a signal from anyone but the current owner is an OLD-epoch
|
|
132
|
+
// straggler and must never drive a removal (draft §Oracle round-3 #5).
|
|
133
|
+
if (this.#currentOwner === undefined || event.sender !== this.#currentOwner) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (this.#priming) {
|
|
137
|
+
this.#refreshDuringPrime = true;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.#scheduleRefresh();
|
|
141
|
+
}
|
|
142
|
+
// ── authoritative refresh ──────────────────────────────────────────────────────
|
|
143
|
+
#scheduleRefresh() {
|
|
144
|
+
const owner = this.#currentOwner;
|
|
145
|
+
if (owner === undefined || this.#stopped) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (this.#refreshing) {
|
|
149
|
+
this.#refreshQueued = true;
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
void this.#runRefresh(owner);
|
|
153
|
+
}
|
|
154
|
+
async #runRefresh(epochOwner) {
|
|
155
|
+
this.#refreshing = true;
|
|
156
|
+
try {
|
|
157
|
+
const reply = await this.#transport.callMethod({
|
|
158
|
+
destination: this.#destination,
|
|
159
|
+
path: MM_ROOT_PATH,
|
|
160
|
+
interface: OBJECT_MANAGER_IFACE,
|
|
161
|
+
member: 'GetManagedObjects',
|
|
162
|
+
});
|
|
163
|
+
// Late reply from a superseded epoch — discard (draft §Oracle round-3 #5).
|
|
164
|
+
if (this.#currentOwner !== epochOwner || this.#stopped) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const tree = asManagedObjects(reply.body[0]);
|
|
168
|
+
const rowsChanged = this.#store.reconcile(tree);
|
|
169
|
+
const healthChanged = this.#store.markHealthy();
|
|
170
|
+
this.#notifyEpochRefresh(epochOwner, tree);
|
|
171
|
+
if (rowsChanged || healthChanged) {
|
|
172
|
+
this.#emit();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
if (this.#currentOwner === epochOwner && !this.#stopped) {
|
|
177
|
+
this.#markUnavailable('bus-error');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
this.#refreshing = false;
|
|
182
|
+
if (this.#refreshQueued && !this.#stopped) {
|
|
183
|
+
this.#refreshQueued = false;
|
|
184
|
+
this.#scheduleRefresh();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ── source-unavailable transitions ─────────────────────────────────────────────
|
|
189
|
+
#handleSourceGone(reason) {
|
|
190
|
+
this.#currentOwner = undefined;
|
|
191
|
+
this.#markUnavailable(reason);
|
|
192
|
+
}
|
|
193
|
+
#markUnavailable(reason) {
|
|
194
|
+
if (this.#store.markUnavailable(reason)) {
|
|
195
|
+
this.#emit();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
#notifyEpochRefresh(epoch, tree) {
|
|
199
|
+
if (this.#onEpochRefresh === undefined) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
this.#onEpochRefresh({ epoch, tree });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// A consumer's hook must never break the observer's refresh loop.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
#emit() {
|
|
210
|
+
const list = this.#store.list();
|
|
211
|
+
for (const listener of [...this.#listeners]) {
|
|
212
|
+
listener(list);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Construct an epoch-scoped ModemManager observer over an A2.4 transport. */
|
|
217
|
+
export function createMmDbusObserver(options) {
|
|
218
|
+
return new MmDbusObserver(options);
|
|
219
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EpochMillis } from '../domain/index.js';
|
|
2
|
+
/** How a board can power-cycle a modem. Only `none` is implemented in Phase A. */
|
|
3
|
+
export type PowerCapabilityKind = 'none' | 'gpio-cut' | 'pwrkey-pulse' | 'usb-hub-port-cycle';
|
|
4
|
+
/**
|
|
5
|
+
* The USB data mode a modem should re-enumerate into after a power cycle. A4.2 owns
|
|
6
|
+
* the certified USB-mode catalog; the power contract only records the operator's
|
|
7
|
+
* preferred post-cycle enumeration mode.
|
|
8
|
+
*/
|
|
9
|
+
export type PreferredUsbMode = 'qmi' | 'mbim' | 'ecm-ncm' | 'rndis' | 'router-ethernet';
|
|
10
|
+
/** A board's modem power-control capability description. */
|
|
11
|
+
export interface PowerCapability {
|
|
12
|
+
readonly power: PowerCapabilityKind;
|
|
13
|
+
/** The board can pulse a USB-level reset on the modem's port. */
|
|
14
|
+
readonly usbReset?: boolean;
|
|
15
|
+
/** How long to wait for the modem to re-enumerate after a cycle. */
|
|
16
|
+
readonly enumerationTimeoutMs: number;
|
|
17
|
+
/** Which USB mode to prefer when the modem re-enumerates (A4.2 catalog vocabulary). */
|
|
18
|
+
readonly preferredUsbMode?: PreferredUsbMode;
|
|
19
|
+
}
|
|
20
|
+
/** The Phase-A capability: no board power control exists — a no-op. */
|
|
21
|
+
export declare const NONE_POWER_CAPABILITY: PowerCapability;
|
|
22
|
+
/** Outcome of asking the power hook to cycle a modem. */
|
|
23
|
+
export interface PowerCycleResult {
|
|
24
|
+
readonly status: 'applied' | 'unsupported' | 'failed';
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
}
|
|
27
|
+
/** The minimal context handed to the power hook. */
|
|
28
|
+
export interface PowerCycleContext {
|
|
29
|
+
readonly stableKey: string;
|
|
30
|
+
readonly at: EpochMillis;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The pluggable power-cycle hook (recovery ladder rung 4). Phase A ships only the
|
|
34
|
+
* `none` no-op; a real GPIO / PWRKEY / USB-hub implementation is hardware-gated and
|
|
35
|
+
* injected later without changing the ladder.
|
|
36
|
+
*/
|
|
37
|
+
export interface PowerHook {
|
|
38
|
+
readonly capability: PowerCapability;
|
|
39
|
+
cycle(context: PowerCycleContext): Promise<PowerCycleResult>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build a Phase-A power hook for `capability`. EVERY capability returns
|
|
43
|
+
* `unsupported`: `none` because there is nothing to cut, and every real mechanism
|
|
44
|
+
* because its hardware driver is not implemented yet — the contract field exists so
|
|
45
|
+
* a board can DECLARE the capability, but rung 4 never actuates in Phase A.
|
|
46
|
+
*/
|
|
47
|
+
export declare function unsupportedPowerHook(capability: PowerCapability): PowerHook;
|
|
48
|
+
/** The Phase-A default power hook: describes `none` and always returns `unsupported`. */
|
|
49
|
+
export declare const NONE_POWER_HOOK: PowerHook;
|