@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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { parseHilinkXmlValue } from './hilink-protocol.js';
|
|
3
|
+
export * from './hilink-protocol.js';
|
|
4
|
+
const SIM_OBJECT_PATH = /^\/org\/freedesktop\/ModemManager1\/SIM\/\d+$/;
|
|
5
|
+
/** The mmcli spelling of `MM_MODEM_STATE_FAILED_REASON_SIM_MISSING` — the ONLY proof of absence. */
|
|
6
|
+
export const SIM_MISSING_FAILED_REASON = 'sim-missing';
|
|
7
|
+
/** The fields a presence decision may be drawn from, in the order they are inspected. */
|
|
8
|
+
export const SIM_PRESENCE_FIELDS = ['sim', 'simSlots', 'failedReason'];
|
|
9
|
+
/** The presence decision together with the fact that produced it. */
|
|
10
|
+
export function readSimPresence(facts) {
|
|
11
|
+
const sim = facts.sim?.trim() ?? '';
|
|
12
|
+
if (SIM_OBJECT_PATH.test(sim)) {
|
|
13
|
+
return { presence: 'present', evidence: { kind: 'sim-object-path', field: 'sim', value: sim } };
|
|
14
|
+
}
|
|
15
|
+
const slot = facts.simSlots
|
|
16
|
+
?.map((each) => each.trim())
|
|
17
|
+
.find((each) => SIM_OBJECT_PATH.test(each));
|
|
18
|
+
if (slot !== undefined) {
|
|
19
|
+
return {
|
|
20
|
+
presence: 'present',
|
|
21
|
+
evidence: { kind: 'sim-slot-object-path', field: 'simSlots', value: slot },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (facts.failedReason === SIM_MISSING_FAILED_REASON) {
|
|
25
|
+
return {
|
|
26
|
+
presence: 'absent',
|
|
27
|
+
evidence: {
|
|
28
|
+
kind: 'state-failed-reason',
|
|
29
|
+
field: 'failedReason',
|
|
30
|
+
value: SIM_MISSING_FAILED_REASON,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { presence: 'unknown', evidence: { kind: 'no-evidence', inspected: SIM_PRESENCE_FIELDS } };
|
|
35
|
+
}
|
|
36
|
+
export function deriveSimPresence(facts) {
|
|
37
|
+
return readSimPresence(facts).presence;
|
|
38
|
+
}
|
|
39
|
+
const unknown = (reason) => ({
|
|
40
|
+
state: 'unknown',
|
|
41
|
+
reason,
|
|
42
|
+
});
|
|
43
|
+
const known = (value) => ({ state: 'known', value });
|
|
44
|
+
function numericMetric(value) {
|
|
45
|
+
if (value === undefined || String(value).trim() === '')
|
|
46
|
+
return unknown('not-reported');
|
|
47
|
+
const parsed = Number.parseFloat(String(value));
|
|
48
|
+
return Number.isFinite(parsed) ? known(parsed) : unknown('malformed');
|
|
49
|
+
}
|
|
50
|
+
const flatRecordSchema = z.record(z.string(), z.union([z.string(), z.number()]));
|
|
51
|
+
const ufiBodySchema = z.object({
|
|
52
|
+
reply: z.string(),
|
|
53
|
+
params: flatRecordSchema.optional(),
|
|
54
|
+
});
|
|
55
|
+
function parseFlatRecord(body) {
|
|
56
|
+
const parsed = z
|
|
57
|
+
.string()
|
|
58
|
+
.transform((value, context) => {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(value);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!(error instanceof SyntaxError))
|
|
64
|
+
throw error;
|
|
65
|
+
context.addIssue({ code: 'custom', message: 'invalid JSON' });
|
|
66
|
+
return z.NEVER;
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.pipe(flatRecordSchema)
|
|
70
|
+
.safeParse(body);
|
|
71
|
+
return parsed.success ? parsed.data : undefined;
|
|
72
|
+
}
|
|
73
|
+
function parseUfiBody(body) {
|
|
74
|
+
const parsed = z
|
|
75
|
+
.string()
|
|
76
|
+
.transform((value, context) => {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(value);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!(error instanceof SyntaxError))
|
|
82
|
+
throw error;
|
|
83
|
+
context.addIssue({ code: 'custom', message: 'invalid JSON' });
|
|
84
|
+
return z.NEVER;
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
.pipe(ufiBodySchema)
|
|
88
|
+
.safeParse(body);
|
|
89
|
+
return parsed.success ? parsed.data : undefined;
|
|
90
|
+
}
|
|
91
|
+
export function parseHilinkSignal(input) {
|
|
92
|
+
const authStatus = parseHilinkXmlValue(input.status, 'code') === '125002';
|
|
93
|
+
const authSignal = parseHilinkXmlValue(input.signal, 'code') === '125002';
|
|
94
|
+
const statusReason = authStatus ? 'auth-expired' : 'not-reported';
|
|
95
|
+
const signalReason = authSignal ? 'auth-expired' : 'not-reported';
|
|
96
|
+
const metric = (tag) => authSignal ? unknown(signalReason) : numericMetric(parseHilinkXmlValue(input.signal, tag));
|
|
97
|
+
return {
|
|
98
|
+
provenance: 'hilink-admin-api',
|
|
99
|
+
freshness: authStatus && authSignal ? 'unknown' : 'live',
|
|
100
|
+
bars: authStatus
|
|
101
|
+
? unknown(statusReason)
|
|
102
|
+
: numericMetric(parseHilinkXmlValue(input.status, 'SignalIcon')),
|
|
103
|
+
max_bars: authStatus
|
|
104
|
+
? unknown(statusReason)
|
|
105
|
+
: numericMetric(parseHilinkXmlValue(input.status, 'maxsignal')),
|
|
106
|
+
dbm: metric('rssi'),
|
|
107
|
+
rsrp: metric('rsrp'),
|
|
108
|
+
rsrq: metric('rsrq'),
|
|
109
|
+
snr: unknown('unsupported'),
|
|
110
|
+
sinr: metric('sinr'),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function parseZteSignal(body) {
|
|
114
|
+
const record = parseFlatRecord(body);
|
|
115
|
+
const malformed = record === undefined;
|
|
116
|
+
const metric = (key) => malformed ? unknown('malformed') : numericMetric(record[key]);
|
|
117
|
+
const bars = metric('signalbar');
|
|
118
|
+
return {
|
|
119
|
+
provenance: 'zte-goform',
|
|
120
|
+
freshness: malformed ? 'unknown' : 'live',
|
|
121
|
+
bars,
|
|
122
|
+
max_bars: bars.state === 'known' ? known(5) : unknown('not-reported'),
|
|
123
|
+
dbm: metric('rssi'),
|
|
124
|
+
rsrp: metric('lte_rsrp'),
|
|
125
|
+
rsrq: metric('lte_rsrq'),
|
|
126
|
+
snr: metric('lte_snr'),
|
|
127
|
+
sinr: unknown('unsupported'),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function parseUfiSignal(input) {
|
|
131
|
+
const bodies = [input.sysinfo, input.overview, input.status].map(parseUfiBody);
|
|
132
|
+
const authExpired = bodies.every((body) => body?.reply === 'SessionOut');
|
|
133
|
+
const answered = bodies.some((body) => body?.reply === 'ok');
|
|
134
|
+
const value = bodies[0]?.params?.SIGNAL ?? bodies[1]?.params?.SIGNAL ?? bodies[2]?.params?.signalStrength;
|
|
135
|
+
const dbm = authExpired
|
|
136
|
+
? unknown('auth-expired')
|
|
137
|
+
: answered
|
|
138
|
+
? numericMetric(value)
|
|
139
|
+
: unknown('malformed');
|
|
140
|
+
return {
|
|
141
|
+
provenance: 'ufi-himiapi',
|
|
142
|
+
freshness: answered ? 'live' : 'unknown',
|
|
143
|
+
bars: unknown('unsupported'),
|
|
144
|
+
max_bars: unknown('unsupported'),
|
|
145
|
+
dbm,
|
|
146
|
+
rsrp: unknown('unsupported'),
|
|
147
|
+
rsrq: unknown('unsupported'),
|
|
148
|
+
snr: unknown('unsupported'),
|
|
149
|
+
sinr: unknown('unsupported'),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The vendor's own "unset" placeholders. A UFI answers `-` for a WAN address, an
|
|
154
|
+
* IMSI and an ICCID it does not have, and the ZTE builds answer `--` / `N/A` for
|
|
155
|
+
* an unpopulated counter — publishing any of those as a reading puts a value on
|
|
156
|
+
* screen that reads like a real one.
|
|
157
|
+
*/
|
|
158
|
+
const PLACEHOLDERS = new Set(['-', '--', 'n/a', 'N/A']);
|
|
159
|
+
function stated(value) {
|
|
160
|
+
if (value === undefined)
|
|
161
|
+
return undefined;
|
|
162
|
+
const normalized = String(value).trim();
|
|
163
|
+
return normalized === '' || PLACEHOLDERS.has(normalized) ? undefined : normalized;
|
|
164
|
+
}
|
|
165
|
+
function compact(entries) {
|
|
166
|
+
const result = {};
|
|
167
|
+
for (const [key, value] of entries)
|
|
168
|
+
if (value !== undefined)
|
|
169
|
+
result[key] = value;
|
|
170
|
+
return Object.keys(result).length === 0 ? undefined : result;
|
|
171
|
+
}
|
|
172
|
+
export function parseZteDetails(body) {
|
|
173
|
+
const value = parseFlatRecord(body);
|
|
174
|
+
if (value === undefined)
|
|
175
|
+
return undefined;
|
|
176
|
+
return compact([
|
|
177
|
+
['network_type', stated(value.network_type)],
|
|
178
|
+
[
|
|
179
|
+
'provider',
|
|
180
|
+
stated(value.network_provider_fullname) ??
|
|
181
|
+
stated(value.network_provider) ??
|
|
182
|
+
stated(value.provider),
|
|
183
|
+
],
|
|
184
|
+
['cell_id', stated(value.cell_id)],
|
|
185
|
+
['roaming', stated(value.simcard_roam)],
|
|
186
|
+
['mcc', stated(value.rmcc)],
|
|
187
|
+
['mnc', stated(value.rmnc)],
|
|
188
|
+
['pci', stated(value.lte_pci)],
|
|
189
|
+
// `band` and `network_band` are two DIFFERENT readings and must not be
|
|
190
|
+
// folded onto one key: `lte_band` is the serving cell's band, while
|
|
191
|
+
// `wan_active_band` is the band the WAN leg is active on, and the two
|
|
192
|
+
// disagree the moment carrier aggregation is up. Publishing either under
|
|
193
|
+
// the other's name reports a band the device never claimed for that leg.
|
|
194
|
+
['band', stated(value.lte_band) ?? stated(value.band)],
|
|
195
|
+
['network_band', stated(value.wan_active_band)],
|
|
196
|
+
['carrier_aggregation', stated(value.wan_lte_ca)],
|
|
197
|
+
['pcell_arfcn', stated(value.lte_ca_pcell_arfcn)],
|
|
198
|
+
['pcell_band', stated(value.lte_ca_pcell_band)],
|
|
199
|
+
['pcell_bandwidth', stated(value.lte_ca_pcell_bandwidth)],
|
|
200
|
+
['scell_arfcn', stated(value.lte_ca_scell_arfcn)],
|
|
201
|
+
['scell_band', stated(value.lte_ca_scell_band)],
|
|
202
|
+
['scell_bandwidth', stated(value.lte_ca_scell_bandwidth)],
|
|
203
|
+
['monthly_tx_bytes', stated(value.monthly_tx_bytes)],
|
|
204
|
+
['monthly_rx_bytes', stated(value.monthly_rx_bytes)],
|
|
205
|
+
['monthly_time', stated(value.monthly_time)],
|
|
206
|
+
['monthly_period', stated(value.date_month)],
|
|
207
|
+
// Named `session_*` rather than `realtime_*`: these are cumulative counters
|
|
208
|
+
// and a throughput, and the vendor's own prefix reads as "live rate" for
|
|
209
|
+
// all five.
|
|
210
|
+
['session_tx_bytes', stated(value.realtime_tx_bytes)],
|
|
211
|
+
['session_rx_bytes', stated(value.realtime_rx_bytes)],
|
|
212
|
+
['session_tx_rate', stated(value.realtime_tx_thrpt)],
|
|
213
|
+
['session_rx_rate', stated(value.realtime_rx_thrpt)],
|
|
214
|
+
['session_time', stated(value.realtime_time)],
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
export function parseUfiDetails(input) {
|
|
218
|
+
const overview = parseUfiBody(input.overview ?? '')?.params;
|
|
219
|
+
const sysinfo = parseUfiBody(input.sysinfo ?? '')?.params;
|
|
220
|
+
const product = parseUfiBody(input.produceInfo ?? '')?.params;
|
|
221
|
+
return compact([
|
|
222
|
+
['product', stated(product?.productname) ?? stated(product?.ProductName)],
|
|
223
|
+
['ssid', stated(overview?.SSID)],
|
|
224
|
+
['wan_ip', stated(overview?.WANIP)],
|
|
225
|
+
['imsi', stated(overview?.IMSI)],
|
|
226
|
+
['iccid', stated(overview?.ICCID)],
|
|
227
|
+
['web_version', stated(overview?.WEBVER)],
|
|
228
|
+
['cell_id', stated(sysinfo?.cellid)],
|
|
229
|
+
['station_id', stated(sysinfo?.bsid)],
|
|
230
|
+
['cpu_temp', stated(sysinfo?.cputemp)],
|
|
231
|
+
['wifi_clients', stated(sysinfo?.wifinum)],
|
|
232
|
+
['eth_clients', stated(sysinfo?.ethnum)],
|
|
233
|
+
]);
|
|
234
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const PACKAGE_NAME = "@ceralive/modem-control";
|
|
2
|
+
export * from './backend/index.js';
|
|
3
|
+
export * from './band/index.js';
|
|
4
|
+
export * from './capability/index.js';
|
|
5
|
+
export * from './domain/index.js';
|
|
6
|
+
export * from './fcc/index.js';
|
|
7
|
+
export * from './hardware/router-parsers.js';
|
|
8
|
+
export * from './journal/index.js';
|
|
9
|
+
export * from './location/index.js';
|
|
10
|
+
export * from './observations/index.js';
|
|
11
|
+
export * from './operation-ids.js';
|
|
12
|
+
export * from './operations/index.js';
|
|
13
|
+
export * from './ports/index.js';
|
|
14
|
+
export * from './providers/index.js';
|
|
15
|
+
export * from './radio/index.js';
|
|
16
|
+
export * from './redact.js';
|
|
17
|
+
export * from './safety/index.js';
|
|
18
|
+
export * from './sms/index.js';
|
|
19
|
+
export * from './usb-mode/index.js';
|
|
20
|
+
export * from './ussd/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @ceralive/modem-control — package entry point.
|
|
2
|
+
//
|
|
3
|
+
// Phase A: the domain model (identity + orthogonal state + revisions) under
|
|
4
|
+
// `./domain`, the MM / NM / Router port contracts + desired-state planner under
|
|
5
|
+
// `./ports`, the redaction module (`./redact`), and the epoch-scoped ModemManager
|
|
6
|
+
// D-Bus observer under `./backend`. The NetworkManager adapter, USB composition-mode
|
|
7
|
+
// model, and data-usage sampler land in later waves.
|
|
8
|
+
export const PACKAGE_NAME = '@ceralive/modem-control';
|
|
9
|
+
export * from './backend/index.js';
|
|
10
|
+
export * from './band/index.js';
|
|
11
|
+
export * from './capability/index.js';
|
|
12
|
+
export * from './domain/index.js';
|
|
13
|
+
export * from './fcc/index.js';
|
|
14
|
+
export * from './hardware/router-parsers.js';
|
|
15
|
+
export * from './journal/index.js';
|
|
16
|
+
export * from './location/index.js';
|
|
17
|
+
export * from './observations/index.js';
|
|
18
|
+
export * from './operation-ids.js';
|
|
19
|
+
export * from './operations/index.js';
|
|
20
|
+
export * from './ports/index.js';
|
|
21
|
+
export * from './providers/index.js';
|
|
22
|
+
export * from './radio/index.js';
|
|
23
|
+
export * from './redact.js';
|
|
24
|
+
export * from './safety/index.js';
|
|
25
|
+
export * from './sms/index.js';
|
|
26
|
+
export * from './usb-mode/index.js';
|
|
27
|
+
export * from './ussd/index.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type JournalEntry } from './entry.js';
|
|
2
|
+
/** Why one record could not be read. `field` names the offending key, never its value. */
|
|
3
|
+
export interface JournalDecodeFailure {
|
|
4
|
+
readonly code: 'empty' | 'invalid-json' | 'not-an-object' | 'unsupported-schema-version' | 'schema-mismatch' | 'unreadable';
|
|
5
|
+
readonly field?: string;
|
|
6
|
+
/** Byte offset for a JSON syntax error, when the runtime reported one. */
|
|
7
|
+
readonly offset?: number;
|
|
8
|
+
}
|
|
9
|
+
export type JournalDecodeResult<T> = {
|
|
10
|
+
readonly ok: true;
|
|
11
|
+
readonly value: T;
|
|
12
|
+
} | {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
readonly failure: JournalDecodeFailure;
|
|
15
|
+
};
|
|
16
|
+
declare function record(raw: unknown, field: string): Record<string, unknown>;
|
|
17
|
+
declare function requiredString(source: Record<string, unknown>, field: string): string;
|
|
18
|
+
declare function optionalString(source: Record<string, unknown>, field: string): string | undefined;
|
|
19
|
+
declare function requiredNonNegativeInteger(source: Record<string, unknown>, field: string): number;
|
|
20
|
+
declare function member<T extends string>(source: Record<string, unknown>, field: string, allowed: readonly T[]): T;
|
|
21
|
+
/**
|
|
22
|
+
* Serialize one entry to a single line WITHOUT its terminator.
|
|
23
|
+
*
|
|
24
|
+
* Keys are emitted in a fixed order because the round-trip tests compare bytes,
|
|
25
|
+
* and byte comparison is the only assertion that catches a field a reader silently
|
|
26
|
+
* dropped (`JSON.parse` + a permissive validator will happily lose one and still
|
|
27
|
+
* report success — the same trap the srtla telemetry byte-parity suite exists for).
|
|
28
|
+
*/
|
|
29
|
+
export declare function encodeJournalEntry(entry: JournalEntry): string;
|
|
30
|
+
/** Decode one line. An empty/whitespace-only line is reported, never silently dropped. */
|
|
31
|
+
export declare function decodeJournalEntry(line: string): JournalDecodeResult<JournalEntry>;
|
|
32
|
+
/** Decode an arbitrary JSON document with a caller-supplied validator. */
|
|
33
|
+
export declare function decodeJournalDocument<T>(text: string, validate: (raw: unknown) => T): JournalDecodeResult<T>;
|
|
34
|
+
/** The validator helpers the legacy reader reuses, so both shapes fail the same way. */
|
|
35
|
+
export declare const journalSchema: {
|
|
36
|
+
readonly record: typeof record;
|
|
37
|
+
readonly requiredString: typeof requiredString;
|
|
38
|
+
readonly optionalString: typeof optionalString;
|
|
39
|
+
readonly requiredNonNegativeInteger: typeof requiredNonNegativeInteger;
|
|
40
|
+
readonly member: typeof member;
|
|
41
|
+
readonly schemaError: (field: string) => Error;
|
|
42
|
+
readonly schemaVersionError: () => Error;
|
|
43
|
+
};
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Line codec for the journal: exactly one entry per line, JSON, no wrapper.
|
|
2
|
+
//
|
|
3
|
+
// WHY DECODING RETURNS A RESULT INSTEAD OF THROWING. A journal is read at exactly
|
|
4
|
+
// one moment — recovery after an unclean restart — and that is the moment a throw
|
|
5
|
+
// is most expensive: it aborts the read at the first damaged byte and takes every
|
|
6
|
+
// still-valid entry after it with it. The whole point of this module is that a
|
|
7
|
+
// damaged record is DATA, reported alongside the records that survived, so nothing
|
|
8
|
+
// earlier or later is discarded to make one bad line disappear.
|
|
9
|
+
//
|
|
10
|
+
// FAILURES NAME A FIELD, NEVER CONTENT. Same rule the two policy stores follow: a
|
|
11
|
+
// classification carries the offending field name and a byte count, never the bytes
|
|
12
|
+
// themselves, because a journal line can hold provider identifiers and refusal
|
|
13
|
+
// reasons and a log line is the wrong place to reproduce them.
|
|
14
|
+
import { MODEM_CONTROL_JOURNAL_SCHEMA_VERSION, } from './entry.js';
|
|
15
|
+
class SchemaError extends Error {
|
|
16
|
+
field;
|
|
17
|
+
constructor(field) {
|
|
18
|
+
super(`schema-mismatch: ${field}`);
|
|
19
|
+
this.field = field;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class SchemaVersionError extends Error {
|
|
23
|
+
constructor() {
|
|
24
|
+
super('unsupported-schema-version');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function record(raw, field) {
|
|
28
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
29
|
+
throw new SchemaError(field);
|
|
30
|
+
return raw;
|
|
31
|
+
}
|
|
32
|
+
function requiredString(source, field) {
|
|
33
|
+
const value = source[field];
|
|
34
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
35
|
+
throw new SchemaError(field);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function optionalString(source, field) {
|
|
39
|
+
const value = source[field];
|
|
40
|
+
if (value === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
if (typeof value !== 'string')
|
|
43
|
+
throw new SchemaError(field);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function requiredNonNegativeInteger(source, field) {
|
|
47
|
+
const value = source[field];
|
|
48
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
49
|
+
throw new SchemaError(field);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function requiredStringArray(source, field) {
|
|
54
|
+
const value = source[field];
|
|
55
|
+
if (!Array.isArray(value) || value.some((member) => typeof member !== 'string')) {
|
|
56
|
+
throw new SchemaError(field);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function member(source, field, allowed) {
|
|
61
|
+
const value = source[field];
|
|
62
|
+
if (typeof value !== 'string' || !allowed.includes(value)) {
|
|
63
|
+
throw new SchemaError(field);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
const AUTHORITIES = ['provider', 'controller', 'hardware'];
|
|
68
|
+
const IMPACTS = ['read', 'write', 'session', 'disruptive', 'recovery'];
|
|
69
|
+
const CONFIDENCES = ['high', 'medium', 'low', 'unknown'];
|
|
70
|
+
const UNKNOWN_OUTCOME_REASONS = [
|
|
71
|
+
'stale-generation',
|
|
72
|
+
'write-reply-timed-out',
|
|
73
|
+
'write-reply-dropped',
|
|
74
|
+
];
|
|
75
|
+
function parseDescriptor(raw) {
|
|
76
|
+
const source = record(raw, 'descriptor');
|
|
77
|
+
return {
|
|
78
|
+
descriptorId: requiredString(source, 'descriptorId'),
|
|
79
|
+
provider: requiredString(source, 'provider'),
|
|
80
|
+
authority: member(source, 'authority', AUTHORITIES),
|
|
81
|
+
mutationImpact: member(source, 'mutationImpact', IMPACTS),
|
|
82
|
+
profiles: requiredStringArray(source, 'profiles'),
|
|
83
|
+
firmware: requiredStringArray(source, 'firmware'),
|
|
84
|
+
confidence: member(source, 'confidence', CONFIDENCES),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function parseOutcome(raw) {
|
|
88
|
+
const source = record(raw, 'outcome');
|
|
89
|
+
switch (member(source, 'status', ['applied', 'refused', 'failed', 'unknown-outcome'])) {
|
|
90
|
+
case 'applied':
|
|
91
|
+
return { status: 'applied' };
|
|
92
|
+
case 'refused':
|
|
93
|
+
return { status: 'refused', reason: requiredString(source, 'reason') };
|
|
94
|
+
case 'failed':
|
|
95
|
+
return { status: 'failed', reason: requiredString(source, 'reason') };
|
|
96
|
+
case 'unknown-outcome':
|
|
97
|
+
return {
|
|
98
|
+
status: 'unknown-outcome',
|
|
99
|
+
reason: member(source, 'reason', UNKNOWN_OUTCOME_REASONS),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function parseEntry(raw) {
|
|
104
|
+
const source = record(raw, 'entry');
|
|
105
|
+
if (source.schemaVersion !== MODEM_CONTROL_JOURNAL_SCHEMA_VERSION)
|
|
106
|
+
throw new SchemaVersionError();
|
|
107
|
+
const base = {
|
|
108
|
+
schemaVersion: MODEM_CONTROL_JOURNAL_SCHEMA_VERSION,
|
|
109
|
+
operationId: requiredString(source, 'operationId'),
|
|
110
|
+
physicalModemId: requiredString(source, 'physicalModemId'),
|
|
111
|
+
generation: requiredNonNegativeInteger(source, 'generation'),
|
|
112
|
+
recordedAtMs: requiredNonNegativeInteger(source, 'recordedAtMs'),
|
|
113
|
+
descriptor: parseDescriptor(source.descriptor),
|
|
114
|
+
};
|
|
115
|
+
if (member(source, 'phase', ['started', 'completed']) === 'started') {
|
|
116
|
+
return { ...base, phase: 'started' };
|
|
117
|
+
}
|
|
118
|
+
return { ...base, phase: 'completed', outcome: parseOutcome(source.outcome) };
|
|
119
|
+
}
|
|
120
|
+
/** Classify a decode failure into a metadata-only result (never raw content). */
|
|
121
|
+
function classify(error) {
|
|
122
|
+
if (error instanceof SchemaVersionError)
|
|
123
|
+
return { code: 'unsupported-schema-version' };
|
|
124
|
+
if (error instanceof SchemaError)
|
|
125
|
+
return { code: 'schema-mismatch', field: error.field };
|
|
126
|
+
if (error instanceof SyntaxError) {
|
|
127
|
+
const offset = /position (\d+)/.exec(error.message)?.[1];
|
|
128
|
+
return offset === undefined
|
|
129
|
+
? { code: 'invalid-json' }
|
|
130
|
+
: { code: 'invalid-json', offset: Number(offset) };
|
|
131
|
+
}
|
|
132
|
+
return { code: 'unreadable' };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Serialize one entry to a single line WITHOUT its terminator.
|
|
136
|
+
*
|
|
137
|
+
* Keys are emitted in a fixed order because the round-trip tests compare bytes,
|
|
138
|
+
* and byte comparison is the only assertion that catches a field a reader silently
|
|
139
|
+
* dropped (`JSON.parse` + a permissive validator will happily lose one and still
|
|
140
|
+
* report success — the same trap the srtla telemetry byte-parity suite exists for).
|
|
141
|
+
*/
|
|
142
|
+
export function encodeJournalEntry(entry) {
|
|
143
|
+
const descriptor = {
|
|
144
|
+
descriptorId: entry.descriptor.descriptorId,
|
|
145
|
+
provider: entry.descriptor.provider,
|
|
146
|
+
authority: entry.descriptor.authority,
|
|
147
|
+
mutationImpact: entry.descriptor.mutationImpact,
|
|
148
|
+
profiles: entry.descriptor.profiles,
|
|
149
|
+
firmware: entry.descriptor.firmware,
|
|
150
|
+
confidence: entry.descriptor.confidence,
|
|
151
|
+
};
|
|
152
|
+
const base = {
|
|
153
|
+
schemaVersion: entry.schemaVersion,
|
|
154
|
+
phase: entry.phase,
|
|
155
|
+
operationId: entry.operationId,
|
|
156
|
+
physicalModemId: entry.physicalModemId,
|
|
157
|
+
generation: entry.generation,
|
|
158
|
+
recordedAtMs: entry.recordedAtMs,
|
|
159
|
+
descriptor,
|
|
160
|
+
};
|
|
161
|
+
return JSON.stringify(entry.phase === 'started' ? base : { ...base, outcome: entry.outcome });
|
|
162
|
+
}
|
|
163
|
+
/** Decode one line. An empty/whitespace-only line is reported, never silently dropped. */
|
|
164
|
+
export function decodeJournalEntry(line) {
|
|
165
|
+
if (line.trim().length === 0)
|
|
166
|
+
return { ok: false, failure: { code: 'empty' } };
|
|
167
|
+
try {
|
|
168
|
+
return { ok: true, value: parseEntry(JSON.parse(line)) };
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return { ok: false, failure: classify(error) };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Decode an arbitrary JSON document with a caller-supplied validator. */
|
|
175
|
+
export function decodeJournalDocument(text, validate) {
|
|
176
|
+
if (text.trim().length === 0)
|
|
177
|
+
return { ok: false, failure: { code: 'empty' } };
|
|
178
|
+
try {
|
|
179
|
+
return { ok: true, value: validate(JSON.parse(text)) };
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return { ok: false, failure: classify(error) };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** The validator helpers the legacy reader reuses, so both shapes fail the same way. */
|
|
186
|
+
export const journalSchema = {
|
|
187
|
+
record,
|
|
188
|
+
requiredString,
|
|
189
|
+
optionalString,
|
|
190
|
+
requiredNonNegativeInteger,
|
|
191
|
+
member,
|
|
192
|
+
schemaError(field) {
|
|
193
|
+
return new SchemaError(field);
|
|
194
|
+
},
|
|
195
|
+
schemaVersionError() {
|
|
196
|
+
return new SchemaVersionError();
|
|
197
|
+
},
|
|
198
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { OperationJournalEvent, OperationJournalHook } from '../operations/contracts.js';
|
|
2
|
+
import { type JournalRecovery } from './recovery.js';
|
|
3
|
+
import type { JournalStore } from './store.js';
|
|
4
|
+
export interface JournalEngineOptions {
|
|
5
|
+
/** The store — and therefore the path — is supplied by the composition root. */
|
|
6
|
+
readonly store: JournalStore;
|
|
7
|
+
readonly now?: () => number;
|
|
8
|
+
}
|
|
9
|
+
export declare class JournalEngine {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(options: JournalEngineOptions);
|
|
12
|
+
/** Where this engine journals to; useful in a recovery log line. */
|
|
13
|
+
get path(): string;
|
|
14
|
+
/**
|
|
15
|
+
* The `OperationJournalHook` implementation.
|
|
16
|
+
*
|
|
17
|
+
* Generic per CALL rather than per instance, so one engine journals every
|
|
18
|
+
* descriptor in the process. A generic method satisfies the non-generic
|
|
19
|
+
* `OperationJournalHook<I, O>` member by instantiation, which is what lets an
|
|
20
|
+
* `OperationExecution` take the engine itself as its `journal`.
|
|
21
|
+
*/
|
|
22
|
+
record<I, O>(event: OperationJournalEvent<I, O>): Promise<void>;
|
|
23
|
+
/** An explicitly typed hook, for a caller that prefers a narrow object. */
|
|
24
|
+
hook<I, O>(): OperationJournalHook<I, O>;
|
|
25
|
+
/** Read the journal back and reconstruct what was in flight. */
|
|
26
|
+
recover(): Promise<JournalRecovery>;
|
|
27
|
+
}
|
|
28
|
+
export declare function createJournalEngine(options: JournalEngineOptions): JournalEngine;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The journal engine: the operation engine's journal hook, plus replay.
|
|
2
|
+
//
|
|
3
|
+
// `OperationExecution.journal` (todo 20's `OperationJournalHook`) is the seam the
|
|
4
|
+
// operation engine already calls at `started` and at `completed`, and a descriptor
|
|
5
|
+
// that declares `journal: { required: true }` is REFUSED outright when no hook is
|
|
6
|
+
// supplied. This class is the durable implementation of that seam: it flattens the
|
|
7
|
+
// event onto a journal entry and appends it, and it reads the same file back.
|
|
8
|
+
//
|
|
9
|
+
// THE ENGINE HOLDS NO PATH. It holds a `JournalStore`, and the store was handed a
|
|
10
|
+
// path by whoever composed it. That is the same injection shape todo 19 used for
|
|
11
|
+
// the ownership lock (`FlockResourceOwnershipOptions.lockPath` is REQUIRED and the
|
|
12
|
+
// adapter substitutes nothing) and the same reason: an embedding process owns its
|
|
13
|
+
// filesystem layout, and a library that guesses one is a library that writes to the
|
|
14
|
+
// wrong disk on a device it has never seen.
|
|
15
|
+
//
|
|
16
|
+
// THE CLOCK IS INJECTED TOO. `now` defaults to `Date.now`, but a test that wants
|
|
17
|
+
// deterministic timestamps supplies its own — the observation layer's rule that
|
|
18
|
+
// this package never stamps data with a time it did not come from applies here as
|
|
19
|
+
// well, and a replay assertion over timestamps must not be a race.
|
|
20
|
+
import { journalDescriptorEvidence, journalOutcome } from './entry.js';
|
|
21
|
+
import { reconstructJournalRecovery } from './recovery.js';
|
|
22
|
+
export class JournalEngine {
|
|
23
|
+
#store;
|
|
24
|
+
#now;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.#store = options.store;
|
|
27
|
+
this.#now = options.now ?? (() => Date.now());
|
|
28
|
+
}
|
|
29
|
+
/** Where this engine journals to; useful in a recovery log line. */
|
|
30
|
+
get path() {
|
|
31
|
+
return this.#store.path;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The `OperationJournalHook` implementation.
|
|
35
|
+
*
|
|
36
|
+
* Generic per CALL rather than per instance, so one engine journals every
|
|
37
|
+
* descriptor in the process. A generic method satisfies the non-generic
|
|
38
|
+
* `OperationJournalHook<I, O>` member by instantiation, which is what lets an
|
|
39
|
+
* `OperationExecution` take the engine itself as its `journal`.
|
|
40
|
+
*/
|
|
41
|
+
record(event) {
|
|
42
|
+
return this.#store.append(entryFor(event, this.#now()));
|
|
43
|
+
}
|
|
44
|
+
/** An explicitly typed hook, for a caller that prefers a narrow object. */
|
|
45
|
+
hook() {
|
|
46
|
+
return { record: (event) => this.record(event) };
|
|
47
|
+
}
|
|
48
|
+
/** Read the journal back and reconstruct what was in flight. */
|
|
49
|
+
async recover() {
|
|
50
|
+
return reconstructJournalRecovery(await this.#store.read());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function entryFor(event, recordedAtMs) {
|
|
54
|
+
const base = {
|
|
55
|
+
schemaVersion: 1,
|
|
56
|
+
operationId: event.operationId,
|
|
57
|
+
physicalModemId: event.physicalModemId,
|
|
58
|
+
generation: event.generation,
|
|
59
|
+
recordedAtMs,
|
|
60
|
+
descriptor: journalDescriptorEvidence(event.descriptor),
|
|
61
|
+
};
|
|
62
|
+
return event.phase === 'started'
|
|
63
|
+
? { ...base, phase: 'started' }
|
|
64
|
+
: { ...base, phase: 'completed', outcome: journalOutcome(event.result) };
|
|
65
|
+
}
|
|
66
|
+
export function createJournalEngine(options) {
|
|
67
|
+
return new JournalEngine(options);
|
|
68
|
+
}
|