From bb63a5738481918ec777f887273a2ed6194e0acd Mon Sep 17 00:00:00 2001 From: hzhang Date: Sat, 16 May 2026 17:47:14 +0100 Subject: [PATCH] feat(security): send x-commands-sync-key when configured (Guild C-2) syncCommands attaches the FABRIC_COMMANDS_SYNC_KEY header when the operator sets it, so the guild can restrict slash-command catalog writes to this plugin. No-op / backward compatible when unset. Co-Authored-By: Claude Opus 4.7 (1M context) --- dist/fabric/src/fabric-client.js | 9 +++++++-- src/fabric-client.ts | 14 +++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/dist/fabric/src/fabric-client.js b/dist/fabric/src/fabric-client.js index 1cde727..e65c59e 100644 --- a/dist/fabric/src/fabric-client.js +++ b/dist/fabric/src/fabric-client.js @@ -23,12 +23,13 @@ export class FabricClient { } // Generic JSON request (GET/PUT/PATCH/DELETE). Empty 2xx body -> null // (Fabric returns an empty body when a channel has no canvas). - async req(method, url, auth, body) { + async req(method, url, auth, body, extraHeaders) { const res = await fetch(url, { method, headers: { ...(body !== undefined ? { 'content-type': 'application/json' } : {}), ...(auth ? { authorization: `Bearer ${auth}` } : {}), + ...(extraHeaders ?? {}), }, body: body !== undefined ? JSON.stringify(body) : undefined, }); @@ -75,7 +76,11 @@ export class FabricClient { // full replace). The frontend GETs it for `/` autocomplete; execution // still flows as a normal / message into OpenClaw's command system. syncCommands(guildEndpoint, guildToken, commands) { - return this.req('PUT', `${guildEndpoint}/api/commands`, guildToken, { commands }); + // Guild C-2: when the operator sets a shared sync key on both sides + // (FABRIC_COMMANDS_SYNC_KEY here / FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY + // on the guild), the catalog write is restricted to this plugin. + const key = process.env.FABRIC_COMMANDS_SYNC_KEY; + return this.req('PUT', `${guildEndpoint}/api/commands`, guildToken, { commands }, key ? { 'x-commands-sync-key': key } : undefined); } // [{ userId, bypass }] — bypass is true only for discuss/work bypass-list channelMembers(guildEndpoint, guildToken, channelId) { diff --git a/src/fabric-client.ts b/src/fabric-client.ts index aa1518f..486335a 100644 --- a/src/fabric-client.ts +++ b/src/fabric-client.ts @@ -36,12 +36,14 @@ export class FabricClient { url: string, auth?: string, body?: unknown, + extraHeaders?: Record, ): Promise { const res = await fetch(url, { method, headers: { ...(body !== undefined ? { 'content-type': 'application/json' } : {}), ...(auth ? { authorization: `Bearer ${auth}` } : {}), + ...(extraHeaders ?? {}), }, body: body !== undefined ? JSON.stringify(body) : undefined, }); @@ -123,7 +125,17 @@ export class FabricClient { guildToken: string, commands: unknown[], ): Promise { - return this.req('PUT', `${guildEndpoint}/api/commands`, guildToken, { commands }); + // Guild C-2: when the operator sets a shared sync key on both sides + // (FABRIC_COMMANDS_SYNC_KEY here / FABRIC_BACKEND_GUILD_COMMANDS_SYNC_KEY + // on the guild), the catalog write is restricted to this plugin. + const key = process.env.FABRIC_COMMANDS_SYNC_KEY; + return this.req( + 'PUT', + `${guildEndpoint}/api/commands`, + guildToken, + { commands }, + key ? { 'x-commands-sync-key': key } : undefined, + ); } // [{ userId, bypass }] — bypass is true only for discuss/work bypass-list