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
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue