You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
meshcore-client/lib/storage/scoping_inventory.dart

366 lines
15 KiB

/// The declared scoping verdict for every persistence surface (#853, #936).
///
/// Feature #853's acceptance bar is that "every store has a verified scoping
/// verdict (per-radio or deliberately-global) and no hole remains". Until this
/// file existed, the only way to check that was for a human to re-read an audit
/// in an issue body, which is how #754 and #856 survived three earlier bleed
/// fixes. This file is that audit, in code, with a test that fails when a new
/// candidate key appears undeclared.
///
/// **Adding a storage key means adding a row here.** The test in
/// `test/storage/scoping_inventory_test.dart` scans `lib/` for the shape every
/// key in this repo uses, a `const String` whose value is lower snake_case, and
/// fails if one is not classified below. A key that is not a storage key goes
/// in [nonStorageConstants] with a reason, so "it isn't a key" is a written
/// claim rather than a silence.
library;
enum ScopingVerdict {
/// Scoped by the first 10 hex of the connected radio's public key, the
/// scheme every per-radio store in this repo uses. Radio A's data is never
/// visible while radio B is connected.
perRadio,
/// Global, and that is correct. The reason is mandatory: without it a later
/// audit re-opens the surface as a hole, which is exactly the churn #853
/// exists to end.
globalOnPurpose,
/// Global, and that is a defect. Each of these has an open issue. The test
/// asserts the set matches the known holes, so a NEW hole cannot hide among
/// them and a FIXED one cannot be left mislabelled.
knownHole,
}
class StorageSurface {
const StorageSurface(
this.key, {
required this.verdict,
required this.reason,
required this.owner,
this.issue,
});
/// The key or key prefix exactly as the source declares it.
final String key;
final ScopingVerdict verdict;
/// Why this verdict. For [ScopingVerdict.perRadio], where the scoping comes
/// from. For [ScopingVerdict.globalOnPurpose], why radio scoping would be
/// wrong. For [ScopingVerdict.knownHole], what bleeds.
final String reason;
/// The file that owns the key, so a reader can go check.
final String owner;
/// Required for [ScopingVerdict.knownHole], so every hole is tracked.
final int? issue;
}
/// Every persistence surface in the client, with its verdict.
const List<StorageSurface> storageSurfaces = [
// ---------------------------------------------------------------------------
// Per-radio. Each is fed the device-key prefix on connect.
// ---------------------------------------------------------------------------
StorageSurface(
'channel_messages_',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key, then PSK identity within the radio (#194), '
'so history survives a channel rename and does not follow a reused '
'slot. The cross-radio bleed was #472',
owner: 'lib/storage/channel_message_store.dart',
),
StorageSurface(
'messages_',
verdict: ScopingVerdict.perRadio,
reason: 'prefix + device key + contact key',
owner: 'lib/storage/message_store.dart',
),
StorageSurface(
'channels',
verdict: ScopingVerdict.perRadio,
reason: 'prefix + device key',
owner: 'lib/storage/channel_store.dart',
),
StorageSurface(
'channel_order_',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key; display order is per radio because the '
'channel list is',
owner: 'lib/storage/channel_order_store.dart',
),
StorageSurface(
'channel_region_scope_',
verdict: ScopingVerdict.perRadio,
reason: 'prefix + device key',
owner: 'lib/storage/channel_region_scope_store.dart',
),
StorageSurface(
'contacts',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key; a contact list belongs to the radio that '
'holds it',
owner: 'lib/storage/contact_store.dart',
),
StorageSurface(
'contact_groups',
verdict: ScopingVerdict.perRadio,
reason: 'prefix + device key; groups reference that radio\'s contacts',
owner: 'lib/storage/contact_group_store.dart',
),
StorageSurface(
'contact_unread_count',
verdict: ScopingVerdict.perRadio,
reason: 'prefix + device key; unread counts follow the message history',
owner: 'lib/storage/unread_store.dart',
),
StorageSurface(
'discovered_contacts',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key since #754. A discovered node is what THIS '
'radio heard. An unset key loads nothing and never writes, so it '
'cannot touch the orphaned legacy blob',
owner: 'lib/storage/contact_discovery_store.dart',
),
StorageSurface(
'communities_v1',
verdict: ScopingVerdict.perRadio,
reason: 'prefix + device key',
owner: 'lib/storage/community_store.dart',
),
StorageSurface(
'channel_cyr2lat_',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key + channel index, via keyForCyr2Lat. NOT '
'previously audited: #853\'s audit listed channel_settings_store as '
'one surface, but it owns two keys and this inventory found the '
'second',
owner: 'lib/storage/channel_settings_store.dart',
),
StorageSurface(
'contact_cyr2lat_',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key + contact key. NOT previously audited, same '
'reason as channel_cyr2lat_',
owner: 'lib/storage/contact_settings_store.dart',
),
StorageSurface(
'contact_customname_',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key + contact key, via keyForCustomName. The user\'s '
'own label for a contact (#750). Per-radio for the same reason as its '
'neighbour in this store. Deliberately NOT in contact_store: that '
'record is rebuilt from radio contact-sync data, where no custom name '
'exists, so a save after a sync would wipe the rename',
owner: 'lib/storage/contact_settings_store.dart',
),
StorageSurface(
'block_keys_v1',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key. A blank radio is no longer offloaded the '
'client list (#471)',
owner: 'lib/storage/block_store.dart',
),
StorageSurface(
'block_names_v1',
verdict: ScopingVerdict.perRadio,
reason: 'prefix + device key, alongside block_keys_v1',
owner: 'lib/storage/block_store.dart',
),
// ---------------------------------------------------------------------------
// Known holes. Each has an issue and each is fixed in Epic B (#937).
// ---------------------------------------------------------------------------
StorageSurface(
'path_history_',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key + contact key, in DRIFT, since #854 and #924. '
'Was keyed by contact alone in prefs, so a route measured by a 1 W '
'handheld was applied to a repeater. The move out of prefs went with '
'the scoping because that one store held 2,092 keys and 86% of a '
'4.28 MB file, and every prefs write re-encodes the whole file '
'(#923). Unset key loads nothing and never writes',
owner: 'lib/services/storage_service.dart',
),
StorageSurface(
'delivery_observations',
verdict: ScopingVerdict.perRadio,
reason:
'prefix + device key since #856. Was one global list, so a '
'repeater\'s 45-second deliveries trained the ACK-timeout model a '
'1 W handheld then used. Stays in prefs unlike path history: one '
'small key, 3,091 bytes and 14 entries on the owner\'s install, so '
'there is no cost argument for moving it',
owner: 'lib/services/storage_service.dart',
),
// `pending_messages` (#855) was listed here and is GONE, not reclassified.
// It had no callers anywhere in lib/ or test/ and the key was absent from
// the owner's live prefs, so the fix was deletion rather than scoping. The
// inventory's "declared but vanished from source" check is what keeps that
// honest in the other direction: if the key ever comes back, the gate fails
// until somebody declares it and says why.
// ---------------------------------------------------------------------------
// Global on purpose. The reason is the point: it stops a later audit
// re-opening these as holes.
// ---------------------------------------------------------------------------
StorageSurface(
'client_install_id',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'the identity of this app INSTALL, not of any radio. Scoping it '
'per radio would mint a new install id per radio, which is the '
'opposite of what it is for',
owner: 'lib/storage/client_id_store.dart',
),
StorageSurface(
'app_settings',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'app-wide UX: theme, language, units, map and notification '
'preferences. The per-radio data it does carry is scoped INSIDE it, by '
'channelNotifyModesByRadio: device key -> channel PSK -> mode (#839). '
'So the blob is global and the radio-specific part within it is not',
owner: 'lib/models/app_settings.dart',
),
StorageSurface(
'repeater_passwords',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'keyed by the repeater NODE, not by the radio connecting to it. '
'The same repeater has the same password whichever radio reaches it, '
'so per-radio scoping would make the user re-enter it per radio',
owner: 'lib/services/storage_service.dart',
),
StorageSurface(
'repeater_auto_clock_sync_after_login',
verdict: ScopingVerdict.globalOnPurpose,
reason: 'keyed by repeater node, same reasoning as repeater_passwords',
owner: 'lib/services/storage_service.dart',
),
StorageSurface(
'map_removed_marker_ids',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'a set of node ids the user dismissed from the map. "Do not show '
'me this node" is a statement about the NODE, like repeater_passwords, '
'not about the radio that happened to hear it. NOT previously audited: '
'surfaced by this inventory, see #936',
owner: 'lib/services/map_marker_service.dart',
),
StorageSurface(
'map_tile_cache',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'cached OSM tiles. Geography does not depend on which radio is '
'connected, and per-radio caching would multiply the download',
owner: 'lib/services/map_tile_cache_service.dart',
),
StorageSurface(
'radio_presets_v1_',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'prefix + preset FILE name. A cached catalogue of region presets '
'fetched from assets or upstream, not state belonging to a radio. NOT '
'previously audited: surfaced by this inventory',
owner: 'lib/services/radio_preset_service.dart',
),
StorageSurface(
'anr_last_exit_ts_ms',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'watermark for which ApplicationExitInfo records have been '
'processed (#906). A property of the app process, not of a radio. NOT '
'previously audited: surfaced by this inventory',
owner: 'lib/services/app_exit_info_service.dart',
),
StorageSurface(
'__msgrows_migrated',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'the set of store keys whose blob has been moved into message rows '
'(#920). Global because the keys it lists are themselves already '
'radio-scoped, so scoping the marker would scope it twice',
owner: 'lib/storage/drift/blob_store.dart',
),
StorageSurface(
'__msgrows_migrated_at',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'when each migrated store key migrated, for the stale-blob sweep '
'(#962). Global for the same reason as __msgrows_migrated: it is keyed '
'BY those store keys, which already carry the radio, so scoping it '
'would scope it twice. Kept separate from that marker because the '
'marker is read on every save and load through a cache, and reshaping '
'it into a map would put a migration on the hottest storage path',
owner: 'lib/storage/drift/blob_store.dart',
),
StorageSurface(
'__msgrows_reclaim_audit',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'a durable record of what each stale-blob sweep deleted (#962). Global '
'because one sweep spans every radio and the entries name store keys, '
'which already carry the radio. It exists because the app log is gated '
'on a setting that is off by default, so an irreversible delete would '
'otherwise leave no trail at all; capped at the last few sweeps',
owner: 'lib/storage/drift/blob_store.dart',
),
];
/// Constants that match the key shape but are not persistence keys.
///
/// This list exists so "that is not a storage key" has to be written down and
/// justified, instead of a new key slipping through because nobody looked.
const Map<String, String> nonStorageConstants = {
'offband_store': 'the drift database FILE name, not a key within it',
'__all__':
'the sentinel value for "all groups" in the contacts filter '
'(UiViewStateService). A filter VALUE, not a key, and not persisted '
'under this name',
'__reaction_send__':
'a prefix for an in-memory send-queue id in the '
'connector, distinguishing a reaction send from a message send. Never '
'reaches storage',
'psk_':
'a marker inside a channel-history key, distinguishing a PSK '
'identity from a legacy slot index (#194). Not a key by itself',
'anr':
'a log tag / exit-reason discriminator in the ANR capture path, not '
'persisted under this name',
'los_error_select_start_end':
'an in-memory error sentinel in the line-of-sight screen, assigned to '
'_error and compared against. Never written to storage',
'los_error_invalid_input': 'the same, for invalid input',
'los_error_elevation_unavailable':
'the same, raised by the line-of-sight service when elevation data is '
'missing. Never written to storage',
// MQTT broker config field names. These are keys within a config map that is
// itself persisted under another surface, not top-level storage keys.
'auth_type': 'MQTT broker config field name',
'ca_cert': 'MQTT broker config field name',
'enabled': 'MQTT broker config field name',
'password': 'MQTT broker config field name',
'port': 'MQTT broker config field name',
'topic_prefix': 'MQTT broker config field name',
'transport': 'MQTT broker config field name',
'url': 'MQTT broker config field name',
'username': 'MQTT broker config field name',
'jwt_aud': 'JWT claim name in a broker auth payload',
'jwt_email': 'JWT claim name',
'jwt_owner': 'JWT claim name',
'jwt_refresh': 'JWT claim name',
'jwt_token': 'JWT claim name',
'iata_override': 'a field name within a config payload, not a storage key',
};

Powered by TurnKey Linux.