feat(trust): full Duniter web-of-trust membership (params + referents)
Slice 4 of Block 2. The pure rule was already parameterised; this adds the policy, cold-start and UI around it. - commons_core: WotParams (sigQty/stepMax/sigValidity, Duniter defaults) + WebOfTrust.membersWith; npubToHex helper (NIP-19 decode) for adding roots. - WotSettings (keystore): the active parameters, configurable — a young network loosens them and tightens as it grows, as Ğ1 did. Defaults Duniter. - TrustReferents: the bootstrap 'seeds' membership is measured from — a bundled asset (empty until real founders are curated, no invented keys) unioned with referents the user adds by npub/QR. Honest cold-start. - TrustCubit: computes the full membership verdict against referents+params alongside the personal circle, and exposes a TrustTier (networkMember > inYourCircle > vouched > unknown). Certifications issued with the active validity (they expire and renew, Duniter rule). - UI: chat trust badge by tier; a 'Network of trust' screen (manage roots + advanced params) reached from the profile. i18n en/es/pt/ast. Tests: WotParams/membersWith, npubToHex, TrustReferents, WotSettings, and TrustCubit tiers/membership. Resolves the WoT-parameters decision (open-decisions §B). Trust net stays empty/undetermined until seeded — by design; users bootstrap their own roots.
This commit is contained in:
parent
a96049dd36
commit
4cf53f259f
29 changed files with 1111 additions and 39 deletions
5
apps/app_seeds/assets/trust/referents.json
Normal file
5
apps/app_seeds/assets/trust/referents.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"_comment": "Web-of-trust bootstrap referents (Duniter 'seeds'): the founding identities every membership calculation trusts by construction. Fill 'referents' with npub (or 64-char hex) public keys of real founding members — e.g. Ğ1 seed-group organisers running Tanemaki. Empty by design until curated; users can also add their own referents in-app. Locale-agnostic.",
|
||||||
|
"version": 1,
|
||||||
|
"referents": []
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,8 @@ import 'services/profile_store.dart';
|
||||||
import 'services/social_account_store.dart';
|
import 'services/social_account_store.dart';
|
||||||
import 'services/social_service.dart';
|
import 'services/social_service.dart';
|
||||||
import 'services/social_settings.dart';
|
import 'services/social_settings.dart';
|
||||||
|
import 'services/trust_referents.dart';
|
||||||
|
import 'services/wot_settings.dart';
|
||||||
import 'state/inventory_cubit.dart';
|
import 'state/inventory_cubit.dart';
|
||||||
import 'state/variety_detail_cubit.dart';
|
import 'state/variety_detail_cubit.dart';
|
||||||
import 'ui/about_screen.dart';
|
import 'ui/about_screen.dart';
|
||||||
|
|
@ -34,6 +36,7 @@ import 'ui/offline_banner.dart';
|
||||||
import 'ui/profile_screen.dart';
|
import 'ui/profile_screen.dart';
|
||||||
import 'ui/settings_screen.dart';
|
import 'ui/settings_screen.dart';
|
||||||
import 'ui/theme.dart';
|
import 'ui/theme.dart';
|
||||||
|
import 'ui/trust_network_screen.dart';
|
||||||
import 'ui/variety_detail_screen.dart';
|
import 'ui/variety_detail_screen.dart';
|
||||||
|
|
||||||
/// Root widget. Provides the repositories to the tree and wires go_router:
|
/// Root widget. Provides the repositories to the tree and wires go_router:
|
||||||
|
|
@ -52,6 +55,8 @@ class TaneApp extends StatelessWidget {
|
||||||
this.profileStore,
|
this.profileStore,
|
||||||
this.profileCache,
|
this.profileCache,
|
||||||
this.socialAccounts,
|
this.socialAccounts,
|
||||||
|
this.trustReferents,
|
||||||
|
this.wotSettings,
|
||||||
this.inbox,
|
this.inbox,
|
||||||
this.showIntro = false,
|
this.showIntro = false,
|
||||||
this.autoBackup,
|
this.autoBackup,
|
||||||
|
|
@ -68,6 +73,8 @@ class TaneApp extends StatelessWidget {
|
||||||
profileStore,
|
profileStore,
|
||||||
profileCache,
|
profileCache,
|
||||||
socialAccounts,
|
socialAccounts,
|
||||||
|
trustReferents,
|
||||||
|
wotSettings,
|
||||||
inbox,
|
inbox,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -98,6 +105,10 @@ class TaneApp extends StatelessWidget {
|
||||||
/// Optional store of the active social identity, for the profile switcher.
|
/// Optional store of the active social identity, for the profile switcher.
|
||||||
final SocialAccountStore? socialAccounts;
|
final SocialAccountStore? socialAccounts;
|
||||||
|
|
||||||
|
/// Web-of-trust bootstrap referents + parameters (network membership).
|
||||||
|
final TrustReferents? trustReferents;
|
||||||
|
final WotSettings? wotSettings;
|
||||||
|
|
||||||
/// App-wide inbox listener; drives the messages list's live refresh.
|
/// App-wide inbox listener; drives the messages list's live refresh.
|
||||||
final InboxService? inbox;
|
final InboxService? inbox;
|
||||||
final bool showIntro;
|
final bool showIntro;
|
||||||
|
|
@ -119,6 +130,8 @@ class TaneApp extends StatelessWidget {
|
||||||
ProfileStore? profileStore,
|
ProfileStore? profileStore,
|
||||||
ProfileCache? profileCache,
|
ProfileCache? profileCache,
|
||||||
SocialAccountStore? socialAccounts,
|
SocialAccountStore? socialAccounts,
|
||||||
|
TrustReferents? trustReferents,
|
||||||
|
WotSettings? wotSettings,
|
||||||
InboxService? inbox,
|
InboxService? inbox,
|
||||||
) {
|
) {
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
|
|
@ -175,6 +188,16 @@ class TaneApp extends StatelessWidget {
|
||||||
settings: socialSettings,
|
settings: socialSettings,
|
||||||
profileStore: profileStore,
|
profileStore: profileStore,
|
||||||
accounts: socialAccounts,
|
accounts: socialAccounts,
|
||||||
|
trustNetworkEnabled:
|
||||||
|
trustReferents != null && wotSettings != null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (trustReferents != null && wotSettings != null)
|
||||||
|
GoRoute(
|
||||||
|
path: '/trust',
|
||||||
|
builder: (context, state) => TrustNetworkScreen(
|
||||||
|
referents: trustReferents,
|
||||||
|
wotSettings: wotSettings,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (social != null && socialSettings != null)
|
if (social != null && socialSettings != null)
|
||||||
|
|
@ -186,6 +209,8 @@ class TaneApp extends StatelessWidget {
|
||||||
peerPubkey: state.pathParameters['pubkey']!,
|
peerPubkey: state.pathParameters['pubkey']!,
|
||||||
messageStore: messageStore,
|
messageStore: messageStore,
|
||||||
profileCache: profileCache,
|
profileCache: profileCache,
|
||||||
|
trustReferents: trustReferents,
|
||||||
|
wotSettings: wotSettings,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import 'services/profile_store.dart';
|
||||||
import 'services/social_account_store.dart';
|
import 'services/social_account_store.dart';
|
||||||
import 'services/social_service.dart';
|
import 'services/social_service.dart';
|
||||||
import 'services/social_settings.dart';
|
import 'services/social_settings.dart';
|
||||||
|
import 'services/trust_referents.dart';
|
||||||
|
import 'services/wot_settings.dart';
|
||||||
import 'ui/theme.dart';
|
import 'ui/theme.dart';
|
||||||
|
|
||||||
/// Boots the app WITHOUT blocking the first frame: paints a splash immediately,
|
/// Boots the app WITHOUT blocking the first frame: paints a splash immediately,
|
||||||
|
|
@ -69,6 +71,8 @@ class _BootstrapState extends State<Bootstrap> {
|
||||||
profileStore: getIt<ProfileStore>(),
|
profileStore: getIt<ProfileStore>(),
|
||||||
profileCache: getIt<ProfileCache>(),
|
profileCache: getIt<ProfileCache>(),
|
||||||
socialAccounts: getIt<SocialAccountStore>(),
|
socialAccounts: getIt<SocialAccountStore>(),
|
||||||
|
trustReferents: getIt<TrustReferents>(),
|
||||||
|
wotSettings: getIt<WotSettings>(),
|
||||||
inbox: inbox,
|
inbox: inbox,
|
||||||
showIntro: !await onboarding.introSeen(),
|
showIntro: !await onboarding.introSeen(),
|
||||||
autoBackup: getIt.isRegistered<AutoBackupService>()
|
autoBackup: getIt.isRegistered<AutoBackupService>()
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ import '../services/profile_store.dart';
|
||||||
import '../services/social_account_store.dart';
|
import '../services/social_account_store.dart';
|
||||||
import '../services/social_service.dart';
|
import '../services/social_service.dart';
|
||||||
import '../services/social_settings.dart';
|
import '../services/social_settings.dart';
|
||||||
|
import '../services/trust_referents.dart';
|
||||||
|
import '../services/wot_settings.dart';
|
||||||
|
|
||||||
/// The app's service locator. Kept to the composition root — widgets get their
|
/// The app's service locator. Kept to the composition root — widgets get their
|
||||||
/// repositories from here (or via BlocProvider), never by reaching into it deep
|
/// repositories from here (or via BlocProvider), never by reaching into it deep
|
||||||
|
|
@ -146,6 +148,9 @@ Future<void> configureDependencies() async {
|
||||||
..registerSingleton<LocaleStore>(LocaleStore(secretStore))
|
..registerSingleton<LocaleStore>(LocaleStore(secretStore))
|
||||||
..registerSingleton<SocialSettings>(SocialSettings(secretStore))
|
..registerSingleton<SocialSettings>(SocialSettings(secretStore))
|
||||||
..registerSingleton<SocialAccountStore>(accounts)
|
..registerSingleton<SocialAccountStore>(accounts)
|
||||||
|
// Web of trust: network-wide (not per-identity), so registered once.
|
||||||
|
..registerSingleton<TrustReferents>(TrustReferents(secretStore))
|
||||||
|
..registerSingleton<WotSettings>(WotSettings(secretStore))
|
||||||
..registerSingleton<OfferOutbox>(OfferOutbox(secretStore))
|
..registerSingleton<OfferOutbox>(OfferOutbox(secretStore))
|
||||||
// Per-identity stores are namespaced by the active account's scope.
|
// Per-identity stores are namespaced by the active account's scope.
|
||||||
..registerSingleton<MessageStore>(
|
..registerSingleton<MessageStore>(
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,28 @@
|
||||||
"count": "Avalada por {n}",
|
"count": "Avalada por {n}",
|
||||||
"vouch": "Conozo a esta persona",
|
"vouch": "Conozo a esta persona",
|
||||||
"vouched": "Avales a esta persona",
|
"vouched": "Avales a esta persona",
|
||||||
"circle": "Nel to círculu"
|
"circle": "Nel to círculu",
|
||||||
|
"member": "Miembru de confianza de la rede"
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"title": "Rede de confianza",
|
||||||
|
"open": "Rede de confianza",
|
||||||
|
"help": "Cómo se decide quién ye miembru de confianza: la xente aválase ente sí, y la confianza estiéndese dende un conxuntu de raíces fundadores.",
|
||||||
|
"roots": "Raíces de confianza",
|
||||||
|
"rootsHelp": "Les identidaes fundadores qu'anclen la rede. Amiesta a quien te fíes (apega'l so códigu d'identidá); la pertenencia mídese p'afuera dende elles.",
|
||||||
|
"noRoots": "Entá nun hai raíces de confianza — nun se pue calcular la pertenencia hasta semar la rede.",
|
||||||
|
"addRoot": "Amestar una raíz de confianza",
|
||||||
|
"rootHint": "Apega un códigu d'identidá (npub…)",
|
||||||
|
"add": "Amestar",
|
||||||
|
"rootAdded": "Raíz de confianza amestada",
|
||||||
|
"rootInvalid": "Esi nun ye un códigu d'identidá válidu",
|
||||||
|
"remove": "Quitar",
|
||||||
|
"params": "Parámetros avanzaos",
|
||||||
|
"paramsHelp": "Regles Duniter. Aflóxales pa una rede nueva; apriétales según crez.",
|
||||||
|
"sigQty": "Avales pa ser miembru",
|
||||||
|
"stepMax": "Distancia máxima a una raíz",
|
||||||
|
"validityDays": "Validez del aval (díes)",
|
||||||
|
"reset": "Reafitar a valores Duniter",
|
||||||
|
"saved": "Guardáu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -446,6 +446,28 @@
|
||||||
"count": "Vouched for by {n}",
|
"count": "Vouched for by {n}",
|
||||||
"vouch": "I know this person",
|
"vouch": "I know this person",
|
||||||
"vouched": "You vouch for them",
|
"vouched": "You vouch for them",
|
||||||
"circle": "In your circle"
|
"circle": "In your circle",
|
||||||
|
"member": "Trusted member of the network"
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"title": "Network of trust",
|
||||||
|
"open": "Network of trust",
|
||||||
|
"help": "How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.",
|
||||||
|
"roots": "Trust roots",
|
||||||
|
"rootsHelp": "The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.",
|
||||||
|
"noRoots": "No trust roots yet — membership can't be worked out until the network is seeded.",
|
||||||
|
"addRoot": "Add a trust root",
|
||||||
|
"rootHint": "Paste an identity code (npub…)",
|
||||||
|
"add": "Add",
|
||||||
|
"rootAdded": "Trust root added",
|
||||||
|
"rootInvalid": "That's not a valid identity code",
|
||||||
|
"remove": "Remove",
|
||||||
|
"params": "Advanced parameters",
|
||||||
|
"paramsHelp": "Duniter rules. Loosen them for a young network; tighten as it grows.",
|
||||||
|
"sigQty": "Vouches to become a member",
|
||||||
|
"stepMax": "Max distance from a root",
|
||||||
|
"validityDays": "Vouch validity (days)",
|
||||||
|
"reset": "Reset to Duniter defaults",
|
||||||
|
"saved": "Saved"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -445,6 +445,28 @@
|
||||||
"count": "Avalada por {n}",
|
"count": "Avalada por {n}",
|
||||||
"vouch": "Conozco a esta persona",
|
"vouch": "Conozco a esta persona",
|
||||||
"vouched": "Avalas a esta persona",
|
"vouched": "Avalas a esta persona",
|
||||||
"circle": "En tu círculo"
|
"circle": "En tu círculo",
|
||||||
|
"member": "Miembro de confianza de la red"
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"title": "Red de confianza",
|
||||||
|
"open": "Red de confianza",
|
||||||
|
"help": "Cómo se decide quién es miembro de confianza: la gente se avala entre sí, y la confianza se extiende desde un conjunto de raíces fundadoras.",
|
||||||
|
"roots": "Raíces de confianza",
|
||||||
|
"rootsHelp": "Las identidades fundadoras que anclan la red. Añade a quien te fíes (pega su código de identidad); la membresía se mide hacia fuera desde ellas.",
|
||||||
|
"noRoots": "Aún no hay raíces de confianza — no se puede calcular la membresía hasta sembrar la red.",
|
||||||
|
"addRoot": "Añadir una raíz de confianza",
|
||||||
|
"rootHint": "Pega un código de identidad (npub…)",
|
||||||
|
"add": "Añadir",
|
||||||
|
"rootAdded": "Raíz de confianza añadida",
|
||||||
|
"rootInvalid": "Ese no es un código de identidad válido",
|
||||||
|
"remove": "Quitar",
|
||||||
|
"params": "Parámetros avanzados",
|
||||||
|
"paramsHelp": "Reglas Duniter. Aflójalas para una red joven; apriétalas según crece.",
|
||||||
|
"sigQty": "Avales para ser miembro",
|
||||||
|
"stepMax": "Distancia máxima a una raíz",
|
||||||
|
"validityDays": "Validez del aval (días)",
|
||||||
|
"reset": "Restablecer a valores Duniter",
|
||||||
|
"saved": "Guardado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -442,6 +442,28 @@
|
||||||
"count": "Avalizada por {n}",
|
"count": "Avalizada por {n}",
|
||||||
"vouch": "Conheço esta pessoa",
|
"vouch": "Conheço esta pessoa",
|
||||||
"vouched": "Avalizas esta pessoa",
|
"vouched": "Avalizas esta pessoa",
|
||||||
"circle": "No teu círculo"
|
"circle": "No teu círculo",
|
||||||
|
"member": "Membro de confiança da rede"
|
||||||
|
},
|
||||||
|
"wot": {
|
||||||
|
"title": "Rede de confiança",
|
||||||
|
"open": "Rede de confiança",
|
||||||
|
"help": "Como se decide quem é membro de confiança: as pessoas avalizam-se entre si, e a confiança estende-se a partir de um conjunto de raízes fundadoras.",
|
||||||
|
"roots": "Raízes de confiança",
|
||||||
|
"rootsHelp": "As identidades fundadoras que ancoram a rede. Adiciona quem confias (cola o seu código de identidade); a filiação mede-se para fora a partir delas.",
|
||||||
|
"noRoots": "Ainda não há raízes de confiança — não se pode calcular a filiação até semear a rede.",
|
||||||
|
"addRoot": "Adicionar uma raiz de confiança",
|
||||||
|
"rootHint": "Cola um código de identidade (npub…)",
|
||||||
|
"add": "Adicionar",
|
||||||
|
"rootAdded": "Raiz de confiança adicionada",
|
||||||
|
"rootInvalid": "Esse não é um código de identidade válido",
|
||||||
|
"remove": "Remover",
|
||||||
|
"params": "Parâmetros avançados",
|
||||||
|
"paramsHelp": "Regras Duniter. Afrouxa-as para uma rede jovem; aperta-as à medida que cresce.",
|
||||||
|
"sigQty": "Avais para ser membro",
|
||||||
|
"stepMax": "Distância máxima a uma raiz",
|
||||||
|
"validityDays": "Validade do aval (dias)",
|
||||||
|
"reset": "Repor para valores Duniter",
|
||||||
|
"saved": "Guardado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 4
|
/// Locales: 4
|
||||||
/// Strings: 1580 (395 per locale)
|
/// Strings: 1660 (415 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-07-10 at 18:22 UTC
|
/// Built on 2026-07-10 at 19:01 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ class TranslationsAst extends Translations with BaseTranslations<AppLocale, Tran
|
||||||
@override late final _Translations$chatList$ast chatList = _Translations$chatList$ast._(_root);
|
@override late final _Translations$chatList$ast chatList = _Translations$chatList$ast._(_root);
|
||||||
@override late final _Translations$chat$ast chat = _Translations$chat$ast._(_root);
|
@override late final _Translations$chat$ast chat = _Translations$chat$ast._(_root);
|
||||||
@override late final _Translations$trust$ast trust = _Translations$trust$ast._(_root);
|
@override late final _Translations$trust$ast trust = _Translations$trust$ast._(_root);
|
||||||
|
@override late final _Translations$wot$ast wot = _Translations$wot$ast._(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: app
|
// Path: app
|
||||||
|
|
@ -762,6 +763,35 @@ class _Translations$trust$ast extends Translations$trust$en {
|
||||||
@override String get vouch => 'Conozo a esta persona';
|
@override String get vouch => 'Conozo a esta persona';
|
||||||
@override String get vouched => 'Avales a esta persona';
|
@override String get vouched => 'Avales a esta persona';
|
||||||
@override String get circle => 'Nel to círculu';
|
@override String get circle => 'Nel to círculu';
|
||||||
|
@override String get member => 'Miembru de confianza de la rede';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: wot
|
||||||
|
class _Translations$wot$ast extends Translations$wot$en {
|
||||||
|
_Translations$wot$ast._(TranslationsAst root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsAst _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Rede de confianza';
|
||||||
|
@override String get open => 'Rede de confianza';
|
||||||
|
@override String get help => 'Cómo se decide quién ye miembru de confianza: la xente aválase ente sí, y la confianza estiéndese dende un conxuntu de raíces fundadores.';
|
||||||
|
@override String get roots => 'Raíces de confianza';
|
||||||
|
@override String get rootsHelp => 'Les identidaes fundadores qu\'anclen la rede. Amiesta a quien te fíes (apega\'l so códigu d\'identidá); la pertenencia mídese p\'afuera dende elles.';
|
||||||
|
@override String get noRoots => 'Entá nun hai raíces de confianza — nun se pue calcular la pertenencia hasta semar la rede.';
|
||||||
|
@override String get addRoot => 'Amestar una raíz de confianza';
|
||||||
|
@override String get rootHint => 'Apega un códigu d\'identidá (npub…)';
|
||||||
|
@override String get add => 'Amestar';
|
||||||
|
@override String get rootAdded => 'Raíz de confianza amestada';
|
||||||
|
@override String get rootInvalid => 'Esi nun ye un códigu d\'identidá válidu';
|
||||||
|
@override String get remove => 'Quitar';
|
||||||
|
@override String get params => 'Parámetros avanzaos';
|
||||||
|
@override String get paramsHelp => 'Regles Duniter. Aflóxales pa una rede nueva; apriétales según crez.';
|
||||||
|
@override String get sigQty => 'Avales pa ser miembru';
|
||||||
|
@override String get stepMax => 'Distancia máxima a una raíz';
|
||||||
|
@override String get validityDays => 'Validez del aval (díes)';
|
||||||
|
@override String get reset => 'Reafitar a valores Duniter';
|
||||||
|
@override String get saved => 'Guardáu';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: intro.slides
|
// Path: intro.slides
|
||||||
|
|
@ -1499,6 +1529,26 @@ extension on TranslationsAst {
|
||||||
'trust.vouch' => 'Conozo a esta persona',
|
'trust.vouch' => 'Conozo a esta persona',
|
||||||
'trust.vouched' => 'Avales a esta persona',
|
'trust.vouched' => 'Avales a esta persona',
|
||||||
'trust.circle' => 'Nel to círculu',
|
'trust.circle' => 'Nel to círculu',
|
||||||
|
'trust.member' => 'Miembru de confianza de la rede',
|
||||||
|
'wot.title' => 'Rede de confianza',
|
||||||
|
'wot.open' => 'Rede de confianza',
|
||||||
|
'wot.help' => 'Cómo se decide quién ye miembru de confianza: la xente aválase ente sí, y la confianza estiéndese dende un conxuntu de raíces fundadores.',
|
||||||
|
'wot.roots' => 'Raíces de confianza',
|
||||||
|
'wot.rootsHelp' => 'Les identidaes fundadores qu\'anclen la rede. Amiesta a quien te fíes (apega\'l so códigu d\'identidá); la pertenencia mídese p\'afuera dende elles.',
|
||||||
|
'wot.noRoots' => 'Entá nun hai raíces de confianza — nun se pue calcular la pertenencia hasta semar la rede.',
|
||||||
|
'wot.addRoot' => 'Amestar una raíz de confianza',
|
||||||
|
'wot.rootHint' => 'Apega un códigu d\'identidá (npub…)',
|
||||||
|
'wot.add' => 'Amestar',
|
||||||
|
'wot.rootAdded' => 'Raíz de confianza amestada',
|
||||||
|
'wot.rootInvalid' => 'Esi nun ye un códigu d\'identidá válidu',
|
||||||
|
'wot.remove' => 'Quitar',
|
||||||
|
'wot.params' => 'Parámetros avanzaos',
|
||||||
|
'wot.paramsHelp' => 'Regles Duniter. Aflóxales pa una rede nueva; apriétales según crez.',
|
||||||
|
'wot.sigQty' => 'Avales pa ser miembru',
|
||||||
|
'wot.stepMax' => 'Distancia máxima a una raíz',
|
||||||
|
'wot.validityDays' => 'Validez del aval (díes)',
|
||||||
|
'wot.reset' => 'Reafitar a valores Duniter',
|
||||||
|
'wot.saved' => 'Guardáu',
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
||||||
late final Translations$chatList$en chatList = Translations$chatList$en.internal(_root);
|
late final Translations$chatList$en chatList = Translations$chatList$en.internal(_root);
|
||||||
late final Translations$chat$en chat = Translations$chat$en.internal(_root);
|
late final Translations$chat$en chat = Translations$chat$en.internal(_root);
|
||||||
late final Translations$trust$en trust = Translations$trust$en.internal(_root);
|
late final Translations$trust$en trust = Translations$trust$en.internal(_root);
|
||||||
|
late final Translations$wot$en wot = Translations$wot$en.internal(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: app
|
// Path: app
|
||||||
|
|
@ -1423,6 +1424,75 @@ class Translations$trust$en {
|
||||||
|
|
||||||
/// en: 'In your circle'
|
/// en: 'In your circle'
|
||||||
String get circle => 'In your circle';
|
String get circle => 'In your circle';
|
||||||
|
|
||||||
|
/// en: 'Trusted member of the network'
|
||||||
|
String get member => 'Trusted member of the network';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: wot
|
||||||
|
class Translations$wot$en {
|
||||||
|
Translations$wot$en.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'Network of trust'
|
||||||
|
String get title => 'Network of trust';
|
||||||
|
|
||||||
|
/// en: 'Network of trust'
|
||||||
|
String get open => 'Network of trust';
|
||||||
|
|
||||||
|
/// en: 'How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.'
|
||||||
|
String get help => 'How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.';
|
||||||
|
|
||||||
|
/// en: 'Trust roots'
|
||||||
|
String get roots => 'Trust roots';
|
||||||
|
|
||||||
|
/// en: 'The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.'
|
||||||
|
String get rootsHelp => 'The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.';
|
||||||
|
|
||||||
|
/// en: 'No trust roots yet — membership can't be worked out until the network is seeded.'
|
||||||
|
String get noRoots => 'No trust roots yet — membership can\'t be worked out until the network is seeded.';
|
||||||
|
|
||||||
|
/// en: 'Add a trust root'
|
||||||
|
String get addRoot => 'Add a trust root';
|
||||||
|
|
||||||
|
/// en: 'Paste an identity code (npub…)'
|
||||||
|
String get rootHint => 'Paste an identity code (npub…)';
|
||||||
|
|
||||||
|
/// en: 'Add'
|
||||||
|
String get add => 'Add';
|
||||||
|
|
||||||
|
/// en: 'Trust root added'
|
||||||
|
String get rootAdded => 'Trust root added';
|
||||||
|
|
||||||
|
/// en: 'That's not a valid identity code'
|
||||||
|
String get rootInvalid => 'That\'s not a valid identity code';
|
||||||
|
|
||||||
|
/// en: 'Remove'
|
||||||
|
String get remove => 'Remove';
|
||||||
|
|
||||||
|
/// en: 'Advanced parameters'
|
||||||
|
String get params => 'Advanced parameters';
|
||||||
|
|
||||||
|
/// en: 'Duniter rules. Loosen them for a young network; tighten as it grows.'
|
||||||
|
String get paramsHelp => 'Duniter rules. Loosen them for a young network; tighten as it grows.';
|
||||||
|
|
||||||
|
/// en: 'Vouches to become a member'
|
||||||
|
String get sigQty => 'Vouches to become a member';
|
||||||
|
|
||||||
|
/// en: 'Max distance from a root'
|
||||||
|
String get stepMax => 'Max distance from a root';
|
||||||
|
|
||||||
|
/// en: 'Vouch validity (days)'
|
||||||
|
String get validityDays => 'Vouch validity (days)';
|
||||||
|
|
||||||
|
/// en: 'Reset to Duniter defaults'
|
||||||
|
String get reset => 'Reset to Duniter defaults';
|
||||||
|
|
||||||
|
/// en: 'Saved'
|
||||||
|
String get saved => 'Saved';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: intro.slides
|
// Path: intro.slides
|
||||||
|
|
@ -2279,6 +2349,26 @@ extension on Translations {
|
||||||
'trust.vouch' => 'I know this person',
|
'trust.vouch' => 'I know this person',
|
||||||
'trust.vouched' => 'You vouch for them',
|
'trust.vouched' => 'You vouch for them',
|
||||||
'trust.circle' => 'In your circle',
|
'trust.circle' => 'In your circle',
|
||||||
|
'trust.member' => 'Trusted member of the network',
|
||||||
|
'wot.title' => 'Network of trust',
|
||||||
|
'wot.open' => 'Network of trust',
|
||||||
|
'wot.help' => 'How trusted membership is decided: people vouch for each other, and trust spreads outward from a set of founding roots.',
|
||||||
|
'wot.roots' => 'Trust roots',
|
||||||
|
'wot.rootsHelp' => 'The founding identities the network is anchored to. Add people you trust (paste their identity code); membership is measured outward from them.',
|
||||||
|
'wot.noRoots' => 'No trust roots yet — membership can\'t be worked out until the network is seeded.',
|
||||||
|
'wot.addRoot' => 'Add a trust root',
|
||||||
|
'wot.rootHint' => 'Paste an identity code (npub…)',
|
||||||
|
'wot.add' => 'Add',
|
||||||
|
'wot.rootAdded' => 'Trust root added',
|
||||||
|
'wot.rootInvalid' => 'That\'s not a valid identity code',
|
||||||
|
'wot.remove' => 'Remove',
|
||||||
|
'wot.params' => 'Advanced parameters',
|
||||||
|
'wot.paramsHelp' => 'Duniter rules. Loosen them for a young network; tighten as it grows.',
|
||||||
|
'wot.sigQty' => 'Vouches to become a member',
|
||||||
|
'wot.stepMax' => 'Max distance from a root',
|
||||||
|
'wot.validityDays' => 'Vouch validity (days)',
|
||||||
|
'wot.reset' => 'Reset to Duniter defaults',
|
||||||
|
'wot.saved' => 'Saved',
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
||||||
@override late final _Translations$chatList$es chatList = _Translations$chatList$es._(_root);
|
@override late final _Translations$chatList$es chatList = _Translations$chatList$es._(_root);
|
||||||
@override late final _Translations$chat$es chat = _Translations$chat$es._(_root);
|
@override late final _Translations$chat$es chat = _Translations$chat$es._(_root);
|
||||||
@override late final _Translations$trust$es trust = _Translations$trust$es._(_root);
|
@override late final _Translations$trust$es trust = _Translations$trust$es._(_root);
|
||||||
|
@override late final _Translations$wot$es wot = _Translations$wot$es._(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: app
|
// Path: app
|
||||||
|
|
@ -764,6 +765,35 @@ class _Translations$trust$es extends Translations$trust$en {
|
||||||
@override String get vouch => 'Conozco a esta persona';
|
@override String get vouch => 'Conozco a esta persona';
|
||||||
@override String get vouched => 'Avalas a esta persona';
|
@override String get vouched => 'Avalas a esta persona';
|
||||||
@override String get circle => 'En tu círculo';
|
@override String get circle => 'En tu círculo';
|
||||||
|
@override String get member => 'Miembro de confianza de la red';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: wot
|
||||||
|
class _Translations$wot$es extends Translations$wot$en {
|
||||||
|
_Translations$wot$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Red de confianza';
|
||||||
|
@override String get open => 'Red de confianza';
|
||||||
|
@override String get help => 'Cómo se decide quién es miembro de confianza: la gente se avala entre sí, y la confianza se extiende desde un conjunto de raíces fundadoras.';
|
||||||
|
@override String get roots => 'Raíces de confianza';
|
||||||
|
@override String get rootsHelp => 'Las identidades fundadoras que anclan la red. Añade a quien te fíes (pega su código de identidad); la membresía se mide hacia fuera desde ellas.';
|
||||||
|
@override String get noRoots => 'Aún no hay raíces de confianza — no se puede calcular la membresía hasta sembrar la red.';
|
||||||
|
@override String get addRoot => 'Añadir una raíz de confianza';
|
||||||
|
@override String get rootHint => 'Pega un código de identidad (npub…)';
|
||||||
|
@override String get add => 'Añadir';
|
||||||
|
@override String get rootAdded => 'Raíz de confianza añadida';
|
||||||
|
@override String get rootInvalid => 'Ese no es un código de identidad válido';
|
||||||
|
@override String get remove => 'Quitar';
|
||||||
|
@override String get params => 'Parámetros avanzados';
|
||||||
|
@override String get paramsHelp => 'Reglas Duniter. Aflójalas para una red joven; apriétalas según crece.';
|
||||||
|
@override String get sigQty => 'Avales para ser miembro';
|
||||||
|
@override String get stepMax => 'Distancia máxima a una raíz';
|
||||||
|
@override String get validityDays => 'Validez del aval (días)';
|
||||||
|
@override String get reset => 'Restablecer a valores Duniter';
|
||||||
|
@override String get saved => 'Guardado';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: intro.slides
|
// Path: intro.slides
|
||||||
|
|
@ -1503,6 +1533,26 @@ extension on TranslationsEs {
|
||||||
'trust.vouch' => 'Conozco a esta persona',
|
'trust.vouch' => 'Conozco a esta persona',
|
||||||
'trust.vouched' => 'Avalas a esta persona',
|
'trust.vouched' => 'Avalas a esta persona',
|
||||||
'trust.circle' => 'En tu círculo',
|
'trust.circle' => 'En tu círculo',
|
||||||
|
'trust.member' => 'Miembro de confianza de la red',
|
||||||
|
'wot.title' => 'Red de confianza',
|
||||||
|
'wot.open' => 'Red de confianza',
|
||||||
|
'wot.help' => 'Cómo se decide quién es miembro de confianza: la gente se avala entre sí, y la confianza se extiende desde un conjunto de raíces fundadoras.',
|
||||||
|
'wot.roots' => 'Raíces de confianza',
|
||||||
|
'wot.rootsHelp' => 'Las identidades fundadoras que anclan la red. Añade a quien te fíes (pega su código de identidad); la membresía se mide hacia fuera desde ellas.',
|
||||||
|
'wot.noRoots' => 'Aún no hay raíces de confianza — no se puede calcular la membresía hasta sembrar la red.',
|
||||||
|
'wot.addRoot' => 'Añadir una raíz de confianza',
|
||||||
|
'wot.rootHint' => 'Pega un código de identidad (npub…)',
|
||||||
|
'wot.add' => 'Añadir',
|
||||||
|
'wot.rootAdded' => 'Raíz de confianza añadida',
|
||||||
|
'wot.rootInvalid' => 'Ese no es un código de identidad válido',
|
||||||
|
'wot.remove' => 'Quitar',
|
||||||
|
'wot.params' => 'Parámetros avanzados',
|
||||||
|
'wot.paramsHelp' => 'Reglas Duniter. Aflójalas para una red joven; apriétalas según crece.',
|
||||||
|
'wot.sigQty' => 'Avales para ser miembro',
|
||||||
|
'wot.stepMax' => 'Distancia máxima a una raíz',
|
||||||
|
'wot.validityDays' => 'Validez del aval (días)',
|
||||||
|
'wot.reset' => 'Restablecer a valores Duniter',
|
||||||
|
'wot.saved' => 'Guardado',
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ class TranslationsPt extends Translations with BaseTranslations<AppLocale, Trans
|
||||||
@override late final _Translations$chatList$pt chatList = _Translations$chatList$pt._(_root);
|
@override late final _Translations$chatList$pt chatList = _Translations$chatList$pt._(_root);
|
||||||
@override late final _Translations$chat$pt chat = _Translations$chat$pt._(_root);
|
@override late final _Translations$chat$pt chat = _Translations$chat$pt._(_root);
|
||||||
@override late final _Translations$trust$pt trust = _Translations$trust$pt._(_root);
|
@override late final _Translations$trust$pt trust = _Translations$trust$pt._(_root);
|
||||||
|
@override late final _Translations$wot$pt wot = _Translations$wot$pt._(_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: app
|
// Path: app
|
||||||
|
|
@ -761,6 +762,35 @@ class _Translations$trust$pt extends Translations$trust$en {
|
||||||
@override String get vouch => 'Conheço esta pessoa';
|
@override String get vouch => 'Conheço esta pessoa';
|
||||||
@override String get vouched => 'Avalizas esta pessoa';
|
@override String get vouched => 'Avalizas esta pessoa';
|
||||||
@override String get circle => 'No teu círculo';
|
@override String get circle => 'No teu círculo';
|
||||||
|
@override String get member => 'Membro de confiança da rede';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path: wot
|
||||||
|
class _Translations$wot$pt extends Translations$wot$en {
|
||||||
|
_Translations$wot$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsPt _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Rede de confiança';
|
||||||
|
@override String get open => 'Rede de confiança';
|
||||||
|
@override String get help => 'Como se decide quem é membro de confiança: as pessoas avalizam-se entre si, e a confiança estende-se a partir de um conjunto de raízes fundadoras.';
|
||||||
|
@override String get roots => 'Raízes de confiança';
|
||||||
|
@override String get rootsHelp => 'As identidades fundadoras que ancoram a rede. Adiciona quem confias (cola o seu código de identidade); a filiação mede-se para fora a partir delas.';
|
||||||
|
@override String get noRoots => 'Ainda não há raízes de confiança — não se pode calcular a filiação até semear a rede.';
|
||||||
|
@override String get addRoot => 'Adicionar uma raiz de confiança';
|
||||||
|
@override String get rootHint => 'Cola um código de identidade (npub…)';
|
||||||
|
@override String get add => 'Adicionar';
|
||||||
|
@override String get rootAdded => 'Raiz de confiança adicionada';
|
||||||
|
@override String get rootInvalid => 'Esse não é um código de identidade válido';
|
||||||
|
@override String get remove => 'Remover';
|
||||||
|
@override String get params => 'Parâmetros avançados';
|
||||||
|
@override String get paramsHelp => 'Regras Duniter. Afrouxa-as para uma rede jovem; aperta-as à medida que cresce.';
|
||||||
|
@override String get sigQty => 'Avais para ser membro';
|
||||||
|
@override String get stepMax => 'Distância máxima a uma raiz';
|
||||||
|
@override String get validityDays => 'Validade do aval (dias)';
|
||||||
|
@override String get reset => 'Repor para valores Duniter';
|
||||||
|
@override String get saved => 'Guardado';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path: intro.slides
|
// Path: intro.slides
|
||||||
|
|
@ -1497,6 +1527,26 @@ extension on TranslationsPt {
|
||||||
'trust.vouch' => 'Conheço esta pessoa',
|
'trust.vouch' => 'Conheço esta pessoa',
|
||||||
'trust.vouched' => 'Avalizas esta pessoa',
|
'trust.vouched' => 'Avalizas esta pessoa',
|
||||||
'trust.circle' => 'No teu círculo',
|
'trust.circle' => 'No teu círculo',
|
||||||
|
'trust.member' => 'Membro de confiança da rede',
|
||||||
|
'wot.title' => 'Rede de confiança',
|
||||||
|
'wot.open' => 'Rede de confiança',
|
||||||
|
'wot.help' => 'Como se decide quem é membro de confiança: as pessoas avalizam-se entre si, e a confiança estende-se a partir de um conjunto de raízes fundadoras.',
|
||||||
|
'wot.roots' => 'Raízes de confiança',
|
||||||
|
'wot.rootsHelp' => 'As identidades fundadoras que ancoram a rede. Adiciona quem confias (cola o seu código de identidade); a filiação mede-se para fora a partir delas.',
|
||||||
|
'wot.noRoots' => 'Ainda não há raízes de confiança — não se pode calcular a filiação até semear a rede.',
|
||||||
|
'wot.addRoot' => 'Adicionar uma raiz de confiança',
|
||||||
|
'wot.rootHint' => 'Cola um código de identidade (npub…)',
|
||||||
|
'wot.add' => 'Adicionar',
|
||||||
|
'wot.rootAdded' => 'Raiz de confiança adicionada',
|
||||||
|
'wot.rootInvalid' => 'Esse não é um código de identidade válido',
|
||||||
|
'wot.remove' => 'Remover',
|
||||||
|
'wot.params' => 'Parâmetros avançados',
|
||||||
|
'wot.paramsHelp' => 'Regras Duniter. Afrouxa-as para uma rede jovem; aperta-as à medida que cresce.',
|
||||||
|
'wot.sigQty' => 'Avais para ser membro',
|
||||||
|
'wot.stepMax' => 'Distância máxima a uma raiz',
|
||||||
|
'wot.validityDays' => 'Validade do aval (dias)',
|
||||||
|
'wot.reset' => 'Repor para valores Duniter',
|
||||||
|
'wot.saved' => 'Guardado',
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
73
apps/app_seeds/lib/services/trust_referents.dart
Normal file
73
apps/app_seeds/lib/services/trust_referents.dart
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:commons_core/commons_core.dart';
|
||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
|
||||||
|
import '../security/secret_store.dart';
|
||||||
|
|
||||||
|
/// The web-of-trust bootstrap referents (Duniter "seeds"): identities every
|
||||||
|
/// membership calculation trusts by construction. The network's roots.
|
||||||
|
///
|
||||||
|
/// Sources, unioned: a bundled asset (`assets/trust/referents.json`, a curated
|
||||||
|
/// founding set — empty until real founders are added) plus referents the user
|
||||||
|
/// adds in-app (by npub / QR), kept in the keystore. All keys are stored and
|
||||||
|
/// returned as 64-char hex. Solving cold-start honestly: no invented identities
|
||||||
|
/// ship, and a user can bootstrap their own roots.
|
||||||
|
class TrustReferents {
|
||||||
|
TrustReferents(this._store, {this.assetPath = 'assets/trust/referents.json'});
|
||||||
|
|
||||||
|
final SecretStore _store;
|
||||||
|
final String assetPath;
|
||||||
|
|
||||||
|
static const _userKey = 'tane.social.trust.referents';
|
||||||
|
|
||||||
|
List<String>? _bundledCache;
|
||||||
|
|
||||||
|
/// Every referent (bundled ∪ user-added), as hex public keys.
|
||||||
|
Future<Set<String>> all() async =>
|
||||||
|
{...await _bundled(), ...await userAdded()};
|
||||||
|
|
||||||
|
/// Referents the user added themselves (hex), for the management UI.
|
||||||
|
Future<Set<String>> userAdded() async {
|
||||||
|
final raw = await _store.read(_userKey);
|
||||||
|
if (raw == null || raw.isEmpty) return {};
|
||||||
|
return raw.split('\n').where((s) => s.isNotEmpty).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a referent from an [npubOrHex]; returns its hex form. Idempotent.
|
||||||
|
/// Throws [FormatException] if it is neither an npub nor a hex key.
|
||||||
|
Future<String> add(String npubOrHex) async {
|
||||||
|
final hex = npubToHex(npubOrHex); // validates
|
||||||
|
final current = await userAdded();
|
||||||
|
if (current.add(hex)) {
|
||||||
|
await _store.write(_userKey, current.join('\n'));
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a user-added referent (bundled ones can't be removed).
|
||||||
|
Future<void> remove(String hex) async {
|
||||||
|
final current = await userAdded()..remove(hex);
|
||||||
|
await _store.write(_userKey, current.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> _bundled() async {
|
||||||
|
final cached = _bundledCache;
|
||||||
|
if (cached != null) return cached;
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(await rootBundle.loadString(assetPath));
|
||||||
|
final list = (decoded is Map ? decoded['referents'] : decoded) as List?;
|
||||||
|
final out = <String>[];
|
||||||
|
for (final entry in list ?? const []) {
|
||||||
|
try {
|
||||||
|
out.add(npubToHex(entry as String));
|
||||||
|
} catch (_) {
|
||||||
|
// Skip a malformed entry rather than break the whole set.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _bundledCache = out;
|
||||||
|
} catch (_) {
|
||||||
|
return _bundledCache = const []; // no/invalid asset → no bundled referents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
apps/app_seeds/lib/services/wot_settings.dart
Normal file
45
apps/app_seeds/lib/services/wot_settings.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import 'package:commons_core/commons_core.dart';
|
||||||
|
|
||||||
|
import '../security/secret_store.dart';
|
||||||
|
|
||||||
|
/// Persists the web-of-trust parameters (Duniter sigQty / stepMax / validity),
|
||||||
|
/// so a young network can loosen them and tighten as it grows — exactly how Ğ1
|
||||||
|
/// itself evolved. Defaults to [WotParams.duniter]. Keystore-backed.
|
||||||
|
class WotSettings {
|
||||||
|
WotSettings(this._store);
|
||||||
|
|
||||||
|
final SecretStore _store;
|
||||||
|
|
||||||
|
static const _sigQtyKey = 'tane.social.wot.sigQty';
|
||||||
|
static const _stepMaxKey = 'tane.social.wot.stepMax';
|
||||||
|
static const _validityDaysKey = 'tane.social.wot.validityDays';
|
||||||
|
|
||||||
|
/// The active parameters (any unset field falls back to the Duniter default).
|
||||||
|
Future<WotParams> params() async {
|
||||||
|
const base = WotParams.duniter;
|
||||||
|
final sigQty = await _readInt(_sigQtyKey) ?? base.sigQty;
|
||||||
|
final stepMax = await _readInt(_stepMaxKey) ?? base.stepMax;
|
||||||
|
final validityDays =
|
||||||
|
await _readInt(_validityDaysKey) ?? base.sigValidity.inDays;
|
||||||
|
return WotParams(
|
||||||
|
sigQty: sigQty,
|
||||||
|
stepMax: stepMax,
|
||||||
|
sigValidity: Duration(days: validityDays),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> save(WotParams params) async {
|
||||||
|
await _store.write(_sigQtyKey, '${params.sigQty}');
|
||||||
|
await _store.write(_stepMaxKey, '${params.stepMax}');
|
||||||
|
await _store.write(_validityDaysKey, '${params.sigValidity.inDays}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores the Ğ1 reference values.
|
||||||
|
Future<void> reset() => save(WotParams.duniter);
|
||||||
|
|
||||||
|
Future<int?> _readInt(String key) async {
|
||||||
|
final raw = await _store.read(key);
|
||||||
|
if (raw == null || raw.isEmpty) return null;
|
||||||
|
return int.tryParse(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,38 +5,72 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../services/social_service.dart';
|
import '../services/social_service.dart';
|
||||||
import '../services/social_settings.dart';
|
import '../services/social_settings.dart';
|
||||||
|
|
||||||
/// Trust standing of one peer: how many people vouch for them, and whether YOU
|
/// Where a peer stands, from strongest to weakest signal.
|
||||||
/// do. Kept simple on purpose — the full "known member" rule (threshold +
|
enum TrustTier {
|
||||||
/// distance) and its bootstrap set are a policy decision still open
|
/// Passes the full Duniter membership rule (sigQty certifications from members,
|
||||||
/// (network-trust.md §2). Vouching + a live count is the useful, honest first
|
/// within stepMax of a bootstrap referent).
|
||||||
/// step.
|
networkMember,
|
||||||
|
|
||||||
|
/// In your personal circle (you vouch, or a friend-of-a-friend does).
|
||||||
|
inYourCircle,
|
||||||
|
|
||||||
|
/// Vouched for by someone, but not (yet) a member nor in your circle.
|
||||||
|
vouched,
|
||||||
|
|
||||||
|
/// No certifications seen.
|
||||||
|
unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trust standing of one peer: the full Duniter membership verdict, your own
|
||||||
|
/// circle, and the raw certifier count — computed from the public certification
|
||||||
|
/// graph with the active [WotParams] and bootstrap referents.
|
||||||
class TrustState extends Equatable {
|
class TrustState extends Equatable {
|
||||||
const TrustState({
|
const TrustState({
|
||||||
this.certifierCount = 0,
|
this.certifierCount = 0,
|
||||||
this.iVouch = false,
|
this.iVouch = false,
|
||||||
this.knownToYou = false,
|
this.knownToYou = false,
|
||||||
|
this.isNetworkMember = false,
|
||||||
|
this.networkBootstrapped = false,
|
||||||
this.loading = true,
|
this.loading = true,
|
||||||
this.busy = false,
|
this.busy = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// How many people (network-wide) vouch for the peer.
|
/// How many people (network-wide) currently vouch for the peer.
|
||||||
final int certifierCount;
|
final int certifierCount;
|
||||||
|
|
||||||
/// Whether this user vouches for the peer.
|
/// Whether this user vouches for the peer.
|
||||||
final bool iVouch;
|
final bool iVouch;
|
||||||
|
|
||||||
/// Whether the peer is within the user's own circle of trust — you vouch for
|
/// Whether the peer is within the user's own circle (you, or a
|
||||||
/// them, or someone you vouch for does (friend-of-a-friend). This is the
|
/// friend-of-a-friend, vouch). Spam-resistant and works from day one.
|
||||||
/// meaningful signal; a raw network count invites spam.
|
|
||||||
final bool knownToYou;
|
final bool knownToYou;
|
||||||
|
|
||||||
|
/// Whether the peer is a full member per the Duniter rule (sigQty + stepMax
|
||||||
|
/// from the bootstrap referents).
|
||||||
|
final bool isNetworkMember;
|
||||||
|
|
||||||
|
/// Whether any bootstrap referents exist at all. When false, network
|
||||||
|
/// membership can't be determined yet (the trust net isn't seeded) and the UI
|
||||||
|
/// should say so instead of implying the peer failed the rule.
|
||||||
|
final bool networkBootstrapped;
|
||||||
|
|
||||||
final bool loading;
|
final bool loading;
|
||||||
final bool busy;
|
final bool busy;
|
||||||
|
|
||||||
|
/// The strongest applicable signal, for the badge.
|
||||||
|
TrustTier get tier {
|
||||||
|
if (isNetworkMember) return TrustTier.networkMember;
|
||||||
|
if (knownToYou) return TrustTier.inYourCircle;
|
||||||
|
if (certifierCount > 0) return TrustTier.vouched;
|
||||||
|
return TrustTier.unknown;
|
||||||
|
}
|
||||||
|
|
||||||
TrustState copyWith({
|
TrustState copyWith({
|
||||||
int? certifierCount,
|
int? certifierCount,
|
||||||
bool? iVouch,
|
bool? iVouch,
|
||||||
bool? knownToYou,
|
bool? knownToYou,
|
||||||
|
bool? isNetworkMember,
|
||||||
|
bool? networkBootstrapped,
|
||||||
bool? loading,
|
bool? loading,
|
||||||
bool? busy,
|
bool? busy,
|
||||||
}) =>
|
}) =>
|
||||||
|
|
@ -44,21 +78,34 @@ class TrustState extends Equatable {
|
||||||
certifierCount: certifierCount ?? this.certifierCount,
|
certifierCount: certifierCount ?? this.certifierCount,
|
||||||
iVouch: iVouch ?? this.iVouch,
|
iVouch: iVouch ?? this.iVouch,
|
||||||
knownToYou: knownToYou ?? this.knownToYou,
|
knownToYou: knownToYou ?? this.knownToYou,
|
||||||
|
isNetworkMember: isNetworkMember ?? this.isNetworkMember,
|
||||||
|
networkBootstrapped: networkBootstrapped ?? this.networkBootstrapped,
|
||||||
loading: loading ?? this.loading,
|
loading: loading ?? this.loading,
|
||||||
busy: busy ?? this.busy,
|
busy: busy ?? this.busy,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props =>
|
List<Object?> get props => [
|
||||||
[certifierCount, iVouch, knownToYou, loading, busy];
|
certifierCount,
|
||||||
|
iVouch,
|
||||||
|
knownToYou,
|
||||||
|
isNetworkMember,
|
||||||
|
networkBootstrapped,
|
||||||
|
loading,
|
||||||
|
busy,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport].
|
/// Reads and toggles this user's vouch for [peerPubkey] over a [TrustTransport],
|
||||||
|
/// and computes the full Duniter membership verdict against the bootstrap
|
||||||
|
/// [referents] and active [params].
|
||||||
class TrustCubit extends Cubit<TrustState> {
|
class TrustCubit extends Cubit<TrustState> {
|
||||||
TrustCubit(
|
TrustCubit(
|
||||||
this._transport, {
|
this._transport, {
|
||||||
required this.peerPubkey,
|
required this.peerPubkey,
|
||||||
required this.selfPubkey,
|
required this.selfPubkey,
|
||||||
|
this.referents = const {},
|
||||||
|
this.params = WotParams.duniter,
|
||||||
Future<void> Function()? onDispose,
|
Future<void> Function()? onDispose,
|
||||||
}) : _onDispose = onDispose,
|
}) : _onDispose = onDispose,
|
||||||
super(const TrustState());
|
super(const TrustState());
|
||||||
|
|
@ -66,17 +113,26 @@ class TrustCubit extends Cubit<TrustState> {
|
||||||
final TrustTransport? _transport;
|
final TrustTransport? _transport;
|
||||||
final String peerPubkey;
|
final String peerPubkey;
|
||||||
final String selfPubkey;
|
final String selfPubkey;
|
||||||
|
|
||||||
|
/// Bootstrap referents (Duniter seeds) the membership rule trusts by
|
||||||
|
/// construction. Empty = the network isn't seeded yet.
|
||||||
|
final Set<String> referents;
|
||||||
|
|
||||||
|
/// Active web-of-trust parameters (sigQty / stepMax / validity).
|
||||||
|
final WotParams params;
|
||||||
final Future<void> Function()? _onDispose;
|
final Future<void> Function()? _onDispose;
|
||||||
|
|
||||||
bool get isOnline => _transport != null;
|
bool get isOnline => _transport != null;
|
||||||
|
|
||||||
/// Bootstrap "circle" rule: one vouch from your side, out to a friend-of-a-
|
/// Personal "circle" rule: one vouch from your side, out to a friend-of-a-
|
||||||
/// friend. Deliberately loose (the full member policy — threshold/distance —
|
/// friend. Loose by design — "people near you", separate from formal
|
||||||
/// is still open, network-trust.md §2); this is just "people near you".
|
/// membership.
|
||||||
static const _circleThreshold = 1;
|
static const _circleThreshold = 1;
|
||||||
static const _circleDistance = 2;
|
static const _circleDistance = 2;
|
||||||
|
|
||||||
/// Loads the peer's certifiers and whether they're within your circle.
|
/// Loads the certification graph and computes: the peer's certifier count,
|
||||||
|
/// whether you vouch, whether they're in your circle, and whether they pass
|
||||||
|
/// the full Duniter membership rule against the bootstrap referents.
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final transport = _transport;
|
final transport = _transport;
|
||||||
if (transport == null) {
|
if (transport == null) {
|
||||||
|
|
@ -94,15 +150,23 @@ class TrustCubit extends Cubit<TrustState> {
|
||||||
threshold: _circleThreshold,
|
threshold: _circleThreshold,
|
||||||
maxDistance: _circleDistance,
|
maxDistance: _circleDistance,
|
||||||
);
|
);
|
||||||
|
// Full membership: only meaningful once the trust net has referents.
|
||||||
|
final members = referents.isEmpty
|
||||||
|
? const <String>{}
|
||||||
|
: wot.membersWith(seeds: referents, params: params);
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
certifierCount: certifiers.length,
|
certifierCount: certifiers.length,
|
||||||
iVouch: certifiers.contains(selfPubkey),
|
iVouch: certifiers.contains(selfPubkey),
|
||||||
knownToYou: circle.contains(peerPubkey),
|
knownToYou: circle.contains(peerPubkey),
|
||||||
|
isNetworkMember: members.contains(peerPubkey),
|
||||||
|
networkBootstrapped: referents.isNotEmpty,
|
||||||
loading: false,
|
loading: false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds or removes this user's vouch, then reloads. Never vouches for self.
|
/// Adds or removes this user's vouch, then reloads. Never vouches for self.
|
||||||
|
/// The certification is issued with the active validity so it expires and
|
||||||
|
/// must be renewed (Duniter rule).
|
||||||
Future<void> toggleVouch() async {
|
Future<void> toggleVouch() async {
|
||||||
final transport = _transport;
|
final transport = _transport;
|
||||||
if (transport == null || peerPubkey == selfPubkey) return;
|
if (transport == null || peerPubkey == selfPubkey) return;
|
||||||
|
|
@ -110,7 +174,10 @@ class TrustCubit extends Cubit<TrustState> {
|
||||||
if (state.iVouch) {
|
if (state.iVouch) {
|
||||||
await transport.revoke(subjectPubkey: peerPubkey);
|
await transport.revoke(subjectPubkey: peerPubkey);
|
||||||
} else {
|
} else {
|
||||||
await transport.certify(subjectPubkey: peerPubkey);
|
await transport.certify(
|
||||||
|
subjectPubkey: peerPubkey,
|
||||||
|
validity: params.sigValidity,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await load();
|
await load();
|
||||||
emit(state.copyWith(busy: false));
|
emit(state.copyWith(busy: false));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:commons_core/commons_core.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
@ -10,6 +11,8 @@ import '../services/message_store.dart';
|
||||||
import '../services/profile_cache.dart';
|
import '../services/profile_cache.dart';
|
||||||
import '../services/social_service.dart';
|
import '../services/social_service.dart';
|
||||||
import '../services/social_settings.dart';
|
import '../services/social_settings.dart';
|
||||||
|
import '../services/trust_referents.dart';
|
||||||
|
import '../services/wot_settings.dart';
|
||||||
import '../state/messages_cubit.dart';
|
import '../state/messages_cubit.dart';
|
||||||
import '../state/trust_cubit.dart';
|
import '../state/trust_cubit.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
@ -24,6 +27,8 @@ class ChatScreen extends StatefulWidget {
|
||||||
required this.peerPubkey,
|
required this.peerPubkey,
|
||||||
this.messageStore,
|
this.messageStore,
|
||||||
this.profileCache,
|
this.profileCache,
|
||||||
|
this.trustReferents,
|
||||||
|
this.wotSettings,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -37,6 +42,10 @@ class ChatScreen extends StatefulWidget {
|
||||||
/// Optional cache of peer display names; null in tests.
|
/// Optional cache of peer display names; null in tests.
|
||||||
final ProfileCache? profileCache;
|
final ProfileCache? profileCache;
|
||||||
|
|
||||||
|
/// Web-of-trust bootstrap referents + parameters, for the membership verdict.
|
||||||
|
final TrustReferents? trustReferents;
|
||||||
|
final WotSettings? wotSettings;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChatScreen> createState() => _ChatScreenState();
|
State<ChatScreen> createState() => _ChatScreenState();
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +78,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final cachedName = await widget.profileCache?.name(widget.peerPubkey);
|
final cachedName = await widget.profileCache?.name(widget.peerPubkey);
|
||||||
|
final referents = await widget.trustReferents?.all() ?? const <String>{};
|
||||||
|
final wotParams =
|
||||||
|
await widget.wotSettings?.params() ?? WotParams.duniter;
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
await session?.close();
|
await session?.close();
|
||||||
return;
|
return;
|
||||||
|
|
@ -80,8 +92,13 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||||
selfPubkey: self,
|
selfPubkey: self,
|
||||||
store: widget.messageStore,
|
store: widget.messageStore,
|
||||||
)..start();
|
)..start();
|
||||||
final trust = TrustCubit(session?.trust,
|
final trust = TrustCubit(
|
||||||
peerPubkey: widget.peerPubkey, selfPubkey: self);
|
session?.trust,
|
||||||
|
peerPubkey: widget.peerPubkey,
|
||||||
|
selfPubkey: self,
|
||||||
|
referents: referents,
|
||||||
|
params: wotParams,
|
||||||
|
);
|
||||||
unawaited(trust.load());
|
unawaited(trust.load());
|
||||||
setState(() {
|
setState(() {
|
||||||
_session = session;
|
_session = session;
|
||||||
|
|
@ -249,30 +266,33 @@ class _TrustBanner extends StatelessWidget {
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final cubit = context.read<TrustCubit>();
|
final cubit = context.read<TrustCubit>();
|
||||||
if (!cubit.isOnline || state.loading) return const SizedBox.shrink();
|
if (!cubit.isOnline || state.loading) return const SizedBox.shrink();
|
||||||
final known = state.knownToYou;
|
// Strongest applicable signal decides the badge (member > circle >
|
||||||
|
// vouched > unknown).
|
||||||
|
final (IconData icon, String label, bool strong) = switch (state.tier) {
|
||||||
|
TrustTier.networkMember => (Icons.verified, t.trust.member, true),
|
||||||
|
TrustTier.inYourCircle => (Icons.verified_user, t.trust.circle, true),
|
||||||
|
TrustTier.vouched => (
|
||||||
|
Icons.people_outline,
|
||||||
|
t.trust.count(n: state.certifierCount),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
TrustTier.unknown => (Icons.person_outline, t.trust.none, false),
|
||||||
|
};
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: seedPrimaryContainer.withValues(alpha: 0.5),
|
color: seedPrimaryContainer.withValues(alpha: 0.5),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(icon, size: 18, color: strong ? seedGreen : seedMuted),
|
||||||
known ? Icons.verified : Icons.verified_user_outlined,
|
|
||||||
size: 18,
|
|
||||||
color: seedGreen,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
known
|
label,
|
||||||
? t.trust.circle
|
|
||||||
: state.certifierCount == 0
|
|
||||||
? t.trust.none
|
|
||||||
: t.trust.count(n: state.certifierCount),
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: known ? seedGreen : seedOnSurface,
|
color: strong ? seedGreen : seedOnSurface,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: known ? FontWeight.w600 : FontWeight.w400,
|
fontWeight: strong ? FontWeight.w600 : FontWeight.w400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../di/injector.dart' show switchSocialAccount;
|
import '../di/injector.dart' show switchSocialAccount;
|
||||||
import '../i18n/strings.g.dart';
|
import '../i18n/strings.g.dart';
|
||||||
|
|
@ -21,6 +22,7 @@ class ProfileScreen extends StatefulWidget {
|
||||||
required this.settings,
|
required this.settings,
|
||||||
required this.profileStore,
|
required this.profileStore,
|
||||||
required this.accounts,
|
required this.accounts,
|
||||||
|
this.trustNetworkEnabled = false,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -29,6 +31,9 @@ class ProfileScreen extends StatefulWidget {
|
||||||
final ProfileStore profileStore;
|
final ProfileStore profileStore;
|
||||||
final SocialAccountStore accounts;
|
final SocialAccountStore accounts;
|
||||||
|
|
||||||
|
/// Whether to offer the "Network of trust" entry (needs the WoT services).
|
||||||
|
final bool trustNetworkEnabled;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ProfileScreen> createState() => _ProfileScreenState();
|
State<ProfileScreen> createState() => _ProfileScreenState();
|
||||||
}
|
}
|
||||||
|
|
@ -207,6 +212,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
onSwitch: (a) => _switchTo(a),
|
onSwitch: (a) => _switchTo(a),
|
||||||
onNew: _newIdentity,
|
onNew: _newIdentity,
|
||||||
),
|
),
|
||||||
|
if (widget.trustNetworkEnabled) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
key: const Key('profile.trustNetwork'),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: const Icon(Icons.hub_outlined, color: seedGreen),
|
||||||
|
title: Text(t.wot.open),
|
||||||
|
trailing: const Icon(Icons.chevron_right, color: seedMuted),
|
||||||
|
onTap: () => context.push('/trust'),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
228
apps/app_seeds/lib/ui/trust_network_screen.dart
Normal file
228
apps/app_seeds/lib/ui/trust_network_screen.dart
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
import 'package:commons_core/commons_core.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../i18n/strings.g.dart';
|
||||||
|
import '../services/profile_cache.dart' show shortPubkey;
|
||||||
|
import '../services/trust_referents.dart';
|
||||||
|
import '../services/wot_settings.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// Manage the web of trust: the bootstrap "roots" (Duniter seeds) membership is
|
||||||
|
/// measured from, and the advanced parameters (sigQty / stepMax / validity).
|
||||||
|
/// This is where a young network is seeded and tuned — progressive disclosure,
|
||||||
|
/// reached from the profile.
|
||||||
|
class TrustNetworkScreen extends StatefulWidget {
|
||||||
|
const TrustNetworkScreen({
|
||||||
|
required this.referents,
|
||||||
|
required this.wotSettings,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final TrustReferents referents;
|
||||||
|
final WotSettings wotSettings;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TrustNetworkScreen> createState() => _TrustNetworkScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TrustNetworkScreenState extends State<TrustNetworkScreen> {
|
||||||
|
final _input = TextEditingController();
|
||||||
|
Set<String> _roots = {};
|
||||||
|
WotParams _params = WotParams.duniter;
|
||||||
|
bool _loading = true;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
final roots = await widget.referents.userAdded();
|
||||||
|
final params = await widget.wotSettings.params();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_roots = roots;
|
||||||
|
_params = params;
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _addRoot() async {
|
||||||
|
final t = context.t;
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
try {
|
||||||
|
await widget.referents.add(_input.text);
|
||||||
|
_input.clear();
|
||||||
|
await _load();
|
||||||
|
messenger.showSnackBar(SnackBar(content: Text(t.wot.rootAdded)));
|
||||||
|
} on FormatException {
|
||||||
|
setState(() => _error = t.wot.rootInvalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removeRoot(String hex) async {
|
||||||
|
await widget.referents.remove(hex);
|
||||||
|
await _load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveParams(WotParams next) async {
|
||||||
|
await widget.wotSettings.save(next);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _params = next);
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text(context.t.wot.saved)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_input.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(t.wot.title)),
|
||||||
|
body: _loading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: ListView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
children: [
|
||||||
|
Text(t.wot.help,
|
||||||
|
style: const TextStyle(color: seedMuted, fontSize: 13)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(t.wot.roots,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600, color: seedOnSurface)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(t.wot.rootsHelp,
|
||||||
|
style: const TextStyle(color: seedMuted, fontSize: 12)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (_roots.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Text(t.wot.noRoots,
|
||||||
|
style:
|
||||||
|
const TextStyle(color: seedMuted, fontSize: 13)),
|
||||||
|
),
|
||||||
|
for (final hex in _roots)
|
||||||
|
ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: const Icon(Icons.hub_outlined, color: seedGreen),
|
||||||
|
title: Text(shortPubkey(hex),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'monospace', fontSize: 13)),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 20),
|
||||||
|
tooltip: t.wot.remove,
|
||||||
|
onPressed: () => _removeRoot(hex),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
key: const Key('wot.rootInput'),
|
||||||
|
controller: _input,
|
||||||
|
onChanged: (_) {
|
||||||
|
if (_error != null) setState(() => _error = null);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: t.wot.addRoot,
|
||||||
|
hintText: t.wot.rootHint,
|
||||||
|
errorText: _error,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
key: const Key('wot.addRoot'),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
onPressed: _addRoot,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Divider(),
|
||||||
|
ExpansionTile(
|
||||||
|
tilePadding: EdgeInsets.zero,
|
||||||
|
title: Text(t.wot.params),
|
||||||
|
subtitle: Text(t.wot.paramsHelp,
|
||||||
|
style: const TextStyle(fontSize: 12)),
|
||||||
|
children: [
|
||||||
|
_Stepper(
|
||||||
|
label: t.wot.sigQty,
|
||||||
|
value: _params.sigQty,
|
||||||
|
min: 1,
|
||||||
|
onChanged: (v) =>
|
||||||
|
_saveParams(_params.copyWith(sigQty: v)),
|
||||||
|
),
|
||||||
|
_Stepper(
|
||||||
|
label: t.wot.stepMax,
|
||||||
|
value: _params.stepMax,
|
||||||
|
min: 1,
|
||||||
|
onChanged: (v) =>
|
||||||
|
_saveParams(_params.copyWith(stepMax: v)),
|
||||||
|
),
|
||||||
|
_Stepper(
|
||||||
|
label: t.wot.validityDays,
|
||||||
|
value: _params.sigValidity.inDays,
|
||||||
|
min: 1,
|
||||||
|
step: 30,
|
||||||
|
onChanged: (v) => _saveParams(
|
||||||
|
_params.copyWith(sigValidity: Duration(days: v))),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => _saveParams(WotParams.duniter),
|
||||||
|
child: Text(t.wot.reset),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A "- value +" row for an integer parameter, clamped to [min].
|
||||||
|
class _Stepper extends StatelessWidget {
|
||||||
|
const _Stepper({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
this.min = 0,
|
||||||
|
this.step = 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final int value;
|
||||||
|
final int min;
|
||||||
|
final int step;
|
||||||
|
final ValueChanged<int> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(label)),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove_circle_outline),
|
||||||
|
onPressed:
|
||||||
|
value - step >= min ? () => onChanged(value - step) : null,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 44,
|
||||||
|
child: Text('$value', textAlign: TextAlign.center),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
|
onPressed: () => onChanged(value + step),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -124,6 +124,9 @@ flutter:
|
||||||
# Seed strings for slang live under lib/i18n (not a Flutter asset; compiled).
|
# Seed strings for slang live under lib/i18n (not a Flutter asset; compiled).
|
||||||
assets:
|
assets:
|
||||||
- assets/catalog/species.json
|
- assets/catalog/species.json
|
||||||
|
# Web-of-trust bootstrap referents (Duniter "seeds"). Empty until real
|
||||||
|
# founding identities are curated; users bootstrap by adding their own.
|
||||||
|
- assets/trust/referents.json
|
||||||
- assets/logo.png
|
- assets/logo.png
|
||||||
# Bundled so the Linux runner can use it as the GTK window icon.
|
# Bundled so the Linux runner can use it as the GTK window icon.
|
||||||
- assets/icon.png
|
- assets/icon.png
|
||||||
|
|
|
||||||
45
apps/app_seeds/test/services/trust_referents_test.dart
Normal file
45
apps/app_seeds/test/services/trust_referents_test.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tane/services/trust_referents.dart';
|
||||||
|
|
||||||
|
import '../support/test_support.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
const npub =
|
||||||
|
'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
|
||||||
|
const hex =
|
||||||
|
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d';
|
||||||
|
|
||||||
|
// No bundled asset in the unit test bundle → _bundled() degrades to empty, so
|
||||||
|
// these exercise the user-added set (the cold-start bootstrap path).
|
||||||
|
late TrustReferents referents;
|
||||||
|
setUp(() => referents = TrustReferents(InMemorySecretStore()));
|
||||||
|
|
||||||
|
test('starts empty', () async {
|
||||||
|
expect(await referents.all(), isEmpty);
|
||||||
|
expect(await referents.userAdded(), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds a referent from an npub, storing hex', () async {
|
||||||
|
final stored = await referents.add(npub);
|
||||||
|
expect(stored, hex);
|
||||||
|
expect(await referents.userAdded(), {hex});
|
||||||
|
expect(await referents.all(), contains(hex));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts a hex key directly and is idempotent', () async {
|
||||||
|
await referents.add(hex);
|
||||||
|
await referents.add(npub); // same identity, npub form
|
||||||
|
expect(await referents.userAdded(), {hex});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects an invalid identity code', () async {
|
||||||
|
expect(() => referents.add('not-a-key'), throwsFormatException);
|
||||||
|
expect(await referents.userAdded(), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes a referent', () async {
|
||||||
|
await referents.add(hex);
|
||||||
|
await referents.remove(hex);
|
||||||
|
expect(await referents.userAdded(), isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
28
apps/app_seeds/test/services/wot_settings_test.dart
Normal file
28
apps/app_seeds/test/services/wot_settings_test.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:commons_core/commons_core.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tane/services/wot_settings.dart';
|
||||||
|
|
||||||
|
import '../support/test_support.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late WotSettings settings;
|
||||||
|
setUp(() => settings = WotSettings(InMemorySecretStore()));
|
||||||
|
|
||||||
|
test('defaults to the Duniter parameters', () async {
|
||||||
|
expect(await settings.params(), WotParams.duniter);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('persists saved parameters', () async {
|
||||||
|
const custom = WotParams(
|
||||||
|
sigQty: 3, stepMax: 4, sigValidity: Duration(days: 180));
|
||||||
|
await settings.save(custom);
|
||||||
|
expect(await settings.params(), custom);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reset restores the Duniter defaults', () async {
|
||||||
|
await settings.save(const WotParams(
|
||||||
|
sigQty: 1, stepMax: 1, sigValidity: Duration(days: 30)));
|
||||||
|
await settings.reset();
|
||||||
|
expect(await settings.params(), WotParams.duniter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -91,6 +91,55 @@ void main() {
|
||||||
await outside.close();
|
await outside.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('network membership: enough referent vouches → member', () async {
|
||||||
|
const params =
|
||||||
|
WotParams(sigQty: 2, stepMax: 5, sigValidity: Duration(days: 365));
|
||||||
|
final transport = FakeTrustTransport(me)
|
||||||
|
..addCert('r1', peer)
|
||||||
|
..addCert('r2', peer);
|
||||||
|
final cubit = TrustCubit(
|
||||||
|
transport,
|
||||||
|
peerPubkey: peer,
|
||||||
|
selfPubkey: me,
|
||||||
|
referents: {'r1', 'r2'},
|
||||||
|
params: params,
|
||||||
|
);
|
||||||
|
await cubit.load();
|
||||||
|
expect(cubit.state.networkBootstrapped, isTrue);
|
||||||
|
expect(cubit.state.isNetworkMember, isTrue);
|
||||||
|
expect(cubit.state.tier, TrustTier.networkMember);
|
||||||
|
await cubit.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with no referents, membership is undetermined (not a member)',
|
||||||
|
() async {
|
||||||
|
final transport = FakeTrustTransport(me)
|
||||||
|
..addCert('r1', peer)
|
||||||
|
..addCert('r2', peer);
|
||||||
|
// referents default to {} → the trust net isn't seeded.
|
||||||
|
final cubit = TrustCubit(transport, peerPubkey: peer, selfPubkey: me);
|
||||||
|
await cubit.load();
|
||||||
|
expect(cubit.state.networkBootstrapped, isFalse);
|
||||||
|
expect(cubit.state.isNetworkMember, isFalse);
|
||||||
|
await cubit.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tier falls back to vouched, then unknown', () async {
|
||||||
|
final vouched = TrustCubit(
|
||||||
|
FakeTrustTransport(me)..addCert('someone', peer),
|
||||||
|
peerPubkey: peer,
|
||||||
|
selfPubkey: me);
|
||||||
|
await vouched.load();
|
||||||
|
expect(vouched.state.tier, TrustTier.vouched);
|
||||||
|
await vouched.close();
|
||||||
|
|
||||||
|
final unknown = TrustCubit(FakeTrustTransport(me),
|
||||||
|
peerPubkey: 'nobody', selfPubkey: me);
|
||||||
|
await unknown.load();
|
||||||
|
expect(unknown.state.tier, TrustTier.unknown);
|
||||||
|
await unknown.close();
|
||||||
|
});
|
||||||
|
|
||||||
test('never vouches for self', () async {
|
test('never vouches for self', () async {
|
||||||
final cubit =
|
final cubit =
|
||||||
TrustCubit(FakeTrustTransport(me), peerPubkey: me, selfPubkey: me);
|
TrustCubit(FakeTrustTransport(me), peerPubkey: me, selfPubkey: me);
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ Estas fijan `schemaVersion = 1` y el arranque técnico:
|
||||||
|
|
||||||
- **Transporte de la capa social → Nostr (confirmado).** No se renuncia a Nostr: **Duniter no tiene mensajería**, así que Nostr es la vía para mensajes y ofertas. Se mantiene "una sola identidad" derivando la clave **secp256k1** (Nostr) de la semilla raíz Ğ1. Falta: confirmar madurez de los NIPs y la ruta de derivación. (Datapods de Duniter descartados: no están en servicio.) → [g1-integration.md](g1-integration.md)
|
- **Transporte de la capa social → Nostr (confirmado).** No se renuncia a Nostr: **Duniter no tiene mensajería**, así que Nostr es la vía para mensajes y ofertas. Se mantiene "una sola identidad" derivando la clave **secp256k1** (Nostr) de la semilla raíz Ğ1. Falta: confirmar madurez de los NIPs y la ruta de derivación. (Datapods de Duniter descartados: no están en servicio.) → [g1-integration.md](g1-integration.md)
|
||||||
- **Estrategia de relays:** app-as-relay oportunista + proximidad física + relays comunitarios. → network-trust §3
|
- **Estrategia de relays:** app-as-relay oportunista + proximidad física + relays comunitarios. → network-trust §3
|
||||||
- **Parámetros de la red de confianza:** umbral de certificaciones (¿5, estilo Duniter?), distancia, caducidad, certificación por colectivo. → network-trust §2
|
- **Parámetros de la red de confianza — RESUELTO/IMPLEMENTADO (2026-07-10).** Modelo **membresía Duniter completa**: regla pura `WebOfTrust.membersWith(seeds, WotParams)` con `WotParams` (sigQty/stepMax/sigValidity) por defecto Ğ1 (5/5/1año) **configurables** (`WotSettings`, keystore) — una red joven los afloja y aprieta al crecer. Cold-start honesto (sin inventar identidades): referentes "semilla" desde un asset empaquetado (vacío hasta curar fundadores reales) ∪ referentes que el usuario añade por npub/QR (`TrustReferents`). Caducidad ya la ponía el transporte (`certify` con `expiration`). UI: badge por `TrustTier` (miembro de la red > en tu círculo > avalado > desconocido) en el chat, y pantalla "Red de confianza" (gestión de raíces + parámetros) desde el perfil. Certificación por colectivo queda para sharing-model §6. → network-trust §2
|
||||||
- **Mensajería:** alcance v1 (1:1 atada a oferta), apoyo en NIP-17. → network-trust §4
|
- **Mensajería:** alcance v1 (1:1 atada a oferta), apoyo en NIP-17. → network-trust §4
|
||||||
- **Reputación** atada a un trato/oferta cerrada (evitar reseñas falsas). → sharing-model §6
|
- **Reputación** atada a un trato/oferta cerrada (evitar reseñas falsas). → sharing-model §6
|
||||||
- **Precio:** ¿monedas comunitarias / de tiempo además de dinero? → sharing-model §6
|
- **Precio:** ¿monedas comunitarias / de tiempo además de dinero? → sharing-model §6
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export 'src/ids/id_gen.dart';
|
||||||
export 'src/social/certification.dart';
|
export 'src/social/certification.dart';
|
||||||
export 'src/social/geohash.dart';
|
export 'src/social/geohash.dart';
|
||||||
export 'src/social/message_transport.dart';
|
export 'src/social/message_transport.dart';
|
||||||
|
export 'src/social/nostr_ids.dart';
|
||||||
export 'src/social/nostr/nostr_channel.dart';
|
export 'src/social/nostr/nostr_channel.dart';
|
||||||
export 'src/social/nostr/nostr_connection.dart';
|
export 'src/social/nostr/nostr_connection.dart';
|
||||||
export 'src/social/nostr/nostr_message_transport.dart';
|
export 'src/social/nostr/nostr_message_transport.dart';
|
||||||
|
|
|
||||||
21
packages/commons_core/lib/src/social/nostr_ids.dart
Normal file
21
packages/commons_core/lib/src/social/nostr_ids.dart
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import 'package:nostr/nostr.dart';
|
||||||
|
|
||||||
|
/// Decodes a NIP-19 `npub…` into its 64-char hex public key.
|
||||||
|
///
|
||||||
|
/// Accepts an already-hex key unchanged (so callers can take either form), and
|
||||||
|
/// throws [FormatException] on anything that is neither. Used to add web-of-trust
|
||||||
|
/// referents shared as an `npub` (or scanned from a QR).
|
||||||
|
String npubToHex(String input) {
|
||||||
|
final value = input.trim();
|
||||||
|
if (_isHex64(value)) return value.toLowerCase();
|
||||||
|
if (!value.startsWith('npub1')) {
|
||||||
|
throw const FormatException('not an npub or hex public key');
|
||||||
|
}
|
||||||
|
final decoded = Nip19.decode(payload: value);
|
||||||
|
if (decoded.prefix != Nip19Prefix.npub || !_isHex64(decoded.data)) {
|
||||||
|
throw const FormatException('not an npub public key');
|
||||||
|
}
|
||||||
|
return decoded.data.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isHex64(String s) => RegExp(r'^[0-9a-fA-F]{64}$').hasMatch(s);
|
||||||
|
|
@ -1,5 +1,44 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
import 'certification.dart';
|
import 'certification.dart';
|
||||||
|
|
||||||
|
/// The Duniter/Ğ1 web-of-trust parameters. Kept as data (not magic numbers) so
|
||||||
|
/// they can be persisted and tuned as the network grows — a young network needs
|
||||||
|
/// looser values than a mature one, exactly as Ğ1 itself started.
|
||||||
|
///
|
||||||
|
/// - [sigQty]: certifications from existing members needed to become a member.
|
||||||
|
/// - [stepMax]: max certification hops from a bootstrap referent (seed).
|
||||||
|
/// - [sigValidity]: how long a certification counts before it must be renewed.
|
||||||
|
class WotParams extends Equatable {
|
||||||
|
const WotParams({
|
||||||
|
required this.sigQty,
|
||||||
|
required this.stepMax,
|
||||||
|
required this.sigValidity,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The Ğ1 reference values (sigQty 5, stepMax 5, ~1-year validity — Ğ1 uses a
|
||||||
|
/// longer window, but the seed app renews yearly to match `certify`'s default).
|
||||||
|
static const duniter = WotParams(
|
||||||
|
sigQty: 5,
|
||||||
|
stepMax: 5,
|
||||||
|
sigValidity: Duration(days: 365),
|
||||||
|
);
|
||||||
|
|
||||||
|
final int sigQty;
|
||||||
|
final int stepMax;
|
||||||
|
final Duration sigValidity;
|
||||||
|
|
||||||
|
WotParams copyWith({int? sigQty, int? stepMax, Duration? sigValidity}) =>
|
||||||
|
WotParams(
|
||||||
|
sigQty: sigQty ?? this.sigQty,
|
||||||
|
stepMax: stepMax ?? this.stepMax,
|
||||||
|
sigValidity: sigValidity ?? this.sigValidity,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [sigQty, stepMax, sigValidity];
|
||||||
|
}
|
||||||
|
|
||||||
/// Pure web-of-trust computation (Duniter/Ğ1 rules), Flutter-free and I/O-free.
|
/// Pure web-of-trust computation (Duniter/Ğ1 rules), Flutter-free and I/O-free.
|
||||||
/// Answers "who counts as a known member" from certification edges, using two
|
/// Answers "who counts as a known member" from certification edges, using two
|
||||||
/// rules: **N certifications from existing members** (sigQty) and **within
|
/// rules: **N certifications from existing members** (sigQty) and **within
|
||||||
|
|
@ -76,6 +115,17 @@ class WebOfTrust {
|
||||||
members(seeds: seeds, threshold: threshold, maxDistance: maxDistance)
|
members(seeds: seeds, threshold: threshold, maxDistance: maxDistance)
|
||||||
.contains(pubkey);
|
.contains(pubkey);
|
||||||
|
|
||||||
|
/// Members using a [WotParams] bundle (Duniter naming: sigQty/stepMax).
|
||||||
|
Set<String> membersWith({
|
||||||
|
required Set<String> seeds,
|
||||||
|
required WotParams params,
|
||||||
|
}) =>
|
||||||
|
members(
|
||||||
|
seeds: seeds,
|
||||||
|
threshold: params.sigQty,
|
||||||
|
maxDistance: params.stepMax,
|
||||||
|
);
|
||||||
|
|
||||||
/// BFS hop-distance from the nearest seed over certification edges.
|
/// BFS hop-distance from the nearest seed over certification edges.
|
||||||
Map<String, int> _distancesFromSeeds(Set<String> seeds) {
|
Map<String, int> _distancesFromSeeds(Set<String> seeds) {
|
||||||
final dist = <String, int>{for (final s in seeds) s: 0};
|
final dist = <String, int>{for (final s in seeds) s: 0};
|
||||||
|
|
|
||||||
34
packages/commons_core/test/social/nostr_ids_test.dart
Normal file
34
packages/commons_core/test/social/nostr_ids_test.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import 'package:commons_core/commons_core.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('npubToHex', () {
|
||||||
|
// The canonical NIP-19 example pair.
|
||||||
|
const npub =
|
||||||
|
'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
|
||||||
|
const hex =
|
||||||
|
'3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d';
|
||||||
|
|
||||||
|
test('decodes a bech32 npub to its hex public key', () {
|
||||||
|
expect(npubToHex(npub), hex);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes a 64-char hex key through (lower-cased)', () {
|
||||||
|
expect(npubToHex(hex), hex);
|
||||||
|
expect(npubToHex(hex.toUpperCase()), hex);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trims surrounding whitespace', () {
|
||||||
|
expect(npubToHex(' $npub '), hex);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects an nsec (secret key), not a public identity', () {
|
||||||
|
expect(() => npubToHex('nsec1' 'garbage'), throwsFormatException);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects nonsense', () {
|
||||||
|
expect(() => npubToHex('not-a-key'), throwsFormatException);
|
||||||
|
expect(() => npubToHex(''), throwsFormatException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -79,4 +79,28 @@ void main() {
|
||||||
final wot = WebOfTrust.fromCertifications([cert('a', 'a')], now: now);
|
final wot = WebOfTrust.fromCertifications([cert('a', 'a')], now: now);
|
||||||
expect(wot.certifierCount('a'), 0);
|
expect(wot.certifierCount('a'), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('WotParams', () {
|
||||||
|
test('Duniter defaults are sigQty 5, stepMax 5, 1-year validity', () {
|
||||||
|
const p = WotParams.duniter;
|
||||||
|
expect(p.sigQty, 5);
|
||||||
|
expect(p.stepMax, 5);
|
||||||
|
expect(p.sigValidity, const Duration(days: 365));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('membersWith uses sigQty/stepMax like the raw rule', () {
|
||||||
|
final seeds = {'s1', 's2', 's3'};
|
||||||
|
final certs = [for (final s in seeds) cert(s, 'newcomer')];
|
||||||
|
final wot = WebOfTrust.fromCertifications(certs, now: now);
|
||||||
|
const params = WotParams(
|
||||||
|
sigQty: 3, stepMax: 5, sigValidity: Duration(days: 365));
|
||||||
|
expect(wot.membersWith(seeds: seeds, params: params),
|
||||||
|
contains('newcomer'));
|
||||||
|
// One short of sigQty → not a member.
|
||||||
|
const strict = WotParams(
|
||||||
|
sigQty: 4, stepMax: 5, sigValidity: Duration(days: 365));
|
||||||
|
expect(wot.membersWith(seeds: seeds, params: strict),
|
||||||
|
isNot(contains('newcomer')));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue