@open-everywhere/provider-discord 0.1.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 (166) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +47 -0
  4. package/agent/access/discord-grant-click.ts +237 -0
  5. package/agent/approvals/discord-admin.ts +227 -0
  6. package/agent/approvals/discord-card.ts +248 -0
  7. package/agent/approvals/discord-flow.ts +469 -0
  8. package/agent/approvals/discord-scope-label.ts +27 -0
  9. package/agent/channels/discord-approval-card.ts +373 -0
  10. package/agent/channels/discord-backscroll.ts +593 -0
  11. package/agent/channels/discord-bot-name.ts +113 -0
  12. package/agent/channels/discord-channel-info.ts +83 -0
  13. package/agent/channels/discord-directory.ts +179 -0
  14. package/agent/channels/discord-dispatch-target.ts +103 -0
  15. package/agent/channels/discord-events.ts +175 -0
  16. package/agent/channels/discord-front.ts +355 -0
  17. package/agent/channels/discord-gateway.ts +3256 -0
  18. package/agent/channels/discord-identity.ts +269 -0
  19. package/agent/channels/discord-interaction-defer.ts +123 -0
  20. package/agent/channels/discord-message-ops.ts +127 -0
  21. package/agent/channels/discord-origin-codec.ts +105 -0
  22. package/agent/channels/discord-post-arm.ts +230 -0
  23. package/agent/channels/discord-post.ts +463 -0
  24. package/agent/channels/discord-read.ts +140 -0
  25. package/agent/channels/discord-rest.ts +85 -0
  26. package/agent/channels/discord.ts +244 -0
  27. package/agent/commands/discord-command-sync.ts +285 -0
  28. package/agent/commands/discord-commands.ts +291 -0
  29. package/agent/contributions.ts +31 -0
  30. package/agent/docs-generated.ts +15 -0
  31. package/agent/identity/discord-link-consent.ts +124 -0
  32. package/agent/ops.ts +14 -0
  33. package/agent/provider-events.ts +86 -0
  34. package/agent/register.ts +147 -0
  35. package/agent/surface-parity.ts +95 -0
  36. package/dist/agent/access/discord-grant-click.d.ts +25 -0
  37. package/dist/agent/access/discord-grant-click.d.ts.map +1 -0
  38. package/dist/agent/access/discord-grant-click.js +210 -0
  39. package/dist/agent/access/discord-grant-click.js.map +1 -0
  40. package/dist/agent/approvals/discord-admin.d.ts +63 -0
  41. package/dist/agent/approvals/discord-admin.d.ts.map +1 -0
  42. package/dist/agent/approvals/discord-admin.js +219 -0
  43. package/dist/agent/approvals/discord-admin.js.map +1 -0
  44. package/dist/agent/approvals/discord-card.d.ts +102 -0
  45. package/dist/agent/approvals/discord-card.d.ts.map +1 -0
  46. package/dist/agent/approvals/discord-card.js +217 -0
  47. package/dist/agent/approvals/discord-card.js.map +1 -0
  48. package/dist/agent/approvals/discord-flow.d.ts +109 -0
  49. package/dist/agent/approvals/discord-flow.d.ts.map +1 -0
  50. package/dist/agent/approvals/discord-flow.js +375 -0
  51. package/dist/agent/approvals/discord-flow.js.map +1 -0
  52. package/dist/agent/approvals/discord-scope-label.d.ts +3 -0
  53. package/dist/agent/approvals/discord-scope-label.d.ts.map +1 -0
  54. package/dist/agent/approvals/discord-scope-label.js +27 -0
  55. package/dist/agent/approvals/discord-scope-label.js.map +1 -0
  56. package/dist/agent/channels/discord-approval-card.d.ts +81 -0
  57. package/dist/agent/channels/discord-approval-card.d.ts.map +1 -0
  58. package/dist/agent/channels/discord-approval-card.js +303 -0
  59. package/dist/agent/channels/discord-approval-card.js.map +1 -0
  60. package/dist/agent/channels/discord-backscroll.d.ts +209 -0
  61. package/dist/agent/channels/discord-backscroll.d.ts.map +1 -0
  62. package/dist/agent/channels/discord-backscroll.js +505 -0
  63. package/dist/agent/channels/discord-backscroll.js.map +1 -0
  64. package/dist/agent/channels/discord-bot-name.d.ts +19 -0
  65. package/dist/agent/channels/discord-bot-name.d.ts.map +1 -0
  66. package/dist/agent/channels/discord-bot-name.js +98 -0
  67. package/dist/agent/channels/discord-bot-name.js.map +1 -0
  68. package/dist/agent/channels/discord-channel-info.d.ts +22 -0
  69. package/dist/agent/channels/discord-channel-info.d.ts.map +1 -0
  70. package/dist/agent/channels/discord-channel-info.js +65 -0
  71. package/dist/agent/channels/discord-channel-info.js.map +1 -0
  72. package/dist/agent/channels/discord-directory.d.ts +11 -0
  73. package/dist/agent/channels/discord-directory.d.ts.map +1 -0
  74. package/dist/agent/channels/discord-directory.js +170 -0
  75. package/dist/agent/channels/discord-directory.js.map +1 -0
  76. package/dist/agent/channels/discord-dispatch-target.d.ts +15 -0
  77. package/dist/agent/channels/discord-dispatch-target.d.ts.map +1 -0
  78. package/dist/agent/channels/discord-dispatch-target.js +99 -0
  79. package/dist/agent/channels/discord-dispatch-target.js.map +1 -0
  80. package/dist/agent/channels/discord-events.d.ts +58 -0
  81. package/dist/agent/channels/discord-events.d.ts.map +1 -0
  82. package/dist/agent/channels/discord-events.js +150 -0
  83. package/dist/agent/channels/discord-events.js.map +1 -0
  84. package/dist/agent/channels/discord-front.d.ts +115 -0
  85. package/dist/agent/channels/discord-front.d.ts.map +1 -0
  86. package/dist/agent/channels/discord-front.js +279 -0
  87. package/dist/agent/channels/discord-front.js.map +1 -0
  88. package/dist/agent/channels/discord-gateway.d.ts +783 -0
  89. package/dist/agent/channels/discord-gateway.d.ts.map +1 -0
  90. package/dist/agent/channels/discord-gateway.js +2591 -0
  91. package/dist/agent/channels/discord-gateway.js.map +1 -0
  92. package/dist/agent/channels/discord-identity.d.ts +110 -0
  93. package/dist/agent/channels/discord-identity.d.ts.map +1 -0
  94. package/dist/agent/channels/discord-identity.js +201 -0
  95. package/dist/agent/channels/discord-identity.js.map +1 -0
  96. package/dist/agent/channels/discord-interaction-defer.d.ts +33 -0
  97. package/dist/agent/channels/discord-interaction-defer.d.ts.map +1 -0
  98. package/dist/agent/channels/discord-interaction-defer.js +102 -0
  99. package/dist/agent/channels/discord-interaction-defer.js.map +1 -0
  100. package/dist/agent/channels/discord-message-ops.d.ts +3 -0
  101. package/dist/agent/channels/discord-message-ops.d.ts.map +1 -0
  102. package/dist/agent/channels/discord-message-ops.js +123 -0
  103. package/dist/agent/channels/discord-message-ops.js.map +1 -0
  104. package/dist/agent/channels/discord-origin-codec.d.ts +7 -0
  105. package/dist/agent/channels/discord-origin-codec.d.ts.map +1 -0
  106. package/dist/agent/channels/discord-origin-codec.js +98 -0
  107. package/dist/agent/channels/discord-origin-codec.js.map +1 -0
  108. package/dist/agent/channels/discord-post-arm.d.ts +3 -0
  109. package/dist/agent/channels/discord-post-arm.d.ts.map +1 -0
  110. package/dist/agent/channels/discord-post-arm.js +195 -0
  111. package/dist/agent/channels/discord-post-arm.js.map +1 -0
  112. package/dist/agent/channels/discord-post.d.ts +120 -0
  113. package/dist/agent/channels/discord-post.d.ts.map +1 -0
  114. package/dist/agent/channels/discord-post.js +426 -0
  115. package/dist/agent/channels/discord-post.js.map +1 -0
  116. package/dist/agent/channels/discord-read.d.ts +40 -0
  117. package/dist/agent/channels/discord-read.d.ts.map +1 -0
  118. package/dist/agent/channels/discord-read.js +128 -0
  119. package/dist/agent/channels/discord-read.js.map +1 -0
  120. package/dist/agent/channels/discord-rest.d.ts +33 -0
  121. package/dist/agent/channels/discord-rest.d.ts.map +1 -0
  122. package/dist/agent/channels/discord-rest.js +75 -0
  123. package/dist/agent/channels/discord-rest.js.map +1 -0
  124. package/dist/agent/channels/discord.d.ts +3 -0
  125. package/dist/agent/channels/discord.d.ts.map +1 -0
  126. package/dist/agent/channels/discord.js +230 -0
  127. package/dist/agent/channels/discord.js.map +1 -0
  128. package/dist/agent/commands/discord-command-sync.d.ts +66 -0
  129. package/dist/agent/commands/discord-command-sync.d.ts.map +1 -0
  130. package/dist/agent/commands/discord-command-sync.js +224 -0
  131. package/dist/agent/commands/discord-command-sync.js.map +1 -0
  132. package/dist/agent/commands/discord-commands.d.ts +45 -0
  133. package/dist/agent/commands/discord-commands.d.ts.map +1 -0
  134. package/dist/agent/commands/discord-commands.js +266 -0
  135. package/dist/agent/commands/discord-commands.js.map +1 -0
  136. package/dist/agent/contributions.d.ts +17 -0
  137. package/dist/agent/contributions.d.ts.map +1 -0
  138. package/dist/agent/contributions.js +31 -0
  139. package/dist/agent/contributions.js.map +1 -0
  140. package/dist/agent/docs-generated.d.ts +3 -0
  141. package/dist/agent/docs-generated.d.ts.map +1 -0
  142. package/dist/agent/docs-generated.js +13 -0
  143. package/dist/agent/docs-generated.js.map +1 -0
  144. package/dist/agent/identity/discord-link-consent.d.ts +13 -0
  145. package/dist/agent/identity/discord-link-consent.d.ts.map +1 -0
  146. package/dist/agent/identity/discord-link-consent.js +111 -0
  147. package/dist/agent/identity/discord-link-consent.js.map +1 -0
  148. package/dist/agent/ops.d.ts +2 -0
  149. package/dist/agent/ops.d.ts.map +1 -0
  150. package/dist/agent/ops.js +13 -0
  151. package/dist/agent/ops.js.map +1 -0
  152. package/dist/agent/provider-events.d.ts +3 -0
  153. package/dist/agent/provider-events.d.ts.map +1 -0
  154. package/dist/agent/provider-events.js +78 -0
  155. package/dist/agent/provider-events.js.map +1 -0
  156. package/dist/agent/register.d.ts +5 -0
  157. package/dist/agent/register.d.ts.map +1 -0
  158. package/dist/agent/register.js +125 -0
  159. package/dist/agent/register.js.map +1 -0
  160. package/dist/agent/surface-parity.d.ts +3 -0
  161. package/dist/agent/surface-parity.d.ts.map +1 -0
  162. package/dist/agent/surface-parity.js +90 -0
  163. package/dist/agent/surface-parity.js.map +1 -0
  164. package/package.json +185 -0
  165. package/tsconfig.build.json +22 -0
  166. package/tsconfig.json +15 -0
@@ -0,0 +1,98 @@
1
+ // discord-bot-name — the bot's OWN handle on Discord, read from `GET /users/@me` (OPE-610). The
2
+ // discord arm of core's per-surface bot-display-name registry (identity/bot-display-name.ts), so a
3
+ // Discord-pinned session's instructions can name the address this deployment's app is mentioned by.
4
+ //
5
+ // ── A HANDLE, NOT AN IDENTITY (OPE-610) ──────────────────────────────────────────────────────────
6
+ // Tommy settled the naming model on the ticket (2026-08-18): one org agent keeps ONE name
7
+ // (`identity.name`) on every surface it speaks through; a per-surface app's own display name is a
8
+ // HANDLE/address, never a substitute identity. This arm answers "what is this app's own Discord
9
+ // account called" — instructions/access.ts is the ONLY consumer, and it renders the answer as a
10
+ // handle line ("your mention/handle on Discord appears as X"), never as the identity line. That
11
+ // split lives in access.ts, not here; this module's only job is a correct, cached answer to
12
+ // `GET /users/@me`.
13
+ //
14
+ // ── WHY A NEW CALL, NOT THE PER-MESSAGE RELAY VALUE ──────────────────────────────────────────────
15
+ // discord-gateway.ts already carries a `botName` PER MESSAGE (the relay's `bot.name`), which feeds
16
+ // the wake name-drop matcher (`wakeNameDropTokens`, turn-delivery.ts) — that coupling is
17
+ // deliberately untouched by this change; a resolver that disagreed with the per-message value would
18
+ // leave the agent recognising one name in backscroll and being told about a different one in its
19
+ // instructions. The registry arm needs a CACHED, delivery-independent answer (the assembler renders
20
+ // the identity/handle block on every turn, not just on a delivery that happens to carry `bot.name`),
21
+ // so it calls Discord directly and memoises the result — the same shape teams-bot-name.ts uses for
22
+ // Microsoft Graph.
23
+ //
24
+ // ── WHICH FIELD ──────────────────────────────────────────────────────────────────────────────────
25
+ // `GET /users/@me` returns `username` (the account's Discord username) and `global_name` (the
26
+ // newer display name shown in clients; frequently `null` for a bot account, which almost never sets
27
+ // one). `global_name` wins when present and non-blank — it is what a human actually SEES beside the
28
+ // bot's messages — and `username` is the fallback every bot account always has.
29
+ //
30
+ // ── CACHING ──────────────────────────────────────────────────────────────────────────────────────
31
+ // One memoised promise per instance, matching the slack/teams arms exactly: the instruction
32
+ // assembler renders the handle line every turn, so an uncached arm would add a REST round trip per
33
+ // turn. A rename needs a cold start to take effect. A FAILED resolution is cached too, so a
34
+ // deployment missing the token does not re-probe Discord on every turn.
35
+ const DISCORD_API_BASE = "https://discord.com/api/v10";
36
+ const liveDiscordSelfRequest = async () => {
37
+ const token = process.env.DISCORD_BOT_TOKEN;
38
+ if (!token)
39
+ throw new Error("bot-display-name: DISCORD_BOT_TOKEN is not set.");
40
+ const res = await fetch(`${DISCORD_API_BASE}/users/@me`, {
41
+ headers: { authorization: `Bot ${token}` },
42
+ });
43
+ if (!res.ok)
44
+ throw new Error(`bot-display-name: Discord GET /users/@me failed (HTTP ${res.status}).`);
45
+ return res.json();
46
+ };
47
+ function trimmedName(value) {
48
+ if (typeof value !== "string")
49
+ return undefined;
50
+ const trimmed = value.trim();
51
+ return trimmed.length > 0 ? trimmed : undefined;
52
+ }
53
+ /**
54
+ * The bot's own Discord handle — `global_name` when Discord reports one, else `username` — or
55
+ * `undefined` when neither resolves. Pure but for the injected `req`, so the resolution rule is
56
+ * unit-testable with a canned requester: no token mint and no network call. FAIL-SOFT on
57
+ * everything: a thrown request (no token / non-ok status / transport failure) and a response that
58
+ * is not an object both read `undefined`.
59
+ */
60
+ export async function discordBotDisplayNameViaRest(req) {
61
+ try {
62
+ const json = (await req());
63
+ return trimmedName(json?.global_name) ?? trimmedName(json?.username);
64
+ }
65
+ catch {
66
+ return undefined; // no token / 401 / transport failure ⇒ core renders no handle line
67
+ }
68
+ }
69
+ /** The memoised per-instance promise — see the CACHING note in the header. */
70
+ let handlePromise;
71
+ /** Live arm: {@link discordBotDisplayNameViaRest} through the real REST transport, resolved once
72
+ * per instance and registered into core's registry from register.ts. */
73
+ export const discordBotDisplayName = () => {
74
+ if (!handlePromise) {
75
+ handlePromise = (async () => {
76
+ try {
77
+ // The memoised value is a PROMISE and instruction assembly awaits it on every Discord turn,
78
+ // so a hung REST call would block every turn on the instance, not one. The race bounds it:
79
+ // a stall settles the memo to `undefined` (no handle line), same as any other miss —
80
+ // matching teams-bot-name.ts's bound.
81
+ const timeout = new Promise((resolve) => {
82
+ const t = setTimeout(() => resolve(undefined), 5000);
83
+ t.unref?.();
84
+ });
85
+ return await Promise.race([discordBotDisplayNameViaRest(liveDiscordSelfRequest), timeout]);
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ })();
91
+ }
92
+ return handlePromise;
93
+ };
94
+ /** Test seam: drop the memoised handle so one test cannot inherit another's. */
95
+ export function _resetDiscordBotDisplayNameForTest() {
96
+ handlePromise = undefined;
97
+ }
98
+ //# sourceMappingURL=discord-bot-name.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-bot-name.js","sourceRoot":"","sources":["../../../agent/channels/discord-bot-name.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /** The channel facts both consumers read. `parentId` is set ONLY for threads. */
2
+ export interface DiscordChannelInfo {
3
+ readonly name?: string;
4
+ /** Present (with a thread type) when the channel is a THREAD — its parent channel id. */
5
+ readonly parentId?: string;
6
+ readonly isThread: boolean;
7
+ }
8
+ /** Injectable seams (tests swap both; live callers pass nothing). */
9
+ export interface DiscordInfoDeps {
10
+ readonly fetchFn?: typeof fetch;
11
+ readonly token?: string;
12
+ }
13
+ /** Test hook: drop every cached verdict (unit suites re-run lookups with fresh fakes). */
14
+ export declare function clearDiscordInfoCacheForTest(): void;
15
+ export declare function fetchDiscordInfo(kind: "channels" | "guilds", id: string, deps?: DiscordInfoDeps): Promise<DiscordChannelInfo | undefined>;
16
+ /**
17
+ * The parent channel id of a Discord THREAD, or undefined for a non-thread channel or
18
+ * ANY lookup failure. The grain consumer's convenience: undefined means "keep today's
19
+ * thread-grain behaviour" (fail-open — parent resolution must never block a turn).
20
+ */
21
+ export declare function threadParentChannelId(channelId: string, deps?: DiscordInfoDeps): Promise<string | undefined>;
22
+ //# sourceMappingURL=discord-channel-info.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-channel-info.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-channel-info.ts"],"names":[],"mappings":"AAcA,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,qEAAqE;AACrE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AASD,0FAA0F;AAC1F,wBAAgB,4BAA4B,IAAI,IAAI,CAEnD;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,UAAU,GAAG,QAAQ,EAC3B,EAAE,EAAE,MAAM,EACV,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CA4BzC;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAGtH"}
@@ -0,0 +1,65 @@
1
+ // Shared Discord channel-info lookup — `GET /channels/{id}` (and `GET /guilds/{id}`)
2
+ // via the bot token, cached per id. ONE impl + ONE cache for the two consumers that
3
+ // need the same facts:
4
+ // - approvals/scope-label.ts — friendly card labels ("#parent · thread \"…\"");
5
+ // - channels/discord-gateway.ts — thread→parent grain resolution (backlog item 22):
6
+ // a Discord thread is its OWN channel id, so domain/spend grain must resolve the
7
+ // PARENT channel or a thread silently falls out of its channel's domain and cap.
8
+ //
9
+ // FAIL-OPEN BY CONTRACT: every failure (no token, non-ok status, network error,
10
+ // malformed body) resolves `undefined` — the caller degrades (raw key on a card,
11
+ // thread-grain accounting on the gateway), never blocks or throws. The bot token is a
12
+ // SECRET — header only, never logged. Verdicts (including some failures — see the
13
+ // cache note) are cached in-process; a serverless instance pays each lookup once.
14
+ // Promise-cached per `kind:id` — concurrent callers share one in-flight request, and a
15
+ // settled result (including `undefined` from a hard failure) is kept for the instance's
16
+ // life: names/parents change on admin edits, not per message, and the pre-extraction
17
+ // scope-label cache behaved exactly this way (a card propose + its decided re-render
18
+ // must not pay twice).
19
+ const infoCache = new Map();
20
+ /** Test hook: drop every cached verdict (unit suites re-run lookups with fresh fakes). */
21
+ export function clearDiscordInfoCacheForTest() {
22
+ infoCache.clear();
23
+ }
24
+ export function fetchDiscordInfo(kind, id, deps = {}) {
25
+ const key = `${kind}:${id}`;
26
+ let p = infoCache.get(key);
27
+ if (!p) {
28
+ p = (async () => {
29
+ const token = deps.token ?? process.env.DISCORD_BOT_TOKEN;
30
+ if (!token)
31
+ return undefined;
32
+ try {
33
+ const doFetch = deps.fetchFn ?? fetch;
34
+ const res = await doFetch(`https://discord.com/api/v10/${kind}/${id}`, {
35
+ headers: { authorization: `Bot ${token}` },
36
+ });
37
+ if (!res.ok)
38
+ return undefined;
39
+ const body = (await res.json());
40
+ const name = typeof body.name === "string" && body.name.trim() ? body.name.trim() : undefined;
41
+ const isThread = body.type === 10 || body.type === 11 || body.type === 12;
42
+ const parentId = isThread && typeof body.parent_id === "string" && body.parent_id ? body.parent_id : undefined;
43
+ // A nameless channel resolves undefined wholesale — kept from the original
44
+ // scope-label impl (its callers require the name), and harmless for the grain
45
+ // consumer: every real guild channel/thread carries a name.
46
+ return name ? { name, parentId, isThread } : undefined;
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ })();
52
+ infoCache.set(key, p);
53
+ }
54
+ return p;
55
+ }
56
+ /**
57
+ * The parent channel id of a Discord THREAD, or undefined for a non-thread channel or
58
+ * ANY lookup failure. The grain consumer's convenience: undefined means "keep today's
59
+ * thread-grain behaviour" (fail-open — parent resolution must never block a turn).
60
+ */
61
+ export async function threadParentChannelId(channelId, deps = {}) {
62
+ const info = await fetchDiscordInfo("channels", channelId, deps);
63
+ return info?.parentId;
64
+ }
65
+ //# sourceMappingURL=discord-channel-info.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-channel-info.js","sourceRoot":"","sources":["../../../agent/channels/discord-channel-info.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,oFAAoF;AACpF,uBAAuB;AACvB,kFAAkF;AAClF,sFAAsF;AACtF,qFAAqF;AACrF,qFAAqF;AACrF,EAAE;AACF,gFAAgF;AAChF,iFAAiF;AACjF,sFAAsF;AACtF,kFAAkF;AAClF,kFAAkF;AAgBlF,uFAAuF;AACvF,wFAAwF;AACxF,qFAAqF;AACrF,qFAAqF;AACrF,uBAAuB;AACvB,MAAM,SAAS,GAAG,IAAI,GAAG,EAAmD,CAAC;AAE7E,0FAA0F;AAC1F,MAAM,UAAU,4BAA4B;IAC1C,SAAS,CAAC,KAAK,EAAE,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,IAA2B,EAC3B,EAAU,EACV,IAAI,GAAoB,EAAE;IAE1B,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;IAC5B,IAAI,CAAC,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YACd,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;YAC1D,IAAI,CAAC,KAAK;gBAAE,OAAO,SAAS,CAAC;YAC7B,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC;gBACtC,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,+BAA+B,IAAI,IAAI,EAAE,EAAE,EAAE;oBACrE,OAAO,EAAE,EAAE,aAAa,EAAE,OAAO,KAAK,EAAE,EAAE;iBAC3C,CAAC,CAAC;gBACH,IAAI,CAAC,GAAG,CAAC,EAAE;oBAAE,OAAO,SAAS,CAAC;gBAC9B,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4D,CAAC;gBAC3F,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC9F,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC;gBAC1E,MAAM,QAAQ,GAAG,QAAQ,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC/G,2EAA2E;gBAC3E,8EAA8E;gBAC9E,4DAA4D;gBAC5D,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YACzD,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QACL,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,SAAiB,EAAE,IAAI,GAAoB,EAAE;IACvF,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IACjE,OAAO,IAAI,EAAE,QAAQ,CAAC;AACxB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { type DirectoryCandidate } from "@open-everywhere/core/agent/adapter-api.js";
2
+ /**
3
+ * Every account in the DECLARED guilds whose username or nickname matches `query`, as
4
+ * `{nativeId, name, handle}`. Fail-soft to `[]`; never throws.
5
+ *
6
+ * `tenant` is deliberately unused: Discord is not a tenant-scoped platform (core
7
+ * identity/principal-key.ts), so a Discord principal id is vendor-global and core never passes one.
8
+ * The guild set comes from this deployment's own config instead — see the header.
9
+ */
10
+ export declare function fetchGuildDirectory(query: string): Promise<readonly DirectoryCandidate[]>;
11
+ //# sourceMappingURL=discord-directory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-directory.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-directory.ts"],"names":[],"mappings":"AAkDA,OAAO,EAA6B,KAAK,kBAAkB,EAAE,MAAM,4CAA4C,CAAC;AA+BhH;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC,CAgC/F"}
@@ -0,0 +1,170 @@
1
+ // THE DISCORD ACCOUNT DIRECTORY — "who, in the guilds this deployment serves, is called that?"
2
+ // (OPE-623). The Discord arm of core's identity/account-directory.ts registry, and the second one
3
+ // ever written; the Slack arm (`fetchWorkspaceRoster`, provider-slack channels/slack-events.ts) is
4
+ // the shape this follows.
5
+ //
6
+ // WHAT IT IS FOR. The reverse-initiated identity link (core identity/link-flow.ts) lets a person NAME
7
+ // the other account instead of carrying a bearer code between two surfaces. Naming needs a lookup,
8
+ // and this is Discord's. Core owns the matching, the ranking and the cap (`rankDirectoryCandidates`,
9
+ // pure), so this arm may over-return.
10
+ //
11
+ // 🔴 A DIRECTORY HIT IS NOT EVIDENCE OF ANYTHING. It says an account with that name is in one of
12
+ // these guilds — nothing about whose it is. Anybody can set their Discord display name to "Tommy
13
+ // Long", and a guild nickname is set per guild by the member themselves. The proof in this flow is
14
+ // the CONSENT the named account gives in its own DM, which is why this arm is useless without
15
+ // {@link deliverDiscordDirectMessage} and why core requires both before it will offer the transport.
16
+ //
17
+ // TWO DIVERGENCES FROM THE SLACK ARM, both forced by the platform:
18
+ //
19
+ // 1. THE SEARCH IS SERVER-SIDE. Slack has no user-search method, so its arm walks the whole
20
+ // roster (up to ten pages) and caches it for a minute. Discord has
21
+ // `GET /guilds/{guild}/members/search?query=` — Discord itself does the prefix match against
22
+ // username and nickname and answers with the matches. So this arm makes ONE call per guild per
23
+ // search and holds NO cache: there is no ten-page walk to collapse, and a second search in the
24
+ // same turn is a DIFFERENT query, which a roster cache could not have served anyway.
25
+ // 2. THERE IS NO SINGLE "WORKSPACE". Slack's arm has one workspace because a bot token has one.
26
+ // A Discord bot is in every guild it was invited to, so the population this deployment may
27
+ // search is the set of guilds its own config DECLARES — `admittedTenantsFromConfig("discord")`,
28
+ // the same admitted set the wake door fences on. A guild nobody configured is never searched,
29
+ // so this arm can never surface a member of some unrelated server the bot happens to be in.
30
+ //
31
+ // 🔴 IT NEEDS THE PRIVILEGED `GUILD_MEMBERS` INTENT, and a deployment without it degrades cleanly.
32
+ // Discord gates both member endpoints behind the SERVER MEMBERS INTENT toggle in the application's
33
+ // own developer-portal settings. Without it the search answers 403, this arm resolves to nothing,
34
+ // and `link_identity` falls back to the code transport by itself and says which capability was
35
+ // missing. Tommy ruled directory rights are fine to REQUIRE for the better path; they are never a
36
+ // precondition for linking at all. The refusal is LOGGED once per guild so an operator can act on
37
+ // it — a silent empty answer reads as "nobody matched", which is a different and misleading fact.
38
+ //
39
+ // A RAW ID IS LOOKED UP DIRECTLY. `rankDirectoryCandidates` matches `nativeId` exactly, so pasting a
40
+ // snowflake should find precisely that account — but Discord's search matches names, never ids, so
41
+ // the search alone would answer nothing. A query that is SHAPED like a snowflake therefore also gets
42
+ // a `GET /guilds/{guild}/members/{user}`, whose 404 is the platform's own "not in this guild".
43
+ //
44
+ // FAIL-SOFT PER GUILD. A refused, failed, hung or unreadable guild contributes nothing and the other
45
+ // guilds still answer. Nothing here throws: core's resolver catches, but an arm that leans on that
46
+ // would lose every OTHER guild's rows to one guild's 403.
47
+ //
48
+ // BOTS ARE DROPPED. A link is a statement about a HUMAN, and the consent step would DM a bot that
49
+ // can never answer.
50
+ import { admittedTenantsFromConfig } from "@open-everywhere/core/agent/adapter-api.js";
51
+ const API = "https://discord.com/api/v10";
52
+ /** How many matches one guild's search may return. Core caps the ANSWER at
53
+ * `DIRECTORY_MAX_CANDIDATES` (8) after ranking across every guild, so this is deliberately larger:
54
+ * a first name that matches twelve people in one guild must not lose the right person to an
55
+ * arbitrary Discord-side truncation before core ever ranks them. Discord's own ceiling is 1000. */
56
+ const DISCORD_MEMBER_SEARCH_LIMIT = 25;
57
+ /** How many guilds one search may touch. A deployment declaring more servers than this is not a
58
+ * shape we have; the bound exists so a config change can never turn a human-paced lookup into an
59
+ * unbounded fan-out of live calls inside one turn. A stop is logged rather than silent. */
60
+ const DISCORD_DIRECTORY_MAX_GUILDS = 8;
61
+ /** A Discord snowflake as a person would paste one: digits only, and long enough to be an id rather
62
+ * than someone whose display name is "1234". Discord ids are 17–19 digits today and the epoch makes
63
+ * them grow, so the upper bound is generous. */
64
+ const SNOWFLAKE = /^\d{17,20}$/u;
65
+ function log(msg) {
66
+ console.log(`[open-everywhere:discord-directory] ${msg}`);
67
+ }
68
+ /**
69
+ * Every account in the DECLARED guilds whose username or nickname matches `query`, as
70
+ * `{nativeId, name, handle}`. Fail-soft to `[]`; never throws.
71
+ *
72
+ * `tenant` is deliberately unused: Discord is not a tenant-scoped platform (core
73
+ * identity/principal-key.ts), so a Discord principal id is vendor-global and core never passes one.
74
+ * The guild set comes from this deployment's own config instead — see the header.
75
+ */
76
+ export async function fetchGuildDirectory(query) {
77
+ const token = process.env.DISCORD_BOT_TOKEN;
78
+ if (!token)
79
+ return [];
80
+ // The same normalization core's matcher applies (`@tommy` and `tommy` are one query to a person),
81
+ // done here too because Discord's search would take the `@` literally and match nothing.
82
+ const q = query.trim().replace(/^@/u, "");
83
+ if (q.length === 0)
84
+ return [];
85
+ const guilds = [...admittedTenantsFromConfig("discord")];
86
+ if (guilds.length === 0)
87
+ return [];
88
+ if (guilds.length > DISCORD_DIRECTORY_MAX_GUILDS) {
89
+ log(`this deployment declares ${guilds.length} guilds — searching the first ${DISCORD_DIRECTORY_MAX_GUILDS}; a wider search may not see every member.`);
90
+ }
91
+ // 🔴 THE GUILDS ARE SEARCHED CONCURRENTLY, and that is about the ARM TIMEOUT rather than speed.
92
+ // Core gives an arm 8 seconds and answers `[]` when it overruns (identity/account-directory.ts) —
93
+ // so eight guilds searched one after another could turn a working directory into an empty one on a
94
+ // slow day, and the person would see the code fallback with no way to tell why. The fan-out is
95
+ // bounded by DISCORD_DIRECTORY_MAX_GUILDS above, so "concurrent" here means at most eight calls.
96
+ const perGuild = await Promise.all(guilds.slice(0, DISCORD_DIRECTORY_MAX_GUILDS).map((guild) => guildMembersMatching(token, guild, q)));
97
+ // Deduped across guilds: one person in three declared servers is ONE account, and three identical
98
+ // rows would crowd the other candidates out of core's cap. `perGuild` keeps the declared order, so
99
+ // which guild's nickname wins is deterministic rather than a race.
100
+ const byId = new Map();
101
+ for (const members of perGuild) {
102
+ for (const member of members) {
103
+ const candidate = candidateFromMember(member);
104
+ if (candidate && !byId.has(candidate.nativeId))
105
+ byId.set(candidate.nativeId, candidate);
106
+ }
107
+ }
108
+ return [...byId.values()];
109
+ }
110
+ /** One guild's matches for `q` — the name search, plus the direct id read when `q` is a snowflake.
111
+ * Never throws: a guild that refuses contributes nothing and the caller moves on. */
112
+ async function guildMembersMatching(token, guildId, q) {
113
+ const out = [];
114
+ const search = await guildRequest(token, `${API}/guilds/${encodeURIComponent(guildId)}/members/search?query=${encodeURIComponent(q)}&limit=${DISCORD_MEMBER_SEARCH_LIMIT}`, guildId, "member search");
115
+ if (Array.isArray(search))
116
+ out.push(...search);
117
+ if (SNOWFLAKE.test(q)) {
118
+ // A 404 here is Discord's own "not in this guild" and is the ordinary answer, so `guildRequest`
119
+ // does not log it. Anything readable is one member object rather than an array.
120
+ const one = await guildRequest(token, `${API}/guilds/${encodeURIComponent(guildId)}/members/${encodeURIComponent(q)}`, guildId, "member read");
121
+ if (one && typeof one === "object" && !Array.isArray(one))
122
+ out.push(one);
123
+ }
124
+ return out;
125
+ }
126
+ /** One GET under the bot token, or undefined. The token is a SECRET — header only, never a log line,
127
+ * an error string or a return value (the same rule channels/discord-post.ts states). */
128
+ async function guildRequest(token, url, guildId, what) {
129
+ try {
130
+ const res = await fetch(url, { headers: { authorization: `Bot ${token}` } });
131
+ if (!res.ok) {
132
+ // 403 is nearly always the missing SERVER MEMBERS INTENT — the one failure an operator can
133
+ // fix, and the one a silent empty answer would hide. 404 on the direct read is the ordinary
134
+ // "not in this guild" and is not news.
135
+ if (res.status !== 404) {
136
+ log(`${what} did not answer (${res.status}) for guild ${guildId} — the account directory is unavailable there; linking falls back to the code flow. A 403 usually means the SERVER MEMBERS INTENT is off for this app.`);
137
+ }
138
+ return undefined;
139
+ }
140
+ return (await res.json());
141
+ }
142
+ catch (err) {
143
+ log(`${what} failed for guild ${guildId}: ${err?.message ?? String(err)}`);
144
+ return undefined;
145
+ }
146
+ }
147
+ /**
148
+ * One member row as a candidate, or undefined when it names no human account.
149
+ *
150
+ * `handle` is the account's GLOBAL username (unique across Discord since the username migration) and
151
+ * `name` prefers the guild NICKNAME, because that is the name the people in that server actually see
152
+ * and therefore the name the initiator will type. Both are self-set strings; core sanitizes them and
153
+ * the tool fences them before a model reads either.
154
+ */
155
+ function candidateFromMember(member) {
156
+ const user = member?.user;
157
+ if (!user || typeof user !== "object")
158
+ return undefined;
159
+ if (user.bot === true)
160
+ return undefined; // a bot is a guild member and is not a person
161
+ const nativeId = typeof user.id === "string" ? user.id.trim() : "";
162
+ if (nativeId.length === 0)
163
+ return undefined;
164
+ const username = typeof user.username === "string" ? user.username.trim() : "";
165
+ const globalName = typeof user.global_name === "string" ? user.global_name.trim() : "";
166
+ const nick = typeof member.nick === "string" ? member.nick.trim() : "";
167
+ const name = nick || globalName || username;
168
+ return { nativeId, ...(name ? { name } : {}), ...(username ? { handle: username } : {}) };
169
+ }
170
+ //# sourceMappingURL=discord-directory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-directory.js","sourceRoot":"","sources":["../../../agent/channels/discord-directory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Routine, DispatchTarget } from "@open-everywhere/core/agent/adapter-api.js";
2
+ export declare function resolveDiscordTarget(r: Routine): DispatchTarget | null;
3
+ /**
4
+ * Derive a post-back target for a CONFIGURED Discord report channel ({@link Domain.reportChannel},
5
+ * OPE-148) from its surfaceKey alone — no stored row, no live binding.
6
+ *
7
+ * A Discord surfaceKey is `discord/<guildId>/<channelId>` (approvals/flow.ts postedToSurfaceKey and
8
+ * discordDecisionWakeTarget both build it that way), so the third segment IS the channel id and
9
+ * `{ channelId }` is exactly what {@link resolveDiscordTarget} would have read off a stored target.
10
+ * Mode is deliberately NOT derived: a configured report channel is a guild channel by construction
11
+ * (a DM cannot be a domain member), and the absent `channelType` lands on the conservative
12
+ * "channel" arm above — capped rather than spend-exempt, which is the safe direction.
13
+ */
14
+ export declare function discordReportTargetFor(surfaceKey: string): Readonly<Record<string, unknown>> | null;
15
+ //# sourceMappingURL=discord-dispatch-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-dispatch-target.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-dispatch-target.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAkC,MAAM,4CAA4C,CAAC;AAW1H,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CA+DtE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAInG"}
@@ -0,0 +1,99 @@
1
+ // discord-dispatch-target — Discord's routine-dispatch target resolver (OPE-114 stage 4).
2
+ // Moved out of core's schedules/dispatch.ts (resolveDiscordTarget) and registered on the unified
3
+ // routine-dispatch seam (schedules/routine-dispatch.ts registerRoutineDispatch, shared with teams)
4
+ // by registerDiscordProvider(). The bundle imports its own eve channel object directly,
5
+ // which is what retired the interim per-platform dispatch-channel registry the first cut of this
6
+ // extraction used — one seam, not two.
7
+ //
8
+ // Post-back = a channel message via Discord's REST create-message endpoint (bot token) — eve's
9
+ // discordChannel().receive owns the send; NO Gateway websocket is involved (that transport is
10
+ // inbound-ambient only). Target = `{ channelId }`.
11
+ import discord from "./discord.js";
12
+ /** One stored-target field as a non-empty string, or undefined (never a coerced non-string — the
13
+ * store row is data, validated on the way out, not trusted). Local copy of core dispatch.ts's
14
+ * module-private `storedString`. */
15
+ function storedString(t, key) {
16
+ const v = t?.[key];
17
+ return typeof v === "string" && v.length > 0 ? v : undefined;
18
+ }
19
+ export function resolveDiscordTarget(r) {
20
+ const channelId = storedString(r.target, "channelId");
21
+ if (!channelId)
22
+ return null;
23
+ const channelType = storedString(r.target, "channelType");
24
+ // Mode from the stored channel type ONLY ("1" DM / "3" group DM — see discord-identity.ts).
25
+ // discordMode's no-guild→dm rule is deliberately NOT reused here: it is sound for a live
26
+ // interaction (no guild id on the payload ⇒ a DM) but unsound for a stored target that may
27
+ // simply predate guildId capture — and a guild channel wrongly classed "dm" would slip the
28
+ // spend cap. Ambiguous ⇒ "channel" (capped — the conservative arm).
29
+ const mode = channelType === "1" ? "dm" : channelType === "3" ? "mpim" : "channel";
30
+ const storedGuildId = storedString(r.target, "guildId");
31
+ // guild_id (for ATTRIBUTES only, below) falls back to the surfaceKey's workspace segment for
32
+ // targets that predate guildId capture — the same fallback the attributes use, so the origin and
33
+ // bindDiscord's re-derived workspace always agree.
34
+ const guildId = storedGuildId ?? r.surfaceKey.split("/")[1] ?? "";
35
+ // OPE-486 — the fire's WAKE ORIGIN, parity with resolveSlackTarget (routine-dispatch.ts). Absent
36
+ // before this fix, so a Discord fire could never divert into the unified window (fire-window.ts's
37
+ // planFireWindow fails closed on a missing origin). Two shapes, matching the codec's XOR
38
+ // (discord-origin-codec.ts): a DM/group-DM fire (mode "dm"/"mpim") gets the `dm: true` variant —
39
+ // NEVER a guild-shaped origin with a possibly-empty guildId, which the codec refuses by
40
+ // construction (D1/D3). Every other fire gets the guild variant; an empty guildId (only possible
41
+ // when the surfaceKey itself carries none) fails isUnifiedWakeOrigin downstream and the fire
42
+ // stays native — fail-closed, not a guess. No messageId/autoThreadName: a proactive fire has no
43
+ // triggering message to anchor to.
44
+ //
45
+ // OPE-503 — GUILD EVIDENCE, NOT JUST "NOT A KNOWN DM MODE". A row with neither a stored
46
+ // channelType nor a stored guildId (a LEGACY row, pre-dating both captures) carries NO positive
47
+ // signal it is a guild surface. bindDiscord derives workspace as `guildId ?? userId`, so on an
48
+ // equally-legacy DM row the surfaceKey's workspace segment is the partner's user id, not a guild
49
+ // id — and the attributes-level `guildId` fallback above cannot tell the two apart either (that
50
+ // is fine for attributes, see the comment below, but NOT for an origin: the codec's guild arm
51
+ // only checks non-empty, so a user id validates, and the fire then diverts under
52
+ // `discord/<userId>/<channelId>` — a guild-shaped routing key minted from a DM). Mint the guild
53
+ // origin only when there is real evidence of a guild: an explicitly stored guildId, or a
54
+ // channelType that positively names a non-DM channel. Otherwise leave `origin` absent — the
55
+ // documented fail-closed default (DispatchTarget.origin) — so the fire still posts natively; it
56
+ // just does not divert into the unified window on a guess.
57
+ const guildEvidence = storedGuildId !== undefined || channelType !== undefined;
58
+ const origin = mode === "dm" || mode === "mpim"
59
+ ? { platform: "discord", channelId, dm: true }
60
+ : guildEvidence
61
+ ? { platform: "discord", channelId, guildId }
62
+ : undefined;
63
+ return {
64
+ channel: discord,
65
+ // Strict DiscordReceiveTarget shape — guildId/channelType feed attributes only.
66
+ target: { channelId },
67
+ mode,
68
+ // The facts bindDiscord reads. guild_id falls back to the surfaceKey's workspace segment for
69
+ // targets that predate guildId capture: bindDiscord derives workspace as `guildId ?? userId`,
70
+ // so that segment IS the original guild id for a guild routine and the creator's user id for
71
+ // a DM routine — stamping it back yields the identical workspace either way (attribution lands
72
+ // right; only the legacy DM's mode stays the conservative "channel"). Unlike `origin` above,
73
+ // this fallback is harmless on a legacy DM row: bindDiscord treats it as an opaque workspace
74
+ // identity, never as a guild-keyed authority.
75
+ attributes: {
76
+ channel_id: channelId,
77
+ guild_id: guildId,
78
+ ...(channelType ? { channel_type: channelType } : {}),
79
+ },
80
+ ...(origin !== undefined ? { origin } : {}),
81
+ };
82
+ }
83
+ /**
84
+ * Derive a post-back target for a CONFIGURED Discord report channel ({@link Domain.reportChannel},
85
+ * OPE-148) from its surfaceKey alone — no stored row, no live binding.
86
+ *
87
+ * A Discord surfaceKey is `discord/<guildId>/<channelId>` (approvals/flow.ts postedToSurfaceKey and
88
+ * discordDecisionWakeTarget both build it that way), so the third segment IS the channel id and
89
+ * `{ channelId }` is exactly what {@link resolveDiscordTarget} would have read off a stored target.
90
+ * Mode is deliberately NOT derived: a configured report channel is a guild channel by construction
91
+ * (a DM cannot be a domain member), and the absent `channelType` lands on the conservative
92
+ * "channel" arm above — capped rather than spend-exempt, which is the safe direction.
93
+ */
94
+ export function discordReportTargetFor(surfaceKey) {
95
+ const segs = surfaceKey.split("/");
96
+ const channelId = segs.length === 3 && segs[0] === "discord" ? segs[2] : undefined;
97
+ return channelId && channelId.length > 0 ? { channelId } : null;
98
+ }
99
+ //# sourceMappingURL=discord-dispatch-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-dispatch-target.js","sourceRoot":"","sources":["../../../agent/channels/discord-dispatch-target.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import { type Channel, type RouteHandlerArgs } from "eve/channels";
2
+ /** The public URL to paste into the app's developer-portal **Webhooks** page (NOT the
3
+ * Interactions Endpoint URL, which stays {@link PUBLIC_DISCORD_ROUTE}). */
4
+ export declare const PUBLIC_DISCORD_EVENTS_ROUTE = "/eve/v1/discord/events";
5
+ /** The decision one verified Webhook Events body resolves to. `ack` covers everything we
6
+ * deliberately don't act on (a PING, an entitlement/social-SDK event, an unparseable body) —
7
+ * all of which still get Discord's required 204. */
8
+ export type DiscordEventDecision = {
9
+ readonly kind: "ack";
10
+ readonly reason: string;
11
+ } | {
12
+ readonly kind: "audit";
13
+ readonly action: string;
14
+ readonly workspace: string;
15
+ readonly principalId?: string;
16
+ readonly detail: Record<string, unknown>;
17
+ };
18
+ /**
19
+ * Classify one raw Webhook Events body. PURE — no I/O, so every branch is unit-tested.
20
+ *
21
+ * The audit partition mirrors what the rest of the Discord surface uses: the GUILD is the
22
+ * workspace (the org boundary — `bindDiscord` maps guild→workspace), and the authorizing user is
23
+ * the principal. A USER install (integration_type 1) has no guild at all, so it partitions under
24
+ * the sentinel workspace `user-install` rather than being forced into a guild it doesn't have —
25
+ * a fabricated workspace id would be worse than an honest one.
26
+ */
27
+ export declare function classifyDiscordEvent(rawBody: string): DiscordEventDecision;
28
+ /** Injectable seams so the route is testable without live crypto env or the real store. */
29
+ export interface DiscordEventsOptions {
30
+ /** The app public key; defaults to DISCORD_PUBLIC_KEY (the same env the interactions path reads). */
31
+ readonly publicKey?: string;
32
+ readonly now?: () => number;
33
+ /** Audit writer (defaults to the real appendAudit under the deployment org). */
34
+ readonly audit?: (row: {
35
+ action: string;
36
+ workspace: string;
37
+ principalId?: string;
38
+ detail: Record<string, unknown>;
39
+ }) => Promise<void>;
40
+ }
41
+ /**
42
+ * Build the Webhook Events route handler: verify (fail-closed 401), classify, audit, ack.
43
+ *
44
+ * The audit write is deliberately AWAITED rather than backgrounded — a `surface.app_*` row is one
45
+ * cheap insert and Discord's budget is 3 seconds, so we'd rather the row land than shave a
46
+ * millisecond. It is also wrapped: an audit failure must never turn into a non-2xx, because
47
+ * Discord retries a failure and then suspends the endpoint — losing the very observability this
48
+ * route exists to provide.
49
+ */
50
+ export declare function buildDiscordEventsHandler(opts?: DiscordEventsOptions): (req: Request, args: RouteHandlerArgs) => Promise<Response>;
51
+ /**
52
+ * Mount the Webhook Events route onto the Discord channel. Additive: it only PREPENDS a route to
53
+ * the same channel object (one channel name → one session namespace, the frontDiscordChannel
54
+ * shape), touching neither the interactions front nor eve's own adapter/receive. With no URL
55
+ * configured in the developer portal this route is simply never hit.
56
+ */
57
+ export declare function withDiscordEventsRoute<C extends Channel<any, any, any>>(channel: C, opts?: DiscordEventsOptions): C;
58
+ //# sourceMappingURL=discord-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-events.d.ts","sourceRoot":"","sources":["../../../agent/channels/discord-events.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE;4EAC4E;AAC5E,eAAO,MAAM,2BAA2B,2BAA2B,CAAC;AAUpE;;qDAEqD;AACrD,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAU7J;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,oBAAoB,CA2C1E;AAED,2FAA2F;AAC3F,MAAM,WAAW,oBAAoB;IACnC,qGAAqG;IACrG,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvI;AAKD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,GAAE,oBAAyB,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,KAAK,OAAO,CAAC,QAAQ,CAAC,CA6BtI;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,GAAE,oBAAyB,GAAG,CAAC,CAEvH"}