@opengeni/api-router 0.11.2 → 0.12.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.
@@ -2,6 +2,7 @@ import {
2
2
  getDocumentChunk,
3
3
  listDocumentBases,
4
4
  searchDocuments,
5
+ type DocumentAccessFilter,
5
6
  type DocumentServices,
6
7
  } from "@opengeni/documents";
7
8
  import { createKnowledgeMemory, listKnowledgeMemories, type Database } from "@opengeni/db";
@@ -44,12 +45,23 @@ export function buildDocumentsMcpServer(
44
45
  accountId: string,
45
46
  workspaceId: string,
46
47
  documentServices: DocumentServices,
47
- options: { createdBySessionId?: string | undefined } = {},
48
+ options: {
49
+ createdBySessionId?: string | undefined;
50
+ /** The human subject whose agent is making this retrieval request. */
51
+ viewerSubjectId?: string | undefined;
52
+ } = {},
48
53
  ): McpServer {
49
54
  const server = new McpServer({
50
55
  name: "opengeni-documents",
51
56
  version: "1.0.0",
52
57
  });
58
+ // This server is the agent retrieval surface. Agent-disabled documents are
59
+ // never reachable. Workspace-visible documents are shared; private
60
+ // documents are available only to the creating subject's agent.
61
+ const agentAccess: DocumentAccessFilter = {
62
+ agentOnly: true,
63
+ ...(options.viewerSubjectId ? { viewerSubjectId: options.viewerSubjectId } : {}),
64
+ };
53
65
 
54
66
  server.registerTool(
55
67
  "list_document_bases",
@@ -68,7 +80,7 @@ export function buildDocumentsMcpServer(
68
80
  description: "Search indexed documents with hybrid, vector, or keyword retrieval.",
69
81
  inputSchema: SearchInputSchema,
70
82
  },
71
- async (input) => searchContent(db, workspaceId, documentServices, input),
83
+ async (input) => searchContent(db, workspaceId, documentServices, input, agentAccess),
72
84
  );
73
85
 
74
86
  server.registerTool(
@@ -78,7 +90,7 @@ export function buildDocumentsMcpServer(
78
90
  "Search company knowledge sources with optional base, source-kind, ACL, and retrieval-mode filters.",
79
91
  inputSchema: SearchInputSchema,
80
92
  },
81
- async (input) => searchContent(db, workspaceId, documentServices, input),
93
+ async (input) => searchContent(db, workspaceId, documentServices, input, agentAccess),
82
94
  );
83
95
 
84
96
  server.registerTool(
@@ -90,7 +102,7 @@ export function buildDocumentsMcpServer(
90
102
  },
91
103
  },
92
104
  async ({ chunkId }) => {
93
- const found = await getDocumentChunk(db, workspaceId, chunkId);
105
+ const found = await getDocumentChunk(db, workspaceId, chunkId, agentAccess);
94
106
  return {
95
107
  content: [
96
108
  { type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
@@ -109,7 +121,7 @@ export function buildDocumentsMcpServer(
109
121
  },
110
122
  },
111
123
  async ({ chunkId }) => {
112
- const found = await getDocumentChunk(db, workspaceId, chunkId);
124
+ const found = await getDocumentChunk(db, workspaceId, chunkId, agentAccess);
113
125
  return {
114
126
  content: [
115
127
  { type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
@@ -214,6 +226,7 @@ async function searchContent(
214
226
  | undefined;
215
227
  aclTags?: string[] | undefined;
216
228
  },
229
+ access: DocumentAccessFilter,
217
230
  ) {
218
231
  return {
219
232
  content: [
@@ -230,6 +243,7 @@ async function searchContent(
230
243
  ...(input.mode ? { mode: input.mode } : {}),
231
244
  ...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
232
245
  ...(input.aclTags ? { aclTags: input.aclTags } : {}),
246
+ access,
233
247
  },
234
248
  documentServices,
235
249
  ),
package/src/mcp/server.ts CHANGED
@@ -68,6 +68,7 @@ import {
68
68
  } from "@opengeni/db";
69
69
  import { appendAndPublishEvents, publishDurableSessionEvents } from "@opengeni/events";
70
70
  import {
71
+ createSignedState,
71
72
  createGitHubAppInstallationToken,
72
73
  GitHubAppConfigurationError,
73
74
  githubAppMissingSettings,
@@ -82,7 +83,12 @@ import {
82
83
  } from "@opengeni/core";
83
84
  import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
84
85
  import type { ApiRouteDeps } from "@opengeni/core";
85
- import { listWorkspaceGitHubRepositories } from "../github-access";
86
+ import {
87
+ githubBindingStatus,
88
+ listWorkspaceGitHubInstallationBindings,
89
+ listWorkspaceGitHubRepositories,
90
+ } from "../github-access";
91
+ import { githubBrowserBaseUrl, githubBrowserGrantClaims } from "../github-browser-flow";
86
92
  import {
87
93
  promoteVerifiedDefinitionEditChangeForApi,
88
94
  proposeRigChangeForApi,
@@ -138,11 +144,14 @@ import {
138
144
  } from "./session-view";
139
145
  import type { ToolspaceMcpSurface } from "./toolspace";
140
146
  import { ensureSessionGroupReady as ensureViewerSessionGroupReady } from "../sandbox/viewer";
147
+ import {
148
+ createOpenGeniSlackBotClient,
149
+ resolveSlackBotConnectionForTool,
150
+ } from "../integrations/slack-bot";
141
151
 
142
152
  export type McpServerOptions = {
143
- // Origin of the HTTP request that reached the MCP route. Retained in the
144
- // options ABI for browser-oriented tools; github_connect_link does not use
145
- // it or mint state while new installation binding is disabled.
153
+ // Origin of the HTTP request that reached the MCP route. Browser-oriented
154
+ // tools use it only when no configured public base URL is available.
146
155
  requestOrigin?: string | null;
147
156
  toolspace?: ToolspaceMcpSurface | null;
148
157
  workspaceMemoryEnabled?: boolean | undefined;
@@ -214,6 +223,7 @@ export function buildOpenGeniMcpServer(
214
223
  }
215
224
  if (!toolspaceMode) {
216
225
  registerRigTools(server, deps, grant, can, sessionId, json);
226
+ registerSlackBotTools(server, deps, grant, sessionId, json);
217
227
  }
218
228
 
219
229
  // Orchestration, variableSet, and GitHub status/token tools are permission-gated
@@ -229,7 +239,7 @@ export function buildOpenGeniMcpServer(
229
239
  registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId, toolspaceMode, json);
230
240
  registerVariableSetTools(server, deps, grant, can, json);
231
241
  if (can("github:use")) {
232
- registerGitHubConnectTool(server, deps, json);
242
+ registerGitHubConnectTool(server, deps, grant, options, json);
233
243
  // TOKEN-BROKER (B1): the agent-refreshable git token. Session-scoped (keys off the
234
244
  // worker-signed sessionId claim so it mints for THIS session's repos), gated on
235
245
  // the same github:use capability as github_connect_link.
@@ -647,6 +657,121 @@ export function buildOpenGeniMcpServer(
647
657
  return server;
648
658
  }
649
659
 
660
+ function registerSlackBotTools(
661
+ server: McpServer,
662
+ deps: ApiRouteDeps,
663
+ grant: AccessGrant,
664
+ sessionId: string | null,
665
+ json: JsonResult,
666
+ ): void {
667
+ const clientFor = async (connectionId?: string) => {
668
+ const resolved = await resolveSlackBotConnectionForTool({
669
+ db: deps.db,
670
+ grant,
671
+ sessionId,
672
+ ...(connectionId ? { requestedConnectionId: connectionId } : {}),
673
+ });
674
+ return createOpenGeniSlackBotClient(deps, resolved);
675
+ };
676
+
677
+ server.registerTool(
678
+ "slack_bot_list_channels",
679
+ {
680
+ description:
681
+ "List public and bot-visible private Slack channels through the workspace-shared OpenGeni bot. isMember identifies channels the bot may read/post in; the bot never joins channels automatically.",
682
+ inputSchema: {
683
+ connectionId: z4.string().uuid().optional(),
684
+ cursor: z4.string().max(1024).optional(),
685
+ limit: z4.number().int().min(1).max(200).optional(),
686
+ },
687
+ },
688
+ async ({ connectionId, cursor, limit }) =>
689
+ json(
690
+ await (
691
+ await clientFor(connectionId)
692
+ ).listChannels({
693
+ ...(cursor ? { cursor } : {}),
694
+ ...(limit !== undefined ? { limit } : {}),
695
+ }),
696
+ ),
697
+ );
698
+
699
+ server.registerTool(
700
+ "slack_bot_channel_history",
701
+ {
702
+ description:
703
+ "Read Slack channel history as the workspace-shared OpenGeni bot. Public and private channels both require bot membership; invite the bot to private channels first.",
704
+ inputSchema: {
705
+ connectionId: z4.string().uuid().optional(),
706
+ channelId: z4.string().min(1).max(64),
707
+ cursor: z4.string().max(1024).optional(),
708
+ limit: z4.number().int().min(1).max(100).optional(),
709
+ },
710
+ },
711
+ async ({ connectionId, channelId, cursor, limit }) =>
712
+ json(
713
+ await (
714
+ await clientFor(connectionId)
715
+ ).channelHistory({
716
+ channelId,
717
+ ...(cursor ? { cursor } : {}),
718
+ ...(limit !== undefined ? { limit } : {}),
719
+ }),
720
+ ),
721
+ );
722
+
723
+ server.registerTool(
724
+ "slack_bot_list_users",
725
+ {
726
+ description: "List Slack workspace users through the workspace-shared OpenGeni bot.",
727
+ inputSchema: {
728
+ connectionId: z4.string().uuid().optional(),
729
+ cursor: z4.string().max(1024).optional(),
730
+ limit: z4.number().int().min(1).max(200).optional(),
731
+ },
732
+ },
733
+ async ({ connectionId, cursor, limit }) =>
734
+ json(
735
+ await (
736
+ await clientFor(connectionId)
737
+ ).listUsers({
738
+ ...(cursor ? { cursor } : {}),
739
+ ...(limit !== undefined ? { limit } : {}),
740
+ }),
741
+ ),
742
+ );
743
+
744
+ server.registerTool(
745
+ "slack_bot_post_message",
746
+ {
747
+ description:
748
+ "Post as the workspace-shared OpenGeni bot. Pass channelId for a channel where the bot is already a member, or userId to open/post a DM; pass exactly one. Generate one operationId UUID per intended message and reuse that same UUID on every retry.",
749
+ inputSchema: {
750
+ connectionId: z4.string().uuid().optional(),
751
+ operationId: z4.string().uuid(),
752
+ channelId: z4.string().min(1).max(64).optional(),
753
+ userId: z4.string().min(1).max(64).optional(),
754
+ text: z4.string().min(1).max(40_000),
755
+ },
756
+ },
757
+ async ({ connectionId, operationId, channelId, userId, text }) => {
758
+ if (Boolean(channelId) === Boolean(userId)) {
759
+ throw new Error("exactly one of channelId or userId is required");
760
+ }
761
+ return json(
762
+ await (
763
+ await clientFor(connectionId)
764
+ ).postMessage({
765
+ operationId,
766
+ ...(channelId ? { channelId } : {}),
767
+ ...(userId ? { userId } : {}),
768
+ text,
769
+ }),
770
+ );
771
+ },
772
+ );
773
+ }
774
+
650
775
  function registerToolspaceProxyTools(server: McpServer, surface: ToolspaceMcpSurface | null): void {
651
776
  if (!surface) {
652
777
  return;
@@ -2031,15 +2156,18 @@ function registerVariableSetTools(
2031
2156
  }
2032
2157
  }
2033
2158
 
2034
- // The tool remains registered for compatibility, but every new installation
2035
- // binding entry point is fail-closed until a provider-supported authority
2036
- // proof stronger than installation visibility is available.
2037
- function registerGitHubConnectTool(server: McpServer, deps: ApiRouteDeps, json: JsonResult): void {
2159
+ function registerGitHubConnectTool(
2160
+ server: McpServer,
2161
+ deps: ApiRouteDeps,
2162
+ grant: AccessGrant,
2163
+ options: McpServerOptions,
2164
+ json: JsonResult,
2165
+ ): void {
2038
2166
  server.registerTool(
2039
2167
  "github_connect_link",
2040
2168
  {
2041
2169
  description:
2042
- "Report GitHub App connection availability. New installation binding is disabled until GitHub installation authority can be proven, so installUrl and linkUrl are null.",
2170
+ "Report truthful GitHub App workspace binding status and, for a human grant with github:manage, return the fresh GitHub owner-consent link. Server App configuration alone is never reported as a usable binding.",
2043
2171
  inputSchema: {},
2044
2172
  },
2045
2173
  async () => {
@@ -2049,17 +2177,35 @@ function registerGitHubConnectTool(server: McpServer, deps: ApiRouteDeps, json:
2049
2177
  if (missing.length > 0 || !slug) {
2050
2178
  return json({
2051
2179
  configured: false,
2180
+ status: "disabled",
2052
2181
  appSlug: slug,
2053
2182
  installUrl: null,
2054
2183
  linkUrl: null,
2055
2184
  missing,
2056
2185
  });
2057
2186
  }
2187
+ const installations = await listWorkspaceGitHubInstallationBindings(deps, grant.workspaceId);
2188
+ const status = githubBindingStatus(true, installations);
2189
+ const baseUrl = githubBrowserBaseUrl(settings, options.requestOrigin);
2190
+ const state =
2191
+ baseUrl && hasPermission(grant.permissions, "github:manage")
2192
+ ? createSignedState(deps.githubStateSecret, {
2193
+ accountId: grant.accountId,
2194
+ workspaceId: grant.workspaceId,
2195
+ intent: "installation_authority",
2196
+ ...githubBrowserGrantClaims(settings, grant),
2197
+ })
2198
+ : null;
2199
+ const connectUrl = state
2200
+ ? `${baseUrl}/v1/workspaces/${grant.workspaceId}/github/connect?state=${encodeURIComponent(state)}`
2201
+ : null;
2058
2202
  return json({
2059
2203
  configured: true,
2204
+ status,
2060
2205
  appSlug: slug,
2061
- installUrl: null,
2062
- linkUrl: null,
2206
+ installUrl: connectUrl,
2207
+ linkUrl: connectUrl,
2208
+ installations,
2063
2209
  missing: [],
2064
2210
  });
2065
2211
  },
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ConnectOpenGeniSlackBotRequest,
2
3
  ConnectionResponse,
3
4
  CreateConnectionRequest,
4
5
  IntegrationClientMetadata,
@@ -7,12 +8,19 @@ import {
7
8
  OAuthStartResponse,
8
9
  UpdateConnectionRequest,
9
10
  } from "@opengeni/contracts";
10
- import { requireAccessGrant, requireEnvironmentEncryption } from "@opengeni/core";
11
+ import {
12
+ hasReservedOpenGeniSlackBotMetadata,
13
+ isOpenGeniSlackBotConnection,
14
+ openGeniSlackBotMetadata,
15
+ requireAccessGrant,
16
+ requireEnvironmentEncryption,
17
+ } from "@opengeni/core";
11
18
  import {
12
19
  createConnection,
13
20
  encryptEnvironmentValue,
14
21
  getConnectionMetadata,
15
22
  listConnectionsMetadata,
23
+ recordAuditEvent,
16
24
  revokeConnection,
17
25
  updateConnection,
18
26
  } from "@opengeni/db";
@@ -25,6 +33,11 @@ import {
25
33
  startMcpOAuth,
26
34
  } from "../integrations/oauth-client";
27
35
  import { canonicalProviderDomain } from "../integrations/provider-domain";
36
+ import { verifyOpenGeniSlackBotCredential } from "../integrations/slack-bot";
37
+ import {
38
+ OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
39
+ OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
40
+ } from "@opengeni/contracts";
28
41
 
29
42
  export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
30
43
  const { db, settings, observability } = deps;
@@ -49,6 +62,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
49
62
  const workspaceId = c.req.param("workspaceId");
50
63
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
51
64
  const payload = CreateConnectionRequest.parse(await c.req.json());
65
+ assertNotReservedSlackBotMetadata(payload.metadata);
52
66
  const key = requireEnvironmentEncryption(settings);
53
67
  const subjectId = writableSubjectId(payload.subjectId, grant.subjectId);
54
68
  const connection = await createConnection(db, {
@@ -66,6 +80,102 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
66
80
  return c.json(ConnectionResponse.parse({ connection }), 201);
67
81
  });
68
82
 
83
+ app.post("/v1/workspaces/:workspaceId/connections/slack-bot", async (c) => {
84
+ const workspaceId = c.req.param("workspaceId");
85
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
86
+ const payload = ConnectOpenGeniSlackBotRequest.parse(await c.req.json());
87
+ const verified = await verifyOpenGeniSlackBotCredential(
88
+ payload.token,
89
+ deps.slackFetch ?? fetch,
90
+ );
91
+ const key = requireEnvironmentEncryption(settings);
92
+ const credentialEncrypted = encryptCredentialBundle(
93
+ key,
94
+ slackBotCredentialBundle(payload.token),
95
+ );
96
+ const existing = payload.connectionId
97
+ ? await getConnectionMetadata(db, workspaceId, payload.connectionId, grant.subjectId)
98
+ : null;
99
+ if (payload.connectionId && !existing) {
100
+ throw new HTTPException(404, { message: "connection not found" });
101
+ }
102
+ if (existing && !isOpenGeniSlackBotConnection(existing)) {
103
+ throw new HTTPException(422, {
104
+ message: "connectionId is not an OpenGeni Slack bot connection",
105
+ });
106
+ }
107
+ const existingMetadata = existing ? openGeniSlackBotMetadata(existing.metadata) : null;
108
+ if (existingMetadata && existingMetadata.slackTeamId !== verified.metadata.slackTeamId) {
109
+ throw new HTTPException(409, {
110
+ message: "a Slack bot connection can only be reinstalled for its original Slack workspace",
111
+ });
112
+ }
113
+ if (
114
+ existingMetadata &&
115
+ (existingMetadata.botId !== verified.metadata.botId ||
116
+ existingMetadata.botUserId !== verified.metadata.botUserId)
117
+ ) {
118
+ throw new HTTPException(409, {
119
+ message:
120
+ "a different Slack bot requires a new connection and explicit scheduled-task rebinding",
121
+ });
122
+ }
123
+ const verifiedInstallAt = new Date(verified.metadata.verifiedAt);
124
+ const connection = existing
125
+ ? await updateConnection(db, {
126
+ workspaceId,
127
+ connectionId: existing.id,
128
+ visibleToSubjectId: grant.subjectId,
129
+ expectedVersion: existing.version,
130
+ subjectId: null,
131
+ providerDomain: "slack.com",
132
+ kind: "app_install",
133
+ status: "active",
134
+ credentialEncrypted,
135
+ grantedScopes: verified.grantedScopes,
136
+ expiresAt: null,
137
+ verifiedInstallAt,
138
+ verifiedInstallVersion: existing.version + 1,
139
+ metadata: verified.metadata,
140
+ updatedBySubjectId: grant.subjectId,
141
+ })
142
+ : await createConnection(db, {
143
+ accountId: grant.accountId,
144
+ workspaceId,
145
+ subjectId: null,
146
+ providerDomain: "slack.com",
147
+ kind: "app_install",
148
+ credentialEncrypted,
149
+ grantedScopes: verified.grantedScopes,
150
+ expiresAt: null,
151
+ verifiedInstallAt,
152
+ verifiedInstallVersion: 1,
153
+ metadata: verified.metadata,
154
+ createdBySubjectId: grant.subjectId,
155
+ });
156
+ if (!connection) {
157
+ throw new HTTPException(409, {
158
+ message: "Slack bot connection changed during reinstall; retry with the current connection",
159
+ });
160
+ }
161
+ await recordAuditEvent(db, {
162
+ accountId: grant.accountId,
163
+ workspaceId,
164
+ subjectId: grant.subjectId,
165
+ action: existing ? "slack_bot.reinstalled" : "slack_bot.connected",
166
+ targetType: "connection",
167
+ targetId: connection.id,
168
+ metadata: {
169
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
170
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
171
+ connectionId: connection.id,
172
+ slackTeamId: verified.metadata.slackTeamId,
173
+ outcome: "succeeded",
174
+ },
175
+ });
176
+ return c.json(ConnectionResponse.parse({ connection }), existing ? 200 : 201);
177
+ });
178
+
69
179
  app.get("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
70
180
  const workspaceId = c.req.param("workspaceId");
71
181
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
@@ -85,6 +195,18 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
85
195
  const workspaceId = c.req.param("workspaceId");
86
196
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
87
197
  const payload = UpdateConnectionRequest.parse(await c.req.json());
198
+ assertNotReservedSlackBotMetadata(payload.metadata);
199
+ const existing = await getConnectionMetadata(
200
+ db,
201
+ workspaceId,
202
+ c.req.param("connectionId"),
203
+ grant.subjectId,
204
+ );
205
+ if (existing && isOpenGeniSlackBotConnection(existing)) {
206
+ throw new HTTPException(422, {
207
+ message: "use the dedicated OpenGeni Slack bot reinstall flow to update this connection",
208
+ });
209
+ }
88
210
  // Status is not a free-form field: revocation goes through DELETE, and the
89
211
  // broker owns needs_reauth/error. Reactivating a connection is only
90
212
  // meaningful together with a fresh credential bundle — otherwise a PATCH
@@ -144,6 +266,24 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
144
266
  if (!connection) {
145
267
  throw new HTTPException(404, { message: "connection not found" });
146
268
  }
269
+ if (isOpenGeniSlackBotConnection(connection)) {
270
+ const metadata = openGeniSlackBotMetadata(connection.metadata)!;
271
+ await recordAuditEvent(db, {
272
+ accountId: grant.accountId,
273
+ workspaceId,
274
+ subjectId: grant.subjectId,
275
+ action: "slack_bot.disconnected",
276
+ targetType: "connection",
277
+ targetId: connection.id,
278
+ metadata: {
279
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
280
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
281
+ connectionId: connection.id,
282
+ slackTeamId: metadata.slackTeamId,
283
+ outcome: "succeeded",
284
+ },
285
+ });
286
+ }
147
287
  return c.json(ConnectionResponse.parse({ connection }));
148
288
  });
149
289
 
@@ -200,6 +340,14 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
200
340
  });
201
341
  }
202
342
 
343
+ function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
344
+ if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
345
+ throw new HTTPException(422, {
346
+ message: "OpenGeni Slack bot metadata is reserved for the dedicated connection flow",
347
+ });
348
+ }
349
+ }
350
+
203
351
  function writableSubjectId(
204
352
  requested: string | null | undefined,
205
353
  grantSubjectId: string,
@@ -216,3 +364,9 @@ function writableSubjectId(
216
364
  function encryptCredentialBundle(key: Uint8Array, credential: Record<string, unknown>): string {
217
365
  return encryptEnvironmentValue(key, JSON.stringify(credential));
218
366
  }
367
+
368
+ function slackBotCredentialBundle(token: string): Record<string, unknown> {
369
+ const headerName = ["author", "ization"].join("");
370
+ const scheme = ["Bear", "er"].join("");
371
+ return { headers: { [headerName]: `${scheme} ${token}` } };
372
+ }