@open-everywhere/provider-discord 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/approvals/discord-admin.ts +5 -4
- package/agent/approvals/discord-flow.ts +2 -1
- package/agent/channels/discord-backscroll.ts +2 -1
- package/agent/channels/discord-bot-name.ts +2 -1
- package/agent/channels/discord-channel-info.ts +3 -1
- package/agent/channels/discord-credentials.ts +41 -0
- package/agent/channels/discord-directory.ts +2 -1
- package/agent/channels/discord-events.ts +3 -3
- package/agent/channels/discord-front.ts +37 -3
- package/agent/channels/discord-gateway.ts +10 -9
- package/agent/channels/discord-invite.ts +2 -1
- package/agent/channels/discord-message-ops.ts +6 -5
- package/agent/channels/discord-post.ts +10 -9
- package/agent/channels/discord-read.ts +2 -1
- package/agent/channels/discord.ts +11 -4
- package/agent/commands/discord-command-sync.ts +13 -10
- package/agent/docs-generated.ts +55 -34
- package/agent/ops.ts +9 -4
- package/agent/register.ts +8 -4
- package/dist/agent/approvals/discord-admin.d.ts.map +1 -1
- package/dist/agent/approvals/discord-admin.js +5 -4
- package/dist/agent/approvals/discord-admin.js.map +1 -1
- package/dist/agent/approvals/discord-flow.d.ts.map +1 -1
- package/dist/agent/approvals/discord-flow.js +2 -1
- package/dist/agent/approvals/discord-flow.js.map +1 -1
- package/dist/agent/channels/discord-backscroll.d.ts.map +1 -1
- package/dist/agent/channels/discord-backscroll.js +2 -1
- package/dist/agent/channels/discord-backscroll.js.map +1 -1
- package/dist/agent/channels/discord-bot-name.d.ts.map +1 -1
- package/dist/agent/channels/discord-bot-name.js +2 -1
- package/dist/agent/channels/discord-bot-name.js.map +1 -1
- package/dist/agent/channels/discord-channel-info.d.ts.map +1 -1
- package/dist/agent/channels/discord-channel-info.js +2 -1
- package/dist/agent/channels/discord-channel-info.js.map +1 -1
- package/dist/agent/channels/discord-credentials.d.ts +13 -0
- package/dist/agent/channels/discord-credentials.d.ts.map +1 -0
- package/dist/agent/channels/discord-credentials.js +38 -0
- package/dist/agent/channels/discord-credentials.js.map +1 -0
- package/dist/agent/channels/discord-directory.d.ts.map +1 -1
- package/dist/agent/channels/discord-directory.js +2 -1
- package/dist/agent/channels/discord-directory.js.map +1 -1
- package/dist/agent/channels/discord-events.d.ts +1 -1
- package/dist/agent/channels/discord-events.d.ts.map +1 -1
- package/dist/agent/channels/discord-events.js +2 -2
- package/dist/agent/channels/discord-events.js.map +1 -1
- package/dist/agent/channels/discord-front.d.ts +18 -1
- package/dist/agent/channels/discord-front.d.ts.map +1 -1
- package/dist/agent/channels/discord-front.js +33 -2
- package/dist/agent/channels/discord-front.js.map +1 -1
- package/dist/agent/channels/discord-gateway.d.ts +3 -3
- package/dist/agent/channels/discord-gateway.d.ts.map +1 -1
- package/dist/agent/channels/discord-gateway.js +10 -9
- package/dist/agent/channels/discord-gateway.js.map +1 -1
- package/dist/agent/channels/discord-invite.d.ts.map +1 -1
- package/dist/agent/channels/discord-invite.js +2 -1
- package/dist/agent/channels/discord-invite.js.map +1 -1
- package/dist/agent/channels/discord-message-ops.d.ts.map +1 -1
- package/dist/agent/channels/discord-message-ops.js +6 -5
- package/dist/agent/channels/discord-message-ops.js.map +1 -1
- package/dist/agent/channels/discord-post.d.ts.map +1 -1
- package/dist/agent/channels/discord-post.js +10 -9
- package/dist/agent/channels/discord-post.js.map +1 -1
- package/dist/agent/channels/discord-read.d.ts.map +1 -1
- package/dist/agent/channels/discord-read.js +2 -1
- package/dist/agent/channels/discord-read.js.map +1 -1
- package/dist/agent/channels/discord.js +11 -4
- package/dist/agent/channels/discord.js.map +1 -1
- package/dist/agent/commands/discord-command-sync.d.ts +8 -7
- package/dist/agent/commands/discord-command-sync.d.ts.map +1 -1
- package/dist/agent/commands/discord-command-sync.js +13 -10
- package/dist/agent/commands/discord-command-sync.js.map +1 -1
- package/dist/agent/docs-generated.d.ts.map +1 -1
- package/dist/agent/docs-generated.js +55 -34
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +9 -4
- package/dist/agent/ops.js.map +1 -1
- package/dist/agent/register.d.ts.map +1 -1
- package/dist/agent/register.js +7 -5
- package/dist/agent/register.js.map +1 -1
- package/package.json +6 -2
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// a SECRET — header only, never logged.
|
|
25
25
|
|
|
26
26
|
import { isGuildAdministrator } from "./discord-card.js";
|
|
27
|
+
import { discordBotToken } from "../channels/discord-credentials.js";
|
|
27
28
|
|
|
28
29
|
function log(msg: string): void {
|
|
29
30
|
console.log(`[open-everywhere:discord-admin] ${msg}`);
|
|
@@ -71,7 +72,7 @@ const memberVerdictCache = new Map<string, { verdict: boolean; at: number }>();
|
|
|
71
72
|
* (the user's role ids). NEVER throws; every failure is `false` (see the file header).
|
|
72
73
|
*/
|
|
73
74
|
export async function isGuildAdminViaRest(guildId: string, userId: string, deps: DiscordAdminDeps = {}): Promise<boolean> {
|
|
74
|
-
const token = deps.token ??
|
|
75
|
+
const token = deps.token ?? (await discordBotToken());
|
|
75
76
|
if (!token || !guildId || !userId) return false;
|
|
76
77
|
const cacheKey = `${guildId}:${userId}`;
|
|
77
78
|
const now = deps.nowMs ?? Date.now();
|
|
@@ -130,7 +131,7 @@ export async function isGuildAdminViaRest(guildId: string, userId: string, deps:
|
|
|
130
131
|
* error, a malformed body — all "not a member", never a guess. NEVER throws.
|
|
131
132
|
*/
|
|
132
133
|
export async function isGuildMemberViaRest(guildId: string, userId: string, deps: DiscordAdminDeps = {}): Promise<boolean> {
|
|
133
|
-
const token = deps.token ??
|
|
134
|
+
const token = deps.token ?? (await discordBotToken());
|
|
134
135
|
if (!token || !guildId || !userId) return false;
|
|
135
136
|
const cacheKey = `${guildId}:${userId}`;
|
|
136
137
|
const now = deps.nowMs ?? Date.now();
|
|
@@ -189,7 +190,7 @@ const roleEvidenceCache = new Map<string, { roles: readonly string[]; at: number
|
|
|
189
190
|
* NEVER throws.
|
|
190
191
|
*/
|
|
191
192
|
export async function discordAuthorRoles(guildId: string, userId: string, deps: DiscordAdminDeps = {}): Promise<readonly string[] | undefined> {
|
|
192
|
-
const token = deps.token ??
|
|
193
|
+
const token = deps.token ?? (await discordBotToken());
|
|
193
194
|
if (!token || !guildId || !userId) return undefined; // cannot check — never a guess
|
|
194
195
|
const cacheKey = `${guildId}:${userId}`;
|
|
195
196
|
const now = deps.nowMs ?? Date.now();
|
|
@@ -306,7 +307,7 @@ function overwritesOf(channel: Record<string, unknown>): DiscordOverwrite[] {
|
|
|
306
307
|
* only about a person with live cover.
|
|
307
308
|
*/
|
|
308
309
|
export async function discordMemberCanViewChannel(guildId: string, channelId: string, userId: string, deps: DiscordAdminDeps = {}): Promise<boolean> {
|
|
309
|
-
const token = deps.token ??
|
|
310
|
+
const token = deps.token ?? (await discordBotToken());
|
|
310
311
|
if (!token || !guildId || !channelId || !userId) return false;
|
|
311
312
|
const doFetch = deps.fetchFn ?? fetch;
|
|
312
313
|
const headers = { authorization: `Bot ${token}` };
|
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
type DiscordCardMessage,
|
|
80
80
|
} from "./discord-card.js";
|
|
81
81
|
import { guildIdFromScopeKey, isGuildAdminViaRest } from "./discord-admin.js";
|
|
82
|
+
import { discordBotToken } from "../channels/discord-credentials.js";
|
|
82
83
|
|
|
83
84
|
function log(msg: string): void {
|
|
84
85
|
console.log(`[open-everywhere:discord-approvals] ${msg}`);
|
|
@@ -160,7 +161,7 @@ async function postDiscordCardMessage(
|
|
|
160
161
|
what: string,
|
|
161
162
|
render: () => DiscordCardMessage,
|
|
162
163
|
): Promise<string | undefined> {
|
|
163
|
-
const token = deps.token ??
|
|
164
|
+
const token = deps.token ?? (await discordBotToken());
|
|
164
165
|
if (!token) {
|
|
165
166
|
log(`DISCORD_BOT_TOKEN is not set — cannot post the ${what} to channel ${row.channelId}`);
|
|
166
167
|
return undefined;
|
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
SUPERVISOR_BACKSCROLL_REACHBACK_HOURS,
|
|
80
80
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
81
81
|
import { discordAuthorRoles } from "../approvals/discord-admin.js";
|
|
82
|
+
import { discordBotToken } from "./discord-credentials.js";
|
|
82
83
|
|
|
83
84
|
/** The triggering wake, narrowed to what the backscroll needs. */
|
|
84
85
|
export interface DiscordBackscrollInput {
|
|
@@ -109,7 +110,7 @@ export interface DiscordBackscrollDeps {
|
|
|
109
110
|
// any throw → undefined. Kept local rather than imported from discord-gateway.ts so the module
|
|
110
111
|
// graph stays a DAG (discord-gateway → here; importing back up would cycle).
|
|
111
112
|
const liveRequest: DiscordRequester = async (channelId, query) => {
|
|
112
|
-
const token =
|
|
113
|
+
const token = await discordBotToken();
|
|
113
114
|
if (!token || !channelId) return undefined;
|
|
114
115
|
try {
|
|
115
116
|
const qs = new URLSearchParams(query).toString();
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
// deployment missing the token does not re-probe Discord on every turn.
|
|
35
35
|
|
|
36
36
|
import type { BotDisplayNameArm } from "@open-everywhere/core/agent/adapter-api.js";
|
|
37
|
+
import { discordBotToken } from "./discord-credentials.js";
|
|
37
38
|
|
|
38
39
|
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
|
39
40
|
|
|
@@ -43,7 +44,7 @@ const DISCORD_API_BASE = "https://discord.com/api/v10";
|
|
|
43
44
|
export type DiscordSelfRequester = () => Promise<unknown>;
|
|
44
45
|
|
|
45
46
|
const liveDiscordSelfRequest: DiscordSelfRequester = async () => {
|
|
46
|
-
const token =
|
|
47
|
+
const token = await discordBotToken();
|
|
47
48
|
if (!token) throw new Error("bot-display-name: DISCORD_BOT_TOKEN is not set.");
|
|
48
49
|
const res = await fetch(`${DISCORD_API_BASE}/users/@me`, {
|
|
49
50
|
headers: { authorization: `Bot ${token}` },
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// SECRET — header only, never logged. Definitive verdicts are cached in-process (see the
|
|
15
15
|
// cache note); a transient failure is asked again.
|
|
16
16
|
|
|
17
|
+
import { discordBotToken } from "./discord-credentials.js";
|
|
18
|
+
|
|
17
19
|
/** The channel facts both consumers read. `parentId` is set ONLY for threads. */
|
|
18
20
|
export interface DiscordChannelInfo {
|
|
19
21
|
readonly name?: string;
|
|
@@ -62,7 +64,7 @@ export function fetchDiscordInfo(
|
|
|
62
64
|
// 404, no token — is kept.
|
|
63
65
|
let transient = false;
|
|
64
66
|
p = (async () => {
|
|
65
|
-
const token = deps.token ??
|
|
67
|
+
const token = deps.token ?? (await discordBotToken());
|
|
66
68
|
if (!token) return undefined;
|
|
67
69
|
try {
|
|
68
70
|
const doFetch = deps.fetchFn ?? fetch;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// WHERE THE DISCORD BUNDLE'S TWO SECRETS COME FROM (OPE-2650) — the bot token every Discord REST call
|
|
2
|
+
// authenticates with, and the shared secret the gateway relay signs its deliveries with.
|
|
3
|
+
//
|
|
4
|
+
// Both are OPERATOR SECRETS: `oe secret set DISCORD_BOT_TOKEN`. Core's `operatorSecretOrUnset` reads
|
|
5
|
+
// them: the db credential store first, the env var of the same name as its warned on-ramp, each store
|
|
6
|
+
// lookup remembered for thirty seconds. So a deployment that still holds the env vars works unchanged,
|
|
7
|
+
// and one that stored them works with no env var at all.
|
|
8
|
+
//
|
|
9
|
+
// 🔴 A FAULT READS AS "NO TOKEN", AND IT IS LOGGED. Most Discord call sites promise "NEVER throws"
|
|
10
|
+
// (an admin check answers false, a post answers null). A stored row this process cannot decrypt (no
|
|
11
|
+
// armed `OE_CREDENTIAL_ENCRYPTION_KEY`) is therefore reported once per process and answered as
|
|
12
|
+
// `undefined`. Every caller already fails closed on `undefined`: no token means no call.
|
|
13
|
+
|
|
14
|
+
import { operatorSecretOrUnset } from "@open-everywhere/core/agent/adapter-api.js";
|
|
15
|
+
|
|
16
|
+
/** The operator secret holding the bot token. */
|
|
17
|
+
export const DISCORD_BOT_TOKEN_NAME = "DISCORD_BOT_TOKEN";
|
|
18
|
+
/** The operator secret holding the gateway relay's shared secret (the app side; the relay process
|
|
19
|
+
* keeps its own env). */
|
|
20
|
+
const OE_RELAY_SECRET_NAME = "OE_RELAY_SECRET";
|
|
21
|
+
|
|
22
|
+
/** The bot token, or `undefined` when this deployment holds none. */
|
|
23
|
+
export async function discordBotToken(): Promise<string | undefined> {
|
|
24
|
+
return await operatorSecretOrUnset(DISCORD_BOT_TOKEN_NAME);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* eve's outbound bot-token provider (`discordChannel({ credentials: { botToken } })`). Throws eve's OWN
|
|
29
|
+
* message when none resolves, byte for byte. Without it eve reads the DISCORD_BOT_TOKEN env var
|
|
30
|
+
* itself and never sees a stored value.
|
|
31
|
+
*/
|
|
32
|
+
export async function discordBotTokenForEve(): Promise<string> {
|
|
33
|
+
const token = await discordBotToken();
|
|
34
|
+
if (!token) throw new Error("DISCORD_BOT_TOKEN is required.");
|
|
35
|
+
return token;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The gateway relay's shared secret, or `undefined` when this deployment holds none. */
|
|
39
|
+
export async function discordRelaySecret(): Promise<string | undefined> {
|
|
40
|
+
return await operatorSecretOrUnset(OE_RELAY_SECRET_NAME);
|
|
41
|
+
}
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
// can never answer.
|
|
50
50
|
|
|
51
51
|
import { admittedTenantsFromConfig, type DirectoryCandidate } from "@open-everywhere/core/agent/adapter-api.js";
|
|
52
|
+
import { discordBotToken } from "./discord-credentials.js";
|
|
52
53
|
|
|
53
54
|
const API = "https://discord.com/api/v10";
|
|
54
55
|
|
|
@@ -88,7 +89,7 @@ interface DiscordGuildMember {
|
|
|
88
89
|
* The guild set comes from this deployment's own config instead — see the header.
|
|
89
90
|
*/
|
|
90
91
|
export async function fetchGuildDirectory(query: string): Promise<readonly DirectoryCandidate[]> {
|
|
91
|
-
const token =
|
|
92
|
+
const token = await discordBotToken();
|
|
92
93
|
if (!token) return [];
|
|
93
94
|
// The same normalization core's matcher applies (`@tommy` and `tommy` are one query to a person),
|
|
94
95
|
// done here too because Discord's search would take the `@` literally and match nothing.
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
// verification path — one key, one verifier.
|
|
29
29
|
|
|
30
30
|
import { POST, type Channel, type RouteHandlerArgs } from "eve/channels";
|
|
31
|
-
import { verifyDiscordInteraction } from "./discord-front.js";
|
|
31
|
+
import { discordPublicKey, verifyDiscordInteraction } from "./discord-front.js";
|
|
32
32
|
import { appendAudit, deploymentOrg } from "@open-everywhere/core/agent/adapter-api.js";
|
|
33
33
|
import type { AuditDetail } from "@open-everywhere/core/agent/adapter-api.js";
|
|
34
34
|
|
|
@@ -115,7 +115,7 @@ export function classifyDiscordEvent(rawBody: string): DiscordEventDecision {
|
|
|
115
115
|
|
|
116
116
|
/** Injectable seams so the route is testable without live crypto env or the real store. */
|
|
117
117
|
export interface DiscordEventsOptions {
|
|
118
|
-
/** The app public key; defaults to
|
|
118
|
+
/** The app public key; defaults to the `discord.publicKey` setting (the same key the interactions path reads). */
|
|
119
119
|
readonly publicKey?: string;
|
|
120
120
|
readonly now?: () => number;
|
|
121
121
|
/** Audit writer (defaults to the real appendAudit under the deployment org). */
|
|
@@ -136,7 +136,7 @@ const ACK = () => new Response(null, { status: 204 });
|
|
|
136
136
|
*/
|
|
137
137
|
export function buildDiscordEventsHandler(opts: DiscordEventsOptions = {}): (req: Request, args: RouteHandlerArgs) => Promise<Response> {
|
|
138
138
|
const now = opts.now ?? (() => Date.now());
|
|
139
|
-
const publicKey = () => opts.publicKey ??
|
|
139
|
+
const publicKey = () => opts.publicKey ?? discordPublicKey();
|
|
140
140
|
const audit =
|
|
141
141
|
opts.audit ??
|
|
142
142
|
(async (row) => {
|
|
@@ -33,7 +33,7 @@ import { POST, type Channel, type HttpRouteDefinition, type RouteHandlerArgs } f
|
|
|
33
33
|
import { verifyDiscordSignature } from "eve/channels/discord";
|
|
34
34
|
import { OE_DISCORD_APPROVAL_PREFIX } from "../approvals/discord-card.js";
|
|
35
35
|
import { handleDiscordApprovalInteraction, type DiscordInteractionResponse } from "../approvals/discord-flow.js";
|
|
36
|
-
import { LINK_CONSENT_CLICK_PREFIX, OE_GRANT_ACTION_PREFIX, friendlyDiscordBody } from "@open-everywhere/core/agent/adapter-api.js";
|
|
36
|
+
import { LINK_CONSENT_CLICK_PREFIX, OE_GRANT_ACTION_PREFIX, friendlyDiscordBody, getSettings } from "@open-everywhere/core/agent/adapter-api.js";
|
|
37
37
|
import { handleDiscordLinkConsentInteraction } from "../identity/discord-link-consent.js";
|
|
38
38
|
import { handleDiscordGrantCardInteraction } from "../access/discord-grant-click.js";
|
|
39
39
|
import { oeCommandName, type AttachSessionForCancel } from "@open-everywhere/core/agent/adapter-api.js";
|
|
@@ -175,9 +175,43 @@ export function classifyDiscordInteraction(rawBody: string): DiscordFrontDecisio
|
|
|
175
175
|
return { kind: "forward", reason: "component click — eve's HITL owns it" };
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// WHICH DISCORD APP THIS DEPLOYMENT IS (OPE-2650). The application id and its interaction public key
|
|
179
|
+
// are the `discord.applicationId` and `discord.publicKey` settings, no longer env. Both are public. Each
|
|
180
|
+
// is read per call, so a settings override reaches the next delivery. `undefined` means unset.
|
|
181
|
+
|
|
182
|
+
/** The `discord.publicKey` setting, or `undefined` when it is unset. */
|
|
183
|
+
export function discordPublicKey(): string | undefined {
|
|
184
|
+
return getSettings().discord.publicKey || undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The `discord.applicationId` setting, or `undefined` when it is unset. */
|
|
188
|
+
export function discordApplicationId(): string | undefined {
|
|
189
|
+
return getSettings().discord.applicationId || undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The two settings as eve's lazy credential fields, for `discordChannel({ credentials })`.
|
|
194
|
+
*
|
|
195
|
+
* 🔴 A FIELD LEFT UNDEFINED FALLS BACK TO ENV INSIDE EVE (`DISCORD_APPLICATION_ID`,
|
|
196
|
+
* `DISCORD_PUBLIC_KEY`), so the channel must always pass both. Each resolver throws when its setting
|
|
197
|
+
* is unset, which is eve's own "is required" refusal. An unset `discord.publicKey` refuses every
|
|
198
|
+
* delivery. eve resolves `discord.applicationId` only when an interaction carries no application id
|
|
199
|
+
* of its own, so an unset id skips the command sync and does not refuse a delivery.
|
|
200
|
+
*/
|
|
201
|
+
export function discordAppCredentials(): { readonly applicationId: () => string; readonly publicKey: () => string } {
|
|
202
|
+
const required = (value: string | undefined, path: string): string => {
|
|
203
|
+
if (value === undefined) throw new Error(`${path} is required: set it in agent/settings/settings.ts.`);
|
|
204
|
+
return value;
|
|
205
|
+
};
|
|
206
|
+
return {
|
|
207
|
+
applicationId: () => required(discordApplicationId(), "discord.applicationId"),
|
|
208
|
+
publicKey: () => required(discordPublicKey(), "discord.publicKey"),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
178
212
|
/** Injectable seams so the front route is testable without live crypto env / the real store. */
|
|
179
213
|
export interface DiscordFrontOptions {
|
|
180
|
-
/** The app public key; defaults to
|
|
214
|
+
/** The app public key; defaults to the `discord.publicKey` setting (the key eve's channel verifies with). */
|
|
181
215
|
readonly publicKey?: string;
|
|
182
216
|
readonly now?: () => number;
|
|
183
217
|
/** Approval-click handler (defaults to the real approvals/discord-flow.ts). Injectable so
|
|
@@ -222,7 +256,7 @@ async function interactionJson(body: DiscordInteractionResponse): Promise<Respon
|
|
|
222
256
|
*/
|
|
223
257
|
export function buildDiscordFrontHandler(innerHandler: RouteHandler, opts: DiscordFrontOptions = {}): RouteHandler {
|
|
224
258
|
const now = opts.now ?? (() => Date.now());
|
|
225
|
-
const publicKey = () => opts.publicKey ??
|
|
259
|
+
const publicKey = () => opts.publicKey ?? discordPublicKey();
|
|
226
260
|
const handleClick = opts.handleApprovalInteraction ?? handleDiscordApprovalInteraction;
|
|
227
261
|
const handleLinkConsent = opts.handleLinkConsentInteraction ?? handleDiscordLinkConsentInteraction;
|
|
228
262
|
const handleGrantCard = opts.handleGrantCardInteraction ?? handleDiscordGrantCardInteraction;
|
|
@@ -182,6 +182,7 @@ import {
|
|
|
182
182
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
183
183
|
import { discordMemberCanViewChannel } from "../approvals/discord-admin.js";
|
|
184
184
|
import { fetchDiscordBackscrollLines, gradeDiscordContextLines, DISCORD_RAW_FETCH_REACHBACK_HOURS } from "./discord-backscroll.js";
|
|
185
|
+
import { discordBotToken, discordRelaySecret } from "./discord-credentials.js";
|
|
185
186
|
|
|
186
187
|
function log(msg: string): void {
|
|
187
188
|
console.log(`[open-everywhere:discord-gateway] ${msg}`);
|
|
@@ -543,14 +544,14 @@ export interface RelayRejection {
|
|
|
543
544
|
|
|
544
545
|
/**
|
|
545
546
|
* Gate one relay delivery. Returns null when it may proceed, else the {@link RelayRejection}.
|
|
546
|
-
* `secret`
|
|
547
|
-
*
|
|
547
|
+
* `secret` is the caller's resolved `OE_RELAY_SECRET` operator secret ({@link discordRelaySecret}),
|
|
548
|
+
* passed in so the gate stays pure and unit-tested with no store. Fail-closed: unset/empty secret → 401; missing/malformed/mismatched
|
|
548
549
|
* signature → 401 (verifyWebhookSignature returns false for a null or non-`sha256=` header).
|
|
549
550
|
*/
|
|
550
551
|
export function relayGate(
|
|
551
552
|
signatureHeader: string | null,
|
|
552
553
|
rawBody: string,
|
|
553
|
-
secret: string | undefined
|
|
554
|
+
secret: string | undefined,
|
|
554
555
|
): RelayRejection | null {
|
|
555
556
|
if (!secret) {
|
|
556
557
|
return { status: 401, reason: "OE_RELAY_SECRET is not set — rejecting unverified relay delivery" };
|
|
@@ -868,7 +869,7 @@ const AFFORDANCE_RETRY: RetryPolicy = { maxAttempts: 2, capMs: 2_000, totalBudge
|
|
|
868
869
|
* lives in one place (the bot token is a SECRET — never logged).
|
|
869
870
|
*/
|
|
870
871
|
export async function discordCreateMessage(channelId: string, content: string, deps: DiscordFetchDeps = {}): Promise<string | null> {
|
|
871
|
-
const token =
|
|
872
|
+
const token = await discordBotToken();
|
|
872
873
|
if (!token) {
|
|
873
874
|
log(`DISCORD_BOT_TOKEN is not set — cannot post to channel ${channelId}`);
|
|
874
875
|
return null;
|
|
@@ -923,7 +924,7 @@ export async function discordCreateMessage(channelId: string, content: string, d
|
|
|
923
924
|
* must stay silent rather than fall back to the channel.
|
|
924
925
|
*/
|
|
925
926
|
export async function openDiscordDm(userId: string, deps: DiscordFetchDeps = {}): Promise<string | null> {
|
|
926
|
-
const token =
|
|
927
|
+
const token = await discordBotToken();
|
|
927
928
|
if (!token) return null;
|
|
928
929
|
try {
|
|
929
930
|
const res = await discordFetch(
|
|
@@ -1101,7 +1102,7 @@ export function typingGateFeed(
|
|
|
1101
1102
|
}
|
|
1102
1103
|
|
|
1103
1104
|
export async function triggerDiscordTyping(channelId: string, deps: DiscordFetchDeps = {}): Promise<void> {
|
|
1104
|
-
const token =
|
|
1105
|
+
const token = await discordBotToken();
|
|
1105
1106
|
if (!token || !channelId) return;
|
|
1106
1107
|
try {
|
|
1107
1108
|
const res = await discordFetch(
|
|
@@ -1144,7 +1145,7 @@ export async function setDiscordReaction(
|
|
|
1144
1145
|
action: "PUT" | "DELETE",
|
|
1145
1146
|
deps: DiscordFetchDeps = {},
|
|
1146
1147
|
): Promise<void> {
|
|
1147
|
-
const token =
|
|
1148
|
+
const token = await discordBotToken();
|
|
1148
1149
|
if (!token) return; // no token → silently skip the affordance (degrades to no reaction)
|
|
1149
1150
|
if (!channelId || !messageId) return;
|
|
1150
1151
|
const encoded = encodeURIComponent(emoji);
|
|
@@ -1253,7 +1254,7 @@ export async function createDiscordThread(
|
|
|
1253
1254
|
name: string,
|
|
1254
1255
|
deps: DiscordFetchDeps = {},
|
|
1255
1256
|
): Promise<string | null> {
|
|
1256
|
-
const token =
|
|
1257
|
+
const token = await discordBotToken();
|
|
1257
1258
|
if (!token) {
|
|
1258
1259
|
log(`DISCORD_BOT_TOKEN is not set — cannot create thread on channel ${channelId}`);
|
|
1259
1260
|
return null;
|
|
@@ -3030,7 +3031,7 @@ export default withEndsTurnFinalChannel(defineChannel<DiscordGatewayState, Disco
|
|
|
3030
3031
|
|
|
3031
3032
|
// 1) Verify the relay HMAC over the RAW body, fail-closed. Reject BEFORE parsing so we
|
|
3032
3033
|
// never act on (or even JSON.parse) an unauthenticated payload.
|
|
3033
|
-
const rejection = relayGate(req.headers.get("x-oe-relay-signature"), rawBody);
|
|
3034
|
+
const rejection = relayGate(req.headers.get("x-oe-relay-signature"), rawBody, await discordRelaySecret());
|
|
3034
3035
|
if (rejection) {
|
|
3035
3036
|
log(`rejected (${rejection.status}): ${rejection.reason}`);
|
|
3036
3037
|
return new Response(rejection.reason, { status: rejection.status });
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// looked up once per ask, and an expired code must read as expired.
|
|
9
9
|
|
|
10
10
|
import type { MeetingInviteLookup, MeetingInviteResolver } from "@open-everywhere/core/agent/adapter-api.js";
|
|
11
|
+
import { discordBotToken } from "./discord-credentials.js";
|
|
11
12
|
|
|
12
13
|
/** Discord's channel types for a voice room: GUILD_VOICE (2) and GUILD_STAGE_VOICE (13). */
|
|
13
14
|
const GUILD_VOICE = 2;
|
|
@@ -22,7 +23,7 @@ export interface DiscordInviteDeps {
|
|
|
22
23
|
|
|
23
24
|
/** Look one invite code up. NEVER throws: every failure is a `found: false` answer core turns into a plain reason. */
|
|
24
25
|
export async function lookUpDiscordInvite(code: string, deps: DiscordInviteDeps = {}): Promise<MeetingInviteLookup> {
|
|
25
|
-
const token = deps.token ??
|
|
26
|
+
const token = deps.token ?? (await discordBotToken());
|
|
26
27
|
if (!token) return { found: false, why: "unreachable" };
|
|
27
28
|
try {
|
|
28
29
|
const doFetch = deps.fetchFn ?? fetch;
|
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
import type { SurfaceMessageOps, OutboundFile } from "@open-everywhere/core/agent/adapter-api.js";
|
|
16
16
|
import { discordPostMessage } from "./discord-post.js";
|
|
17
|
+
import { discordBotToken } from "./discord-credentials.js";
|
|
17
18
|
import { discordFetch } from "./discord-rest.js";
|
|
18
19
|
|
|
19
20
|
const API = "https://discord.com/api/v10";
|
|
20
21
|
|
|
21
|
-
function botToken(): string {
|
|
22
|
-
const token =
|
|
22
|
+
async function botToken(): Promise<string> {
|
|
23
|
+
const token = await discordBotToken();
|
|
23
24
|
if (!token) throw new Error("DISCORD_BOT_TOKEN is not set — the bot token is required to post to Discord.");
|
|
24
25
|
return token;
|
|
25
26
|
}
|
|
@@ -32,7 +33,7 @@ export const discordMessageOps: SurfaceMessageOps = {
|
|
|
32
33
|
async editMessage(channelId, messageId, text) {
|
|
33
34
|
const res = await discordFetch("PATCH /channels/{id}/messages/{id}", `${API}/channels/${channelId}/messages/${messageId}`, {
|
|
34
35
|
method: "PATCH",
|
|
35
|
-
headers: { authorization: `Bot ${botToken()}`, "content-type": "application/json" },
|
|
36
|
+
headers: { authorization: `Bot ${await botToken()}`, "content-type": "application/json" },
|
|
36
37
|
// 🔴 `components: []` STRIPS THE BUTTONS, AND IT IS LOAD-BEARING (OPE-1260). Discord leaves a
|
|
37
38
|
// message's components untouched when a PATCH omits them, so without this line a card rewritten
|
|
38
39
|
// to a closed state kept four live buttons over copy saying the request was already over.
|
|
@@ -91,7 +92,7 @@ export const discordMessageOps: SurfaceMessageOps = {
|
|
|
91
92
|
const res = await discordFetch(
|
|
92
93
|
on ? "PUT /channels/{id}/messages/{id}/reactions/{emoji}/@me" : "DELETE /channels/{id}/messages/{id}/reactions/{emoji}/@me",
|
|
93
94
|
`${API}/channels/${channelId}/messages/${messageId}/reactions/${encoded}/@me`,
|
|
94
|
-
{ method: on ? "PUT" : "DELETE", headers: { authorization: `Bot ${botToken()}` } },
|
|
95
|
+
{ method: on ? "PUT" : "DELETE", headers: { authorization: `Bot ${await botToken()}` } },
|
|
95
96
|
);
|
|
96
97
|
if (!res.ok && !(!on && res.status === 404)) {
|
|
97
98
|
console.warn(`[open-everywhere:discord] reaction ${on ? "add" : "remove"} failed (${res.status}) for message ${messageId}`);
|
|
@@ -112,7 +113,7 @@ export const discordMessageOps: SurfaceMessageOps = {
|
|
|
112
113
|
// No explicit content-type header — fetch derives the multipart boundary from the FormData.
|
|
113
114
|
const res = await discordFetch("POST /channels/{id}/messages", `${API}/channels/${channelId}/messages`, {
|
|
114
115
|
method: "POST",
|
|
115
|
-
headers: { authorization: `Bot ${botToken()}` },
|
|
116
|
+
headers: { authorization: `Bot ${await botToken()}` },
|
|
116
117
|
body: form,
|
|
117
118
|
});
|
|
118
119
|
if (!res.ok) throw new Error(`Discord create-message (files) failed (HTTP ${res.status}) for channel ${channelId}`);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// post-back, a tool must report whether the post actually landed).
|
|
13
13
|
|
|
14
14
|
import { markNotApplied } from "@open-everywhere/core/agent/adapter-api.js";
|
|
15
|
+
import { discordBotToken } from "./discord-credentials.js";
|
|
15
16
|
import { discordFetch } from "./discord-rest.js";
|
|
16
17
|
|
|
17
18
|
const API = "https://discord.com/api/v10";
|
|
@@ -24,8 +25,8 @@ function botHeaders(token: string): Record<string, string> {
|
|
|
24
25
|
return { authorization: `Bot ${token}`, "content-type": "application/json" };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function botToken(): string {
|
|
28
|
-
const token =
|
|
28
|
+
async function botToken(): Promise<string> {
|
|
29
|
+
const token = await discordBotToken();
|
|
29
30
|
if (!token) {
|
|
30
31
|
throw new Error("DISCORD_BOT_TOKEN is not set — the reply tool needs the bot token to post to Discord.");
|
|
31
32
|
}
|
|
@@ -174,7 +175,7 @@ export async function discordPostMessage(channelId: string, content: string, opt
|
|
|
174
175
|
export async function discordOpenDmChannel(userId: string): Promise<string> {
|
|
175
176
|
const res = await discordFetch("POST /users/@me/channels", `${API}/users/@me/channels`, {
|
|
176
177
|
method: "POST",
|
|
177
|
-
headers: botHeaders(botToken()),
|
|
178
|
+
headers: botHeaders(await botToken()),
|
|
178
179
|
body: JSON.stringify({ recipient_id: userId }),
|
|
179
180
|
});
|
|
180
181
|
if (!res.ok) {
|
|
@@ -229,7 +230,7 @@ export async function discordPostMessageWithButtons(
|
|
|
229
230
|
}
|
|
230
231
|
const res = await discordFetch("POST /channels/{id}/messages", `${API}/channels/${channelId}/messages`, {
|
|
231
232
|
method: "POST",
|
|
232
|
-
headers: botHeaders(botToken()),
|
|
233
|
+
headers: botHeaders(await botToken()),
|
|
233
234
|
body: JSON.stringify({
|
|
234
235
|
content,
|
|
235
236
|
components: [
|
|
@@ -262,7 +263,7 @@ export async function discordPostMessageWithButtons(
|
|
|
262
263
|
async function discordPostOne(channelId: string, content: string, replyToMessageId?: string): Promise<string> {
|
|
263
264
|
const res = await discordFetch("POST /channels/{id}/messages", `${API}/channels/${channelId}/messages`, {
|
|
264
265
|
method: "POST",
|
|
265
|
-
headers: botHeaders(botToken()),
|
|
266
|
+
headers: botHeaders(await botToken()),
|
|
266
267
|
body: JSON.stringify({
|
|
267
268
|
content,
|
|
268
269
|
...(replyToMessageId ? { message_reference: { message_id: replyToMessageId, fail_if_not_exists: false } } : {}),
|
|
@@ -310,7 +311,7 @@ export async function discordEditMessage(channelId: string, messageId: string, c
|
|
|
310
311
|
}
|
|
311
312
|
const res = await discordFetch("PATCH /channels/{id}/messages/{id}", `${API}/channels/${channelId}/messages/${messageId}`, {
|
|
312
313
|
method: "PATCH",
|
|
313
|
-
headers: botHeaders(botToken()),
|
|
314
|
+
headers: botHeaders(await botToken()),
|
|
314
315
|
body: JSON.stringify({ content }),
|
|
315
316
|
});
|
|
316
317
|
if (!res.ok) {
|
|
@@ -390,7 +391,7 @@ export async function discordAddReaction(channelId: string, messageId: string, e
|
|
|
390
391
|
const res = await discordFetch(
|
|
391
392
|
"PUT /channels/{id}/messages/{id}/reactions/{emoji}/@me",
|
|
392
393
|
`${API}/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(unicode)}/@me`,
|
|
393
|
-
{ method: "PUT", headers: botHeaders(botToken()) },
|
|
394
|
+
{ method: "PUT", headers: botHeaders(await botToken()) },
|
|
394
395
|
);
|
|
395
396
|
if (!res.ok) {
|
|
396
397
|
throw Object.assign(new Error(`react couldn't post to Discord — ${res.status} on channel ${channelId}`), { status: res.status });
|
|
@@ -412,7 +413,7 @@ export async function discordAddReaction(channelId: string, messageId: string, e
|
|
|
412
413
|
export async function discordExistingThreadForMessage(channelId: string, messageId: string): Promise<string | null> {
|
|
413
414
|
try {
|
|
414
415
|
const msgRes = await discordFetch("GET /channels/{id}/messages/{id}", `${API}/channels/${channelId}/messages/${messageId}`, {
|
|
415
|
-
headers: botHeaders(botToken()),
|
|
416
|
+
headers: botHeaders(await botToken()),
|
|
416
417
|
});
|
|
417
418
|
if (!msgRes.ok) return null;
|
|
418
419
|
const msg = (await msgRes.json()) as { thread?: { id?: unknown } };
|
|
@@ -438,7 +439,7 @@ export async function discordThreadForMessage(channelId: string, messageId: stri
|
|
|
438
439
|
`${API}/channels/${channelId}/messages/${messageId}/threads`,
|
|
439
440
|
{
|
|
440
441
|
method: "POST",
|
|
441
|
-
headers: botHeaders(botToken()),
|
|
442
|
+
headers: botHeaders(await botToken()),
|
|
442
443
|
body: JSON.stringify({ name: name.slice(0, 100) || "thread", auto_archive_duration: 1440 }),
|
|
443
444
|
},
|
|
444
445
|
);
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
21
21
|
import { linesFromDiscordMessages, type DiscordRoleProbe } from "./discord-backscroll.js";
|
|
22
22
|
import { discordAuthorRoles } from "../approvals/discord-admin.js";
|
|
23
|
+
import { discordBotToken } from "./discord-credentials.js";
|
|
23
24
|
|
|
24
25
|
const DISCORD_CHANNEL_DEFAULT_LIMIT = 50;
|
|
25
26
|
const DISCORD_THREAD_DEFAULT_LIMIT = 100;
|
|
@@ -29,7 +30,7 @@ const DISCORD_MAX_PAGE = 100;
|
|
|
29
30
|
export type DiscordReadRequester = (channelId: string, limit: number) => Promise<unknown>;
|
|
30
31
|
|
|
31
32
|
const liveDiscordRead: DiscordReadRequester = async (channelId, limit) => {
|
|
32
|
-
const token =
|
|
33
|
+
const token = await discordBotToken();
|
|
33
34
|
if (!token) throw new Error("read: DISCORD_BOT_TOKEN is not set — the Discord read tools need the bot token.");
|
|
34
35
|
const qs = new URLSearchParams({ limit: String(Math.min(limit, DISCORD_MAX_PAGE)) }).toString();
|
|
35
36
|
const res = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages?${qs}`, {
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
// derives a thinner, guild-scoped auth). Kept MINIMAL for launch — identity binding +
|
|
18
18
|
// dispatch only; no status-UX / onboarding extras (cf. slack.ts).
|
|
19
19
|
//
|
|
20
|
-
// Credentials:
|
|
21
|
-
//
|
|
22
|
-
//
|
|
20
|
+
// Credentials: the application id and the public key come from the `discord.applicationId` and
|
|
21
|
+
// `discord.publicKey` settings, and the bot token from `discordBotTokenForEve` (the DISCORD_BOT_TOKEN
|
|
22
|
+
// operator secret, ./discord-credentials.ts) — all OPE-2650, all passed below so eve never falls back
|
|
23
|
+
// to its own env names for them. Capturing the channel type / guild presence is a
|
|
23
24
|
// RAW-fact job; the security-critical interpretation (fail-closed to private) lives in
|
|
24
25
|
// the binding (discordVisibility).
|
|
25
26
|
//
|
|
@@ -51,8 +52,9 @@ import {
|
|
|
51
52
|
type ChatAuditPartition,
|
|
52
53
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
53
54
|
import { withEndsTurnFinalChannel } from "@open-everywhere/core/agent/adapter-api.js";
|
|
54
|
-
import { frontDiscordChannel, INTERNAL_DISCORD_ROUTE } from "./discord-front.js";
|
|
55
|
+
import { discordAppCredentials, frontDiscordChannel, INTERNAL_DISCORD_ROUTE } from "./discord-front.js";
|
|
55
56
|
import { withDiscordEventsRoute } from "./discord-events.js";
|
|
57
|
+
import { discordBotTokenForEve } from "./discord-credentials.js";
|
|
56
58
|
|
|
57
59
|
// The audit partition for a terminal event, rebuilt from the LIVE session state the same
|
|
58
60
|
// way slack.ts rebuilds its own (the completion handler has no auth context): guild →
|
|
@@ -109,6 +111,11 @@ function authFromInteraction(interaction: DiscordCommandInteraction): OpenEveryw
|
|
|
109
111
|
const discordInteractionChannel = discordChannel({
|
|
110
112
|
// Internal mount — the public URL is the approval-click front (frontDiscordChannel below).
|
|
111
113
|
route: INTERNAL_DISCORD_ROUTE,
|
|
114
|
+
// 🔴 EVE MUST BE HANDED EVERY FIELD (OPE-2650). With a field missing, eve's `mergeCredentials` falls
|
|
115
|
+
// back to its own env read (DISCORD_APPLICATION_ID, DISCORD_PUBLIC_KEY, DISCORD_BOT_TOKEN) and never
|
|
116
|
+
// sees a setting or a value `oe secret set` stored. Functions, not values: a value read at module
|
|
117
|
+
// load would pin whatever was true at boot.
|
|
118
|
+
credentials: { ...discordAppCredentials(), botToken: discordBotTokenForEve },
|
|
112
119
|
// 🔴 QUEUE, NEVER STEER (OPE-412) — eve's default `steer` cancels the turn in flight when a
|
|
113
120
|
// message arrives. Same reasoning as the Slack channel; see slack.ts for the full note.
|
|
114
121
|
turnPolicy: "queue",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
import type { RetryPolicy, SurfaceCommandOutcome, SurfaceCommandScope } from "@open-everywhere/core/agent/adapter-api.js";
|
|
36
36
|
import { discordFetch, type DiscordFetchLike } from "../channels/discord-rest.js";
|
|
37
37
|
import { buildOeDiscordCommandDefinition } from "./discord-commands.js";
|
|
38
|
+
import { discordApplicationId } from "../channels/discord-front.js";
|
|
39
|
+
import { discordBotToken } from "../channels/discord-credentials.js";
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* 🔴 NO 429 BACKOFF AT ALL — the reconcile sits on the FIRST REQUEST's latency path.
|
|
@@ -251,22 +253,23 @@ async function reconcileOneGuild(
|
|
|
251
253
|
/**
|
|
252
254
|
* THE RECONCILER the discord bundle registers with core's command plane.
|
|
253
255
|
*
|
|
254
|
-
* Runs at boot, so it is gated on BOTH
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
256
|
+
* Runs at boot, so it is gated on BOTH halves being present: the `discord.applicationId` setting says
|
|
257
|
+
* which app this is and `DISCORD_BOT_TOKEN` authenticates the call. Either missing ⇒ `skipped`, which
|
|
258
|
+
* is the correct reading of a deployment that carries the bundle but no Discord app — and it is also
|
|
259
|
+
* what keeps every offline test tier network-free, since no test sets the bot token.
|
|
258
260
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* in `agent/commands/discord-commands.ts` and
|
|
261
|
+
* The application id is a setting since OPE-2650: which app this deployment is, is a tracked fact.
|
|
262
|
+
* The bot token is an operator secret (OPE-2650): `oe secret set DISCORD_BOT_TOKEN`, with the env var
|
|
263
|
+
* as its on-ramp. The GUILD ids are tracked literals in `agent/commands/discord-commands.ts` and
|
|
264
|
+
* arrive here as the declared scope.
|
|
262
265
|
*/
|
|
263
266
|
export async function reconcileDiscordCommands(scope: SurfaceCommandScope, deps: DiscordCommandSyncDeps = {}): Promise<SurfaceCommandOutcome> {
|
|
264
|
-
const applicationId = deps.applicationId ??
|
|
265
|
-
const botToken = deps.botToken ??
|
|
267
|
+
const applicationId = deps.applicationId ?? discordApplicationId();
|
|
268
|
+
const botToken = deps.botToken ?? (await discordBotToken());
|
|
266
269
|
if (!applicationId || !botToken) {
|
|
267
270
|
return {
|
|
268
271
|
status: "skipped",
|
|
269
|
-
detail: "
|
|
272
|
+
detail: "the discord.applicationId setting and DISCORD_BOT_TOKEN must both be set for this deployment to register its own commands.",
|
|
270
273
|
};
|
|
271
274
|
}
|
|
272
275
|
|