@vellumai/assistant 0.11.2-dev.202608070534.d55eaf1 → 0.11.2-dev.202608070733.a268a16

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.
@@ -40,3 +40,42 @@ export function isChannelId(value: unknown): value is ChannelId {
40
40
  (CHANNEL_IDS as readonly string[]).includes(value)
41
41
  );
42
42
  }
43
+
44
+ /**
45
+ * The provider key holding each channel's bot credential.
46
+ *
47
+ * Two senses of "connected" share the provider registry: a provider the user
48
+ * authorized so the assistant can act **as them** (`slack`, `discord`,
49
+ * `google`), and a bot credential letting people reach the assistant **as
50
+ * itself**. Nothing in a provider key says which, and the naming actively
51
+ * misleads: `slack` and `discord` name the user integration while their bots
52
+ * take a `_channel` suffix, yet `telegram` *is* the bot, because Telegram has
53
+ * no user-identity integration.
54
+ *
55
+ * That irregularity is why this is stated rather than derived from the key,
56
+ * and it is stated here because this file already owns what a channel is.
57
+ *
58
+ * Deliberately only the key. What fields each credential requires is declared
59
+ * once already, per service, in the gateway's credential specs; restating it
60
+ * here would be a second copy of a different fact.
61
+ *
62
+ * Channels absent from this map reach the assistant without a bot credential
63
+ * of their own: `phone` through the voice provider, `vellum` and `platform`
64
+ * internally.
65
+ */
66
+ export const CHANNEL_BOT_PROVIDER = {
67
+ slack: "slack_channel",
68
+ discord: "discord_channel",
69
+ telegram: "telegram",
70
+ } as const satisfies Partial<Record<ChannelId, string>>;
71
+
72
+ /**
73
+ * Whether a provider key names a bot the assistant is reached through, rather
74
+ * than a grant letting it act as the user. This is the "which sense of
75
+ * connected" question, for any provider key.
76
+ */
77
+ export function isChannelBotProvider(providerKey: string): boolean {
78
+ return (Object.values(CHANNEL_BOT_PROVIDER) as readonly string[]).includes(
79
+ providerKey,
80
+ );
81
+ }
@@ -40,3 +40,42 @@ export function isChannelId(value: unknown): value is ChannelId {
40
40
  (CHANNEL_IDS as readonly string[]).includes(value)
41
41
  );
42
42
  }
43
+
44
+ /**
45
+ * The provider key holding each channel's bot credential.
46
+ *
47
+ * Two senses of "connected" share the provider registry: a provider the user
48
+ * authorized so the assistant can act **as them** (`slack`, `discord`,
49
+ * `google`), and a bot credential letting people reach the assistant **as
50
+ * itself**. Nothing in a provider key says which, and the naming actively
51
+ * misleads: `slack` and `discord` name the user integration while their bots
52
+ * take a `_channel` suffix, yet `telegram` *is* the bot, because Telegram has
53
+ * no user-identity integration.
54
+ *
55
+ * That irregularity is why this is stated rather than derived from the key,
56
+ * and it is stated here because this file already owns what a channel is.
57
+ *
58
+ * Deliberately only the key. What fields each credential requires is declared
59
+ * once already, per service, in the gateway's credential specs; restating it
60
+ * here would be a second copy of a different fact.
61
+ *
62
+ * Channels absent from this map reach the assistant without a bot credential
63
+ * of their own: `phone` through the voice provider, `vellum` and `platform`
64
+ * internally.
65
+ */
66
+ export const CHANNEL_BOT_PROVIDER = {
67
+ slack: "slack_channel",
68
+ discord: "discord_channel",
69
+ telegram: "telegram",
70
+ } as const satisfies Partial<Record<ChannelId, string>>;
71
+
72
+ /**
73
+ * Whether a provider key names a bot the assistant is reached through, rather
74
+ * than a grant letting it act as the user. This is the "which sense of
75
+ * connected" question, for any provider key.
76
+ */
77
+ export function isChannelBotProvider(providerKey: string): boolean {
78
+ return (Object.values(CHANNEL_BOT_PROVIDER) as readonly string[]).includes(
79
+ providerKey,
80
+ );
81
+ }
@@ -40,3 +40,42 @@ export function isChannelId(value: unknown): value is ChannelId {
40
40
  (CHANNEL_IDS as readonly string[]).includes(value)
41
41
  );
42
42
  }
43
+
44
+ /**
45
+ * The provider key holding each channel's bot credential.
46
+ *
47
+ * Two senses of "connected" share the provider registry: a provider the user
48
+ * authorized so the assistant can act **as them** (`slack`, `discord`,
49
+ * `google`), and a bot credential letting people reach the assistant **as
50
+ * itself**. Nothing in a provider key says which, and the naming actively
51
+ * misleads: `slack` and `discord` name the user integration while their bots
52
+ * take a `_channel` suffix, yet `telegram` *is* the bot, because Telegram has
53
+ * no user-identity integration.
54
+ *
55
+ * That irregularity is why this is stated rather than derived from the key,
56
+ * and it is stated here because this file already owns what a channel is.
57
+ *
58
+ * Deliberately only the key. What fields each credential requires is declared
59
+ * once already, per service, in the gateway's credential specs; restating it
60
+ * here would be a second copy of a different fact.
61
+ *
62
+ * Channels absent from this map reach the assistant without a bot credential
63
+ * of their own: `phone` through the voice provider, `vellum` and `platform`
64
+ * internally.
65
+ */
66
+ export const CHANNEL_BOT_PROVIDER = {
67
+ slack: "slack_channel",
68
+ discord: "discord_channel",
69
+ telegram: "telegram",
70
+ } as const satisfies Partial<Record<ChannelId, string>>;
71
+
72
+ /**
73
+ * Whether a provider key names a bot the assistant is reached through, rather
74
+ * than a grant letting it act as the user. This is the "which sense of
75
+ * connected" question, for any provider key.
76
+ */
77
+ export function isChannelBotProvider(providerKey: string): boolean {
78
+ return (Object.values(CHANNEL_BOT_PROVIDER) as readonly string[]).includes(
79
+ providerKey,
80
+ );
81
+ }
package/openapi.yaml CHANGED
@@ -22151,6 +22151,11 @@ paths:
22151
22151
  anyOf:
22152
22152
  - type: string
22153
22153
  - type: "null"
22154
+ acts_as:
22155
+ type: string
22156
+ enum:
22157
+ - user
22158
+ - assistant
22154
22159
  required:
22155
22160
  - provider_key
22156
22161
  - display_name
@@ -22162,6 +22167,7 @@ paths:
22162
22167
  - supports_managed_mode
22163
22168
  - managed_service_is_paid
22164
22169
  - feature_flag
22170
+ - acts_as
22165
22171
  additionalProperties: false
22166
22172
  - type: "null"
22167
22173
  apps:
@@ -22679,6 +22685,11 @@ paths:
22679
22685
  anyOf:
22680
22686
  - type: string
22681
22687
  - type: "null"
22688
+ acts_as:
22689
+ type: string
22690
+ enum:
22691
+ - user
22692
+ - assistant
22682
22693
  required:
22683
22694
  - provider_key
22684
22695
  - display_name
@@ -22690,6 +22701,7 @@ paths:
22690
22701
  - supports_managed_mode
22691
22702
  - managed_service_is_paid
22692
22703
  - feature_flag
22704
+ - acts_as
22693
22705
  additionalProperties: false
22694
22706
  required:
22695
22707
  - providers
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.202608070733.a268a16",
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 () => {
@@ -291,9 +291,26 @@ describe("serializeProviderSummary", () => {
291
291
  supports_managed_mode: true,
292
292
  managed_service_is_paid: false,
293
293
  feature_flag: null,
294
+ acts_as: "user",
294
295
  });
295
296
  });
296
297
 
298
+ test("names which sense of connected a provider represents", () => {
299
+ // The provider key does not say, and the naming actively misleads:
300
+ // `slack` is the user integration while its bot is `slack_channel`, but
301
+ // `telegram` is itself the bot. A caller asking "is X connected?" needs
302
+ // this to answer which X it found.
303
+ const sense = (provider: string) =>
304
+ serializeProviderSummary(makeRow({ provider }))!.acts_as;
305
+
306
+ expect(sense("slack")).toBe("user");
307
+ expect(sense("slack_channel")).toBe("assistant");
308
+ expect(sense("discord")).toBe("user");
309
+ expect(sense("discord_channel")).toBe("assistant");
310
+ expect(sense("telegram")).toBe("assistant");
311
+ expect(sense("google")).toBe("user");
312
+ });
313
+
297
314
  test("nullifies missing optional fields", () => {
298
315
  const row = makeRow({
299
316
  displayLabel: null,
@@ -178,6 +178,7 @@ describe("GET /v1/oauth/providers", () => {
178
178
  "supports_managed_mode",
179
179
  "managed_service_is_paid",
180
180
  "feature_flag",
181
+ "acts_as",
181
182
  ];
182
183
 
183
184
  for (const provider of providers) {
@@ -13,6 +13,8 @@ const log = getCliLogger("cli");
13
13
 
14
14
  interface SerializedProvider {
15
15
  providerKey: string;
16
+ /** Which sense of connected: a grant to act as the user, or a bot. */
17
+ actsAs?: "user" | "assistant";
16
18
  displayName?: string | null;
17
19
  description?: string | null;
18
20
  supportsManagedMode?: boolean;
@@ -301,6 +303,7 @@ export function registerProviderCommands(oauth: Command): void {
301
303
  dashboardUrl: p.dashboard_url as string | null,
302
304
  clientIdPlaceholder: p.client_id_placeholder as string | null,
303
305
  featureFlag: p.feature_flag as string | null,
306
+ actsAs: p.acts_as as "user" | "assistant" | undefined,
304
307
  }));
305
308
 
306
309
  if (opts.providerKey) {
@@ -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, {
@@ -19,6 +19,7 @@ import {
19
19
  getSecureKeyResultAsync,
20
20
  type SecureKeyResult,
21
21
  } from "../security/secure-keys.js";
22
+ import { manualTokenProvider } from "./manual-token-providers.js";
22
23
 
23
24
  // ── Types ─────────────────────────────────────────────────────────────
24
25
 
@@ -44,16 +45,8 @@ export interface ConnectionAccessTokenResult {
44
45
  * rather than assuming the OAuth access-token path.
45
46
  */
46
47
  function manualTokenAccessCredentialKey(provider: string): string | null {
47
- switch (provider) {
48
- case "slack_channel":
49
- return credentialKey("slack_channel", "bot_token");
50
- case "telegram":
51
- return credentialKey("telegram", "bot_token");
52
- case "sanity":
53
- return credentialKey("sanity", "api_token");
54
- default:
55
- return null;
56
- }
48
+ const spec = manualTokenProvider(provider);
49
+ return spec ? credentialKey(provider, spec.accessField) : null;
57
50
  }
58
51
 
59
52
  /**
@@ -12,6 +12,10 @@ import { credentialKey } from "../security/credential-key.js";
12
12
  import { getSecureKeyResultAsync } from "../security/secure-keys.js";
13
13
  import { getTelegramBotUsername } from "../telegram/bot-username.js";
14
14
  import { getLogger } from "../util/logger.js";
15
+ import {
16
+ MANUAL_TOKEN_PROVIDERS,
17
+ manualTokenProvider,
18
+ } from "./manual-token-providers.js";
15
19
  import {
16
20
  createConnection,
17
21
  deleteConnection,
@@ -135,69 +139,30 @@ export async function syncManualTokenConnection(
135
139
  resolvedAccountInfo,
136
140
  );
137
141
 
138
- switch (provider) {
139
- case "telegram": {
140
- const botTokenResult = await getSecureKeyResultAsync(
141
- credentialKey("telegram", "bot_token"),
142
- );
143
- const webhookSecretResult = await getSecureKeyResultAsync(
144
- credentialKey("telegram", "webhook_secret"),
145
- );
146
- if (botTokenResult.unreachable || webhookSecretResult.unreachable) {
147
- log.warn(
148
- "Skipping telegram manual-token reconciliation — credential backend unreachable",
149
- );
150
- return;
151
- }
152
- if (botTokenResult.value && webhookSecretResult.value) {
153
- await ensureManualTokenConnection(provider, accountInfoToStore);
154
- } else {
155
- removeManualTokenConnection(provider);
156
- }
157
- return;
158
- }
159
-
160
- case "slack_channel": {
161
- const botTokenResult = await getSecureKeyResultAsync(
162
- credentialKey("slack_channel", "bot_token"),
163
- );
164
- const appTokenResult = await getSecureKeyResultAsync(
165
- credentialKey("slack_channel", "app_token"),
166
- );
167
- if (botTokenResult.unreachable || appTokenResult.unreachable) {
168
- log.warn(
169
- "Skipping slack_channel manual-token reconciliation — credential backend unreachable",
170
- );
171
- return;
172
- }
173
- if (botTokenResult.value && appTokenResult.value) {
174
- await ensureManualTokenConnection(provider, accountInfoToStore);
175
- } else {
176
- removeManualTokenConnection(provider);
177
- }
178
- return;
179
- }
142
+ const spec = manualTokenProvider(provider);
143
+ if (!spec) {
144
+ return;
145
+ }
180
146
 
181
- case "sanity": {
182
- const tokenResult = await getSecureKeyResultAsync(
183
- credentialKey("sanity", "api_token"),
184
- );
185
- if (tokenResult.unreachable) {
186
- log.warn(
187
- "Skipping sanity manual-token reconciliation — credential backend unreachable",
188
- );
189
- return;
190
- }
191
- if (tokenResult.value) {
192
- await ensureManualTokenConnection(provider, accountInfoToStore);
193
- } else {
194
- removeManualTokenConnection(provider);
195
- }
196
- return;
197
- }
147
+ // Every field must be present and readable. An unreachable backend is not
148
+ // evidence a credential was removed, so the row is left as it stands rather
149
+ // than being read as a disconnection.
150
+ const results = await Promise.all(
151
+ spec.fields.map((field) =>
152
+ getSecureKeyResultAsync(credentialKey(provider, field)),
153
+ ),
154
+ );
155
+ if (results.some((r) => r.unreachable)) {
156
+ log.warn(
157
+ `Skipping ${provider} manual-token reconciliation: credential backend unreachable`,
158
+ );
159
+ return;
160
+ }
198
161
 
199
- default:
200
- return;
162
+ if (results.every((r) => r.value)) {
163
+ await ensureManualTokenConnection(provider, accountInfoToStore);
164
+ } else {
165
+ removeManualTokenConnection(provider);
201
166
  }
202
167
  }
203
168
 
@@ -214,7 +179,7 @@ export async function syncManualTokenConnection(
214
179
  * connection row.
215
180
  */
216
181
  export async function backfillManualTokenConnections(): Promise<void> {
217
- await syncManualTokenConnection("telegram");
218
- await syncManualTokenConnection("slack_channel");
219
- await syncManualTokenConnection("sanity");
182
+ for (const provider of Object.keys(MANUAL_TOKEN_PROVIDERS)) {
183
+ await syncManualTokenConnection(provider);
184
+ }
220
185
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Providers whose credentials are pasted in rather than granted through a
3
+ * browser flow, and what each one stores.
4
+ *
5
+ * Two questions are answered together because they are the same fact seen from
6
+ * two sides, and answering them in separate tables lets them drift: which
7
+ * fields must be present for the provider to count as configured
8
+ * (reconciliation), and which single field an API request authenticates with
9
+ * (token resolution). Slack stores an app token and Telegram a webhook secret,
10
+ * and neither authenticates a request.
11
+ *
12
+ * `accessField` is named rather than taken as the first of `fields`, so
13
+ * reordering that list cannot silently change which secret gets sent.
14
+ *
15
+ * Channel bots are keyed off the shared channel contract, so adding a channel
16
+ * does not mean remembering this file exists. `sanity` is not in that contract
17
+ * because it is a user's own API token: manual to enter, but not a bot and not
18
+ * a channel.
19
+ *
20
+ * The gateway declares the same field sets for its own reading of these
21
+ * credentials (`gateway/src/credential-reader.ts`). One declaration serving
22
+ * both is worth doing and needs the specs hoisted into a shared package.
23
+ */
24
+
25
+ import { CHANNEL_BOT_PROVIDER } from "@vellumai/service-contracts/channels";
26
+
27
+ export interface ManualTokenProviderSpec {
28
+ /** Every field that must be present for the provider to count as connected. */
29
+ fields: readonly string[];
30
+ /** The one field an API request authenticates with. */
31
+ accessField: string;
32
+ }
33
+
34
+ export const MANUAL_TOKEN_PROVIDERS: Record<string, ManualTokenProviderSpec> = {
35
+ [CHANNEL_BOT_PROVIDER.slack]: {
36
+ fields: ["bot_token", "app_token"],
37
+ accessField: "bot_token",
38
+ },
39
+ [CHANNEL_BOT_PROVIDER.discord]: {
40
+ fields: ["bot_token"],
41
+ accessField: "bot_token",
42
+ },
43
+ [CHANNEL_BOT_PROVIDER.telegram]: {
44
+ fields: ["bot_token", "webhook_secret"],
45
+ accessField: "bot_token",
46
+ },
47
+ sanity: { fields: ["api_token"], accessField: "api_token" },
48
+ };
49
+
50
+ /**
51
+ * The spec for a provider, or undefined when it is not a manual-token one.
52
+ *
53
+ * Own-property lookup so an inherited key (`constructor`, `__proto__`) reads
54
+ * as absent rather than returning a function.
55
+ */
56
+ export function manualTokenProvider(
57
+ provider: string,
58
+ ): ManualTokenProviderSpec | undefined {
59
+ return Object.prototype.hasOwnProperty.call(MANUAL_TOKEN_PROVIDERS, provider)
60
+ ? MANUAL_TOKEN_PROVIDERS[provider]
61
+ : undefined;
62
+ }
@@ -18,6 +18,8 @@ import type { OAuthProviderRow } from "./oauth-store.js";
18
18
  export type SerializedProvider = ReturnType<typeof serializeProvider> &
19
19
  Record<string, unknown>;
20
20
 
21
+ import { isChannelBotProvider } from "@vellumai/service-contracts/channels";
22
+
21
23
  /**
22
24
  * Lightweight summary projection of an OAuth provider, suitable for API
23
25
  * list responses where full detail is not needed. All keys are snake_case
@@ -34,6 +36,16 @@ export interface SerializedProviderSummary {
34
36
  supports_managed_mode: boolean;
35
37
  managed_service_is_paid: boolean;
36
38
  feature_flag: string | null;
39
+ /**
40
+ * Which sense of "connected" this provider represents: `assistant` for a bot
41
+ * credential people reach the assistant through, `user` for a grant letting
42
+ * it act as the person who authorized it.
43
+ *
44
+ * Derived rather than stored, because the provider key does not say: `slack`
45
+ * and `discord` name the user integration while their bots take a `_channel`
46
+ * suffix, yet `telegram` is the bot.
47
+ */
48
+ acts_as: "user" | "assistant";
37
49
  }
38
50
 
39
51
  /**
@@ -152,5 +164,6 @@ export function serializeProviderSummary(
152
164
  supports_managed_mode: !!row.managedServiceConfigKey,
153
165
  managed_service_is_paid: !!row.managedServiceIsPaid,
154
166
  feature_flag: row.featureFlag ?? null,
167
+ acts_as: isChannelBotProvider(row.provider) ? "assistant" : "user",
155
168
  };
156
169
  }
@@ -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",
@@ -397,6 +397,7 @@ export const oauthProviderSummarySchema = z.object({
397
397
  supports_managed_mode: z.boolean(),
398
398
  managed_service_is_paid: z.boolean(),
399
399
  feature_flag: z.string().nullable(),
400
+ acts_as: z.enum(["user", "assistant"]),
400
401
  });
401
402
 
402
403
  /**