@opengeni/api-router 0.14.4 → 0.15.1
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/dist/app.d.ts +5 -1
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth.d.ts +2 -2
- package/dist/{chunk-36PW33ND.js → chunk-UVL2KH56.js} +1470 -213
- package/dist/chunk-UVL2KH56.js.map +1 -0
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/integrations/slack-bot.d.ts +21 -0
- package/dist/integrations/slack-interactions.d.ts +30 -0
- package/dist/routes/workspace-artifacts.d.ts +3 -0
- package/package.json +10 -10
- package/src/app.ts +59 -28
- package/src/http/auth.ts +4 -1
- package/src/index.ts +17 -4
- package/src/integrations/slack-bot.ts +80 -11
- package/src/integrations/slack-interactions.ts +856 -0
- package/src/mcp/server.ts +209 -0
- package/src/routes/sessions.ts +23 -10
- package/src/routes/workspace-artifacts.ts +274 -0
- package/dist/chunk-36PW33ND.js.map +0 -1
package/src/app.ts
CHANGED
|
@@ -68,11 +68,13 @@ import { registerSocialRoutes } from "./routes/social";
|
|
|
68
68
|
import { registerWorkspaceRoutes } from "./routes/workspaces";
|
|
69
69
|
import { registerWorkspaceInstructionPolicyRoutes } from "./routes/workspace-instruction-policies";
|
|
70
70
|
import { registerWorkspaceStateRoutes } from "./routes/workspace-state";
|
|
71
|
+
import { registerWorkspaceArtifactRoutes } from "./routes/workspace-artifacts";
|
|
71
72
|
import { registerPreferenceRegistryRoutes } from "./routes/preference-registry";
|
|
72
73
|
import { registerInsightsRoutes } from "./routes/insights";
|
|
73
74
|
import { registerTranscriptionRoutes } from "./routes/transcriptions";
|
|
74
75
|
import { projectClientModel } from "./model-catalog";
|
|
75
76
|
import { createTranscriptionService } from "./transcription/service";
|
|
77
|
+
import { registerSlackInteractionRoutes } from "./integrations/slack-interactions";
|
|
76
78
|
|
|
77
79
|
export type {
|
|
78
80
|
ApiRouteDeps,
|
|
@@ -104,6 +106,13 @@ export function apiRequestBodyLimitBytes(settings: { voiceInputMaxSizeBytes: num
|
|
|
104
106
|
const API_PUBLIC_ERROR_MESSAGE_MAX_BYTES = 512;
|
|
105
107
|
|
|
106
108
|
export function createApp(deps: AppDependencies): Hono {
|
|
109
|
+
return createAppComposition(deps).app;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function createAppComposition(deps: AppDependencies): {
|
|
113
|
+
app: Hono;
|
|
114
|
+
routeDeps: ApiRouteDeps;
|
|
115
|
+
} {
|
|
107
116
|
const managedAuth = deps.managedAuth ?? createManagedAuth(deps.settings, deps.db);
|
|
108
117
|
const objectStorage =
|
|
109
118
|
deps.objectStorage === undefined ? createObjectStorage(deps.settings) : deps.objectStorage;
|
|
@@ -186,28 +195,34 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
186
195
|
await next();
|
|
187
196
|
});
|
|
188
197
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
198
|
+
const corsHeaders = {
|
|
199
|
+
allowHeaders: [
|
|
200
|
+
"Accept",
|
|
201
|
+
"Authorization",
|
|
202
|
+
"Content-Type",
|
|
203
|
+
"X-OpenGeni-Access-Key",
|
|
204
|
+
"X-OpenGeni-Api-Contract",
|
|
205
|
+
"X-OpenGeni-Correlation-Id",
|
|
206
|
+
"X-OpenGeni-Subject",
|
|
207
|
+
],
|
|
208
|
+
exposeHeaders: ["X-OpenGeni-Api-Contract", "X-OpenGeni-Correlation-Id"],
|
|
209
|
+
};
|
|
210
|
+
const publicApiCors = cors({ ...corsHeaders, credentials: false, origin: "*" });
|
|
211
|
+
const credentialedCors = cors({
|
|
212
|
+
...corsHeaders,
|
|
213
|
+
credentials: true,
|
|
214
|
+
origin: (origin) =>
|
|
215
|
+
allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin) ? origin : null,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
app.use("*", (c, next) => {
|
|
219
|
+
const origin = c.req.header("origin");
|
|
220
|
+
const middleware =
|
|
221
|
+
origin && allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin)
|
|
222
|
+
? credentialedCors
|
|
223
|
+
: publicApiCors;
|
|
224
|
+
return middleware(c, next);
|
|
225
|
+
});
|
|
211
226
|
|
|
212
227
|
app.use(
|
|
213
228
|
"*",
|
|
@@ -392,7 +407,9 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
392
407
|
boundedRequest = await boundedMcpRequest(c.req.raw);
|
|
393
408
|
} catch (error) {
|
|
394
409
|
if (error instanceof McpPayloadTooLargeError) {
|
|
395
|
-
throw new HTTPException(413, {
|
|
410
|
+
throw new HTTPException(413, {
|
|
411
|
+
message: "MCP request body exceeds the safety limit",
|
|
412
|
+
});
|
|
396
413
|
}
|
|
397
414
|
throw error;
|
|
398
415
|
}
|
|
@@ -414,7 +431,9 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
414
431
|
throw new HTTPException(404, { message: "session not found" });
|
|
415
432
|
}
|
|
416
433
|
if (error instanceof SessionAuthorizationUnavailableError) {
|
|
417
|
-
throw new HTTPException(503, {
|
|
434
|
+
throw new HTTPException(503, {
|
|
435
|
+
message: "session authorization is unavailable",
|
|
436
|
+
});
|
|
418
437
|
}
|
|
419
438
|
throw error;
|
|
420
439
|
}
|
|
@@ -422,10 +441,15 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
422
441
|
let toolspace: Awaited<ReturnType<typeof prepareToolspaceMcpSurface>> = null;
|
|
423
442
|
if (toolspaceGrant) {
|
|
424
443
|
try {
|
|
425
|
-
toolspace = await prepareToolspaceMcpSurface({
|
|
444
|
+
toolspace = await prepareToolspaceMcpSurface({
|
|
445
|
+
deps: routeDeps,
|
|
446
|
+
grant,
|
|
447
|
+
});
|
|
426
448
|
} catch (error) {
|
|
427
449
|
if (error instanceof McpPayloadTooLargeError) {
|
|
428
|
-
throw new HTTPException(413, {
|
|
450
|
+
throw new HTTPException(413, {
|
|
451
|
+
message: "MCP tool list exceeds the safety limit",
|
|
452
|
+
});
|
|
429
453
|
}
|
|
430
454
|
throw error;
|
|
431
455
|
}
|
|
@@ -458,6 +482,7 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
458
482
|
registerInsightsRoutes(app, routeDeps);
|
|
459
483
|
registerWorkspaceInstructionPolicyRoutes(app, routeDeps);
|
|
460
484
|
registerWorkspaceStateRoutes(app, routeDeps);
|
|
485
|
+
registerWorkspaceArtifactRoutes(app, routeDeps);
|
|
461
486
|
registerPreferenceRegistryRoutes(app, routeDeps);
|
|
462
487
|
registerSocialRoutes(app, routeDeps);
|
|
463
488
|
registerConnectionRoutes(app, routeDeps);
|
|
@@ -472,6 +497,7 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
472
497
|
registerScheduledTaskRoutes(app, routeDeps);
|
|
473
498
|
registerCodexRoutes(app, routeDeps);
|
|
474
499
|
registerTranscriptionRoutes(app, routeDeps);
|
|
500
|
+
registerSlackInteractionRoutes(app, routeDeps);
|
|
475
501
|
|
|
476
502
|
app.notFound((c) => {
|
|
477
503
|
if (!new URL(c.req.url).pathname.startsWith("/v1/")) return c.text("Not Found", 404);
|
|
@@ -513,7 +539,7 @@ export function createApp(deps: AppDependencies): Hono {
|
|
|
513
539
|
return c.json(envelope, status as ContentfulStatusCode);
|
|
514
540
|
});
|
|
515
541
|
|
|
516
|
-
return app;
|
|
542
|
+
return { app, routeDeps };
|
|
517
543
|
}
|
|
518
544
|
|
|
519
545
|
async function requireMcpAccessGrant(
|
|
@@ -694,7 +720,9 @@ async function runReadinessChecks<const Checks extends Readonly<Record<string, R
|
|
|
694
720
|
}
|
|
695
721
|
}),
|
|
696
722
|
);
|
|
697
|
-
const result = Object.fromEntries(entries) as {
|
|
723
|
+
const result = Object.fromEntries(entries) as {
|
|
724
|
+
[Name in keyof Checks]: ReadinessCheckResult;
|
|
725
|
+
};
|
|
698
726
|
return {
|
|
699
727
|
ok: Object.values(result).every((check) => check.ok),
|
|
700
728
|
checks: result,
|
|
@@ -1122,6 +1150,9 @@ export function isApiContractProtectedMutation(method: string, pathname: string)
|
|
|
1122
1150
|
pathname.startsWith("/v1/auth/") ||
|
|
1123
1151
|
pathname.startsWith("/v1/webhooks/") ||
|
|
1124
1152
|
pathname.startsWith("/v1/integrations/oauth/") ||
|
|
1153
|
+
pathname === "/v1/integrations/slack/events" ||
|
|
1154
|
+
pathname === "/v1/integrations/slack/commands" ||
|
|
1155
|
+
pathname === "/v1/integrations/slack/interactions" ||
|
|
1125
1156
|
pathname.startsWith("/v1/github/") ||
|
|
1126
1157
|
pathname === "/v1/enrollments/device/start" ||
|
|
1127
1158
|
pathname === "/v1/enrollments/device/poll" ||
|
package/src/http/auth.ts
CHANGED
|
@@ -50,7 +50,10 @@ function isAuthExempt(c: Context, settings: Settings): boolean {
|
|
|
50
50
|
if (
|
|
51
51
|
path === "/v1/integrations/oauth/callback" ||
|
|
52
52
|
path === "/v1/integrations/oauth/client-metadata.json" ||
|
|
53
|
-
path === "/v1/integrations/slack/callback"
|
|
53
|
+
path === "/v1/integrations/slack/callback" ||
|
|
54
|
+
path === "/v1/integrations/slack/events" ||
|
|
55
|
+
path === "/v1/integrations/slack/commands" ||
|
|
56
|
+
path === "/v1/integrations/slack/interactions"
|
|
54
57
|
) {
|
|
55
58
|
return true;
|
|
56
59
|
}
|
package/src/index.ts
CHANGED
|
@@ -30,10 +30,11 @@ import {
|
|
|
30
30
|
WorkflowExecutionAlreadyStartedError,
|
|
31
31
|
} from "@temporalio/client";
|
|
32
32
|
import type { ScheduleOptions, ScheduleSpec, ScheduleUpdateOptions } from "@temporalio/client";
|
|
33
|
-
import {
|
|
33
|
+
import { createAppComposition, type DocumentIndexClient, type SessionWorkflowClient } from "./app";
|
|
34
34
|
import { observabilityEventLogger } from "./observability";
|
|
35
35
|
import { startAuthCalloutResponder } from "./sandbox/auth-callout";
|
|
36
36
|
import { startHelloIngestion, startMetricsIngestion } from "./sandbox/metrics-ingestion";
|
|
37
|
+
import { startSlackInteractionPump } from "./integrations/slack-interactions";
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* A REJECT_DUPLICATE start collides on the deterministic workflowId when the
|
|
@@ -310,7 +311,7 @@ export async function startApi() {
|
|
|
310
311
|
await dbClient.close();
|
|
311
312
|
throw new Error("OpenGeni API startup dependencies were not initialized");
|
|
312
313
|
}
|
|
313
|
-
const app =
|
|
314
|
+
const { app, routeDeps } = createAppComposition({
|
|
314
315
|
settings,
|
|
315
316
|
db: dbClient.db,
|
|
316
317
|
bus,
|
|
@@ -327,6 +328,9 @@ export async function startApi() {
|
|
|
327
328
|
idleTimeout: 255,
|
|
328
329
|
fetch: app.fetch,
|
|
329
330
|
});
|
|
331
|
+
const stopSlackInteractionPump = settings.slackSigningSecret
|
|
332
|
+
? startSlackInteractionPump(routeDeps)
|
|
333
|
+
: undefined;
|
|
330
334
|
// M10 — start the metrics-ingestion consumer (agent heartbeats → DB last-sample
|
|
331
335
|
// + downsampled series), gated on the selfhosted flag. A no-op when disabled.
|
|
332
336
|
let stopMetricsIngestion: (() => void) | undefined;
|
|
@@ -342,8 +346,16 @@ export async function startApi() {
|
|
|
342
346
|
// user), separate from the privileged control-plane bus.
|
|
343
347
|
let authCalloutResponder: ResponderConnection | undefined;
|
|
344
348
|
if (settings.sandboxSelfhostedEnabled) {
|
|
345
|
-
stopMetricsIngestion = startMetricsIngestion({
|
|
346
|
-
|
|
349
|
+
stopMetricsIngestion = startMetricsIngestion({
|
|
350
|
+
db: dbClient.db,
|
|
351
|
+
bus,
|
|
352
|
+
observability,
|
|
353
|
+
});
|
|
354
|
+
stopHelloIngestion = startHelloIngestion({
|
|
355
|
+
db: dbClient.db,
|
|
356
|
+
bus,
|
|
357
|
+
observability,
|
|
358
|
+
});
|
|
347
359
|
observability.info("OpenGeni machine-metrics + hello ingestion consumers started", {});
|
|
348
360
|
|
|
349
361
|
const callout = resolveNatsCalloutConfig(settings);
|
|
@@ -375,6 +387,7 @@ export async function startApi() {
|
|
|
375
387
|
server,
|
|
376
388
|
close: async () => {
|
|
377
389
|
server.stop(true);
|
|
390
|
+
stopSlackInteractionPump?.();
|
|
378
391
|
stopMetricsIngestion?.();
|
|
379
392
|
stopHelloIngestion?.();
|
|
380
393
|
await Promise.allSettled([
|
|
@@ -96,10 +96,14 @@ export async function exchangeOpenGeniSlackAuthorizationCode(
|
|
|
96
96
|
signal: AbortSignal.timeout(SLACK_TIMEOUT_MS),
|
|
97
97
|
});
|
|
98
98
|
} catch {
|
|
99
|
-
throw new HTTPException(502, {
|
|
99
|
+
throw new HTTPException(502, {
|
|
100
|
+
message: "Slack installation token exchange failed",
|
|
101
|
+
});
|
|
100
102
|
}
|
|
101
103
|
if (!response.ok) {
|
|
102
|
-
throw new HTTPException(502, {
|
|
104
|
+
throw new HTTPException(502, {
|
|
105
|
+
message: "Slack installation token exchange failed",
|
|
106
|
+
});
|
|
103
107
|
}
|
|
104
108
|
const payload = await readResponseJsonBounded<unknown>(
|
|
105
109
|
response,
|
|
@@ -112,7 +116,9 @@ export async function exchangeOpenGeniSlackAuthorizationCode(
|
|
|
112
116
|
}
|
|
113
117
|
const accessToken = slackString(record.access_token);
|
|
114
118
|
if (!accessToken?.startsWith("xoxb-")) {
|
|
115
|
-
throw new HTTPException(502, {
|
|
119
|
+
throw new HTTPException(502, {
|
|
120
|
+
message: "Slack installation did not return a bot token",
|
|
121
|
+
});
|
|
116
122
|
}
|
|
117
123
|
return accessToken;
|
|
118
124
|
}
|
|
@@ -342,6 +348,11 @@ export class OpenGeniSlackBotClient {
|
|
|
342
348
|
});
|
|
343
349
|
}
|
|
344
350
|
|
|
351
|
+
async verifyChannelAccess(channelId: string) {
|
|
352
|
+
const headers = await this.headersFor("channel_history.read");
|
|
353
|
+
return await this.requireMemberChannel(headers, channelId);
|
|
354
|
+
}
|
|
355
|
+
|
|
345
356
|
async channelHistory(input: { channelId: string; limit?: number; cursor?: string }) {
|
|
346
357
|
return await this.withAudit("channel_history.read", async (headers) => {
|
|
347
358
|
const info = await this.requireMemberChannel(headers, input.channelId);
|
|
@@ -484,7 +495,9 @@ export class OpenGeniSlackBotClient {
|
|
|
484
495
|
const headers = await this.headersFor(operation);
|
|
485
496
|
let channelId = input.channelId;
|
|
486
497
|
if (input.userId) {
|
|
487
|
-
const opened = await this.call(headers, "conversations.open", {
|
|
498
|
+
const opened = await this.call(headers, "conversations.open", {
|
|
499
|
+
users: input.userId,
|
|
500
|
+
});
|
|
488
501
|
channelId = requiredSlackString(slackRecord(opened.channel)?.id, "channel.id");
|
|
489
502
|
} else if (channelId) {
|
|
490
503
|
await this.requireMemberChannel(headers, channelId);
|
|
@@ -704,7 +717,10 @@ export class OpenGeniSlackBotClient {
|
|
|
704
717
|
`${SLACK_API_BASE}chat.getPermalink`,
|
|
705
718
|
);
|
|
706
719
|
try {
|
|
707
|
-
await this.call(headers, "chat.getPermalink", {
|
|
720
|
+
await this.call(headers, "chat.getPermalink", {
|
|
721
|
+
channel: channelId,
|
|
722
|
+
message_ts: timestamp,
|
|
723
|
+
});
|
|
708
724
|
return true;
|
|
709
725
|
} catch (error) {
|
|
710
726
|
if (error instanceof SlackBotProviderError && error.code === "message_not_found") {
|
|
@@ -715,7 +731,9 @@ export class OpenGeniSlackBotClient {
|
|
|
715
731
|
}
|
|
716
732
|
|
|
717
733
|
private async requireMemberChannel(headers: Record<string, string>, channelId: string) {
|
|
718
|
-
const payload = await this.call(headers, "conversations.info", {
|
|
734
|
+
const payload = await this.call(headers, "conversations.info", {
|
|
735
|
+
channel: channelId,
|
|
736
|
+
});
|
|
719
737
|
const projected = projectChannel(payload.channel);
|
|
720
738
|
if (!projected || projected.isMember !== true) {
|
|
721
739
|
throw new SlackBotProviderError("not_in_channel");
|
|
@@ -946,7 +964,10 @@ export class OpenGeniSlackBotClient {
|
|
|
946
964
|
}
|
|
947
965
|
|
|
948
966
|
private completedDeleteResult(
|
|
949
|
-
operation: {
|
|
967
|
+
operation: {
|
|
968
|
+
slackChannelId: string | null;
|
|
969
|
+
slackMessageTimestamp: string | null;
|
|
970
|
+
},
|
|
950
971
|
operationId: string,
|
|
951
972
|
) {
|
|
952
973
|
if (!operation.slackChannelId || !operation.slackMessageTimestamp) {
|
|
@@ -1008,9 +1029,15 @@ export class OpenGeniSlackBotClient {
|
|
|
1008
1029
|
return { type: "subject", id: this.context.subjectId };
|
|
1009
1030
|
}
|
|
1010
1031
|
if (this.context.scheduledTaskId) {
|
|
1011
|
-
return {
|
|
1032
|
+
return {
|
|
1033
|
+
type: "service",
|
|
1034
|
+
id: `scheduler:${this.context.scheduledTaskId}`,
|
|
1035
|
+
};
|
|
1012
1036
|
}
|
|
1013
|
-
return {
|
|
1037
|
+
return {
|
|
1038
|
+
type: "service",
|
|
1039
|
+
id: `session:${this.context.sessionId ?? "workspace"}`,
|
|
1040
|
+
};
|
|
1014
1041
|
}
|
|
1015
1042
|
|
|
1016
1043
|
private fileListPage(input: {
|
|
@@ -1020,13 +1047,19 @@ export class OpenGeniSlackBotClient {
|
|
|
1020
1047
|
}): SlackFilesListPage {
|
|
1021
1048
|
const key = environmentsEncryptionKeyBytes(this.settings);
|
|
1022
1049
|
if (!key) throw new Error("connection encryption is not configured");
|
|
1023
|
-
return resolveSlackFilesListPage(input, {
|
|
1050
|
+
return resolveSlackFilesListPage(input, {
|
|
1051
|
+
connectionId: this.connection.id,
|
|
1052
|
+
key,
|
|
1053
|
+
});
|
|
1024
1054
|
}
|
|
1025
1055
|
|
|
1026
1056
|
private fileListCursor(input: { channelId: string; count: number; page: number }): string {
|
|
1027
1057
|
const key = environmentsEncryptionKeyBytes(this.settings);
|
|
1028
1058
|
if (!key) throw new Error("connection encryption is not configured");
|
|
1029
|
-
return createSlackFilesListCursor(input, {
|
|
1059
|
+
return createSlackFilesListCursor(input, {
|
|
1060
|
+
connectionId: this.connection.id,
|
|
1061
|
+
key,
|
|
1062
|
+
});
|
|
1030
1063
|
}
|
|
1031
1064
|
|
|
1032
1065
|
private async recordAudit(
|
|
@@ -1076,6 +1109,42 @@ export function createOpenGeniSlackBotClient(
|
|
|
1076
1109
|
);
|
|
1077
1110
|
}
|
|
1078
1111
|
|
|
1112
|
+
export async function createOpenGeniSlackBotInteractionClient(
|
|
1113
|
+
deps: { db: Database; settings: Settings; slackFetch?: typeof fetch },
|
|
1114
|
+
input: {
|
|
1115
|
+
accountId: string;
|
|
1116
|
+
workspaceId: string;
|
|
1117
|
+
connectionId: string;
|
|
1118
|
+
subjectId: string;
|
|
1119
|
+
sessionId?: string | null;
|
|
1120
|
+
},
|
|
1121
|
+
): Promise<OpenGeniSlackBotClient> {
|
|
1122
|
+
const connection = await requireOpenGeniSlackBotConnection(
|
|
1123
|
+
deps.db,
|
|
1124
|
+
input.workspaceId,
|
|
1125
|
+
input.connectionId,
|
|
1126
|
+
);
|
|
1127
|
+
if (connection.accountId !== input.accountId) {
|
|
1128
|
+
throw new Error("OpenGeni Slack bot connection tenant mismatch");
|
|
1129
|
+
}
|
|
1130
|
+
const metadata = openGeniSlackBotMetadata(connection.metadata);
|
|
1131
|
+
if (!metadata) throw new Error("OpenGeni Slack bot connection metadata is invalid");
|
|
1132
|
+
return new OpenGeniSlackBotClient(
|
|
1133
|
+
deps.db,
|
|
1134
|
+
deps.settings,
|
|
1135
|
+
connection,
|
|
1136
|
+
metadata,
|
|
1137
|
+
{
|
|
1138
|
+
accountId: input.accountId,
|
|
1139
|
+
workspaceId: input.workspaceId,
|
|
1140
|
+
subjectId: input.subjectId,
|
|
1141
|
+
sessionId: input.sessionId ?? null,
|
|
1142
|
+
scheduledTaskId: null,
|
|
1143
|
+
},
|
|
1144
|
+
deps.slackFetch,
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1079
1148
|
async function slackApiFetch(
|
|
1080
1149
|
fetchImpl: FetchLike,
|
|
1081
1150
|
method: string,
|