@opengeni/api-router 0.14.4 → 0.15.4
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 +6 -1
- package/dist/app.js +5 -1
- package/dist/auth/managed-auth.d.ts +2 -2
- package/dist/{chunk-36PW33ND.js → chunk-ICRZC3JH.js} +11438 -8894
- package/dist/chunk-ICRZC3JH.js.map +1 -0
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/integrations/slack-bot.d.ts +30 -1
- package/dist/integrations/slack-interactions.d.ts +47 -0
- package/dist/integrations/social-api.d.ts +91 -0
- package/dist/integrations/social-oauth.d.ts +84 -0
- package/dist/routes/workspace-artifacts.d.ts +3 -0
- package/package.json +10 -10
- package/src/app.ts +115 -28
- package/src/http/auth.ts +9 -1
- package/src/index.ts +17 -4
- package/src/integrations/oauth-client.ts +27 -10
- package/src/integrations/slack-bot.ts +100 -14
- package/src/integrations/slack-interactions.ts +1033 -0
- package/src/integrations/social-api.ts +504 -0
- package/src/integrations/social-oauth.ts +745 -0
- package/src/mcp/server.ts +383 -0
- package/src/routes/sessions.ts +23 -10
- package/src/routes/social.ts +67 -2
- package/src/routes/workspace-artifacts.ts +274 -0
- package/dist/chunk-36PW33ND.js.map +0 -1
|
@@ -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
|
}
|
|
@@ -147,7 +153,10 @@ type SlackBotContext = {
|
|
|
147
153
|
};
|
|
148
154
|
|
|
149
155
|
export class SlackBotProviderError extends Error {
|
|
150
|
-
constructor(
|
|
156
|
+
constructor(
|
|
157
|
+
readonly code: string,
|
|
158
|
+
readonly retryAfterMs: number | null = null,
|
|
159
|
+
) {
|
|
151
160
|
super(`Slack bot request failed: ${safeSlackCode(code)}`);
|
|
152
161
|
this.name = "SlackBotProviderError";
|
|
153
162
|
}
|
|
@@ -342,6 +351,11 @@ export class OpenGeniSlackBotClient {
|
|
|
342
351
|
});
|
|
343
352
|
}
|
|
344
353
|
|
|
354
|
+
async verifyChannelAccess(channelId: string) {
|
|
355
|
+
const headers = await this.headersFor("channel_history.read");
|
|
356
|
+
return await this.requireMemberChannel(headers, channelId);
|
|
357
|
+
}
|
|
358
|
+
|
|
345
359
|
async channelHistory(input: { channelId: string; limit?: number; cursor?: string }) {
|
|
346
360
|
return await this.withAudit("channel_history.read", async (headers) => {
|
|
347
361
|
const info = await this.requireMemberChannel(headers, input.channelId);
|
|
@@ -484,7 +498,9 @@ export class OpenGeniSlackBotClient {
|
|
|
484
498
|
const headers = await this.headersFor(operation);
|
|
485
499
|
let channelId = input.channelId;
|
|
486
500
|
if (input.userId) {
|
|
487
|
-
const opened = await this.call(headers, "conversations.open", {
|
|
501
|
+
const opened = await this.call(headers, "conversations.open", {
|
|
502
|
+
users: input.userId,
|
|
503
|
+
});
|
|
488
504
|
channelId = requiredSlackString(slackRecord(opened.channel)?.id, "channel.id");
|
|
489
505
|
} else if (channelId) {
|
|
490
506
|
await this.requireMemberChannel(headers, channelId);
|
|
@@ -704,7 +720,10 @@ export class OpenGeniSlackBotClient {
|
|
|
704
720
|
`${SLACK_API_BASE}chat.getPermalink`,
|
|
705
721
|
);
|
|
706
722
|
try {
|
|
707
|
-
await this.call(headers, "chat.getPermalink", {
|
|
723
|
+
await this.call(headers, "chat.getPermalink", {
|
|
724
|
+
channel: channelId,
|
|
725
|
+
message_ts: timestamp,
|
|
726
|
+
});
|
|
708
727
|
return true;
|
|
709
728
|
} catch (error) {
|
|
710
729
|
if (error instanceof SlackBotProviderError && error.code === "message_not_found") {
|
|
@@ -715,9 +734,14 @@ export class OpenGeniSlackBotClient {
|
|
|
715
734
|
}
|
|
716
735
|
|
|
717
736
|
private async requireMemberChannel(headers: Record<string, string>, channelId: string) {
|
|
718
|
-
const payload = await this.call(headers, "conversations.info", {
|
|
737
|
+
const payload = await this.call(headers, "conversations.info", {
|
|
738
|
+
channel: channelId,
|
|
739
|
+
});
|
|
719
740
|
const projected = projectChannel(payload.channel);
|
|
720
|
-
|
|
741
|
+
// Slack omits `is_member` for a bot's one-to-one App Home conversation.
|
|
742
|
+
// A successful conversations.info response with `is_im` is itself proof
|
|
743
|
+
// that this installation can address that direct conversation.
|
|
744
|
+
if (!projected || (projected.isMember !== true && projected.isDirectMessage !== true)) {
|
|
721
745
|
throw new SlackBotProviderError("not_in_channel");
|
|
722
746
|
}
|
|
723
747
|
return projected;
|
|
@@ -946,7 +970,10 @@ export class OpenGeniSlackBotClient {
|
|
|
946
970
|
}
|
|
947
971
|
|
|
948
972
|
private completedDeleteResult(
|
|
949
|
-
operation: {
|
|
973
|
+
operation: {
|
|
974
|
+
slackChannelId: string | null;
|
|
975
|
+
slackMessageTimestamp: string | null;
|
|
976
|
+
},
|
|
950
977
|
operationId: string,
|
|
951
978
|
) {
|
|
952
979
|
if (!operation.slackChannelId || !operation.slackMessageTimestamp) {
|
|
@@ -1008,9 +1035,15 @@ export class OpenGeniSlackBotClient {
|
|
|
1008
1035
|
return { type: "subject", id: this.context.subjectId };
|
|
1009
1036
|
}
|
|
1010
1037
|
if (this.context.scheduledTaskId) {
|
|
1011
|
-
return {
|
|
1038
|
+
return {
|
|
1039
|
+
type: "service",
|
|
1040
|
+
id: `scheduler:${this.context.scheduledTaskId}`,
|
|
1041
|
+
};
|
|
1012
1042
|
}
|
|
1013
|
-
return {
|
|
1043
|
+
return {
|
|
1044
|
+
type: "service",
|
|
1045
|
+
id: `session:${this.context.sessionId ?? "workspace"}`,
|
|
1046
|
+
};
|
|
1014
1047
|
}
|
|
1015
1048
|
|
|
1016
1049
|
private fileListPage(input: {
|
|
@@ -1020,13 +1053,19 @@ export class OpenGeniSlackBotClient {
|
|
|
1020
1053
|
}): SlackFilesListPage {
|
|
1021
1054
|
const key = environmentsEncryptionKeyBytes(this.settings);
|
|
1022
1055
|
if (!key) throw new Error("connection encryption is not configured");
|
|
1023
|
-
return resolveSlackFilesListPage(input, {
|
|
1056
|
+
return resolveSlackFilesListPage(input, {
|
|
1057
|
+
connectionId: this.connection.id,
|
|
1058
|
+
key,
|
|
1059
|
+
});
|
|
1024
1060
|
}
|
|
1025
1061
|
|
|
1026
1062
|
private fileListCursor(input: { channelId: string; count: number; page: number }): string {
|
|
1027
1063
|
const key = environmentsEncryptionKeyBytes(this.settings);
|
|
1028
1064
|
if (!key) throw new Error("connection encryption is not configured");
|
|
1029
|
-
return createSlackFilesListCursor(input, {
|
|
1065
|
+
return createSlackFilesListCursor(input, {
|
|
1066
|
+
connectionId: this.connection.id,
|
|
1067
|
+
key,
|
|
1068
|
+
});
|
|
1030
1069
|
}
|
|
1031
1070
|
|
|
1032
1071
|
private async recordAudit(
|
|
@@ -1076,6 +1115,42 @@ export function createOpenGeniSlackBotClient(
|
|
|
1076
1115
|
);
|
|
1077
1116
|
}
|
|
1078
1117
|
|
|
1118
|
+
export async function createOpenGeniSlackBotInteractionClient(
|
|
1119
|
+
deps: { db: Database; settings: Settings; slackFetch?: typeof fetch },
|
|
1120
|
+
input: {
|
|
1121
|
+
accountId: string;
|
|
1122
|
+
workspaceId: string;
|
|
1123
|
+
connectionId: string;
|
|
1124
|
+
subjectId: string;
|
|
1125
|
+
sessionId?: string | null;
|
|
1126
|
+
},
|
|
1127
|
+
): Promise<OpenGeniSlackBotClient> {
|
|
1128
|
+
const connection = await requireOpenGeniSlackBotConnection(
|
|
1129
|
+
deps.db,
|
|
1130
|
+
input.workspaceId,
|
|
1131
|
+
input.connectionId,
|
|
1132
|
+
);
|
|
1133
|
+
if (connection.accountId !== input.accountId) {
|
|
1134
|
+
throw new Error("OpenGeni Slack bot connection tenant mismatch");
|
|
1135
|
+
}
|
|
1136
|
+
const metadata = openGeniSlackBotMetadata(connection.metadata);
|
|
1137
|
+
if (!metadata) throw new Error("OpenGeni Slack bot connection metadata is invalid");
|
|
1138
|
+
return new OpenGeniSlackBotClient(
|
|
1139
|
+
deps.db,
|
|
1140
|
+
deps.settings,
|
|
1141
|
+
connection,
|
|
1142
|
+
metadata,
|
|
1143
|
+
{
|
|
1144
|
+
accountId: input.accountId,
|
|
1145
|
+
workspaceId: input.workspaceId,
|
|
1146
|
+
subjectId: input.subjectId,
|
|
1147
|
+
sessionId: input.sessionId ?? null,
|
|
1148
|
+
scheduledTaskId: null,
|
|
1149
|
+
},
|
|
1150
|
+
deps.slackFetch,
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1079
1154
|
async function slackApiFetch(
|
|
1080
1155
|
fetchImpl: FetchLike,
|
|
1081
1156
|
method: string,
|
|
@@ -1117,8 +1192,9 @@ async function slackApiFetchWithHeaders(
|
|
|
1117
1192
|
throw new SlackBotProviderError("transport_error");
|
|
1118
1193
|
}
|
|
1119
1194
|
if (!response.ok) {
|
|
1195
|
+
const retryAfterMs = slackRetryAfterMs(response);
|
|
1120
1196
|
await response.body?.cancel().catch(() => undefined);
|
|
1121
|
-
throw new SlackBotProviderError(`http_${response.status}
|
|
1197
|
+
throw new SlackBotProviderError(`http_${response.status}`, retryAfterMs);
|
|
1122
1198
|
}
|
|
1123
1199
|
let payload: SlackPayload;
|
|
1124
1200
|
try {
|
|
@@ -1136,6 +1212,15 @@ async function slackApiFetchWithHeaders(
|
|
|
1136
1212
|
return { response, payload };
|
|
1137
1213
|
}
|
|
1138
1214
|
|
|
1215
|
+
function slackRetryAfterMs(response: Response): number | null {
|
|
1216
|
+
if (response.status !== 429) return null;
|
|
1217
|
+
const raw = response.headers.get("retry-after");
|
|
1218
|
+
if (!raw || !/^\d+$/.test(raw)) return null;
|
|
1219
|
+
const seconds = Number(raw);
|
|
1220
|
+
if (!Number.isSafeInteger(seconds) || seconds < 1) return null;
|
|
1221
|
+
return Math.min(seconds, 3_600) * 1_000;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1139
1224
|
function assertOpenGeniSlackBotScopes(grantedScopes: string[]): void {
|
|
1140
1225
|
const policy = evaluateOpenGeniSlackBotScopes(grantedScopes);
|
|
1141
1226
|
if (!policy.accepted) {
|
|
@@ -1176,6 +1261,7 @@ function projectChannel(value: unknown) {
|
|
|
1176
1261
|
name: boundedSlackString(channel.name, 256),
|
|
1177
1262
|
isPrivate: channel.is_private === true,
|
|
1178
1263
|
isMember: channel.is_member === true,
|
|
1264
|
+
isDirectMessage: channel.is_im === true,
|
|
1179
1265
|
isArchived: channel.is_archived === true,
|
|
1180
1266
|
topic: boundedSlackString(slackRecord(channel.topic)?.value, 1_024),
|
|
1181
1267
|
purpose: boundedSlackString(slackRecord(channel.purpose)?.value, 1_024),
|