@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
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
// The sampler orchestration — ambiguous-identity pause, threshold advisory
|
|
2
|
-
// appear/clear, rate-limited persistence + shutdown flush, reboot re-baselining, and
|
|
3
|
-
// the queryable snapshot API the A6.1 bench CLI `usage` command consumes.
|
|
4
|
-
|
|
5
|
-
import { describe, expect, test } from 'bun:test';
|
|
6
|
-
import { type DesiredUsage, logicalSlotId } from '../../domain';
|
|
7
|
-
import type { CounterSource } from './proc-net-dev';
|
|
8
|
-
import { createUsageSampler, type UsageObservation } from './sampler';
|
|
9
|
-
import { type PersistedUsage, USAGE_SCHEMA_VERSION, type UsageStore } from './store';
|
|
10
|
-
|
|
11
|
-
const SLOT_A = logicalSlotId('slot-a');
|
|
12
|
-
const SLOT_B = logicalSlotId('slot-b');
|
|
13
|
-
|
|
14
|
-
class FakeCounters implements CounterSource {
|
|
15
|
-
readonly #map = new Map<string, number>();
|
|
16
|
-
set(ifname: string, value: number): void {
|
|
17
|
-
this.#map.set(ifname, value);
|
|
18
|
-
}
|
|
19
|
-
async read(): Promise<ReadonlyMap<string, number>> {
|
|
20
|
-
return new Map(this.#map);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
class MemStore implements UsageStore {
|
|
25
|
-
doc: PersistedUsage | null = null;
|
|
26
|
-
saves = 0;
|
|
27
|
-
async load(bootId: string, nowMs: number): Promise<PersistedUsage> {
|
|
28
|
-
return this.doc ?? { schemaVersion: USAGE_SCHEMA_VERSION, bootId, savedAtMs: nowMs, slots: [] };
|
|
29
|
-
}
|
|
30
|
-
async save(state: PersistedUsage): Promise<void> {
|
|
31
|
-
this.saves += 1;
|
|
32
|
-
this.doc = state;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function obs(
|
|
37
|
-
slot: string,
|
|
38
|
-
ifname: string,
|
|
39
|
-
usage: DesiredUsage = {},
|
|
40
|
-
confidence: 'high' | 'medium' | 'low' = 'high',
|
|
41
|
-
mappingGeneration = 0,
|
|
42
|
-
): UsageObservation {
|
|
43
|
-
return { logicalSlotId: logicalSlotId(slot), mappingGeneration, ifname, confidence, usage };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe('UsageSampler — snapshot API (A6.1 usage command)', () => {
|
|
47
|
-
test('reports independent per-slot cumulative usage', async () => {
|
|
48
|
-
const counters = new FakeCounters();
|
|
49
|
-
const sampler = await createUsageSampler({
|
|
50
|
-
bootId: 'boot-1',
|
|
51
|
-
source: counters,
|
|
52
|
-
store: new MemStore(),
|
|
53
|
-
now: () => 1000,
|
|
54
|
-
});
|
|
55
|
-
counters.set('wwan0', 100);
|
|
56
|
-
counters.set('wwan1', 5000);
|
|
57
|
-
await sampler.sample([obs(SLOT_A, 'wwan0'), obs(SLOT_B, 'wwan1')]);
|
|
58
|
-
counters.set('wwan0', 300);
|
|
59
|
-
counters.set('wwan1', 5001);
|
|
60
|
-
await sampler.sample([obs(SLOT_A, 'wwan0'), obs(SLOT_B, 'wwan1')]);
|
|
61
|
-
|
|
62
|
-
const snap = sampler.snapshot();
|
|
63
|
-
expect(snap.bootId).toBe('boot-1');
|
|
64
|
-
expect(snap.slots).toHaveLength(2);
|
|
65
|
-
const a = snap.slots.find((s) => s.logicalSlotId === 'slot-a');
|
|
66
|
-
const b = snap.slots.find((s) => s.logicalSlotId === 'slot-b');
|
|
67
|
-
expect(a?.cycleBytes).toBe(200);
|
|
68
|
-
expect(a?.paused).toBe(false);
|
|
69
|
-
expect(a?.thresholdExceeded).toBe(false);
|
|
70
|
-
expect(b?.cycleBytes).toBe(1);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe('UsageSampler — ambiguous identity pauses sampling', () => {
|
|
75
|
-
test('a low-confidence slot attributes nothing and reports paused', async () => {
|
|
76
|
-
const counters = new FakeCounters();
|
|
77
|
-
const sampler = await createUsageSampler({
|
|
78
|
-
bootId: 'boot-1',
|
|
79
|
-
source: counters,
|
|
80
|
-
store: new MemStore(),
|
|
81
|
-
now: () => 1000,
|
|
82
|
-
});
|
|
83
|
-
counters.set('wwan0', 100);
|
|
84
|
-
await sampler.sample([obs(SLOT_A, 'wwan0')]);
|
|
85
|
-
counters.set('wwan0', 400);
|
|
86
|
-
await sampler.sample([obs(SLOT_A, 'wwan0')]);
|
|
87
|
-
expect(sampler.snapshot().slots[0]?.cycleBytes).toBe(300);
|
|
88
|
-
|
|
89
|
-
// Identity turns ambiguous — counter keeps climbing, nothing is attributed.
|
|
90
|
-
counters.set('wwan0', 9999);
|
|
91
|
-
await sampler.sample([obs(SLOT_A, 'wwan0', {}, 'low')]);
|
|
92
|
-
const paused = sampler.snapshot().slots[0];
|
|
93
|
-
expect(paused?.paused).toBe(true);
|
|
94
|
-
expect(paused?.cycleBytes).toBe(300);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe('UsageSampler — threshold advisory appears and clears', () => {
|
|
99
|
-
test('crossing thresholdBytes flags an advisory that clears on cycle rollover', async () => {
|
|
100
|
-
const counters = new FakeCounters();
|
|
101
|
-
let clock = Date.UTC(2024, 5, 15); // mid-June
|
|
102
|
-
const usage: DesiredUsage = { cycleDay: 1, thresholdBytes: 1000 };
|
|
103
|
-
const sampler = await createUsageSampler({
|
|
104
|
-
bootId: 'boot-1',
|
|
105
|
-
source: counters,
|
|
106
|
-
store: new MemStore(),
|
|
107
|
-
now: () => clock,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
counters.set('wwan0', 0);
|
|
111
|
-
await sampler.sample([obs(SLOT_A, 'wwan0', usage)]);
|
|
112
|
-
counters.set('wwan0', 2000); // +2000 > 1000 threshold
|
|
113
|
-
await sampler.sample([obs(SLOT_A, 'wwan0', usage)]);
|
|
114
|
-
const exceeded = sampler.snapshot().slots[0];
|
|
115
|
-
expect(exceeded?.cycleBytes).toBe(2000);
|
|
116
|
-
expect(exceeded?.thresholdBytes).toBe(1000);
|
|
117
|
-
expect(exceeded?.thresholdExceeded).toBe(true);
|
|
118
|
-
|
|
119
|
-
// New billing cycle (July 1) → per-cycle total resets → advisory clears.
|
|
120
|
-
clock = Date.UTC(2024, 6, 2);
|
|
121
|
-
counters.set('wwan0', 2100); // +100 into the fresh cycle
|
|
122
|
-
await sampler.sample([obs(SLOT_A, 'wwan0', usage)]);
|
|
123
|
-
const cleared = sampler.snapshot().slots[0];
|
|
124
|
-
expect(cleared?.cycleBytes).toBe(100);
|
|
125
|
-
expect(cleared?.thresholdExceeded).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('UsageSampler — rate-limited persistence + shutdown flush', () => {
|
|
130
|
-
test('persists at most once per minute; flush forces an immediate write', async () => {
|
|
131
|
-
const counters = new FakeCounters();
|
|
132
|
-
const store = new MemStore();
|
|
133
|
-
let clock = 1000;
|
|
134
|
-
const sampler = await createUsageSampler({
|
|
135
|
-
bootId: 'boot-1',
|
|
136
|
-
source: counters,
|
|
137
|
-
store,
|
|
138
|
-
now: () => clock,
|
|
139
|
-
persistIntervalMs: 60_000,
|
|
140
|
-
});
|
|
141
|
-
counters.set('wwan0', 100);
|
|
142
|
-
await sampler.sample([obs(SLOT_A, 'wwan0')]); // t=1000, within interval → no save
|
|
143
|
-
clock = 2000;
|
|
144
|
-
await sampler.sample([obs(SLOT_A, 'wwan0')]); // t=2000, still within → no save
|
|
145
|
-
expect(store.saves).toBe(0);
|
|
146
|
-
|
|
147
|
-
await sampler.flush(); // shutdown hook — forces a write despite the rate limit
|
|
148
|
-
expect(store.saves).toBe(1);
|
|
149
|
-
|
|
150
|
-
clock = 62_000; // > 60s since the flush persist → next sample persists
|
|
151
|
-
counters.set('wwan0', 150);
|
|
152
|
-
await sampler.sample([obs(SLOT_A, 'wwan0')]);
|
|
153
|
-
expect(store.saves).toBe(2);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('a clean flush persists the latest cycle total', async () => {
|
|
157
|
-
const counters = new FakeCounters();
|
|
158
|
-
const store = new MemStore();
|
|
159
|
-
const sampler = await createUsageSampler({
|
|
160
|
-
bootId: 'boot-1',
|
|
161
|
-
source: counters,
|
|
162
|
-
store,
|
|
163
|
-
now: () => 1000,
|
|
164
|
-
});
|
|
165
|
-
counters.set('wwan0', 100);
|
|
166
|
-
await sampler.sample([obs(SLOT_A, 'wwan0')]);
|
|
167
|
-
counters.set('wwan0', 500);
|
|
168
|
-
await sampler.sample([obs(SLOT_A, 'wwan0')]);
|
|
169
|
-
await sampler.flush();
|
|
170
|
-
expect(store.doc?.slots[0]?.cycleBytes).toBe(400);
|
|
171
|
-
expect(store.doc?.slots[0]?.lastObserved).toBe(500);
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
describe('UsageSampler — reboot (new boot id) re-baselines without losing the cycle total', () => {
|
|
176
|
-
test('same-boot reload resumes the baseline; a new boot id drops it', async () => {
|
|
177
|
-
const counters = new FakeCounters();
|
|
178
|
-
const store = new MemStore();
|
|
179
|
-
const first = await createUsageSampler({
|
|
180
|
-
bootId: 'boot-1',
|
|
181
|
-
source: counters,
|
|
182
|
-
store,
|
|
183
|
-
now: () => 1000,
|
|
184
|
-
});
|
|
185
|
-
counters.set('wwan0', 100);
|
|
186
|
-
await first.sample([obs(SLOT_A, 'wwan0')]);
|
|
187
|
-
counters.set('wwan0', 300);
|
|
188
|
-
await first.sample([obs(SLOT_A, 'wwan0')]);
|
|
189
|
-
await first.flush();
|
|
190
|
-
expect(store.doc?.slots[0]?.cycleBytes).toBe(200);
|
|
191
|
-
|
|
192
|
-
// Same boot id → baseline resumes; counter continues from 300.
|
|
193
|
-
const resumed = await createUsageSampler({
|
|
194
|
-
bootId: 'boot-1',
|
|
195
|
-
source: counters,
|
|
196
|
-
store,
|
|
197
|
-
now: () => 1000,
|
|
198
|
-
});
|
|
199
|
-
counters.set('wwan0', 350);
|
|
200
|
-
await resumed.sample([obs(SLOT_A, 'wwan0')]);
|
|
201
|
-
expect(resumed.snapshot().slots[0]?.cycleBytes).toBe(250);
|
|
202
|
-
await resumed.flush();
|
|
203
|
-
|
|
204
|
-
// Reboot: new boot id + counters reset to a low value. The cycle total is kept,
|
|
205
|
-
// but the baseline is dropped so the reset is not mis-attributed.
|
|
206
|
-
const rebooted = await createUsageSampler({
|
|
207
|
-
bootId: 'boot-2',
|
|
208
|
-
source: counters,
|
|
209
|
-
store,
|
|
210
|
-
now: () => 1000,
|
|
211
|
-
});
|
|
212
|
-
counters.set('wwan0', 50); // kernel counters reset on reboot
|
|
213
|
-
await rebooted.sample([obs(SLOT_A, 'wwan0')]); // zero-delta re-baseline
|
|
214
|
-
expect(rebooted.snapshot().slots[0]?.cycleBytes).toBe(250);
|
|
215
|
-
counters.set('wwan0', 150);
|
|
216
|
-
await rebooted.sample([obs(SLOT_A, 'wwan0')]);
|
|
217
|
-
expect(rebooted.snapshot().slots[0]?.cycleBytes).toBe(350);
|
|
218
|
-
});
|
|
219
|
-
});
|
|
@@ -1,228 +0,0 @@
|
|
|
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
|
-
|
|
14
|
-
import { type DesiredUsage, epochMillis, type LogicalSlotId } from '../../domain';
|
|
15
|
-
import { applySample, type BaselineKey, initialAccount, type SlotAccount } from './accounting';
|
|
16
|
-
import { cycleStart } from './billing-cycle';
|
|
17
|
-
import type { CounterSource } from './proc-net-dev';
|
|
18
|
-
import type { PersistedSlot, PersistedUsage, UsageStore } from './store';
|
|
19
|
-
import { USAGE_SCHEMA_VERSION } from './store';
|
|
20
|
-
|
|
21
|
-
/** One slot's observation for a sampling pass — identity + mapping + local policy. */
|
|
22
|
-
export interface UsageObservation {
|
|
23
|
-
readonly logicalSlotId: LogicalSlotId;
|
|
24
|
-
/** Bumped by A4.2 whenever the ifname-to-slot mapping changes (→ zero-delta rebase). */
|
|
25
|
-
readonly mappingGeneration: number;
|
|
26
|
-
readonly ifname: string;
|
|
27
|
-
/** A3.2 identity-ladder confidence; `low` pauses sampling for this slot. */
|
|
28
|
-
readonly confidence: 'high' | 'medium' | 'low';
|
|
29
|
-
/** Local-controller-owned usage policy (cycle day + advisory threshold). */
|
|
30
|
-
readonly usage: DesiredUsage;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** A queryable per-slot usage figure — consumed by the A6.1 bench CLI `usage` command. */
|
|
34
|
-
export interface SlotUsageSnapshot {
|
|
35
|
-
readonly logicalSlotId: string;
|
|
36
|
-
readonly cycleBytes: number;
|
|
37
|
-
readonly cycleStartMs: number;
|
|
38
|
-
readonly paused: boolean;
|
|
39
|
-
readonly thresholdBytes?: number;
|
|
40
|
-
/** Advisory-only: `cycleBytes > thresholdBytes`. Never gates the connection. */
|
|
41
|
-
readonly thresholdExceeded: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** The sampler's current state, per slot, at a point in time. */
|
|
45
|
-
export interface UsageSnapshot {
|
|
46
|
-
readonly bootId: string;
|
|
47
|
-
readonly generatedAtMs: number;
|
|
48
|
-
readonly slots: readonly SlotUsageSnapshot[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface UsageSamplerOptions {
|
|
52
|
-
/** The kernel boot id (see `readBootId`) — the session identity. */
|
|
53
|
-
readonly bootId: string;
|
|
54
|
-
readonly source: CounterSource;
|
|
55
|
-
readonly store: UsageStore;
|
|
56
|
-
/** Injectable clock (defaults to `Date.now`). */
|
|
57
|
-
readonly now?: () => number;
|
|
58
|
-
/** Minimum spacing between persists (default 60_000 ms = the ≤1-min loss bound). */
|
|
59
|
-
readonly persistIntervalMs?: number;
|
|
60
|
-
/** Cycle day used when a slot's policy omits `cycleDay` (default 1 = 1st of month). */
|
|
61
|
-
readonly defaultCycleDay?: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const DEFAULT_PERSIST_INTERVAL_MS = 60_000;
|
|
65
|
-
const DEFAULT_CYCLE_DAY = 1;
|
|
66
|
-
|
|
67
|
-
function toPersistedSlot(logicalSlotId: string, account: SlotAccount): PersistedSlot {
|
|
68
|
-
return {
|
|
69
|
-
logicalSlotId,
|
|
70
|
-
cycleBytes: account.cycleBytes,
|
|
71
|
-
cycleStartMs: account.cycleStartMs,
|
|
72
|
-
...(account.key !== undefined
|
|
73
|
-
? { mappingGeneration: account.key.mappingGeneration, ifname: account.key.ifname }
|
|
74
|
-
: {}),
|
|
75
|
-
...(account.lastObserved !== undefined ? { lastObserved: account.lastObserved } : {}),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export class UsageSampler {
|
|
80
|
-
readonly #bootId: string;
|
|
81
|
-
readonly #source: CounterSource;
|
|
82
|
-
readonly #store: UsageStore;
|
|
83
|
-
readonly #now: () => number;
|
|
84
|
-
readonly #persistIntervalMs: number;
|
|
85
|
-
readonly #defaultCycleDay: number;
|
|
86
|
-
readonly #accounts = new Map<string, SlotAccount>();
|
|
87
|
-
readonly #policies = new Map<string, DesiredUsage>();
|
|
88
|
-
#lastPersistMs: number;
|
|
89
|
-
#dirty = false;
|
|
90
|
-
|
|
91
|
-
private constructor(options: UsageSamplerOptions, initial: PersistedUsage) {
|
|
92
|
-
this.#bootId = options.bootId;
|
|
93
|
-
this.#source = options.source;
|
|
94
|
-
this.#store = options.store;
|
|
95
|
-
this.#now = options.now ?? Date.now;
|
|
96
|
-
this.#persistIntervalMs = options.persistIntervalMs ?? DEFAULT_PERSIST_INTERVAL_MS;
|
|
97
|
-
this.#defaultCycleDay = options.defaultCycleDay ?? DEFAULT_CYCLE_DAY;
|
|
98
|
-
this.#lastPersistMs = this.#now();
|
|
99
|
-
this.#hydrate(initial);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Load persisted state (recreating a fresh file if absent/corrupt) then build the sampler. */
|
|
103
|
-
static async create(options: UsageSamplerOptions): Promise<UsageSampler> {
|
|
104
|
-
const now = options.now ?? Date.now;
|
|
105
|
-
const initial = await options.store.load(options.bootId, now());
|
|
106
|
-
return new UsageSampler(options, initial);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Rebuild in-memory accounts. A reboot (differing boot id) drops the baselines. */
|
|
110
|
-
#hydrate(initial: PersistedUsage): void {
|
|
111
|
-
const sameBoot = initial.bootId === this.#bootId;
|
|
112
|
-
for (const slot of initial.slots) {
|
|
113
|
-
const canResume =
|
|
114
|
-
sameBoot &&
|
|
115
|
-
slot.ifname !== undefined &&
|
|
116
|
-
slot.mappingGeneration !== undefined &&
|
|
117
|
-
slot.lastObserved !== undefined;
|
|
118
|
-
if (canResume) {
|
|
119
|
-
const key: BaselineKey = {
|
|
120
|
-
logicalSlotId: slot.logicalSlotId,
|
|
121
|
-
mappingGeneration: slot.mappingGeneration as number,
|
|
122
|
-
ifname: slot.ifname as string,
|
|
123
|
-
bootId: this.#bootId,
|
|
124
|
-
};
|
|
125
|
-
this.#accounts.set(slot.logicalSlotId, {
|
|
126
|
-
cycleBytes: slot.cycleBytes,
|
|
127
|
-
cycleStartMs: slot.cycleStartMs,
|
|
128
|
-
paused: false,
|
|
129
|
-
key,
|
|
130
|
-
lastObserved: slot.lastObserved as number,
|
|
131
|
-
});
|
|
132
|
-
} else {
|
|
133
|
-
this.#accounts.set(slot.logicalSlotId, {
|
|
134
|
-
cycleBytes: slot.cycleBytes,
|
|
135
|
-
cycleStartMs: slot.cycleStartMs,
|
|
136
|
-
paused: false,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Take one sampling pass over the current counters for the given observations. */
|
|
143
|
-
async sample(observations: readonly UsageObservation[]): Promise<void> {
|
|
144
|
-
const counters = await this.#source.read();
|
|
145
|
-
const now = this.#now();
|
|
146
|
-
for (const obs of observations) {
|
|
147
|
-
const slotId = obs.logicalSlotId as string;
|
|
148
|
-
this.#policies.set(slotId, obs.usage);
|
|
149
|
-
const cycleDay = obs.usage.cycleDay ?? this.#defaultCycleDay;
|
|
150
|
-
const cycleStartMs = cycleStart(epochMillis(now), cycleDay);
|
|
151
|
-
const current = counters.get(obs.ifname);
|
|
152
|
-
if (current === undefined) {
|
|
153
|
-
// No reading for this interface — ensure the slot exists, attribute nothing.
|
|
154
|
-
if (!this.#accounts.has(slotId)) {
|
|
155
|
-
this.#accounts.set(slotId, initialAccount(cycleStartMs));
|
|
156
|
-
}
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
const key: BaselineKey = {
|
|
160
|
-
logicalSlotId: slotId,
|
|
161
|
-
mappingGeneration: obs.mappingGeneration,
|
|
162
|
-
ifname: obs.ifname,
|
|
163
|
-
bootId: this.#bootId,
|
|
164
|
-
};
|
|
165
|
-
const next = applySample(this.#accounts.get(slotId), {
|
|
166
|
-
key,
|
|
167
|
-
current,
|
|
168
|
-
confidence: obs.confidence,
|
|
169
|
-
cycleStartMs,
|
|
170
|
-
});
|
|
171
|
-
this.#accounts.set(slotId, next);
|
|
172
|
-
}
|
|
173
|
-
this.#dirty = true;
|
|
174
|
-
await this.#maybePersist(now);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Current per-slot usage — the queryable snapshot the CLI and platform read. */
|
|
178
|
-
snapshot(): UsageSnapshot {
|
|
179
|
-
const generatedAtMs = this.#now();
|
|
180
|
-
const slots: SlotUsageSnapshot[] = [];
|
|
181
|
-
for (const [slotId, account] of this.#accounts) {
|
|
182
|
-
const thresholdBytes = this.#policies.get(slotId)?.thresholdBytes;
|
|
183
|
-
slots.push({
|
|
184
|
-
logicalSlotId: slotId,
|
|
185
|
-
cycleBytes: account.cycleBytes,
|
|
186
|
-
cycleStartMs: account.cycleStartMs,
|
|
187
|
-
paused: account.paused,
|
|
188
|
-
...(thresholdBytes !== undefined ? { thresholdBytes } : {}),
|
|
189
|
-
thresholdExceeded: thresholdBytes !== undefined && account.cycleBytes > thresholdBytes,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
return { bootId: this.#bootId, generatedAtMs, slots };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** Flush unpersisted state immediately — the shutdown hook (bounds loss to ≤1 min). */
|
|
196
|
-
async flush(): Promise<void> {
|
|
197
|
-
if (this.#dirty) {
|
|
198
|
-
await this.#persist(this.#now());
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async #maybePersist(now: number): Promise<void> {
|
|
203
|
-
if (now - this.#lastPersistMs >= this.#persistIntervalMs) {
|
|
204
|
-
await this.#persist(now);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async #persist(now: number): Promise<void> {
|
|
209
|
-
const slots: PersistedSlot[] = [];
|
|
210
|
-
for (const [slotId, account] of this.#accounts) {
|
|
211
|
-
slots.push(toPersistedSlot(slotId, account));
|
|
212
|
-
}
|
|
213
|
-
const state: PersistedUsage = {
|
|
214
|
-
schemaVersion: USAGE_SCHEMA_VERSION,
|
|
215
|
-
bootId: this.#bootId,
|
|
216
|
-
savedAtMs: now,
|
|
217
|
-
slots,
|
|
218
|
-
};
|
|
219
|
-
await this.#store.save(state);
|
|
220
|
-
this.#lastPersistMs = now;
|
|
221
|
-
this.#dirty = false;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Load persisted state and build a ready sampler. See `UsageSampler.create`. */
|
|
226
|
-
export function createUsageSampler(options: UsageSamplerOptions): Promise<UsageSampler> {
|
|
227
|
-
return UsageSampler.create(options);
|
|
228
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// Persistence contract — mode 0600 (real fs.stat), fail-soft corruption recovery
|
|
2
|
-
// with METADATA-ONLY logging, and the no-PII guarantee on the serialized bytes.
|
|
3
|
-
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
5
|
-
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import {
|
|
9
|
-
createUsageFileStore,
|
|
10
|
-
type PersistedUsage,
|
|
11
|
-
USAGE_SCHEMA_VERSION,
|
|
12
|
-
type UsageLogEvent,
|
|
13
|
-
} from './store';
|
|
14
|
-
|
|
15
|
-
let dir: string;
|
|
16
|
-
let path: string;
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
dir = await mkdtemp(join(tmpdir(), 'usage-store-'));
|
|
20
|
-
path = join(dir, 'usage.json');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await rm(dir, { recursive: true, force: true });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const sample: PersistedUsage = {
|
|
28
|
-
schemaVersion: USAGE_SCHEMA_VERSION,
|
|
29
|
-
bootId: 'boot-uuid-1',
|
|
30
|
-
savedAtMs: 1_700_000_000_000,
|
|
31
|
-
slots: [
|
|
32
|
-
{ logicalSlotId: 'slot-a', cycleBytes: 1234, cycleStartMs: 1_699_000_000_000 },
|
|
33
|
-
{
|
|
34
|
-
logicalSlotId: 'slot-b',
|
|
35
|
-
cycleBytes: 42,
|
|
36
|
-
cycleStartMs: 1_699_000_000_000,
|
|
37
|
-
mappingGeneration: 2,
|
|
38
|
-
ifname: 'wwan0',
|
|
39
|
-
lastObserved: 999,
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
describe('UsageStore — versioned round-trip', () => {
|
|
45
|
-
test('save then load returns identical, schema-versioned state', async () => {
|
|
46
|
-
const store = createUsageFileStore({ path });
|
|
47
|
-
await store.save(sample);
|
|
48
|
-
const loaded = await store.load('boot-uuid-1', 1_700_000_100_000);
|
|
49
|
-
expect(loaded.schemaVersion).toBe(USAGE_SCHEMA_VERSION);
|
|
50
|
-
expect(loaded).toEqual(sample);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('an absent file loads as fresh empty state (no write, no warning)', async () => {
|
|
54
|
-
const events: UsageLogEvent[] = [];
|
|
55
|
-
const store = createUsageFileStore({ path, logger: (e) => events.push(e) });
|
|
56
|
-
const loaded = await store.load('boot-uuid-9', 5);
|
|
57
|
-
expect(loaded.slots).toEqual([]);
|
|
58
|
-
expect(loaded.bootId).toBe('boot-uuid-9');
|
|
59
|
-
expect(events).toHaveLength(0);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('UsageStore — mode 0600 via chmod-after-write', () => {
|
|
64
|
-
test('the persisted file ends up owner-read/write only (real fs.stat)', async () => {
|
|
65
|
-
const store = createUsageFileStore({ path });
|
|
66
|
-
await store.save(sample);
|
|
67
|
-
const info = await stat(path);
|
|
68
|
-
expect(info.mode & 0o777).toBe(0o600);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe('UsageStore — fail-soft corruption recovery', () => {
|
|
73
|
-
test('a corrupt file logs METADATA ONLY and recreates a fresh 0600 file', async () => {
|
|
74
|
-
// Corrupt content carrying a secret-looking token we must never echo in the log.
|
|
75
|
-
const corrupt = '{ this is not json ICCID=8988211000000123456 }';
|
|
76
|
-
await writeFile(path, corrupt);
|
|
77
|
-
const events: UsageLogEvent[] = [];
|
|
78
|
-
const store = createUsageFileStore({ path, logger: (e) => events.push(e) });
|
|
79
|
-
|
|
80
|
-
const loaded = await store.load('boot-uuid-2', 77);
|
|
81
|
-
|
|
82
|
-
// Exactly one metadata-only warning.
|
|
83
|
-
expect(events).toHaveLength(1);
|
|
84
|
-
const event = events[0];
|
|
85
|
-
expect(event?.kind).toBe('corrupt-state');
|
|
86
|
-
expect(event?.bytes).toBe(Buffer.byteLength(corrupt, 'utf8'));
|
|
87
|
-
expect(typeof event?.reason).toBe('string');
|
|
88
|
-
// The log NEVER contains the raw content (no secret token leak).
|
|
89
|
-
expect(JSON.stringify(event)).not.toContain('ICCID');
|
|
90
|
-
expect(JSON.stringify(event)).not.toContain('8988211000000123456');
|
|
91
|
-
|
|
92
|
-
// Fail-soft: fresh empty state returned and the file recreated at mode 0600.
|
|
93
|
-
expect(loaded.slots).toEqual([]);
|
|
94
|
-
expect(loaded.bootId).toBe('boot-uuid-2');
|
|
95
|
-
const info = await stat(path);
|
|
96
|
-
expect(info.mode & 0o777).toBe(0o600);
|
|
97
|
-
|
|
98
|
-
// The recreated file is now valid — a second load is clean (no new warning).
|
|
99
|
-
const reloaded = await store.load('boot-uuid-2', 78);
|
|
100
|
-
expect(reloaded.slots).toEqual([]);
|
|
101
|
-
expect(events).toHaveLength(1);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('an incompatible schema version is treated as corruption and recreated', async () => {
|
|
105
|
-
await writeFile(
|
|
106
|
-
path,
|
|
107
|
-
JSON.stringify({ schemaVersion: 999, bootId: 'x', savedAtMs: 0, slots: [] }),
|
|
108
|
-
);
|
|
109
|
-
const events: UsageLogEvent[] = [];
|
|
110
|
-
const store = createUsageFileStore({ path, logger: (e) => events.push(e) });
|
|
111
|
-
const loaded = await store.load('boot-uuid-3', 1);
|
|
112
|
-
expect(events).toHaveLength(1);
|
|
113
|
-
expect(events[0]?.reason).toContain('schemaVersion');
|
|
114
|
-
expect(loaded.slots).toEqual([]);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('UsageStore — no PII in the persisted bytes', () => {
|
|
119
|
-
test('the serialized file contains only opaque ids and numbers', async () => {
|
|
120
|
-
const store = createUsageFileStore({ path });
|
|
121
|
-
await store.save(sample);
|
|
122
|
-
const raw = await readFile(path, 'utf8');
|
|
123
|
-
|
|
124
|
-
// No subscriber/device-identifying fields, by construction. Word-bounded so a
|
|
125
|
-
// benign key like `mappingGeneration` (contains "pin") is not a false positive.
|
|
126
|
-
expect(raw).not.toMatch(
|
|
127
|
-
/\b(iccid|imsi|imei|eid|msisdn|operator|manufacturer|subscriber|pin|puk|password)\b/i,
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
// Every slot object exposes only the allowed keys; ids are opaque strings.
|
|
131
|
-
const parsed = JSON.parse(raw) as PersistedUsage;
|
|
132
|
-
const allowed = new Set([
|
|
133
|
-
'logicalSlotId',
|
|
134
|
-
'cycleBytes',
|
|
135
|
-
'cycleStartMs',
|
|
136
|
-
'mappingGeneration',
|
|
137
|
-
'ifname',
|
|
138
|
-
'lastObserved',
|
|
139
|
-
]);
|
|
140
|
-
for (const slot of parsed.slots) {
|
|
141
|
-
for (const keyName of Object.keys(slot)) {
|
|
142
|
-
expect(allowed.has(keyName)).toBe(true);
|
|
143
|
-
}
|
|
144
|
-
expect(typeof slot.logicalSlotId).toBe('string');
|
|
145
|
-
expect(typeof slot.cycleBytes).toBe('number');
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
});
|