atproto relay implementation in zig zlay.waow.tech

feat: evict cached signing key on #identity event

matches indigo's pattern — purge DID key cache before any other
processing so next commit triggers fresh DID doc resolution.
handles did:web key rotation (ephemeral keys on PDS restart).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+15
+5
src/subscriber.zig
··· 165 else 166 payload.getString("did"); 167 168 // validate commit frames using pre-decoded payload 169 var commit_data_cid: ?[]const u8 = null; 170 var commit_rev: ?[]const u8 = null;
··· 165 else 166 payload.getString("did"); 167 168 + // on #identity event, evict cached signing key so next commit re-resolves 169 + if (std.mem.eql(u8, frame_type, "#identity")) { 170 + if (did) |d| sub.validator.evictKey(d); 171 + } 172 + 173 // validate commit frames using pre-decoded payload 174 var commit_data_cid: ?[]const u8 = null; 175 var commit_rev: ?[]const u8 = null;
+10
src/validator.zig
··· 349 } 350 } 351 352 /// cache size (for diagnostics) 353 pub fn cacheSize(self: *Validator) usize { 354 self.cache_mutex.lock();
··· 349 } 350 } 351 352 + /// evict a DID's cached signing key (e.g. on #identity event). 353 + /// the next commit from this DID will trigger a fresh resolution. 354 + pub fn evictKey(self: *Validator, did: []const u8) void { 355 + self.cache_mutex.lock(); 356 + defer self.cache_mutex.unlock(); 357 + if (self.cache.fetchRemove(did)) |entry| { 358 + self.allocator.free(entry.key); 359 + } 360 + } 361 + 362 /// cache size (for diagnostics) 363 pub fn cacheSize(self: *Validator) usize { 364 self.cache_mutex.lock();