@spinabot/brigade 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/channels/discord/account-config.d.ts +60 -0
- package/dist/agents/channels/discord/account-config.d.ts.map +1 -1
- package/dist/agents/channels/discord/account-config.js +89 -0
- package/dist/agents/channels/discord/account-config.js.map +1 -1
- package/dist/agents/channels/discord/adapter.d.ts +24 -1
- package/dist/agents/channels/discord/adapter.d.ts.map +1 -1
- package/dist/agents/channels/discord/adapter.js +208 -41
- package/dist/agents/channels/discord/adapter.js.map +1 -1
- package/dist/agents/channels/discord/component-blocks.d.ts +108 -0
- package/dist/agents/channels/discord/component-blocks.d.ts.map +1 -0
- package/dist/agents/channels/discord/component-blocks.js +113 -0
- package/dist/agents/channels/discord/component-blocks.js.map +1 -0
- package/dist/agents/channels/discord/components.d.ts +78 -0
- package/dist/agents/channels/discord/components.d.ts.map +1 -1
- package/dist/agents/channels/discord/components.js +89 -0
- package/dist/agents/channels/discord/components.js.map +1 -1
- package/dist/agents/channels/discord/connection.d.ts +195 -12
- package/dist/agents/channels/discord/connection.d.ts.map +1 -1
- package/dist/agents/channels/discord/connection.js +852 -38
- package/dist/agents/channels/discord/connection.js.map +1 -1
- package/dist/agents/channels/discord/directory-cache.d.ts +47 -0
- package/dist/agents/channels/discord/directory-cache.d.ts.map +1 -0
- package/dist/agents/channels/discord/directory-cache.js +131 -0
- package/dist/agents/channels/discord/directory-cache.js.map +1 -0
- package/dist/agents/channels/discord/directory-live.d.ts +61 -0
- package/dist/agents/channels/discord/directory-live.d.ts.map +1 -0
- package/dist/agents/channels/discord/directory-live.js +140 -0
- package/dist/agents/channels/discord/directory-live.js.map +1 -0
- package/dist/agents/channels/discord/format.d.ts +15 -0
- package/dist/agents/channels/discord/format.d.ts.map +1 -1
- package/dist/agents/channels/discord/format.js +56 -0
- package/dist/agents/channels/discord/format.js.map +1 -1
- package/dist/agents/channels/discord/guilds.d.ts +25 -0
- package/dist/agents/channels/discord/guilds.d.ts.map +1 -0
- package/dist/agents/channels/discord/guilds.js +46 -0
- package/dist/agents/channels/discord/guilds.js.map +1 -0
- package/dist/agents/channels/discord/inbound-extras.d.ts +166 -9
- package/dist/agents/channels/discord/inbound-extras.d.ts.map +1 -1
- package/dist/agents/channels/discord/inbound-extras.js +246 -8
- package/dist/agents/channels/discord/inbound-extras.js.map +1 -1
- package/dist/agents/channels/discord/index.d.ts +10 -3
- package/dist/agents/channels/discord/index.d.ts.map +1 -1
- package/dist/agents/channels/discord/index.js +10 -3
- package/dist/agents/channels/discord/index.js.map +1 -1
- package/dist/agents/channels/discord/modal-registry.d.ts +89 -0
- package/dist/agents/channels/discord/modal-registry.d.ts.map +1 -0
- package/dist/agents/channels/discord/modal-registry.js +104 -0
- package/dist/agents/channels/discord/modal-registry.js.map +1 -0
- package/dist/agents/channels/discord/modals.d.ts +100 -0
- package/dist/agents/channels/discord/modals.d.ts.map +1 -0
- package/dist/agents/channels/discord/modals.js +124 -0
- package/dist/agents/channels/discord/modals.js.map +1 -0
- package/dist/agents/channels/discord/permission-audit.d.ts +43 -0
- package/dist/agents/channels/discord/permission-audit.d.ts.map +1 -0
- package/dist/agents/channels/discord/permission-audit.js +192 -0
- package/dist/agents/channels/discord/permission-audit.js.map +1 -0
- package/dist/agents/channels/discord/plugin.d.ts +18 -2
- package/dist/agents/channels/discord/plugin.d.ts.map +1 -1
- package/dist/agents/channels/discord/plugin.js +73 -4
- package/dist/agents/channels/discord/plugin.js.map +1 -1
- package/dist/agents/channels/discord/probe.d.ts +23 -1
- package/dist/agents/channels/discord/probe.d.ts.map +1 -1
- package/dist/agents/channels/discord/probe.js +40 -5
- package/dist/agents/channels/discord/probe.js.map +1 -1
- package/dist/agents/channels/discord/rest-actions.d.ts +346 -0
- package/dist/agents/channels/discord/rest-actions.d.ts.map +1 -0
- package/dist/agents/channels/discord/rest-actions.js +559 -0
- package/dist/agents/channels/discord/rest-actions.js.map +1 -0
- package/dist/agents/channels/discord/rest-components.d.ts +122 -0
- package/dist/agents/channels/discord/rest-components.d.ts.map +1 -0
- package/dist/agents/channels/discord/rest-components.js +243 -0
- package/dist/agents/channels/discord/rest-components.js.map +1 -0
- package/dist/agents/channels/discord/security-audit.d.ts +29 -0
- package/dist/agents/channels/discord/security-audit.d.ts.map +1 -0
- package/dist/agents/channels/discord/security-audit.js +94 -0
- package/dist/agents/channels/discord/security-audit.js.map +1 -0
- package/dist/agents/channels/discord/security-doctor.d.ts +43 -0
- package/dist/agents/channels/discord/security-doctor.d.ts.map +1 -0
- package/dist/agents/channels/discord/security-doctor.js +83 -0
- package/dist/agents/channels/discord/security-doctor.js.map +1 -0
- package/dist/agents/channels/discord/status-issues.d.ts +37 -0
- package/dist/agents/channels/discord/status-issues.d.ts.map +1 -0
- package/dist/agents/channels/discord/status-issues.js +66 -0
- package/dist/agents/channels/discord/status-issues.js.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts +57 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.js +98 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.js.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding.d.ts +95 -0
- package/dist/agents/channels/discord/subagent-thread-binding.d.ts.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding.js +208 -0
- package/dist/agents/channels/discord/subagent-thread-binding.js.map +1 -0
- package/dist/agents/channels/discord/system-events.d.ts +31 -0
- package/dist/agents/channels/discord/system-events.d.ts.map +1 -0
- package/dist/agents/channels/discord/system-events.js +74 -0
- package/dist/agents/channels/discord/system-events.js.map +1 -0
- package/dist/agents/channels/general-callback.d.ts +12 -0
- package/dist/agents/channels/general-callback.d.ts.map +1 -1
- package/dist/agents/channels/general-callback.js +18 -0
- package/dist/agents/channels/general-callback.js.map +1 -1
- package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
- package/dist/agents/channels/inbound-pipeline.js +5 -3
- package/dist/agents/channels/inbound-pipeline.js.map +1 -1
- package/dist/agents/extensions/types.d.ts +7 -0
- package/dist/agents/extensions/types.d.ts.map +1 -1
- package/dist/agents/extensions/types.js.map +1 -1
- package/dist/agents/subagent-announce-delivery.d.ts +10 -0
- package/dist/agents/subagent-announce-delivery.d.ts.map +1 -1
- package/dist/agents/subagent-announce-delivery.js +1 -0
- package/dist/agents/subagent-announce-delivery.js.map +1 -1
- package/dist/agents/subagent-completion-bridge.d.ts.map +1 -1
- package/dist/agents/subagent-completion-bridge.js +81 -0
- package/dist/agents/subagent-completion-bridge.js.map +1 -1
- package/dist/agents/subagent-spawn.d.ts.map +1 -1
- package/dist/agents/subagent-spawn.js +57 -4
- package/dist/agents/subagent-spawn.js.map +1 -1
- package/dist/agents/tools/discord-action-tool.d.ts +224 -0
- package/dist/agents/tools/discord-action-tool.d.ts.map +1 -0
- package/dist/agents/tools/discord-action-tool.js +848 -0
- package/dist/agents/tools/discord-action-tool.js.map +1 -0
- package/dist/agents/tools/registry.d.ts.map +1 -1
- package/dist/agents/tools/registry.js +21 -0
- package/dist/agents/tools/registry.js.map +1 -1
- package/dist/agents/tools/sessions/index.d.ts +8 -0
- package/dist/agents/tools/sessions/index.d.ts.map +1 -1
- package/dist/agents/tools/sessions/index.js +15 -3
- package/dist/agents/tools/sessions/index.js.map +1 -1
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/channels.d.ts +2 -0
- package/dist/cli/commands/channels.d.ts.map +1 -1
- package/dist/cli/commands/channels.js +58 -1
- package/dist/cli/commands/channels.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord REST-JSON component serializers (Fix A1).
|
|
3
|
+
*
|
|
4
|
+
* The Phase-3 builders (`components.ts` / `component-blocks.ts`) emit discord.js
|
|
5
|
+
* *Builder* objects for the GATEWAY `sendInteractive` path. The `discord_action`
|
|
6
|
+
* tool, however, talks straight to Discord REST v10 (`rest-actions.ts`), which
|
|
7
|
+
* needs RAW component JSON — the on-the-wire shape Discord documents, identical
|
|
8
|
+
* whether discord.js serialized it or we did.
|
|
9
|
+
*
|
|
10
|
+
* This module turns high-level STRUCTURED specs (selects / modal triggers / V2
|
|
11
|
+
* blocks) into that raw JSON, carrying the SAME custom_id codecs the press-
|
|
12
|
+
* routing in `connection.ts handleInteraction` already understands:
|
|
13
|
+
*
|
|
14
|
+
* - SELECT rows get a `custom_id` prefixed with {@link GENERAL_CALLBACK_PREFIX}
|
|
15
|
+
* (via {@link buildDiscordSelectRow}) so a selection routes through the
|
|
16
|
+
* general-callback branch → `callbackQuery{ data, values }`.
|
|
17
|
+
* - MODAL triggers register the heavy form in `modal-registry.ts` and emit a
|
|
18
|
+
* button whose `custom_id` is the `modal:<id>` marker `handleInteraction`
|
|
19
|
+
* recognizes → `showModal`.
|
|
20
|
+
* - V2 blocks become Components-V2 JSON; the caller sets the `IsComponentsV2`
|
|
21
|
+
* message flag (1 << 15) and moves all text into TextDisplay blocks.
|
|
22
|
+
*
|
|
23
|
+
* Pure / deterministic — no I/O, no discord.js, no globals. Returns a discriminated
|
|
24
|
+
* result so the tool can surface a clean validation error instead of shipping a
|
|
25
|
+
* half-built component Discord would 400 on.
|
|
26
|
+
*/
|
|
27
|
+
import { type DiscordButtonStyleValue, type DiscordSelectKind, type DiscordSelectOption } from "./components.js";
|
|
28
|
+
import { type DiscordBlockSpec } from "./component-blocks.js";
|
|
29
|
+
import { type DiscordModalRegistration } from "./modal-registry.js";
|
|
30
|
+
/** Discord component-type ids on the wire (v10). */
|
|
31
|
+
export declare const DISCORD_COMPONENT_TYPE: {
|
|
32
|
+
readonly actionRow: 1;
|
|
33
|
+
readonly button: 2;
|
|
34
|
+
readonly stringSelect: 3;
|
|
35
|
+
readonly textInput: 4;
|
|
36
|
+
readonly userSelect: 5;
|
|
37
|
+
readonly roleSelect: 6;
|
|
38
|
+
readonly mentionableSelect: 7;
|
|
39
|
+
readonly channelSelect: 8;
|
|
40
|
+
readonly section: 9;
|
|
41
|
+
readonly textDisplay: 10;
|
|
42
|
+
readonly thumbnail: 11;
|
|
43
|
+
readonly mediaGallery: 12;
|
|
44
|
+
readonly file: 13;
|
|
45
|
+
readonly separator: 14;
|
|
46
|
+
readonly container: 17;
|
|
47
|
+
};
|
|
48
|
+
/** Discord caps a message at 5 select rows / 5 action rows. */
|
|
49
|
+
export declare const DISCORD_MAX_SELECT_ROWS = 5;
|
|
50
|
+
/** Discord caps a string select at 25 options. */
|
|
51
|
+
export declare const DISCORD_MAX_SELECT_OPTIONS = 25;
|
|
52
|
+
/** The structured `select` spec the tool accepts (validated → REST JSON). */
|
|
53
|
+
export interface DiscordSelectInput {
|
|
54
|
+
kind: DiscordSelectKind;
|
|
55
|
+
/** App-defined token (the tool prefixes it with the general marker on press-routing). */
|
|
56
|
+
customId: string;
|
|
57
|
+
placeholder?: string;
|
|
58
|
+
minValues?: number;
|
|
59
|
+
maxValues?: number;
|
|
60
|
+
options?: DiscordSelectOption[];
|
|
61
|
+
}
|
|
62
|
+
/** The structured `modal` spec the tool accepts (registered + a trigger button emitted). */
|
|
63
|
+
export interface DiscordModalInput {
|
|
64
|
+
/** Trigger button label. */
|
|
65
|
+
buttonLabel: string;
|
|
66
|
+
title?: string;
|
|
67
|
+
fields: DiscordModalRegistration["fields"];
|
|
68
|
+
sessionKey?: string;
|
|
69
|
+
agentId?: string;
|
|
70
|
+
accountId?: string;
|
|
71
|
+
allowedUsers?: string[];
|
|
72
|
+
buttonStyle?: DiscordButtonStyleValue;
|
|
73
|
+
}
|
|
74
|
+
/** The structured `blocks` (Components-V2) spec the tool accepts. */
|
|
75
|
+
export interface DiscordBlocksInput {
|
|
76
|
+
blocks: DiscordBlockSpec[];
|
|
77
|
+
accentColor?: number;
|
|
78
|
+
}
|
|
79
|
+
/** A failure result — the caller renders `error` instead of shipping bad JSON. */
|
|
80
|
+
export interface DiscordRestComponentError {
|
|
81
|
+
ok: false;
|
|
82
|
+
error: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Serialize one structured select spec into a Discord action-row JSON object
|
|
86
|
+
* wrapping a select component. The select's `custom_id` carries the general
|
|
87
|
+
* callback prefix (via {@link buildDiscordSelectRow}) so a press routes through
|
|
88
|
+
* the existing select branch in `handleInteraction`. Returns an error result when
|
|
89
|
+
* the spec is unusable (empty token / over-budget id / a string select with no
|
|
90
|
+
* usable option).
|
|
91
|
+
*/
|
|
92
|
+
export declare function serializeDiscordSelectRow(input: DiscordSelectInput): {
|
|
93
|
+
ok: true;
|
|
94
|
+
row: Record<string, unknown>;
|
|
95
|
+
} | DiscordRestComponentError;
|
|
96
|
+
/**
|
|
97
|
+
* Register the modal definition in the TTL registry and serialize the
|
|
98
|
+
* MODAL-TRIGGER button into a Discord action-row JSON object. The button's
|
|
99
|
+
* `custom_id` is the `modal:<id>` marker `handleInteraction` recognizes → it
|
|
100
|
+
* calls `showModal` instead of routing a turn. Returns the row JSON + the minted
|
|
101
|
+
* modal id, or an error when the spec is unusable (empty label / no field).
|
|
102
|
+
*/
|
|
103
|
+
export declare function serializeDiscordModalTrigger(input: DiscordModalInput): {
|
|
104
|
+
ok: true;
|
|
105
|
+
row: Record<string, unknown>;
|
|
106
|
+
modalId: string;
|
|
107
|
+
} | DiscordRestComponentError;
|
|
108
|
+
/**
|
|
109
|
+
* Serialize a structured V2 (`blocks`) spec into a Components-V2 message body
|
|
110
|
+
* fragment: a single top-level CONTAINER (type 17) holding the serialized blocks,
|
|
111
|
+
* plus the {@link DISCORD_FLAG_IS_COMPONENTS_V2} flag the caller ORs into the
|
|
112
|
+
* message `flags`. The high-level spec is validated through
|
|
113
|
+
* {@link buildDiscordV2Message} first (drops empty blocks, caps section texts,
|
|
114
|
+
* rejects non-`attachment://` file refs); an empty container yields an error so
|
|
115
|
+
* the caller can fall back to a plain text send.
|
|
116
|
+
*/
|
|
117
|
+
export declare function serializeDiscordV2Message(input: DiscordBlocksInput): {
|
|
118
|
+
ok: true;
|
|
119
|
+
components: Array<Record<string, unknown>>;
|
|
120
|
+
flags: number;
|
|
121
|
+
} | DiscordRestComponentError;
|
|
122
|
+
//# sourceMappingURL=rest-components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest-components.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/rest-components.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAKN,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAIN,KAAK,gBAAgB,EAGrB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAwB,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAG1F,oDAAoD;AACpD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;CAgBzB,CAAC;AAEX,+DAA+D;AAC/D,eAAO,MAAM,uBAAuB,IAAmB,CAAC;AACxD,kDAAkD;AAClD,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAI7C,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED,4FAA4F;AAC5F,MAAM,WAAW,iBAAiB;IACjC,4BAA4B;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACtC;AAED,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IAClC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,kFAAkF;AAClF,MAAM,WAAW,yBAAyB;IACzC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACd;AAID;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACxC,KAAK,EAAE,kBAAkB,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,yBAAyB,CAuCxE;AAID;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC3C,KAAK,EAAE,iBAAiB,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,yBAAyB,CAqBzF;AA4ED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,KAAK,EAAE,kBAAkB,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,yBAAyB,CAsBrG"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord REST-JSON component serializers (Fix A1).
|
|
3
|
+
*
|
|
4
|
+
* The Phase-3 builders (`components.ts` / `component-blocks.ts`) emit discord.js
|
|
5
|
+
* *Builder* objects for the GATEWAY `sendInteractive` path. The `discord_action`
|
|
6
|
+
* tool, however, talks straight to Discord REST v10 (`rest-actions.ts`), which
|
|
7
|
+
* needs RAW component JSON — the on-the-wire shape Discord documents, identical
|
|
8
|
+
* whether discord.js serialized it or we did.
|
|
9
|
+
*
|
|
10
|
+
* This module turns high-level STRUCTURED specs (selects / modal triggers / V2
|
|
11
|
+
* blocks) into that raw JSON, carrying the SAME custom_id codecs the press-
|
|
12
|
+
* routing in `connection.ts handleInteraction` already understands:
|
|
13
|
+
*
|
|
14
|
+
* - SELECT rows get a `custom_id` prefixed with {@link GENERAL_CALLBACK_PREFIX}
|
|
15
|
+
* (via {@link buildDiscordSelectRow}) so a selection routes through the
|
|
16
|
+
* general-callback branch → `callbackQuery{ data, values }`.
|
|
17
|
+
* - MODAL triggers register the heavy form in `modal-registry.ts` and emit a
|
|
18
|
+
* button whose `custom_id` is the `modal:<id>` marker `handleInteraction`
|
|
19
|
+
* recognizes → `showModal`.
|
|
20
|
+
* - V2 blocks become Components-V2 JSON; the caller sets the `IsComponentsV2`
|
|
21
|
+
* message flag (1 << 15) and moves all text into TextDisplay blocks.
|
|
22
|
+
*
|
|
23
|
+
* Pure / deterministic — no I/O, no discord.js, no globals. Returns a discriminated
|
|
24
|
+
* result so the tool can surface a clean validation error instead of shipping a
|
|
25
|
+
* half-built component Discord would 400 on.
|
|
26
|
+
*/
|
|
27
|
+
import { DISCORD_BUTTON_STYLE, DISCORD_MAX_ROWS, DISCORD_SELECT_COMPONENT_TYPE, buildDiscordSelectRow, } from "./components.js";
|
|
28
|
+
import { DISCORD_FLAG_IS_COMPONENTS_V2, DISCORD_BUTTON_STYLE_LINK, buildDiscordV2Message, } from "./component-blocks.js";
|
|
29
|
+
import { registerDiscordModal } from "./modal-registry.js";
|
|
30
|
+
import { buildDiscordModalCustomId } from "./modals.js";
|
|
31
|
+
/** Discord component-type ids on the wire (v10). */
|
|
32
|
+
export const DISCORD_COMPONENT_TYPE = {
|
|
33
|
+
actionRow: 1,
|
|
34
|
+
button: 2,
|
|
35
|
+
stringSelect: 3,
|
|
36
|
+
textInput: 4,
|
|
37
|
+
userSelect: 5,
|
|
38
|
+
roleSelect: 6,
|
|
39
|
+
mentionableSelect: 7,
|
|
40
|
+
channelSelect: 8,
|
|
41
|
+
section: 9,
|
|
42
|
+
textDisplay: 10,
|
|
43
|
+
thumbnail: 11,
|
|
44
|
+
mediaGallery: 12,
|
|
45
|
+
file: 13,
|
|
46
|
+
separator: 14,
|
|
47
|
+
container: 17,
|
|
48
|
+
};
|
|
49
|
+
/** Discord caps a message at 5 select rows / 5 action rows. */
|
|
50
|
+
export const DISCORD_MAX_SELECT_ROWS = DISCORD_MAX_ROWS;
|
|
51
|
+
/** Discord caps a string select at 25 options. */
|
|
52
|
+
export const DISCORD_MAX_SELECT_OPTIONS = 25;
|
|
53
|
+
/* ───────────────────────────── select → JSON ───────────────────────────── */
|
|
54
|
+
/**
|
|
55
|
+
* Serialize one structured select spec into a Discord action-row JSON object
|
|
56
|
+
* wrapping a select component. The select's `custom_id` carries the general
|
|
57
|
+
* callback prefix (via {@link buildDiscordSelectRow}) so a press routes through
|
|
58
|
+
* the existing select branch in `handleInteraction`. Returns an error result when
|
|
59
|
+
* the spec is unusable (empty token / over-budget id / a string select with no
|
|
60
|
+
* usable option).
|
|
61
|
+
*/
|
|
62
|
+
export function serializeDiscordSelectRow(input) {
|
|
63
|
+
const cappedOptions = input.kind === "string" && Array.isArray(input.options)
|
|
64
|
+
? input.options.slice(0, DISCORD_MAX_SELECT_OPTIONS)
|
|
65
|
+
: input.options;
|
|
66
|
+
const spec = buildDiscordSelectRow({
|
|
67
|
+
kind: input.kind,
|
|
68
|
+
customIdToken: input.customId,
|
|
69
|
+
...(input.placeholder !== undefined ? { placeholder: input.placeholder } : {}),
|
|
70
|
+
...(typeof input.minValues === "number" ? { minValues: input.minValues } : {}),
|
|
71
|
+
...(typeof input.maxValues === "number" ? { maxValues: input.maxValues } : {}),
|
|
72
|
+
...(cappedOptions ? { options: cappedOptions } : {}),
|
|
73
|
+
});
|
|
74
|
+
if (!spec) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: input.kind === "string"
|
|
78
|
+
? "select requires a non-empty customId and at least one option with a label + value."
|
|
79
|
+
: "select requires a non-empty customId that fits Discord's 100-char custom_id budget.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const componentType = DISCORD_SELECT_COMPONENT_TYPE[spec.kind];
|
|
83
|
+
const select = {
|
|
84
|
+
type: componentType,
|
|
85
|
+
custom_id: spec.customId,
|
|
86
|
+
};
|
|
87
|
+
if (spec.placeholder)
|
|
88
|
+
select.placeholder = spec.placeholder;
|
|
89
|
+
if (typeof spec.minValues === "number")
|
|
90
|
+
select.min_values = spec.minValues;
|
|
91
|
+
if (typeof spec.maxValues === "number")
|
|
92
|
+
select.max_values = spec.maxValues;
|
|
93
|
+
if (spec.kind === "string" && spec.options) {
|
|
94
|
+
select.options = spec.options.map((opt) => ({
|
|
95
|
+
label: opt.label,
|
|
96
|
+
value: opt.value,
|
|
97
|
+
...(opt.description ? { description: opt.description } : {}),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, row: { type: DISCORD_COMPONENT_TYPE.actionRow, components: [select] } };
|
|
101
|
+
}
|
|
102
|
+
/* ───────────────────────────── modal → JSON ───────────────────────────── */
|
|
103
|
+
/**
|
|
104
|
+
* Register the modal definition in the TTL registry and serialize the
|
|
105
|
+
* MODAL-TRIGGER button into a Discord action-row JSON object. The button's
|
|
106
|
+
* `custom_id` is the `modal:<id>` marker `handleInteraction` recognizes → it
|
|
107
|
+
* calls `showModal` instead of routing a turn. Returns the row JSON + the minted
|
|
108
|
+
* modal id, or an error when the spec is unusable (empty label / no field).
|
|
109
|
+
*/
|
|
110
|
+
export function serializeDiscordModalTrigger(input) {
|
|
111
|
+
const label = (input.buttonLabel ?? "").trim();
|
|
112
|
+
if (!label)
|
|
113
|
+
return { ok: false, error: "modal requires a non-empty buttonLabel." };
|
|
114
|
+
const fields = Array.isArray(input.fields) ? input.fields.filter((f) => f && typeof f.id === "string" && f.id.trim()) : [];
|
|
115
|
+
if (fields.length === 0)
|
|
116
|
+
return { ok: false, error: "modal requires at least one field with an id." };
|
|
117
|
+
const modalId = registerDiscordModal({
|
|
118
|
+
...(input.title !== undefined ? { title: input.title } : {}),
|
|
119
|
+
fields,
|
|
120
|
+
...(input.sessionKey !== undefined ? { sessionKey: input.sessionKey } : {}),
|
|
121
|
+
...(input.agentId !== undefined ? { agentId: input.agentId } : {}),
|
|
122
|
+
...(input.accountId !== undefined ? { accountId: input.accountId } : {}),
|
|
123
|
+
...(input.allowedUsers !== undefined ? { allowedUsers: input.allowedUsers } : {}),
|
|
124
|
+
});
|
|
125
|
+
const button = {
|
|
126
|
+
type: DISCORD_COMPONENT_TYPE.button,
|
|
127
|
+
style: input.buttonStyle ?? DISCORD_BUTTON_STYLE.Primary,
|
|
128
|
+
label: label.slice(0, 80),
|
|
129
|
+
custom_id: buildDiscordModalCustomId(modalId),
|
|
130
|
+
};
|
|
131
|
+
return { ok: true, row: { type: DISCORD_COMPONENT_TYPE.actionRow, components: [button] }, modalId };
|
|
132
|
+
}
|
|
133
|
+
/* ───────────────────────────── V2 blocks → JSON ───────────────────────────── */
|
|
134
|
+
/** Serialize one V2 button (link OR general-prefixed interactive) into wire JSON. */
|
|
135
|
+
function serializeV2Button(b) {
|
|
136
|
+
if (typeof b.url === "string" && b.url.length > 0) {
|
|
137
|
+
const link = b;
|
|
138
|
+
return { type: DISCORD_COMPONENT_TYPE.button, style: DISCORD_BUTTON_STYLE_LINK, label: link.label, url: link.url };
|
|
139
|
+
}
|
|
140
|
+
const btn = b;
|
|
141
|
+
return {
|
|
142
|
+
type: DISCORD_COMPONENT_TYPE.button,
|
|
143
|
+
style: btn.style ?? DISCORD_BUTTON_STYLE.Secondary,
|
|
144
|
+
label: btn.label,
|
|
145
|
+
custom_id: btn.customId,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** Map a separator spacing word to Discord's numeric spacing (1=small, 2=large). */
|
|
149
|
+
function separatorSpacing(spacing) {
|
|
150
|
+
if (spacing === "small")
|
|
151
|
+
return 1;
|
|
152
|
+
if (spacing === "large")
|
|
153
|
+
return 2;
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
/** Serialize one validated V2 block into its Discord wire-JSON component object. */
|
|
157
|
+
function serializeV2Block(block) {
|
|
158
|
+
switch (block.type) {
|
|
159
|
+
case "text":
|
|
160
|
+
return { type: DISCORD_COMPONENT_TYPE.textDisplay, content: block.text };
|
|
161
|
+
case "section": {
|
|
162
|
+
const out = {
|
|
163
|
+
type: DISCORD_COMPONENT_TYPE.section,
|
|
164
|
+
components: block.texts.map((t) => ({ type: DISCORD_COMPONENT_TYPE.textDisplay, content: t })),
|
|
165
|
+
};
|
|
166
|
+
if (block.accessory) {
|
|
167
|
+
if (block.accessory.kind === "thumbnail") {
|
|
168
|
+
out.accessory = { type: DISCORD_COMPONENT_TYPE.thumbnail, media: { url: block.accessory.url } };
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
out.accessory = serializeV2Button(block.accessory.button);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
case "separator": {
|
|
177
|
+
const out = { type: DISCORD_COMPONENT_TYPE.separator };
|
|
178
|
+
if (typeof block.divider === "boolean")
|
|
179
|
+
out.divider = block.divider;
|
|
180
|
+
const spacing = separatorSpacing(block.spacing);
|
|
181
|
+
if (spacing !== undefined)
|
|
182
|
+
out.spacing = spacing;
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
case "actions":
|
|
186
|
+
return {
|
|
187
|
+
type: DISCORD_COMPONENT_TYPE.actionRow,
|
|
188
|
+
components: block.buttons.map((b) => serializeV2Button(b)),
|
|
189
|
+
};
|
|
190
|
+
case "media-gallery":
|
|
191
|
+
return {
|
|
192
|
+
type: DISCORD_COMPONENT_TYPE.mediaGallery,
|
|
193
|
+
items: block.items.map((it) => ({
|
|
194
|
+
media: { url: it.url },
|
|
195
|
+
...(it.description ? { description: it.description } : {}),
|
|
196
|
+
...(it.spoiler ? { spoiler: true } : {}),
|
|
197
|
+
})),
|
|
198
|
+
};
|
|
199
|
+
case "file": {
|
|
200
|
+
const out = { type: DISCORD_COMPONENT_TYPE.file, file: { url: block.url } };
|
|
201
|
+
if (typeof block.spoiler === "boolean")
|
|
202
|
+
out.spoiler = block.spoiler;
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
default:
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Serialize a structured V2 (`blocks`) spec into a Components-V2 message body
|
|
211
|
+
* fragment: a single top-level CONTAINER (type 17) holding the serialized blocks,
|
|
212
|
+
* plus the {@link DISCORD_FLAG_IS_COMPONENTS_V2} flag the caller ORs into the
|
|
213
|
+
* message `flags`. The high-level spec is validated through
|
|
214
|
+
* {@link buildDiscordV2Message} first (drops empty blocks, caps section texts,
|
|
215
|
+
* rejects non-`attachment://` file refs); an empty container yields an error so
|
|
216
|
+
* the caller can fall back to a plain text send.
|
|
217
|
+
*/
|
|
218
|
+
export function serializeDiscordV2Message(input) {
|
|
219
|
+
const spec = buildDiscordV2Message({
|
|
220
|
+
blocks: input.blocks ?? [],
|
|
221
|
+
...(typeof input.accentColor === "number" ? { accentColor: input.accentColor } : {}),
|
|
222
|
+
});
|
|
223
|
+
if (!spec) {
|
|
224
|
+
return { ok: false, error: "blocks produced no renderable Components-V2 content." };
|
|
225
|
+
}
|
|
226
|
+
const children = [];
|
|
227
|
+
for (const block of spec.blocks) {
|
|
228
|
+
const serialized = serializeV2Block(block);
|
|
229
|
+
if (serialized)
|
|
230
|
+
children.push(serialized);
|
|
231
|
+
}
|
|
232
|
+
if (children.length === 0) {
|
|
233
|
+
return { ok: false, error: "blocks produced no renderable Components-V2 content." };
|
|
234
|
+
}
|
|
235
|
+
const container = {
|
|
236
|
+
type: DISCORD_COMPONENT_TYPE.container,
|
|
237
|
+
components: children,
|
|
238
|
+
};
|
|
239
|
+
if (typeof spec.accentColor === "number")
|
|
240
|
+
container.accent_color = spec.accentColor;
|
|
241
|
+
return { ok: true, components: [container], flags: DISCORD_FLAG_IS_COMPONENTS_V2 };
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=rest-components.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rest-components.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/rest-components.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord security audit — the structured findings `brigade doctor` renders via
|
|
3
|
+
* the central `channel-security-registry.ts` (`collectChannelSecurityAudit`).
|
|
4
|
+
*
|
|
5
|
+
* The single concern here: name-based (MUTABLE) allow-from entries. A Discord
|
|
6
|
+
* username/tag can be changed by its owner, so an allow-list keyed on a name can
|
|
7
|
+
* silently grant access to a DIFFERENT person later. Id-based entries (`123`,
|
|
8
|
+
* `<@123>`, `user:123`) are stable and fine; bare names / tags / empty-prefixed
|
|
9
|
+
* entries get a `warn` finding telling the operator to use stable ids.
|
|
10
|
+
*
|
|
11
|
+
* Walks `channels.discord.allowFrom`, `channels.discord.dm.allowFrom`, and the
|
|
12
|
+
* per-guild `channels.discord.guilds.<id>.users` +
|
|
13
|
+
* `channels.discord.guilds.<id>.channels.<id>.users` lists (plus the per-account
|
|
14
|
+
* variants). Pure-ish: reads only the supplied config. Defensive — a malformed
|
|
15
|
+
* shape contributes nothing.
|
|
16
|
+
*/
|
|
17
|
+
import type { BrigadeConfig } from "../../../config/io.js";
|
|
18
|
+
import type { ChannelSecurityAuditFinding } from "../types.adapters.js";
|
|
19
|
+
/**
|
|
20
|
+
* Collect Discord security audit findings. Returns a single `warn` finding when
|
|
21
|
+
* the allow-lists contain name/tag (mutable-identity) entries, naming a few
|
|
22
|
+
* examples; returns `[]` when every entry is a stable id. The `accountId` (when
|
|
23
|
+
* supplied) selects the per-account allow-list in addition to the top-level one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function collectDiscordSecurityAuditFindings(params: {
|
|
26
|
+
cfg: BrigadeConfig;
|
|
27
|
+
accountId?: string | null;
|
|
28
|
+
}): ChannelSecurityAuditFinding[];
|
|
29
|
+
//# sourceMappingURL=security-audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-audit.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/security-audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AAqBxE;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,MAAM,EAAE;IAC3D,GAAG,EAAE,aAAa,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,GAAG,2BAA2B,EAAE,CAqDhC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord security audit — the structured findings `brigade doctor` renders via
|
|
3
|
+
* the central `channel-security-registry.ts` (`collectChannelSecurityAudit`).
|
|
4
|
+
*
|
|
5
|
+
* The single concern here: name-based (MUTABLE) allow-from entries. A Discord
|
|
6
|
+
* username/tag can be changed by its owner, so an allow-list keyed on a name can
|
|
7
|
+
* silently grant access to a DIFFERENT person later. Id-based entries (`123`,
|
|
8
|
+
* `<@123>`, `user:123`) are stable and fine; bare names / tags / empty-prefixed
|
|
9
|
+
* entries get a `warn` finding telling the operator to use stable ids.
|
|
10
|
+
*
|
|
11
|
+
* Walks `channels.discord.allowFrom`, `channels.discord.dm.allowFrom`, and the
|
|
12
|
+
* per-guild `channels.discord.guilds.<id>.users` +
|
|
13
|
+
* `channels.discord.guilds.<id>.channels.<id>.users` lists (plus the per-account
|
|
14
|
+
* variants). Pure-ish: reads only the supplied config. Defensive — a malformed
|
|
15
|
+
* shape contributes nothing.
|
|
16
|
+
*/
|
|
17
|
+
import { isDiscordMutableAllowEntry } from "./security-doctor.js";
|
|
18
|
+
const CHANNEL_ID = "discord";
|
|
19
|
+
/** Collect mutable (name-based) entries from one allow-from list into the set, with source labels. */
|
|
20
|
+
function collectMutableEntries(values, source, into) {
|
|
21
|
+
if (!Array.isArray(values))
|
|
22
|
+
return;
|
|
23
|
+
for (const value of values) {
|
|
24
|
+
const text = String(value ?? "").trim();
|
|
25
|
+
if (!text || !isDiscordMutableAllowEntry(text))
|
|
26
|
+
continue;
|
|
27
|
+
into.add(`${source}: ${text}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Read the `channels.discord` slot loosely (schema keeps it open). */
|
|
31
|
+
function discordSlot(cfg) {
|
|
32
|
+
const slot = cfg.channels?.[CHANNEL_ID];
|
|
33
|
+
return slot && typeof slot === "object" ? slot : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Collect Discord security audit findings. Returns a single `warn` finding when
|
|
37
|
+
* the allow-lists contain name/tag (mutable-identity) entries, naming a few
|
|
38
|
+
* examples; returns `[]` when every entry is a stable id. The `accountId` (when
|
|
39
|
+
* supplied) selects the per-account allow-list in addition to the top-level one.
|
|
40
|
+
*/
|
|
41
|
+
export function collectDiscordSecurityAuditFindings(params) {
|
|
42
|
+
const slot = discordSlot(params.cfg);
|
|
43
|
+
if (!slot)
|
|
44
|
+
return [];
|
|
45
|
+
const accountId = (params.accountId ?? "").trim();
|
|
46
|
+
const mutable = new Set();
|
|
47
|
+
// Top-level allow-from + dm.allowFrom.
|
|
48
|
+
collectMutableEntries(slot.allowFrom, "channels.discord.allowFrom", mutable);
|
|
49
|
+
const dm = slot.dm;
|
|
50
|
+
collectMutableEntries(dm?.allowFrom, "channels.discord.dm.allowFrom", mutable);
|
|
51
|
+
// Per-account allow-from (when an account is in scope).
|
|
52
|
+
if (accountId) {
|
|
53
|
+
const accounts = Array.isArray(slot.accounts) ? slot.accounts : [];
|
|
54
|
+
for (const entry of accounts) {
|
|
55
|
+
if (typeof entry?.id === "string" && entry.id.trim() === accountId) {
|
|
56
|
+
collectMutableEntries(entry.allowFrom, `channels.discord.accounts.${accountId}.allowFrom`, mutable);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Per-guild users + per-channel users.
|
|
61
|
+
const guilds = slot.guilds;
|
|
62
|
+
if (guilds && typeof guilds === "object") {
|
|
63
|
+
for (const [guildKey, guildValue] of Object.entries(guilds)) {
|
|
64
|
+
if (!guildValue || typeof guildValue !== "object")
|
|
65
|
+
continue;
|
|
66
|
+
const guild = guildValue;
|
|
67
|
+
collectMutableEntries(guild.users, `channels.discord.guilds.${guildKey}.users`, mutable);
|
|
68
|
+
const channels = guild.channels;
|
|
69
|
+
if (!channels || typeof channels !== "object")
|
|
70
|
+
continue;
|
|
71
|
+
for (const [channelKey, channelValue] of Object.entries(channels)) {
|
|
72
|
+
if (!channelValue || typeof channelValue !== "object")
|
|
73
|
+
continue;
|
|
74
|
+
const channel = channelValue;
|
|
75
|
+
collectMutableEntries(channel.users, `channels.discord.guilds.${guildKey}.channels.${channelKey}.users`, mutable);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (mutable.size === 0)
|
|
80
|
+
return [];
|
|
81
|
+
const examples = Array.from(mutable).slice(0, 5);
|
|
82
|
+
const more = mutable.size > examples.length ? ` (+${mutable.size - examples.length} more)` : "";
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
checkId: "channels.discord.allowFrom.name_based_entries",
|
|
86
|
+
severity: "warn",
|
|
87
|
+
title: "Discord allowlist contains name or tag entries",
|
|
88
|
+
detail: "Discord name/tag allowlist matching keys on a mutable identity — a username can be changed by its owner and later resolve to a different person. " +
|
|
89
|
+
`Found: ${examples.join(", ")}${more}.`,
|
|
90
|
+
remediation: "Prefer stable Discord ids (a numeric id, <@id>, or user:<id>) in channels.discord.allowFrom and channels.discord.guilds.*.users.",
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=security-audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-audit.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/security-audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord security-doctor helpers — the pure predicates the security audit +
|
|
3
|
+
* numeric-id scan build on.
|
|
4
|
+
*
|
|
5
|
+
* Two concerns:
|
|
6
|
+
* 1. `isDiscordMutableAllowEntry` — is an allow-from entry a MUTABLE identity
|
|
7
|
+
* (a name / tag / handle) rather than a stable id? A Discord username can be
|
|
8
|
+
* changed by its owner, so a name-based allow entry can silently grant
|
|
9
|
+
* access to a DIFFERENT person later. Id-based entries (`123`, `<@123>`,
|
|
10
|
+
* `user:123`) are stable and fine; bare names + empty-prefixed entries warn.
|
|
11
|
+
* 2. `scanDiscordNumericIdHazards` — find snowflake ids that were parsed as JS
|
|
12
|
+
* NUMBERS in config (precision loss above 2^53). A Discord id stored as a
|
|
13
|
+
* number silently corrupts above that boundary; this flags them and marks
|
|
14
|
+
* which can be safely repaired to a string (lossless) vs which already lost
|
|
15
|
+
* precision (refuse — the original digits are gone).
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* True when `raw` is a MUTABLE allow-from identity (a name/tag/handle) rather
|
|
19
|
+
* than a stable id. A bare numeric id, a `<@id>` / `<@!id>` mention, or a
|
|
20
|
+
* `discord:` / `user:` / `pk:` prefix carrying a non-empty id → stable (returns
|
|
21
|
+
* false). A bare name, or a prefix with an EMPTY id, → mutable (returns true).
|
|
22
|
+
* `*` (wildcard) and empty are not mutable identities (returns false).
|
|
23
|
+
*/
|
|
24
|
+
export declare function isDiscordMutableAllowEntry(raw: string): boolean;
|
|
25
|
+
/** One flagged snowflake-as-number hazard. */
|
|
26
|
+
export interface DiscordNumericIdHazard {
|
|
27
|
+
/** Config path where the lossy number lives (e.g. `channels.discord.guilds.123`). */
|
|
28
|
+
path: string;
|
|
29
|
+
/** The raw number as Brigade read it. */
|
|
30
|
+
value: number;
|
|
31
|
+
/** True when the value is ≤ 2^53-1 → can be losslessly repaired to a string. */
|
|
32
|
+
repairable: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Scan a `channels.discord` config slot for snowflake ids that were parsed as JS
|
|
36
|
+
* numbers (JSON without quotes). Returns the flagged hazards: `repairable: true`
|
|
37
|
+
* ones are ≤ 2^53-1 and can be safely converted to strings; `repairable: false`
|
|
38
|
+
* ones already lost precision (refuse to "repair" — the original id is
|
|
39
|
+
* unrecoverable; the operator must re-enter it as a quoted string). Pure; the
|
|
40
|
+
* caller decides whether to warn-only or repair.
|
|
41
|
+
*/
|
|
42
|
+
export declare function scanDiscordNumericIdHazards(discordConfig: unknown, basePath?: string): DiscordNumericIdHazard[];
|
|
43
|
+
//# sourceMappingURL=security-doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-doctor.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/security-doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAa/D;AAED,8CAA8C;AAC9C,MAAM,WAAW,sBAAsB;IACtC,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;CACpB;AA4BD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,aAAa,EAAE,OAAO,EAAE,QAAQ,SAAqB,GAAG,sBAAsB,EAAE,CAK3H"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord security-doctor helpers — the pure predicates the security audit +
|
|
3
|
+
* numeric-id scan build on.
|
|
4
|
+
*
|
|
5
|
+
* Two concerns:
|
|
6
|
+
* 1. `isDiscordMutableAllowEntry` — is an allow-from entry a MUTABLE identity
|
|
7
|
+
* (a name / tag / handle) rather than a stable id? A Discord username can be
|
|
8
|
+
* changed by its owner, so a name-based allow entry can silently grant
|
|
9
|
+
* access to a DIFFERENT person later. Id-based entries (`123`, `<@123>`,
|
|
10
|
+
* `user:123`) are stable and fine; bare names + empty-prefixed entries warn.
|
|
11
|
+
* 2. `scanDiscordNumericIdHazards` — find snowflake ids that were parsed as JS
|
|
12
|
+
* NUMBERS in config (precision loss above 2^53). A Discord id stored as a
|
|
13
|
+
* number silently corrupts above that boundary; this flags them and marks
|
|
14
|
+
* which can be safely repaired to a string (lossless) vs which already lost
|
|
15
|
+
* precision (refuse — the original digits are gone).
|
|
16
|
+
*/
|
|
17
|
+
/** JS-safe integer ceiling — above this a snowflake-as-number has lost precision. */
|
|
18
|
+
const MAX_SAFE = Number.MAX_SAFE_INTEGER; // 2^53 - 1
|
|
19
|
+
/**
|
|
20
|
+
* True when `raw` is a MUTABLE allow-from identity (a name/tag/handle) rather
|
|
21
|
+
* than a stable id. A bare numeric id, a `<@id>` / `<@!id>` mention, or a
|
|
22
|
+
* `discord:` / `user:` / `pk:` prefix carrying a non-empty id → stable (returns
|
|
23
|
+
* false). A bare name, or a prefix with an EMPTY id, → mutable (returns true).
|
|
24
|
+
* `*` (wildcard) and empty are not mutable identities (returns false).
|
|
25
|
+
*/
|
|
26
|
+
export function isDiscordMutableAllowEntry(raw) {
|
|
27
|
+
const text = (raw ?? "").trim();
|
|
28
|
+
if (!text || text === "*")
|
|
29
|
+
return false;
|
|
30
|
+
// `<@123>` / `<@!123>` mention → strip the wrapper; numeric inside = stable.
|
|
31
|
+
const maybeMentionId = text.replace(/^<@!?/, "").replace(/>$/, "");
|
|
32
|
+
if (/^\d+$/.test(maybeMentionId))
|
|
33
|
+
return false;
|
|
34
|
+
for (const prefix of ["discord:", "user:", "pk:"]) {
|
|
35
|
+
if (!text.startsWith(prefix))
|
|
36
|
+
continue;
|
|
37
|
+
// `user:` with an empty id is a mutable/incomplete entry; `user:123` is stable.
|
|
38
|
+
return text.slice(prefix.length).trim().length === 0;
|
|
39
|
+
}
|
|
40
|
+
// Anything else (a bare name / tag) is mutable.
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/** Recursively walk an object, flagging numeric values that look like snowflakes. */
|
|
44
|
+
function walkForNumericIds(node, path, out) {
|
|
45
|
+
if (typeof node === "number") {
|
|
46
|
+
// A Discord snowflake is a large integer. Flag big integers (≥ 1e15, near
|
|
47
|
+
// the 2^53 safe-integer ceiling) — small numbers (debounceMs, position,
|
|
48
|
+
// durations) aren't ids. `repairable` is true only when the value is still
|
|
49
|
+
// ≤ 2^53-1 (lossless to stringify); a larger value already lost precision.
|
|
50
|
+
// NOTE: real snowflakes (17-19 digits) ALWAYS exceed 2^53, so an
|
|
51
|
+
// unquoted-snowflake-in-JSON is effectively never safely repairable — this
|
|
52
|
+
// flags the hazard so the operator re-enters the id as a quoted string.
|
|
53
|
+
if (Number.isInteger(node) && node >= 1e15) {
|
|
54
|
+
out.push({ path, value: node, repairable: node <= MAX_SAFE });
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(node)) {
|
|
59
|
+
node.forEach((child, i) => walkForNumericIds(child, `${path}[${i}]`, out));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (node && typeof node === "object") {
|
|
63
|
+
for (const [key, child] of Object.entries(node)) {
|
|
64
|
+
walkForNumericIds(child, path ? `${path}.${key}` : key, out);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Scan a `channels.discord` config slot for snowflake ids that were parsed as JS
|
|
70
|
+
* numbers (JSON without quotes). Returns the flagged hazards: `repairable: true`
|
|
71
|
+
* ones are ≤ 2^53-1 and can be safely converted to strings; `repairable: false`
|
|
72
|
+
* ones already lost precision (refuse to "repair" — the original id is
|
|
73
|
+
* unrecoverable; the operator must re-enter it as a quoted string). Pure; the
|
|
74
|
+
* caller decides whether to warn-only or repair.
|
|
75
|
+
*/
|
|
76
|
+
export function scanDiscordNumericIdHazards(discordConfig, basePath = "channels.discord") {
|
|
77
|
+
const out = [];
|
|
78
|
+
if (!discordConfig || typeof discordConfig !== "object")
|
|
79
|
+
return out;
|
|
80
|
+
walkForNumericIds(discordConfig, basePath, out);
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=security-doctor.js.map
|