@sentry/junior 0.1.0 → 0.2.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,323 +0,0 @@
1
- import {
2
- hasRedisConfig
3
- } from "./chunk-GDNDYMGX.js";
4
-
5
- // src/chat/state.ts
6
- import { createRedisState } from "@chat-adapter/state-redis";
7
- import { createMemoryState } from "@chat-adapter/state-memory";
8
- var MIN_LOCK_TTL_MS = 1e3 * 60 * 5;
9
- var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
10
- var QUEUE_MESSAGE_PROCESSING_PREFIX = "junior:queue_message";
11
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
12
- var QUEUE_MESSAGE_PROCESSING_TTL_MS = 30 * 60 * 1e3;
13
- var QUEUE_MESSAGE_COMPLETED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
14
- var QUEUE_MESSAGE_FAILED_TTL_MS = 6 * 60 * 60 * 1e3;
15
- var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
16
- var CLAIM_OR_RECLAIM_PROCESSING_SCRIPT = `
17
- local key = KEYS[1]
18
- local nowMs = tonumber(ARGV[1])
19
- local ttlMs = tonumber(ARGV[2])
20
- local payload = ARGV[3]
21
- local current = redis.call("get", key)
22
-
23
- if not current then
24
- redis.call("set", key, payload, "PX", ttlMs)
25
- return 1
26
- end
27
-
28
- local ok, parsed = pcall(cjson.decode, current)
29
- if not ok or type(parsed) ~= "table" then
30
- return 0
31
- end
32
-
33
- local status = parsed["status"]
34
- if status == "failed" then
35
- redis.call("set", key, payload, "PX", ttlMs)
36
- return 3
37
- end
38
- if status ~= "processing" then
39
- return 0
40
- end
41
-
42
- local updatedAtMs = tonumber(parsed["updatedAtMs"])
43
- if not updatedAtMs then
44
- return 0
45
- end
46
-
47
- if updatedAtMs + ttlMs < nowMs then
48
- redis.call("set", key, payload, "PX", ttlMs)
49
- return 2
50
- end
51
-
52
- return 0
53
- `;
54
- var UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT = `
55
- local key = KEYS[1]
56
- local ownerToken = ARGV[1]
57
- local ttlMs = tonumber(ARGV[2])
58
- local payload = ARGV[3]
59
- local current = redis.call("get", key)
60
-
61
- if not current then
62
- return 0
63
- end
64
-
65
- local ok, parsed = pcall(cjson.decode, current)
66
- if not ok or type(parsed) ~= "table" then
67
- return 0
68
- end
69
-
70
- local currentOwner = parsed["ownerToken"]
71
- local status = parsed["status"]
72
- if currentOwner ~= ownerToken then
73
- return 0
74
- end
75
- if status ~= "processing" then
76
- return 0
77
- end
78
-
79
- redis.call("set", key, payload, "PX", ttlMs)
80
- return 1
81
- `;
82
- function createQueuedStateAdapter(base) {
83
- const acquireLock = async (threadId, ttlMs) => {
84
- const effectiveTtlMs = Math.max(ttlMs, MIN_LOCK_TTL_MS);
85
- const lock = await base.acquireLock(threadId, effectiveTtlMs);
86
- return lock;
87
- };
88
- return {
89
- connect: () => base.connect(),
90
- disconnect: () => base.disconnect(),
91
- subscribe: (threadId) => base.subscribe(threadId),
92
- unsubscribe: (threadId) => base.unsubscribe(threadId),
93
- isSubscribed: (threadId) => base.isSubscribed(threadId),
94
- acquireLock,
95
- releaseLock: (lock) => base.releaseLock(lock),
96
- extendLock: (lock, ttlMs) => base.extendLock(lock, Math.max(ttlMs, MIN_LOCK_TTL_MS)),
97
- get: (key) => base.get(key),
98
- set: (key, value, ttlMs) => base.set(key, value, ttlMs),
99
- setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
100
- delete: (key) => base.delete(key)
101
- };
102
- }
103
- function createStateAdapter() {
104
- if (process.env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory") {
105
- _redisStateAdapter = void 0;
106
- return createQueuedStateAdapter(createMemoryState());
107
- }
108
- if (!hasRedisConfig()) {
109
- throw new Error("REDIS_URL is required for durable Slack thread state");
110
- }
111
- const redisState = createRedisState({
112
- url: process.env.REDIS_URL
113
- });
114
- _redisStateAdapter = redisState;
115
- return createQueuedStateAdapter(redisState);
116
- }
117
- var _stateAdapter;
118
- var _redisStateAdapter;
119
- function getRedisStateAdapter() {
120
- if (!_redisStateAdapter) {
121
- getStateAdapter();
122
- }
123
- if (!_redisStateAdapter) {
124
- throw new Error("Redis state adapter is unavailable for this runtime");
125
- }
126
- return _redisStateAdapter;
127
- }
128
- function queueMessageKey(rawKey) {
129
- return `${QUEUE_MESSAGE_PROCESSING_PREFIX}:${rawKey}`;
130
- }
131
- function parseQueueMessageState(value) {
132
- if (typeof value !== "string") {
133
- return void 0;
134
- }
135
- try {
136
- const parsed = JSON.parse(value);
137
- if (!parsed || parsed.status !== "processing" && parsed.status !== "completed" && parsed.status !== "failed" || typeof parsed.updatedAtMs !== "number") {
138
- return void 0;
139
- }
140
- return {
141
- status: parsed.status,
142
- updatedAtMs: parsed.updatedAtMs,
143
- ...typeof parsed.ownerToken === "string" ? { ownerToken: parsed.ownerToken } : {},
144
- ...typeof parsed.queueMessageId === "string" ? { queueMessageId: parsed.queueMessageId } : {},
145
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {}
146
- };
147
- } catch {
148
- return void 0;
149
- }
150
- }
151
- function agentTurnSessionKey(conversationId, sessionId) {
152
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
153
- }
154
- function isRecord(value) {
155
- return typeof value === "object" && value !== null;
156
- }
157
- function parseAgentTurnSessionCheckpoint(value) {
158
- if (typeof value !== "string") {
159
- return void 0;
160
- }
161
- try {
162
- const parsed = JSON.parse(value);
163
- if (!isRecord(parsed)) {
164
- return void 0;
165
- }
166
- const status = parsed.state;
167
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed") {
168
- return void 0;
169
- }
170
- const conversationId = parsed.conversationId;
171
- const sessionId = parsed.sessionId;
172
- const sliceId = parsed.sliceId;
173
- const checkpointVersion = parsed.checkpointVersion;
174
- const updatedAtMs = parsed.updatedAtMs;
175
- if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
176
- return void 0;
177
- }
178
- return {
179
- checkpointVersion,
180
- conversationId,
181
- sessionId,
182
- sliceId,
183
- state: status,
184
- updatedAtMs,
185
- piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
186
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
187
- ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
188
- };
189
- } catch {
190
- return void 0;
191
- }
192
- }
193
- function getStateAdapter() {
194
- if (!_stateAdapter) {
195
- _stateAdapter = createStateAdapter();
196
- }
197
- return _stateAdapter;
198
- }
199
- async function claimQueueIngressDedup(rawKey, ttlMs) {
200
- await getStateAdapter().connect();
201
- const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
202
- const result = await getRedisStateAdapter().getClient().set(key, "1", {
203
- NX: true,
204
- PX: ttlMs
205
- });
206
- return result === "OK";
207
- }
208
- async function hasQueueIngressDedup(rawKey) {
209
- await getStateAdapter().connect();
210
- const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
211
- const value = await getRedisStateAdapter().getClient().get(key);
212
- return typeof value === "string" && value.length > 0;
213
- }
214
- async function getQueueMessageProcessingState(rawKey) {
215
- await getStateAdapter().connect();
216
- const state = await getStateAdapter().get(queueMessageKey(rawKey));
217
- return parseQueueMessageState(state);
218
- }
219
- async function acquireQueueMessageProcessingOwnership(args) {
220
- await getStateAdapter().connect();
221
- const key = queueMessageKey(args.rawKey);
222
- const nowMs = Date.now();
223
- const payload = JSON.stringify({
224
- status: "processing",
225
- updatedAtMs: nowMs,
226
- ownerToken: args.ownerToken,
227
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
228
- });
229
- const result = await getRedisStateAdapter().getClient().eval(CLAIM_OR_RECLAIM_PROCESSING_SCRIPT, {
230
- keys: [key],
231
- arguments: [String(nowMs), String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
232
- });
233
- if (result === 1) {
234
- return "acquired";
235
- }
236
- if (result === 2) {
237
- return "reclaimed";
238
- }
239
- if (result === 3) {
240
- return "recovered";
241
- }
242
- return "blocked";
243
- }
244
- async function refreshQueueMessageProcessingOwnership(args) {
245
- await getStateAdapter().connect();
246
- const nowMs = Date.now();
247
- const payload = JSON.stringify({
248
- status: "processing",
249
- updatedAtMs: nowMs,
250
- ownerToken: args.ownerToken,
251
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
252
- });
253
- const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
254
- keys: [queueMessageKey(args.rawKey)],
255
- arguments: [args.ownerToken, String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
256
- });
257
- return result === 1;
258
- }
259
- async function completeQueueMessageProcessingOwnership(args) {
260
- await getStateAdapter().connect();
261
- const payload = JSON.stringify({
262
- status: "completed",
263
- updatedAtMs: Date.now(),
264
- ownerToken: args.ownerToken,
265
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
266
- });
267
- const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
268
- keys: [queueMessageKey(args.rawKey)],
269
- arguments: [args.ownerToken, String(QUEUE_MESSAGE_COMPLETED_TTL_MS), payload]
270
- });
271
- return result === 1;
272
- }
273
- async function failQueueMessageProcessingOwnership(args) {
274
- await getStateAdapter().connect();
275
- const payload = JSON.stringify({
276
- status: "failed",
277
- updatedAtMs: Date.now(),
278
- ownerToken: args.ownerToken,
279
- errorMessage: args.errorMessage,
280
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
281
- });
282
- const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
283
- keys: [queueMessageKey(args.rawKey)],
284
- arguments: [args.ownerToken, String(QUEUE_MESSAGE_FAILED_TTL_MS), payload]
285
- });
286
- return result === 1;
287
- }
288
- async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
289
- await getStateAdapter().connect();
290
- const value = await getStateAdapter().get(agentTurnSessionKey(conversationId, sessionId));
291
- return parseAgentTurnSessionCheckpoint(value);
292
- }
293
- async function upsertAgentTurnSessionCheckpoint(args) {
294
- await getStateAdapter().connect();
295
- const existing = await getAgentTurnSessionCheckpoint(args.conversationId, args.sessionId);
296
- const checkpoint = {
297
- checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
298
- conversationId: args.conversationId,
299
- sessionId: args.sessionId,
300
- sliceId: args.sliceId,
301
- state: args.state,
302
- updatedAtMs: Date.now(),
303
- piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
304
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
305
- ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
306
- };
307
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
308
- await getStateAdapter().set(agentTurnSessionKey(args.conversationId, args.sessionId), JSON.stringify(checkpoint), ttlMs);
309
- return checkpoint;
310
- }
311
-
312
- export {
313
- getStateAdapter,
314
- claimQueueIngressDedup,
315
- hasQueueIngressDedup,
316
- getQueueMessageProcessingState,
317
- acquireQueueMessageProcessingOwnership,
318
- refreshQueueMessageProcessingOwnership,
319
- completeQueueMessageProcessingOwnership,
320
- failQueueMessageProcessingOwnership,
321
- getAgentTurnSessionCheckpoint,
322
- upsertAgentTurnSessionCheckpoint
323
- };
@@ -1,10 +0,0 @@
1
- import {
2
- createQueueCallbackHandler,
3
- enqueueThreadMessage,
4
- getThreadMessageTopic
5
- } from "./chunk-ZA2IDPVG.js";
6
- export {
7
- createQueueCallbackHandler,
8
- enqueueThreadMessage,
9
- getThreadMessageTopic
10
- };
@@ -1,309 +0,0 @@
1
- import {
2
- escapeXml,
3
- generateAssistantReply,
4
- getOAuthProviderConfig,
5
- getUserTokenStore,
6
- publishAppHomeView,
7
- resolveBaseUrl,
8
- truncateStatusText
9
- } from "./chunk-7E56WM6K.js";
10
- import {
11
- getStateAdapter
12
- } from "./chunk-ZBFSIN6G.js";
13
- import "./chunk-MM3YNA4F.js";
14
- import {
15
- botConfig,
16
- getSlackClient
17
- } from "./chunk-GDNDYMGX.js";
18
- import {
19
- logException,
20
- logInfo
21
- } from "./chunk-BBOVH5RF.js";
22
-
23
- // app/api/oauth/callback/[provider]/route.ts
24
- import { after } from "next/server";
25
- var runtime = "nodejs";
26
- function htmlErrorResponse(title, message, status) {
27
- const safeTitle = escapeXml(title);
28
- const html = `<!DOCTYPE html>
29
- <html>
30
- <head><title>${safeTitle}</title></head>
31
- <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
32
- <div style="text-align: center; max-width: 480px;">
33
- <h1>${safeTitle}</h1>
34
- <p>${message}</p>
35
- <p style="margin-top: 2rem; color: #666; font-size: 0.9em;">You can close this tab and return to Slack to try again.</p>
36
- </div>
37
- </body>
38
- </html>`;
39
- return new Response(html, {
40
- status,
41
- headers: { "Content-Type": "text/html; charset=utf-8" }
42
- });
43
- }
44
- async function postSlackMessage(channelId, threadTs, text) {
45
- try {
46
- await getSlackClient().chat.postMessage({ channel: channelId, thread_ts: threadTs, text });
47
- } catch {
48
- }
49
- }
50
- async function setAssistantStatus(channelId, threadTs, status) {
51
- try {
52
- await getSlackClient().assistant.threads.setStatus({ channel_id: channelId, thread_ts: threadTs, status });
53
- } catch {
54
- }
55
- }
56
- var STATUS_DEBOUNCE_MS = 1e3;
57
- function createDebouncedStatusPoster(channelId, threadTs) {
58
- let lastPostAt = 0;
59
- let currentStatus = "";
60
- let pendingStatus = null;
61
- let pendingTimer = null;
62
- let stopped = false;
63
- const flush = async () => {
64
- if (stopped || !pendingStatus) return;
65
- const status = pendingStatus;
66
- pendingStatus = null;
67
- pendingTimer = null;
68
- lastPostAt = Date.now();
69
- currentStatus = status;
70
- await setAssistantStatus(channelId, threadTs, status);
71
- };
72
- const post = async (status) => {
73
- if (stopped) return;
74
- const truncated = truncateStatusText(status);
75
- if (!truncated || truncated === currentStatus) return;
76
- const now = Date.now();
77
- const elapsed = now - lastPostAt;
78
- if (elapsed >= STATUS_DEBOUNCE_MS) {
79
- if (pendingTimer) {
80
- clearTimeout(pendingTimer);
81
- pendingTimer = null;
82
- }
83
- pendingStatus = null;
84
- lastPostAt = now;
85
- currentStatus = truncated;
86
- await setAssistantStatus(channelId, threadTs, truncated);
87
- return;
88
- }
89
- pendingStatus = truncated;
90
- if (!pendingTimer) {
91
- pendingTimer = setTimeout(() => {
92
- void flush();
93
- }, Math.max(1, STATUS_DEBOUNCE_MS - elapsed));
94
- }
95
- };
96
- post.stop = () => {
97
- stopped = true;
98
- if (pendingTimer) {
99
- clearTimeout(pendingTimer);
100
- pendingTimer = null;
101
- }
102
- pendingStatus = null;
103
- };
104
- return post;
105
- }
106
- function createReadOnlyConfigService(values) {
107
- const entries = Object.entries(values).map(([key, value]) => ({
108
- key,
109
- value,
110
- scope: "conversation",
111
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
112
- }));
113
- return {
114
- get: async (key) => entries.find((e) => e.key === key),
115
- set: async () => {
116
- throw new Error("Read-only configuration in resumed context");
117
- },
118
- unset: async () => false,
119
- list: async ({ prefix } = {}) => entries.filter((e) => !prefix || e.key.startsWith(prefix)),
120
- resolve: async (key) => values[key],
121
- resolveValues: async ({ keys, prefix } = {}) => {
122
- const filtered = {};
123
- for (const [key, value] of Object.entries(values)) {
124
- if (prefix && !key.startsWith(prefix)) continue;
125
- if (keys && !keys.includes(key)) continue;
126
- filtered[key] = value;
127
- }
128
- return filtered;
129
- }
130
- };
131
- }
132
- async function resumePendingMessage(stored) {
133
- if (!stored.pendingMessage || !stored.channelId || !stored.threadTs) return;
134
- const providerLabel = stored.provider.charAt(0).toUpperCase() + stored.provider.slice(1);
135
- await postSlackMessage(
136
- stored.channelId,
137
- stored.threadTs,
138
- `Your ${providerLabel} account is now connected. Processing your request...`
139
- );
140
- const postStatus = createDebouncedStatusPoster(stored.channelId, stored.threadTs);
141
- await setAssistantStatus(stored.channelId, stored.threadTs, "Thinking...");
142
- try {
143
- const reply = await generateAssistantReply(stored.pendingMessage, {
144
- assistant: { userName: botConfig.userName },
145
- requester: { userId: stored.userId },
146
- correlation: {
147
- channelId: stored.channelId,
148
- threadTs: stored.threadTs,
149
- requesterId: stored.userId
150
- },
151
- configuration: stored.configuration,
152
- channelConfiguration: stored.configuration ? createReadOnlyConfigService(stored.configuration) : void 0,
153
- onStatus: postStatus
154
- });
155
- postStatus.stop();
156
- if (reply.text) {
157
- await postSlackMessage(stored.channelId, stored.threadTs, reply.text);
158
- }
159
- logInfo(
160
- "oauth_callback_resume_complete",
161
- {},
162
- {
163
- "app.credential.provider": stored.provider,
164
- "app.ai.outcome": reply.diagnostics.outcome,
165
- "app.ai.tool_calls": reply.diagnostics.toolCalls.length
166
- },
167
- "Auto-resumed pending message after OAuth callback"
168
- );
169
- } catch (error) {
170
- postStatus.stop();
171
- logException(
172
- error,
173
- "oauth_callback_resume_failed",
174
- {},
175
- { "app.credential.provider": stored.provider },
176
- "Failed to auto-resume pending message after OAuth callback"
177
- );
178
- await postSlackMessage(
179
- stored.channelId,
180
- stored.threadTs,
181
- `I connected your account but hit an error processing your request. Please try \`${stored.pendingMessage}\` again.`
182
- );
183
- }
184
- }
185
- async function GET(request, context) {
186
- const { provider } = await context.params;
187
- const providerConfig = getOAuthProviderConfig(provider);
188
- if (!providerConfig) {
189
- return htmlErrorResponse("Unknown provider", "The OAuth provider in this link is not recognized.", 404);
190
- }
191
- const providerLabel = provider.charAt(0).toUpperCase() + provider.slice(1);
192
- const url = new URL(request.url);
193
- const errorParam = url.searchParams.get("error");
194
- const code = url.searchParams.get("code");
195
- const state = url.searchParams.get("state");
196
- if (errorParam) {
197
- if (state) {
198
- const cleanupAdapter = getStateAdapter();
199
- await cleanupAdapter.delete(`oauth-state:${state}`);
200
- }
201
- if (errorParam === "access_denied") {
202
- return htmlErrorResponse(
203
- "Authorization declined",
204
- `You declined the ${providerLabel} authorization request. Return to Slack and run the auth command again if you change your mind.`,
205
- 400
206
- );
207
- }
208
- return htmlErrorResponse(
209
- "Authorization failed",
210
- `${providerLabel} returned an error: ${escapeXml(errorParam)}. Return to Slack and try again.`,
211
- 400
212
- );
213
- }
214
- if (!code || !state) {
215
- return htmlErrorResponse("Invalid request", "This authorization link is missing required parameters.", 400);
216
- }
217
- const stateAdapter = getStateAdapter();
218
- const stateKey = `oauth-state:${state}`;
219
- const stored = await stateAdapter.get(stateKey);
220
- if (!stored) {
221
- return htmlErrorResponse(
222
- "Link expired",
223
- `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.`,
224
- 400
225
- );
226
- }
227
- if (stored.provider !== provider) {
228
- return htmlErrorResponse("Provider mismatch", "This authorization link does not match the expected provider.", 400);
229
- }
230
- await stateAdapter.delete(stateKey);
231
- const clientId = process.env[providerConfig.clientIdEnv]?.trim();
232
- const clientSecret = process.env[providerConfig.clientSecretEnv]?.trim();
233
- if (!clientId || !clientSecret) {
234
- return htmlErrorResponse("Configuration error", "OAuth client credentials are not configured on the server.", 500);
235
- }
236
- const baseUrl = resolveBaseUrl();
237
- if (!baseUrl) {
238
- return htmlErrorResponse("Configuration error", "The server cannot determine its base URL.", 500);
239
- }
240
- const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
241
- let tokenResponse;
242
- try {
243
- tokenResponse = await fetch(providerConfig.tokenEndpoint, {
244
- method: "POST",
245
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
246
- body: new URLSearchParams({
247
- grant_type: "authorization_code",
248
- code,
249
- client_id: clientId,
250
- client_secret: clientSecret,
251
- redirect_uri: redirectUri
252
- })
253
- });
254
- } catch {
255
- return htmlErrorResponse("Connection failed", "Failed to exchange the authorization code. Please try again.", 500);
256
- }
257
- if (!tokenResponse.ok) {
258
- return htmlErrorResponse("Connection failed", "The token exchange with the provider failed. Please try again.", 500);
259
- }
260
- const tokenData = await tokenResponse.json();
261
- if (!tokenData.access_token || !tokenData.refresh_token || typeof tokenData.expires_in !== "number") {
262
- return htmlErrorResponse("Connection failed", "The provider returned an incomplete token response. Please try again.", 500);
263
- }
264
- const accessToken = tokenData.access_token;
265
- const refreshToken = tokenData.refresh_token;
266
- const expiresAt = Date.now() + tokenData.expires_in * 1e3;
267
- const userTokenStore = getUserTokenStore();
268
- await userTokenStore.set(stored.userId, provider, {
269
- accessToken,
270
- refreshToken,
271
- expiresAt
272
- });
273
- after(async () => {
274
- try {
275
- await publishAppHomeView(getSlackClient(), stored.userId, userTokenStore);
276
- } catch {
277
- }
278
- });
279
- if (stored.pendingMessage && stored.channelId && stored.threadTs) {
280
- after(() => resumePendingMessage(stored));
281
- } else if (stored.channelId && stored.threadTs) {
282
- after(async () => {
283
- await postSlackMessage(
284
- stored.channelId,
285
- stored.threadTs,
286
- `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
287
- );
288
- });
289
- }
290
- const statusMessage = stored.pendingMessage ? "Your request is being processed in Slack." : "You can close this tab and return to Slack.";
291
- const html = `<!DOCTYPE html>
292
- <html>
293
- <head><title>${providerLabel} Connected</title></head>
294
- <body style="font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0;">
295
- <div style="text-align: center;">
296
- <h1>${providerLabel} account connected</h1>
297
- <p>${statusMessage}</p>
298
- </div>
299
- </body>
300
- </html>`;
301
- return new Response(html, {
302
- status: 200,
303
- headers: { "Content-Type": "text/html; charset=utf-8" }
304
- });
305
- }
306
- export {
307
- GET,
308
- runtime
309
- };