@openclaw/discord 2026.9.8 → 2026.9.33

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 (75) hide show
  1. package/dist/.setup/{account-inspect-De8hOUxG.mjs → account-inspect-Bp8nbBSx.mjs} +2 -2
  2. package/dist/.setup/{channel-type-DRCrEMss.mjs → allow-list-Kty_8Xca.mjs} +7 -7
  3. package/dist/.setup/{api-DVbJwZVt.mjs → api-CotnTQLx.mjs} +3 -3
  4. package/dist/.setup/{approval-handler.runtime-DxCK2Rux.mjs → approval-handler.runtime-DqU_yO2L.mjs} +9 -9
  5. package/dist/.setup/{approval-message-updates-BKFqavHZ.mjs → approval-message-updates-CdnQ98c8.mjs} +2 -2
  6. package/dist/.setup/{audit-CLg9I71N.mjs → audit-CqUeNcJH.mjs} +4 -4
  7. package/dist/.setup/{channel-DxlUVU9t.mjs → channel-Bu1qLdnm.mjs} +28 -23
  8. package/dist/.setup/{channel.setup-CRo1GjV7.mjs → channel.setup-DRCLt54O.mjs} +4 -4
  9. package/dist/.setup/{components-Cq1PvIpN.mjs → components-CwpgbGDR.mjs} +3 -3
  10. package/dist/.setup/{conversation-binding-route-Caz_XuFH.mjs → conversation-binding-route-DC0Y2v1B.mjs} +1 -1
  11. package/dist/.setup/{directory-config-esqNn0Eo.mjs → directory-config-DLT0PP2j.mjs} +1 -1
  12. package/dist/.setup/{directory-live-DtKRHuGV.mjs → directory-live-DPVUIJ8-.mjs} +4 -4
  13. package/dist/.setup/{discord-0xJYfdDD.mjs → discord-DTLP81g6.mjs} +3 -3
  14. package/dist/.setup/{doctor-BCl1H5tl.mjs → doctor-E3AF74Ms.mjs} +5 -5
  15. package/dist/.setup/{handle-action-CUvQWnE7.mjs → handle-action-cmTFQysA.mjs} +8 -8
  16. package/dist/.setup/{handle-action.guild-admin-Df-8gVle.mjs → handle-action.guild-admin-CH8nj2w0.mjs} +2 -2
  17. package/dist/.setup/{live-policy-config-8Pv20CHc.mjs → live-policy-config-D--q9Zta.mjs} +1 -1
  18. package/dist/.setup/{message-handler-B-Jk4oDk.mjs → message-handler-BVZO6wCg.mjs} +12 -11
  19. package/dist/.setup/{message-handler.preflight-v-__NSDq.mjs → message-handler.preflight-Be_b_NFp.mjs} +17 -17
  20. package/dist/.setup/{message-handler.preflight-pluralkit-CWLUBVcx.mjs → message-handler.preflight-pluralkit-TPGXsd8P.mjs} +7 -7
  21. package/dist/.setup/{message-handler.process-BNFFuChn.mjs → message-handler.process-BUBGcAoh.mjs} +21 -21
  22. package/dist/.setup/{outbound-adapter-BobT-kse.mjs → outbound-adapter-CSiCDzCN.mjs} +9 -9
  23. package/dist/.setup/{outbound-session-route-zVGxxceg.mjs → outbound-session-route-D0ryINch.mjs} +13 -13
  24. package/dist/.setup/{probe-LYrIHlBF.mjs → probe-DTSDdlpy.mjs} +4 -4
  25. package/dist/.setup/{provider-B_G5hASF.mjs → provider-BTm60b3t.mjs} +81 -81
  26. package/dist/.setup/{provider-session.runtime-BYlTBFD2.mjs → provider-session.runtime-DKJ-qSt9.mjs} +3 -3
  27. package/dist/.setup/provider.runtime-BvoZBuZY.mjs +2 -0
  28. package/dist/.setup/{realtime-session.runtime-oUd9DOcz.mjs → realtime-session.runtime-JZd7A5bS.mjs} +19 -19
  29. package/dist/.setup/{resolve-allowlist-common-DPdRggu5.mjs → resolve-allowlist-common-Ck4M_Ot2.mjs} +3 -3
  30. package/dist/.setup/{resolve-channels-BFPNRmVv.mjs → resolve-channels-CEaKGzuh.mjs} +3 -3
  31. package/dist/.setup/{resolve-users-0-aBuCy3.mjs → resolve-users-CMz4PVnV.mjs} +2 -2
  32. package/dist/.setup/{runtime-FN000lxA.mjs → runtime-DwW5C0WK.mjs} +12 -12
  33. package/dist/.setup/{runtime-sUksZ--Q.mjs → runtime-Lsy4dEx7.mjs} +2 -2
  34. package/dist/.setup/{send-CrfEI6Bs.mjs → send-DY5EeBPr.mjs} +15 -15
  35. package/dist/.setup/{send.components-DxQAMfES.mjs → send.components-DGwzyA_4.mjs} +7 -7
  36. package/dist/.setup/{send.outbound-BISKnb-a.mjs → send.outbound-DR8O-Pvo.mjs} +8 -8
  37. package/dist/.setup/{retry-pz_cSha2.mjs → send.receipt-D0JBijZQ.mjs} +217 -217
  38. package/dist/.setup/{send.shared-DGFkbkQR.mjs → send.shared-r0Fj2v_h.mjs} +40 -40
  39. package/dist/.setup/{sender-identity-B9X5Vb9R.mjs → sender-identity-DQCoikZl.mjs} +1 -1
  40. package/dist/.setup/setup-plugin-api-BEoo_L8p.mjs +3 -0
  41. package/dist/.setup/{shared-B3wZ4hN-.mjs → shared-CDq4vJm3.mjs} +7 -7
  42. package/dist/.setup/{subagent-hooks-nlUE9ow7.mjs → subagent-hooks-Bk0mMyHh.mjs} +2 -2
  43. package/dist/.setup/{system-events-CdeZnvPV.mjs → system-events-I1rH-U6o.mjs} +2 -2
  44. package/dist/.setup/{target-resolver-BuLapkhy.mjs → target-resolver-Dcg0WjsF.mjs} +3 -3
  45. package/dist/.setup/targets-CJkVZIkE.mjs +3 -0
  46. package/dist/.setup/{thread-bindings-D7TfgjR_.mjs → thread-bindings-E0TiUQUX.mjs} +6 -6
  47. package/dist/.setup/{thread-bindings.manager-DN-NissX.mjs → thread-bindings.manager-CEoFDOs9.mjs} +14 -14
  48. package/dist/.setup/{thread-bindings.session-updates-CshfKghV.mjs → thread-bindings.session-updates-DvtjzKO-.mjs} +2 -2
  49. package/dist/.setup/{transcripts-source-p8EV179h.mjs → transcripts-source-CHhpvL97.mjs} +17 -17
  50. package/dist/.setup/{typing-BiHCzBRu.mjs → typing-Cr0b2zBq.mjs} +1 -1
  51. package/dist/.setup/{voice-runtime-B46rcwrG.mjs → voice-runtime-BngIfPc3.mjs} +46 -46
  52. package/dist/account-inspect-api.js +1 -1
  53. package/dist/action-runtime-api.js +1 -1
  54. package/dist/activities-api.js +791 -1
  55. package/dist/api.js +15 -15
  56. package/dist/channel-config-api.js +1 -1
  57. package/dist/channel-plugin-api.js +1 -1
  58. package/dist/directory-contract-api.js +1 -1
  59. package/dist/doctor-contract-api.js +1 -1
  60. package/dist/index.js +9 -5
  61. package/dist/runtime-api.js +20 -20
  62. package/dist/setup-entry.js +1 -1
  63. package/dist/setup-plugin-api.js +1 -1
  64. package/dist/src/voice/audio-worker.runtime.js +3 -3
  65. package/dist/subagent-hooks-api.js +1 -1
  66. package/dist/transcripts-source-api.js +1 -1
  67. package/package.json +4 -4
  68. package/dist/.setup/activities-api-tvbsU2e3.mjs +0 -792
  69. package/dist/.setup/provider.runtime-CUE3UxER.mjs +0 -2
  70. package/dist/.setup/setup-plugin-api-BQUBxbhe.mjs +0 -3
  71. package/dist/.setup/targets-0KUcEIYx.mjs +0 -3
  72. package/dist/.setup/{accounts-ChaMcixG.mjs → accounts-DS-A-gCx.mjs} +3 -3
  73. package/dist/.setup/{config-schema-oxELUSDx.mjs → config-schema-DKUSr985.mjs} +2 -2
  74. package/dist/.setup/{receive-recovery-CHPOsnLc.mjs → receive-recovery-D6eikHf5.mjs} +2 -2
  75. package/dist/.setup/{thread-bindings.state-g6v9zAym.mjs → thread-bindings.state-755lJHln.mjs} +3 -3
@@ -1,2 +1,792 @@
1
- import { t as registerDiscordActivities } from "./.setup/activities-api-tvbsU2e3.mjs";
1
+ import { c as resolveDiscordAccount } from "./.setup/accounts-DS-A-gCx.mjs";
2
+ import { E as getDiscordEndpointRuntime } from "./.setup/allow-list-Kty_8Xca.mjs";
3
+ import { m as resolveDiscordChannelId$1 } from "./.setup/send.receipt-D0JBijZQ.mjs";
4
+ import { S as parseDiscordActivityCustomId, i as buildDiscordPresentationComponents } from "./.setup/components-CwpgbGDR.mjs";
5
+ import { r as sendDiscordComponentMessage } from "./.setup/send.components-DGwzyA_4.mjs";
6
+ import { r as setDiscordActivitiesRuntime, t as DiscordActivitiesRuntime } from "./.setup/runtime-Lsy4dEx7.mjs";
7
+ import { randomBytes } from "node:crypto";
8
+ import { fetchWithSsrFGuard as fetchWithSsrFGuard$1 } from "openclaw/plugin-sdk/ssrf-runtime";
9
+ import { logError } from "openclaw/plugin-sdk/logging-core";
10
+ import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
11
+ import fs from "node:fs/promises";
12
+ import { resolveRequestClientIp } from "openclaw/plugin-sdk/webhook-ingress";
13
+ import { WEBHOOK_BODY_READ_DEFAULTS, readJsonBodyWithLimit, sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
14
+ import { WIDGET_CDN_ORIGINS } from "openclaw/plugin-sdk/widget-html";
15
+ //#region extensions/discord/src/activities/discord-api.ts
16
+ const DISCORD_TOKEN_URL = "https://discord.com/api/oauth2/token";
17
+ const DISCORD_USER_URL = "https://discord.com/api/v10/users/@me";
18
+ const DISCORD_HOST = "discord.com";
19
+ const DISCORD_ACTIVITY_REQUEST_TIMEOUT_MS = 15e3;
20
+ const JSON_MAX_BYTES = 65536;
21
+ const INSTANCE_ID_MAX_LENGTH = 256;
22
+ function normalizeInstanceId(value) {
23
+ const instanceId = value?.trim();
24
+ let hasControlCharacter = false;
25
+ for (let index = 0; index < (instanceId?.length ?? 0); index += 1) {
26
+ const codePoint = instanceId?.charCodeAt(index) ?? 0;
27
+ if (codePoint < 32 || codePoint === 127) {
28
+ hasControlCharacter = true;
29
+ break;
30
+ }
31
+ }
32
+ if (!instanceId || instanceId.length > INSTANCE_ID_MAX_LENGTH || hasControlCharacter) return;
33
+ return instanceId;
34
+ }
35
+ async function fetchDiscordJson(params) {
36
+ const endpoint = Object.hasOwn(params, "endpointRuntime") ? params.endpointRuntime : getDiscordEndpointRuntime();
37
+ const liveUrl = new URL(params.url);
38
+ const endpointPath = liveUrl.pathname === "/api/oauth2/token" ? "/oauth2/token" : liveUrl.pathname.startsWith("/api/v10/") ? liveUrl.pathname.slice(8) : void 0;
39
+ if (endpoint && !endpointPath) throw new Error("Discord Activity request is outside the configured endpoint routes");
40
+ const { response, release } = endpoint ? {
41
+ response: await endpoint.fetch(`${endpoint.descriptor.restApiBaseUrl}${endpointPath}${liveUrl.search}`, {
42
+ ...params.init,
43
+ signal: params.init.signal ? AbortSignal.any([params.init.signal, AbortSignal.timeout(DISCORD_ACTIVITY_REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(DISCORD_ACTIVITY_REQUEST_TIMEOUT_MS)
44
+ }),
45
+ release: async () => {}
46
+ } : await params.fetchGuard({
47
+ url: params.url,
48
+ fetchImpl: params.fetchImpl,
49
+ init: params.init,
50
+ policy: { allowedHostnames: [DISCORD_HOST] },
51
+ auditContext: params.auditContext,
52
+ timeoutMs: DISCORD_ACTIVITY_REQUEST_TIMEOUT_MS
53
+ });
54
+ try {
55
+ if (!response.ok) {
56
+ await response.body?.cancel().catch(() => void 0);
57
+ return {
58
+ ok: false,
59
+ status: response.status
60
+ };
61
+ }
62
+ return {
63
+ ok: true,
64
+ status: response.status,
65
+ body: await readProviderJsonResponse(response, "Discord Activity OAuth", { maxBytes: JSON_MAX_BYTES })
66
+ };
67
+ } finally {
68
+ await release();
69
+ }
70
+ }
71
+ async function resolveActivityInstanceChannel(params) {
72
+ const endpointRuntime = getDiscordEndpointRuntime() ?? null;
73
+ let result;
74
+ try {
75
+ result = await fetchDiscordJson({
76
+ fetchGuard: params.fetchGuard,
77
+ fetchImpl: params.proxyFetch,
78
+ url: `https://discord.com/api/v10/applications/${encodeURIComponent(params.applicationId)}/activity-instances/${encodeURIComponent(params.instanceId)}`,
79
+ init: { headers: { Authorization: `Bot ${params.botAuth}` } },
80
+ auditContext: "discord.activities.instance",
81
+ endpointRuntime
82
+ });
83
+ } catch {
84
+ return;
85
+ }
86
+ if (!result.ok || !Array.isArray(result.body?.users) || !result.body.users.includes(params.discordUserId) || !result.body.location || typeof result.body.location !== "object") return;
87
+ const channelId = result.body.location.channel_id;
88
+ return typeof channelId === "string" && /^\d+$/.test(channelId) ? channelId : void 0;
89
+ }
90
+ //#endregion
91
+ //#region extensions/discord/src/activities/rate-limit.ts
92
+ const TOKEN_RATE_LIMIT = 10;
93
+ const TOKEN_GLOBAL_RATE_LIMIT = 60;
94
+ const TOKEN_RATE_WINDOW_MS = 6e4;
95
+ const TOKEN_RATE_MAX_KEYS = 1024;
96
+ var TokenRateLimiter = class {
97
+ constructor(now) {
98
+ this.now = now;
99
+ this.attempts = /* @__PURE__ */ new Map();
100
+ this.globalAttempts = [];
101
+ }
102
+ allowKey(key) {
103
+ const now = this.now();
104
+ const active = (this.attempts.get(key) ?? []).filter((timestamp) => timestamp > now - TOKEN_RATE_WINDOW_MS);
105
+ if (active.length >= TOKEN_RATE_LIMIT) {
106
+ this.attempts.delete(key);
107
+ this.attempts.set(key, active);
108
+ return false;
109
+ }
110
+ if (!this.attempts.has(key) && this.attempts.size >= TOKEN_RATE_MAX_KEYS) {
111
+ const oldestKey = this.attempts.keys().next().value;
112
+ if (typeof oldestKey === "string") this.attempts.delete(oldestKey);
113
+ }
114
+ active.push(now);
115
+ this.attempts.delete(key);
116
+ this.attempts.set(key, active);
117
+ return true;
118
+ }
119
+ reserveGlobal() {
120
+ const now = this.now();
121
+ this.globalAttempts = this.globalAttempts.filter((reservation) => reservation.at > now - TOKEN_RATE_WINDOW_MS);
122
+ if (this.globalAttempts.length >= TOKEN_GLOBAL_RATE_LIMIT) return null;
123
+ const reservation = { at: now };
124
+ this.globalAttempts.push(reservation);
125
+ return reservation;
126
+ }
127
+ releaseGlobal(reservation) {
128
+ const index = this.globalAttempts.indexOf(reservation);
129
+ if (index >= 0) this.globalAttempts.splice(index, 1);
130
+ }
131
+ };
132
+ //#endregion
133
+ //#region extensions/discord/src/activities/shell.ts
134
+ const DISCORD_ACTIVITY_ROUTE_PREFIX = "/discord/activity";
135
+ const DISCORD_ACTIVITY_TOKEN_REQUEST_TIMEOUT_MS = 35e3;
136
+ const DISCORD_ACTIVITY_WIDGET_REQUEST_TIMEOUT_MS = 2e4;
137
+ const DISCORD_ACTIVITY_SHELL_CSP = "default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; connect-src 'self'; frame-src 'self'; img-src data:; base-uri 'none'; frame-ancestors *";
138
+ const DISCORD_ACTIVITY_SHELL_HTML = `<!doctype html>
139
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
140
+ <title>OpenClaw widget</title><style>
141
+ :root{color-scheme:dark;font:14px system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#1e1f22;color:#dbdee1}
142
+ *{box-sizing:border-box}html,body,#app{height:100%;margin:0}#app{display:grid;place-items:center}main{max-width:560px;padding:32px;text-align:center}
143
+ h1{font-size:18px;margin:0 0 8px;color:#f2f3f5}p{margin:0;color:#b5bac1;line-height:1.5}.widget{display:grid;grid-template-rows:42px 1fr;width:100%;height:100%;background:#111214}
144
+ .bar{display:flex;align-items:center;padding:0 14px;border-bottom:1px solid #2b2d31;font-weight:600;color:#f2f3f5}.widget iframe{border:0;width:100%;height:100%;background:#111214}
145
+ </style></head><body><div id="app"><main><h1>Opening widget</h1><p>Connecting to Discord…</p></main></div>
146
+ <script type="module" src="./shell.js"><\/script></body></html>`;
147
+ const DISCORD_ACTIVITY_SHELL_JS = `import { DiscordSDK } from "./vendor/embedded-app-sdk.mjs";
148
+
149
+ const tokenRequestTimeoutMs = ${DISCORD_ACTIVITY_TOKEN_REQUEST_TIMEOUT_MS};
150
+ const widgetRequestTimeoutMs = ${DISCORD_ACTIVITY_WIDGET_REQUEST_TIMEOUT_MS};
151
+ const app = document.querySelector("#app");
152
+ function show(message, detail) {
153
+ app.className = "";
154
+ app.innerHTML = "";
155
+ const main = document.createElement("main");
156
+ const heading = document.createElement("h1");
157
+ const paragraph = document.createElement("p");
158
+ heading.textContent = message;
159
+ paragraph.textContent = detail;
160
+ main.append(heading, paragraph);
161
+ app.append(main);
162
+ }
163
+ function proxiedDocUrl(value) {
164
+ const url = new URL(value, window.location.origin);
165
+ if (window.location.hostname.endsWith(".discordsays.com")) {
166
+ // The ROOT mapping target already includes this prefix; strip it before proxying.
167
+ const gatewayPrefix = "/discord/activity";
168
+ const mappedPath = url.pathname.startsWith(gatewayPrefix + "/")
169
+ ? url.pathname.slice(gatewayPrefix.length)
170
+ : url.pathname;
171
+ return "/.proxy" + mappedPath + url.search;
172
+ }
173
+ return url.pathname + url.search;
174
+ }
175
+ async function readJson(response) {
176
+ let body;
177
+ try {
178
+ body = await response.json();
179
+ } catch (error) {
180
+ // Error responses may legitimately omit JSON details; successful responses must not
181
+ // turn an aborted or malformed body into an apparently valid empty payload.
182
+ if (response.ok) throw error;
183
+ body = {};
184
+ }
185
+ if (!response.ok) {
186
+ const error = new Error(typeof body.error === "string" ? body.error : "request failed");
187
+ error.status = response.status;
188
+ throw error;
189
+ }
190
+ return body;
191
+ }
192
+ async function fetchJsonWithDeadline(input, init, timeoutMs) {
193
+ // Activities also run in mobile webviews; avoid requiring the newer
194
+ // AbortSignal.timeout() static API just to enforce this request boundary.
195
+ const controller = new AbortController();
196
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
197
+ try {
198
+ return await readJson(await fetch(input, { ...init, signal: controller.signal }));
199
+ } finally {
200
+ clearTimeout(timeout);
201
+ }
202
+ }
203
+ async function run() {
204
+ const match = window.location.hostname.match(/^(\\d+)\\.discordsays\\.com$/i);
205
+ if (!match) {
206
+ show("Open inside Discord", "This widget must be launched from its Discord button.");
207
+ return;
208
+ }
209
+ const clientId = match[1];
210
+ const sdk = new DiscordSDK(clientId);
211
+ await sdk.ready();
212
+ const { code } = await sdk.commands.authorize({
213
+ client_id: clientId,
214
+ response_type: "code",
215
+ state: "",
216
+ prompt: "none",
217
+ scope: ["identify"],
218
+ });
219
+ const auth = await fetchJsonWithDeadline(
220
+ "./api/token",
221
+ {
222
+ method: "POST",
223
+ headers: { "Content-Type": "application/json" },
224
+ body: JSON.stringify({ code }),
225
+ },
226
+ tokenRequestTimeoutMs,
227
+ );
228
+ await sdk.commands.authenticate({ access_token: auth.access_token });
229
+ const query = new URLSearchParams({
230
+ custom_id: sdk.customId ?? "",
231
+ instance_id: sdk.instanceId,
232
+ });
233
+ const widget = await fetchJsonWithDeadline(
234
+ "./api/widget?" + query,
235
+ { headers: { Authorization: "Bearer " + auth.session_token } },
236
+ widgetRequestTimeoutMs,
237
+ );
238
+ app.className = "widget";
239
+ app.innerHTML = "";
240
+ const bar = document.createElement("div");
241
+ const frame = document.createElement("iframe");
242
+ bar.className = "bar";
243
+ bar.textContent = widget.title;
244
+ frame.title = widget.title;
245
+ // Intentionally minimal: no top-navigation, popups, or same-origin access.
246
+ frame.setAttribute("sandbox", "allow-scripts");
247
+ frame.referrerPolicy = "no-referrer";
248
+ frame.src = proxiedDocUrl(widget.docUrl);
249
+ app.append(bar, frame);
250
+ }
251
+ run().catch((error) => {
252
+ if (error?.status === 404) {
253
+ show("Widget unavailable", "No widget could be resolved for this channel.");
254
+ } else {
255
+ show("Gateway offline", "The OpenClaw gateway could not load this widget. Try again shortly.");
256
+ }
257
+ });
258
+ `;
259
+ //#endregion
260
+ //#region extensions/discord/src/activities/http.ts
261
+ const BODY_MAX_BYTES = 8192;
262
+ const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
263
+ const WIDGET_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
264
+ const DOC_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
265
+ const DISCORD_ACTIVITY_WIDGET_CSP = `sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline' ${WIDGET_CDN_ORIGINS.join(" ")}; style-src 'unsafe-inline' ${WIDGET_CDN_ORIGINS.join(" ")}; img-src data: blob:; font-src data: ${WIDGET_CDN_ORIGINS.join(" ")}; connect-src 'none'; frame-ancestors *`;
266
+ function setCommonHeaders(res) {
267
+ res.setHeader("Cache-Control", "no-store");
268
+ res.setHeader("Referrer-Policy", "no-referrer");
269
+ res.setHeader("X-Content-Type-Options", "nosniff");
270
+ }
271
+ function respond(res, statusCode, body, contentType, headers) {
272
+ res.statusCode = statusCode;
273
+ setCommonHeaders(res);
274
+ res.setHeader("Content-Type", contentType);
275
+ for (const [key, value] of Object.entries(headers ?? {})) res.setHeader(key, value);
276
+ res.end(body);
277
+ return true;
278
+ }
279
+ function jsonBody(body) {
280
+ return `${JSON.stringify(body)}\n`;
281
+ }
282
+ function respondJson(res, statusCode, body) {
283
+ return respond(res, statusCode, jsonBody(body), JSON_CONTENT_TYPE);
284
+ }
285
+ function notFound(res, widgetDocument = false) {
286
+ return respond(res, 404, "not found", "text/plain; charset=utf-8", widgetDocument ? { "Content-Security-Policy": DISCORD_ACTIVITY_WIDGET_CSP } : void 0);
287
+ }
288
+ function readHeader(req, name) {
289
+ const value = req.headers[name];
290
+ return Array.isArray(value) ? value[0] : value;
291
+ }
292
+ function extractApplicationId(req) {
293
+ for (const header of [readHeader(req, "origin"), readHeader(req, "referer")]) {
294
+ if (!header) continue;
295
+ try {
296
+ const match = new URL(header).hostname.match(/^(\d+)\.discordsays\.com$/i);
297
+ if (match?.[1]) return match[1];
298
+ } catch {}
299
+ }
300
+ }
301
+ function bearerToken(req) {
302
+ return ((readHeader(req, "authorization")?.trim())?.match(/^Bearer\s+([A-Za-z0-9_-]{43})$/i))?.[1];
303
+ }
304
+ function widgetIdFromCustomId(customId) {
305
+ if (WIDGET_ID_PATTERN.test(customId)) return customId;
306
+ return parseDiscordActivityCustomId(customId)?.widgetId;
307
+ }
308
+ function createDiscordActivityHttpHandler(deps) {
309
+ const fetchGuard = deps.fetchGuard ?? fetchWithSsrFGuard$1;
310
+ const limiter = new TokenRateLimiter(deps.now ?? Date.now);
311
+ const readVendorAsset = deps.readVendorAsset ?? ((assetPath) => fs.readFile(assetPath));
312
+ const reportError = deps.logError ?? logError;
313
+ const bodyTimeoutMs = deps.bodyTimeoutMs ?? WEBHOOK_BODY_READ_DEFAULTS.preAuth.timeoutMs;
314
+ let vendorAsset;
315
+ let pendingLaunchFailureLogged = false;
316
+ function logPendingLaunchFailure(error) {
317
+ if (pendingLaunchFailureLogged) return;
318
+ pendingLaunchFailureLogged = true;
319
+ reportError(`discord activity: failed to consume pending launch: ${String(error)}`);
320
+ }
321
+ async function handleToken(req, res) {
322
+ const cfg = deps.runtime.currentConfig();
323
+ const sourceIp = resolveRequestClientIp(req, cfg.gateway?.trustedProxies, cfg.gateway?.allowRealIpFallback === true) ?? "unknown";
324
+ if (!limiter.allowKey(sourceIp)) return respondJson(res, 429, { error: "too many token requests" });
325
+ const applicationId = extractApplicationId(req);
326
+ const account = deps.runtime.resolveHttpAccount(applicationId);
327
+ if (!account) return respondJson(res, 503, { error: "Discord Activities is not fully configured" });
328
+ const endpointRuntime = getDiscordEndpointRuntime() ?? null;
329
+ const bodyResult = await readJsonBodyWithLimit(req, {
330
+ maxBytes: BODY_MAX_BYTES,
331
+ timeoutMs: bodyTimeoutMs,
332
+ emptyObjectOnEmpty: true,
333
+ destroyOnLimit: false
334
+ });
335
+ if (!bodyResult.ok && (bodyResult.code === "REQUEST_BODY_TIMEOUT" || bodyResult.code === "PAYLOAD_TOO_LARGE")) {
336
+ const timedOut = bodyResult.code === "REQUEST_BODY_TIMEOUT";
337
+ await sendHttpRequestRejection(req, res, timedOut ? 408 : 413, jsonBody({ error: timedOut ? "request body timeout" : "request body too large" }), JSON_CONTENT_TYPE);
338
+ return true;
339
+ }
340
+ const body = bodyResult.ok && bodyResult.value && typeof bodyResult.value === "object" && !Array.isArray(bodyResult.value) ? bodyResult.value : null;
341
+ const code = typeof body?.code === "string" ? body.code.trim() : "";
342
+ if (!code) return respondJson(res, 401, { error: "invalid authorization code" });
343
+ const reservation = limiter.reserveGlobal();
344
+ if (reservation === null) return respondJson(res, 429, { error: "too many token requests" });
345
+ let completed = false;
346
+ try {
347
+ let tokenResponse;
348
+ try {
349
+ tokenResponse = await fetchDiscordJson({
350
+ fetchGuard,
351
+ fetchImpl: account.proxyFetch,
352
+ url: DISCORD_TOKEN_URL,
353
+ init: {
354
+ method: "POST",
355
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
356
+ body: new URLSearchParams({
357
+ grant_type: "authorization_code",
358
+ client_id: account.applicationId,
359
+ client_secret: account.clientSecret,
360
+ code
361
+ })
362
+ },
363
+ auditContext: "discord.activities.oauth.token",
364
+ endpointRuntime
365
+ });
366
+ } catch {
367
+ return respondJson(res, 503, { error: "Discord token exchange unavailable" });
368
+ }
369
+ const granted = typeof tokenResponse.body?.access_token === "string" ? tokenResponse.body.access_token.trim() : "";
370
+ if (!tokenResponse.ok || !granted) return respondJson(res, 401, { error: "invalid authorization code" });
371
+ let userResponse;
372
+ try {
373
+ userResponse = await fetchDiscordJson({
374
+ fetchGuard,
375
+ fetchImpl: account.proxyFetch,
376
+ url: DISCORD_USER_URL,
377
+ init: { headers: { Authorization: `Bearer ${granted}` } },
378
+ auditContext: "discord.activities.oauth.user",
379
+ endpointRuntime
380
+ });
381
+ } catch {
382
+ return respondJson(res, 503, { error: "Discord user lookup unavailable" });
383
+ }
384
+ const discordUserId = typeof userResponse.body?.id === "string" ? userResponse.body.id : void 0;
385
+ if (!userResponse.ok || !discordUserId) return respondJson(res, 401, { error: "Discord user lookup failed" });
386
+ const minted = await deps.runtime.store.createSession({
387
+ discordUserId,
388
+ accountId: account.accountId
389
+ });
390
+ completed = true;
391
+ return respondJson(res, 200, {
392
+ access_token: granted,
393
+ session_token: minted
394
+ });
395
+ } finally {
396
+ if (!completed) limiter.releaseGlobal(reservation);
397
+ }
398
+ }
399
+ async function handleWidget(req, res, url) {
400
+ const token = bearerToken(req);
401
+ const session = token ? await deps.runtime.store.lookupSession(token) : void 0;
402
+ if (!session) return respondJson(res, 401, { error: "invalid session" });
403
+ const customId = url.searchParams.get("custom_id")?.trim() ?? "";
404
+ const instanceId = normalizeInstanceId(url.searchParams.get("instance_id"));
405
+ const account = deps.runtime.resolveAccount(session.accountId);
406
+ const channelId = instanceId && account ? await resolveActivityInstanceChannel({
407
+ fetchGuard,
408
+ applicationId: account.applicationId,
409
+ instanceId,
410
+ discordUserId: session.discordUserId,
411
+ botAuth: account.botAuth,
412
+ proxyFetch: account.proxyFetch
413
+ }) : void 0;
414
+ if (!channelId) return respondJson(res, 404, { error: "widget not found" });
415
+ let resolved = null;
416
+ const requestedWidgetId = widgetIdFromCustomId(customId);
417
+ if (requestedWidgetId) {
418
+ const widget = await deps.runtime.store.lookupWidget(requestedWidgetId);
419
+ if (widget?.accountId !== session.accountId || widget.channelId !== channelId) return respondJson(res, 404, { error: "widget not found" });
420
+ resolved = {
421
+ id: requestedWidgetId,
422
+ widget
423
+ };
424
+ try {
425
+ await deps.runtime.store.retirePendingLaunch(session.accountId, channelId, session.discordUserId, requestedWidgetId);
426
+ } catch (error) {
427
+ logPendingLaunchFailure(error);
428
+ }
429
+ } else {
430
+ try {
431
+ const pendingLaunch = await deps.runtime.store.consumePendingLaunch(session.accountId, channelId, session.discordUserId);
432
+ if (pendingLaunch) {
433
+ const widget = await deps.runtime.store.lookupWidget(pendingLaunch.widgetId);
434
+ if (widget?.accountId === session.accountId && widget.channelId === channelId) resolved = {
435
+ id: pendingLaunch.widgetId,
436
+ widget
437
+ };
438
+ }
439
+ } catch (error) {
440
+ logPendingLaunchFailure(error);
441
+ }
442
+ resolved ??= await deps.runtime.store.latestPostedWidgetForChannel(session.accountId, channelId);
443
+ }
444
+ if (!resolved) return respondJson(res, 404, { error: "widget not found" });
445
+ const docToken = await deps.runtime.store.createDocToken({
446
+ widgetId: resolved.id,
447
+ accountId: session.accountId
448
+ });
449
+ return respondJson(res, 200, {
450
+ id: resolved.id,
451
+ title: resolved.widget.title,
452
+ docUrl: `${DISCORD_ACTIVITY_ROUTE_PREFIX}/api/widget/${encodeURIComponent(resolved.id)}/doc?wt=${encodeURIComponent(docToken)}`
453
+ });
454
+ }
455
+ async function handleDocument(res, widgetId, token) {
456
+ if (!WIDGET_ID_PATTERN.test(widgetId) || !DOC_TOKEN_PATTERN.test(token)) return notFound(res, true);
457
+ const capability = await deps.runtime.store.consumeDocToken(token);
458
+ if (!capability || capability.widgetId !== widgetId) return notFound(res, true);
459
+ const widget = await deps.runtime.store.lookupWidget(widgetId);
460
+ if (!widget || widget.accountId !== capability.accountId) return notFound(res, true);
461
+ return respond(res, 200, widget.html, "text/html; charset=utf-8", { "Content-Security-Policy": DISCORD_ACTIVITY_WIDGET_CSP });
462
+ }
463
+ return { async handleHttpRequest(req, res) {
464
+ const url = new URL(req.url ?? "/", "http://localhost");
465
+ if (url.pathname !== "/discord/activity" && !url.pathname.startsWith(`/discord/activity/`)) return false;
466
+ if (!deps.runtime.hasEnabledAccounts()) return false;
467
+ const relative = url.pathname.slice(17) || "/";
468
+ if (req.method === "GET" && (relative === "/" || relative === "/index.html")) return respond(res, 200, DISCORD_ACTIVITY_SHELL_HTML, "text/html; charset=utf-8", { "Content-Security-Policy": DISCORD_ACTIVITY_SHELL_CSP });
469
+ if (req.method === "GET" && relative === "/shell.js") return respond(res, 200, DISCORD_ACTIVITY_SHELL_JS, "text/javascript; charset=utf-8");
470
+ if (req.method === "GET" && relative === "/vendor/embedded-app-sdk.mjs") {
471
+ const pendingAsset = vendorAsset ??= readVendorAsset(deps.vendorAssetPath);
472
+ try {
473
+ return respond(res, 200, await pendingAsset, "text/javascript; charset=utf-8");
474
+ } catch {
475
+ if (vendorAsset === pendingAsset) vendorAsset = void 0;
476
+ return notFound(res);
477
+ }
478
+ }
479
+ if (req.method === "POST" && relative === "/api/token") return await handleToken(req, res);
480
+ if (req.method === "GET" && relative === "/api/widget") return await handleWidget(req, res, url);
481
+ const documentMatch = relative.match(/^\/api\/widget\/([^/]+)\/doc$/);
482
+ if (req.method === "GET" && documentMatch?.[1]) {
483
+ let widgetId;
484
+ try {
485
+ widgetId = decodeURIComponent(documentMatch[1]);
486
+ } catch {
487
+ return notFound(res, true);
488
+ }
489
+ return await handleDocument(res, widgetId, url.searchParams.get("wt") ?? "");
490
+ }
491
+ return notFound(res);
492
+ } };
493
+ }
494
+ //#endregion
495
+ //#region extensions/discord/src/activities/presenter.ts
496
+ const DISCORD_WIDGET_HTML_MAX_BYTES = 49152;
497
+ function resolveDiscordChannelId(context) {
498
+ const raw = context.nativeChannelId?.trim() || context.currentMessagingTarget?.trim() || context.currentChannelId?.trim() || context.deliveryContext?.to?.trim();
499
+ if (!raw) return;
500
+ try {
501
+ return resolveDiscordChannelId$1(raw);
502
+ } catch {
503
+ return;
504
+ }
505
+ }
506
+ function resolveDiscordPresentationRoute(context, runtime) {
507
+ if (context.messageChannel !== "discord") return;
508
+ const cfg = runtime.currentConfig();
509
+ const account = resolveDiscordAccount({
510
+ cfg,
511
+ accountId: context.accountId ?? context.deliveryContext?.accountId
512
+ });
513
+ const channelId = resolveDiscordChannelId(context);
514
+ const activityAccount = runtime.resolveAccount(account.accountId, cfg);
515
+ if (!channelId || !activityAccount) return;
516
+ return {
517
+ account: activityAccount,
518
+ cfg,
519
+ channelId
520
+ };
521
+ }
522
+ /** Presents a canonical core widget document in the active Discord channel. */
523
+ function createDiscordWidgetPresenter(runtime, deps = {}) {
524
+ return {
525
+ target: "current_channel",
526
+ description: "Post an Activity launch button in the current Discord channel",
527
+ capabilities: {
528
+ sourceKinds: ["html"],
529
+ maxSourceBytes: DISCORD_WIDGET_HTML_MAX_BYTES
530
+ },
531
+ match: (context) => resolveDiscordPresentationRoute(context, runtime) !== void 0,
532
+ async availability(context) {
533
+ return resolveDiscordPresentationRoute(context, runtime) ? {
534
+ ok: true,
535
+ value: { available: true }
536
+ } : {
537
+ ok: false,
538
+ error: {
539
+ code: "unavailable",
540
+ message: "Discord Activities are unavailable for the current channel and account."
541
+ }
542
+ };
543
+ },
544
+ async present({ context, document, title }) {
545
+ const route = resolveDiscordPresentationRoute(context, runtime);
546
+ if (!route) return {
547
+ ok: false,
548
+ error: {
549
+ code: "unavailable",
550
+ message: "Discord Activities are unavailable for the current channel and account."
551
+ }
552
+ };
553
+ if (Array.from(title).length > 80) return {
554
+ ok: false,
555
+ error: {
556
+ code: "presentation_error",
557
+ message: "title must be 80 characters or fewer"
558
+ }
559
+ };
560
+ const widgetId = await runtime.store.createWidget({
561
+ html: document.html,
562
+ title,
563
+ channelId: route.channelId,
564
+ accountId: route.account.accountId,
565
+ createdAt: (deps.now ?? Date.now)()
566
+ });
567
+ let result;
568
+ let deliveredResult;
569
+ let deliveryRecord;
570
+ let deliveryRecordError;
571
+ const recordDelivery = async (deliveryResult) => {
572
+ deliveredResult = deliveryResult;
573
+ deliveryRecord ??= runtime.store.markWidgetDelivered(widgetId, deliveryResult.messageId);
574
+ try {
575
+ await deliveryRecord;
576
+ } catch (error) {
577
+ deliveryRecordError ??= new Error("Discord widget was delivered, but its delivery state could not be saved", { cause: error });
578
+ throw deliveryRecordError;
579
+ }
580
+ };
581
+ try {
582
+ const components = buildDiscordPresentationComponents({ blocks: [{
583
+ type: "buttons",
584
+ buttons: [{
585
+ label: "Open widget",
586
+ action: {
587
+ type: "web-app",
588
+ widgetId
589
+ }
590
+ }]
591
+ }] });
592
+ if (!components) throw new Error("Discord widget launch button could not be rendered");
593
+ result = await (deps.sendComponentMessage ?? sendDiscordComponentMessage)(`channel:${route.channelId}`, {
594
+ ...components,
595
+ text: title
596
+ }, {
597
+ cfg: route.cfg,
598
+ accountId: route.account.accountId,
599
+ allowedMentions: { parse: [] },
600
+ onDeliveryResult: recordDelivery
601
+ });
602
+ await recordDelivery(result);
603
+ } catch (error) {
604
+ if (deliveryRecordError) throw deliveryRecordError;
605
+ if (!deliveredResult) {
606
+ await runtime.store.deleteWidget(widgetId);
607
+ throw error;
608
+ }
609
+ result = deliveredResult;
610
+ }
611
+ return {
612
+ ok: true,
613
+ value: {
614
+ kind: "message",
615
+ receipt: result.receipt
616
+ }
617
+ };
618
+ }
619
+ };
620
+ }
621
+ //#endregion
622
+ //#region extensions/discord/src/activities/store.ts
623
+ const DAY_MS = 864e5;
624
+ const DISCORD_EPOCH_MS = 14200704e5;
625
+ const WIDGET_TTL_MS = 7 * DAY_MS;
626
+ const SESSION_TTL_MS = 9e5;
627
+ const DOC_TOKEN_TTL_MS = 6e4;
628
+ const PENDING_LAUNCH_TTL_MS = 12e4;
629
+ function requireAtomicComparison(store) {
630
+ if (!store.observe || !store.compareAndApply) throw new Error("Discord Activities require atomic plugin state comparisons");
631
+ return store;
632
+ }
633
+ function openDiscordActivityStores(openKeyedStore) {
634
+ return {
635
+ widgets: requireAtomicComparison(openKeyedStore({
636
+ namespace: "activities-widgets",
637
+ maxEntries: 64,
638
+ overflowPolicy: "evict-oldest",
639
+ defaultTtlMs: WIDGET_TTL_MS
640
+ })),
641
+ sessions: openKeyedStore({
642
+ namespace: "activities-sessions",
643
+ maxEntries: 256,
644
+ overflowPolicy: "evict-oldest",
645
+ defaultTtlMs: SESSION_TTL_MS
646
+ }),
647
+ docTokens: openKeyedStore({
648
+ namespace: "activities-doc-tokens",
649
+ maxEntries: 256,
650
+ overflowPolicy: "evict-oldest",
651
+ defaultTtlMs: DOC_TOKEN_TTL_MS
652
+ }),
653
+ launches: requireAtomicComparison(openKeyedStore({
654
+ namespace: "activities-launches",
655
+ maxEntries: 256,
656
+ overflowPolicy: "evict-oldest",
657
+ defaultTtlMs: PENDING_LAUNCH_TTL_MS
658
+ }))
659
+ };
660
+ }
661
+ function pendingLaunchKey(accountId, channelId, discordUserId) {
662
+ return `${accountId}:${channelId}:${discordUserId}`;
663
+ }
664
+ async function applyActivityStoreIntent(store, key, intent) {
665
+ let observed = await store.observe(key);
666
+ while (true) {
667
+ const result = await store.compareAndApply(key, observed.comparison, intent(observed.value));
668
+ if (result.status !== "conflict") return result.status;
669
+ observed = result.current;
670
+ }
671
+ }
672
+ function pendingLaunchForWidget(existing, widgetId, createdAt) {
673
+ return existing && (existing.state === "ambiguous" || existing.widgetId !== widgetId) ? {
674
+ state: "ambiguous",
675
+ createdAt
676
+ } : {
677
+ state: "single",
678
+ widgetId,
679
+ createdAt
680
+ };
681
+ }
682
+ var DiscordActivityStore = class {
683
+ constructor(stores) {
684
+ this.stores = stores;
685
+ this.lastWidgetCreatedAt = 0;
686
+ }
687
+ async createWidget(value) {
688
+ const id = randomBytes(16).toString("base64url");
689
+ const createdAt = Math.max(value.createdAt, this.lastWidgetCreatedAt + 1);
690
+ this.lastWidgetCreatedAt = createdAt;
691
+ await this.stores.widgets.register(id, {
692
+ ...value,
693
+ createdAt,
694
+ deliveredMessageId: null
695
+ });
696
+ return id;
697
+ }
698
+ async markWidgetDelivered(id, messageId) {
699
+ if (!/^\d+$/u.test(messageId)) throw new Error("Discord Activity delivery returned an invalid message ID");
700
+ if (await applyActivityStoreIntent(this.stores.widgets, id, (widget) => widget ? {
701
+ operation: "update",
702
+ action: "set",
703
+ value: {
704
+ ...widget,
705
+ deliveredMessageId: messageId
706
+ }
707
+ } : {
708
+ operation: "update",
709
+ action: "keep"
710
+ }) === "unchanged") throw new Error("Discord Activity widget disappeared before delivery was recorded");
711
+ }
712
+ async deleteWidget(id) {
713
+ await this.stores.widgets.delete(id);
714
+ }
715
+ async lookupWidget(id) {
716
+ return await this.stores.widgets.lookup(id);
717
+ }
718
+ async latestPostedWidgetForChannel(accountId, channelId) {
719
+ const entries = await this.stores.widgets.entries();
720
+ let match;
721
+ for (const entry of entries) {
722
+ if (entry.value.accountId !== accountId || entry.value.channelId !== channelId) continue;
723
+ if (entry.value.deliveredMessageId === null) continue;
724
+ const deliveryOrder = entry.value.deliveredMessageId ? BigInt(entry.value.deliveredMessageId) : BigInt(Math.max(0, Math.trunc(entry.value.createdAt - DISCORD_EPOCH_MS))) << 22n;
725
+ if (!match || deliveryOrder > match.deliveryOrder) match = {
726
+ entry,
727
+ deliveryOrder
728
+ };
729
+ }
730
+ return match ? {
731
+ id: match.entry.key,
732
+ widget: match.entry.value
733
+ } : null;
734
+ }
735
+ async createSession(value) {
736
+ const token = randomBytes(32).toString("base64url");
737
+ await this.stores.sessions.register(token, value);
738
+ return token;
739
+ }
740
+ async lookupSession(token) {
741
+ return await this.stores.sessions.lookup(token);
742
+ }
743
+ async createDocToken(value) {
744
+ const token = randomBytes(32).toString("base64url");
745
+ await this.stores.docTokens.register(token, value);
746
+ return token;
747
+ }
748
+ async consumeDocToken(token) {
749
+ return await this.stores.docTokens.consume(token);
750
+ }
751
+ async recordPendingLaunch(params) {
752
+ const key = pendingLaunchKey(params.accountId, params.channelId, params.discordUserId);
753
+ const { widgetId, createdAt } = params;
754
+ await applyActivityStoreIntent(this.stores.launches, key, (existing) => ({
755
+ operation: "update",
756
+ action: "set",
757
+ value: pendingLaunchForWidget(existing, widgetId, createdAt)
758
+ }));
759
+ }
760
+ async retirePendingLaunch(accountId, channelId, discordUserId, widgetId) {
761
+ const key = pendingLaunchKey(accountId, channelId, discordUserId);
762
+ await applyActivityStoreIntent(this.stores.launches, key, (existing) => ({
763
+ operation: "delete",
764
+ action: existing?.state === "single" && existing.widgetId === widgetId ? "delete" : "keep"
765
+ }));
766
+ }
767
+ async consumePendingLaunch(accountId, channelId, discordUserId) {
768
+ const launch = await this.stores.launches.consume(pendingLaunchKey(accountId, channelId, discordUserId));
769
+ return launch?.state === "single" ? launch : void 0;
770
+ }
771
+ };
772
+ //#endregion
773
+ //#region extensions/discord/src/activities/register.ts
774
+ function registerDiscordActivities(api) {
775
+ setDiscordActivitiesRuntime(void 0);
776
+ const store = new DiscordActivityStore(openDiscordActivityStores(api.runtime.state.openKeyedStore.bind(api.runtime.state)));
777
+ const runtime = new DiscordActivitiesRuntime(store, api.config, api.runtime.config?.current ? () => api.runtime.config.current() : void 0);
778
+ setDiscordActivitiesRuntime(runtime);
779
+ const http = createDiscordActivityHttpHandler({
780
+ runtime,
781
+ vendorAssetPath: api.resolvePath("assets/embedded-app-sdk.mjs")
782
+ });
783
+ api.registerHttpRoute({
784
+ path: DISCORD_ACTIVITY_ROUTE_PREFIX,
785
+ auth: "plugin",
786
+ match: "prefix",
787
+ handler: async (req, res) => await http.handleHttpRequest(req, res)
788
+ });
789
+ api.registerWidgetPresenter(createDiscordWidgetPresenter(runtime));
790
+ }
791
+ //#endregion
2
792
  export { registerDiscordActivities };