@vellumai/assistant 0.11.2-dev.202608070534.d55eaf1 → 0.11.2-dev.202608070630.7754d48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.2-dev.202608070534.d55eaf1",
3
+ "version": "0.11.2-dev.202608070630.7754d48",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -139,6 +139,51 @@ describe("syncManualTokenConnection", () => {
139
139
  warnings.length = 0;
140
140
  });
141
141
 
142
+ // ---- Discord server bot: one required field ----
143
+
144
+ test("connects discord_channel once the bot token is stored", async () => {
145
+ // Seeding the provider without this reconciliation leaves it listed and
146
+ // permanently disconnected, which is worse than not listing it: the
147
+ // setup skill stores the token and nothing ever reflects that.
148
+ setCredentialResult("discord_channel", "bot_token", {
149
+ value: "bot-token-value",
150
+ unreachable: false,
151
+ });
152
+
153
+ await syncManualTokenConnection("discord_channel");
154
+
155
+ expect(createdConnections.map((c) => c.provider)).toContain(
156
+ "discord_channel",
157
+ );
158
+ });
159
+
160
+ test("removes the discord_channel connection when the bot token is gone", async () => {
161
+ seedConnection("discord_channel");
162
+ setCredentialResult("discord_channel", "bot_token", {
163
+ value: undefined,
164
+ unreachable: false,
165
+ });
166
+
167
+ await syncManualTokenConnection("discord_channel");
168
+
169
+ expect(deletedConnectionIds).toContain("conn-discord_channel");
170
+ });
171
+
172
+ test("leaves the discord_channel row alone when the backend is unreachable", async () => {
173
+ // An unreachable credential store is not evidence the token is gone, so
174
+ // reconciliation must not read it as a disconnection.
175
+ seedConnection("discord_channel");
176
+ setCredentialResult("discord_channel", "bot_token", {
177
+ value: undefined,
178
+ unreachable: true,
179
+ });
180
+
181
+ await syncManualTokenConnection("discord_channel");
182
+
183
+ expect(deletedConnectionIds).not.toContain("conn-discord_channel");
184
+ expect(connectionStore["discord_channel"]).toBeDefined();
185
+ });
186
+
142
187
  // ---- Slack: reachable backend, missing tokens -> removes row ----
143
188
 
144
189
  test("removes slack_channel connection when bot token is missing and backend is reachable", async () => {
@@ -47,6 +47,11 @@ export class BYOOAuthConnection implements OAuthConnection {
47
47
  async (token) => {
48
48
  const effectiveBaseUrl = req.baseUrl ?? this.baseUrl;
49
49
  const isTelegram = this.provider === "telegram";
50
+ // Discord bot tokens authenticate with the `Bot ` scheme, not
51
+ // `Bearer`. Sending Bearer here reaches Discord as an unusable
52
+ // credential and comes back 401, which reads as a revoked token.
53
+ const authScheme =
54
+ this.provider === "discord_channel" ? "Bot" : "Bearer";
50
55
  const requestPath = isTelegram
51
56
  ? buildTelegramBotApiPath(req.path, token)
52
57
  : req.path;
@@ -87,7 +92,7 @@ export class BYOOAuthConnection implements OAuthConnection {
87
92
  }
88
93
  }
89
94
  if (!isTelegram) {
90
- headers.set("Authorization", `Bearer ${token}`);
95
+ headers.set("Authorization", `${authScheme} ${token}`);
91
96
  }
92
97
 
93
98
  const resp = await fetch(fullUrl, {
@@ -49,6 +49,8 @@ function manualTokenAccessCredentialKey(provider: string): string | null {
49
49
  return credentialKey("slack_channel", "bot_token");
50
50
  case "telegram":
51
51
  return credentialKey("telegram", "bot_token");
52
+ case "discord_channel":
53
+ return credentialKey("discord_channel", "bot_token");
52
54
  case "sanity":
53
55
  return credentialKey("sanity", "api_token");
54
56
  default:
@@ -178,6 +178,24 @@ export async function syncManualTokenConnection(
178
178
  return;
179
179
  }
180
180
 
181
+ case "discord_channel": {
182
+ const botTokenResult = await getSecureKeyResultAsync(
183
+ credentialKey("discord_channel", "bot_token"),
184
+ );
185
+ if (botTokenResult.unreachable) {
186
+ log.warn(
187
+ "Skipping discord_channel manual-token reconciliation — credential backend unreachable",
188
+ );
189
+ return;
190
+ }
191
+ if (botTokenResult.value) {
192
+ await ensureManualTokenConnection(provider, accountInfoToStore);
193
+ } else {
194
+ removeManualTokenConnection(provider);
195
+ }
196
+ return;
197
+ }
198
+
181
199
  case "sanity": {
182
200
  const tokenResult = await getSecureKeyResultAsync(
183
201
  credentialKey("sanity", "api_token"),
@@ -216,5 +234,6 @@ export async function syncManualTokenConnection(
216
234
  export async function backfillManualTokenConnections(): Promise<void> {
217
235
  await syncManualTokenConnection("telegram");
218
236
  await syncManualTokenConnection("slack_channel");
237
+ await syncManualTokenConnection("discord_channel");
219
238
  await syncManualTokenConnection("sanity");
220
239
  }
@@ -454,16 +454,15 @@ export const PROVIDER_SEED_DATA: Record<
454
454
  pingUrl: "https://discord.com/api/v10/users/@me",
455
455
  baseUrl: "https://discord.com/api/v10",
456
456
  displayLabel: "Discord",
457
- description: "Servers and messages",
457
+ // Your servers, not their contents. These scopes reach the guild list and
458
+ // your membership in one; reading messages is not among them. Discord
459
+ // documents `messages.read` as local RPC only, so requesting it would put
460
+ // a permission on the consent screen that grants nothing here.
461
+ description: "Your servers and profile",
458
462
  dashboardUrl: "https://discord.com/developers/applications",
459
463
  clientIdPlaceholder: null,
460
464
  logoUrl: "https://cdn.simpleicons.org/discord",
461
- defaultScopes: [
462
- "identify",
463
- "guilds",
464
- "guilds.members.read",
465
- "messages.read",
466
- ],
465
+ defaultScopes: ["identify", "guilds", "guilds.members.read"],
467
466
  availableScopes:
468
467
  "https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes",
469
468
  loopbackPort: 17326,
@@ -777,6 +776,35 @@ export const PROVIDER_SEED_DATA: Record<
777
776
  ],
778
777
  },
779
778
 
779
+ // The bot that sits in a server and talks to people there, which is a
780
+ // different thing from the `discord` integration above and starts at the
781
+ // same Discord authorize URL. Both are reasonably called "connecting
782
+ // Discord", so each has to be nameable on its own. The bot token is already
783
+ // stored under this key by `skills/discord-app-setup`; seeding the provider
784
+ // is what lets the product tell the two apart.
785
+ discord_channel: {
786
+ provider: "discord_channel",
787
+ authorizeUrl: "urn:manual-token",
788
+ tokenExchangeUrl: "urn:manual-token",
789
+ pingUrl: "https://discord.com/api/v10/users/@me",
790
+ baseUrl: "https://discord.com/api/v10",
791
+ displayLabel: "Discord Server",
792
+ description: "A bot in your server",
793
+ dashboardUrl: "https://discord.com/developers/applications",
794
+ clientIdPlaceholder: null,
795
+ requiresClientSecret: false,
796
+ logoUrl: "https://cdn.simpleicons.org/discord",
797
+ defaultScopes: [],
798
+ injectionTemplates: [
799
+ {
800
+ hostPattern: "discord.com",
801
+ injectionType: "header",
802
+ headerName: "Authorization",
803
+ valuePrefix: "Bot ",
804
+ },
805
+ ],
806
+ },
807
+
780
808
  telegram: {
781
809
  provider: "telegram",
782
810
  authorizeUrl: "urn:manual-token",