@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,71 @@
|
|
|
1
|
+
import type { DeviceGeneration } from '../../domain/index.js';
|
|
2
|
+
import type { AppliedConfiguration, DesiredProfile, ModemStateView, NormalizationContext, ObservedState, StateDivergence } from '../../observations/index.js';
|
|
3
|
+
import type { ConnectionId, DeviceIfname, GsmProfile, NetworkManagerPort } from '../../ports/index.js';
|
|
4
|
+
import type { NmApplyResult, NmBearerState, NmDesiredRequest, NmObservationInput, NmObservationResult, NmSaveResult } from './types.js';
|
|
5
|
+
export interface NetworkManagerAdapterOptions {
|
|
6
|
+
readonly port: NetworkManagerPort;
|
|
7
|
+
/** Clock for the desired/applied slots only; observations carry their own. */
|
|
8
|
+
readonly now?: () => number;
|
|
9
|
+
}
|
|
10
|
+
export interface NmApplyOptions {
|
|
11
|
+
readonly operationId: string;
|
|
12
|
+
readonly generation: DeviceGeneration;
|
|
13
|
+
}
|
|
14
|
+
export declare class NetworkManagerAdapter {
|
|
15
|
+
#private;
|
|
16
|
+
constructor(options: NetworkManagerAdapterOptions);
|
|
17
|
+
/** Every connection this adapter holds a slot for, in insertion order. */
|
|
18
|
+
trackedConnections(): readonly ConnectionId[];
|
|
19
|
+
/**
|
|
20
|
+
* Save an operator's profile and record it as DESIRED.
|
|
21
|
+
*
|
|
22
|
+
* The desired slot is built from what the caller ASKED for, not from what NM read
|
|
23
|
+
* back. That is the whole point of the slot: if NM normalized a field on the way
|
|
24
|
+
* in, the next `describeStateDivergence` is supposed to report it, and seeding
|
|
25
|
+
* desired from the readback would make that divergence structurally invisible.
|
|
26
|
+
*/
|
|
27
|
+
saveDesiredProfile(request: NmDesiredRequest): Promise<NmSaveResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Record that the operator wants NO bearer on this connection's device.
|
|
30
|
+
*
|
|
31
|
+
* It performs no I/O on purpose — desired is a record of intent, and the
|
|
32
|
+
* deactivation it implies happens when `applyDesired` is called, so a request and
|
|
33
|
+
* its execution stay two separately observable facts.
|
|
34
|
+
*/
|
|
35
|
+
releaseDesired(id: ConnectionId, requestedBy: string): NmSaveResult;
|
|
36
|
+
desiredFor(id: ConnectionId): DesiredProfile<NmBearerState> | null;
|
|
37
|
+
/** The saved profile as NM itself holds it — a straight read, no slot involved. */
|
|
38
|
+
readSavedProfile(id: ConnectionId): Promise<GsmProfile | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* Put the desired state into force and record what was applied.
|
|
41
|
+
*
|
|
42
|
+
* The applied slot is built from the profile NM READ BACK, not from the request:
|
|
43
|
+
* "what was actually put into force" is a claim about NM's content, and asserting
|
|
44
|
+
* it from the input would make a silently-rejected field look applied.
|
|
45
|
+
*/
|
|
46
|
+
applyDesired(id: ConnectionId, options: NmApplyOptions): Promise<NmApplyResult>;
|
|
47
|
+
appliedFor(id: ConnectionId): AppliedConfiguration<NmBearerState> | null;
|
|
48
|
+
/**
|
|
49
|
+
* The interface the applied bearer landed on, or `null` when no bearer is in
|
|
50
|
+
* force. An applied `unbound` state resolves to `null` rather than to its device:
|
|
51
|
+
* there is a device, but no bearer on it, and returning the name would read as one.
|
|
52
|
+
*/
|
|
53
|
+
resolveAppliedInterface(id: ConnectionId): DeviceIfname | null;
|
|
54
|
+
/**
|
|
55
|
+
* Fold one complete NM readout.
|
|
56
|
+
*
|
|
57
|
+
* Never touches the desired slot. Clears the applied slot only when the readout
|
|
58
|
+
* positively contradicts it, and always says which of the four contradictions it
|
|
59
|
+
* was.
|
|
60
|
+
*/
|
|
61
|
+
observe(input: NmObservationInput): NmObservationResult;
|
|
62
|
+
observedFor(id: ConnectionId): ObservedState<NmBearerState> | null;
|
|
63
|
+
/**
|
|
64
|
+
* The three-slot view. `context` supplies the provenance for the "we have not
|
|
65
|
+
* observed this yet" case — this layer has no clock or epoch counter of its own
|
|
66
|
+
* for observations, so it cannot manufacture one.
|
|
67
|
+
*/
|
|
68
|
+
stateView(id: ConnectionId, context: NormalizationContext): ModemStateView<NmBearerState, NmBearerState, NmBearerState> | null;
|
|
69
|
+
/** `desiredVsApplied` ("did our write happen") and `appliedVsObserved` ("did it stick"). */
|
|
70
|
+
divergence(id: ConnectionId, context: NormalizationContext): StateDivergence | null;
|
|
71
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// The thin NetworkManager adapter: desired profiles, applied bearers, and the
|
|
2
|
+
// interface each one landed on — the saved-vs-applied boundary and nothing else.
|
|
3
|
+
//
|
|
4
|
+
// It is the ONLY bearer/APN authority surface in the package. `ModemManagerPort` has
|
|
5
|
+
// no connect verb by construction (`ports/forbidden-surface.test.ts`), and the
|
|
6
|
+
// ModemManager provider has no bearer operation, so nothing else can put a bearer into
|
|
7
|
+
// force. Correspondingly, this adapter performs no radio, band, SIM or power operation:
|
|
8
|
+
// those belong to the ModemManager provider, and expressing them here would create a
|
|
9
|
+
// second writer for a resource the ownership matrix gives to exactly one.
|
|
10
|
+
//
|
|
11
|
+
// Two rules shape every method below.
|
|
12
|
+
//
|
|
13
|
+
// 1. OBSERVED STATE NEVER WRITES THE DESIRED SLOT. `observe()` may clear the applied
|
|
14
|
+
// slot and always rewrites the observed one; it does not touch `desired` on any
|
|
15
|
+
// path. Reality overtaking a write does not un-ask the question the operator asked
|
|
16
|
+
// — and if it did, a re-enumeration would silently erase the configuration the
|
|
17
|
+
// controller is supposed to restore.
|
|
18
|
+
// 2. WRITES GO THROUGH THE PORT'S TYPED OPERATIONS, AND ONLY THOSE. The NM half of the
|
|
19
|
+
// port-tagged op set (`ports/ops.ts` `NmOp`) is create / update / activate /
|
|
20
|
+
// deactivate. There is deliberately no delete path here: profile removal is not in
|
|
21
|
+
// that set, so this adapter cannot express it.
|
|
22
|
+
import { epochMillis } from '../../domain/index.js';
|
|
23
|
+
import { appliedConfiguration, describeStateDivergence, desiredProfile, freshObservation, observedState, unavailableObservation, } from '../../observations/index.js';
|
|
24
|
+
import { receipt } from '../../ports/index.js';
|
|
25
|
+
import { boundBearer, nmBearerStateEquals, unboundBearer } from './types.js';
|
|
26
|
+
const SOURCE = 'networkmanager';
|
|
27
|
+
/** The device states in which NM is still settling an activation, not losing it. */
|
|
28
|
+
const TRANSITIONAL_STATES = new Set([
|
|
29
|
+
'prepare',
|
|
30
|
+
'config',
|
|
31
|
+
'need-auth',
|
|
32
|
+
'ip-config',
|
|
33
|
+
'ip-check',
|
|
34
|
+
'secondaries',
|
|
35
|
+
'deactivating',
|
|
36
|
+
]);
|
|
37
|
+
export class NetworkManagerAdapter {
|
|
38
|
+
#port;
|
|
39
|
+
#now;
|
|
40
|
+
#slots = new Map();
|
|
41
|
+
#observedGeneration = null;
|
|
42
|
+
constructor(options) {
|
|
43
|
+
this.#port = options.port;
|
|
44
|
+
this.#now = options.now ?? Date.now;
|
|
45
|
+
}
|
|
46
|
+
/** Every connection this adapter holds a slot for, in insertion order. */
|
|
47
|
+
trackedConnections() {
|
|
48
|
+
return [...this.#slots.keys()];
|
|
49
|
+
}
|
|
50
|
+
// ── desired ────────────────────────────────────────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* Save an operator's profile and record it as DESIRED.
|
|
53
|
+
*
|
|
54
|
+
* The desired slot is built from what the caller ASKED for, not from what NM read
|
|
55
|
+
* back. That is the whole point of the slot: if NM normalized a field on the way
|
|
56
|
+
* in, the next `describeStateDivergence` is supposed to report it, and seeding
|
|
57
|
+
* desired from the readback would make that divergence structurally invisible.
|
|
58
|
+
*/
|
|
59
|
+
async saveDesiredProfile(request) {
|
|
60
|
+
let saved;
|
|
61
|
+
try {
|
|
62
|
+
saved =
|
|
63
|
+
request.connectionId === undefined
|
|
64
|
+
? await this.#port.createGsmProfile(request.profile)
|
|
65
|
+
: await this.#port.updateGsmProfile(request.connectionId, request.profile);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
reason: 'write-failed',
|
|
71
|
+
receipt: receipt('connection', 'failed', describeError(error)),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const id = saved.connectionId;
|
|
75
|
+
const desired = desiredProfile(boundBearer({
|
|
76
|
+
connectionId: id,
|
|
77
|
+
deviceIfname: request.deviceIfname,
|
|
78
|
+
apn: request.profile.apn,
|
|
79
|
+
autoConfig: request.profile.autoConfig,
|
|
80
|
+
homeOnly: request.profile.homeOnly,
|
|
81
|
+
}), epochMillis(this.#now()), request.requestedBy);
|
|
82
|
+
this.#slotsFor(id).desired = desired;
|
|
83
|
+
return { ok: true, connectionId: id, desired };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Record that the operator wants NO bearer on this connection's device.
|
|
87
|
+
*
|
|
88
|
+
* It performs no I/O on purpose — desired is a record of intent, and the
|
|
89
|
+
* deactivation it implies happens when `applyDesired` is called, so a request and
|
|
90
|
+
* its execution stay two separately observable facts.
|
|
91
|
+
*/
|
|
92
|
+
releaseDesired(id, requestedBy) {
|
|
93
|
+
const slots = this.#slots.get(id);
|
|
94
|
+
const ifname = slots === undefined ? undefined : targetIfname(slots);
|
|
95
|
+
if (ifname === undefined) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
reason: 'no-desired-profile',
|
|
99
|
+
receipt: receipt('connection', 'failed', `no tracked device for connection ${id}`),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const desired = desiredProfile(unboundBearer(ifname), epochMillis(this.#now()), requestedBy);
|
|
103
|
+
this.#slotsFor(id).desired = desired;
|
|
104
|
+
return { ok: true, connectionId: id, desired };
|
|
105
|
+
}
|
|
106
|
+
desiredFor(id) {
|
|
107
|
+
return this.#slots.get(id)?.desired ?? null;
|
|
108
|
+
}
|
|
109
|
+
/** The saved profile as NM itself holds it — a straight read, no slot involved. */
|
|
110
|
+
async readSavedProfile(id) {
|
|
111
|
+
return await this.#port.readGsmProfile(id);
|
|
112
|
+
}
|
|
113
|
+
// ── applied ────────────────────────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Put the desired state into force and record what was applied.
|
|
116
|
+
*
|
|
117
|
+
* The applied slot is built from the profile NM READ BACK, not from the request:
|
|
118
|
+
* "what was actually put into force" is a claim about NM's content, and asserting
|
|
119
|
+
* it from the input would make a silently-rejected field look applied.
|
|
120
|
+
*/
|
|
121
|
+
async applyDesired(id, options) {
|
|
122
|
+
const desired = this.desiredFor(id);
|
|
123
|
+
if (desired === null) {
|
|
124
|
+
return refuse('no-desired-profile', `connection ${id} has no desired profile`);
|
|
125
|
+
}
|
|
126
|
+
return desired.profile.kind === 'unbound'
|
|
127
|
+
? await this.#deactivate(id, desired.profile.deviceIfname, options)
|
|
128
|
+
: await this.#activate(id, desired.profile, options);
|
|
129
|
+
}
|
|
130
|
+
appliedFor(id) {
|
|
131
|
+
return this.#slots.get(id)?.applied ?? null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The interface the applied bearer landed on, or `null` when no bearer is in
|
|
135
|
+
* force. An applied `unbound` state resolves to `null` rather than to its device:
|
|
136
|
+
* there is a device, but no bearer on it, and returning the name would read as one.
|
|
137
|
+
*/
|
|
138
|
+
resolveAppliedInterface(id) {
|
|
139
|
+
const applied = this.appliedFor(id);
|
|
140
|
+
if (applied === null || applied.configuration.kind !== 'bound') {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return applied.configuration.binding.deviceIfname;
|
|
144
|
+
}
|
|
145
|
+
// ── observed ───────────────────────────────────────────────────────────────
|
|
146
|
+
/**
|
|
147
|
+
* Fold one complete NM readout.
|
|
148
|
+
*
|
|
149
|
+
* Never touches the desired slot. Clears the applied slot only when the readout
|
|
150
|
+
* positively contradicts it, and always says which of the four contradictions it
|
|
151
|
+
* was.
|
|
152
|
+
*/
|
|
153
|
+
observe(input) {
|
|
154
|
+
const generation = input.context.generation;
|
|
155
|
+
if (this.#observedGeneration !== null && generation < this.#observedGeneration) {
|
|
156
|
+
return {
|
|
157
|
+
kind: 'refused',
|
|
158
|
+
reason: 'superseded-generation',
|
|
159
|
+
currentGeneration: this.#observedGeneration,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
this.#observedGeneration = generation;
|
|
163
|
+
const devices = new Map(input.devices.map((device) => [device.ifname, device]));
|
|
164
|
+
const outcomes = [];
|
|
165
|
+
const losses = [];
|
|
166
|
+
for (const [id, slots] of this.#slots) {
|
|
167
|
+
const ifname = targetIfname(slots);
|
|
168
|
+
if (ifname === undefined) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const device = devices.get(ifname);
|
|
172
|
+
slots.observed = observedState(device === undefined
|
|
173
|
+
? unavailableObservation(SOURCE, input.context, 'device-absent')
|
|
174
|
+
: freshObservation(SOURCE, input.context, observedBearer(device)));
|
|
175
|
+
const outcome = this.#classify(id, slots, ifname, device, input.context.observedAt);
|
|
176
|
+
outcomes.push({ connectionId: id, outcome });
|
|
177
|
+
if (outcome.status === 'lost') {
|
|
178
|
+
losses.push(outcome.loss);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
kind: 'accepted',
|
|
183
|
+
generation,
|
|
184
|
+
observedAt: input.context.observedAt,
|
|
185
|
+
outcomes,
|
|
186
|
+
losses,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
observedFor(id) {
|
|
190
|
+
return this.#slots.get(id)?.observed ?? null;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The three-slot view. `context` supplies the provenance for the "we have not
|
|
194
|
+
* observed this yet" case — this layer has no clock or epoch counter of its own
|
|
195
|
+
* for observations, so it cannot manufacture one.
|
|
196
|
+
*/
|
|
197
|
+
stateView(id, context) {
|
|
198
|
+
const slots = this.#slots.get(id);
|
|
199
|
+
if (slots === undefined) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
desired: slots.desired,
|
|
204
|
+
applied: slots.applied,
|
|
205
|
+
observed: slots.observed ??
|
|
206
|
+
observedState(unavailableObservation(SOURCE, context, 'provider-unavailable')),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** `desiredVsApplied` ("did our write happen") and `appliedVsObserved` ("did it stick"). */
|
|
210
|
+
divergence(id, context) {
|
|
211
|
+
const view = this.stateView(id, context);
|
|
212
|
+
return view === null ? null : describeStateDivergence(view, nmBearerStateEquals);
|
|
213
|
+
}
|
|
214
|
+
// ── internals ──────────────────────────────────────────────────────────────
|
|
215
|
+
async #activate(id, desired, options) {
|
|
216
|
+
if (desired.kind !== 'bound') {
|
|
217
|
+
return refuse('no-desired-profile', `connection ${id} has no bearer to activate`);
|
|
218
|
+
}
|
|
219
|
+
const ifname = desired.binding.deviceIfname;
|
|
220
|
+
const saved = await this.#port.readGsmProfile(id);
|
|
221
|
+
if (saved === undefined) {
|
|
222
|
+
return refuse('profile-absent', `connection ${id} is no longer saved in NetworkManager`);
|
|
223
|
+
}
|
|
224
|
+
let activation;
|
|
225
|
+
try {
|
|
226
|
+
activation = await this.#port.activate(id, ifname);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
return refuse('activation-failed', describeError(error));
|
|
230
|
+
}
|
|
231
|
+
if (activation.status !== 'applied') {
|
|
232
|
+
return { ok: false, reason: 'activation-failed', receipt: activation };
|
|
233
|
+
}
|
|
234
|
+
const applied = appliedConfiguration({
|
|
235
|
+
configuration: boundBearer({
|
|
236
|
+
connectionId: id,
|
|
237
|
+
deviceIfname: ifname,
|
|
238
|
+
apn: saved.apn,
|
|
239
|
+
autoConfig: saved.autoConfig,
|
|
240
|
+
homeOnly: saved.homeOnly,
|
|
241
|
+
}),
|
|
242
|
+
appliedAt: epochMillis(this.#now()),
|
|
243
|
+
generation: options.generation,
|
|
244
|
+
operationId: options.operationId,
|
|
245
|
+
});
|
|
246
|
+
this.#slotsFor(id).applied = applied;
|
|
247
|
+
return { ok: true, applied, receipt: activation };
|
|
248
|
+
}
|
|
249
|
+
async #deactivate(id, ifname, options) {
|
|
250
|
+
let result;
|
|
251
|
+
try {
|
|
252
|
+
result = await this.#port.deactivate(id, ifname);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
return refuse('deactivation-failed', describeError(error));
|
|
256
|
+
}
|
|
257
|
+
if (result.status !== 'applied') {
|
|
258
|
+
return { ok: false, reason: 'deactivation-failed', receipt: result };
|
|
259
|
+
}
|
|
260
|
+
const applied = appliedConfiguration({
|
|
261
|
+
configuration: unboundBearer(ifname),
|
|
262
|
+
appliedAt: epochMillis(this.#now()),
|
|
263
|
+
generation: options.generation,
|
|
264
|
+
operationId: options.operationId,
|
|
265
|
+
});
|
|
266
|
+
this.#slotsFor(id).applied = applied;
|
|
267
|
+
return { ok: true, applied, receipt: result };
|
|
268
|
+
}
|
|
269
|
+
#classify(id, slots, ifname, device, observedAt) {
|
|
270
|
+
const applied = slots.applied;
|
|
271
|
+
if (applied === null) {
|
|
272
|
+
return { status: 'unapplied' };
|
|
273
|
+
}
|
|
274
|
+
const lose = (reason) => {
|
|
275
|
+
slots.applied = null;
|
|
276
|
+
return {
|
|
277
|
+
status: 'lost',
|
|
278
|
+
loss: {
|
|
279
|
+
connectionId: id,
|
|
280
|
+
deviceIfname: ifname,
|
|
281
|
+
reason,
|
|
282
|
+
lostAt: observedAt,
|
|
283
|
+
generation: applied.generation,
|
|
284
|
+
previous: applied,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
if (device === undefined) {
|
|
289
|
+
return lose('interface-absent');
|
|
290
|
+
}
|
|
291
|
+
if (device.state === 'failed') {
|
|
292
|
+
return lose('activation-failed');
|
|
293
|
+
}
|
|
294
|
+
if (applied.configuration.kind === 'unbound') {
|
|
295
|
+
// We deliberately took the bearer down; anything active here is somebody else.
|
|
296
|
+
return device.activeConnection === undefined
|
|
297
|
+
? { status: 'retained', applied }
|
|
298
|
+
: lose('connection-replaced');
|
|
299
|
+
}
|
|
300
|
+
if (device.activeConnection === undefined) {
|
|
301
|
+
return lose('interface-detached');
|
|
302
|
+
}
|
|
303
|
+
if (device.activeConnection.connectionId !== id) {
|
|
304
|
+
return lose('connection-replaced');
|
|
305
|
+
}
|
|
306
|
+
if (device.state === 'activated') {
|
|
307
|
+
return { status: 'retained', applied };
|
|
308
|
+
}
|
|
309
|
+
return TRANSITIONAL_STATES.has(device.state)
|
|
310
|
+
? { status: 'pending', applied, deviceState: device.state }
|
|
311
|
+
: lose('interface-detached');
|
|
312
|
+
}
|
|
313
|
+
#slotsFor(id) {
|
|
314
|
+
const existing = this.#slots.get(id);
|
|
315
|
+
if (existing !== undefined) {
|
|
316
|
+
return existing;
|
|
317
|
+
}
|
|
318
|
+
const created = { desired: null, applied: null, observed: null };
|
|
319
|
+
this.#slots.set(id, created);
|
|
320
|
+
return created;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/** The device the slots are about: where the bearer IS, else where it was asked for. */
|
|
324
|
+
function targetIfname(slots) {
|
|
325
|
+
const state = slots.applied?.configuration ?? slots.desired?.profile;
|
|
326
|
+
if (state === undefined) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
return state.kind === 'bound' ? state.binding.deviceIfname : state.deviceIfname;
|
|
330
|
+
}
|
|
331
|
+
function observedBearer(device) {
|
|
332
|
+
const active = device.activeConnection;
|
|
333
|
+
return active === undefined
|
|
334
|
+
? unboundBearer(device.ifname)
|
|
335
|
+
: boundBearer({
|
|
336
|
+
connectionId: active.connectionId,
|
|
337
|
+
deviceIfname: device.ifname,
|
|
338
|
+
apn: active.apn,
|
|
339
|
+
autoConfig: active.autoConfig,
|
|
340
|
+
homeOnly: active.homeOnly,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function refuse(reason, message) {
|
|
344
|
+
return { ok: false, reason, receipt: receipt('connection', 'failed', message) };
|
|
345
|
+
}
|
|
346
|
+
function describeError(error) {
|
|
347
|
+
return error instanceof Error ? error.message : 'NetworkManager rejected the request';
|
|
348
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { DeviceGeneration, EpochMillis } from '../../domain/index.js';
|
|
2
|
+
import type { AppliedConfiguration, DesiredProfile, NormalizationContext } from '../../observations/index.js';
|
|
3
|
+
import type { ConnectionId, DeviceIfname, GsmProfileInput, Receipt } from '../../ports/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* NM's own device states (`NMDeviceState`), by name.
|
|
6
|
+
*
|
|
7
|
+
* The transitional members are kept rather than collapsed into "not activated",
|
|
8
|
+
* because a device in `prepare` carrying our connection is COMING UP, and reporting
|
|
9
|
+
* that as a lost bearer would turn every ordinary activation into a false alarm.
|
|
10
|
+
*/
|
|
11
|
+
export declare const NM_DEVICE_STATES: readonly ["unknown", "unmanaged", "unavailable", "disconnected", "prepare", "config", "need-auth", "ip-config", "ip-check", "secondaries", "activated", "deactivating", "failed"];
|
|
12
|
+
export type NmDeviceState = (typeof NM_DEVICE_STATES)[number];
|
|
13
|
+
/**
|
|
14
|
+
* The bearer identity all three slots compare in.
|
|
15
|
+
*
|
|
16
|
+
* `username` and `password` are deliberately NOT members. A state slot is read,
|
|
17
|
+
* compared, and surfaced in divergence output; `gsm.password` is the one field in the
|
|
18
|
+
* profile the redaction module masks everywhere else, so putting it in a comparable
|
|
19
|
+
* value would re-open that hole through a side door. The credential lives in NM, is
|
|
20
|
+
* written through the port, and is never mirrored here.
|
|
21
|
+
*/
|
|
22
|
+
export interface NmBearerBinding {
|
|
23
|
+
readonly connectionId: ConnectionId;
|
|
24
|
+
readonly deviceIfname: DeviceIfname;
|
|
25
|
+
readonly apn: string;
|
|
26
|
+
readonly autoConfig: boolean;
|
|
27
|
+
readonly homeOnly: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A bearer, or the positive absence of one.
|
|
31
|
+
*
|
|
32
|
+
* `unbound` exists so "NM answered, and it says nothing is in force on this device"
|
|
33
|
+
* is a VALUE rather than an unavailable observation. The distinction is load-bearing:
|
|
34
|
+
* an unavailable observation compares `indeterminate` against everything, which is
|
|
35
|
+
* right for "the device is gone" and wrong for "the device is here and idle" — the
|
|
36
|
+
* second is a definite divergence from a desired bearer, and an operator needs to see
|
|
37
|
+
* it as one.
|
|
38
|
+
*/
|
|
39
|
+
export type NmBearerState = {
|
|
40
|
+
readonly kind: 'bound';
|
|
41
|
+
readonly binding: NmBearerBinding;
|
|
42
|
+
} | {
|
|
43
|
+
readonly kind: 'unbound';
|
|
44
|
+
readonly deviceIfname: DeviceIfname;
|
|
45
|
+
};
|
|
46
|
+
export declare function boundBearer(binding: NmBearerBinding): NmBearerState;
|
|
47
|
+
export declare function unboundBearer(deviceIfname: DeviceIfname): NmBearerState;
|
|
48
|
+
export declare function nmBearerBindingEquals(left: NmBearerBinding, right: NmBearerBinding): boolean;
|
|
49
|
+
/** The equality `describeStateDivergence` is driven with for NM state. */
|
|
50
|
+
export declare function nmBearerStateEquals(left: NmBearerState, right: NmBearerState): boolean;
|
|
51
|
+
/** The connection NM reports active on a device, with the settings it put in force. */
|
|
52
|
+
export interface NmObservedActiveConnection {
|
|
53
|
+
readonly connectionId: ConnectionId;
|
|
54
|
+
readonly apn: string;
|
|
55
|
+
readonly autoConfig: boolean;
|
|
56
|
+
readonly homeOnly: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** One device exactly as NM reports it in a readout. */
|
|
59
|
+
export interface NmObservedDevice {
|
|
60
|
+
readonly ifname: DeviceIfname;
|
|
61
|
+
readonly state: NmDeviceState;
|
|
62
|
+
readonly activeConnection?: NmObservedActiveConnection;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* ONE COMPLETE NM readout — an enumeration, never a delta.
|
|
66
|
+
*
|
|
67
|
+
* That is what makes re-enumeration detectable at all: a device missing from `devices`
|
|
68
|
+
* means the device is GONE, and a delta stream has no way to say that without a
|
|
69
|
+
* removal event nobody guarantees will arrive.
|
|
70
|
+
*
|
|
71
|
+
* `context` supplies the clock, the generation and the source epoch, exactly as the
|
|
72
|
+
* observation layer requires — this adapter has no clock of its own for observations
|
|
73
|
+
* and therefore cannot stamp a readout with a time it did not come from.
|
|
74
|
+
*/
|
|
75
|
+
export interface NmObservationInput {
|
|
76
|
+
readonly context: NormalizationContext;
|
|
77
|
+
readonly devices: readonly NmObservedDevice[];
|
|
78
|
+
}
|
|
79
|
+
/** What an operator asked for: a saved profile, targeted at an exact device. */
|
|
80
|
+
export interface NmDesiredRequest {
|
|
81
|
+
readonly profile: GsmProfileInput;
|
|
82
|
+
readonly deviceIfname: DeviceIfname;
|
|
83
|
+
/** Opaque origin label (an RPC caller, a policy engine). Never a credential. */
|
|
84
|
+
readonly requestedBy: string;
|
|
85
|
+
/** Update this existing profile instead of creating a new one. */
|
|
86
|
+
readonly connectionId?: ConnectionId;
|
|
87
|
+
}
|
|
88
|
+
export declare const NM_ADAPTER_REFUSAL_REASONS: readonly ["no-desired-profile", "profile-absent", "activation-failed", "deactivation-failed", "write-failed"];
|
|
89
|
+
export type NmAdapterRefusalReason = (typeof NM_ADAPTER_REFUSAL_REASONS)[number];
|
|
90
|
+
export type NmSaveResult = {
|
|
91
|
+
readonly ok: true;
|
|
92
|
+
readonly connectionId: ConnectionId;
|
|
93
|
+
readonly desired: DesiredProfile<NmBearerState>;
|
|
94
|
+
} | {
|
|
95
|
+
readonly ok: false;
|
|
96
|
+
readonly reason: NmAdapterRefusalReason;
|
|
97
|
+
readonly receipt: Receipt;
|
|
98
|
+
};
|
|
99
|
+
/** Applying a desired profile: the write either took, or it did not and says why. */
|
|
100
|
+
export type NmApplyResult = {
|
|
101
|
+
readonly ok: true;
|
|
102
|
+
readonly applied: AppliedConfiguration<NmBearerState>;
|
|
103
|
+
readonly receipt: Receipt;
|
|
104
|
+
} | {
|
|
105
|
+
readonly ok: false;
|
|
106
|
+
readonly reason: NmAdapterRefusalReason;
|
|
107
|
+
readonly receipt: Receipt;
|
|
108
|
+
};
|
|
109
|
+
export declare const NM_APPLIED_LOSS_REASONS: readonly ["interface-absent", "interface-detached", "connection-replaced", "activation-failed"];
|
|
110
|
+
export type NmAppliedLossReason = (typeof NM_APPLIED_LOSS_REASONS)[number];
|
|
111
|
+
/**
|
|
112
|
+
* The applied bearer stopped being in force.
|
|
113
|
+
*
|
|
114
|
+
* `previous` is retained rather than discarded: a caller deciding whether to re-apply
|
|
115
|
+
* or to roll back needs to know what WAS in force, and the applied slot has just been
|
|
116
|
+
* cleared precisely because it no longer describes reality.
|
|
117
|
+
*/
|
|
118
|
+
export interface NmAppliedLoss {
|
|
119
|
+
readonly connectionId: ConnectionId;
|
|
120
|
+
readonly deviceIfname: DeviceIfname;
|
|
121
|
+
readonly reason: NmAppliedLossReason;
|
|
122
|
+
readonly lostAt: EpochMillis;
|
|
123
|
+
readonly generation: DeviceGeneration;
|
|
124
|
+
readonly previous: AppliedConfiguration<NmBearerState>;
|
|
125
|
+
}
|
|
126
|
+
export type NmAppliedOutcome =
|
|
127
|
+
/** A desired profile is tracked, and nothing has been put into force for it yet. */
|
|
128
|
+
{
|
|
129
|
+
readonly status: 'unapplied';
|
|
130
|
+
} | {
|
|
131
|
+
readonly status: 'retained';
|
|
132
|
+
readonly applied: AppliedConfiguration<NmBearerState>;
|
|
133
|
+
} | {
|
|
134
|
+
readonly status: 'pending';
|
|
135
|
+
readonly applied: AppliedConfiguration<NmBearerState>;
|
|
136
|
+
readonly deviceState: NmDeviceState;
|
|
137
|
+
} | {
|
|
138
|
+
readonly status: 'lost';
|
|
139
|
+
readonly loss: NmAppliedLoss;
|
|
140
|
+
};
|
|
141
|
+
export interface NmConnectionOutcome {
|
|
142
|
+
readonly connectionId: ConnectionId;
|
|
143
|
+
readonly outcome: NmAppliedOutcome;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The result of folding one readout.
|
|
147
|
+
*
|
|
148
|
+
* A readout from a superseded generation is REFUSED rather than applied late: the
|
|
149
|
+
* generation fence exists so a reply about a previous enumeration cannot clear applied
|
|
150
|
+
* state that belongs to the current one.
|
|
151
|
+
*/
|
|
152
|
+
export type NmObservationResult = {
|
|
153
|
+
readonly kind: 'refused';
|
|
154
|
+
readonly reason: 'superseded-generation';
|
|
155
|
+
readonly currentGeneration: DeviceGeneration;
|
|
156
|
+
} | {
|
|
157
|
+
readonly kind: 'accepted';
|
|
158
|
+
readonly generation: DeviceGeneration;
|
|
159
|
+
readonly observedAt: EpochMillis;
|
|
160
|
+
readonly outcomes: readonly NmConnectionOutcome[];
|
|
161
|
+
/** Just the losses, so the case that matters is not behind a filter. */
|
|
162
|
+
readonly losses: readonly NmAppliedLoss[];
|
|
163
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The saved-vs-applied vocabulary, in NetworkManager's own terms.
|
|
2
|
+
//
|
|
3
|
+
// NM is the sole writer of bearers, APN, auth, roaming, autoconnect and activation
|
|
4
|
+
// (`ports/README.md` ownership matrix), and it is also the stack that draws the
|
|
5
|
+
// distinction this whole module is built on: a connection PROFILE is what was saved,
|
|
6
|
+
// an ACTIVE connection is what was actually put into force on a device, and the device
|
|
7
|
+
// itself reports what it is currently doing. `observations/state-separation.ts` models
|
|
8
|
+
// those three slots generically; this file gives them an NM-shaped payload.
|
|
9
|
+
//
|
|
10
|
+
// Radio, band, SIM and power operations are deliberately absent. Those belong to the
|
|
11
|
+
// ModemManager provider, and a second surface expressing them here would make two
|
|
12
|
+
// writers for one resource — the exact thing the ownership matrix exists to prevent.
|
|
13
|
+
// Nothing here carries a `PhysicalModemId` either: every slot is keyed by NM's own
|
|
14
|
+
// connection UUID, so this adapter can never become a second authority on which
|
|
15
|
+
// physical modem is which.
|
|
16
|
+
/**
|
|
17
|
+
* NM's own device states (`NMDeviceState`), by name.
|
|
18
|
+
*
|
|
19
|
+
* The transitional members are kept rather than collapsed into "not activated",
|
|
20
|
+
* because a device in `prepare` carrying our connection is COMING UP, and reporting
|
|
21
|
+
* that as a lost bearer would turn every ordinary activation into a false alarm.
|
|
22
|
+
*/
|
|
23
|
+
export const NM_DEVICE_STATES = [
|
|
24
|
+
'unknown',
|
|
25
|
+
'unmanaged',
|
|
26
|
+
'unavailable',
|
|
27
|
+
'disconnected',
|
|
28
|
+
'prepare',
|
|
29
|
+
'config',
|
|
30
|
+
'need-auth',
|
|
31
|
+
'ip-config',
|
|
32
|
+
'ip-check',
|
|
33
|
+
'secondaries',
|
|
34
|
+
'activated',
|
|
35
|
+
'deactivating',
|
|
36
|
+
'failed',
|
|
37
|
+
];
|
|
38
|
+
export function boundBearer(binding) {
|
|
39
|
+
return { kind: 'bound', binding };
|
|
40
|
+
}
|
|
41
|
+
export function unboundBearer(deviceIfname) {
|
|
42
|
+
return { kind: 'unbound', deviceIfname };
|
|
43
|
+
}
|
|
44
|
+
export function nmBearerBindingEquals(left, right) {
|
|
45
|
+
return (left.connectionId === right.connectionId &&
|
|
46
|
+
left.deviceIfname === right.deviceIfname &&
|
|
47
|
+
left.apn === right.apn &&
|
|
48
|
+
left.autoConfig === right.autoConfig &&
|
|
49
|
+
left.homeOnly === right.homeOnly);
|
|
50
|
+
}
|
|
51
|
+
/** The equality `describeStateDivergence` is driven with for NM state. */
|
|
52
|
+
export function nmBearerStateEquals(left, right) {
|
|
53
|
+
if (left.kind === 'bound' && right.kind === 'bound') {
|
|
54
|
+
return nmBearerBindingEquals(left.binding, right.binding);
|
|
55
|
+
}
|
|
56
|
+
if (left.kind === 'unbound' && right.kind === 'unbound') {
|
|
57
|
+
return left.deviceIfname === right.deviceIfname;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
export const NM_ADAPTER_REFUSAL_REASONS = [
|
|
62
|
+
'no-desired-profile',
|
|
63
|
+
'profile-absent',
|
|
64
|
+
'activation-failed',
|
|
65
|
+
'deactivation-failed',
|
|
66
|
+
'write-failed',
|
|
67
|
+
];
|
|
68
|
+
export const NM_APPLIED_LOSS_REASONS = [
|
|
69
|
+
/** The readout has no such device at all — a re-enumeration, or an unplug. */
|
|
70
|
+
'interface-absent',
|
|
71
|
+
/** The device is present and carries no active connection. */
|
|
72
|
+
'interface-detached',
|
|
73
|
+
/** A DIFFERENT connection is active on our device. */
|
|
74
|
+
'connection-replaced',
|
|
75
|
+
/** NM reports the device itself failed. */
|
|
76
|
+
'activation-failed',
|
|
77
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DomainError } from '../domain/index.js';
|
|
2
|
+
import type { ProviderDefinition, ProviderOperationsSurface } from './contracts.js';
|
|
3
|
+
export declare class ProviderRegistryError extends DomainError {
|
|
4
|
+
readonly reason: 'duplicate-provider' | 'empty-profile-version';
|
|
5
|
+
readonly name = "ProviderRegistryError";
|
|
6
|
+
constructor(reason: 'duplicate-provider' | 'empty-profile-version');
|
|
7
|
+
}
|
|
8
|
+
export interface ProviderRegistry {
|
|
9
|
+
readonly revision: number;
|
|
10
|
+
register<TObservation, TOperations extends ProviderOperationsSurface>(definition: ProviderDefinition<TObservation, TOperations>): void;
|
|
11
|
+
list(): readonly ProviderDefinition[];
|
|
12
|
+
}
|
|
13
|
+
export declare function createProviderRegistry(): ProviderRegistry;
|