diff --git a/meshtastic/admin.options b/meshtastic/admin.options index 6574db58..a4191595 100644 --- a/meshtastic/admin.options +++ b/meshtastic/admin.options @@ -19,3 +19,11 @@ *HamParameters.call_sign max_size:8 *HamParameters.short_name max_size:5 *NodeRemoteHardwarePinsResponse.node_remote_hardware_pins max_count:16 + +# Bounded client-app metadata store. app_id is ASCII [a-z0-9._-]{1,32}, so +# 33 covers the 32-byte max plus the nanopb null terminator. Payload cap is +# 512 bytes per the v1 RFC. TODO(maintainer): confirm sizes during review. +*ClientAppData.app_id max_size:33 +*ClientAppData.payload max_size:512 +*AdminMessage.get_client_app_data_request max_size:33 +*AdminMessage.delete_client_app_data_request max_size:33 diff --git a/meshtastic/admin.proto b/meshtastic/admin.proto index ac29cc8a..d2545657 100644 --- a/meshtastic/admin.proto +++ b/meshtastic/admin.proto @@ -521,6 +521,39 @@ message AdminMessage { * Parameters and sensor configuration */ SensorConfig sensor_config = 103; + + /* + * Write a bounded local client-app metadata record. Local-only by + * default: the firmware refuses set_client_app_data from non-local + * senders. See top-level ClientAppData for the namespaced-not-owned + * caveat: any admin-capable client may overwrite any app_id, so + * callers must treat stored payloads as untrusted and recoverable. + * Validation failures (bad app_id, oversize payload, no slot free) + * are reported via meshtastic_Routing_Error_BAD_REQUEST. + * TODO(maintainer): confirm field-number allocation for 104..107. + */ + ClientAppData set_client_app_data = 104; + + /* + * Read a stored client-app metadata record by app_id. The firmware + * replies with get_client_app_data_response: a populated record on + * hit, or a record with empty app_id to signal NOT_FOUND. + */ + string get_client_app_data_request = 105; + + /* + * Stored client-app metadata in response to get_client_app_data_request. + * If app_id is empty, no record exists for the requested key. + */ + ClientAppData get_client_app_data_response = 106; + + /* + * Delete a stored client-app metadata record by app_id. Local-only by + * default. Returns Routing_Error_NONE on success (record removed) or + * Routing_Error_BAD_REQUEST on invalid app_id. Deleting a missing + * app_id is treated as success (idempotent). + */ + string delete_client_app_data_request = 107; } } @@ -760,3 +793,64 @@ message SHTXX_config { */ optional uint32 set_accuracy = 1; } + +/* + * Optional, bounded, local-node-only storage for opaque, app-owned metadata + * that a companion application can ask its locally-connected node to persist + * on its behalf. Not a filesystem, not a database, and not arbitrary NVRAM. + * It is a tiny fixed-slot table for a few small records. + * + * Intended to give clients an explicit, firmware-bounded place to store + * non-secret convenience metadata, without overloading user-visible + * fields like long_name or unrelated configuration surfaces. + * + * IMPORTANT: namespaced, not owned. The firmware enforces shape, payload + * size, and record-count limits, but does NOT authenticate which companion + * application is making a write request. app_id prevents accidental name + * collisions between apps, NOT malicious or intentional overwrites: any + * admin-capable client may overwrite or delete any app_id. Callers must + * therefore treat stored payloads as untrusted, optional, and recoverable. + * + * Do NOT store secrets, identity keys, session keys, paid-entitlement + * state, trust authority, blocklists, or any data used to make security, + * routing, authentication, or purchase decisions. Treat this as a + * convenience cache for non-critical app state only. + * + * Firmware never interprets `payload`, never broadcasts these records over + * LoRa, never includes them in NodeInfo, and never relays them via MQTT. + * Records are local-node-only, survive reboot, and are cleared by factory + * reset. + * + * Client guidance: gracefully fall back to app-local storage when the + * firmware does not support this feature; version your payloads via the + * `version` field below; keep payloads small (well under the 512-byte cap) + * to leave headroom for other apps. + */ +message ClientAppData { + /* + * Namespacing key. Must match `^[a-z0-9._-]{1,32}$`. + * Convention examples: "meshtastic-ios", + * "meshtastic-android", "thirdparty.example". + */ + string app_id = 1; + + /* + * Application-defined schema version for `payload`. The firmware does not + * interpret this; clients use it to migrate or reject stale payloads. + */ + uint32 version = 2; + + /* + * Opaque app-owned bytes, max 512. Firmware never inspects or interprets + * the contents. + */ + bytes payload = 3; + + /* + * Unix epoch seconds, set by the firmware on every successful write. + * May be 0 if the firmware does not yet have a valid wall-clock time. + * Useful for clients to detect that another admin-capable client has + * overwritten or deleted-then-recreated the record since the last read. + */ + fixed32 updated_at = 4; +} diff --git a/meshtastic/localonly.options b/meshtastic/localonly.options new file mode 100644 index 00000000..60678bbb --- /dev/null +++ b/meshtastic/localonly.options @@ -0,0 +1,8 @@ +# nanopb sizing for local-only persistence wrappers. +# LocalConfig and LocalModuleConfig contain only nested message types whose +# sizing is governed by their own .options files, so they have no entries +# here. + +# Bounded client-app metadata store: max 4 records per node by default. +# TODO(maintainer): confirm record cap during review. +*LocalClientAppData.records max_count:4 diff --git a/meshtastic/localonly.proto b/meshtastic/localonly.proto index 2a6c7cac..3010d779 100644 --- a/meshtastic/localonly.proto +++ b/meshtastic/localonly.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package meshtastic; +import "meshtastic/admin.proto"; import "meshtastic/config.proto"; import "meshtastic/module_config.proto"; @@ -153,3 +154,25 @@ message LocalModuleConfig { */ uint32 version = 8; } + +/* + * On-disk wrapper for the bounded local client-app metadata store. + * Persisted by the firmware to /prefs/clientappdata.proto. Never sent over + * the wire and never broadcast. See ClientAppData (admin.proto) for the + * namespaced-not-owned caveat that governs how clients should treat the + * stored payloads. + */ +message LocalClientAppData { + /* + * The set of stored records. Bounded by localonly.options to a small + * number (initially 4). Firmware enforces uniqueness on app_id. + */ + repeated ClientAppData records = 1; + + /* + * A version integer used to invalidate old save files when we make + * incompatible changes. Set at build time by NodeDB.cpp in the device + * code, mirroring LocalConfig.version / LocalModuleConfig.version. + */ + uint32 version = 2; +}