diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c287c..4d95a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ lockstep. ## Unreleased +### Added (0.79.0) + +- **Maintainers in the store.** The detail sheet always answers "who + maintains this?" — one selectable line per maintainer from the + store index, or an honest "not specified" when the index names + nobody. The install trust gate carries the same fact when present. + All bundled seed entries name their maintainer (guarded hub-side). + ### Fixed — honest install badge in the flow editor (0.78.0) - **Install is only offered when the store can deliver.** The diff --git a/integration_test/dialog_shots_test.dart b/integration_test/dialog_shots_test.dart index 25646f8..3fa17a0 100644 --- a/integration_test/dialog_shots_test.dart +++ b/integration_test/dialog_shots_test.dart @@ -69,6 +69,7 @@ StoreItem _storeItem(String verification) => StoreItem( provider: '', source: 'bundled', installVerification: verification, + maintainers: const ['Dr. Stefan Flemming (Flemming.AI) '], ); void main() { diff --git a/lib/data/about_info.dart b/lib/data/about_info.dart index b241df1..71f20cb 100644 --- a/lib/data/about_info.dart +++ b/lib/data/about_info.dart @@ -4,7 +4,7 @@ /// Studio's own build version. Bump on every UI release so the /// running app self-identifies. -const String kStudioVersion = '0.78.0'; +const String kStudioVersion = '0.79.0'; const String kProductName = 'Ch∆In Studio'; const String kVendorName = 'Flemming.AI (F∆I)'; diff --git a/lib/data/hub.dart b/lib/data/hub.dart index 11a0cac..aa04481 100644 --- a/lib/data/hub.dart +++ b/lib/data/hub.dart @@ -938,6 +938,7 @@ class HubService { canonicalCategory: e.canonicalCategory, canonicalCategoryLabel: e.canonicalCategoryLabel, installVerification: e.installVerification, + maintainers: e.maintainers, ), ) .toList(); @@ -2245,6 +2246,11 @@ class StoreItem { /// Human-readable label for [canonicalCategory] (e.g. "Data & Formats"). final String canonicalCategoryLabel; + /// Maintainer display strings from the store index (one per + /// person/organization). Empty when the index does not specify + /// any — the detail sheet renders an honest "not specified". + final List maintainers; + /// How an install of this entry would be verified under the /// hub's CURRENT policy — computed hub-side with the same /// resolvers the install gate enforces, so this can never @@ -2286,5 +2292,6 @@ class StoreItem { this.canonicalCategory = '', this.canonicalCategoryLabel = '', this.installVerification = '', + this.maintainers = const [], }); } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f976bfc..858dbdb 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1630,6 +1630,9 @@ "installConfirmNeedsCapabilities": "Benötigte Fähigkeiten", "installConfirmTrustTitle": "Vertrauen & Sicherheit", "installConfirmTrustBody": "Das Modul läuft in einer Sandbox: Es darf nur auf die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen zugreifen, die es selbst deklariert — der Hub setzt diese Liste durch. Die vollständige Berechtigungsliste sehen Sie nach der Installation in den Modul-Details.", + "storeSectionMaintainers": "Maintainer", + "storeMaintainersNone": "nicht angegeben", + "installConfirmMaintainers": "Maintainer", "storePolicyUnverifiedNotice": "Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — Installationen werden nicht kryptografisch geprüft. Der Installations-Dialog zeigt den Status je Modul; für geprüfte Installationen security.require_signatures aktivieren.", "verifPillBlocked": "blockiert", "verifPinnedKey": "Signatur wird geprüft — hinterlegter Store-Schlüssel", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 13864d3..c2c3a96 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1654,6 +1654,9 @@ "installConfirmNeedsCapabilities": "Required capabilities", "installConfirmTrustTitle": "Trust & security", "installConfirmTrustBody": "The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.", + "storeSectionMaintainers": "Maintainers", + "storeMaintainersNone": "not specified", + "installConfirmMaintainers": "Maintainers", "storePolicyUnverifiedNotice": "Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.", "verifPillBlocked": "blocked", "verifPinnedKey": "Signature checked — pinned store key", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 9adc9af..ff78b39 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4946,6 +4946,24 @@ abstract class AppLocalizations { /// **'The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.'** String get installConfirmTrustBody; + /// No description provided for @storeSectionMaintainers. + /// + /// In en, this message translates to: + /// **'Maintainers'** + String get storeSectionMaintainers; + + /// No description provided for @storeMaintainersNone. + /// + /// In en, this message translates to: + /// **'not specified'** + String get storeMaintainersNone; + + /// No description provided for @installConfirmMaintainers. + /// + /// In en, this message translates to: + /// **'Maintainers'** + String get installConfirmMaintainers; + /// No description provided for @storePolicyUnverifiedNotice. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 2246e2a..8c792fe 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2914,6 +2914,15 @@ class AppLocalizationsDe extends AppLocalizations { String get installConfirmTrustBody => 'Das Modul läuft in einer Sandbox: Es darf nur auf die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen zugreifen, die es selbst deklariert — der Hub setzt diese Liste durch. Die vollständige Berechtigungsliste sehen Sie nach der Installation in den Modul-Details.'; + @override + String get storeSectionMaintainers => 'Maintainer'; + + @override + String get storeMaintainersNone => 'nicht angegeben'; + + @override + String get installConfirmMaintainers => 'Maintainer'; + @override String get storePolicyUnverifiedNotice => 'Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — Installationen werden nicht kryptografisch geprüft. Der Installations-Dialog zeigt den Status je Modul; für geprüfte Installationen security.require_signatures aktivieren.'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f2edce7..e9a515f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2909,6 +2909,15 @@ class AppLocalizationsEn extends AppLocalizations { String get installConfirmTrustBody => 'The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.'; + @override + String get storeSectionMaintainers => 'Maintainers'; + + @override + String get storeMaintainersNone => 'not specified'; + + @override + String get installConfirmMaintainers => 'Maintainers'; + @override String get storePolicyUnverifiedNotice => 'Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.'; diff --git a/lib/pages/store.dart b/lib/pages/store.dart index 14523ee..48f00ab 100644 --- a/lib/pages/store.dart +++ b/lib/pages/store.dart @@ -2686,6 +2686,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { _DocsPanel(text: _docsResult!.text), const SizedBox(height: ChainSpace.lg), ], + StoreMaintainersSection(maintainers: item.maintainers), if (item.repository.isNotEmpty) ...[ _SectionHeader(l.storeSectionSource), const SizedBox(height: ChainSpace.sm), @@ -3835,6 +3836,44 @@ class _ProvenancePill extends StatelessWidget { } } +/// Who maintains this module — always shown in the detail sheet +/// (decision-maker transparency, same motivation as the About +/// page): one selectable line per maintainer, and an honest +/// "not specified" when the store index names nobody. Public so +/// the widget test pumps both states directly. +class StoreMaintainersSection extends StatelessWidget { + final List maintainers; + + const StoreMaintainersSection({super.key, required this.maintainers}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SectionHeader(l.storeSectionMaintainers), + const SizedBox(height: ChainSpace.sm), + if (maintainers.isEmpty) + Text( + l.storeMaintainersNone, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ) + else + for (final m in maintainers) + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: SelectableText(m, style: theme.textTheme.bodySmall), + ), + const SizedBox(height: ChainSpace.lg), + ], + ); + } +} + /// Signature-verification warning pill. Same philosophy as the /// provenance pill: the GOOD path (signature checked at install) /// stays quiet, and so does the policy-off case — that one is a diff --git a/lib/widgets/chain_install_confirm.dart b/lib/widgets/chain_install_confirm.dart index 12c0c15..9cca1f2 100644 --- a/lib/widgets/chain_install_confirm.dart +++ b/lib/widgets/chain_install_confirm.dart @@ -112,6 +112,8 @@ class ChainInstallConfirmDialog extends StatelessWidget { : item.source, ), if (item.license.isNotEmpty) (l.installConfirmLicense, item.license), + if (item.maintainers.isNotEmpty) + (l.installConfirmMaintainers, item.maintainers.join(', ')), if (item.status.isNotEmpty) (l.installConfirmStatus, _statusLabel(l)), if (item.requiresServices.isNotEmpty) (l.installConfirmNeedsServices, item.requiresServices.join(', ')), diff --git a/pubspec.yaml b/pubspec.yaml index 2b8c955..2937cb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: chain_studio description: "Ch∆In Studio — desktop GUI for the Ch∆In hub" publish_to: 'none' -version: 0.78.0 +version: 0.79.0 environment: sdk: ^3.11.0-200.1.beta diff --git a/test/store_maintainers_test.dart b/test/store_maintainers_test.dart new file mode 100644 index 0000000..3b7840b --- /dev/null +++ b/test/store_maintainers_test.dart @@ -0,0 +1,97 @@ +// Maintainer transparency in the store (decision-maker finding +// class): the detail sheet always answers "who maintains this?" — +// with the index's list, or an honest "not specified" — and the +// install trust gate carries the same fact when present. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:chain_studio/data/hub.dart'; +import 'package:chain_studio/l10n/app_localizations.dart'; +import 'package:chain_studio/pages/store.dart' show StoreMaintainersSection; +import 'package:chain_studio/widgets/chain_install_confirm.dart'; + +Widget _host(Widget child) => MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: const Locale('de'), + home: Scaffold(body: SingleChildScrollView(child: child)), +); + +StoreItem _item({List maintainers = const []}) => StoreItem( + name: 'text.extract', + taglineEn: '', + taglineDe: '', + descriptionEn: '', + descriptionDe: '', + category: 'text', + tags: const [], + requiresCapabilities: const [], + requiresServices: const [], + license: 'Apache-2.0', + repository: '', + bestVersion: '0.1.0', + status: 'alpha', + installed: false, + featured: false, + iconUrl: '', + screenshotUrls: const [], + docsUrl: '', + kind: 'native', + provider: '', + source: 'bundled', + maintainers: maintainers, +); + +void main() { + testWidgets('lists every maintainer on its own selectable line', ( + tester, + ) async { + await tester.pumpWidget( + _host( + const StoreMaintainersSection( + maintainers: [ + 'Jane Doe (Example Org) ', + 'John Roe ', + ], + ), + ), + ); + expect( + find.text('Jane Doe (Example Org) '), + findsOneWidget, + ); + expect(find.text('John Roe '), findsOneWidget); + expect(find.text('nicht angegeben'), findsNothing); + }); + + testWidgets('an empty list renders the honest fallback', (tester) async { + await tester.pumpWidget( + _host(const StoreMaintainersSection(maintainers: [])), + ); + expect(find.text('MAINTAINER'), findsOneWidget); + expect(find.text('nicht angegeben'), findsOneWidget); + }); + + testWidgets('the trust gate names the maintainers when present', ( + tester, + ) async { + await tester.pumpWidget( + _host( + ChainInstallConfirmDialog( + item: _item(maintainers: ['Jane Doe ']), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.text('Jane Doe '), findsOneWidget); + }); + + testWidgets('the trust gate stays terse without maintainer data', ( + tester, + ) async { + await tester.pumpWidget(_host(ChainInstallConfirmDialog(item: _item()))); + await tester.pumpAndSettle(); + expect(find.text('Maintainer'), findsNothing); + }); +}