From 76255092370d5df95627e976169d034109d05963 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Fri, 19 Jun 2026 02:54:50 +0200 Subject: [PATCH] feat(studio): optional pinned-key field in the store-add form The module-store manager's 'Add a store' form gains an optional PEM public-key field; when set it pins that publisher key to the store (per-store signing trust). Localized DE/EN. Signed-off-by: flemming-it --- lib/data/hub.dart | 8 ++++++- lib/l10n/app_de.arb | 2 ++ lib/l10n/app_en.arb | 2 ++ lib/l10n/app_localizations.dart | 12 ++++++++++ lib/l10n/app_localizations_de.dart | 8 +++++++ lib/l10n/app_localizations_en.dart | 8 +++++++ lib/widgets/chain_stores_dialog.dart | 33 ++++++++++++++++++++++++---- 7 files changed, 68 insertions(+), 5 deletions(-) diff --git a/lib/data/hub.dart b/lib/data/hub.dart index 5d67fba..27eb86c 100644 --- a/lib/data/hub.dart +++ b/lib/data/hub.dart @@ -220,8 +220,14 @@ class HubService { required String name, required String url, String bearerEnv = '', + String pinnedPubkeyPem = '', }) => - _client.addStore(name: name, url: url, bearerEnv: bearerEnv); + _client.addStore( + name: name, + url: url, + bearerEnv: bearerEnv, + pinnedPubkeyPem: pinnedPubkeyPem, + ); /// Drop a configured store by name (the bundled seed cannot be removed). Future removeStore(String name) => diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 76d4382..d74a093 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -332,6 +332,8 @@ "storesManagerRemoveFailed": "Store konnte nicht entfernt werden", "storesManagerSuggested": "Vorgeschlagene Stores", "storesManagerAddShort": "Hinzufügen", + "storesManagerPinnedKeyLabel": "Angepinnter Public-Key — PEM (optional)", + "storesManagerPinnedKeyHint": "Signierte Module dieses Stores werden gegen diesen Key geprüft statt gegen den eingebauten Vendor-Key. Füge einen PEM-Public-Key ein.", "storesManagerNotAvailable": "Noch nicht verfügbar", "storesManagerNotAvailableHint": "Der Anbieter hat diesen Store-Index noch nicht veröffentlicht. Sobald er das tut, ist er installierbar.", "storesSuggestedReclaimDesc": "Recl∆Im — Module für Rechts- und Pflichten-Reform-Mapping", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 45f0e53..2f5c0e1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -345,6 +345,8 @@ "storesManagerRemoveFailed": "Could not remove store", "storesManagerSuggested": "Suggested stores", "storesManagerAddShort": "Add", + "storesManagerPinnedKeyLabel": "Pinned public key — PEM (optional)", + "storesManagerPinnedKeyHint": "Signed modules from this store verify against this key instead of the built-in vendor key. Paste a PEM public key.", "storesManagerNotAvailable": "Not available yet", "storesManagerNotAvailableHint": "The provider hasn't published this store index yet. It will become installable once they do.", "storesSuggestedReclaimDesc": "Recl∆Im — modules for law & duty reform-mapping", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c1410f0..5948825 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1514,6 +1514,18 @@ abstract class AppLocalizations { /// **'Add'** String get storesManagerAddShort; + /// No description provided for @storesManagerPinnedKeyLabel. + /// + /// In en, this message translates to: + /// **'Pinned public key — PEM (optional)'** + String get storesManagerPinnedKeyLabel; + + /// No description provided for @storesManagerPinnedKeyHint. + /// + /// In en, this message translates to: + /// **'Signed modules from this store verify against this key instead of the built-in vendor key. Paste a PEM public key.'** + String get storesManagerPinnedKeyHint; + /// No description provided for @storesManagerNotAvailable. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ee21fb7..6ba161e 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -815,6 +815,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get storesManagerAddShort => 'Hinzufügen'; + @override + String get storesManagerPinnedKeyLabel => + 'Angepinnter Public-Key — PEM (optional)'; + + @override + String get storesManagerPinnedKeyHint => + 'Signierte Module dieses Stores werden gegen diesen Key geprüft statt gegen den eingebauten Vendor-Key. Füge einen PEM-Public-Key ein.'; + @override String get storesManagerNotAvailable => 'Noch nicht verfügbar'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 606c3f1..c1114a9 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -833,6 +833,14 @@ class AppLocalizationsEn extends AppLocalizations { @override String get storesManagerAddShort => 'Add'; + @override + String get storesManagerPinnedKeyLabel => + 'Pinned public key — PEM (optional)'; + + @override + String get storesManagerPinnedKeyHint => + 'Signed modules from this store verify against this key instead of the built-in vendor key. Paste a PEM public key.'; + @override String get storesManagerNotAvailable => 'Not available yet'; diff --git a/lib/widgets/chain_stores_dialog.dart b/lib/widgets/chain_stores_dialog.dart index d84e091..f7a8162 100644 --- a/lib/widgets/chain_stores_dialog.dart +++ b/lib/widgets/chain_stores_dialog.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import '../data/error_presentation.dart'; import '../data/hub.dart'; import '../l10n/app_localizations.dart'; +import '../theme/theme.dart'; import '../theme/tokens.dart'; class ChainStoresDialog extends StatefulWidget { @@ -30,6 +31,7 @@ class _ChainStoresDialogState extends State { late Future> _future; final _name = TextEditingController(); final _url = TextEditingController(); + final _pubkey = TextEditingController(); bool _busy = false; /// Reachability of each suggested store's index URL. `null` while the @@ -81,24 +83,31 @@ class _ChainStoresDialogState extends State { void dispose() { _name.dispose(); _url.dispose(); + _pubkey.dispose(); super.dispose(); } - Future _add() => - _addStore(_name.text.trim(), _url.text.trim(), fromFields: true); + Future _add() => _addStore( + _name.text.trim(), + _url.text.trim(), + pinnedPubkeyPem: _pubkey.text.trim(), + fromFields: true, + ); /// Shared add path — used by both the manual name/URL form and the /// one-click suggested-store buttons. Future _addStore(String name, String url, - {bool fromFields = false}) async { + {String pinnedPubkeyPem = '', bool fromFields = false}) async { if (name.isEmpty || url.isEmpty) return; setState(() => _busy = true); try { - final r = await HubService.instance.addStore(name: name, url: url); + final r = await HubService.instance + .addStore(name: name, url: url, pinnedPubkeyPem: pinnedPubkeyPem); if (!mounted) return; if (fromFields) { _name.clear(); _url.clear(); + _pubkey.clear(); } ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(r.message))); @@ -248,6 +257,22 @@ class _ChainStoresDialogState extends State { ), ], ), + const SizedBox(height: ChainSpace.sm), + // Optional per-store pinned publisher key (PEM). When set, + // signed modules from this store verify against this key + // instead of the built-in vendor anchor. + TextField( + controller: _pubkey, + minLines: 1, + maxLines: 4, + style: ChainTheme.mono(size: 11), + decoration: InputDecoration( + labelText: l.storesManagerPinnedKeyLabel, + helperText: l.storesManagerPinnedKeyHint, + helperMaxLines: 3, + isDense: true, + ), + ), ], ), ),