@opengeni/api-router 0.30.3 → 2.2.0-canary.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/dist/api-websocket.d.ts +18 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth.d.ts +6 -0
- package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/codemode.d.ts +8 -1
- package/dist/company-brain-okf.d.ts +24 -0
- package/dist/connected-machine-computer-access.d.ts +11 -0
- package/dist/connection-authority-owner.d.ts +59 -0
- package/dist/connection-ownership.d.ts +47 -0
- package/dist/controller-data-plane.d.ts +16 -0
- package/dist/editable-artifact-websocket.d.ts +5 -15
- package/dist/editable-artifact-workspace-files.d.ts +1 -1
- package/dist/http/api-error.d.ts +10 -0
- package/dist/http/cors.d.ts +5 -0
- package/dist/http/interaction-control-error.d.ts +13 -0
- package/dist/http/sse.d.ts +2 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.js +326 -82
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +17 -0
- package/dist/integrations/fiken.d.ts +7 -2
- package/dist/integrations/google-drive.d.ts +18 -0
- package/dist/integrations/oauth-client.d.ts +22 -3
- package/dist/integrations/oauth-profiles.d.ts +195 -0
- package/dist/integrations/personal-github-repositories.d.ts +45 -0
- package/dist/integrations/personal-github.d.ts +40 -0
- package/dist/integrations/pr-review-provider.d.ts +22 -0
- package/dist/integrations/provider-oauth.d.ts +8 -0
- package/dist/integrations/slack-app-home.d.ts +24 -0
- package/dist/integrations/slack-bot.d.ts +157 -9
- package/dist/integrations/slack-interactions.d.ts +39 -5
- package/dist/integrations/slack-routing.d.ts +120 -0
- package/dist/integrations/social-oauth.d.ts +5 -0
- package/dist/interaction-frame-proxy.d.ts +68 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
- package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
- package/dist/mcp/documents.d.ts +1 -0
- package/dist/mcp/remember.d.ts +28 -0
- package/dist/mcp/request-abort.d.ts +19 -0
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +15 -2
- package/dist/mcp/session-view.d.ts +4 -0
- package/dist/mcp/session-wait.d.ts +137 -0
- package/dist/routes/automations.d.ts +4 -0
- package/dist/routes/billing.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +4 -1
- package/dist/routes/company-brain.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +2 -1
- package/dist/routes/connection-authorities.d.ts +3 -0
- package/dist/routes/personal-github-git-broker.d.ts +29 -0
- package/dist/routes/personal-github.d.ts +3 -0
- package/dist/routes/pr-review.d.ts +3 -0
- package/dist/routes/sessions.d.ts +1 -0
- package/dist/routes/user-resource-authorities.d.ts +3 -0
- package/dist/routes/workspace-capture.d.ts +10 -2
- package/dist/routes/workspace-learning.d.ts +3 -0
- package/dist/routes/workspaces.d.ts +19 -0
- package/dist/sandbox/channel-a.d.ts +11 -1
- package/dist/sandbox/connection-authority.d.ts +3 -0
- package/dist/sandbox/enrollment.d.ts +2 -0
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +10 -20
- package/dist/sandbox/viewer.d.ts +24 -1
- package/dist/sandbox-file-artifacts.d.ts +11 -0
- package/dist/scheduled-task-deletion.d.ts +14 -0
- package/dist/slack-reaction-files.d.ts +1 -1
- package/dist/temporal-schedule-cleanup.d.ts +1 -0
- package/package.json +20 -18
- package/src/api-websocket.ts +23 -0
- package/src/app.ts +161 -23
- package/src/auth/managed-auth.ts +25 -3
- package/src/codemode.ts +53 -23
- package/src/codex-realtime.ts +8 -2
- package/src/company-brain-okf.ts +340 -0
- package/src/connected-machine-computer-access.ts +33 -0
- package/src/connection-authority-owner.ts +61 -0
- package/src/connection-ownership.ts +180 -0
- package/src/controller-data-plane.ts +47 -0
- package/src/editable-artifact-native-kernel.ts +26 -15
- package/src/editable-artifact-office-import.ts +28 -3
- package/src/editable-artifact-production.ts +13 -7
- package/src/editable-artifact-websocket.ts +11 -18
- package/src/editable-artifact-workspace-files.ts +31 -6
- package/src/http/api-error.ts +28 -0
- package/src/http/auth.ts +4 -0
- package/src/http/cors.ts +35 -0
- package/src/http/interaction-control-error.ts +164 -0
- package/src/http/sse.ts +112 -66
- package/src/index.ts +46 -6
- package/src/integrations/atlassian.ts +146 -35
- package/src/integrations/fiken.ts +102 -22
- package/src/integrations/google-drive.ts +301 -92
- package/src/integrations/oauth-client.ts +272 -143
- package/src/integrations/oauth-profiles.ts +477 -0
- package/src/integrations/personal-github-repositories.ts +445 -0
- package/src/integrations/personal-github.ts +705 -0
- package/src/integrations/pr-review-provider.ts +246 -0
- package/src/integrations/provider-oauth.ts +121 -5
- package/src/integrations/slack-app-home.ts +300 -0
- package/src/integrations/slack-bot.ts +342 -57
- package/src/integrations/slack-interactions.ts +1898 -259
- package/src/integrations/slack-routing.ts +337 -0
- package/src/integrations/social-oauth.ts +25 -0
- package/src/interaction-frame-proxy.ts +409 -0
- package/src/mcp/company-brain-governed-writes.ts +262 -0
- package/src/mcp/company-profile-agent-admin.ts +205 -0
- package/src/mcp/documents.ts +37 -6
- package/src/mcp/files.ts +16 -1
- package/src/mcp/remember.ts +181 -0
- package/src/mcp/request-abort.ts +44 -0
- package/src/mcp/scheduled-task-view.ts +1 -0
- package/src/mcp/server.ts +719 -369
- package/src/mcp/session-view.ts +54 -0
- package/src/mcp/session-wait.ts +554 -0
- package/src/model-catalog.ts +20 -12
- package/src/routes/api-integrations.ts +4 -0
- package/src/routes/api-keys.ts +1 -0
- package/src/routes/automations.ts +534 -0
- package/src/routes/billing.ts +57 -1
- package/src/routes/browser-sessions.ts +329 -79
- package/src/routes/channels.ts +17 -1
- package/src/routes/codex.ts +50 -0
- package/src/routes/company-brain.ts +367 -0
- package/src/routes/company-profile.ts +1 -1
- package/src/routes/computer-sessions.ts +320 -93
- package/src/routes/connection-authorities.ts +139 -0
- package/src/routes/connections.ts +144 -34
- package/src/routes/documents.ts +353 -4
- package/src/routes/editable-artifacts.ts +11 -3
- package/src/routes/enrollments.ts +116 -26
- package/src/routes/environments.ts +127 -55
- package/src/routes/files.ts +72 -11
- package/src/routes/install.ts +82 -28
- package/src/routes/integration-facets.ts +29 -0
- package/src/routes/interaction-resources.ts +20 -2
- package/src/routes/machines.ts +275 -13
- package/src/routes/organization-memberships.ts +717 -28
- package/src/routes/packs.ts +4 -3
- package/src/routes/personal-github-git-broker.ts +785 -0
- package/src/routes/personal-github.ts +319 -0
- package/src/routes/pr-review.ts +531 -0
- package/src/routes/rigs.ts +104 -39
- package/src/routes/scheduled-tasks.ts +19 -21
- package/src/routes/sessions.ts +775 -38
- package/src/routes/social.ts +14 -2
- package/src/routes/supergrok.ts +23 -4
- package/src/routes/transcription-recordings.ts +8 -2
- package/src/routes/user-resource-authorities.ts +138 -0
- package/src/routes/workspace-artifacts.ts +4 -1
- package/src/routes/workspace-capture.ts +21 -0
- package/src/routes/workspace-learning.ts +228 -0
- package/src/routes/workspaces.ts +59 -6
- package/src/sandbox/auth-callout.ts +38 -12
- package/src/sandbox/channel-a.ts +242 -19
- package/src/sandbox/connection-authority.ts +3 -0
- package/src/sandbox/enrollment.ts +15 -3
- package/src/sandbox/machines.ts +186 -64
- package/src/sandbox/metrics-ingestion.ts +220 -58
- package/src/sandbox/viewer.ts +243 -19
- package/src/sandbox-file-artifacts.ts +329 -0
- package/src/scheduled-task-deletion.ts +127 -0
- package/src/slack-reaction-files.ts +16 -5
- package/src/temporal-schedule-cleanup.ts +13 -0
- package/dist/chunk-WCXHI3FF.js.map +0 -1
|
@@ -5,8 +5,10 @@ import {
|
|
|
5
5
|
OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
|
|
6
6
|
OPENGENI_SLACK_BOT_REQUIRED_SCOPES,
|
|
7
7
|
evaluateOpenGeniSlackBotScopes,
|
|
8
|
+
hasOpenGeniSlackBotSearchScopes,
|
|
8
9
|
type AccessGrant,
|
|
9
10
|
type ConnectionMetadata,
|
|
11
|
+
type OpenGeniSlackBotDisplayName,
|
|
10
12
|
type OpenGeniSlackBotConnectionMetadata,
|
|
11
13
|
} from "@opengeni/contracts";
|
|
12
14
|
import {
|
|
@@ -64,6 +66,12 @@ const MAX_REACTION_CONTEXT_CHECKPOINT_FILE_LABEL_CHARS = 1_500;
|
|
|
64
66
|
const MAX_REACTION_CONTEXT_CHECKPOINT_FILES = 16;
|
|
65
67
|
const SLACK_REACTION_CONTEXT_CHECKPOINT_VERSION = 1;
|
|
66
68
|
const MAX_USER_PAGE = 200;
|
|
69
|
+
/** Slack caps `assistant.search.context` at 20 results per request. */
|
|
70
|
+
const MAX_SEARCH_PAGE = 20;
|
|
71
|
+
/** Bounded projection cap for one search result's content excerpt. */
|
|
72
|
+
const MAX_SEARCH_CONTENT_CHARS = 2_000;
|
|
73
|
+
/** Matches the MCP input schema's cap; Slack signals its own via query_too_long. */
|
|
74
|
+
const MAX_SEARCH_QUERY_CHARS = 500;
|
|
67
75
|
const MAX_FILE_PAGE = 200;
|
|
68
76
|
const MAX_FILE_CURSOR_LENGTH = 1_024;
|
|
69
77
|
const SLACK_FILE_CURSOR_VERSION = "files-v1";
|
|
@@ -106,6 +114,11 @@ export type DownloadedSlackReactionImage = Readonly<{
|
|
|
106
114
|
bytes: Uint8Array;
|
|
107
115
|
}>;
|
|
108
116
|
|
|
117
|
+
export type ExchangedOpenGeniSlackAuthorization = Readonly<{
|
|
118
|
+
accessToken: string;
|
|
119
|
+
appId: string;
|
|
120
|
+
}>;
|
|
121
|
+
|
|
109
122
|
export async function exchangeOpenGeniSlackAuthorizationCode(
|
|
110
123
|
input: {
|
|
111
124
|
code: string;
|
|
@@ -114,7 +127,7 @@ export async function exchangeOpenGeniSlackAuthorizationCode(
|
|
|
114
127
|
redirectUri: string;
|
|
115
128
|
},
|
|
116
129
|
fetchImpl: FetchLike = fetch,
|
|
117
|
-
): Promise<
|
|
130
|
+
): Promise<ExchangedOpenGeniSlackAuthorization> {
|
|
118
131
|
const body = new URLSearchParams({
|
|
119
132
|
code: input.code,
|
|
120
133
|
client_id: input.clientId,
|
|
@@ -158,7 +171,13 @@ export async function exchangeOpenGeniSlackAuthorizationCode(
|
|
|
158
171
|
message: "Slack installation did not return a bot token",
|
|
159
172
|
});
|
|
160
173
|
}
|
|
161
|
-
|
|
174
|
+
const appId = slackString(record.app_id);
|
|
175
|
+
if (!appId) {
|
|
176
|
+
throw new HTTPException(502, {
|
|
177
|
+
message: "Slack installation did not return an app identity",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return { accessToken, appId };
|
|
162
181
|
}
|
|
163
182
|
|
|
164
183
|
export type SlackBotReceipt = {
|
|
@@ -188,16 +207,58 @@ export type SlackMessageBlock =
|
|
|
188
207
|
style?: "primary" | "danger";
|
|
189
208
|
}>;
|
|
190
209
|
}
|
|
210
|
+
| {
|
|
211
|
+
type: "context";
|
|
212
|
+
block_id?: string;
|
|
213
|
+
elements: Array<{ type: "mrkdwn" | "plain_text"; text: string; emoji?: boolean }>;
|
|
214
|
+
}
|
|
215
|
+
| { type: "divider" };
|
|
216
|
+
|
|
217
|
+
export type SlackHomeBlock =
|
|
218
|
+
| {
|
|
219
|
+
type: "header";
|
|
220
|
+
block_id?: string;
|
|
221
|
+
text: { type: "plain_text"; text: string; emoji?: boolean };
|
|
222
|
+
}
|
|
223
|
+
| {
|
|
224
|
+
type: "section";
|
|
225
|
+
block_id?: string;
|
|
226
|
+
text: { type: "mrkdwn" | "plain_text"; text: string; emoji?: boolean };
|
|
227
|
+
accessory?: {
|
|
228
|
+
type: "button";
|
|
229
|
+
action_id: string;
|
|
230
|
+
text: { type: "plain_text"; text: string; emoji?: boolean };
|
|
231
|
+
url: string;
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
| {
|
|
235
|
+
type: "context";
|
|
236
|
+
block_id?: string;
|
|
237
|
+
elements: Array<{ type: "mrkdwn" | "plain_text"; text: string; emoji?: boolean }>;
|
|
238
|
+
}
|
|
239
|
+
| {
|
|
240
|
+
type: "actions";
|
|
241
|
+
block_id: string;
|
|
242
|
+
elements: Array<{
|
|
243
|
+
type: "button";
|
|
244
|
+
action_id: string;
|
|
245
|
+
text: { type: "plain_text"; text: string; emoji?: boolean };
|
|
246
|
+
url: string;
|
|
247
|
+
style?: "primary" | "danger";
|
|
248
|
+
}>;
|
|
249
|
+
}
|
|
191
250
|
| { type: "divider" };
|
|
192
251
|
|
|
193
252
|
type SlackBotOperation =
|
|
194
253
|
| "channels.list"
|
|
254
|
+
| "search.context"
|
|
195
255
|
| "channel_history.read"
|
|
196
256
|
| "thread_replies.read"
|
|
197
257
|
| "users.list"
|
|
198
258
|
| "files.list"
|
|
199
259
|
| "file.info"
|
|
200
260
|
| "file.content.read"
|
|
261
|
+
| "home.publish"
|
|
201
262
|
| "message.post"
|
|
202
263
|
| "message.update"
|
|
203
264
|
| "message.delete";
|
|
@@ -210,6 +271,9 @@ type SlackBotContext = {
|
|
|
210
271
|
scheduledTaskId?: string | null;
|
|
211
272
|
};
|
|
212
273
|
|
|
274
|
+
type SlackCallAuthority = Readonly<{ operation: SlackBotOperation }>;
|
|
275
|
+
type SlackProviderAuthorization = () => Promise<boolean | void>;
|
|
276
|
+
|
|
213
277
|
export type SlackReactionContextCheckpointBinding = {
|
|
214
278
|
inboxId: string;
|
|
215
279
|
accountId: string;
|
|
@@ -259,6 +323,35 @@ export class SlackBotProviderError extends Error {
|
|
|
259
323
|
}
|
|
260
324
|
}
|
|
261
325
|
|
|
326
|
+
export async function authorizeSlackSharedImageRead(
|
|
327
|
+
channel: {
|
|
328
|
+
isArchived: boolean;
|
|
329
|
+
isShared: boolean;
|
|
330
|
+
isExternallyShared: boolean;
|
|
331
|
+
isOrgShared: boolean;
|
|
332
|
+
isPendingExternallyShared: boolean;
|
|
333
|
+
isMpim: boolean;
|
|
334
|
+
},
|
|
335
|
+
authorizeSharedRead: (() => Promise<void>) | undefined,
|
|
336
|
+
): Promise<void> {
|
|
337
|
+
if (channel.isArchived) {
|
|
338
|
+
throw new SlackBotProviderError("is_archived");
|
|
339
|
+
}
|
|
340
|
+
if (
|
|
341
|
+
!channel.isShared &&
|
|
342
|
+
!channel.isExternallyShared &&
|
|
343
|
+
!channel.isOrgShared &&
|
|
344
|
+
!channel.isPendingExternallyShared &&
|
|
345
|
+
!channel.isMpim
|
|
346
|
+
) {
|
|
347
|
+
throw new SlackBotProviderError("slack_connect_unsupported");
|
|
348
|
+
}
|
|
349
|
+
if (!authorizeSharedRead) {
|
|
350
|
+
throw new SlackBotProviderError("slack_connect_unsupported");
|
|
351
|
+
}
|
|
352
|
+
await authorizeSharedRead();
|
|
353
|
+
}
|
|
354
|
+
|
|
262
355
|
const SLACK_CREDENTIAL_REJECTION_CODES = new Set([
|
|
263
356
|
"account_inactive",
|
|
264
357
|
"invalid_auth",
|
|
@@ -285,12 +378,16 @@ export class SlackBotCredentialVerificationError extends HTTPException {
|
|
|
285
378
|
|
|
286
379
|
/**
|
|
287
380
|
* Validates a write-only xoxb credential before it can enter encrypted storage.
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
381
|
+
* The OAuth exchange's app_id is the immutable app authority. Slack bot profile
|
|
382
|
+
* names are mutable presentation data and may lag manifest/App Home changes, so
|
|
383
|
+
* they must not decide whether a credential belongs to this installation.
|
|
291
384
|
*/
|
|
292
385
|
export async function verifyOpenGeniSlackBotCredential(
|
|
293
386
|
token: string,
|
|
387
|
+
expected: Readonly<{
|
|
388
|
+
appId: string;
|
|
389
|
+
displayName: OpenGeniSlackBotDisplayName;
|
|
390
|
+
}>,
|
|
294
391
|
fetchImpl: FetchLike = fetch,
|
|
295
392
|
now: Date = new Date(),
|
|
296
393
|
): Promise<VerifiedOpenGeniSlackBot> {
|
|
@@ -314,11 +411,11 @@ export async function verifyOpenGeniSlackBotCredential(
|
|
|
314
411
|
);
|
|
315
412
|
}
|
|
316
413
|
const profile = slackRecord(user.profile);
|
|
317
|
-
const
|
|
318
|
-
if (
|
|
414
|
+
const installedAppId = slackString(profile?.api_app_id);
|
|
415
|
+
if (installedAppId !== expected.appId) {
|
|
319
416
|
throw new SlackBotCredentialVerificationError(
|
|
320
417
|
"identity_mismatch",
|
|
321
|
-
|
|
418
|
+
"Slack credential does not belong to the authorized Slack app",
|
|
322
419
|
);
|
|
323
420
|
}
|
|
324
421
|
|
|
@@ -331,7 +428,7 @@ export async function verifyOpenGeniSlackBotCredential(
|
|
|
331
428
|
slackTeamName,
|
|
332
429
|
botUserId,
|
|
333
430
|
botId,
|
|
334
|
-
botDisplayName:
|
|
431
|
+
botDisplayName: expected.displayName,
|
|
335
432
|
verifiedAt: now.toISOString(),
|
|
336
433
|
},
|
|
337
434
|
};
|
|
@@ -427,6 +524,7 @@ export class OpenGeniSlackBotClient {
|
|
|
427
524
|
private readonly metadata: OpenGeniSlackBotConnectionMetadata,
|
|
428
525
|
private readonly context: SlackBotContext,
|
|
429
526
|
private readonly fetchImpl: FetchLike = fetch,
|
|
527
|
+
private readonly authorizeProviderRequest?: SlackProviderAuthorization,
|
|
430
528
|
) {
|
|
431
529
|
this.resolveCredential = buildConnectionTokenResolver(db, settings);
|
|
432
530
|
}
|
|
@@ -453,6 +551,27 @@ export class OpenGeniSlackBotClient {
|
|
|
453
551
|
return await this.requireMemberChannel(headers, channelId);
|
|
454
552
|
}
|
|
455
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Replace one Slack user's private App Home view. `views.publish` is a
|
|
556
|
+
* naturally convergent replace operation: Slack event retries may safely
|
|
557
|
+
* repeat the exact bounded view without creating duplicate provider state.
|
|
558
|
+
*/
|
|
559
|
+
async publishHomeView(input: { userId: string; blocks: SlackHomeBlock[]; hash?: string | null }) {
|
|
560
|
+
const userId = requiredSlackString(input.userId, "user_id");
|
|
561
|
+
const blocks = validateSlackHomeBlocks(input.blocks);
|
|
562
|
+
const hash = input.hash ? requiredSlackString(input.hash, "hash") : null;
|
|
563
|
+
return await this.withAudit("home.publish", async (headers) => {
|
|
564
|
+
const payload = await this.call(headers, "views.publish", {
|
|
565
|
+
user_id: userId,
|
|
566
|
+
view: JSON.stringify({ type: "home", blocks }),
|
|
567
|
+
...(hash ? { hash } : {}),
|
|
568
|
+
});
|
|
569
|
+
return {
|
|
570
|
+
viewId: requiredSlackString(slackRecord(payload.view)?.id, "view.id"),
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
456
575
|
async slackTaskPolicyFacts(channelId: string, userId: string) {
|
|
457
576
|
return await this.withAudit("channel_history.read", async (headers) => {
|
|
458
577
|
const conversation = await this.requireMemberChannel(headers, channelId);
|
|
@@ -649,11 +768,18 @@ export class OpenGeniSlackBotClient {
|
|
|
649
768
|
async prepareReactionImageDownloads(input: {
|
|
650
769
|
channelId: string;
|
|
651
770
|
files: readonly { id: string; name: string; title: string }[];
|
|
771
|
+
authorizeSharedRead?: () => Promise<void>;
|
|
652
772
|
}): Promise<PreparedSlackReactionImage[]> {
|
|
653
773
|
const result = await this.withAudit("file.content.read", async (headers) => {
|
|
654
|
-
|
|
774
|
+
if (input.authorizeSharedRead) {
|
|
775
|
+
const channel = await this.requireMemberChannel(headers, input.channelId);
|
|
776
|
+
await authorizeSlackSharedImageRead(channel, input.authorizeSharedRead);
|
|
777
|
+
} else {
|
|
778
|
+
await this.requireActiveNonSharedMemberChannel(headers, input.channelId);
|
|
779
|
+
}
|
|
655
780
|
const prepared: PreparedSlackReactionImage[] = [];
|
|
656
781
|
for (const candidate of input.files) {
|
|
782
|
+
await input.authorizeSharedRead?.();
|
|
657
783
|
const payload = await this.call(headers, "files.info", { file: candidate.id });
|
|
658
784
|
const fileRecord = slackRecord(payload.file);
|
|
659
785
|
const file = projectFile(fileRecord);
|
|
@@ -688,6 +814,7 @@ export class OpenGeniSlackBotClient {
|
|
|
688
814
|
/** Download and fully validate one previously authorized Slack image. */
|
|
689
815
|
async downloadReactionImage(
|
|
690
816
|
input: PreparedSlackReactionImage,
|
|
817
|
+
authorizeSharedRead?: () => Promise<void>,
|
|
691
818
|
): Promise<DownloadedSlackReactionImage> {
|
|
692
819
|
return await this.withAudit("file.content.read", async () => {
|
|
693
820
|
if (!SLACK_REACTION_IMAGE_MIME_TYPES.has(input.declaredMimeType)) {
|
|
@@ -700,7 +827,11 @@ export class OpenGeniSlackBotClient {
|
|
|
700
827
|
) {
|
|
701
828
|
throw new SlackBotProviderError("invalid_file_size");
|
|
702
829
|
}
|
|
703
|
-
const response = await this.fetchPrivateFile(
|
|
830
|
+
const response = await this.fetchPrivateFile(
|
|
831
|
+
input.downloadUrl,
|
|
832
|
+
"file.content.read",
|
|
833
|
+
authorizeSharedRead,
|
|
834
|
+
);
|
|
704
835
|
const responseContentType = normalizedContentType(response.headers.get("content-type"));
|
|
705
836
|
if (!SLACK_REACTION_IMAGE_MIME_TYPES.has(responseContentType)) {
|
|
706
837
|
await response.body?.cancel().catch(() => undefined);
|
|
@@ -748,6 +879,69 @@ export class OpenGeniSlackBotClient {
|
|
|
748
879
|
});
|
|
749
880
|
}
|
|
750
881
|
|
|
882
|
+
/**
|
|
883
|
+
* Workspace-wide public search through Slack's Real-time Search API
|
|
884
|
+
* (`assistant.search.context`) under the bot identity.
|
|
885
|
+
*
|
|
886
|
+
* The bot never carries private-search authority: `channel_types` is pinned
|
|
887
|
+
* to `public_channel` server-side regardless of caller input, so private
|
|
888
|
+
* channels, DMs, and MPIMs remain reachable only through a member's personal
|
|
889
|
+
* hosted-MCP grant. An install predating the search scopes fails closed with
|
|
890
|
+
* a reinstall hint instead of leaking Slack's `missing_scope` error.
|
|
891
|
+
*/
|
|
892
|
+
async searchContext(input: {
|
|
893
|
+
query: string;
|
|
894
|
+
contentTypes?: readonly ("messages" | "files" | "channels")[];
|
|
895
|
+
includeBots?: boolean;
|
|
896
|
+
/** UNIX seconds bounds on message timestamps. */
|
|
897
|
+
before?: number;
|
|
898
|
+
after?: number;
|
|
899
|
+
sort?: "score" | "timestamp";
|
|
900
|
+
sortDir?: "asc" | "desc";
|
|
901
|
+
cursor?: string;
|
|
902
|
+
limit?: number;
|
|
903
|
+
}) {
|
|
904
|
+
const query = typeof input.query === "string" ? input.query.trim() : "";
|
|
905
|
+
if (!query || query.length > MAX_SEARCH_QUERY_CHARS) {
|
|
906
|
+
throw new SlackBotProviderError("invalid_query");
|
|
907
|
+
}
|
|
908
|
+
const contentTypes = input.contentTypes?.length
|
|
909
|
+
? [...new Set(input.contentTypes)]
|
|
910
|
+
: ["messages"];
|
|
911
|
+
return await this.withAudit("search.context", async (headers) => {
|
|
912
|
+
// Inside the audit boundary so a fail-closed denial on a legacy install
|
|
913
|
+
// leaves the same `failed` evidence as a provider rejection.
|
|
914
|
+
if (!hasOpenGeniSlackBotSearchScopes(this.connection.grantedScopes)) {
|
|
915
|
+
throw new SlackBotProviderError("slack_bot_search_scopes_missing");
|
|
916
|
+
}
|
|
917
|
+
const payload = await this.call(headers, "assistant.search.context", {
|
|
918
|
+
query,
|
|
919
|
+
channel_types: "public_channel",
|
|
920
|
+
content_types: contentTypes.join(","),
|
|
921
|
+
...(input.includeBots ? { include_bots: "true" } : {}),
|
|
922
|
+
...(input.before !== undefined ? { before: String(Math.trunc(input.before)) } : {}),
|
|
923
|
+
...(input.after !== undefined ? { after: String(Math.trunc(input.after)) } : {}),
|
|
924
|
+
...(input.sort ? { sort: input.sort } : {}),
|
|
925
|
+
...(input.sortDir ? { sort_dir: input.sortDir } : {}),
|
|
926
|
+
...(input.cursor ? { cursor: input.cursor } : {}),
|
|
927
|
+
limit: String(boundedInt(input.limit, MAX_SEARCH_PAGE, MAX_SEARCH_PAGE)),
|
|
928
|
+
});
|
|
929
|
+
const results = slackRecord(payload.results);
|
|
930
|
+
return {
|
|
931
|
+
messages: slackArray(results?.messages)
|
|
932
|
+
.map(projectSearchMessage)
|
|
933
|
+
.filter((message): message is NonNullable<typeof message> => message !== null),
|
|
934
|
+
files: slackArray(results?.files)
|
|
935
|
+
.map(projectSearchFile)
|
|
936
|
+
.filter((file): file is NonNullable<typeof file> => file !== null),
|
|
937
|
+
channels: slackArray(results?.channels)
|
|
938
|
+
.map(projectSearchChannel)
|
|
939
|
+
.filter((channel): channel is NonNullable<typeof channel> => channel !== null),
|
|
940
|
+
nextCursor: responseCursor(payload),
|
|
941
|
+
};
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
751
945
|
async listFiles(input: { channelId: string; limit?: number; cursor?: string }) {
|
|
752
946
|
return await this.withAudit("files.list", async (headers) => {
|
|
753
947
|
const requestedPage = this.fileListPage(input);
|
|
@@ -1211,10 +1405,7 @@ export class OpenGeniSlackBotClient {
|
|
|
1211
1405
|
}
|
|
1212
1406
|
|
|
1213
1407
|
private async slackMessageExists(channelId: string, timestamp: string): Promise<boolean> {
|
|
1214
|
-
const headers = await this.
|
|
1215
|
-
"message.delete",
|
|
1216
|
-
`${SLACK_API_BASE}chat.getPermalink`,
|
|
1217
|
-
);
|
|
1408
|
+
const headers = await this.headersFor("message.delete");
|
|
1218
1409
|
try {
|
|
1219
1410
|
await this.call(headers, "chat.getPermalink", {
|
|
1220
1411
|
channel: channelId,
|
|
@@ -1236,7 +1427,7 @@ export class OpenGeniSlackBotClient {
|
|
|
1236
1427
|
text: string;
|
|
1237
1428
|
}): Promise<{ timestamp: string }> {
|
|
1238
1429
|
const method = input.threadTimestamp ? "conversations.replies" : "conversations.history";
|
|
1239
|
-
const headers = await this.
|
|
1430
|
+
const headers = await this.headersFor("message.post");
|
|
1240
1431
|
const seenCursors = new Set<string>();
|
|
1241
1432
|
let cursor: string | null = null;
|
|
1242
1433
|
let matchedTimestamp: string | null = null;
|
|
@@ -1284,7 +1475,7 @@ export class OpenGeniSlackBotClient {
|
|
|
1284
1475
|
return { timestamp: matchedTimestamp };
|
|
1285
1476
|
}
|
|
1286
1477
|
|
|
1287
|
-
private async requireMemberChannel(headers:
|
|
1478
|
+
private async requireMemberChannel(headers: SlackCallAuthority, channelId: string) {
|
|
1288
1479
|
const payload = await this.call(headers, "conversations.info", {
|
|
1289
1480
|
channel: channelId,
|
|
1290
1481
|
});
|
|
@@ -1299,7 +1490,7 @@ export class OpenGeniSlackBotClient {
|
|
|
1299
1490
|
}
|
|
1300
1491
|
|
|
1301
1492
|
private async requireActiveNonSharedMemberChannel(
|
|
1302
|
-
headers:
|
|
1493
|
+
headers: SlackCallAuthority,
|
|
1303
1494
|
channelId: string,
|
|
1304
1495
|
) {
|
|
1305
1496
|
const projected = await this.requireMemberChannel(headers, channelId);
|
|
@@ -1313,7 +1504,7 @@ export class OpenGeniSlackBotClient {
|
|
|
1313
1504
|
}
|
|
1314
1505
|
|
|
1315
1506
|
private async requireFileForChannel(
|
|
1316
|
-
headers:
|
|
1507
|
+
headers: SlackCallAuthority,
|
|
1317
1508
|
operation: "file.info" | "file.content.read",
|
|
1318
1509
|
input: { channelId: string; fileId: string; parentFileId?: string },
|
|
1319
1510
|
) {
|
|
@@ -1390,11 +1581,15 @@ export class OpenGeniSlackBotClient {
|
|
|
1390
1581
|
}
|
|
1391
1582
|
|
|
1392
1583
|
private async call(
|
|
1393
|
-
|
|
1584
|
+
authority: SlackCallAuthority,
|
|
1394
1585
|
method: string,
|
|
1395
1586
|
params: Record<string, string>,
|
|
1396
1587
|
): Promise<SlackPayload> {
|
|
1397
1588
|
try {
|
|
1589
|
+
const headers = await this.headersForDestination(
|
|
1590
|
+
authority.operation,
|
|
1591
|
+
`${SLACK_API_BASE}${method}`,
|
|
1592
|
+
);
|
|
1398
1593
|
return (await slackApiFetchWithHeaders(this.fetchImpl, method, headers, params)).payload;
|
|
1399
1594
|
} catch (error) {
|
|
1400
1595
|
if (slackCredentialRejected(error)) {
|
|
@@ -1410,11 +1605,10 @@ export class OpenGeniSlackBotClient {
|
|
|
1410
1605
|
|
|
1411
1606
|
private async withAudit<T extends Record<string, unknown>>(
|
|
1412
1607
|
operation: SlackBotOperation,
|
|
1413
|
-
run: (
|
|
1608
|
+
run: (authority: SlackCallAuthority) => Promise<T>,
|
|
1414
1609
|
): Promise<T & { receipt: SlackBotReceipt }> {
|
|
1415
1610
|
try {
|
|
1416
|
-
const
|
|
1417
|
-
const result = await run(headers);
|
|
1611
|
+
const result = await run(await this.headersFor(operation));
|
|
1418
1612
|
await this.recordAudit(operation, "succeeded");
|
|
1419
1613
|
return { ...result, receipt: this.receipt(operation) };
|
|
1420
1614
|
} catch (error) {
|
|
@@ -1423,11 +1617,8 @@ export class OpenGeniSlackBotClient {
|
|
|
1423
1617
|
}
|
|
1424
1618
|
}
|
|
1425
1619
|
|
|
1426
|
-
private async headersFor(operation: SlackBotOperation): Promise<
|
|
1427
|
-
return
|
|
1428
|
-
operation,
|
|
1429
|
-
`${SLACK_API_BASE}${slackMethodForOperation(operation)}`,
|
|
1430
|
-
);
|
|
1620
|
+
private async headersFor(operation: SlackBotOperation): Promise<SlackCallAuthority> {
|
|
1621
|
+
return Object.freeze({ operation });
|
|
1431
1622
|
}
|
|
1432
1623
|
|
|
1433
1624
|
private async headersForDestination(
|
|
@@ -1450,14 +1641,40 @@ export class OpenGeniSlackBotClient {
|
|
|
1450
1641
|
if (result.status !== "ok" || result.connectionId !== this.connection.id) {
|
|
1451
1642
|
throw new Error("OpenGeni Slack bot connection needs to be reinstalled");
|
|
1452
1643
|
}
|
|
1644
|
+
const current = await requireOpenGeniSlackBotConnection(
|
|
1645
|
+
this.db,
|
|
1646
|
+
this.context.workspaceId,
|
|
1647
|
+
this.connection.id,
|
|
1648
|
+
);
|
|
1649
|
+
const currentMetadata = openGeniSlackBotMetadata(current.metadata);
|
|
1650
|
+
if (
|
|
1651
|
+
current.accountId !== this.context.accountId ||
|
|
1652
|
+
current.version !== this.connection.version ||
|
|
1653
|
+
result.connectionVersion !== this.connection.version ||
|
|
1654
|
+
!currentMetadata ||
|
|
1655
|
+
currentMetadata.slackTeamId !== this.metadata.slackTeamId ||
|
|
1656
|
+
currentMetadata.botId !== this.metadata.botId ||
|
|
1657
|
+
currentMetadata.botUserId !== this.metadata.botUserId
|
|
1658
|
+
) {
|
|
1659
|
+
throw new Error("OpenGeni Slack bot connection authority changed");
|
|
1660
|
+
}
|
|
1661
|
+
// The adapter callback is a fallible preflight. The canonical credential
|
|
1662
|
+
// callback must remain the final await before the physical fetch.
|
|
1663
|
+
if (this.authorizeProviderRequest && (await this.authorizeProviderRequest()) === false) {
|
|
1664
|
+
throw new Error("OpenGeni Slack bot provider request is no longer authorized");
|
|
1665
|
+
}
|
|
1666
|
+
if (result.authorizeProviderRequest && !(await result.authorizeProviderRequest())) {
|
|
1667
|
+
throw new Error("OpenGeni Slack bot provider request is no longer authorized");
|
|
1668
|
+
}
|
|
1453
1669
|
return result.headers;
|
|
1454
1670
|
}
|
|
1455
1671
|
|
|
1456
1672
|
private async fetchPrivateFile(
|
|
1457
1673
|
url: URL,
|
|
1458
1674
|
operation: "file.info" | "file.content.read",
|
|
1675
|
+
authorizeBeforeFetch?: () => Promise<void>,
|
|
1459
1676
|
): Promise<Response> {
|
|
1460
|
-
const response = await this.fetchPrivateFileOnce(url, operation);
|
|
1677
|
+
const response = await this.fetchPrivateFileOnce(url, operation, authorizeBeforeFetch);
|
|
1461
1678
|
if (response.status < 300 || response.status >= 400) {
|
|
1462
1679
|
return response;
|
|
1463
1680
|
}
|
|
@@ -1478,18 +1695,20 @@ export class OpenGeniSlackBotClient {
|
|
|
1478
1695
|
} catch {
|
|
1479
1696
|
throw new SlackBotProviderError("invalid_file_redirect");
|
|
1480
1697
|
}
|
|
1481
|
-
return await this.fetchPrivateFileOnce(redirected, operation);
|
|
1698
|
+
return await this.fetchPrivateFileOnce(redirected, operation, authorizeBeforeFetch);
|
|
1482
1699
|
}
|
|
1483
1700
|
|
|
1484
1701
|
private async fetchPrivateFileOnce(
|
|
1485
1702
|
url: URL,
|
|
1486
1703
|
operation: "file.info" | "file.content.read",
|
|
1704
|
+
authorizeBeforeFetch?: () => Promise<void>,
|
|
1487
1705
|
): Promise<Response> {
|
|
1488
1706
|
try {
|
|
1489
1707
|
assertPrivateSlackFileUrl(url);
|
|
1490
1708
|
} catch {
|
|
1491
1709
|
throw new SlackBotProviderError("invalid_file_url");
|
|
1492
1710
|
}
|
|
1711
|
+
await authorizeBeforeFetch?.();
|
|
1493
1712
|
const headers = await this.headersForDestination(operation, url.toString());
|
|
1494
1713
|
let response: Response;
|
|
1495
1714
|
try {
|
|
@@ -1701,7 +1920,12 @@ export class OpenGeniSlackBotClient {
|
|
|
1701
1920
|
}
|
|
1702
1921
|
|
|
1703
1922
|
export function createOpenGeniSlackBotClient(
|
|
1704
|
-
deps: {
|
|
1923
|
+
deps: {
|
|
1924
|
+
db: Database;
|
|
1925
|
+
settings: Settings;
|
|
1926
|
+
slackFetch?: typeof fetch;
|
|
1927
|
+
authorizeProviderRequest?: SlackProviderAuthorization;
|
|
1928
|
+
},
|
|
1705
1929
|
resolved: Awaited<ReturnType<typeof resolveSlackBotConnectionForTool>>,
|
|
1706
1930
|
): OpenGeniSlackBotClient {
|
|
1707
1931
|
return new OpenGeniSlackBotClient(
|
|
@@ -1711,11 +1935,17 @@ export function createOpenGeniSlackBotClient(
|
|
|
1711
1935
|
resolved.metadata,
|
|
1712
1936
|
resolved.context,
|
|
1713
1937
|
deps.slackFetch,
|
|
1938
|
+
deps.authorizeProviderRequest,
|
|
1714
1939
|
);
|
|
1715
1940
|
}
|
|
1716
1941
|
|
|
1717
1942
|
export async function createOpenGeniSlackBotInteractionClient(
|
|
1718
|
-
deps: {
|
|
1943
|
+
deps: {
|
|
1944
|
+
db: Database;
|
|
1945
|
+
settings: Settings;
|
|
1946
|
+
slackFetch?: typeof fetch;
|
|
1947
|
+
authorizeProviderRequest?: SlackProviderAuthorization;
|
|
1948
|
+
},
|
|
1719
1949
|
input: {
|
|
1720
1950
|
accountId: string;
|
|
1721
1951
|
workspaceId: string;
|
|
@@ -1747,6 +1977,7 @@ export async function createOpenGeniSlackBotInteractionClient(
|
|
|
1747
1977
|
scheduledTaskId: null,
|
|
1748
1978
|
},
|
|
1749
1979
|
deps.slackFetch,
|
|
1980
|
+
deps.authorizeProviderRequest,
|
|
1750
1981
|
);
|
|
1751
1982
|
}
|
|
1752
1983
|
|
|
@@ -1770,7 +2001,7 @@ async function slackApiFetchWithHeaders(
|
|
|
1770
2001
|
credentialHeaders: Record<string, string>,
|
|
1771
2002
|
params: Record<string, string>,
|
|
1772
2003
|
): Promise<{ response: Response; payload: SlackPayload }> {
|
|
1773
|
-
if (!/^[a-z]
|
|
2004
|
+
if (!/^[a-z]+(?:\.[a-z]+){1,2}$/i.test(method)) {
|
|
1774
2005
|
throw new Error("invalid Slack API method");
|
|
1775
2006
|
}
|
|
1776
2007
|
const url = new URL(method, SLACK_API_BASE);
|
|
@@ -2576,6 +2807,74 @@ function embeddedSlackFileIds(content: string): Set<string> {
|
|
|
2576
2807
|
);
|
|
2577
2808
|
}
|
|
2578
2809
|
|
|
2810
|
+
/**
|
|
2811
|
+
* Bounded projections of `assistant.search.context` results. Content excerpts
|
|
2812
|
+
* are capped, Slack's rich `blocks` and context messages are deliberately
|
|
2813
|
+
* dropped, and permalinks are kept only when they are https URLs.
|
|
2814
|
+
*/
|
|
2815
|
+
function projectSearchMessage(value: unknown) {
|
|
2816
|
+
const message = slackRecord(value);
|
|
2817
|
+
const channelId = slackString(message?.channel_id);
|
|
2818
|
+
const ts = slackString(message?.message_ts);
|
|
2819
|
+
if (!message || !channelId || !ts) return null;
|
|
2820
|
+
return {
|
|
2821
|
+
channelId,
|
|
2822
|
+
channelName: boundedSlackString(message.channel_name, 256),
|
|
2823
|
+
ts,
|
|
2824
|
+
authorUserId: boundedSlackString(message.author_user_id, 64),
|
|
2825
|
+
authorName: boundedSlackString(message.author_name, 256),
|
|
2826
|
+
isAuthorBot: message.is_author_bot === true,
|
|
2827
|
+
content: boundedSlackString(message.content, MAX_SEARCH_CONTENT_CHARS),
|
|
2828
|
+
permalink: safeSlackPermalink(message.permalink),
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function projectSearchFile(value: unknown) {
|
|
2833
|
+
const file = slackRecord(value);
|
|
2834
|
+
const id = slackString(file?.file_id);
|
|
2835
|
+
if (!file || !id) return null;
|
|
2836
|
+
return {
|
|
2837
|
+
id,
|
|
2838
|
+
title: boundedSlackString(file.title, 512),
|
|
2839
|
+
filetype: boundedSlackString(file.file_type, 128),
|
|
2840
|
+
authorUserId: boundedSlackString(file.author_user_id, 64),
|
|
2841
|
+
authorName: boundedSlackString(file.author_name, 256),
|
|
2842
|
+
dateCreated: slackUnixSeconds(file.date_created),
|
|
2843
|
+
dateUpdated: slackUnixSeconds(file.date_updated),
|
|
2844
|
+
content: boundedSlackString(file.content, MAX_SEARCH_CONTENT_CHARS),
|
|
2845
|
+
permalink: safeSlackPermalink(file.permalink),
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
function projectSearchChannel(value: unknown) {
|
|
2850
|
+
const channel = slackRecord(value);
|
|
2851
|
+
const name = slackString(channel?.name);
|
|
2852
|
+
if (!channel || !name) return null;
|
|
2853
|
+
return {
|
|
2854
|
+
name: boundedSlackString(name, 256),
|
|
2855
|
+
topic: boundedSlackString(channel.topic, 512),
|
|
2856
|
+
purpose: boundedSlackString(channel.purpose, 512),
|
|
2857
|
+
creatorUserId: boundedSlackString(channel.creator_user_id, 64),
|
|
2858
|
+
dateCreated: slackUnixSeconds(channel.date_created),
|
|
2859
|
+
permalink: safeSlackPermalink(channel.permalink),
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
function slackUnixSeconds(value: unknown): number | null {
|
|
2864
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
function safeSlackPermalink(value: unknown): string | null {
|
|
2868
|
+
// An over-long URL is dropped, never truncated into a still-parseable one.
|
|
2869
|
+
const raw = typeof value === "string" && value.length <= 1_024 ? value : "";
|
|
2870
|
+
if (!raw) return null;
|
|
2871
|
+
try {
|
|
2872
|
+
return new URL(raw).protocol === "https:" ? raw : null;
|
|
2873
|
+
} catch {
|
|
2874
|
+
return null;
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2579
2878
|
function projectUser(value: unknown) {
|
|
2580
2879
|
const user = slackRecord(value);
|
|
2581
2880
|
const id = slackString(user?.id);
|
|
@@ -2701,30 +3000,6 @@ export function nextSlackFilesListPage(
|
|
|
2701
3000
|
return nextPage;
|
|
2702
3001
|
}
|
|
2703
3002
|
|
|
2704
|
-
function slackMethodForOperation(operation: SlackBotOperation): string {
|
|
2705
|
-
switch (operation) {
|
|
2706
|
-
case "channels.list":
|
|
2707
|
-
return "conversations.list";
|
|
2708
|
-
case "channel_history.read":
|
|
2709
|
-
return "conversations.history";
|
|
2710
|
-
case "thread_replies.read":
|
|
2711
|
-
return "conversations.replies";
|
|
2712
|
-
case "users.list":
|
|
2713
|
-
return "users.list";
|
|
2714
|
-
case "files.list":
|
|
2715
|
-
return "files.list";
|
|
2716
|
-
case "file.info":
|
|
2717
|
-
case "file.content.read":
|
|
2718
|
-
return "files.info";
|
|
2719
|
-
case "message.post":
|
|
2720
|
-
return "chat.postMessage";
|
|
2721
|
-
case "message.update":
|
|
2722
|
-
return "chat.update";
|
|
2723
|
-
case "message.delete":
|
|
2724
|
-
return "chat.delete";
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
3003
|
function validateSlackMessageBlocks(
|
|
2729
3004
|
blocks: SlackMessageBlock[] | undefined,
|
|
2730
3005
|
): SlackMessageBlock[] | undefined {
|
|
@@ -2738,6 +3013,16 @@ function validateSlackMessageBlocks(
|
|
|
2738
3013
|
return blocks;
|
|
2739
3014
|
}
|
|
2740
3015
|
|
|
3016
|
+
function validateSlackHomeBlocks(blocks: SlackHomeBlock[]): SlackHomeBlock[] {
|
|
3017
|
+
if (blocks.length < 1 || blocks.length > 100) {
|
|
3018
|
+
throw new RangeError("Slack App Home blocks exceed the supported count");
|
|
3019
|
+
}
|
|
3020
|
+
if (Buffer.byteLength(JSON.stringify(blocks), "utf8") > 48 * 1024) {
|
|
3021
|
+
throw new RangeError("Slack App Home blocks exceed the supported byte size");
|
|
3022
|
+
}
|
|
3023
|
+
return blocks;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2741
3026
|
function boundedInt(value: number | undefined, max: number, fallback: number): number {
|
|
2742
3027
|
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
2743
3028
|
? Math.min(value, max)
|