@spinabot/brigade 1.6.0 → 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.
Files changed (194) hide show
  1. package/dist/agents/agent-loop.d.ts.map +1 -1
  2. package/dist/agents/agent-loop.js +51 -1
  3. package/dist/agents/agent-loop.js.map +1 -1
  4. package/dist/agents/channels/bundled-channel-metas.d.ts +2 -0
  5. package/dist/agents/channels/bundled-channel-metas.d.ts.map +1 -1
  6. package/dist/agents/channels/bundled-channel-metas.js +11 -0
  7. package/dist/agents/channels/bundled-channel-metas.js.map +1 -1
  8. package/dist/agents/channels/discord/account-config.d.ts +177 -0
  9. package/dist/agents/channels/discord/account-config.d.ts.map +1 -0
  10. package/dist/agents/channels/discord/account-config.js +349 -0
  11. package/dist/agents/channels/discord/account-config.js.map +1 -0
  12. package/dist/agents/channels/discord/adapter.d.ts +79 -0
  13. package/dist/agents/channels/discord/adapter.d.ts.map +1 -0
  14. package/dist/agents/channels/discord/adapter.js +693 -0
  15. package/dist/agents/channels/discord/adapter.js.map +1 -0
  16. package/dist/agents/channels/discord/approval-authorize.d.ts +43 -0
  17. package/dist/agents/channels/discord/approval-authorize.d.ts.map +1 -0
  18. package/dist/agents/channels/discord/approval-authorize.js +71 -0
  19. package/dist/agents/channels/discord/approval-authorize.js.map +1 -0
  20. package/dist/agents/channels/discord/approval-native.d.ts +68 -0
  21. package/dist/agents/channels/discord/approval-native.d.ts.map +1 -0
  22. package/dist/agents/channels/discord/approval-native.js +81 -0
  23. package/dist/agents/channels/discord/approval-native.js.map +1 -0
  24. package/dist/agents/channels/discord/command-menu.d.ts +49 -0
  25. package/dist/agents/channels/discord/command-menu.d.ts.map +1 -0
  26. package/dist/agents/channels/discord/command-menu.js +73 -0
  27. package/dist/agents/channels/discord/command-menu.js.map +1 -0
  28. package/dist/agents/channels/discord/component-blocks.d.ts +108 -0
  29. package/dist/agents/channels/discord/component-blocks.d.ts.map +1 -0
  30. package/dist/agents/channels/discord/component-blocks.js +113 -0
  31. package/dist/agents/channels/discord/component-blocks.js.map +1 -0
  32. package/dist/agents/channels/discord/components.d.ts +175 -0
  33. package/dist/agents/channels/discord/components.d.ts.map +1 -0
  34. package/dist/agents/channels/discord/components.js +220 -0
  35. package/dist/agents/channels/discord/components.js.map +1 -0
  36. package/dist/agents/channels/discord/connection.d.ts +570 -0
  37. package/dist/agents/channels/discord/connection.d.ts.map +1 -0
  38. package/dist/agents/channels/discord/connection.js +1600 -0
  39. package/dist/agents/channels/discord/connection.js.map +1 -0
  40. package/dist/agents/channels/discord/directory-cache.d.ts +47 -0
  41. package/dist/agents/channels/discord/directory-cache.d.ts.map +1 -0
  42. package/dist/agents/channels/discord/directory-cache.js +131 -0
  43. package/dist/agents/channels/discord/directory-cache.js.map +1 -0
  44. package/dist/agents/channels/discord/directory-live.d.ts +61 -0
  45. package/dist/agents/channels/discord/directory-live.d.ts.map +1 -0
  46. package/dist/agents/channels/discord/directory-live.js +140 -0
  47. package/dist/agents/channels/discord/directory-live.js.map +1 -0
  48. package/dist/agents/channels/discord/draft-stream.d.ts +92 -0
  49. package/dist/agents/channels/discord/draft-stream.d.ts.map +1 -0
  50. package/dist/agents/channels/discord/draft-stream.js +213 -0
  51. package/dist/agents/channels/discord/draft-stream.js.map +1 -0
  52. package/dist/agents/channels/discord/format.d.ts +70 -0
  53. package/dist/agents/channels/discord/format.d.ts.map +1 -0
  54. package/dist/agents/channels/discord/format.js +303 -0
  55. package/dist/agents/channels/discord/format.js.map +1 -0
  56. package/dist/agents/channels/discord/guilds.d.ts +25 -0
  57. package/dist/agents/channels/discord/guilds.d.ts.map +1 -0
  58. package/dist/agents/channels/discord/guilds.js +46 -0
  59. package/dist/agents/channels/discord/guilds.js.map +1 -0
  60. package/dist/agents/channels/discord/inbound-extras.d.ts +377 -0
  61. package/dist/agents/channels/discord/inbound-extras.d.ts.map +1 -0
  62. package/dist/agents/channels/discord/inbound-extras.js +589 -0
  63. package/dist/agents/channels/discord/inbound-extras.js.map +1 -0
  64. package/dist/agents/channels/discord/index.d.ts +21 -0
  65. package/dist/agents/channels/discord/index.d.ts.map +1 -0
  66. package/dist/agents/channels/discord/index.js +21 -0
  67. package/dist/agents/channels/discord/index.js.map +1 -0
  68. package/dist/agents/channels/discord/media.d.ts +85 -0
  69. package/dist/agents/channels/discord/media.d.ts.map +1 -0
  70. package/dist/agents/channels/discord/media.js +242 -0
  71. package/dist/agents/channels/discord/media.js.map +1 -0
  72. package/dist/agents/channels/discord/modal-registry.d.ts +89 -0
  73. package/dist/agents/channels/discord/modal-registry.d.ts.map +1 -0
  74. package/dist/agents/channels/discord/modal-registry.js +104 -0
  75. package/dist/agents/channels/discord/modal-registry.js.map +1 -0
  76. package/dist/agents/channels/discord/modals.d.ts +100 -0
  77. package/dist/agents/channels/discord/modals.d.ts.map +1 -0
  78. package/dist/agents/channels/discord/modals.js +124 -0
  79. package/dist/agents/channels/discord/modals.js.map +1 -0
  80. package/dist/agents/channels/discord/module.d.ts +15 -0
  81. package/dist/agents/channels/discord/module.d.ts.map +1 -0
  82. package/dist/agents/channels/discord/module.js +22 -0
  83. package/dist/agents/channels/discord/module.js.map +1 -0
  84. package/dist/agents/channels/discord/permission-audit.d.ts +43 -0
  85. package/dist/agents/channels/discord/permission-audit.d.ts.map +1 -0
  86. package/dist/agents/channels/discord/permission-audit.js +192 -0
  87. package/dist/agents/channels/discord/permission-audit.js.map +1 -0
  88. package/dist/agents/channels/discord/plugin.d.ts +89 -0
  89. package/dist/agents/channels/discord/plugin.d.ts.map +1 -0
  90. package/dist/agents/channels/discord/plugin.js +372 -0
  91. package/dist/agents/channels/discord/plugin.js.map +1 -0
  92. package/dist/agents/channels/discord/probe.d.ts +115 -0
  93. package/dist/agents/channels/discord/probe.d.ts.map +1 -0
  94. package/dist/agents/channels/discord/probe.js +193 -0
  95. package/dist/agents/channels/discord/probe.js.map +1 -0
  96. package/dist/agents/channels/discord/reasoning-lane.d.ts +42 -0
  97. package/dist/agents/channels/discord/reasoning-lane.d.ts.map +1 -0
  98. package/dist/agents/channels/discord/reasoning-lane.js +68 -0
  99. package/dist/agents/channels/discord/reasoning-lane.js.map +1 -0
  100. package/dist/agents/channels/discord/rest-actions.d.ts +346 -0
  101. package/dist/agents/channels/discord/rest-actions.d.ts.map +1 -0
  102. package/dist/agents/channels/discord/rest-actions.js +559 -0
  103. package/dist/agents/channels/discord/rest-actions.js.map +1 -0
  104. package/dist/agents/channels/discord/rest-components.d.ts +122 -0
  105. package/dist/agents/channels/discord/rest-components.d.ts.map +1 -0
  106. package/dist/agents/channels/discord/rest-components.js +243 -0
  107. package/dist/agents/channels/discord/rest-components.js.map +1 -0
  108. package/dist/agents/channels/discord/security-audit.d.ts +29 -0
  109. package/dist/agents/channels/discord/security-audit.d.ts.map +1 -0
  110. package/dist/agents/channels/discord/security-audit.js +94 -0
  111. package/dist/agents/channels/discord/security-audit.js.map +1 -0
  112. package/dist/agents/channels/discord/security-doctor.d.ts +43 -0
  113. package/dist/agents/channels/discord/security-doctor.d.ts.map +1 -0
  114. package/dist/agents/channels/discord/security-doctor.js +83 -0
  115. package/dist/agents/channels/discord/security-doctor.js.map +1 -0
  116. package/dist/agents/channels/discord/status-issues.d.ts +37 -0
  117. package/dist/agents/channels/discord/status-issues.d.ts.map +1 -0
  118. package/dist/agents/channels/discord/status-issues.js +66 -0
  119. package/dist/agents/channels/discord/status-issues.js.map +1 -0
  120. package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts +57 -0
  121. package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts.map +1 -0
  122. package/dist/agents/channels/discord/subagent-thread-binding-store.js +98 -0
  123. package/dist/agents/channels/discord/subagent-thread-binding-store.js.map +1 -0
  124. package/dist/agents/channels/discord/subagent-thread-binding.d.ts +95 -0
  125. package/dist/agents/channels/discord/subagent-thread-binding.d.ts.map +1 -0
  126. package/dist/agents/channels/discord/subagent-thread-binding.js +208 -0
  127. package/dist/agents/channels/discord/subagent-thread-binding.js.map +1 -0
  128. package/dist/agents/channels/discord/system-events.d.ts +31 -0
  129. package/dist/agents/channels/discord/system-events.d.ts.map +1 -0
  130. package/dist/agents/channels/discord/system-events.js +74 -0
  131. package/dist/agents/channels/discord/system-events.js.map +1 -0
  132. package/dist/agents/channels/general-callback.d.ts +12 -0
  133. package/dist/agents/channels/general-callback.d.ts.map +1 -1
  134. package/dist/agents/channels/general-callback.js +18 -0
  135. package/dist/agents/channels/general-callback.js.map +1 -1
  136. package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
  137. package/dist/agents/channels/inbound-pipeline.js +70 -10
  138. package/dist/agents/channels/inbound-pipeline.js.map +1 -1
  139. package/dist/agents/channels/sdk.d.ts +2 -0
  140. package/dist/agents/channels/sdk.d.ts.map +1 -1
  141. package/dist/agents/channels/sdk.js +2 -0
  142. package/dist/agents/channels/sdk.js.map +1 -1
  143. package/dist/agents/extensions/modules/index.d.ts.map +1 -1
  144. package/dist/agents/extensions/modules/index.js +5 -0
  145. package/dist/agents/extensions/modules/index.js.map +1 -1
  146. package/dist/agents/extensions/types.d.ts +7 -0
  147. package/dist/agents/extensions/types.d.ts.map +1 -1
  148. package/dist/agents/extensions/types.js.map +1 -1
  149. package/dist/agents/subagent-announce-delivery.d.ts +10 -0
  150. package/dist/agents/subagent-announce-delivery.d.ts.map +1 -1
  151. package/dist/agents/subagent-announce-delivery.js +1 -0
  152. package/dist/agents/subagent-announce-delivery.js.map +1 -1
  153. package/dist/agents/subagent-completion-bridge.d.ts.map +1 -1
  154. package/dist/agents/subagent-completion-bridge.js +81 -0
  155. package/dist/agents/subagent-completion-bridge.js.map +1 -1
  156. package/dist/agents/subagent-spawn.d.ts.map +1 -1
  157. package/dist/agents/subagent-spawn.js +57 -4
  158. package/dist/agents/subagent-spawn.js.map +1 -1
  159. package/dist/agents/tools/cron-tool.d.ts.map +1 -1
  160. package/dist/agents/tools/cron-tool.js +4 -1
  161. package/dist/agents/tools/cron-tool.js.map +1 -1
  162. package/dist/agents/tools/discord-action-tool.d.ts +224 -0
  163. package/dist/agents/tools/discord-action-tool.d.ts.map +1 -0
  164. package/dist/agents/tools/discord-action-tool.js +848 -0
  165. package/dist/agents/tools/discord-action-tool.js.map +1 -0
  166. package/dist/agents/tools/registry.d.ts.map +1 -1
  167. package/dist/agents/tools/registry.js +21 -0
  168. package/dist/agents/tools/registry.js.map +1 -1
  169. package/dist/agents/tools/sessions/index.d.ts +8 -0
  170. package/dist/agents/tools/sessions/index.d.ts.map +1 -1
  171. package/dist/agents/tools/sessions/index.js +15 -3
  172. package/dist/agents/tools/sessions/index.js.map +1 -1
  173. package/dist/buildstamp.json +1 -1
  174. package/dist/cli/commands/channels.d.ts +2 -0
  175. package/dist/cli/commands/channels.d.ts.map +1 -1
  176. package/dist/cli/commands/channels.js +58 -1
  177. package/dist/cli/commands/channels.js.map +1 -1
  178. package/dist/core/auth-bridge.d.ts +1 -0
  179. package/dist/core/auth-bridge.d.ts.map +1 -1
  180. package/dist/core/auth-bridge.js +46 -1
  181. package/dist/core/auth-bridge.js.map +1 -1
  182. package/dist/core/server.d.ts.map +1 -1
  183. package/dist/core/server.js +18 -2
  184. package/dist/core/server.js.map +1 -1
  185. package/dist/cron/isolated-agent/run-executor.d.ts +11 -0
  186. package/dist/cron/isolated-agent/run-executor.d.ts.map +1 -1
  187. package/dist/cron/isolated-agent/run-executor.js +20 -4
  188. package/dist/cron/isolated-agent/run-executor.js.map +1 -1
  189. package/dist/cron/types.d.ts +8 -0
  190. package/dist/cron/types.d.ts.map +1 -1
  191. package/dist/system-prompt/assembler.d.ts.map +1 -1
  192. package/dist/system-prompt/assembler.js +4 -2
  193. package/dist/system-prompt/assembler.js.map +1 -1
  194. package/package.json +2 -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