@sentry/junior 0.8.0 → 0.9.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/chunk-4G2LA7RO.js +678 -0
- package/dist/{chunk-OGLG4WAL.js → chunk-DIMXJUSL.js} +11346 -9421
- package/dist/{chunk-NNYZHUWR.js → chunk-I3DYWLM6.js} +15 -6
- package/dist/chunk-IJVZEV3K.js +840 -0
- package/dist/{chunk-Z5E25LRN.js → chunk-KCLEEKYX.js} +124 -17
- package/dist/chunk-VM3CPAZF.js +448 -0
- package/dist/chunk-ZBWWHP6Q.js +1436 -0
- package/dist/{chunk-PY4AI2GZ.js → chunk-ZW4OVKF5.js} +376 -79
- package/dist/cli/check.js +4 -2
- package/dist/cli/snapshot-warmup.js +7 -6
- package/dist/handlers/queue-callback.js +7 -7
- package/dist/handlers/router.d.ts +1 -0
- package/dist/handlers/router.js +523 -85
- package/dist/handlers/webhooks.js +2 -2
- package/dist/next-config.js +1 -1
- package/dist/production-XMCJXOOI.js +15 -0
- package/package.json +12 -14
- package/dist/bot-7SE3TX37.js +0 -19
- package/dist/chunk-H3ZG43WE.js +0 -330
- package/dist/chunk-KT5HARSN.js +0 -164
- package/dist/chunk-RKOO42TW.js +0 -1797
- package/dist/chunk-VW26MOSO.js +0 -522
package/dist/handlers/router.js
CHANGED
|
@@ -1,57 +1,73 @@
|
|
|
1
1
|
import {
|
|
2
2
|
POST as POST2
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-I3DYWLM6.js";
|
|
4
4
|
import {
|
|
5
5
|
POST
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-4G2LA7RO.js";
|
|
7
7
|
import {
|
|
8
|
+
buildConversationContext,
|
|
9
|
+
buildSlackOutputMessage,
|
|
10
|
+
coerceThreadArtifactsState,
|
|
11
|
+
coerceThreadConversationState,
|
|
12
|
+
createUserTokenStore,
|
|
13
|
+
deleteMcpAuthSession,
|
|
8
14
|
escapeXml,
|
|
15
|
+
finalizeMcpAuthorization,
|
|
9
16
|
formatProviderLabel,
|
|
10
17
|
generateAssistantReply,
|
|
18
|
+
generateConversationId,
|
|
11
19
|
getSlackClient,
|
|
12
|
-
|
|
20
|
+
isRetryableTurnError,
|
|
21
|
+
markConversationMessage,
|
|
22
|
+
markTurnCompleted,
|
|
23
|
+
markTurnFailed,
|
|
24
|
+
mergeArtifactsState,
|
|
25
|
+
normalizeConversationText,
|
|
26
|
+
persistThreadState,
|
|
13
27
|
publishAppHomeView,
|
|
14
28
|
resolveBaseUrl,
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
resolveReplyDelivery,
|
|
30
|
+
truncateStatusText,
|
|
31
|
+
updateConversationStats,
|
|
32
|
+
uploadFilesToThread,
|
|
33
|
+
upsertConversationMessage
|
|
34
|
+
} from "../chunk-DIMXJUSL.js";
|
|
17
35
|
import {
|
|
18
36
|
GET
|
|
19
37
|
} from "../chunk-4RBEYCOG.js";
|
|
20
|
-
import "../chunk-
|
|
38
|
+
import "../chunk-VM3CPAZF.js";
|
|
21
39
|
import {
|
|
22
40
|
botConfig,
|
|
41
|
+
getStateAdapter
|
|
42
|
+
} from "../chunk-IJVZEV3K.js";
|
|
43
|
+
import {
|
|
23
44
|
buildOAuthTokenRequest,
|
|
24
45
|
getPluginOAuthConfig,
|
|
25
|
-
getStateAdapter,
|
|
26
46
|
parseOAuthTokenResponse
|
|
27
|
-
} from "../chunk-
|
|
28
|
-
import "../chunk-
|
|
47
|
+
} from "../chunk-ZBWWHP6Q.js";
|
|
48
|
+
import "../chunk-KCLEEKYX.js";
|
|
29
49
|
import {
|
|
30
50
|
logException,
|
|
31
|
-
logInfo
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
logInfo,
|
|
52
|
+
logWarn
|
|
53
|
+
} from "../chunk-ZW4OVKF5.js";
|
|
34
54
|
|
|
35
|
-
// src/handlers/oauth-callback.ts
|
|
55
|
+
// src/handlers/mcp-oauth-callback.ts
|
|
56
|
+
import { Buffer } from "buffer";
|
|
36
57
|
import { after } from "next/server";
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
</html>`;
|
|
51
|
-
return new Response(html, {
|
|
52
|
-
status,
|
|
53
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
54
|
-
});
|
|
58
|
+
import { ThreadImpl } from "chat";
|
|
59
|
+
|
|
60
|
+
// src/handlers/oauth-resume.ts
|
|
61
|
+
function resolveReplyTimeoutMs(explicitTimeoutMs) {
|
|
62
|
+
if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
|
|
63
|
+
return explicitTimeoutMs;
|
|
64
|
+
}
|
|
65
|
+
const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
|
|
66
|
+
if (!raw) {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
const parsed = Number.parseInt(raw, 10);
|
|
70
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
55
71
|
}
|
|
56
72
|
async function postSlackMessage(channelId, threadTs, text) {
|
|
57
73
|
try {
|
|
@@ -134,12 +150,12 @@ function createReadOnlyConfigService(values) {
|
|
|
134
150
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
135
151
|
}));
|
|
136
152
|
return {
|
|
137
|
-
get: async (key) => entries.find((
|
|
153
|
+
get: async (key) => entries.find((entry) => entry.key === key),
|
|
138
154
|
set: async () => {
|
|
139
155
|
throw new Error("Read-only configuration in resumed context");
|
|
140
156
|
},
|
|
141
157
|
unset: async () => false,
|
|
142
|
-
list: async ({ prefix } = {}) => entries.filter((
|
|
158
|
+
list: async ({ prefix } = {}) => entries.filter((entry) => !prefix || entry.key.startsWith(prefix)),
|
|
143
159
|
resolve: async (key) => values[key],
|
|
144
160
|
resolveValues: async ({ keys, prefix } = {}) => {
|
|
145
161
|
const filtered = {};
|
|
@@ -152,63 +168,467 @@ function createReadOnlyConfigService(values) {
|
|
|
152
168
|
}
|
|
153
169
|
};
|
|
154
170
|
}
|
|
155
|
-
async function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
await
|
|
159
|
-
stored.channelId,
|
|
160
|
-
stored.threadTs,
|
|
161
|
-
`Your ${providerLabel} account is now connected. Processing your request...`
|
|
162
|
-
);
|
|
163
|
-
const postStatus = createDebouncedStatusPoster(
|
|
164
|
-
stored.channelId,
|
|
165
|
-
stored.threadTs
|
|
166
|
-
);
|
|
167
|
-
await setAssistantStatus(stored.channelId, stored.threadTs, "Thinking...");
|
|
171
|
+
async function resumeAuthorizedRequest(args) {
|
|
172
|
+
const postStatus = createDebouncedStatusPoster(args.channelId, args.threadTs);
|
|
173
|
+
await postSlackMessage(args.channelId, args.threadTs, args.connectedText);
|
|
174
|
+
await setAssistantStatus(args.channelId, args.threadTs, "Thinking...");
|
|
168
175
|
try {
|
|
169
|
-
const
|
|
176
|
+
const generateReply = args.generateReply ?? generateAssistantReply;
|
|
177
|
+
const replyPromise = generateReply(args.messageText, {
|
|
170
178
|
assistant: { userName: botConfig.userName },
|
|
171
|
-
requester: { userId:
|
|
179
|
+
requester: { userId: args.requesterUserId },
|
|
172
180
|
correlation: {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
conversationId: args.correlation?.conversationId,
|
|
182
|
+
turnId: args.correlation?.turnId,
|
|
183
|
+
channelId: args.correlation?.channelId ?? args.channelId,
|
|
184
|
+
threadTs: args.correlation?.threadTs ?? args.threadTs,
|
|
185
|
+
requesterId: args.correlation?.requesterId ?? args.requesterUserId
|
|
176
186
|
},
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
toolChannelId: args.toolChannelId,
|
|
188
|
+
conversationContext: args.conversationContext,
|
|
189
|
+
artifactState: args.artifactState,
|
|
190
|
+
configuration: args.configuration,
|
|
191
|
+
channelConfiguration: args.configuration ? createReadOnlyConfigService(args.configuration) : void 0,
|
|
179
192
|
onStatus: postStatus
|
|
180
193
|
});
|
|
194
|
+
const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
|
|
195
|
+
const reply = typeof replyTimeoutMs === "number" ? await Promise.race([
|
|
196
|
+
replyPromise,
|
|
197
|
+
new Promise(
|
|
198
|
+
(_, reject) => setTimeout(
|
|
199
|
+
() => reject(
|
|
200
|
+
new Error(
|
|
201
|
+
`generateAssistantReply timed out after ${replyTimeoutMs}ms`
|
|
202
|
+
)
|
|
203
|
+
),
|
|
204
|
+
replyTimeoutMs
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
]) : await replyPromise;
|
|
181
208
|
postStatus.stop();
|
|
182
|
-
|
|
183
|
-
|
|
209
|
+
await setAssistantStatus(args.channelId, args.threadTs, "");
|
|
210
|
+
if (args.onReply) {
|
|
211
|
+
await args.onReply(reply);
|
|
212
|
+
} else if (reply.text) {
|
|
213
|
+
await postSlackMessage(args.channelId, args.threadTs, reply.text);
|
|
184
214
|
}
|
|
185
|
-
|
|
186
|
-
"oauth_callback_resume_complete",
|
|
187
|
-
{},
|
|
188
|
-
{
|
|
189
|
-
"app.credential.provider": stored.provider,
|
|
190
|
-
"app.ai.outcome": reply.diagnostics.outcome,
|
|
191
|
-
"app.ai.tool_calls": reply.diagnostics.toolCalls.length
|
|
192
|
-
},
|
|
193
|
-
"Auto-resumed pending message after OAuth callback"
|
|
194
|
-
);
|
|
215
|
+
await args.onSuccess?.(reply);
|
|
195
216
|
} catch (error) {
|
|
196
217
|
postStatus.stop();
|
|
218
|
+
await setAssistantStatus(args.channelId, args.threadTs, "");
|
|
219
|
+
if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
|
|
220
|
+
await args.onAuthPause(error);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
await args.onFailure?.(error);
|
|
224
|
+
await postSlackMessage(args.channelId, args.threadTs, args.failureText);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/handlers/mcp-oauth-callback.ts
|
|
229
|
+
var CALLBACK_PAGES = {
|
|
230
|
+
missing_state: {
|
|
231
|
+
title: "Authorization failed",
|
|
232
|
+
message: "Missing state parameter.",
|
|
233
|
+
status: 400
|
|
234
|
+
},
|
|
235
|
+
provider_error: {
|
|
236
|
+
title: "Authorization failed",
|
|
237
|
+
message: "The provider returned an authorization error.",
|
|
238
|
+
status: 400
|
|
239
|
+
},
|
|
240
|
+
missing_code: {
|
|
241
|
+
title: "Authorization failed",
|
|
242
|
+
message: "Missing code parameter.",
|
|
243
|
+
status: 400
|
|
244
|
+
},
|
|
245
|
+
success: {
|
|
246
|
+
title: "Authorization complete",
|
|
247
|
+
message: "Your MCP access is connected. Junior will continue the paused request in Slack.",
|
|
248
|
+
status: 200
|
|
249
|
+
},
|
|
250
|
+
failure: {
|
|
251
|
+
title: "Authorization failed",
|
|
252
|
+
message: "Junior could not finish the authorization callback. Return to Slack and retry the original request.",
|
|
253
|
+
status: 500
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function htmlResponse(kind) {
|
|
257
|
+
const page = CALLBACK_PAGES[kind];
|
|
258
|
+
const html = `<!DOCTYPE html>
|
|
259
|
+
<html>
|
|
260
|
+
<head><title>${page.title}</title></head>
|
|
261
|
+
<body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
|
|
262
|
+
<div style="text-align: center; max-width: 480px;">
|
|
263
|
+
<h1>${page.title}</h1>
|
|
264
|
+
<p>${page.message}</p>
|
|
265
|
+
<p style="margin-top: 2rem; color: #666; font-size: 0.9em;">You can close this tab and return to Slack.</p>
|
|
266
|
+
</div>
|
|
267
|
+
</body>
|
|
268
|
+
</html>`;
|
|
269
|
+
return new Response(html, {
|
|
270
|
+
status: page.status,
|
|
271
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function extractSlackText(text, files) {
|
|
275
|
+
const message = buildSlackOutputMessage(text, files);
|
|
276
|
+
if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
|
|
277
|
+
return message.markdown;
|
|
278
|
+
}
|
|
279
|
+
if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
|
|
280
|
+
return message.raw;
|
|
281
|
+
}
|
|
282
|
+
return text;
|
|
283
|
+
}
|
|
284
|
+
async function normalizeFileUploads(files) {
|
|
285
|
+
const normalized = [];
|
|
286
|
+
for (const file of files) {
|
|
287
|
+
let data;
|
|
288
|
+
if (Buffer.isBuffer(file.data)) {
|
|
289
|
+
data = file.data;
|
|
290
|
+
} else if (file.data instanceof ArrayBuffer) {
|
|
291
|
+
data = Buffer.from(file.data);
|
|
292
|
+
} else {
|
|
293
|
+
data = Buffer.from(await file.data.arrayBuffer());
|
|
294
|
+
}
|
|
295
|
+
normalized.push({
|
|
296
|
+
data,
|
|
297
|
+
filename: file.filename
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return normalized;
|
|
301
|
+
}
|
|
302
|
+
async function deliverReplyToThread(channelId, threadTs, reply) {
|
|
303
|
+
const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
|
|
304
|
+
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
|
|
305
|
+
reply,
|
|
306
|
+
hasStreamedThreadReply: false
|
|
307
|
+
});
|
|
308
|
+
if (shouldPostThreadReply) {
|
|
309
|
+
const text = extractSlackText(
|
|
310
|
+
reply.text,
|
|
311
|
+
attachFiles === "inline" ? replyFiles : void 0
|
|
312
|
+
);
|
|
313
|
+
if (text.trim().length > 0) {
|
|
314
|
+
await postSlackMessage(channelId, threadTs, text);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (!replyFiles || attachFiles === "none") {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const files = await normalizeFileUploads(replyFiles);
|
|
321
|
+
if (files.length === 0) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
await uploadFilesToThread({
|
|
326
|
+
channelId,
|
|
327
|
+
threadTs,
|
|
328
|
+
files
|
|
329
|
+
});
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function createSlackThread(channelId, threadTs) {
|
|
334
|
+
return ThreadImpl.fromJSON({
|
|
335
|
+
_type: "chat:Thread",
|
|
336
|
+
adapterName: "slack",
|
|
337
|
+
channelId,
|
|
338
|
+
id: `slack:${channelId}:${threadTs}`,
|
|
339
|
+
isDM: channelId.startsWith("D")
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function buildDeterministicTurnId(messageId) {
|
|
343
|
+
const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
344
|
+
return `turn_${sanitized}`;
|
|
345
|
+
}
|
|
346
|
+
function getUserMessageIdForTurn(conversation, sessionId) {
|
|
347
|
+
for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
|
|
348
|
+
const message = conversation.messages[index];
|
|
349
|
+
if (message?.role !== "user") {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (buildDeterministicTurnId(message.id) === sessionId) {
|
|
353
|
+
return message.id;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
async function buildResumeConversationContext(channelId, threadTs, sessionId) {
|
|
359
|
+
const thread = createSlackThread(channelId, threadTs);
|
|
360
|
+
const conversation = coerceThreadConversationState(await thread.state);
|
|
361
|
+
const userMessageId = getUserMessageIdForTurn(conversation, sessionId);
|
|
362
|
+
return buildConversationContext(conversation, {
|
|
363
|
+
excludeMessageId: userMessageId
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
async function persistCompletedReplyState(channelId, threadTs, sessionId, reply) {
|
|
367
|
+
const thread = createSlackThread(channelId, threadTs);
|
|
368
|
+
const currentState = await thread.state;
|
|
369
|
+
const conversation = coerceThreadConversationState(currentState);
|
|
370
|
+
const artifacts = coerceThreadArtifactsState(currentState);
|
|
371
|
+
const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
|
|
372
|
+
const userMessageId = getUserMessageIdForTurn(conversation, sessionId);
|
|
373
|
+
markConversationMessage(conversation, userMessageId, {
|
|
374
|
+
replied: true,
|
|
375
|
+
skippedReason: void 0
|
|
376
|
+
});
|
|
377
|
+
upsertConversationMessage(conversation, {
|
|
378
|
+
id: generateConversationId("assistant"),
|
|
379
|
+
role: "assistant",
|
|
380
|
+
text: normalizeConversationText(reply.text) || "[empty response]",
|
|
381
|
+
createdAtMs: Date.now(),
|
|
382
|
+
author: {
|
|
383
|
+
userName: botConfig.userName,
|
|
384
|
+
isBot: true
|
|
385
|
+
},
|
|
386
|
+
meta: {
|
|
387
|
+
replied: true
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
markTurnCompleted({
|
|
391
|
+
conversation,
|
|
392
|
+
nowMs: Date.now(),
|
|
393
|
+
updateConversationStats
|
|
394
|
+
});
|
|
395
|
+
await persistThreadState(thread, {
|
|
396
|
+
artifacts: nextArtifacts,
|
|
397
|
+
conversation,
|
|
398
|
+
sandboxId: reply.sandboxId,
|
|
399
|
+
sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
async function persistFailedReplyState(channelId, threadTs, sessionId) {
|
|
403
|
+
const thread = createSlackThread(channelId, threadTs);
|
|
404
|
+
const currentState = await thread.state;
|
|
405
|
+
const conversation = coerceThreadConversationState(currentState);
|
|
406
|
+
markTurnFailed({
|
|
407
|
+
conversation,
|
|
408
|
+
nowMs: Date.now(),
|
|
409
|
+
userMessageId: getUserMessageIdForTurn(conversation, sessionId),
|
|
410
|
+
markConversationMessage,
|
|
411
|
+
updateConversationStats
|
|
412
|
+
});
|
|
413
|
+
await persistThreadState(thread, {
|
|
414
|
+
conversation
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
async function resumeAuthorizedMcpTurn(args) {
|
|
418
|
+
const { authSession, provider } = args;
|
|
419
|
+
if (!authSession.channelId || !authSession.threadTs) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const conversationContext = await buildResumeConversationContext(
|
|
423
|
+
authSession.channelId,
|
|
424
|
+
authSession.threadTs,
|
|
425
|
+
authSession.sessionId
|
|
426
|
+
);
|
|
427
|
+
await resumeAuthorizedRequest({
|
|
428
|
+
messageText: authSession.userMessage,
|
|
429
|
+
requesterUserId: authSession.userId,
|
|
430
|
+
provider,
|
|
431
|
+
channelId: authSession.channelId,
|
|
432
|
+
threadTs: authSession.threadTs,
|
|
433
|
+
connectedText: `Your ${provider} MCP access is now connected. Continuing the original request...`,
|
|
434
|
+
failureText: "MCP authorization completed, but resuming the request failed. Please retry the original command.",
|
|
435
|
+
correlation: {
|
|
436
|
+
conversationId: authSession.conversationId,
|
|
437
|
+
turnId: authSession.sessionId,
|
|
438
|
+
channelId: authSession.channelId,
|
|
439
|
+
threadTs: authSession.threadTs,
|
|
440
|
+
requesterId: authSession.userId
|
|
441
|
+
},
|
|
442
|
+
toolChannelId: authSession.toolChannelId ?? authSession.artifactState?.assistantContextChannelId ?? authSession.channelId,
|
|
443
|
+
conversationContext,
|
|
444
|
+
artifactState: authSession.artifactState,
|
|
445
|
+
configuration: authSession.configuration,
|
|
446
|
+
onReply: async (reply) => {
|
|
447
|
+
await deliverReplyToThread(
|
|
448
|
+
authSession.channelId,
|
|
449
|
+
authSession.threadTs,
|
|
450
|
+
reply
|
|
451
|
+
);
|
|
452
|
+
},
|
|
453
|
+
onSuccess: async (reply) => {
|
|
454
|
+
try {
|
|
455
|
+
await persistCompletedReplyState(
|
|
456
|
+
authSession.channelId,
|
|
457
|
+
authSession.threadTs,
|
|
458
|
+
authSession.sessionId,
|
|
459
|
+
reply
|
|
460
|
+
);
|
|
461
|
+
} catch (persistError) {
|
|
462
|
+
logException(
|
|
463
|
+
persistError,
|
|
464
|
+
"mcp_oauth_callback_resume_persist_failed",
|
|
465
|
+
{},
|
|
466
|
+
{ "app.credential.provider": provider },
|
|
467
|
+
"Failed to persist resumed MCP turn state"
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
onFailure: async (error) => {
|
|
472
|
+
logException(
|
|
473
|
+
error,
|
|
474
|
+
"mcp_oauth_callback_resume_failed",
|
|
475
|
+
{},
|
|
476
|
+
{ "app.credential.provider": provider },
|
|
477
|
+
"Failed to resume MCP-authorized turn"
|
|
478
|
+
);
|
|
479
|
+
try {
|
|
480
|
+
await persistFailedReplyState(
|
|
481
|
+
authSession.channelId,
|
|
482
|
+
authSession.threadTs,
|
|
483
|
+
authSession.sessionId
|
|
484
|
+
);
|
|
485
|
+
} catch (persistError) {
|
|
486
|
+
logException(
|
|
487
|
+
persistError,
|
|
488
|
+
"mcp_oauth_callback_resume_failure_persist_failed",
|
|
489
|
+
{},
|
|
490
|
+
{ "app.credential.provider": provider },
|
|
491
|
+
"Failed to persist failed MCP resume state"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
onAuthPause: async () => {
|
|
496
|
+
logWarn(
|
|
497
|
+
"mcp_oauth_callback_resume_reparked_for_auth",
|
|
498
|
+
{},
|
|
499
|
+
{ "app.credential.provider": provider },
|
|
500
|
+
"Resumed MCP turn requested another authorization flow"
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async function GET2(request, context) {
|
|
506
|
+
const { provider } = await context.params;
|
|
507
|
+
const url = new URL(request.url);
|
|
508
|
+
const state = url.searchParams.get("state")?.trim();
|
|
509
|
+
const code = url.searchParams.get("code")?.trim();
|
|
510
|
+
const error = url.searchParams.get("error")?.trim();
|
|
511
|
+
if (!state) {
|
|
512
|
+
return htmlResponse("missing_state");
|
|
513
|
+
}
|
|
514
|
+
if (error) {
|
|
515
|
+
return htmlResponse("provider_error");
|
|
516
|
+
}
|
|
517
|
+
if (!code) {
|
|
518
|
+
return htmlResponse("missing_code");
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const authSession = await finalizeMcpAuthorization(provider, state, code);
|
|
522
|
+
try {
|
|
523
|
+
await deleteMcpAuthSession(authSession.authSessionId);
|
|
524
|
+
} catch (cleanupError) {
|
|
525
|
+
logException(
|
|
526
|
+
cleanupError,
|
|
527
|
+
"mcp_oauth_callback_session_cleanup_failed",
|
|
528
|
+
{},
|
|
529
|
+
{ "app.credential.provider": provider },
|
|
530
|
+
"Failed to delete completed MCP auth session"
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
after(async () => {
|
|
534
|
+
await resumeAuthorizedMcpTurn({
|
|
535
|
+
authSession,
|
|
536
|
+
provider
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
return htmlResponse("success");
|
|
540
|
+
} catch (callbackError) {
|
|
197
541
|
logException(
|
|
198
|
-
|
|
199
|
-
"
|
|
542
|
+
callbackError,
|
|
543
|
+
"mcp_oauth_callback_failed",
|
|
200
544
|
{},
|
|
201
|
-
{ "app.credential.provider":
|
|
202
|
-
"Failed to
|
|
203
|
-
);
|
|
204
|
-
await postSlackMessage(
|
|
205
|
-
stored.channelId,
|
|
206
|
-
stored.threadTs,
|
|
207
|
-
`I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`
|
|
545
|
+
{ "app.credential.provider": provider },
|
|
546
|
+
"Failed to process MCP OAuth callback"
|
|
208
547
|
);
|
|
548
|
+
return htmlResponse("failure");
|
|
209
549
|
}
|
|
210
550
|
}
|
|
211
|
-
|
|
551
|
+
|
|
552
|
+
// src/handlers/oauth-callback.ts
|
|
553
|
+
import { after as after2 } from "next/server";
|
|
554
|
+
import { ThreadImpl as ThreadImpl2 } from "chat";
|
|
555
|
+
function htmlErrorResponse(title, message, status) {
|
|
556
|
+
const safeTitle = escapeXml(title);
|
|
557
|
+
const safeMessage = escapeXml(message);
|
|
558
|
+
const html = `<!DOCTYPE html>
|
|
559
|
+
<html>
|
|
560
|
+
<head><title>${safeTitle}</title></head>
|
|
561
|
+
<body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
|
|
562
|
+
<div style="text-align: center; max-width: 480px;">
|
|
563
|
+
<h1>${safeTitle}</h1>
|
|
564
|
+
<p>${safeMessage}</p>
|
|
565
|
+
<p style="margin-top: 2rem; color: #666; font-size: 0.9em;">You can close this tab and return to Slack to try again.</p>
|
|
566
|
+
</div>
|
|
567
|
+
</body>
|
|
568
|
+
</html>`;
|
|
569
|
+
return new Response(html, {
|
|
570
|
+
status,
|
|
571
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function createSlackThread2(channelId, threadTs) {
|
|
575
|
+
return ThreadImpl2.fromJSON({
|
|
576
|
+
_type: "chat:Thread",
|
|
577
|
+
adapterName: "slack",
|
|
578
|
+
channelId,
|
|
579
|
+
id: `slack:${channelId}:${threadTs}`,
|
|
580
|
+
isDM: channelId.startsWith("D")
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
async function buildResumeConversationContext2(channelId, threadTs) {
|
|
584
|
+
const thread = createSlackThread2(channelId, threadTs);
|
|
585
|
+
const conversation = coerceThreadConversationState(await thread.state);
|
|
586
|
+
const latestUserMessageId = [...conversation.messages].reverse().find((message) => message.role === "user")?.id;
|
|
587
|
+
return buildConversationContext(conversation, {
|
|
588
|
+
excludeMessageId: latestUserMessageId
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
async function resumePendingOAuthMessage(stored) {
|
|
592
|
+
if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
|
|
593
|
+
const providerLabel = formatProviderLabel(stored.provider);
|
|
594
|
+
const conversationContext = await buildResumeConversationContext2(
|
|
595
|
+
stored.channelId,
|
|
596
|
+
stored.threadTs
|
|
597
|
+
);
|
|
598
|
+
await resumeAuthorizedRequest({
|
|
599
|
+
messageText: stored.pendingMessage,
|
|
600
|
+
requesterUserId: stored.userId,
|
|
601
|
+
provider: stored.provider,
|
|
602
|
+
channelId: stored.channelId,
|
|
603
|
+
threadTs: stored.threadTs,
|
|
604
|
+
connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
|
|
605
|
+
failureText: `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`,
|
|
606
|
+
conversationContext,
|
|
607
|
+
configuration: stored.configuration,
|
|
608
|
+
onSuccess: async (reply) => {
|
|
609
|
+
logInfo(
|
|
610
|
+
"oauth_callback_resume_complete",
|
|
611
|
+
{},
|
|
612
|
+
{
|
|
613
|
+
"app.credential.provider": stored.provider,
|
|
614
|
+
"app.ai.outcome": reply.diagnostics.outcome,
|
|
615
|
+
"app.ai.tool_calls": reply.diagnostics.toolCalls.length
|
|
616
|
+
},
|
|
617
|
+
"Auto-resumed pending message after OAuth callback"
|
|
618
|
+
);
|
|
619
|
+
},
|
|
620
|
+
onFailure: async (error) => {
|
|
621
|
+
logException(
|
|
622
|
+
error,
|
|
623
|
+
"oauth_callback_resume_failed",
|
|
624
|
+
{},
|
|
625
|
+
{ "app.credential.provider": stored.provider },
|
|
626
|
+
"Failed to auto-resume pending message after OAuth callback"
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
async function GET3(request, context) {
|
|
212
632
|
const { provider } = await context.params;
|
|
213
633
|
const providerConfig = getPluginOAuthConfig(provider);
|
|
214
634
|
if (!providerConfig) {
|
|
@@ -231,7 +651,7 @@ async function GET2(request, context) {
|
|
|
231
651
|
if (errorParam === "access_denied") {
|
|
232
652
|
return htmlErrorResponse(
|
|
233
653
|
"Authorization declined",
|
|
234
|
-
`You declined the ${providerLabel} authorization request. Return to Slack and
|
|
654
|
+
`You declined the ${providerLabel} authorization request. Return to Slack and ask Junior to connect your ${providerLabel} account again if you change your mind.`,
|
|
235
655
|
400
|
|
236
656
|
);
|
|
237
657
|
}
|
|
@@ -254,7 +674,7 @@ async function GET2(request, context) {
|
|
|
254
674
|
if (!stored) {
|
|
255
675
|
return htmlErrorResponse(
|
|
256
676
|
"Link expired",
|
|
257
|
-
`This authorization link has expired (links are valid for 10 minutes). Return to Slack and ask to connect your ${providerLabel} account again
|
|
677
|
+
`This authorization link has expired (links are valid for 10 minutes). Return to Slack and ask Junior to connect your ${providerLabel} account again to get a new link.`,
|
|
258
678
|
400
|
|
259
679
|
);
|
|
260
680
|
}
|
|
@@ -327,19 +747,19 @@ async function GET2(request, context) {
|
|
|
327
747
|
500
|
|
328
748
|
);
|
|
329
749
|
}
|
|
330
|
-
const userTokenStore =
|
|
750
|
+
const userTokenStore = createUserTokenStore();
|
|
331
751
|
await userTokenStore.set(stored.userId, provider, parsedTokenResponse);
|
|
332
|
-
|
|
752
|
+
after2(async () => {
|
|
333
753
|
try {
|
|
334
754
|
await publishAppHomeView(getSlackClient(), stored.userId, userTokenStore);
|
|
335
755
|
} catch {
|
|
336
756
|
}
|
|
337
757
|
});
|
|
338
758
|
if (stored.pendingMessage && stored.channelId && stored.threadTs) {
|
|
339
|
-
|
|
759
|
+
after2(() => resumePendingOAuthMessage(stored));
|
|
340
760
|
} else if (stored.channelId && stored.threadTs) {
|
|
341
761
|
const { channelId, threadTs } = stored;
|
|
342
|
-
|
|
762
|
+
after2(async () => {
|
|
343
763
|
await postSlackMessage(
|
|
344
764
|
channelId,
|
|
345
765
|
threadTs,
|
|
@@ -365,8 +785,19 @@ async function GET2(request, context) {
|
|
|
365
785
|
}
|
|
366
786
|
|
|
367
787
|
// src/handlers/router.ts
|
|
788
|
+
function trimEdgeSlashes(value) {
|
|
789
|
+
let start = 0;
|
|
790
|
+
let end = value.length;
|
|
791
|
+
while (start < end && value[start] === "/") {
|
|
792
|
+
start += 1;
|
|
793
|
+
}
|
|
794
|
+
while (end > start && value[end - 1] === "/") {
|
|
795
|
+
end -= 1;
|
|
796
|
+
}
|
|
797
|
+
return value.slice(start, end);
|
|
798
|
+
}
|
|
368
799
|
function normalizeRoutePath(pathParts) {
|
|
369
|
-
const route = pathParts.join("/")
|
|
800
|
+
const route = trimEdgeSlashes(pathParts.join("/"));
|
|
370
801
|
return route.startsWith("api/") ? route.slice("api/".length) : route;
|
|
371
802
|
}
|
|
372
803
|
function getRoutePathParts(params) {
|
|
@@ -379,15 +810,22 @@ function getRoutePathParts(params) {
|
|
|
379
810
|
}
|
|
380
811
|
return candidate;
|
|
381
812
|
}
|
|
382
|
-
async function
|
|
813
|
+
async function GET4(request, context) {
|
|
383
814
|
const route = normalizeRoutePath(getRoutePathParts(await context.params));
|
|
384
815
|
if (route === "health") {
|
|
385
816
|
return GET();
|
|
386
817
|
}
|
|
818
|
+
const mcpOauthCallbackMatch = route.match(/^oauth\/callback\/mcp\/([^/]+)$/);
|
|
819
|
+
if (mcpOauthCallbackMatch) {
|
|
820
|
+
const provider = mcpOauthCallbackMatch[1];
|
|
821
|
+
return GET2(request, {
|
|
822
|
+
params: Promise.resolve({ provider })
|
|
823
|
+
});
|
|
824
|
+
}
|
|
387
825
|
const oauthCallbackMatch = route.match(/^oauth\/callback\/([^/]+)$/);
|
|
388
826
|
if (oauthCallbackMatch) {
|
|
389
827
|
const provider = oauthCallbackMatch[1];
|
|
390
|
-
return
|
|
828
|
+
return GET3(request, {
|
|
391
829
|
params: Promise.resolve({ provider })
|
|
392
830
|
});
|
|
393
831
|
}
|
|
@@ -408,6 +846,6 @@ async function POST3(request, context) {
|
|
|
408
846
|
return new Response("Not Found", { status: 404 });
|
|
409
847
|
}
|
|
410
848
|
export {
|
|
411
|
-
|
|
849
|
+
GET4 as GET,
|
|
412
850
|
POST3 as POST
|
|
413
851
|
};
|