import 'nostr_connection.dart'; import 'nostr_event.dart'; import 'trust_transport.dart'; import 'web_of_trust.dart'; /// Duniter-style certifications carried as Nostr events, on the shared /// [NostrConnection]. There is no settled NIP for a web of trust (NIP-85 is /// unsettled), so the spike uses a **custom addressable kind** mapped to the /// Duniter model — exactly the "own WoT, Duniter-compatible" path /// g1-integration.md §"WoT propia pero compatible" anticipates. /// /// Addressable by (kind, issuer, `d`=subject): one live certification per /// (certifier, subject), so re-certifying renews and revoking replaces — the /// relay keeps only the latest, like Ğ1 keeps one active cert per pair. class NostrTrustTransport implements TrustTransport { NostrTrustTransport(this._conn); final NostrConnection _conn; /// Custom Tanemaki certification kind (addressable range). static const kindCertification = 30777; int get _now => DateTime.now().millisecondsSinceEpoch ~/ 1000; @override Future certify({ required String subjectPubkey, Duration validity = const Duration(days: 365), String note = '', }) => _publishCert(subjectPubkey, note: note, validity: validity, revoked: false); @override Future revoke({required String subjectPubkey}) => _publishCert(subjectPubkey, note: '', validity: Duration.zero, revoked: true); Future _publishCert( String subject, { required String note, required Duration validity, required bool revoked, }) async { final event = NostrEvent( pubkey: _conn.publicKeyHex, createdAt: _now, kind: kindCertification, tags: [ ['p', subject], ['d', subject], // one addressable cert per (issuer, subject) ['status', revoked ? 'revoked' : 'valid'], if (!revoked) ['expiration', '${_now + validity.inSeconds}'], ], content: note, )..signWith(_conn.privateKeyHex); final r = await _conn.publish(event); if (!r.accepted) throw StateError('relay rejected cert: ${r.message}'); } @override Future> allCertifications() async { final events = await _conn.reqOnce({ 'kinds': [kindCertification], }); return events.map(_toCertification).toList(); } @override Future> certifiersOf(String subjectPubkey) async { final events = await _conn.reqOnce({ 'kinds': [kindCertification], '#p': [subjectPubkey], }); final now = DateTime.now(); return events .map(_toCertification) .where((c) => c.subject == subjectPubkey && c.isValidAt(now)) .map((c) => c.issuer) .toSet(); } Certification _toCertification(NostrEvent e) { final exp = e.tag('expiration'); return Certification( issuer: e.pubkey, subject: e.tag('p') ?? e.tag('d') ?? '', issuedAt: DateTime.fromMillisecondsSinceEpoch(e.createdAt * 1000), expiresAt: exp == null ? null : DateTime.fromMillisecondsSinceEpoch(int.parse(exp) * 1000), revoked: e.tag('status') == 'revoked', note: e.content, ); } @override Future close() => _conn.close(); }