@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,211 @@
|
|
|
1
|
+
// The usage sampler — orchestrates the counter source, pure accounting reducer,
|
|
2
|
+
// billing-cycle math, and fail-soft persistence into one internal service.
|
|
3
|
+
//
|
|
4
|
+
// SESSION = the kernel boot id. Interface byte counters reset to zero on reboot, so
|
|
5
|
+
// the boot id both scopes every baseline key AND lets a reload detect a reboot: a
|
|
6
|
+
// persisted document from a different boot keeps its per-cycle totals but drops its
|
|
7
|
+
// baselines, so the first post-reboot sample re-baselines zero-delta.
|
|
8
|
+
//
|
|
9
|
+
// PERSISTENCE: state is written at most once per minute (rate-limited) plus a
|
|
10
|
+
// `flush()` shutdown hook. A crash therefore loses AT MOST ~1 minute of unpersisted
|
|
11
|
+
// deltas (the window since the last rate-limited write); a clean shutdown calls
|
|
12
|
+
// `flush()` and loses effectively nothing.
|
|
13
|
+
import { epochMillis } from '../../domain/index.js';
|
|
14
|
+
import { applySample, initialAccount } from './accounting.js';
|
|
15
|
+
import { cycleStart } from './billing-cycle.js';
|
|
16
|
+
import { USAGE_SCHEMA_VERSION } from './store.js';
|
|
17
|
+
const DEFAULT_PERSIST_INTERVAL_MS = 60_000;
|
|
18
|
+
const DEFAULT_CYCLE_DAY = 1;
|
|
19
|
+
function toPersistedSlot(logicalSlotId, account) {
|
|
20
|
+
return {
|
|
21
|
+
logicalSlotId,
|
|
22
|
+
cycleBytes: account.cycleBytes,
|
|
23
|
+
cycleStartMs: account.cycleStartMs,
|
|
24
|
+
...(account.key !== undefined
|
|
25
|
+
? { mappingGeneration: account.key.mappingGeneration, ifname: account.key.ifname }
|
|
26
|
+
: {}),
|
|
27
|
+
...(account.lastObserved !== undefined ? { lastObserved: account.lastObserved } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export class UsageSampler {
|
|
31
|
+
#bootId;
|
|
32
|
+
#source;
|
|
33
|
+
#store;
|
|
34
|
+
#now;
|
|
35
|
+
#persistIntervalMs;
|
|
36
|
+
#defaultCycleDay;
|
|
37
|
+
#accounts = new Map();
|
|
38
|
+
#policies = new Map();
|
|
39
|
+
// Policies written through `applyUsagePolicy` OUTRANK whatever an observation
|
|
40
|
+
// carries, for the life of the process. Without this, the next `sample()` would
|
|
41
|
+
// clobber a just-applied write with the policy the composition root happened to
|
|
42
|
+
// build its observation from — and the operator would watch their setting
|
|
43
|
+
// revert. The durable store is the source of truth for both, so an override and
|
|
44
|
+
// an observation can only ever disagree inside that window.
|
|
45
|
+
#policyOverrides = new Map();
|
|
46
|
+
#lastPersistMs;
|
|
47
|
+
#dirty = false;
|
|
48
|
+
constructor(options, initial) {
|
|
49
|
+
this.#bootId = options.bootId;
|
|
50
|
+
this.#source = options.source;
|
|
51
|
+
this.#store = options.store;
|
|
52
|
+
this.#now = options.now ?? Date.now;
|
|
53
|
+
this.#persistIntervalMs = options.persistIntervalMs ?? DEFAULT_PERSIST_INTERVAL_MS;
|
|
54
|
+
this.#defaultCycleDay = options.defaultCycleDay ?? DEFAULT_CYCLE_DAY;
|
|
55
|
+
this.#lastPersistMs = this.#now();
|
|
56
|
+
this.#hydrate(initial);
|
|
57
|
+
}
|
|
58
|
+
/** Load persisted state (recreating a fresh file if absent/corrupt) then build the sampler. */
|
|
59
|
+
static async create(options) {
|
|
60
|
+
const now = options.now ?? Date.now;
|
|
61
|
+
const initial = await options.store.load(options.bootId, now());
|
|
62
|
+
return new UsageSampler(options, initial);
|
|
63
|
+
}
|
|
64
|
+
/** Rebuild in-memory accounts. A reboot (differing boot id) drops the baselines. */
|
|
65
|
+
#hydrate(initial) {
|
|
66
|
+
const sameBoot = initial.bootId === this.#bootId;
|
|
67
|
+
for (const slot of initial.slots) {
|
|
68
|
+
const canResume = sameBoot &&
|
|
69
|
+
slot.ifname !== undefined &&
|
|
70
|
+
slot.mappingGeneration !== undefined &&
|
|
71
|
+
slot.lastObserved !== undefined;
|
|
72
|
+
if (canResume) {
|
|
73
|
+
const key = {
|
|
74
|
+
logicalSlotId: slot.logicalSlotId,
|
|
75
|
+
mappingGeneration: slot.mappingGeneration,
|
|
76
|
+
ifname: slot.ifname,
|
|
77
|
+
bootId: this.#bootId,
|
|
78
|
+
};
|
|
79
|
+
this.#accounts.set(slot.logicalSlotId, {
|
|
80
|
+
cycleBytes: slot.cycleBytes,
|
|
81
|
+
cycleStartMs: slot.cycleStartMs,
|
|
82
|
+
paused: false,
|
|
83
|
+
key,
|
|
84
|
+
lastObserved: slot.lastObserved,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
this.#accounts.set(slot.logicalSlotId, {
|
|
89
|
+
cycleBytes: slot.cycleBytes,
|
|
90
|
+
cycleStartMs: slot.cycleStartMs,
|
|
91
|
+
paused: false,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Take one sampling pass over the current counters for the given observations. */
|
|
97
|
+
async sample(observations) {
|
|
98
|
+
const counters = await this.#source.read();
|
|
99
|
+
const now = this.#now();
|
|
100
|
+
for (const obs of observations) {
|
|
101
|
+
const slotId = obs.logicalSlotId;
|
|
102
|
+
const usage = this.#policyOverrides.get(slotId) ?? obs.usage;
|
|
103
|
+
this.#policies.set(slotId, usage);
|
|
104
|
+
const cycleDay = usage.cycleDay ?? this.#defaultCycleDay;
|
|
105
|
+
const cycleStartMs = cycleStart(epochMillis(now), cycleDay);
|
|
106
|
+
const current = counters.get(obs.ifname);
|
|
107
|
+
if (current === undefined) {
|
|
108
|
+
// No reading for this interface — ensure the slot exists, attribute nothing.
|
|
109
|
+
if (!this.#accounts.has(slotId)) {
|
|
110
|
+
this.#accounts.set(slotId, initialAccount(cycleStartMs));
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const key = {
|
|
115
|
+
logicalSlotId: slotId,
|
|
116
|
+
mappingGeneration: obs.mappingGeneration,
|
|
117
|
+
ifname: obs.ifname,
|
|
118
|
+
bootId: this.#bootId,
|
|
119
|
+
};
|
|
120
|
+
const next = applySample(this.#accounts.get(slotId), {
|
|
121
|
+
key,
|
|
122
|
+
current,
|
|
123
|
+
confidence: obs.confidence,
|
|
124
|
+
cycleStartMs,
|
|
125
|
+
});
|
|
126
|
+
this.#accounts.set(slotId, next);
|
|
127
|
+
}
|
|
128
|
+
this.#dirty = true;
|
|
129
|
+
await this.#maybePersist(now);
|
|
130
|
+
}
|
|
131
|
+
/** Current per-slot usage — the queryable snapshot the CLI and platform read. */
|
|
132
|
+
snapshot() {
|
|
133
|
+
const generatedAtMs = this.#now();
|
|
134
|
+
const slots = [];
|
|
135
|
+
for (const [slotId, account] of this.#accounts) {
|
|
136
|
+
const policy = this.#policies.get(slotId);
|
|
137
|
+
const thresholdBytes = policy?.thresholdBytes;
|
|
138
|
+
slots.push({
|
|
139
|
+
logicalSlotId: slotId,
|
|
140
|
+
cycleBytes: account.cycleBytes,
|
|
141
|
+
cycleStartMs: account.cycleStartMs,
|
|
142
|
+
paused: account.paused,
|
|
143
|
+
...(policy?.cycleDay !== undefined ? { cycleDay: policy.cycleDay } : {}),
|
|
144
|
+
...(thresholdBytes !== undefined ? { thresholdBytes } : {}),
|
|
145
|
+
thresholdExceeded: thresholdBytes !== undefined && account.cycleBytes > thresholdBytes,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return { bootId: this.#bootId, generatedAtMs, slots };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Apply an operator's usage policy to this slot immediately, without waiting
|
|
152
|
+
* for the next sampling pass.
|
|
153
|
+
*
|
|
154
|
+
* A CHANGED CYCLE ANCHOR RESTARTS THE WINDOW AT ZERO, and keeps the counter
|
|
155
|
+
* BASELINE. Those two halves are the honest answer to a question with no
|
|
156
|
+
* truthful one: bytes already accrued were measured under the OLD window, so
|
|
157
|
+
* carrying them into the new one over-reports it, and there is no record of
|
|
158
|
+
* how they were distributed within it. Starting fresh states plainly that the
|
|
159
|
+
* new window began now; keeping `lastObserved` means the next sample still
|
|
160
|
+
* attributes only genuinely new bytes, never a jump. A threshold-only change
|
|
161
|
+
* moves no anchor and therefore resets nothing.
|
|
162
|
+
*/
|
|
163
|
+
applyUsagePolicy(logicalSlotId, usage, atMs) {
|
|
164
|
+
const now = atMs ?? this.#now();
|
|
165
|
+
this.#policyOverrides.set(logicalSlotId, usage);
|
|
166
|
+
this.#policies.set(logicalSlotId, usage);
|
|
167
|
+
const cycleStartMs = cycleStart(epochMillis(now), usage.cycleDay ?? this.#defaultCycleDay);
|
|
168
|
+
const account = this.#accounts.get(logicalSlotId);
|
|
169
|
+
if (account === undefined) {
|
|
170
|
+
this.#accounts.set(logicalSlotId, initialAccount(cycleStartMs));
|
|
171
|
+
this.#dirty = true;
|
|
172
|
+
return { cycleStartMs, cycleReset: false };
|
|
173
|
+
}
|
|
174
|
+
if (account.cycleStartMs === cycleStartMs) {
|
|
175
|
+
return { cycleStartMs, cycleReset: false };
|
|
176
|
+
}
|
|
177
|
+
this.#accounts.set(logicalSlotId, { ...account, cycleBytes: 0, cycleStartMs });
|
|
178
|
+
this.#dirty = true;
|
|
179
|
+
return { cycleStartMs, cycleReset: true };
|
|
180
|
+
}
|
|
181
|
+
/** Flush unpersisted state immediately — the shutdown hook (bounds loss to ≤1 min). */
|
|
182
|
+
async flush() {
|
|
183
|
+
if (this.#dirty) {
|
|
184
|
+
await this.#persist(this.#now());
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async #maybePersist(now) {
|
|
188
|
+
if (now - this.#lastPersistMs >= this.#persistIntervalMs) {
|
|
189
|
+
await this.#persist(now);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async #persist(now) {
|
|
193
|
+
const slots = [];
|
|
194
|
+
for (const [slotId, account] of this.#accounts) {
|
|
195
|
+
slots.push(toPersistedSlot(slotId, account));
|
|
196
|
+
}
|
|
197
|
+
const state = {
|
|
198
|
+
schemaVersion: USAGE_SCHEMA_VERSION,
|
|
199
|
+
bootId: this.#bootId,
|
|
200
|
+
savedAtMs: now,
|
|
201
|
+
slots,
|
|
202
|
+
};
|
|
203
|
+
await this.#store.save(state);
|
|
204
|
+
this.#lastPersistMs = now;
|
|
205
|
+
this.#dirty = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Load persisted state and build a ready sampler. See `UsageSampler.create`. */
|
|
209
|
+
export function createUsageSampler(options) {
|
|
210
|
+
return UsageSampler.create(options);
|
|
211
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** The current on-disk schema version. Bump when the persisted shape changes. */
|
|
2
|
+
export declare const USAGE_SCHEMA_VERSION = 1;
|
|
3
|
+
/** One slot's persisted accounting row — opaque ids and numbers only. */
|
|
4
|
+
export interface PersistedSlot {
|
|
5
|
+
readonly logicalSlotId: string;
|
|
6
|
+
readonly cycleBytes: number;
|
|
7
|
+
readonly cycleStartMs: number;
|
|
8
|
+
readonly mappingGeneration?: number;
|
|
9
|
+
readonly ifname?: string;
|
|
10
|
+
readonly lastObserved?: number;
|
|
11
|
+
}
|
|
12
|
+
/** The full persisted document. `bootId` scopes the baselines to one kernel session. */
|
|
13
|
+
export interface PersistedUsage {
|
|
14
|
+
readonly schemaVersion: typeof USAGE_SCHEMA_VERSION;
|
|
15
|
+
readonly bootId: string;
|
|
16
|
+
readonly savedAtMs: number;
|
|
17
|
+
readonly slots: readonly PersistedSlot[];
|
|
18
|
+
}
|
|
19
|
+
/** A metadata-only log event. Corruption NEVER carries the raw file content. */
|
|
20
|
+
export type UsageLogEvent = {
|
|
21
|
+
readonly kind: 'corrupt-state';
|
|
22
|
+
readonly bytes: number;
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
};
|
|
25
|
+
/** Sink for sampler log events. Defaults to a metadata-only `console.warn`. */
|
|
26
|
+
export type UsageLogger = (event: UsageLogEvent) => void;
|
|
27
|
+
/** The persistence seam the sampler drives. */
|
|
28
|
+
export interface UsageStore {
|
|
29
|
+
/** Load persisted state; recreate a fresh 0600 file if absent or corrupt. */
|
|
30
|
+
load(currentBootId: string, nowMs: number): Promise<PersistedUsage>;
|
|
31
|
+
/** Atomically write state with mode 0600 (temp → chmod → rename). */
|
|
32
|
+
save(state: PersistedUsage): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface UsageFileStoreOptions {
|
|
35
|
+
readonly path: string;
|
|
36
|
+
readonly logger?: UsageLogger;
|
|
37
|
+
}
|
|
38
|
+
export declare function createUsageFileStore(options: UsageFileStoreOptions): UsageStore;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Versioned, fail-soft persistence for the usage sampler.
|
|
2
|
+
//
|
|
3
|
+
// The persisted file is a small JSON document carrying ONLY opaque slot ids,
|
|
4
|
+
// interface names, and numbers — never any subscriber/device identity (no ICCID,
|
|
5
|
+
// IMSI, IMEI, operator or model). By construction the sampler holds no such fields,
|
|
6
|
+
// and `no-pii.test.ts` proves the serialized bytes stay clean.
|
|
7
|
+
//
|
|
8
|
+
// Two hard guarantees:
|
|
9
|
+
// - MODE 0600: the file is written to a temp path, chmod'd to 0600 AFTER the
|
|
10
|
+
// write, then atomically renamed over the target — so the on-disk file always
|
|
11
|
+
// ends up owner-read/write-only regardless of the process umask.
|
|
12
|
+
// - FAIL-SOFT ON CORRUPTION: an unparseable/incompatible file is never fatal. We
|
|
13
|
+
// log METADATA ONLY (byte length + a classification reason — never the raw,
|
|
14
|
+
// possibly-sensitive content), recreate a fresh empty 0600 file, and carry on.
|
|
15
|
+
import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { dirname } from 'node:path';
|
|
17
|
+
/** The current on-disk schema version. Bump when the persisted shape changes. */
|
|
18
|
+
export const USAGE_SCHEMA_VERSION = 1;
|
|
19
|
+
function defaultLogger(event) {
|
|
20
|
+
console.warn(`[usage-sampler] ${event.kind}: bytes=${event.bytes} reason=${event.reason}`);
|
|
21
|
+
}
|
|
22
|
+
function freshState(bootId, nowMs) {
|
|
23
|
+
return { schemaVersion: USAGE_SCHEMA_VERSION, bootId, savedAtMs: nowMs, slots: [] };
|
|
24
|
+
}
|
|
25
|
+
function isFiniteNumber(value) {
|
|
26
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
27
|
+
}
|
|
28
|
+
function validateSlot(raw) {
|
|
29
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
30
|
+
throw new SchemaError('slot');
|
|
31
|
+
}
|
|
32
|
+
const slot = raw;
|
|
33
|
+
if (typeof slot.logicalSlotId !== 'string') {
|
|
34
|
+
throw new SchemaError('logicalSlotId');
|
|
35
|
+
}
|
|
36
|
+
if (!isFiniteNumber(slot.cycleBytes) || !isFiniteNumber(slot.cycleStartMs)) {
|
|
37
|
+
throw new SchemaError('cycleBytes|cycleStartMs');
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
logicalSlotId: slot.logicalSlotId,
|
|
41
|
+
cycleBytes: slot.cycleBytes,
|
|
42
|
+
cycleStartMs: slot.cycleStartMs,
|
|
43
|
+
...(isFiniteNumber(slot.mappingGeneration)
|
|
44
|
+
? { mappingGeneration: slot.mappingGeneration }
|
|
45
|
+
: {}),
|
|
46
|
+
...(typeof slot.ifname === 'string' ? { ifname: slot.ifname } : {}),
|
|
47
|
+
...(isFiniteNumber(slot.lastObserved) ? { lastObserved: slot.lastObserved } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Parse + validate the document. Throws `SchemaError` (metadata-only) on mismatch. */
|
|
51
|
+
function validate(raw) {
|
|
52
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
53
|
+
throw new SchemaError('document');
|
|
54
|
+
}
|
|
55
|
+
const doc = raw;
|
|
56
|
+
if (doc.schemaVersion !== USAGE_SCHEMA_VERSION) {
|
|
57
|
+
throw new SchemaError('schemaVersion');
|
|
58
|
+
}
|
|
59
|
+
if (typeof doc.bootId !== 'string' || !isFiniteNumber(doc.savedAtMs)) {
|
|
60
|
+
throw new SchemaError('bootId|savedAtMs');
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(doc.slots)) {
|
|
63
|
+
throw new SchemaError('slots');
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
schemaVersion: USAGE_SCHEMA_VERSION,
|
|
67
|
+
bootId: doc.bootId,
|
|
68
|
+
savedAtMs: doc.savedAtMs,
|
|
69
|
+
slots: doc.slots.map(validateSlot),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** A schema violation naming only the offending FIELD (never file content). */
|
|
73
|
+
class SchemaError extends Error {
|
|
74
|
+
constructor(field) {
|
|
75
|
+
super(`schema-mismatch: ${field}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Classify a load failure into a metadata-only reason string (no raw content). */
|
|
79
|
+
function classifyFailure(error) {
|
|
80
|
+
if (error instanceof SchemaError) {
|
|
81
|
+
return error.message;
|
|
82
|
+
}
|
|
83
|
+
if (error instanceof SyntaxError) {
|
|
84
|
+
const offset = /position (\d+)/.exec(error.message)?.[1];
|
|
85
|
+
return offset !== undefined ? `invalid-json at offset ${offset}` : 'invalid-json';
|
|
86
|
+
}
|
|
87
|
+
return 'unreadable';
|
|
88
|
+
}
|
|
89
|
+
export function createUsageFileStore(options) {
|
|
90
|
+
const logger = options.logger ?? defaultLogger;
|
|
91
|
+
const { path } = options;
|
|
92
|
+
async function writeAtomic(state) {
|
|
93
|
+
await mkdir(dirname(path), { recursive: true });
|
|
94
|
+
const tmp = `${path}.tmp`;
|
|
95
|
+
await writeFile(tmp, JSON.stringify(state));
|
|
96
|
+
// chmod AFTER the write (not an open flag) so mode is 0600 regardless of umask.
|
|
97
|
+
await chmod(tmp, 0o600);
|
|
98
|
+
await rename(tmp, path);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
async load(currentBootId, nowMs) {
|
|
102
|
+
let text;
|
|
103
|
+
try {
|
|
104
|
+
text = await readFile(path, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Absent (or unreadable) → start empty; the first save lays down a 0600 file.
|
|
108
|
+
return freshState(currentBootId, nowMs);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
return validate(JSON.parse(text));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
logger({
|
|
115
|
+
kind: 'corrupt-state',
|
|
116
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
117
|
+
reason: classifyFailure(error),
|
|
118
|
+
});
|
|
119
|
+
const fresh = freshState(currentBootId, nowMs);
|
|
120
|
+
await writeAtomic(fresh);
|
|
121
|
+
return fresh;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
save: writeAtomic,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** One USB interface's descriptor bytes plus its bound kernel driver, if any. */
|
|
2
|
+
export interface UsbInterface {
|
|
3
|
+
readonly interfaceClass: number;
|
|
4
|
+
readonly interfaceSubClass: number;
|
|
5
|
+
readonly interfaceProtocol: number;
|
|
6
|
+
/** The bound kernel driver (`qmi_wwan`, `cdc_mbim`, `option`, `cdc_ether`, …). */
|
|
7
|
+
readonly driver?: string;
|
|
8
|
+
}
|
|
9
|
+
/** A single USB device as observed from udev/sysfs — the classifier's whole input. */
|
|
10
|
+
export interface UsbDeviceSnapshot {
|
|
11
|
+
readonly vendorId: string;
|
|
12
|
+
readonly productId: string;
|
|
13
|
+
readonly model?: string;
|
|
14
|
+
readonly firmwareRevision?: string;
|
|
15
|
+
readonly manufacturer?: string;
|
|
16
|
+
readonly product?: string;
|
|
17
|
+
readonly databaseVendor?: string;
|
|
18
|
+
readonly databaseModel?: string;
|
|
19
|
+
readonly serialNumber?: string;
|
|
20
|
+
/** The device-descriptor `bDeviceClass` byte (0 ⇒ class is per-interface). */
|
|
21
|
+
readonly bDeviceClass: number;
|
|
22
|
+
readonly interfaces: readonly UsbInterface[];
|
|
23
|
+
/** Stable physical-topology UID (udev `ID_PATH` / physdev) — survives a mode change. */
|
|
24
|
+
readonly physicalUid?: string;
|
|
25
|
+
/** Absolute sysfs path for the USB device, derived from udev's `P:` record. */
|
|
26
|
+
readonly sysfsPath?: string;
|
|
27
|
+
/** The bound network interface name, if the device presents one (`wwan0`, `usb0`). */
|
|
28
|
+
readonly ifname?: string;
|
|
29
|
+
/** Raw udev properties (`ID_USB_MODESWITCH`, `ID_MM_CANDIDATE`, …). */
|
|
30
|
+
readonly udevProperties?: Readonly<Record<string, string>>;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { UsbDeviceSnapshot } from './device-classifier.js';
|
|
2
|
+
/** The injectable dependencies for the enumerator. */
|
|
3
|
+
export interface UsbEnumeratorDeps {
|
|
4
|
+
/** Provides the raw `udevadm info --export-db` text. Defaults to a `Bun.spawn` call. */
|
|
5
|
+
readonly readUdevDatabase?: () => Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
/** A refresh-triggered USB device enumerator. */
|
|
8
|
+
export interface UsbEnumerator {
|
|
9
|
+
/** Snapshot current USB state — re-reads udev every call (never cached). */
|
|
10
|
+
enumerate(): Promise<readonly UsbDeviceSnapshot[]>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse `udevadm info --export-db` output into device snapshots. Pure and exported so
|
|
14
|
+
* the production parse is unit-testable against canned udev text. Interface drivers
|
|
15
|
+
* are stitched from each `usb_interface` record onto its parent `usb_device`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseUdevDatabase(text: string): UsbDeviceSnapshot[];
|
|
18
|
+
/** Create a refresh-triggered USB enumerator over an injectable udev reader. */
|
|
19
|
+
export declare function createUsbEnumerator(deps?: UsbEnumeratorDeps): UsbEnumerator;
|
|
20
|
+
/** Convenience: enumerate current USB devices once via the default udev reader. */
|
|
21
|
+
export declare function enumerateUsbDevices(): Promise<readonly UsbDeviceSnapshot[]>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Production USB enumeration — a refresh-triggered snapshot of udev/sysfs state.
|
|
2
|
+
//
|
|
3
|
+
// `enumerate()` shells out to `udevadm info --export-db` (via `Bun.spawn`) each call
|
|
4
|
+
// — it is deliberately NOT cached, so a caller re-reads current state after a hot-plug
|
|
5
|
+
// or a mode switch. The raw-database read is an injectable seam (`readUdevDatabase`)
|
|
6
|
+
// so tests drive canned udev output with no real hardware, and the parser itself is a
|
|
7
|
+
// pure, exported function. udev exports every interface's class/subclass/protocol on
|
|
8
|
+
// the parent device as `ID_USB_INTERFACES` (`:ff0000:0a0000:` …), and each interface's
|
|
9
|
+
// bound DRIVER on its own `usb_interface` record — this parser stitches the two.
|
|
10
|
+
async function defaultReadUdevDatabase() {
|
|
11
|
+
const proc = Bun.spawn(['udevadm', 'info', '--export-db'], { stdout: 'pipe', stderr: 'pipe' });
|
|
12
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
13
|
+
if (exitCode !== 0) {
|
|
14
|
+
const stderr = await new Response(proc.stderr).text();
|
|
15
|
+
throw new Error(`udevadm info --export-db exited ${exitCode}: ${stderr.trim()}`);
|
|
16
|
+
}
|
|
17
|
+
return stdout;
|
|
18
|
+
}
|
|
19
|
+
function parseRecords(text) {
|
|
20
|
+
const records = [];
|
|
21
|
+
let devpath = '';
|
|
22
|
+
let env = new Map();
|
|
23
|
+
const flush = () => {
|
|
24
|
+
if (env.size > 0 || devpath !== '') {
|
|
25
|
+
records.push({ devpath, env });
|
|
26
|
+
}
|
|
27
|
+
devpath = '';
|
|
28
|
+
env = new Map();
|
|
29
|
+
};
|
|
30
|
+
for (const line of text.split('\n')) {
|
|
31
|
+
if (line.trim() === '') {
|
|
32
|
+
flush();
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const kind = line.slice(0, 2);
|
|
36
|
+
const rest = line.slice(3);
|
|
37
|
+
if (kind === 'P:') {
|
|
38
|
+
devpath = rest;
|
|
39
|
+
}
|
|
40
|
+
else if (kind === 'E:') {
|
|
41
|
+
const eq = rest.indexOf('=');
|
|
42
|
+
if (eq > 0) {
|
|
43
|
+
env.set(rest.slice(0, eq), rest.slice(eq + 1));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
flush();
|
|
48
|
+
return records;
|
|
49
|
+
}
|
|
50
|
+
/** Parse an `ID_USB_INTERFACES` value (`:ff0000:0a0000:`) into class/subclass/protocol triples. */
|
|
51
|
+
function parseInterfaces(value) {
|
|
52
|
+
if (value === undefined) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const interfaces = [];
|
|
56
|
+
for (const token of value.split(':')) {
|
|
57
|
+
if (token.length !== 6) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
interfaces.push({
|
|
61
|
+
interfaceClass: Number.parseInt(token.slice(0, 2), 16),
|
|
62
|
+
interfaceSubClass: Number.parseInt(token.slice(2, 4), 16),
|
|
63
|
+
interfaceProtocol: Number.parseInt(token.slice(4, 6), 16),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return interfaces;
|
|
67
|
+
}
|
|
68
|
+
function interfaceIndex(devpath) {
|
|
69
|
+
const dot = devpath.lastIndexOf('.');
|
|
70
|
+
if (dot < 0) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const index = Number.parseInt(devpath.slice(dot + 1), 10);
|
|
74
|
+
return Number.isNaN(index) ? undefined : index;
|
|
75
|
+
}
|
|
76
|
+
function buildSnapshot(devpath, env) {
|
|
77
|
+
const vendorId = env.get('ID_VENDOR_ID');
|
|
78
|
+
const productId = env.get('ID_MODEL_ID');
|
|
79
|
+
if (vendorId === undefined || productId === undefined) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const props = {};
|
|
83
|
+
for (const [key, value] of env) {
|
|
84
|
+
props[key] = value;
|
|
85
|
+
}
|
|
86
|
+
const model = env.get('ID_MODEL');
|
|
87
|
+
const firmwareRevision = env.get('ID_REVISION');
|
|
88
|
+
const physicalUid = env.get('ID_PATH');
|
|
89
|
+
return {
|
|
90
|
+
vendorId,
|
|
91
|
+
productId,
|
|
92
|
+
sysfsPath: `/sys${devpath}`,
|
|
93
|
+
bDeviceClass: 0,
|
|
94
|
+
interfaces: parseInterfaces(env.get('ID_USB_INTERFACES')),
|
|
95
|
+
udevProperties: props,
|
|
96
|
+
...(model !== undefined ? { model } : {}),
|
|
97
|
+
...(firmwareRevision !== undefined ? { firmwareRevision } : {}),
|
|
98
|
+
...(physicalUid !== undefined ? { physicalUid } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse `udevadm info --export-db` output into device snapshots. Pure and exported so
|
|
103
|
+
* the production parse is unit-testable against canned udev text. Interface drivers
|
|
104
|
+
* are stitched from each `usb_interface` record onto its parent `usb_device`.
|
|
105
|
+
*/
|
|
106
|
+
export function parseUdevDatabase(text) {
|
|
107
|
+
const records = parseRecords(text);
|
|
108
|
+
const devices = new Map();
|
|
109
|
+
const snapshots = new Map();
|
|
110
|
+
for (const record of records) {
|
|
111
|
+
if (record.env.get('DEVTYPE') !== 'usb_device') {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const snapshot = buildSnapshot(record.devpath, record.env);
|
|
115
|
+
if (snapshot !== undefined) {
|
|
116
|
+
snapshots.set(record.devpath, snapshot);
|
|
117
|
+
devices.set(record.devpath, [...snapshot.interfaces]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const record of records) {
|
|
121
|
+
if (record.env.get('DEVTYPE') !== 'usb_interface') {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const driver = record.env.get('DRIVER');
|
|
125
|
+
const index = interfaceIndex(record.devpath);
|
|
126
|
+
if (driver === undefined || index === undefined) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const parent = record.devpath.slice(0, record.devpath.lastIndexOf('/'));
|
|
130
|
+
const ifaces = devices.get(parent);
|
|
131
|
+
const iface = ifaces?.[index];
|
|
132
|
+
if (ifaces !== undefined && iface !== undefined) {
|
|
133
|
+
ifaces[index] = { ...iface, driver };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return [...snapshots.entries()].map(([devpath, snapshot]) => ({
|
|
137
|
+
...snapshot,
|
|
138
|
+
interfaces: devices.get(devpath) ?? snapshot.interfaces,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
/** Create a refresh-triggered USB enumerator over an injectable udev reader. */
|
|
142
|
+
export function createUsbEnumerator(deps = {}) {
|
|
143
|
+
const read = deps.readUdevDatabase ?? defaultReadUdevDatabase;
|
|
144
|
+
return {
|
|
145
|
+
async enumerate() {
|
|
146
|
+
return parseUdevDatabase(await read());
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Convenience: enumerate current USB devices once via the default udev reader. */
|
|
151
|
+
export function enumerateUsbDevices() {
|
|
152
|
+
return createUsbEnumerator().enumerate();
|
|
153
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DeviceIfname, ModemManagerPort, NetworkManagerPort } from '../ports/index.js';
|
|
2
|
+
import { type CertifiedCatalog } from '../usb-mode/index.js';
|
|
3
|
+
import { type AtAuditSink, type AtCommandSender } from './at-lease.js';
|
|
4
|
+
import { type UsbDeviceSnapshot } from './device-classifier.js';
|
|
5
|
+
import type { ModemActor } from './modem-actor.js';
|
|
6
|
+
import { type TransitionInterlock, type UsbModeTransitionOutcome, type UsbModeTransitionRequest } from './transition-preconditions.js';
|
|
7
|
+
/** Construction dependencies for the transition. Everything I/O is injectable. */
|
|
8
|
+
export interface UsbModeTransitionDeps {
|
|
9
|
+
readonly actor: ModemActor;
|
|
10
|
+
readonly nm: NetworkManagerPort;
|
|
11
|
+
readonly modemManager: Pick<ModemManagerPort, 'inhibit' | 'uninhibit'>;
|
|
12
|
+
readonly atSender: AtCommandSender;
|
|
13
|
+
/** Refresh-triggered USB enumeration (e.g. `createUsbEnumerator().enumerate`). */
|
|
14
|
+
readonly enumerate: () => Promise<readonly UsbDeviceSnapshot[]>;
|
|
15
|
+
readonly interlock?: TransitionInterlock;
|
|
16
|
+
readonly catalog?: CertifiedCatalog;
|
|
17
|
+
readonly audit?: AtAuditSink;
|
|
18
|
+
readonly resolveIfname?: (device: UsbDeviceSnapshot) => DeviceIfname | undefined;
|
|
19
|
+
readonly watchdogMs?: number;
|
|
20
|
+
readonly reenumerationTimeoutMs?: number;
|
|
21
|
+
readonly pollIntervalMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/** The USB-mode transition transaction. One instance is reusable across requests. */
|
|
24
|
+
export declare class UsbModeTransition {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(deps: UsbModeTransitionDeps);
|
|
27
|
+
/** Run one transition. Preconditions are checked at entry, then again in-actor. */
|
|
28
|
+
execute(request: UsbModeTransitionRequest): Promise<UsbModeTransitionOutcome>;
|
|
29
|
+
}
|