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) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-16 17:47:14 +01:00
parent fc6edaabfd
commit bb63a57384
2 changed files with 20 additions and 3 deletions

View File

@@ -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 /<cmd> 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) {

View File

@@ -36,12 +36,14 @@ export class FabricClient {
url: string,
auth?: string,
body?: unknown,
extraHeaders?: Record<string, string>,
): Promise<T> {
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<unknown> {
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