feat(studio): optional pinned-key field in the store-add form
Some checks are pending
Security / Security check (push) Waiting to run

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 <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-06-19 02:54:50 +02:00
parent 762788ea9e
commit 7625509237
7 changed files with 68 additions and 5 deletions

View file

@ -220,8 +220,14 @@ class HubService {
required String name, required String name,
required String url, required String url,
String bearerEnv = '', 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). /// Drop a configured store by name (the bundled seed cannot be removed).
Future<RemoveStoreResponse> removeStore(String name) => Future<RemoveStoreResponse> removeStore(String name) =>

View file

@ -332,6 +332,8 @@
"storesManagerRemoveFailed": "Store konnte nicht entfernt werden", "storesManagerRemoveFailed": "Store konnte nicht entfernt werden",
"storesManagerSuggested": "Vorgeschlagene Stores", "storesManagerSuggested": "Vorgeschlagene Stores",
"storesManagerAddShort": "Hinzufügen", "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", "storesManagerNotAvailable": "Noch nicht verfügbar",
"storesManagerNotAvailableHint": "Der Anbieter hat diesen Store-Index noch nicht veröffentlicht. Sobald er das tut, ist er installierbar.", "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", "storesSuggestedReclaimDesc": "Recl∆Im — Module für Rechts- und Pflichten-Reform-Mapping",

View file

@ -345,6 +345,8 @@
"storesManagerRemoveFailed": "Could not remove store", "storesManagerRemoveFailed": "Could not remove store",
"storesManagerSuggested": "Suggested stores", "storesManagerSuggested": "Suggested stores",
"storesManagerAddShort": "Add", "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", "storesManagerNotAvailable": "Not available yet",
"storesManagerNotAvailableHint": "The provider hasn't published this store index yet. It will become installable once they do.", "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", "storesSuggestedReclaimDesc": "Recl∆Im — modules for law & duty reform-mapping",

View file

@ -1514,6 +1514,18 @@ abstract class AppLocalizations {
/// **'Add'** /// **'Add'**
String get storesManagerAddShort; 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. /// No description provided for @storesManagerNotAvailable.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -815,6 +815,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get storesManagerAddShort => 'Hinzufügen'; 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 @override
String get storesManagerNotAvailable => 'Noch nicht verfügbar'; String get storesManagerNotAvailable => 'Noch nicht verfügbar';

View file

@ -833,6 +833,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get storesManagerAddShort => 'Add'; 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 @override
String get storesManagerNotAvailable => 'Not available yet'; String get storesManagerNotAvailable => 'Not available yet';

View file

@ -12,6 +12,7 @@ import 'package:flutter/material.dart';
import '../data/error_presentation.dart'; import '../data/error_presentation.dart';
import '../data/hub.dart'; import '../data/hub.dart';
import '../l10n/app_localizations.dart'; import '../l10n/app_localizations.dart';
import '../theme/theme.dart';
import '../theme/tokens.dart'; import '../theme/tokens.dart';
class ChainStoresDialog extends StatefulWidget { class ChainStoresDialog extends StatefulWidget {
@ -30,6 +31,7 @@ class _ChainStoresDialogState extends State<ChainStoresDialog> {
late Future<List<StoreSource>> _future; late Future<List<StoreSource>> _future;
final _name = TextEditingController(); final _name = TextEditingController();
final _url = TextEditingController(); final _url = TextEditingController();
final _pubkey = TextEditingController();
bool _busy = false; bool _busy = false;
/// Reachability of each suggested store's index URL. `null` while the /// Reachability of each suggested store's index URL. `null` while the
@ -81,24 +83,31 @@ class _ChainStoresDialogState extends State<ChainStoresDialog> {
void dispose() { void dispose() {
_name.dispose(); _name.dispose();
_url.dispose(); _url.dispose();
_pubkey.dispose();
super.dispose(); super.dispose();
} }
Future<void> _add() => Future<void> _add() => _addStore(
_addStore(_name.text.trim(), _url.text.trim(), fromFields: true); _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 /// Shared add path used by both the manual name/URL form and the
/// one-click suggested-store buttons. /// one-click suggested-store buttons.
Future<void> _addStore(String name, String url, Future<void> _addStore(String name, String url,
{bool fromFields = false}) async { {String pinnedPubkeyPem = '', bool fromFields = false}) async {
if (name.isEmpty || url.isEmpty) return; if (name.isEmpty || url.isEmpty) return;
setState(() => _busy = true); setState(() => _busy = true);
try { 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 (!mounted) return;
if (fromFields) { if (fromFields) {
_name.clear(); _name.clear();
_url.clear(); _url.clear();
_pubkey.clear();
} }
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(r.message))); .showSnackBar(SnackBar(content: Text(r.message)));
@ -248,6 +257,22 @@ class _ChainStoresDialogState extends State<ChainStoresDialog> {
), ),
], ],
), ),
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,
),
),
], ],
), ),
), ),