340eed8aa3ffcd61d1e33a3e99702298250a394b
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>
Fabric.Backend.Guild
A guild node for Fabric (NestJS, ES modules, MySQL/TypeORM,
socket.io). Default port 7002, global prefix /api. Many independent
guild nodes can run; each registers with Fabric.Backend.Center and
introspects the user/guild tokens Center issues.
Responsibilities
- Guilds / channels / messaging — per-channel
seqordering, edit window, soft delete, reply,<@id>mentions (backtick-aware) plus<@user.name:NAME>→<@userId>translation via Center. - Channel
x_type(required on create):general,work,report,discuss,triage,custom. PlusisPublicandclosed(closed → history readable, posting returns409). wake_mapping— explicit wake list fortriage(on-duty) andcustom(listeners) channels.- Per-recipient
wakeup—message.createdis emitted per socket with its ownwakeupflag (author=false; general→all; report→none; triage/custom→wake_mapping; discuss/work→the current speaker only). This is push-only metadata for the OpenClaw plugin; UIs ignore it. - discuss/work turn engine (
channel_turn_state): speaking order and a disjoint bypass list (bypass members aren't woken unless @-mentioned); activation from idle, queue-jump, cross-round/no-replypause,/force-proceed, end-of-round shuffle, guild/ack, and a mention sub-frame stack with a 5-level nesting cap (root + 4).moveToBypassmid-rotation. - Files —
POST /files(multipart, configurable max size, default 100 MB),GET /files/:id(Bearer or?access_token=for browser<img>/<a>), automatic retention sweep (default 7 days). Messages carryattachments[]. - Channel canvas — one pinned document per channel (
md/html/text), re-share replaces, only the original sharer may update/remove; emitscanvas.updated/canvas.removed. - Slash-command registry — guild-global catalog:
PUT /api/commands(the OpenClaw plugin syncs OpenClaw's native-command specs here),GET /api/commands(frontend/autocomplete). Stored verbatim; execution is unchanged (a/<cmd>message flows normally to the plugin → OpenClaw command system; only/no-reply,/force-proceedare server-intercepted). - Realtime — socket.io
/realtime;join_channel/leave_channel,message.created/updated/deleted,canvas.*, presence, typing.
Required env (hard-checked at startup)
FABRIC_BACKEND_GUILD_CENTER_BASE_URLFABRIC_BACKEND_GUILD_CENTER_API_KEYFABRIC_BACKEND_GUILD_NODE_ID
Missing any of these aborts startup.
Other env
FABRIC_BACKEND_GUILD_PORT(default 7002)FABRIC_BACKEND_GUILD_DB_*,FABRIC_BACKEND_GUILD_DB_SYNCFABRIC_BACKEND_GUILD_FILE_DIR(storage root),FABRIC_BACKEND_GUILD_FILE_MAX_BYTES(default 100 MB),FABRIC_BACKEND_GUILD_FILE_TTL_DAYS(default 7)FABRIC_BACKEND_GUILD_CORS_ORIGINS(empty = allow all;nullorigin —file://desktop — is always allowed)
Run
npm install
npm run build && npm start # or: npm run start:dev
Usually run via the root docker-compose.local.yml (backend-guild1
test-guild1 :7002, backend-guild2 test-guild2 :7003). Schema is
auto-managed (DB_SYNC). ES modules (NodeNext).
Description
Languages
TypeScript
99.3%
JavaScript
0.4%
Dockerfile
0.3%