@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.
- package/README.md +7 -56
- package/bin/junior.mjs +38 -128
- package/dist/app/layout.d.ts +3 -0
- package/dist/bot-T73QBC4J.js +16 -0
- package/dist/{chunk-OD6TOSY4.js → chunk-JDBWDYGR.js} +2 -2
- package/dist/{chunk-BBOVH5RF.js → chunk-PY4AI2GZ.js} +62 -15
- package/dist/chunk-QHDDCUTN.js +1940 -0
- package/dist/{chunk-7E56WM6K.js → chunk-RXNMJQPY.js} +8588 -6108
- package/dist/chunk-TQSDLJVE.js +272 -0
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +105 -0
- package/dist/cli/run.d.ts +11 -0
- package/dist/cli/run.js +30 -0
- package/dist/cli/snapshot-warmup.d.ts +3 -0
- package/dist/cli/snapshot-warmup.js +57 -0
- package/dist/handlers/health.d.ts +3 -0
- package/dist/handlers/queue-callback.d.ts +7 -0
- package/dist/handlers/queue-callback.js +5 -268
- package/dist/handlers/router.d.ts +18 -3
- package/dist/handlers/router.js +320 -13
- package/dist/handlers/webhooks.d.ts +13 -0
- package/dist/handlers/webhooks.js +2 -2
- package/dist/instrumentation.d.ts +6 -0
- package/dist/next-config.d.ts +8 -0
- package/dist/next-config.js +2 -0
- package/package.json +3 -8
- package/dist/bot-DLML4Z7F.js +0 -3108
- package/dist/channel-HJO33DGJ.js +0 -18
- package/dist/chunk-GDNDYMGX.js +0 -333
- package/dist/chunk-MM3YNA4F.js +0 -203
- package/dist/chunk-ZA2IDPVG.js +0 -39
- package/dist/chunk-ZBFSIN6G.js +0 -323
- package/dist/client-3GAEMIQ3.js +0 -10
- package/dist/route-XLYK6CKP.js +0 -309
package/dist/chunk-ZBFSIN6G.js
DELETED
|
@@ -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
|
-
};
|
package/dist/client-3GAEMIQ3.js
DELETED
package/dist/route-XLYK6CKP.js
DELETED
|
@@ -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
|
-
};
|