@ceralive/modem-control 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +341 -0
- package/dist/backend/at-lease.d.ts +59 -0
- package/dist/backend/at-lease.js +118 -0
- package/dist/backend/cell-info.d.ts +46 -0
- package/dist/backend/cell-info.js +124 -0
- package/dist/backend/constants.d.ts +24 -0
- package/{src/backend/constants.ts → dist/backend/constants.js} +4 -9
- package/dist/backend/device-classifier.d.ts +46 -0
- package/dist/backend/device-classifier.js +258 -0
- package/dist/backend/enrichment.d.ts +28 -0
- package/dist/backend/enrichment.js +59 -0
- package/dist/backend/features.d.ts +61 -0
- package/dist/backend/features.js +109 -0
- package/dist/backend/identity-ladder.d.ts +57 -0
- package/dist/backend/identity-ladder.js +158 -0
- package/dist/backend/identity-registry.d.ts +51 -0
- package/dist/backend/identity-registry.js +101 -0
- package/dist/backend/index.d.ts +30 -0
- package/dist/backend/index.js +35 -0
- package/dist/backend/lifecycle-interlock.d.ts +25 -0
- package/dist/backend/lifecycle-interlock.js +18 -0
- package/dist/backend/managed-objects.d.ts +39 -0
- package/dist/backend/managed-objects.js +82 -0
- package/dist/backend/mapping.d.ts +11 -0
- package/dist/backend/mapping.js +148 -0
- package/dist/backend/mm-backend.d.ts +44 -0
- package/dist/backend/mm-backend.js +154 -0
- package/dist/backend/mm-location.d.ts +21 -0
- package/dist/backend/mm-location.js +237 -0
- package/dist/backend/mm-mutations.d.ts +42 -0
- package/dist/backend/mm-mutations.js +259 -0
- package/dist/backend/modem-actor.d.ts +41 -0
- package/dist/backend/modem-actor.js +78 -0
- package/dist/backend/nm-auto-apn.d.ts +54 -0
- package/dist/backend/nm-auto-apn.js +124 -0
- package/dist/backend/nm-gsm-fields.d.ts +23 -0
- package/dist/backend/nm-gsm-fields.js +107 -0
- package/dist/backend/nmcli-nm-port.d.ts +28 -0
- package/dist/backend/nmcli-nm-port.js +173 -0
- package/dist/backend/nmcli-runner.d.ts +24 -0
- package/dist/backend/nmcli-runner.js +35 -0
- package/dist/backend/observer.d.ts +37 -0
- package/dist/backend/observer.js +219 -0
- package/dist/backend/power-contract.d.ts +49 -0
- package/dist/backend/power-contract.js +34 -0
- package/dist/backend/recovery-attribution.d.ts +32 -0
- package/dist/backend/recovery-attribution.js +57 -0
- package/dist/backend/recovery-budget.d.ts +45 -0
- package/dist/backend/recovery-budget.js +44 -0
- package/dist/backend/recovery-ladder.d.ts +94 -0
- package/dist/backend/recovery-ladder.js +116 -0
- package/dist/backend/router-ethernet.d.ts +19 -0
- package/dist/backend/router-ethernet.js +66 -0
- package/dist/backend/row-store.d.ts +13 -0
- package/dist/backend/row-store.js +82 -0
- package/dist/backend/signal-setup.d.ts +30 -0
- package/dist/backend/signal-setup.js +92 -0
- package/dist/backend/sim-unlock.d.ts +13 -0
- package/dist/backend/sim-unlock.js +153 -0
- package/dist/backend/transition-preconditions.d.ts +101 -0
- package/dist/backend/transition-preconditions.js +132 -0
- package/dist/backend/usage/accounting.d.ts +39 -0
- package/dist/backend/usage/accounting.js +73 -0
- package/dist/backend/usage/billing-cycle.d.ts +11 -0
- package/dist/backend/usage/billing-cycle.js +39 -0
- package/dist/backend/usage/boot-id.d.ts +6 -0
- package/{src/backend/usage/boot-id.ts → dist/backend/usage/boot-id.js} +7 -7
- package/dist/backend/usage/index.d.ts +8 -0
- package/dist/backend/usage/index.js +11 -0
- package/dist/backend/usage/policy-store.d.ts +50 -0
- package/dist/backend/usage/policy-store.js +161 -0
- package/dist/backend/usage/policy-write.d.ts +65 -0
- package/dist/backend/usage/policy-write.js +112 -0
- package/dist/backend/usage/proc-net-dev.d.ts +18 -0
- package/{src/backend/usage/proc-net-dev.ts → dist/backend/usage/proc-net-dev.js} +39 -47
- package/dist/backend/usage/sampler.d.ts +75 -0
- package/dist/backend/usage/sampler.js +211 -0
- package/dist/backend/usage/store.d.ts +38 -0
- package/dist/backend/usage/store.js +126 -0
- package/dist/backend/usb-device-snapshot.d.ts +31 -0
- package/dist/backend/usb-device-snapshot.js +1 -0
- package/dist/backend/usb-enumerator.d.ts +21 -0
- package/dist/backend/usb-enumerator.js +153 -0
- package/dist/backend/usb-mode-transition.d.ts +29 -0
- package/dist/backend/usb-mode-transition.js +216 -0
- package/dist/band/band-names.d.ts +42 -0
- package/dist/band/band-names.js +150 -0
- package/dist/band/certification.d.ts +84 -0
- package/dist/band/certification.js +127 -0
- package/dist/band/certified-bands.json +4 -0
- package/dist/band/index.d.ts +2 -0
- package/dist/band/index.js +8 -0
- package/dist/capability/detect.d.ts +52 -0
- package/dist/capability/detect.js +86 -0
- package/dist/capability/five-g-preference.d.ts +104 -0
- package/dist/capability/five-g-preference.js +171 -0
- package/dist/capability/index.d.ts +3 -0
- package/dist/capability/index.js +10 -0
- package/dist/capability/support-claim.d.ts +39 -0
- package/dist/capability/support-claim.js +81 -0
- package/dist/domain/brand.d.ts +10 -0
- package/dist/domain/brand.js +21 -0
- package/dist/domain/errors.d.ts +32 -0
- package/dist/domain/errors.js +48 -0
- package/dist/domain/generation.d.ts +8 -0
- package/dist/domain/generation.js +12 -0
- package/dist/domain/guards.d.ts +6 -0
- package/dist/domain/guards.js +127 -0
- package/dist/domain/identity.d.ts +109 -0
- package/dist/domain/identity.js +86 -0
- package/dist/domain/index.d.ts +14 -0
- package/dist/domain/index.js +18 -0
- package/dist/domain/mm-enums.d.ts +12 -0
- package/dist/domain/mm-enums.js +139 -0
- package/dist/domain/modem-presentation.d.ts +10 -0
- package/dist/domain/modem-presentation.js +39 -0
- package/dist/domain/observation.d.ts +39 -0
- package/dist/domain/observation.js +4 -0
- package/dist/domain/operation.d.ts +118 -0
- package/dist/domain/operation.js +85 -0
- package/dist/domain/physical-identity.d.ts +43 -0
- package/dist/domain/physical-identity.js +113 -0
- package/{src/domain/policy.ts → dist/domain/policy.d.ts} +34 -69
- package/dist/domain/policy.js +38 -0
- package/dist/domain/shadow-divergence.d.ts +27 -0
- package/dist/domain/shadow-divergence.js +70 -0
- package/dist/domain/snapshot.d.ts +53 -0
- package/dist/domain/snapshot.js +75 -0
- package/{src/domain/state.ts → dist/domain/state.d.ts} +23 -122
- package/dist/domain/state.js +38 -0
- package/dist/fcc/coverage.d.ts +63 -0
- package/dist/fcc/coverage.js +102 -0
- package/dist/fcc/index.d.ts +3 -0
- package/dist/fcc/index.js +12 -0
- package/dist/fcc/policy-store.d.ts +40 -0
- package/dist/fcc/policy-store.js +146 -0
- package/dist/fcc/policy-write.d.ts +32 -0
- package/dist/fcc/policy-write.js +37 -0
- package/dist/hardware/hilink-protocol.d.ts +39 -0
- package/dist/hardware/hilink-protocol.js +58 -0
- package/dist/hardware/index.d.ts +3 -0
- package/dist/hardware/index.js +15 -0
- package/dist/hardware/router-parsers.d.ts +89 -0
- package/dist/hardware/router-parsers.js +234 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +27 -0
- package/dist/journal/codec.d.ts +44 -0
- package/dist/journal/codec.js +198 -0
- package/dist/journal/engine.d.ts +28 -0
- package/dist/journal/engine.js +68 -0
- package/dist/journal/entry.d.ts +74 -0
- package/dist/journal/entry.js +56 -0
- package/dist/journal/index.d.ts +6 -0
- package/dist/journal/index.js +6 -0
- package/dist/journal/legacy-ceraui.d.ts +73 -0
- package/dist/journal/legacy-ceraui.js +227 -0
- package/dist/journal/recovery.d.ts +58 -0
- package/dist/journal/recovery.js +117 -0
- package/dist/journal/store.d.ts +55 -0
- package/dist/journal/store.js +150 -0
- package/dist/location/fix-state.d.ts +53 -0
- package/dist/location/fix-state.js +75 -0
- package/dist/location/index.d.ts +2 -0
- package/dist/location/index.js +8 -0
- package/dist/location/nmea.d.ts +10 -0
- package/dist/location/nmea.js +89 -0
- package/dist/observations/envelope.d.ts +63 -0
- package/dist/observations/envelope.js +77 -0
- package/dist/observations/freshness.d.ts +28 -0
- package/dist/observations/freshness.js +76 -0
- package/dist/observations/index.d.ts +13 -0
- package/dist/observations/index.js +24 -0
- package/dist/observations/metric.d.ts +61 -0
- package/dist/observations/metric.js +73 -0
- package/dist/observations/model.d.ts +80 -0
- package/dist/observations/model.js +11 -0
- package/dist/observations/provenance.d.ts +94 -0
- package/dist/observations/provenance.js +67 -0
- package/dist/observations/raw.d.ts +42 -0
- package/dist/observations/raw.js +146 -0
- package/dist/observations/reading.d.ts +51 -0
- package/dist/observations/reading.js +65 -0
- package/dist/observations/sources/hilink.d.ts +10 -0
- package/dist/observations/sources/hilink.js +84 -0
- package/dist/observations/sources/modemmanager.d.ts +18 -0
- package/dist/observations/sources/modemmanager.js +239 -0
- package/dist/observations/sources/router-shared.d.ts +29 -0
- package/dist/observations/sources/router-shared.js +68 -0
- package/dist/observations/sources/ufi.d.ts +10 -0
- package/dist/observations/sources/ufi.js +111 -0
- package/dist/observations/sources/zte.d.ts +7 -0
- package/dist/observations/sources/zte.js +71 -0
- package/dist/observations/state-separation.d.ts +64 -0
- package/dist/observations/state-separation.js +52 -0
- package/dist/operation-ids.d.ts +2 -0
- package/dist/operation-ids.js +26 -0
- package/dist/operations/contracts.d.ts +72 -0
- package/dist/operations/contracts.js +1 -0
- package/dist/operations/index.d.ts +1 -0
- package/dist/operations/index.js +1 -0
- package/dist/operations/operation-engine.d.ts +16 -0
- package/dist/operations/operation-engine.js +194 -0
- package/dist/ports/index.d.ts +12 -0
- package/{src/ports/index.ts → dist/ports/index.js} +12 -8
- package/dist/ports/location.d.ts +87 -0
- package/dist/ports/location.js +36 -0
- package/dist/ports/modem-manager.d.ts +89 -0
- package/dist/ports/modem-manager.js +9 -0
- package/dist/ports/mutation-admission.d.ts +27 -0
- package/dist/ports/mutation-admission.js +9 -0
- package/dist/ports/network-manager.d.ts +68 -0
- package/dist/ports/network-manager.js +13 -0
- package/{src/ports/observation.ts → dist/ports/observation.d.ts} +16 -27
- package/dist/ports/observation.js +7 -0
- package/dist/ports/ops.d.ts +44 -0
- package/dist/ports/ops.js +16 -0
- package/{src/ports/receipts.ts → dist/ports/receipts.d.ts} +5 -27
- package/dist/ports/receipts.js +9 -0
- package/dist/ports/reconcile.d.ts +33 -0
- package/dist/ports/reconcile.js +200 -0
- package/dist/ports/resource-ownership.d.ts +29 -0
- package/dist/ports/resource-ownership.js +1 -0
- package/dist/ports/router.d.ts +19 -0
- package/dist/ports/router.js +7 -0
- package/dist/ports/sms.d.ts +64 -0
- package/dist/ports/sms.js +24 -0
- package/dist/ports/uhubctl.d.ts +6 -0
- package/dist/ports/uhubctl.js +1 -0
- package/dist/providers/contracts.d.ts +124 -0
- package/dist/providers/contracts.js +10 -0
- package/dist/providers/huawei-hilink/index.d.ts +2 -0
- package/dist/providers/huawei-hilink/index.js +2 -0
- package/dist/providers/huawei-hilink/operations.d.ts +20 -0
- package/dist/providers/huawei-hilink/operations.js +56 -0
- package/dist/providers/huawei-hilink/provider.d.ts +52 -0
- package/dist/providers/huawei-hilink/provider.js +76 -0
- package/dist/providers/huawei-hilink/runtime.d.ts +22 -0
- package/dist/providers/huawei-hilink/runtime.js +171 -0
- package/dist/providers/huawei-hilink/session.d.ts +28 -0
- package/dist/providers/huawei-hilink/session.js +120 -0
- package/dist/providers/huawei-hilink/transport.d.ts +19 -0
- package/dist/providers/huawei-hilink/transport.js +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/matcher.d.ts +3 -0
- package/dist/providers/matcher.js +205 -0
- package/dist/providers/modem-manager/errors.d.ts +7 -0
- package/dist/providers/modem-manager/errors.js +37 -0
- package/dist/providers/modem-manager/generic-operations.d.ts +10 -0
- package/dist/providers/modem-manager/generic-operations.js +209 -0
- package/dist/providers/modem-manager/index.d.ts +4 -0
- package/dist/providers/modem-manager/index.js +4 -0
- package/dist/providers/modem-manager/module-operations.d.ts +21 -0
- package/dist/providers/modem-manager/module-operations.js +118 -0
- package/dist/providers/modem-manager/provider.d.ts +41 -0
- package/dist/providers/modem-manager/provider.js +155 -0
- package/dist/providers/modem-manager/runtime-composition-operation.d.ts +32 -0
- package/dist/providers/modem-manager/runtime-composition-operation.js +151 -0
- package/dist/providers/modem-manager/snapshot.d.ts +5 -0
- package/dist/providers/modem-manager/snapshot.js +204 -0
- package/dist/providers/modem-manager/types.d.ts +137 -0
- package/dist/providers/modem-manager/types.js +1 -0
- package/dist/providers/network-manager/adapter.d.ts +71 -0
- package/dist/providers/network-manager/adapter.js +348 -0
- package/dist/providers/network-manager/index.d.ts +2 -0
- package/dist/providers/network-manager/index.js +2 -0
- package/dist/providers/network-manager/types.d.ts +171 -0
- package/dist/providers/network-manager/types.js +77 -0
- package/dist/providers/registry.d.ts +13 -0
- package/dist/providers/registry.js +33 -0
- package/dist/providers/ufi-himi/index.d.ts +6 -0
- package/dist/providers/ufi-himi/index.js +6 -0
- package/dist/providers/ufi-himi/operations.d.ts +41 -0
- package/dist/providers/ufi-himi/operations.js +66 -0
- package/dist/providers/ufi-himi/prohibitions.d.ts +62 -0
- package/dist/providers/ufi-himi/prohibitions.js +88 -0
- package/dist/providers/ufi-himi/provider.d.ts +41 -0
- package/dist/providers/ufi-himi/provider.js +204 -0
- package/dist/providers/ufi-himi/qualcomm-evidence.d.ts +32 -0
- package/dist/providers/ufi-himi/qualcomm-evidence.js +51 -0
- package/dist/providers/ufi-himi/session.d.ts +46 -0
- package/dist/providers/ufi-himi/session.js +92 -0
- package/dist/providers/ufi-himi/transport.d.ts +29 -0
- package/dist/providers/ufi-himi/transport.js +25 -0
- package/dist/providers/zte-goform/index.d.ts +2 -0
- package/dist/providers/zte-goform/index.js +2 -0
- package/dist/providers/zte-goform/provider.d.ts +56 -0
- package/dist/providers/zte-goform/provider.js +101 -0
- package/dist/providers/zte-goform/session.d.ts +23 -0
- package/dist/providers/zte-goform/session.js +197 -0
- package/dist/providers/zte-goform/transport.d.ts +16 -0
- package/dist/providers/zte-goform/transport.js +1 -0
- package/dist/radio/band-truth.d.ts +50 -0
- package/dist/radio/band-truth.js +92 -0
- package/dist/radio/index.d.ts +3 -0
- package/dist/radio/index.js +10 -0
- package/dist/radio/mode-combinations.d.ts +88 -0
- package/dist/radio/mode-combinations.js +198 -0
- package/dist/radio/mode-truth.d.ts +67 -0
- package/dist/radio/mode-truth.js +112 -0
- package/dist/redact.d.ts +15 -0
- package/dist/redact.js +189 -0
- package/dist/safety/composition-root.d.ts +28 -0
- package/dist/safety/composition-root.js +65 -0
- package/dist/safety/flock-resource-ownership.d.ts +11 -0
- package/dist/safety/flock-resource-ownership.js +138 -0
- package/dist/safety/index.d.ts +2 -0
- package/dist/safety/index.js +2 -0
- package/dist/sms/dbus-messaging.d.ts +17 -0
- package/dist/sms/dbus-messaging.js +185 -0
- package/dist/sms/inbox-store.d.ts +10 -0
- package/dist/sms/inbox-store.js +82 -0
- package/dist/sms/index.d.ts +4 -0
- package/dist/sms/index.js +10 -0
- package/dist/sms/mmcli-parse.d.ts +54 -0
- package/dist/sms/mmcli-parse.js +224 -0
- package/dist/sms/normalize.d.ts +42 -0
- package/dist/sms/normalize.js +95 -0
- package/dist/testing/domain-fakes.d.ts +58 -0
- package/dist/testing/domain-fakes.js +98 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +17 -0
- package/dist/testing/provider-fakes.d.ts +45 -0
- package/dist/testing/provider-fakes.js +64 -0
- package/dist/transport/calls.d.ts +9 -0
- package/dist/transport/calls.js +88 -0
- package/dist/transport/codec.d.ts +3 -0
- package/dist/transport/codec.js +207 -0
- package/dist/transport/dbus-native.d.ts +57 -0
- package/dist/transport/dbus-native.js +17 -0
- package/dist/transport/errors.d.ts +21 -0
- package/{src/transport/errors.ts → dist/transport/errors.js} +34 -46
- package/dist/transport/index.d.ts +4 -0
- package/dist/transport/index.js +9 -0
- package/dist/transport/signals.d.ts +15 -0
- package/dist/transport/signals.js +123 -0
- package/dist/transport/signature.d.ts +7 -0
- package/dist/transport/signature.js +94 -0
- package/dist/transport/transport.d.ts +2 -0
- package/dist/transport/transport.js +202 -0
- package/dist/transport/types.d.ts +61 -0
- package/dist/transport/types.js +19 -0
- package/dist/usb-mode/catalog-schema.d.ts +139 -0
- package/dist/usb-mode/catalog-schema.js +97 -0
- package/dist/usb-mode/catalog.d.ts +21 -0
- package/{src/usb-mode/catalog.ts → dist/usb-mode/catalog.js} +10 -32
- package/dist/usb-mode/certified-catalog.json +67 -0
- package/dist/usb-mode/index.d.ts +6 -0
- package/dist/usb-mode/index.js +16 -0
- package/dist/usb-mode/ingestion.d.ts +111 -0
- package/dist/usb-mode/ingestion.js +187 -0
- package/dist/usb-mode/promotion-review.d.ts +21 -0
- package/dist/usb-mode/promotion-review.js +87 -0
- package/dist/usb-mode/runtime-capability.d.ts +59 -0
- package/dist/usb-mode/runtime-capability.js +157 -0
- package/dist/usb-mode/usb-devices-parse.d.ts +36 -0
- package/dist/usb-mode/usb-devices-parse.js +157 -0
- package/dist/ussd/calls.d.ts +32 -0
- package/dist/ussd/calls.js +96 -0
- package/dist/ussd/index.d.ts +5 -0
- package/dist/ussd/index.js +12 -0
- package/dist/ussd/mm-ussd.d.ts +37 -0
- package/dist/ussd/mm-ussd.js +205 -0
- package/dist/ussd/refusal.d.ts +53 -0
- package/dist/ussd/refusal.js +154 -0
- package/dist/ussd/registration.d.ts +20 -0
- package/dist/ussd/registration.js +101 -0
- package/dist/ussd/session.d.ts +106 -0
- package/dist/ussd/session.js +163 -0
- package/package.json +38 -4
- package/src/backend/at-lease.test.ts +0 -106
- package/src/backend/at-lease.ts +0 -158
- package/src/backend/cell-info.test.ts +0 -154
- package/src/backend/cell-info.ts +0 -160
- package/src/backend/device-classifier.test.ts +0 -168
- package/src/backend/device-classifier.ts +0 -248
- package/src/backend/enrichment.ts +0 -96
- package/src/backend/features.test.ts +0 -162
- package/src/backend/features.ts +0 -179
- package/src/backend/identity-ladder.test.ts +0 -117
- package/src/backend/identity-ladder.ts +0 -221
- package/src/backend/identity-registry.test.ts +0 -89
- package/src/backend/identity-registry.ts +0 -151
- package/src/backend/index.ts +0 -236
- package/src/backend/lifecycle-interlock.ts +0 -38
- package/src/backend/managed-objects.ts +0 -108
- package/src/backend/mapping.ts +0 -160
- package/src/backend/mm-backend.ts +0 -191
- package/src/backend/mm-mutations.ts +0 -228
- package/src/backend/modem-actor.test.ts +0 -95
- package/src/backend/modem-actor.ts +0 -112
- package/src/backend/nm-auto-apn.ts +0 -161
- package/src/backend/nm-gsm-fields.ts +0 -122
- package/src/backend/nmcli-nm-port.ts +0 -228
- package/src/backend/nmcli-runner.ts +0 -52
- package/src/backend/observer.ts +0 -297
- package/src/backend/power-contract.test.ts +0 -40
- package/src/backend/power-contract.ts +0 -83
- package/src/backend/recovery-attribution.test.ts +0 -102
- package/src/backend/recovery-attribution.ts +0 -86
- package/src/backend/recovery-budget.test.ts +0 -64
- package/src/backend/recovery-budget.ts +0 -84
- package/src/backend/recovery-ladder.test.ts +0 -257
- package/src/backend/recovery-ladder.ts +0 -249
- package/src/backend/router-ethernet.test.ts +0 -71
- package/src/backend/router-ethernet.ts +0 -90
- package/src/backend/row-store.ts +0 -105
- package/src/backend/signal-setup.ts +0 -112
- package/src/backend/sim-unlock.ts +0 -193
- package/src/backend/transition-preconditions.ts +0 -149
- package/src/backend/uhubctl-power-hook.test.ts +0 -274
- package/src/backend/uhubctl-power-hook.ts +0 -377
- package/src/backend/usage/accounting.test.ts +0 -147
- package/src/backend/usage/accounting.ts +0 -123
- package/src/backend/usage/billing-cycle.test.ts +0 -62
- package/src/backend/usage/billing-cycle.ts +0 -45
- package/src/backend/usage/index.ts +0 -60
- package/src/backend/usage/policy-store.test.ts +0 -164
- package/src/backend/usage/policy-store.ts +0 -216
- package/src/backend/usage/policy-write.test.ts +0 -198
- package/src/backend/usage/policy-write.ts +0 -207
- package/src/backend/usage/proc-net-dev.test.ts +0 -56
- package/src/backend/usage/sampler.test.ts +0 -327
- package/src/backend/usage/sampler.ts +0 -282
- package/src/backend/usage/store.test.ts +0 -148
- package/src/backend/usage/store.ts +0 -177
- package/src/backend/usb-enumerator.test.ts +0 -87
- package/src/backend/usb-enumerator.ts +0 -181
- package/src/backend/usb-mode-transition.test.ts +0 -323
- package/src/backend/usb-mode-transition.ts +0 -253
- package/src/domain/brand.ts +0 -29
- package/src/domain/errors.ts +0 -77
- package/src/domain/guards.test.ts +0 -218
- package/src/domain/guards.ts +0 -144
- package/src/domain/identity.test.ts +0 -83
- package/src/domain/identity.ts +0 -165
- package/src/domain/index.ts +0 -12
- package/src/domain/snapshot.test.ts +0 -266
- package/src/domain/snapshot.ts +0 -120
- package/src/index.test.ts +0 -6
- package/src/index.ts +0 -15
- package/src/ports/README.md +0 -61
- package/src/ports/forbidden-surface.test.ts +0 -241
- package/src/ports/modem-manager.ts +0 -72
- package/src/ports/network-manager.ts +0 -87
- package/src/ports/ops.ts +0 -60
- package/src/ports/ops.type-test.ts +0 -39
- package/src/ports/receipts.test.ts +0 -153
- package/src/ports/reconcile.test.ts +0 -152
- package/src/ports/reconcile.ts +0 -338
- package/src/ports/router.ts +0 -29
- package/src/redact.test.ts +0 -82
- package/src/redact.ts +0 -73
- package/src/transport/README.md +0 -65
- package/src/transport/calls.ts +0 -113
- package/src/transport/characterization.test.ts +0 -260
- package/src/transport/codec.test.ts +0 -118
- package/src/transport/codec.ts +0 -240
- package/src/transport/conformance-python.test.ts +0 -152
- package/src/transport/conformance-same-lib.test.ts +0 -115
- package/src/transport/dbus-native-lib.d.ts +0 -19
- package/src/transport/dbus-native.ts +0 -85
- package/src/transport/index.ts +0 -30
- package/src/transport/no-library-leak.test.ts +0 -61
- package/src/transport/reliability.test.ts +0 -173
- package/src/transport/signals.ts +0 -150
- package/src/transport/signature.ts +0 -110
- package/src/transport/test-support/fake-service.ts +0 -168
- package/src/transport/test-support/independent-producer.py +0 -110
- package/src/transport/test-support/private-bus.ts +0 -66
- package/src/transport/transport.ts +0 -250
- package/src/transport/types.ts +0 -118
- package/src/usb-mode/catalog-schema.test.ts +0 -181
- package/src/usb-mode/catalog-schema.ts +0 -113
- package/src/usb-mode/certified-catalog.json +0 -67
- package/src/usb-mode/index.ts +0 -58
- package/src/usb-mode/ingestion.test.ts +0 -268
- package/src/usb-mode/ingestion.ts +0 -297
- package/src/usb-mode/promotion-review.ts +0 -117
- package/src/usb-mode/usb-devices-parse.ts +0 -196
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
// Durable persistence for the operator's data-usage POLICY (cycle day + advisory
|
|
2
|
-
// threshold), the write-side counterpart of `store.ts`'s counter persistence.
|
|
3
|
-
//
|
|
4
|
-
// WHY THIS IS LOCAL STATE AND NOT A MODEM WRITE. ModemManager has no data-usage
|
|
5
|
-
// API at all. Verified against a live MM 1.24.2 (`mmcli --help-all`, plus a D-Bus
|
|
6
|
-
// introspection of a real `…/ModemManager1/Modem/N`): the only `Setup`/threshold
|
|
7
|
-
// surface on the whole object is `Modem.Signal.Setup` /
|
|
8
|
-
// `Modem.Signal.SetupThresholds`, whose keys are `rssi-threshold` and
|
|
9
|
-
// `error-rate-threshold` — RADIO QUALITY, not bytes. The only byte counters MM
|
|
10
|
-
// offers are the per-BEARER read-only `Stats` (`rx-bytes`/`tx-bytes`), which
|
|
11
|
-
// reset with every connection and therefore cannot carry a monthly cycle.
|
|
12
|
-
//
|
|
13
|
-
// That is exactly why the sampler in this directory counts `/proc/net/dev`
|
|
14
|
-
// instead, and why `ports/README.md`'s ownership table records usage policy as
|
|
15
|
-
// LOCAL-CONTROLLER owned. So the write path is a local, versioned, fail-soft
|
|
16
|
-
// file — never a D-Bus mutation. (`Modem.Signal.Setup` is additionally forbidden
|
|
17
|
-
// outright by the shadow-mode mutation-freedom contract; nothing here goes near
|
|
18
|
-
// it.)
|
|
19
|
-
//
|
|
20
|
-
// The two hard guarantees are the SAME ones `store.ts` makes, and deliberately
|
|
21
|
-
// implemented the same way so the pair can be read side by side:
|
|
22
|
-
// - MODE 0600 via temp → chmod → atomic rename, regardless of umask.
|
|
23
|
-
// - FAIL-SOFT ON CORRUPTION: an unparseable/incompatible file logs METADATA
|
|
24
|
-
// ONLY (byte length + a classification reason, never the content) and is
|
|
25
|
-
// replaced by a fresh empty 0600 file rather than throwing.
|
|
26
|
-
//
|
|
27
|
-
// A policy row carries ONLY an opaque slot id and two numbers. By construction
|
|
28
|
-
// there is no subscriber or device identity here (no ICCID/IMSI/IMEI, no
|
|
29
|
-
// operator, no model) — the same no-PII property the counter store holds.
|
|
30
|
-
|
|
31
|
-
import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
32
|
-
import { dirname } from 'node:path';
|
|
33
|
-
import type { DesiredUsage } from '../../domain';
|
|
34
|
-
|
|
35
|
-
/** The current on-disk schema version. Bump when the persisted shape changes. */
|
|
36
|
-
export const USAGE_POLICY_SCHEMA_VERSION = 1;
|
|
37
|
-
|
|
38
|
-
/** One slot's persisted usage policy. Both fields are absent when unset. */
|
|
39
|
-
export interface PersistedUsagePolicySlot {
|
|
40
|
-
readonly logicalSlotId: string;
|
|
41
|
-
/** Day of month (1–31) the cycle resets; UTC, month-length clamped (A4.3). */
|
|
42
|
-
readonly cycleDay?: number;
|
|
43
|
-
/** Advisory threshold in bytes; crossing it raises an advisory, never gates. */
|
|
44
|
-
readonly thresholdBytes?: number;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** The full persisted policy document. */
|
|
48
|
-
export interface PersistedUsagePolicy {
|
|
49
|
-
readonly schemaVersion: typeof USAGE_POLICY_SCHEMA_VERSION;
|
|
50
|
-
readonly savedAtMs: number;
|
|
51
|
-
readonly slots: readonly PersistedUsagePolicySlot[];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** A metadata-only log event. Corruption NEVER carries the raw file content. */
|
|
55
|
-
export type UsagePolicyLogEvent = {
|
|
56
|
-
readonly kind: 'corrupt-policy';
|
|
57
|
-
readonly bytes: number;
|
|
58
|
-
readonly reason: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/** Sink for policy-store log events. Defaults to a metadata-only `console.warn`. */
|
|
62
|
-
export type UsagePolicyLogger = (event: UsagePolicyLogEvent) => void;
|
|
63
|
-
|
|
64
|
-
/** The persistence seam `setUsagePolicy` drives. */
|
|
65
|
-
export interface UsagePolicyStore {
|
|
66
|
-
/** Load persisted policy; recreate a fresh 0600 file if absent or corrupt. */
|
|
67
|
-
load(nowMs: number): Promise<PersistedUsagePolicy>;
|
|
68
|
-
/** Atomically write policy with mode 0600 (temp → chmod → rename). */
|
|
69
|
-
save(state: PersistedUsagePolicy): Promise<void>;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface UsagePolicyFileStoreOptions {
|
|
73
|
-
readonly path: string;
|
|
74
|
-
readonly logger?: UsagePolicyLogger;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function defaultLogger(event: UsagePolicyLogEvent): void {
|
|
78
|
-
console.warn(`[usage-policy] ${event.kind}: bytes=${event.bytes} reason=${event.reason}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function freshState(nowMs: number): PersistedUsagePolicy {
|
|
82
|
-
return { schemaVersion: USAGE_POLICY_SCHEMA_VERSION, savedAtMs: nowMs, slots: [] };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** A schema violation naming only the offending FIELD (never file content). */
|
|
86
|
-
class PolicySchemaError extends Error {
|
|
87
|
-
constructor(field: string) {
|
|
88
|
-
super(`schema-mismatch: ${field}`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** True for a value that is a legal cycle day (integer 1–31). */
|
|
93
|
-
export function isValidCycleDay(value: unknown): value is number {
|
|
94
|
-
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 31;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** True for a value that is a legal advisory threshold (non-negative integer). */
|
|
98
|
-
export function isValidThresholdBytes(value: unknown): value is number {
|
|
99
|
-
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function validateSlot(raw: unknown): PersistedUsagePolicySlot {
|
|
103
|
-
if (typeof raw !== 'object' || raw === null) {
|
|
104
|
-
throw new PolicySchemaError('slot');
|
|
105
|
-
}
|
|
106
|
-
const slot = raw as Record<string, unknown>;
|
|
107
|
-
if (typeof slot.logicalSlotId !== 'string' || slot.logicalSlotId.length === 0) {
|
|
108
|
-
throw new PolicySchemaError('logicalSlotId');
|
|
109
|
-
}
|
|
110
|
-
if (slot.cycleDay !== undefined && !isValidCycleDay(slot.cycleDay)) {
|
|
111
|
-
throw new PolicySchemaError('cycleDay');
|
|
112
|
-
}
|
|
113
|
-
if (slot.thresholdBytes !== undefined && !isValidThresholdBytes(slot.thresholdBytes)) {
|
|
114
|
-
throw new PolicySchemaError('thresholdBytes');
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
logicalSlotId: slot.logicalSlotId,
|
|
118
|
-
...(slot.cycleDay !== undefined ? { cycleDay: slot.cycleDay } : {}),
|
|
119
|
-
...(slot.thresholdBytes !== undefined ? { thresholdBytes: slot.thresholdBytes } : {}),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Parse + validate the document. Throws `PolicySchemaError` (metadata-only). */
|
|
124
|
-
function validate(raw: unknown): PersistedUsagePolicy {
|
|
125
|
-
if (typeof raw !== 'object' || raw === null) {
|
|
126
|
-
throw new PolicySchemaError('document');
|
|
127
|
-
}
|
|
128
|
-
const doc = raw as Record<string, unknown>;
|
|
129
|
-
if (doc.schemaVersion !== USAGE_POLICY_SCHEMA_VERSION) {
|
|
130
|
-
throw new PolicySchemaError('schemaVersion');
|
|
131
|
-
}
|
|
132
|
-
if (typeof doc.savedAtMs !== 'number' || !Number.isFinite(doc.savedAtMs)) {
|
|
133
|
-
throw new PolicySchemaError('savedAtMs');
|
|
134
|
-
}
|
|
135
|
-
if (!Array.isArray(doc.slots)) {
|
|
136
|
-
throw new PolicySchemaError('slots');
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
schemaVersion: USAGE_POLICY_SCHEMA_VERSION,
|
|
140
|
-
savedAtMs: doc.savedAtMs,
|
|
141
|
-
slots: doc.slots.map(validateSlot),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Classify a load failure into a metadata-only reason string (no raw content). */
|
|
146
|
-
function classifyFailure(error: unknown): string {
|
|
147
|
-
if (error instanceof PolicySchemaError) {
|
|
148
|
-
return error.message;
|
|
149
|
-
}
|
|
150
|
-
if (error instanceof SyntaxError) {
|
|
151
|
-
const offset = /position (\d+)/.exec(error.message)?.[1];
|
|
152
|
-
return offset !== undefined ? `invalid-json at offset ${offset}` : 'invalid-json';
|
|
153
|
-
}
|
|
154
|
-
return 'unreadable';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Read one slot's policy out of a loaded document.
|
|
159
|
-
*
|
|
160
|
-
* This is the read half the composition root uses to build each slot's
|
|
161
|
-
* `UsageObservation.usage`, so the persisted file — not an in-memory guess — is
|
|
162
|
-
* what the sampler accounts against. An unknown slot answers `{}`, i.e. "no
|
|
163
|
-
* policy set", which is exactly what `defaultCellularPolicy` starts from.
|
|
164
|
-
*/
|
|
165
|
-
export function selectUsagePolicy(
|
|
166
|
-
state: PersistedUsagePolicy,
|
|
167
|
-
logicalSlotId: string,
|
|
168
|
-
): DesiredUsage {
|
|
169
|
-
const slot = state.slots.find((entry) => entry.logicalSlotId === logicalSlotId);
|
|
170
|
-
if (slot === undefined) {
|
|
171
|
-
return {};
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
...(slot.cycleDay !== undefined ? { cycleDay: slot.cycleDay } : {}),
|
|
175
|
-
...(slot.thresholdBytes !== undefined ? { thresholdBytes: slot.thresholdBytes } : {}),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function createUsagePolicyFileStore(options: UsagePolicyFileStoreOptions): UsagePolicyStore {
|
|
180
|
-
const logger = options.logger ?? defaultLogger;
|
|
181
|
-
const { path } = options;
|
|
182
|
-
|
|
183
|
-
async function writeAtomic(state: PersistedUsagePolicy): Promise<void> {
|
|
184
|
-
await mkdir(dirname(path), { recursive: true });
|
|
185
|
-
const tmp = `${path}.tmp`;
|
|
186
|
-
await writeFile(tmp, JSON.stringify(state));
|
|
187
|
-
// chmod AFTER the write (not an open flag) so mode is 0600 regardless of umask.
|
|
188
|
-
await chmod(tmp, 0o600);
|
|
189
|
-
await rename(tmp, path);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
async load(nowMs: number): Promise<PersistedUsagePolicy> {
|
|
194
|
-
let text: string;
|
|
195
|
-
try {
|
|
196
|
-
text = await readFile(path, 'utf8');
|
|
197
|
-
} catch {
|
|
198
|
-
// Absent (or unreadable) → start empty; the first save lays down a 0600 file.
|
|
199
|
-
return freshState(nowMs);
|
|
200
|
-
}
|
|
201
|
-
try {
|
|
202
|
-
return validate(JSON.parse(text));
|
|
203
|
-
} catch (error) {
|
|
204
|
-
logger({
|
|
205
|
-
kind: 'corrupt-policy',
|
|
206
|
-
bytes: Buffer.byteLength(text, 'utf8'),
|
|
207
|
-
reason: classifyFailure(error),
|
|
208
|
-
});
|
|
209
|
-
const fresh = freshState(nowMs);
|
|
210
|
-
await writeAtomic(fresh);
|
|
211
|
-
return fresh;
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
save: writeAtomic,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
// `setUsagePolicy` contract — tri-state merge, typed refusals, persist-before-apply
|
|
2
|
-
// ordering, and the live-sampler cycle-reset rule.
|
|
3
|
-
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
5
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import type { DesiredUsage } from '../../domain';
|
|
9
|
-
import {
|
|
10
|
-
createUsagePolicyFileStore,
|
|
11
|
-
type PersistedUsagePolicy,
|
|
12
|
-
selectUsagePolicy,
|
|
13
|
-
USAGE_POLICY_SCHEMA_VERSION,
|
|
14
|
-
type UsagePolicyStore,
|
|
15
|
-
} from './policy-store';
|
|
16
|
-
import { getUsagePolicy, setUsagePolicy, type UsagePolicyTarget } from './policy-write';
|
|
17
|
-
|
|
18
|
-
let dir: string;
|
|
19
|
-
let path: string;
|
|
20
|
-
let store: UsagePolicyStore;
|
|
21
|
-
|
|
22
|
-
const SLOT = 'slot-a';
|
|
23
|
-
const NOW = 1_700_000_000_000;
|
|
24
|
-
|
|
25
|
-
beforeEach(async () => {
|
|
26
|
-
dir = await mkdtemp(join(tmpdir(), 'usage-policy-write-'));
|
|
27
|
-
path = join(dir, 'policy.json');
|
|
28
|
-
store = createUsagePolicyFileStore({ path });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(async () => {
|
|
32
|
-
await rm(dir, { recursive: true, force: true });
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
function recordingSampler(): UsagePolicyTarget & {
|
|
36
|
-
calls: { slot: string; usage: DesiredUsage; at?: number }[];
|
|
37
|
-
} {
|
|
38
|
-
const calls: { slot: string; usage: DesiredUsage; at?: number }[] = [];
|
|
39
|
-
return {
|
|
40
|
-
calls,
|
|
41
|
-
applyUsagePolicy(slot, usage, at) {
|
|
42
|
-
calls.push({ slot, usage, ...(at !== undefined ? { at } : {}) });
|
|
43
|
-
return { cycleStartMs: NOW, cycleReset: true };
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
describe('setUsagePolicy — persistence', () => {
|
|
49
|
-
test('writes a policy that reads back through getUsagePolicy', async () => {
|
|
50
|
-
const result = await setUsagePolicy(
|
|
51
|
-
{ store, now: () => NOW },
|
|
52
|
-
{ logicalSlotId: SLOT, cycleDay: 15, thresholdBytes: 5_000_000_000 },
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
expect(result.status).toBe('applied');
|
|
56
|
-
expect(await getUsagePolicy({ store }, SLOT)).toEqual({
|
|
57
|
-
cycleDay: 15,
|
|
58
|
-
thresholdBytes: 5_000_000_000,
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('an OMITTED field is left alone — a threshold write never drops the cycle day', async () => {
|
|
63
|
-
await setUsagePolicy({ store }, { logicalSlotId: SLOT, cycleDay: 9 });
|
|
64
|
-
|
|
65
|
-
await setUsagePolicy({ store }, { logicalSlotId: SLOT, thresholdBytes: 100 });
|
|
66
|
-
|
|
67
|
-
expect(await getUsagePolicy({ store }, SLOT)).toEqual({ cycleDay: 9, thresholdBytes: 100 });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('an explicit null CLEARS that field and leaves the sibling standing', async () => {
|
|
71
|
-
await setUsagePolicy({ store }, { logicalSlotId: SLOT, cycleDay: 9, thresholdBytes: 100 });
|
|
72
|
-
|
|
73
|
-
await setUsagePolicy({ store }, { logicalSlotId: SLOT, cycleDay: null });
|
|
74
|
-
|
|
75
|
-
expect(await getUsagePolicy({ store }, SLOT)).toEqual({ thresholdBytes: 100 });
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('clearing BOTH fields removes the row rather than storing an empty one', async () => {
|
|
79
|
-
await setUsagePolicy({ store }, { logicalSlotId: SLOT, cycleDay: 9, thresholdBytes: 100 });
|
|
80
|
-
|
|
81
|
-
await setUsagePolicy({ store }, { logicalSlotId: SLOT, cycleDay: null, thresholdBytes: null });
|
|
82
|
-
|
|
83
|
-
expect((await store.load(NOW)).slots).toEqual([]);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('a write to one slot never disturbs another', async () => {
|
|
87
|
-
await setUsagePolicy({ store }, { logicalSlotId: 'slot-a', cycleDay: 1 });
|
|
88
|
-
await setUsagePolicy({ store }, { logicalSlotId: 'slot-b', cycleDay: 20 });
|
|
89
|
-
|
|
90
|
-
await setUsagePolicy({ store }, { logicalSlotId: 'slot-a', cycleDay: 5 });
|
|
91
|
-
|
|
92
|
-
const state = await store.load(NOW);
|
|
93
|
-
expect(selectUsagePolicy(state, 'slot-a')).toEqual({ cycleDay: 5 });
|
|
94
|
-
expect(selectUsagePolicy(state, 'slot-b')).toEqual({ cycleDay: 20 });
|
|
95
|
-
expect(state.slots).toHaveLength(2);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('setUsagePolicy — typed refusals', () => {
|
|
100
|
-
test.each([
|
|
101
|
-
[{ logicalSlotId: '', cycleDay: 1 }, 'invalid-slot-id'],
|
|
102
|
-
[{ logicalSlotId: SLOT, cycleDay: 0 }, 'invalid-cycle-day'],
|
|
103
|
-
[{ logicalSlotId: SLOT, cycleDay: 32 }, 'invalid-cycle-day'],
|
|
104
|
-
[{ logicalSlotId: SLOT, cycleDay: 3.5 }, 'invalid-cycle-day'],
|
|
105
|
-
[{ logicalSlotId: SLOT, thresholdBytes: -1 }, 'invalid-threshold-bytes'],
|
|
106
|
-
[{ logicalSlotId: SLOT, thresholdBytes: 1.5 }, 'invalid-threshold-bytes'],
|
|
107
|
-
] as const)('%p is rejected as %s and writes nothing', async (request, reason) => {
|
|
108
|
-
const result = await setUsagePolicy({ store }, request);
|
|
109
|
-
|
|
110
|
-
expect(result).toMatchObject({ status: 'rejected', reason });
|
|
111
|
-
expect((await store.load(NOW)).slots).toEqual([]);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test('a refusal is returned, never thrown', async () => {
|
|
115
|
-
expect(setUsagePolicy({ store }, { logicalSlotId: SLOT, cycleDay: 99 })).resolves.toMatchObject(
|
|
116
|
-
{ status: 'rejected' },
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('a store that throws yields a typed failure and never reaches the sampler', async () => {
|
|
121
|
-
const sampler = recordingSampler();
|
|
122
|
-
const broken: UsagePolicyStore = {
|
|
123
|
-
load: async () => ({
|
|
124
|
-
schemaVersion: USAGE_POLICY_SCHEMA_VERSION,
|
|
125
|
-
savedAtMs: NOW,
|
|
126
|
-
slots: [],
|
|
127
|
-
}),
|
|
128
|
-
save: async () => {
|
|
129
|
-
throw new Error('disk full');
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const result = await setUsagePolicy(
|
|
134
|
-
{ store: broken, sampler },
|
|
135
|
-
{ logicalSlotId: SLOT, cycleDay: 3 },
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
expect(result).toMatchObject({ status: 'failed', reason: 'disk full' });
|
|
139
|
-
expect(sampler.calls).toEqual([]);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe('setUsagePolicy — live apply', () => {
|
|
144
|
-
test('the merged policy (not the raw request) reaches the sampler', async () => {
|
|
145
|
-
await setUsagePolicy({ store }, { logicalSlotId: SLOT, cycleDay: 9 });
|
|
146
|
-
const sampler = recordingSampler();
|
|
147
|
-
|
|
148
|
-
const result = await setUsagePolicy(
|
|
149
|
-
{ store, sampler, now: () => NOW },
|
|
150
|
-
{ logicalSlotId: SLOT, thresholdBytes: 250 },
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
expect(sampler.calls).toEqual([
|
|
154
|
-
{ slot: SLOT, usage: { cycleDay: 9, thresholdBytes: 250 }, at: NOW },
|
|
155
|
-
]);
|
|
156
|
-
expect(result).toMatchObject({ status: 'applied', applied: { cycleReset: true } });
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test('with no sampler the write is persistence-only and reports no application', async () => {
|
|
160
|
-
const result = await setUsagePolicy({ store }, { logicalSlotId: SLOT, cycleDay: 2 });
|
|
161
|
-
|
|
162
|
-
expect(result).toEqual({ status: 'applied', logicalSlotId: SLOT, usage: { cycleDay: 2 } });
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('a sampler that throws is a typed failure AFTER the write already landed', async () => {
|
|
166
|
-
const result = await setUsagePolicy(
|
|
167
|
-
{
|
|
168
|
-
store,
|
|
169
|
-
sampler: {
|
|
170
|
-
applyUsagePolicy() {
|
|
171
|
-
throw new Error('sampler gone');
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
{ logicalSlotId: SLOT, cycleDay: 4 },
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
expect(result).toMatchObject({ status: 'failed', reason: 'sampler gone' });
|
|
179
|
-
expect(await getUsagePolicy({ store }, SLOT)).toEqual({ cycleDay: 4 });
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('getUsagePolicy', () => {
|
|
184
|
-
test('an unwritten slot answers "no policy set"', async () => {
|
|
185
|
-
expect(await getUsagePolicy({ store }, 'never-written')).toEqual({});
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test('reads a policy laid down by a previous process', async () => {
|
|
189
|
-
const seeded: PersistedUsagePolicy = {
|
|
190
|
-
schemaVersion: USAGE_POLICY_SCHEMA_VERSION,
|
|
191
|
-
savedAtMs: NOW,
|
|
192
|
-
slots: [{ logicalSlotId: SLOT, cycleDay: 28 }],
|
|
193
|
-
};
|
|
194
|
-
await store.save(seeded);
|
|
195
|
-
|
|
196
|
-
expect(await getUsagePolicy({ store }, SLOT)).toEqual({ cycleDay: 28 });
|
|
197
|
-
});
|
|
198
|
-
});
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
// `setUsagePolicy` — the WRITE half of the data-usage surface.
|
|
2
|
-
//
|
|
3
|
-
// The read half already existed (`UsageSampler.snapshot()` reports `cycleBytes`,
|
|
4
|
-
// `thresholdBytes` and `thresholdExceeded`), but nothing could SET the two
|
|
5
|
-
// numbers those readings are computed against: `DesiredUsage` was a shape the
|
|
6
|
-
// planner echoed into a receipt, with no persistence and no apply path. This
|
|
7
|
-
// module closes that, mirroring the read side's file-store idiom exactly.
|
|
8
|
-
//
|
|
9
|
-
// It is a LOCAL write, not a modem write — see `policy-store.ts`'s header for the
|
|
10
|
-
// ModemManager API evidence. Nothing here touches D-Bus, `mmcli`, or any bearer.
|
|
11
|
-
//
|
|
12
|
-
// TYPED RESULTS, NEVER THROWS ON BAD INPUT. Following the `PowerHook` precedent
|
|
13
|
-
// (`power-contract.ts`: `applied` / `unsupported` / `failed`), an out-of-range
|
|
14
|
-
// day is a `rejected` result carrying a named reason rather than an exception —
|
|
15
|
-
// this is called from an RPC boundary where a throw becomes an opaque 500.
|
|
16
|
-
|
|
17
|
-
import type { DesiredUsage } from '../../domain';
|
|
18
|
-
import {
|
|
19
|
-
isValidCycleDay,
|
|
20
|
-
isValidThresholdBytes,
|
|
21
|
-
type PersistedUsagePolicySlot,
|
|
22
|
-
selectUsagePolicy,
|
|
23
|
-
USAGE_POLICY_SCHEMA_VERSION,
|
|
24
|
-
type UsagePolicyStore,
|
|
25
|
-
} from './policy-store';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* The live-apply seam. `UsageSampler` implements it; a caller with no running
|
|
29
|
-
* sampler simply omits it and the write is persistence-only.
|
|
30
|
-
*/
|
|
31
|
-
export interface UsagePolicyTarget {
|
|
32
|
-
applyUsagePolicy(
|
|
33
|
-
logicalSlotId: string,
|
|
34
|
-
usage: DesiredUsage,
|
|
35
|
-
atMs?: number,
|
|
36
|
-
): UsagePolicyApplication;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** What a live apply did to the slot's accounting window. */
|
|
40
|
-
export interface UsagePolicyApplication {
|
|
41
|
-
/** The UTC start of the cycle the slot is now accruing into. */
|
|
42
|
-
readonly cycleStartMs: number;
|
|
43
|
-
/** True when the cycle ANCHOR moved, so the per-cycle total restarted at 0. */
|
|
44
|
-
readonly cycleReset: boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface SetUsagePolicyDeps {
|
|
48
|
-
readonly store: UsagePolicyStore;
|
|
49
|
-
/** Optional live sampler to apply the change to immediately. */
|
|
50
|
-
readonly sampler?: UsagePolicyTarget;
|
|
51
|
-
/** Injectable clock (defaults to `Date.now`). */
|
|
52
|
-
readonly now?: () => number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The requested change.
|
|
57
|
-
*
|
|
58
|
-
* Tri-state per field, and the distinction is the whole point: `undefined`
|
|
59
|
-
* leaves the persisted value ALONE (so a caller changing only the threshold
|
|
60
|
-
* cannot silently drop a cycle day it never mentioned), while an explicit `null`
|
|
61
|
-
* CLEARS it. A caller that cannot express `null` can never unset a policy.
|
|
62
|
-
*/
|
|
63
|
-
export interface SetUsagePolicyRequest {
|
|
64
|
-
readonly logicalSlotId: string;
|
|
65
|
-
readonly cycleDay?: number | null;
|
|
66
|
-
readonly thresholdBytes?: number | null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export type SetUsagePolicyRejection =
|
|
70
|
-
| 'invalid-slot-id'
|
|
71
|
-
| 'invalid-cycle-day'
|
|
72
|
-
| 'invalid-threshold-bytes';
|
|
73
|
-
|
|
74
|
-
export type SetUsagePolicyResult =
|
|
75
|
-
| {
|
|
76
|
-
readonly status: 'applied';
|
|
77
|
-
readonly logicalSlotId: string;
|
|
78
|
-
/** The policy now persisted for this slot (post-merge). */
|
|
79
|
-
readonly usage: DesiredUsage;
|
|
80
|
-
/** Present only when a live sampler was supplied. */
|
|
81
|
-
readonly applied?: UsagePolicyApplication;
|
|
82
|
-
}
|
|
83
|
-
| {
|
|
84
|
-
readonly status: 'rejected';
|
|
85
|
-
readonly logicalSlotId: string;
|
|
86
|
-
readonly reason: SetUsagePolicyRejection;
|
|
87
|
-
}
|
|
88
|
-
| {
|
|
89
|
-
readonly status: 'failed';
|
|
90
|
-
readonly logicalSlotId: string;
|
|
91
|
-
readonly reason: string;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
function validateRequest(request: SetUsagePolicyRequest): SetUsagePolicyRejection | undefined {
|
|
95
|
-
if (typeof request.logicalSlotId !== 'string' || request.logicalSlotId.length === 0) {
|
|
96
|
-
return 'invalid-slot-id';
|
|
97
|
-
}
|
|
98
|
-
if (
|
|
99
|
-
request.cycleDay !== undefined &&
|
|
100
|
-
request.cycleDay !== null &&
|
|
101
|
-
!isValidCycleDay(request.cycleDay)
|
|
102
|
-
) {
|
|
103
|
-
return 'invalid-cycle-day';
|
|
104
|
-
}
|
|
105
|
-
if (
|
|
106
|
-
request.thresholdBytes !== undefined &&
|
|
107
|
-
request.thresholdBytes !== null &&
|
|
108
|
-
!isValidThresholdBytes(request.thresholdBytes)
|
|
109
|
-
) {
|
|
110
|
-
return 'invalid-threshold-bytes';
|
|
111
|
-
}
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Fold the request onto the currently-persisted policy (tri-state merge). */
|
|
116
|
-
function mergePolicy(current: DesiredUsage, request: SetUsagePolicyRequest): DesiredUsage {
|
|
117
|
-
const cycleDay =
|
|
118
|
-
request.cycleDay === undefined ? current.cycleDay : (request.cycleDay ?? undefined);
|
|
119
|
-
const thresholdBytes =
|
|
120
|
-
request.thresholdBytes === undefined
|
|
121
|
-
? current.thresholdBytes
|
|
122
|
-
: (request.thresholdBytes ?? undefined);
|
|
123
|
-
return {
|
|
124
|
-
...(cycleDay !== undefined ? { cycleDay } : {}),
|
|
125
|
-
...(thresholdBytes !== undefined ? { thresholdBytes } : {}),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function toSlot(logicalSlotId: string, usage: DesiredUsage): PersistedUsagePolicySlot {
|
|
130
|
-
return {
|
|
131
|
-
logicalSlotId,
|
|
132
|
-
...(usage.cycleDay !== undefined ? { cycleDay: usage.cycleDay } : {}),
|
|
133
|
-
...(usage.thresholdBytes !== undefined ? { thresholdBytes: usage.thresholdBytes } : {}),
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Persist a slot's usage policy and, when a live sampler is supplied, apply it
|
|
139
|
-
* to that sampler in the same call.
|
|
140
|
-
*
|
|
141
|
-
* ORDER IS LOAD → VALIDATE → PERSIST → APPLY, and it is deliberate. The store is
|
|
142
|
-
* the source of truth (the composition root rebuilds every `UsageObservation`
|
|
143
|
-
* from it), so a live apply that landed while the write failed would leave the
|
|
144
|
-
* running process disagreeing with what a restart would restore.
|
|
145
|
-
*/
|
|
146
|
-
export async function setUsagePolicy(
|
|
147
|
-
deps: SetUsagePolicyDeps,
|
|
148
|
-
request: SetUsagePolicyRequest,
|
|
149
|
-
): Promise<SetUsagePolicyResult> {
|
|
150
|
-
const logicalSlotId = typeof request.logicalSlotId === 'string' ? request.logicalSlotId : '';
|
|
151
|
-
const rejection = validateRequest(request);
|
|
152
|
-
if (rejection !== undefined) {
|
|
153
|
-
return { status: 'rejected', logicalSlotId, reason: rejection };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const now = deps.now ?? Date.now;
|
|
157
|
-
const at = now();
|
|
158
|
-
|
|
159
|
-
let usage: DesiredUsage;
|
|
160
|
-
try {
|
|
161
|
-
const state = await deps.store.load(at);
|
|
162
|
-
usage = mergePolicy(selectUsagePolicy(state, logicalSlotId), request);
|
|
163
|
-
const others = state.slots.filter((slot) => slot.logicalSlotId !== logicalSlotId);
|
|
164
|
-
// An empty policy is REMOVED rather than stored as an empty row: "no policy"
|
|
165
|
-
// and "a policy that sets nothing" are the same fact, and keeping the row
|
|
166
|
-
// would grow the file by one entry per slot an operator ever cleared.
|
|
167
|
-
const slots =
|
|
168
|
-
usage.cycleDay === undefined && usage.thresholdBytes === undefined
|
|
169
|
-
? others
|
|
170
|
-
: [...others, toSlot(logicalSlotId, usage)];
|
|
171
|
-
await deps.store.save({
|
|
172
|
-
schemaVersion: USAGE_POLICY_SCHEMA_VERSION,
|
|
173
|
-
savedAtMs: at,
|
|
174
|
-
slots,
|
|
175
|
-
});
|
|
176
|
-
} catch (error) {
|
|
177
|
-
return {
|
|
178
|
-
status: 'failed',
|
|
179
|
-
logicalSlotId,
|
|
180
|
-
reason: error instanceof Error ? error.message : 'persist-failed',
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (deps.sampler === undefined) {
|
|
185
|
-
return { status: 'applied', logicalSlotId, usage };
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
const applied = deps.sampler.applyUsagePolicy(logicalSlotId, usage, at);
|
|
189
|
-
return { status: 'applied', logicalSlotId, usage, applied };
|
|
190
|
-
} catch (error) {
|
|
191
|
-
return {
|
|
192
|
-
status: 'failed',
|
|
193
|
-
logicalSlotId,
|
|
194
|
-
reason: error instanceof Error ? error.message : 'apply-failed',
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Read one slot's persisted policy. The read counterpart of `setUsagePolicy`. */
|
|
200
|
-
export async function getUsagePolicy(
|
|
201
|
-
deps: Pick<SetUsagePolicyDeps, 'store' | 'now'>,
|
|
202
|
-
logicalSlotId: string,
|
|
203
|
-
): Promise<DesiredUsage> {
|
|
204
|
-
const now = deps.now ?? Date.now;
|
|
205
|
-
const state = await deps.store.load(now());
|
|
206
|
-
return selectUsagePolicy(state, logicalSlotId);
|
|
207
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// `/proc/net/dev` parser — proves rx+tx cumulative totals over the real fixed-column
|
|
2
|
-
// format, header skipping, and fail-soft handling of malformed rows.
|
|
3
|
-
|
|
4
|
-
import { describe, expect, test } from 'bun:test';
|
|
5
|
-
import { parseProcNetDev } from './proc-net-dev';
|
|
6
|
-
|
|
7
|
-
// A real capture (columns: rx bytes packets errs drop fifo frame compressed multicast
|
|
8
|
-
// | tx bytes packets errs drop fifo colls carrier compressed).
|
|
9
|
-
const REAL = `Inter-| Receive | Transmit
|
|
10
|
-
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
|
|
11
|
-
lo: 160183007830 18519510 0 0 0 0 0 0 160183007830 18519510 0 0 0 0 0 0
|
|
12
|
-
eno2: 88166142004 86575608 0 0 48 0 0 146660 25843393633 48414413 0 3 0 0 0 0
|
|
13
|
-
wlan0: 44865976 194892 0 0 0 0 0 0 5698312 36057 0 23 0 0 0 0
|
|
14
|
-
`;
|
|
15
|
-
|
|
16
|
-
describe('parseProcNetDev', () => {
|
|
17
|
-
test('reads rx+tx cumulative bytes for each interface, skipping the 2-line header', () => {
|
|
18
|
-
const counters = parseProcNetDev(REAL);
|
|
19
|
-
expect(counters.size).toBe(3);
|
|
20
|
-
// lo: 160183007830 (rx) + 160183007830 (tx).
|
|
21
|
-
expect(counters.get('lo')).toBe(160183007830 + 160183007830);
|
|
22
|
-
// eno2: rx 88166142004 + tx 25843393633.
|
|
23
|
-
expect(counters.get('eno2')).toBe(88166142004 + 25843393633);
|
|
24
|
-
// wlan0: rx 44865976 + tx 5698312.
|
|
25
|
-
expect(counters.get('wlan0')).toBe(44865976 + 5698312);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('handles bridge-style names with no leading space and long names', () => {
|
|
29
|
-
const text = `h1
|
|
30
|
-
h2
|
|
31
|
-
br-d739ee545df1: 575402021 542380 0 0 0 0 0 0 450821722 810015 0 4 0 0 0 0
|
|
32
|
-
docker0: 1750867165 1097738 0 0 0 0 0 0 5810268898 1237921 0 12223 0 0 0 0
|
|
33
|
-
`;
|
|
34
|
-
const counters = parseProcNetDev(text);
|
|
35
|
-
expect(counters.get('br-d739ee545df1')).toBe(575402021 + 450821722);
|
|
36
|
-
expect(counters.get('docker0')).toBe(1750867165 + 5810268898);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('ignores malformed / short rows without throwing', () => {
|
|
40
|
-
const text = `h1
|
|
41
|
-
h2
|
|
42
|
-
lo: 100 1 0 0 0 0 0 0 200 2 0 0 0 0 0 0
|
|
43
|
-
garbage-without-colon 1 2 3
|
|
44
|
-
short: 1 2 3
|
|
45
|
-
nan: x 1 0 0 0 0 0 0 y 2 0 0 0 0 0 0
|
|
46
|
-
`;
|
|
47
|
-
const counters = parseProcNetDev(text);
|
|
48
|
-
// Only the well-formed `lo` row survives.
|
|
49
|
-
expect(counters.size).toBe(1);
|
|
50
|
-
expect(counters.get('lo')).toBe(300);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('empty input yields an empty map', () => {
|
|
54
|
-
expect(parseProcNetDev('').size).toBe(0);
|
|
55
|
-
});
|
|
56
|
-
});
|