@sentry/junior 0.7.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.
@@ -1,57 +1,73 @@
1
1
  import {
2
2
  POST as POST2
3
- } from "../chunk-QHKQ2AWX.js";
3
+ } from "../chunk-I3DYWLM6.js";
4
4
  import {
5
5
  POST
6
- } from "../chunk-56WI5Q7P.js";
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
- getUserTokenStore,
20
+ isRetryableTurnError,
21
+ markConversationMessage,
22
+ markTurnCompleted,
23
+ markTurnFailed,
24
+ mergeArtifactsState,
25
+ normalizeConversationText,
26
+ persistThreadState,
13
27
  publishAppHomeView,
14
28
  resolveBaseUrl,
15
- truncateStatusText
16
- } from "../chunk-JRKU55W5.js";
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-KT5HARSN.js";
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-RKOO42TW.js";
28
- import "../chunk-Z5E25LRN.js";
47
+ } from "../chunk-ZBWWHP6Q.js";
48
+ import "../chunk-KCLEEKYX.js";
29
49
  import {
30
50
  logException,
31
- logInfo
32
- } from "../chunk-PY4AI2GZ.js";
33
- import "../chunk-VW26MOSO.js";
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
- function htmlErrorResponse(title, message, status) {
38
- const safeTitle = escapeXml(title);
39
- const safeMessage = escapeXml(message);
40
- const html = `<!DOCTYPE html>
41
- <html>
42
- <head><title>${safeTitle}</title></head>
43
- <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
44
- <div style="text-align: center; max-width: 480px;">
45
- <h1>${safeTitle}</h1>
46
- <p>${safeMessage}</p>
47
- <p style="margin-top: 2rem; color: #666; font-size: 0.9em;">You can close this tab and return to Slack to try again.</p>
48
- </div>
49
- </body>
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((e) => e.key === key),
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((e) => !prefix || e.key.startsWith(prefix)),
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 resumePendingMessage(stored) {
156
- if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
157
- const providerLabel = formatProviderLabel(stored.provider);
158
- await postSlackMessage(
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 reply = await generateAssistantReply(stored.pendingMessage, {
176
+ const generateReply = args.generateReply ?? generateAssistantReply;
177
+ const replyPromise = generateReply(args.messageText, {
170
178
  assistant: { userName: botConfig.userName },
171
- requester: { userId: stored.userId },
179
+ requester: { userId: args.requesterUserId },
172
180
  correlation: {
173
- channelId: stored.channelId,
174
- threadTs: stored.threadTs,
175
- requesterId: stored.userId
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
- configuration: stored.configuration,
178
- channelConfiguration: stored.configuration ? createReadOnlyConfigService(stored.configuration) : void 0,
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
- if (reply.text) {
183
- await postSlackMessage(stored.channelId, stored.threadTs, reply.text);
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
- logInfo(
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
- error,
199
- "oauth_callback_resume_failed",
542
+ callbackError,
543
+ "mcp_oauth_callback_failed",
200
544
  {},
201
- { "app.credential.provider": stored.provider },
202
- "Failed to auto-resume pending message after OAuth callback"
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
- async function GET2(request, context) {
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 run the auth command again if you change your mind.`,
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, or retry your original command to get a new link.`,
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 = getUserTokenStore();
750
+ const userTokenStore = createUserTokenStore();
331
751
  await userTokenStore.set(stored.userId, provider, parsedTokenResponse);
332
- after(async () => {
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
- after(() => resumePendingMessage(stored));
759
+ after2(() => resumePendingOAuthMessage(stored));
340
760
  } else if (stored.channelId && stored.threadTs) {
341
761
  const { channelId, threadTs } = stored;
342
- after(async () => {
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("/").replace(/^\/+|\/+$/g, "");
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 GET3(request, context) {
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 GET2(request, {
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
- GET3 as GET,
849
+ GET4 as GET,
412
850
  POST3 as POST
413
851
  };