@sentry/junior 0.10.3 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -17
- package/dist/app.d.ts +13 -0
- package/dist/{chunk-ASAQ64YN.js → app.js} +9490 -8761
- package/dist/chunk-2KG3PWR4.js +17 -0
- package/dist/{chunk-FNYLIUOK.js → chunk-IRE2LOEJ.js} +5 -4
- package/dist/{chunk-573MJIST.js → chunk-WELSSJJU.js} +2 -2
- package/dist/{chunk-KCLEEKYX.js → chunk-XH7TV4JS.js} +13 -5
- package/dist/chunk-Z3YD6NHK.js +12 -0
- package/dist/{chunk-AVIUX5XL.js → chunk-ZYB3U7Q4.js} +15 -13
- package/dist/cli/check.js +5 -3
- package/dist/cli/env.js +2 -0
- package/dist/cli/init.js +65 -45
- package/dist/cli/run.js +2 -0
- package/dist/cli/snapshot-warmup.js +9 -3
- package/dist/instrumentation.d.ts +3 -11
- package/dist/instrumentation.js +17 -26
- package/dist/nitro.d.ts +27 -0
- package/dist/nitro.js +62 -0
- package/package.json +16 -25
- package/dist/app/layout.d.ts +0 -11
- package/dist/app/layout.js +0 -8
- package/dist/chunk-4RBEYCOG.js +0 -12
- package/dist/handlers/health.d.ts +0 -6
- package/dist/handlers/health.js +0 -6
- package/dist/handlers/router.d.ts +0 -23
- package/dist/handlers/router.js +0 -827
- package/dist/handlers/webhooks.d.ts +0 -28
- package/dist/handlers/webhooks.js +0 -12
- package/dist/next-config.d.ts +0 -22
- package/dist/next-config.js +0 -114
package/dist/handlers/router.js
DELETED
|
@@ -1,827 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
POST,
|
|
3
|
-
buildConversationContext,
|
|
4
|
-
buildSlackOutputMessage,
|
|
5
|
-
coerceThreadArtifactsState,
|
|
6
|
-
coerceThreadConversationState,
|
|
7
|
-
createUserTokenStore,
|
|
8
|
-
deleteMcpAuthSession,
|
|
9
|
-
escapeXml,
|
|
10
|
-
finalizeMcpAuthorization,
|
|
11
|
-
formatProviderLabel,
|
|
12
|
-
generateAssistantReply,
|
|
13
|
-
generateConversationId,
|
|
14
|
-
getPersistedThreadState,
|
|
15
|
-
getSlackClient,
|
|
16
|
-
isRetryableTurnError,
|
|
17
|
-
markConversationMessage,
|
|
18
|
-
markTurnCompleted,
|
|
19
|
-
markTurnFailed,
|
|
20
|
-
maxDuration,
|
|
21
|
-
mergeArtifactsState,
|
|
22
|
-
normalizeConversationText,
|
|
23
|
-
persistThreadStateById,
|
|
24
|
-
publishAppHomeView,
|
|
25
|
-
resolveBaseUrl,
|
|
26
|
-
resolveReplyDelivery,
|
|
27
|
-
truncateStatusText,
|
|
28
|
-
updateConversationStats,
|
|
29
|
-
uploadFilesToThread,
|
|
30
|
-
upsertConversationMessage
|
|
31
|
-
} from "../chunk-ASAQ64YN.js";
|
|
32
|
-
import {
|
|
33
|
-
GET
|
|
34
|
-
} from "../chunk-4RBEYCOG.js";
|
|
35
|
-
import "../chunk-573MJIST.js";
|
|
36
|
-
import {
|
|
37
|
-
botConfig,
|
|
38
|
-
getStateAdapter
|
|
39
|
-
} from "../chunk-FNYLIUOK.js";
|
|
40
|
-
import {
|
|
41
|
-
buildOAuthTokenRequest,
|
|
42
|
-
getPluginOAuthConfig,
|
|
43
|
-
logException,
|
|
44
|
-
logInfo,
|
|
45
|
-
logWarn,
|
|
46
|
-
parseOAuthTokenResponse
|
|
47
|
-
} from "../chunk-AVIUX5XL.js";
|
|
48
|
-
import "../chunk-KCLEEKYX.js";
|
|
49
|
-
|
|
50
|
-
// src/handlers/mcp-oauth-callback.ts
|
|
51
|
-
import { Buffer } from "buffer";
|
|
52
|
-
import { after } from "next/server";
|
|
53
|
-
|
|
54
|
-
// src/handlers/oauth-resume.ts
|
|
55
|
-
function resolveReplyTimeoutMs(explicitTimeoutMs) {
|
|
56
|
-
if (typeof explicitTimeoutMs === "number" && explicitTimeoutMs > 0) {
|
|
57
|
-
return explicitTimeoutMs;
|
|
58
|
-
}
|
|
59
|
-
const raw = process.env.EVAL_AGENT_REPLY_TIMEOUT_MS?.trim();
|
|
60
|
-
if (!raw) {
|
|
61
|
-
return void 0;
|
|
62
|
-
}
|
|
63
|
-
const parsed = Number.parseInt(raw, 10);
|
|
64
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
65
|
-
}
|
|
66
|
-
async function postSlackMessage(channelId, threadTs, text) {
|
|
67
|
-
try {
|
|
68
|
-
await getSlackClient().chat.postMessage({
|
|
69
|
-
channel: channelId,
|
|
70
|
-
thread_ts: threadTs,
|
|
71
|
-
text
|
|
72
|
-
});
|
|
73
|
-
} catch {
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
async function setAssistantStatus(channelId, threadTs, status) {
|
|
77
|
-
try {
|
|
78
|
-
await getSlackClient().assistant.threads.setStatus({
|
|
79
|
-
channel_id: channelId,
|
|
80
|
-
thread_ts: threadTs,
|
|
81
|
-
status
|
|
82
|
-
});
|
|
83
|
-
} catch {
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
var STATUS_DEBOUNCE_MS = 1e3;
|
|
87
|
-
function createDebouncedStatusPoster(channelId, threadTs) {
|
|
88
|
-
let lastPostAt = 0;
|
|
89
|
-
let currentStatus = "";
|
|
90
|
-
let pendingStatus = null;
|
|
91
|
-
let pendingTimer = null;
|
|
92
|
-
let stopped = false;
|
|
93
|
-
const flush = async () => {
|
|
94
|
-
if (stopped || !pendingStatus) return;
|
|
95
|
-
const status = pendingStatus;
|
|
96
|
-
pendingStatus = null;
|
|
97
|
-
pendingTimer = null;
|
|
98
|
-
lastPostAt = Date.now();
|
|
99
|
-
currentStatus = status;
|
|
100
|
-
await setAssistantStatus(channelId, threadTs, status);
|
|
101
|
-
};
|
|
102
|
-
const post = async (status) => {
|
|
103
|
-
if (stopped) return;
|
|
104
|
-
const truncated = truncateStatusText(status);
|
|
105
|
-
if (!truncated || truncated === currentStatus) return;
|
|
106
|
-
const now = Date.now();
|
|
107
|
-
const elapsed = now - lastPostAt;
|
|
108
|
-
if (elapsed >= STATUS_DEBOUNCE_MS) {
|
|
109
|
-
if (pendingTimer) {
|
|
110
|
-
clearTimeout(pendingTimer);
|
|
111
|
-
pendingTimer = null;
|
|
112
|
-
}
|
|
113
|
-
pendingStatus = null;
|
|
114
|
-
lastPostAt = now;
|
|
115
|
-
currentStatus = truncated;
|
|
116
|
-
await setAssistantStatus(channelId, threadTs, truncated);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
pendingStatus = truncated;
|
|
120
|
-
if (!pendingTimer) {
|
|
121
|
-
pendingTimer = setTimeout(
|
|
122
|
-
() => {
|
|
123
|
-
void flush();
|
|
124
|
-
},
|
|
125
|
-
Math.max(1, STATUS_DEBOUNCE_MS - elapsed)
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
post.stop = () => {
|
|
130
|
-
stopped = true;
|
|
131
|
-
if (pendingTimer) {
|
|
132
|
-
clearTimeout(pendingTimer);
|
|
133
|
-
pendingTimer = null;
|
|
134
|
-
}
|
|
135
|
-
pendingStatus = null;
|
|
136
|
-
};
|
|
137
|
-
return post;
|
|
138
|
-
}
|
|
139
|
-
function createReadOnlyConfigService(values) {
|
|
140
|
-
const entries = Object.entries(values).map(([key, value]) => ({
|
|
141
|
-
key,
|
|
142
|
-
value,
|
|
143
|
-
scope: "conversation",
|
|
144
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
145
|
-
}));
|
|
146
|
-
return {
|
|
147
|
-
get: async (key) => entries.find((entry) => entry.key === key),
|
|
148
|
-
set: async () => {
|
|
149
|
-
throw new Error("Read-only configuration in resumed context");
|
|
150
|
-
},
|
|
151
|
-
unset: async () => false,
|
|
152
|
-
list: async ({ prefix } = {}) => entries.filter((entry) => !prefix || entry.key.startsWith(prefix)),
|
|
153
|
-
resolve: async (key) => values[key],
|
|
154
|
-
resolveValues: async ({ keys, prefix } = {}) => {
|
|
155
|
-
const filtered = {};
|
|
156
|
-
for (const [key, value] of Object.entries(values)) {
|
|
157
|
-
if (prefix && !key.startsWith(prefix)) continue;
|
|
158
|
-
if (keys && !keys.includes(key)) continue;
|
|
159
|
-
filtered[key] = value;
|
|
160
|
-
}
|
|
161
|
-
return filtered;
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
async function resumeAuthorizedRequest(args) {
|
|
166
|
-
const postStatus = createDebouncedStatusPoster(args.channelId, args.threadTs);
|
|
167
|
-
await postSlackMessage(args.channelId, args.threadTs, args.connectedText);
|
|
168
|
-
await setAssistantStatus(args.channelId, args.threadTs, "Thinking...");
|
|
169
|
-
try {
|
|
170
|
-
const generateReply = args.generateReply ?? generateAssistantReply;
|
|
171
|
-
const replyPromise = generateReply(args.messageText, {
|
|
172
|
-
assistant: { userName: botConfig.userName },
|
|
173
|
-
requester: { userId: args.requesterUserId },
|
|
174
|
-
correlation: {
|
|
175
|
-
conversationId: args.correlation?.conversationId,
|
|
176
|
-
turnId: args.correlation?.turnId,
|
|
177
|
-
channelId: args.correlation?.channelId ?? args.channelId,
|
|
178
|
-
threadTs: args.correlation?.threadTs ?? args.threadTs,
|
|
179
|
-
requesterId: args.correlation?.requesterId ?? args.requesterUserId
|
|
180
|
-
},
|
|
181
|
-
toolChannelId: args.toolChannelId,
|
|
182
|
-
conversationContext: args.conversationContext,
|
|
183
|
-
artifactState: args.artifactState,
|
|
184
|
-
configuration: args.configuration,
|
|
185
|
-
channelConfiguration: args.configuration ? createReadOnlyConfigService(args.configuration) : void 0,
|
|
186
|
-
onStatus: postStatus
|
|
187
|
-
});
|
|
188
|
-
const replyTimeoutMs = resolveReplyTimeoutMs(args.replyTimeoutMs);
|
|
189
|
-
const reply = typeof replyTimeoutMs === "number" ? await Promise.race([
|
|
190
|
-
replyPromise,
|
|
191
|
-
new Promise(
|
|
192
|
-
(_, reject) => setTimeout(
|
|
193
|
-
() => reject(
|
|
194
|
-
new Error(
|
|
195
|
-
`generateAssistantReply timed out after ${replyTimeoutMs}ms`
|
|
196
|
-
)
|
|
197
|
-
),
|
|
198
|
-
replyTimeoutMs
|
|
199
|
-
)
|
|
200
|
-
)
|
|
201
|
-
]) : await replyPromise;
|
|
202
|
-
postStatus.stop();
|
|
203
|
-
await setAssistantStatus(args.channelId, args.threadTs, "");
|
|
204
|
-
if (args.onReply) {
|
|
205
|
-
await args.onReply(reply);
|
|
206
|
-
} else if (reply.text) {
|
|
207
|
-
await postSlackMessage(args.channelId, args.threadTs, reply.text);
|
|
208
|
-
}
|
|
209
|
-
await args.onSuccess?.(reply);
|
|
210
|
-
} catch (error) {
|
|
211
|
-
postStatus.stop();
|
|
212
|
-
await setAssistantStatus(args.channelId, args.threadTs, "");
|
|
213
|
-
if (isRetryableTurnError(error, "mcp_auth_resume") && args.onAuthPause) {
|
|
214
|
-
await args.onAuthPause(error);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
await args.onFailure?.(error);
|
|
218
|
-
await postSlackMessage(args.channelId, args.threadTs, args.failureText);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// src/handlers/mcp-oauth-callback.ts
|
|
223
|
-
var CALLBACK_PAGES = {
|
|
224
|
-
missing_state: {
|
|
225
|
-
title: "Authorization failed",
|
|
226
|
-
message: "Missing state parameter.",
|
|
227
|
-
status: 400
|
|
228
|
-
},
|
|
229
|
-
provider_error: {
|
|
230
|
-
title: "Authorization failed",
|
|
231
|
-
message: "The provider returned an authorization error.",
|
|
232
|
-
status: 400
|
|
233
|
-
},
|
|
234
|
-
missing_code: {
|
|
235
|
-
title: "Authorization failed",
|
|
236
|
-
message: "Missing code parameter.",
|
|
237
|
-
status: 400
|
|
238
|
-
},
|
|
239
|
-
success: {
|
|
240
|
-
title: "Authorization complete",
|
|
241
|
-
message: "Your MCP access is connected. Junior will continue the paused request in Slack.",
|
|
242
|
-
status: 200
|
|
243
|
-
},
|
|
244
|
-
failure: {
|
|
245
|
-
title: "Authorization failed",
|
|
246
|
-
message: "Junior could not finish the authorization callback. Return to Slack and retry the original request.",
|
|
247
|
-
status: 500
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
function htmlResponse(kind) {
|
|
251
|
-
const page = CALLBACK_PAGES[kind];
|
|
252
|
-
const html = `<!DOCTYPE html>
|
|
253
|
-
<html>
|
|
254
|
-
<head><title>${page.title}</title></head>
|
|
255
|
-
<body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
|
|
256
|
-
<div style="text-align: center; max-width: 480px;">
|
|
257
|
-
<h1>${page.title}</h1>
|
|
258
|
-
<p>${page.message}</p>
|
|
259
|
-
<p style="margin-top: 2rem; color: #666; font-size: 0.9em;">You can close this tab and return to Slack.</p>
|
|
260
|
-
</div>
|
|
261
|
-
</body>
|
|
262
|
-
</html>`;
|
|
263
|
-
return new Response(html, {
|
|
264
|
-
status: page.status,
|
|
265
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
function extractSlackText(text, files) {
|
|
269
|
-
const message = buildSlackOutputMessage(text, files);
|
|
270
|
-
if (typeof message === "object" && message !== null && "markdown" in message && typeof message.markdown === "string") {
|
|
271
|
-
return message.markdown;
|
|
272
|
-
}
|
|
273
|
-
if (typeof message === "object" && message !== null && "raw" in message && typeof message.raw === "string") {
|
|
274
|
-
return message.raw;
|
|
275
|
-
}
|
|
276
|
-
return text;
|
|
277
|
-
}
|
|
278
|
-
async function normalizeFileUploads(files) {
|
|
279
|
-
const normalized = [];
|
|
280
|
-
for (const file of files) {
|
|
281
|
-
let data;
|
|
282
|
-
if (Buffer.isBuffer(file.data)) {
|
|
283
|
-
data = file.data;
|
|
284
|
-
} else if (file.data instanceof ArrayBuffer) {
|
|
285
|
-
data = Buffer.from(file.data);
|
|
286
|
-
} else {
|
|
287
|
-
data = Buffer.from(await file.data.arrayBuffer());
|
|
288
|
-
}
|
|
289
|
-
normalized.push({
|
|
290
|
-
data,
|
|
291
|
-
filename: file.filename
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
return normalized;
|
|
295
|
-
}
|
|
296
|
-
async function deliverReplyToThread(channelId, threadTs, reply) {
|
|
297
|
-
const replyFiles = reply.files && reply.files.length > 0 ? reply.files : void 0;
|
|
298
|
-
const { shouldPostThreadReply, attachFiles } = resolveReplyDelivery({
|
|
299
|
-
reply,
|
|
300
|
-
hasStreamedThreadReply: false
|
|
301
|
-
});
|
|
302
|
-
if (shouldPostThreadReply) {
|
|
303
|
-
const text = extractSlackText(
|
|
304
|
-
reply.text,
|
|
305
|
-
attachFiles === "inline" ? replyFiles : void 0
|
|
306
|
-
);
|
|
307
|
-
if (text.trim().length > 0) {
|
|
308
|
-
await postSlackMessage(channelId, threadTs, text);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
if (!replyFiles || attachFiles === "none") {
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
const files = await normalizeFileUploads(replyFiles);
|
|
315
|
-
if (files.length === 0) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
|
-
await uploadFilesToThread({
|
|
320
|
-
channelId,
|
|
321
|
-
threadTs,
|
|
322
|
-
files
|
|
323
|
-
});
|
|
324
|
-
} catch {
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
function buildDeterministicTurnId(messageId) {
|
|
328
|
-
const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
329
|
-
return `turn_${sanitized}`;
|
|
330
|
-
}
|
|
331
|
-
function getUserMessageIdForTurn(conversation, sessionId) {
|
|
332
|
-
for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
|
|
333
|
-
const message = conversation.messages[index];
|
|
334
|
-
if (message?.role !== "user") {
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
if (buildDeterministicTurnId(message.id) === sessionId) {
|
|
338
|
-
return message.id;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return void 0;
|
|
342
|
-
}
|
|
343
|
-
async function buildResumeConversationContext(channelId, threadTs, sessionId) {
|
|
344
|
-
const threadId = `slack:${channelId}:${threadTs}`;
|
|
345
|
-
const conversation = coerceThreadConversationState(
|
|
346
|
-
await getPersistedThreadState(threadId)
|
|
347
|
-
);
|
|
348
|
-
const userMessageId = getUserMessageIdForTurn(conversation, sessionId);
|
|
349
|
-
return buildConversationContext(conversation, {
|
|
350
|
-
excludeMessageId: userMessageId
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
async function persistCompletedReplyState(channelId, threadTs, sessionId, reply) {
|
|
354
|
-
const threadId = `slack:${channelId}:${threadTs}`;
|
|
355
|
-
const currentState = await getPersistedThreadState(threadId);
|
|
356
|
-
const conversation = coerceThreadConversationState(currentState);
|
|
357
|
-
const artifacts = coerceThreadArtifactsState(currentState);
|
|
358
|
-
const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : void 0;
|
|
359
|
-
const userMessageId = getUserMessageIdForTurn(conversation, sessionId);
|
|
360
|
-
markConversationMessage(conversation, userMessageId, {
|
|
361
|
-
replied: true,
|
|
362
|
-
skippedReason: void 0
|
|
363
|
-
});
|
|
364
|
-
upsertConversationMessage(conversation, {
|
|
365
|
-
id: generateConversationId("assistant"),
|
|
366
|
-
role: "assistant",
|
|
367
|
-
text: normalizeConversationText(reply.text) || "[empty response]",
|
|
368
|
-
createdAtMs: Date.now(),
|
|
369
|
-
author: {
|
|
370
|
-
userName: botConfig.userName,
|
|
371
|
-
isBot: true
|
|
372
|
-
},
|
|
373
|
-
meta: {
|
|
374
|
-
replied: true
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
markTurnCompleted({
|
|
378
|
-
conversation,
|
|
379
|
-
nowMs: Date.now(),
|
|
380
|
-
updateConversationStats
|
|
381
|
-
});
|
|
382
|
-
await persistThreadStateById(threadId, {
|
|
383
|
-
artifacts: nextArtifacts,
|
|
384
|
-
conversation,
|
|
385
|
-
sandboxId: reply.sandboxId,
|
|
386
|
-
sandboxDependencyProfileHash: reply.sandboxDependencyProfileHash
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
async function persistFailedReplyState(channelId, threadTs, sessionId) {
|
|
390
|
-
const threadId = `slack:${channelId}:${threadTs}`;
|
|
391
|
-
const currentState = await getPersistedThreadState(threadId);
|
|
392
|
-
const conversation = coerceThreadConversationState(currentState);
|
|
393
|
-
markTurnFailed({
|
|
394
|
-
conversation,
|
|
395
|
-
nowMs: Date.now(),
|
|
396
|
-
userMessageId: getUserMessageIdForTurn(conversation, sessionId),
|
|
397
|
-
markConversationMessage,
|
|
398
|
-
updateConversationStats
|
|
399
|
-
});
|
|
400
|
-
await persistThreadStateById(threadId, {
|
|
401
|
-
conversation
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
async function resumeAuthorizedMcpTurn(args) {
|
|
405
|
-
const { authSession, provider } = args;
|
|
406
|
-
if (!authSession.channelId || !authSession.threadTs) {
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
const conversationContext = await buildResumeConversationContext(
|
|
410
|
-
authSession.channelId,
|
|
411
|
-
authSession.threadTs,
|
|
412
|
-
authSession.sessionId
|
|
413
|
-
);
|
|
414
|
-
await resumeAuthorizedRequest({
|
|
415
|
-
messageText: authSession.userMessage,
|
|
416
|
-
requesterUserId: authSession.userId,
|
|
417
|
-
provider,
|
|
418
|
-
channelId: authSession.channelId,
|
|
419
|
-
threadTs: authSession.threadTs,
|
|
420
|
-
connectedText: `Your ${provider} MCP access is now connected. Continuing the original request...`,
|
|
421
|
-
failureText: "MCP authorization completed, but resuming the request failed. Please retry the original command.",
|
|
422
|
-
correlation: {
|
|
423
|
-
conversationId: authSession.conversationId,
|
|
424
|
-
turnId: authSession.sessionId,
|
|
425
|
-
channelId: authSession.channelId,
|
|
426
|
-
threadTs: authSession.threadTs,
|
|
427
|
-
requesterId: authSession.userId
|
|
428
|
-
},
|
|
429
|
-
toolChannelId: authSession.toolChannelId ?? authSession.artifactState?.assistantContextChannelId ?? authSession.channelId,
|
|
430
|
-
conversationContext,
|
|
431
|
-
artifactState: authSession.artifactState,
|
|
432
|
-
configuration: authSession.configuration,
|
|
433
|
-
onReply: async (reply) => {
|
|
434
|
-
await deliverReplyToThread(
|
|
435
|
-
authSession.channelId,
|
|
436
|
-
authSession.threadTs,
|
|
437
|
-
reply
|
|
438
|
-
);
|
|
439
|
-
},
|
|
440
|
-
onSuccess: async (reply) => {
|
|
441
|
-
try {
|
|
442
|
-
await persistCompletedReplyState(
|
|
443
|
-
authSession.channelId,
|
|
444
|
-
authSession.threadTs,
|
|
445
|
-
authSession.sessionId,
|
|
446
|
-
reply
|
|
447
|
-
);
|
|
448
|
-
} catch (persistError) {
|
|
449
|
-
logException(
|
|
450
|
-
persistError,
|
|
451
|
-
"mcp_oauth_callback_resume_persist_failed",
|
|
452
|
-
{},
|
|
453
|
-
{ "app.credential.provider": provider },
|
|
454
|
-
"Failed to persist resumed MCP turn state"
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
},
|
|
458
|
-
onFailure: async (error) => {
|
|
459
|
-
logException(
|
|
460
|
-
error,
|
|
461
|
-
"mcp_oauth_callback_resume_failed",
|
|
462
|
-
{},
|
|
463
|
-
{ "app.credential.provider": provider },
|
|
464
|
-
"Failed to resume MCP-authorized turn"
|
|
465
|
-
);
|
|
466
|
-
try {
|
|
467
|
-
await persistFailedReplyState(
|
|
468
|
-
authSession.channelId,
|
|
469
|
-
authSession.threadTs,
|
|
470
|
-
authSession.sessionId
|
|
471
|
-
);
|
|
472
|
-
} catch (persistError) {
|
|
473
|
-
logException(
|
|
474
|
-
persistError,
|
|
475
|
-
"mcp_oauth_callback_resume_failure_persist_failed",
|
|
476
|
-
{},
|
|
477
|
-
{ "app.credential.provider": provider },
|
|
478
|
-
"Failed to persist failed MCP resume state"
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
},
|
|
482
|
-
onAuthPause: async () => {
|
|
483
|
-
logWarn(
|
|
484
|
-
"mcp_oauth_callback_resume_reparked_for_auth",
|
|
485
|
-
{},
|
|
486
|
-
{ "app.credential.provider": provider },
|
|
487
|
-
"Resumed MCP turn requested another authorization flow"
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
async function GET2(request, context) {
|
|
493
|
-
const { provider } = await context.params;
|
|
494
|
-
const url = new URL(request.url);
|
|
495
|
-
const state = url.searchParams.get("state")?.trim();
|
|
496
|
-
const code = url.searchParams.get("code")?.trim();
|
|
497
|
-
const error = url.searchParams.get("error")?.trim();
|
|
498
|
-
if (!state) {
|
|
499
|
-
return htmlResponse("missing_state");
|
|
500
|
-
}
|
|
501
|
-
if (error) {
|
|
502
|
-
return htmlResponse("provider_error");
|
|
503
|
-
}
|
|
504
|
-
if (!code) {
|
|
505
|
-
return htmlResponse("missing_code");
|
|
506
|
-
}
|
|
507
|
-
try {
|
|
508
|
-
const authSession = await finalizeMcpAuthorization(provider, state, code);
|
|
509
|
-
try {
|
|
510
|
-
await deleteMcpAuthSession(authSession.authSessionId);
|
|
511
|
-
} catch (cleanupError) {
|
|
512
|
-
logException(
|
|
513
|
-
cleanupError,
|
|
514
|
-
"mcp_oauth_callback_session_cleanup_failed",
|
|
515
|
-
{},
|
|
516
|
-
{ "app.credential.provider": provider },
|
|
517
|
-
"Failed to delete completed MCP auth session"
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
after(async () => {
|
|
521
|
-
await resumeAuthorizedMcpTurn({
|
|
522
|
-
authSession,
|
|
523
|
-
provider
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
return htmlResponse("success");
|
|
527
|
-
} catch (callbackError) {
|
|
528
|
-
logException(
|
|
529
|
-
callbackError,
|
|
530
|
-
"mcp_oauth_callback_failed",
|
|
531
|
-
{},
|
|
532
|
-
{ "app.credential.provider": provider },
|
|
533
|
-
"Failed to process MCP OAuth callback"
|
|
534
|
-
);
|
|
535
|
-
return htmlResponse("failure");
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// src/handlers/oauth-callback.ts
|
|
540
|
-
import { after as after2 } from "next/server";
|
|
541
|
-
function htmlErrorResponse(title, message, status) {
|
|
542
|
-
const safeTitle = escapeXml(title);
|
|
543
|
-
const safeMessage = escapeXml(message);
|
|
544
|
-
const html = `<!DOCTYPE html>
|
|
545
|
-
<html>
|
|
546
|
-
<head><title>${safeTitle}</title></head>
|
|
547
|
-
<body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
|
|
548
|
-
<div style="text-align: center; max-width: 480px;">
|
|
549
|
-
<h1>${safeTitle}</h1>
|
|
550
|
-
<p>${safeMessage}</p>
|
|
551
|
-
<p style="margin-top: 2rem; color: #666; font-size: 0.9em;">You can close this tab and return to Slack to try again.</p>
|
|
552
|
-
</div>
|
|
553
|
-
</body>
|
|
554
|
-
</html>`;
|
|
555
|
-
return new Response(html, {
|
|
556
|
-
status,
|
|
557
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
async function buildResumeConversationContext2(channelId, threadTs) {
|
|
561
|
-
const conversation = coerceThreadConversationState(
|
|
562
|
-
await getPersistedThreadState(`slack:${channelId}:${threadTs}`)
|
|
563
|
-
);
|
|
564
|
-
const latestUserMessageId = [...conversation.messages].reverse().find((message) => message.role === "user")?.id;
|
|
565
|
-
return buildConversationContext(conversation, {
|
|
566
|
-
excludeMessageId: latestUserMessageId
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
async function resumePendingOAuthMessage(stored) {
|
|
570
|
-
if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
|
|
571
|
-
const providerLabel = formatProviderLabel(stored.provider);
|
|
572
|
-
const conversationContext = await buildResumeConversationContext2(
|
|
573
|
-
stored.channelId,
|
|
574
|
-
stored.threadTs
|
|
575
|
-
);
|
|
576
|
-
await resumeAuthorizedRequest({
|
|
577
|
-
messageText: stored.pendingMessage,
|
|
578
|
-
requesterUserId: stored.userId,
|
|
579
|
-
provider: stored.provider,
|
|
580
|
-
channelId: stored.channelId,
|
|
581
|
-
threadTs: stored.threadTs,
|
|
582
|
-
connectedText: `Your ${providerLabel} account is now connected. Processing your request...`,
|
|
583
|
-
failureText: `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`,
|
|
584
|
-
conversationContext,
|
|
585
|
-
configuration: stored.configuration,
|
|
586
|
-
onSuccess: async (reply) => {
|
|
587
|
-
logInfo(
|
|
588
|
-
"oauth_callback_resume_complete",
|
|
589
|
-
{},
|
|
590
|
-
{
|
|
591
|
-
"app.credential.provider": stored.provider,
|
|
592
|
-
"app.ai.outcome": reply.diagnostics.outcome,
|
|
593
|
-
"app.ai.tool_calls": reply.diagnostics.toolCalls.length
|
|
594
|
-
},
|
|
595
|
-
"Auto-resumed pending message after OAuth callback"
|
|
596
|
-
);
|
|
597
|
-
},
|
|
598
|
-
onFailure: async (error) => {
|
|
599
|
-
logException(
|
|
600
|
-
error,
|
|
601
|
-
"oauth_callback_resume_failed",
|
|
602
|
-
{},
|
|
603
|
-
{ "app.credential.provider": stored.provider },
|
|
604
|
-
"Failed to auto-resume pending message after OAuth callback"
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
async function GET3(request, context) {
|
|
610
|
-
const { provider } = await context.params;
|
|
611
|
-
const providerConfig = getPluginOAuthConfig(provider);
|
|
612
|
-
if (!providerConfig) {
|
|
613
|
-
return htmlErrorResponse(
|
|
614
|
-
"Unknown provider",
|
|
615
|
-
"The OAuth provider in this link is not recognized.",
|
|
616
|
-
404
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
const providerLabel = formatProviderLabel(provider);
|
|
620
|
-
const url = new URL(request.url);
|
|
621
|
-
const errorParam = url.searchParams.get("error");
|
|
622
|
-
const code = url.searchParams.get("code");
|
|
623
|
-
const state = url.searchParams.get("state");
|
|
624
|
-
if (errorParam) {
|
|
625
|
-
if (state) {
|
|
626
|
-
const cleanupAdapter = getStateAdapter();
|
|
627
|
-
await cleanupAdapter.delete(`oauth-state:${state}`);
|
|
628
|
-
}
|
|
629
|
-
if (errorParam === "access_denied") {
|
|
630
|
-
return htmlErrorResponse(
|
|
631
|
-
"Authorization declined",
|
|
632
|
-
`You declined the ${providerLabel} authorization request. Return to Slack and ask Junior to connect your ${providerLabel} account again if you change your mind.`,
|
|
633
|
-
400
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
return htmlErrorResponse(
|
|
637
|
-
"Authorization failed",
|
|
638
|
-
`${providerLabel} returned an error: ${errorParam}. Return to Slack and try again.`,
|
|
639
|
-
400
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
if (!code || !state) {
|
|
643
|
-
return htmlErrorResponse(
|
|
644
|
-
"Invalid request",
|
|
645
|
-
"This authorization link is missing required parameters.",
|
|
646
|
-
400
|
|
647
|
-
);
|
|
648
|
-
}
|
|
649
|
-
const stateAdapter = getStateAdapter();
|
|
650
|
-
const stateKey = `oauth-state:${state}`;
|
|
651
|
-
const stored = await stateAdapter.get(stateKey);
|
|
652
|
-
if (!stored) {
|
|
653
|
-
return htmlErrorResponse(
|
|
654
|
-
"Link expired",
|
|
655
|
-
`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.`,
|
|
656
|
-
400
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
if (stored.provider !== provider) {
|
|
660
|
-
return htmlErrorResponse(
|
|
661
|
-
"Provider mismatch",
|
|
662
|
-
"This authorization link does not match the expected provider.",
|
|
663
|
-
400
|
|
664
|
-
);
|
|
665
|
-
}
|
|
666
|
-
await stateAdapter.delete(stateKey);
|
|
667
|
-
const clientId = process.env[providerConfig.clientIdEnv]?.trim();
|
|
668
|
-
const clientSecret = process.env[providerConfig.clientSecretEnv]?.trim();
|
|
669
|
-
if (!clientId || !clientSecret) {
|
|
670
|
-
return htmlErrorResponse(
|
|
671
|
-
"Configuration error",
|
|
672
|
-
"OAuth client credentials are not configured on the server.",
|
|
673
|
-
500
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
const baseUrl = resolveBaseUrl();
|
|
677
|
-
if (!baseUrl) {
|
|
678
|
-
return htmlErrorResponse(
|
|
679
|
-
"Configuration error",
|
|
680
|
-
"The server cannot determine its base URL.",
|
|
681
|
-
500
|
|
682
|
-
);
|
|
683
|
-
}
|
|
684
|
-
const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
|
|
685
|
-
let tokenResponse;
|
|
686
|
-
try {
|
|
687
|
-
const tokenRequest = buildOAuthTokenRequest({
|
|
688
|
-
clientId,
|
|
689
|
-
clientSecret,
|
|
690
|
-
payload: {
|
|
691
|
-
grant_type: "authorization_code",
|
|
692
|
-
code,
|
|
693
|
-
redirect_uri: redirectUri
|
|
694
|
-
},
|
|
695
|
-
tokenAuthMethod: providerConfig.tokenAuthMethod,
|
|
696
|
-
tokenExtraHeaders: providerConfig.tokenExtraHeaders
|
|
697
|
-
});
|
|
698
|
-
tokenResponse = await fetch(providerConfig.tokenEndpoint, {
|
|
699
|
-
method: "POST",
|
|
700
|
-
headers: tokenRequest.headers,
|
|
701
|
-
body: tokenRequest.body
|
|
702
|
-
});
|
|
703
|
-
} catch {
|
|
704
|
-
return htmlErrorResponse(
|
|
705
|
-
"Connection failed",
|
|
706
|
-
"Failed to exchange the authorization code. Please try again.",
|
|
707
|
-
500
|
|
708
|
-
);
|
|
709
|
-
}
|
|
710
|
-
if (!tokenResponse.ok) {
|
|
711
|
-
return htmlErrorResponse(
|
|
712
|
-
"Connection failed",
|
|
713
|
-
"The token exchange with the provider failed. Please try again.",
|
|
714
|
-
500
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
const tokenData = await tokenResponse.json();
|
|
718
|
-
let parsedTokenResponse;
|
|
719
|
-
try {
|
|
720
|
-
parsedTokenResponse = parseOAuthTokenResponse(tokenData);
|
|
721
|
-
} catch {
|
|
722
|
-
return htmlErrorResponse(
|
|
723
|
-
"Connection failed",
|
|
724
|
-
"The provider returned an incomplete token response. Please try again.",
|
|
725
|
-
500
|
|
726
|
-
);
|
|
727
|
-
}
|
|
728
|
-
const userTokenStore = createUserTokenStore();
|
|
729
|
-
await userTokenStore.set(stored.userId, provider, parsedTokenResponse);
|
|
730
|
-
after2(async () => {
|
|
731
|
-
try {
|
|
732
|
-
await publishAppHomeView(getSlackClient(), stored.userId, userTokenStore);
|
|
733
|
-
} catch {
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
if (stored.pendingMessage && stored.channelId && stored.threadTs) {
|
|
737
|
-
after2(() => resumePendingOAuthMessage(stored));
|
|
738
|
-
} else if (stored.channelId && stored.threadTs) {
|
|
739
|
-
const { channelId, threadTs } = stored;
|
|
740
|
-
after2(async () => {
|
|
741
|
-
await postSlackMessage(
|
|
742
|
-
channelId,
|
|
743
|
-
threadTs,
|
|
744
|
-
`Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
|
|
745
|
-
);
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
const statusMessage = stored.pendingMessage ? "Your request is being processed in Slack." : "You can close this tab and return to Slack.";
|
|
749
|
-
const html = `<!DOCTYPE html>
|
|
750
|
-
<html>
|
|
751
|
-
<head><title>${providerLabel} Connected</title></head>
|
|
752
|
-
<body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
|
|
753
|
-
<div style="text-align: center;">
|
|
754
|
-
<h1>${providerLabel} account connected</h1>
|
|
755
|
-
<p>${statusMessage}</p>
|
|
756
|
-
</div>
|
|
757
|
-
</body>
|
|
758
|
-
</html>`;
|
|
759
|
-
return new Response(html, {
|
|
760
|
-
status: 200,
|
|
761
|
-
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// src/handlers/router.ts
|
|
766
|
-
function trimEdgeSlashes(value) {
|
|
767
|
-
let start = 0;
|
|
768
|
-
let end = value.length;
|
|
769
|
-
while (start < end && value[start] === "/") {
|
|
770
|
-
start += 1;
|
|
771
|
-
}
|
|
772
|
-
while (end > start && value[end - 1] === "/") {
|
|
773
|
-
end -= 1;
|
|
774
|
-
}
|
|
775
|
-
return value.slice(start, end);
|
|
776
|
-
}
|
|
777
|
-
function normalizeRoutePath(pathParts) {
|
|
778
|
-
const route = trimEdgeSlashes(pathParts.join("/"));
|
|
779
|
-
return route.startsWith("api/") ? route.slice("api/".length) : route;
|
|
780
|
-
}
|
|
781
|
-
function getRoutePathParts(params) {
|
|
782
|
-
if (!params || typeof params !== "object" || !("path" in params)) {
|
|
783
|
-
return [];
|
|
784
|
-
}
|
|
785
|
-
const candidate = params.path;
|
|
786
|
-
if (!Array.isArray(candidate) || candidate.some((segment) => typeof segment !== "string")) {
|
|
787
|
-
return [];
|
|
788
|
-
}
|
|
789
|
-
return candidate;
|
|
790
|
-
}
|
|
791
|
-
async function GET4(request, context) {
|
|
792
|
-
const route = normalizeRoutePath(getRoutePathParts(await context.params));
|
|
793
|
-
if (route === "health") {
|
|
794
|
-
return GET();
|
|
795
|
-
}
|
|
796
|
-
const mcpOauthCallbackMatch = route.match(/^oauth\/callback\/mcp\/([^/]+)$/);
|
|
797
|
-
if (mcpOauthCallbackMatch) {
|
|
798
|
-
const provider = mcpOauthCallbackMatch[1];
|
|
799
|
-
return GET2(request, {
|
|
800
|
-
params: Promise.resolve({ provider })
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
const oauthCallbackMatch = route.match(/^oauth\/callback\/([^/]+)$/);
|
|
804
|
-
if (oauthCallbackMatch) {
|
|
805
|
-
const provider = oauthCallbackMatch[1];
|
|
806
|
-
return GET3(request, {
|
|
807
|
-
params: Promise.resolve({ provider })
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
return new Response("Not Found", { status: 404 });
|
|
811
|
-
}
|
|
812
|
-
async function POST2(request, context) {
|
|
813
|
-
const route = normalizeRoutePath(getRoutePathParts(await context.params));
|
|
814
|
-
const webhookMatch = route.match(/^webhooks\/([^/]+)$/);
|
|
815
|
-
if (webhookMatch) {
|
|
816
|
-
const platform = webhookMatch[1];
|
|
817
|
-
return POST(request, {
|
|
818
|
-
params: Promise.resolve({ platform })
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
return new Response("Not Found", { status: 404 });
|
|
822
|
-
}
|
|
823
|
-
export {
|
|
824
|
-
GET4 as GET,
|
|
825
|
-
POST2 as POST,
|
|
826
|
-
maxDuration
|
|
827
|
-
};
|