feat(guild): restore system-key bypass + isSystem msg path
Resurrects the x-fabric-system-key bypass + isSystem branch on POST /channels/:id/messages, dropped inca20df7when dialectic stopped broadcasting topic lifecycle events to Fabric. Re-enabling now because Fabric.OpenclawPlugin's close-sub-discussion needs to write a callback into a parent channel as a system-authored message (not as the closing host), with an optional precision wakeup so the recruitment workflow can resume immediately after an interview sub-discussion closes. Three coupled bits: 1. ApiKeyGuard pre-Bearer bypass: when x-fabric-system-key matches FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY, set req.isSystem=true and skip the Bearer check. Intentionally reuses the existing commands sync env — same shared secret, same consumer (the OpenclawPlugin reads channels.fabric.commandsSyncKey for both paths). One less env to rotate, one less secret to manage. 2. messaging.controller POST /channels/:id/messages adds an isSystem branch (runs before the participant gate): - Looks up the channel directly (not via assertParticipant). - Persists with sentinel author 00000000-0000-0000-0000-000000000000, same UUID the old impl used. - Translates <@user.name:NAME> mentions like the regular path. - When wakeupUserId is set, delivers via emitMessageTargeted so that exactly that one recipient receives wakeup=true; everyone else gets wakeup=false. When omitted, delivers via emitMessageCreated with an empty wakeUserIds set so nobody is woken — silent system log. Two intentional differences from the985b06aoriginal: - No xType=announce restriction. The original was limited to announce because that was Dialectic's only use case; we now need this on dm / general / discuss / etc. for the sub-discussion callback. Closed channels are still rejected (409) on both paths. - The wakeupUserId field is new — old impl only ever sent silent announces. 3. DTO carries wakeupUserId? optional string. Ignored on the regular user-bearer path; load-bearing on the system path. Shared helper: extracted commands.controller's private safeEqual into src/common/safe-equal.ts so api-key.guard.ts can use the same constant- time check. Vitest spec covers equal / inequal / length-mismatch / empty cases. Existing unit tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
src/common/safe-equal.ts
Normal file
12
src/common/safe-equal.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { timingSafeEqual } from 'node:crypto';
|
||||
|
||||
// Constant-time string comparison. Returns false for length mismatch (the
|
||||
// length difference itself is observable, but the per-byte loop isn't).
|
||||
// Used for shared-secret header checks (commands-sync-key, system-key,
|
||||
// etc.) to keep timing-oracle attacks off the table.
|
||||
export function safeEqual(a: string, b: string): boolean {
|
||||
const ab = Buffer.from(a);
|
||||
const bb = Buffer.from(b);
|
||||
if (ab.length !== bb.length) return false;
|
||||
return timingSafeEqual(ab, bb);
|
||||
}
|
||||
Reference in New Issue
Block a user