@sentry/junior 0.223.0 → 0.224.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.
Files changed (52) hide show
  1. package/dist/{agent-hooks-DZU5nsd1.js → agent-hooks-DNLBwmsh.js} +6 -6
  2. package/dist/{agent-Bzbf92vv.js → agent-runner-9g6TQk9y.js} +809 -34
  3. package/dist/api.js +14 -14
  4. package/dist/app.js +1507 -59
  5. package/dist/{briefs-BJPfCrlT.js → briefs-CjXnGKzZ.js} +2 -2
  6. package/dist/{catalog-runtime-BthunUDD.js → catalog-runtime-CVZ9TEuK.js} +1 -1
  7. package/dist/{checkpoint-Deu-OVb3.js → checkpoint-Ceck_fLO.js} +7 -7
  8. package/dist/cli/chat.js +14 -15
  9. package/dist/cli/check.js +3 -3
  10. package/dist/cli/main.js +1 -1
  11. package/dist/cli/plugins.js +3 -3
  12. package/dist/cli/snapshot-create.js +3 -3
  13. package/dist/cli/upgrade.js +3 -3
  14. package/dist/{client-DjC6-6iU.js → client-B27ufhzl.js} +2 -2
  15. package/dist/{config-CtoUVs72.js → config-BAhl7-RC.js} +2 -2
  16. package/dist/{conversation-privacy-xS1t92xa.js → conversation-privacy-C-4JQzKD.js} +1 -1
  17. package/dist/credential-sync-BXHIxbtS.js +940 -0
  18. package/dist/{execution-stats-CrXs9vSH.js → execution-stats-DikPOxJ1.js} +3 -3
  19. package/dist/{executor-AOhG9X1t.js → executor-BG2Armby.js} +1 -1
  20. package/dist/instrumentation.js +2 -2
  21. package/dist/{job-queue-CNd9cpIy.js → job-queue-D4oAVrL9.js} +1 -1
  22. package/dist/{logging-DiN3o3nB.js → logging-iY0ssa2q.js} +1 -1
  23. package/dist/nitro.js +3 -3
  24. package/dist/{pending-auth-D7vmttn_.js → oauth-authorization-BIijVS2N.js} +6 -48
  25. package/dist/{oauth-callback-server-Ca0VLUEw.js → oauth-callback-server-DPkK3kzi.js} +4 -4
  26. package/dist/{profile-BsNrw8lj.js → profile-6KWhg_-U.js} +5 -9
  27. package/dist/{registry-Dankt-Pl.js → registry-Ctgdjt-W.js} +1 -1
  28. package/dist/{resolve-B38CpJe2.js → resolve-CB45ZukQ.js} +2 -2
  29. package/dist/{runner-Ck5rEjCm.js → runner-BwKCqoF3.js} +8 -9
  30. package/dist/{sandbox-egress-signals-BOfFkvly.js → sandbox-egress-signals-Cmy8ODat.js} +1 -1
  31. package/dist/{short-title-BHd_jUFX.js → short-title-xEkZwkcV.js} +2 -2
  32. package/dist/{skills-DbP4TDft.js → skills-c2bMDVRS.js} +2 -2
  33. package/dist/{spawn-DzXc_PKO.js → spawn-Cg6lyjE7.js} +2 -2
  34. package/dist/{stats-pNGp3G5q.js → stats-DP9zl3Jg.js} +6 -6
  35. package/dist/{task-queue-Cy9y1foT.js → task-queue-SL4uLfBB.js} +3 -3
  36. package/dist/{task-runner-DI1FBJFJ.js → task-runner-fntmnqzx.js} +8 -83
  37. package/dist/{thread-state-DXgxOYpZ.js → thread-state-Wfr0GdKS.js} +5 -5
  38. package/dist/{auth-pause-state-Bn2vX4PA.js → turn-failure-response-DLjSEm-R.js} +73 -72
  39. package/dist/{turn-lifecycle-Bjf6y2GO.js → turn-lifecycle-5_jvLqRz.js} +1 -1
  40. package/dist/turn-session-routing-CQaysLoe.js +77 -0
  41. package/dist/{validation-D5CIslEu.js → validation-CnZkCN7o.js} +1 -1
  42. package/dist/{version-_ZQFlslB.js → version-C6lA-h_o.js} +1 -1
  43. package/dist/version.js +1 -1
  44. package/dist/{web-authorization-6mmfS_o-.js → web-authorization-Bbx999_q.js} +1 -1
  45. package/dist/{web-input-BJpWm6go.js → web-input-BoE1xE-K.js} +7 -7
  46. package/dist/{work-DvIlJR3C.js → work-4Nhz9mVv.js} +52 -10
  47. package/dist/{worker-D1qidg-Y.js → worker-CdWPsNL2.js} +5 -5
  48. package/package.json +4 -4
  49. package/dist/agent-runner-2VQ9WhkG.js +0 -679
  50. package/dist/credential-sync-CKdXEUDO.js +0 -2811
  51. package/dist/experimental-CIXf7Uye.js +0 -46
  52. package/dist/turn-execution-BNOXrZ-M.js +0 -47
@@ -0,0 +1,940 @@
1
+ import { C as resolvePluginOAuthAccount, D as lookupSlackUserProfile, V as getMcpProviderErrorAttributes, _ as putMcpStoredOAuthCredentials, at as parseOAuthStatePayload, b as getConversationPendingAuth, d as deleteMcpAuthSession, g as getMcpStoredOAuthCredentials, gt as createUserTokenStore, h as getMcpAuthSession, it as formatProviderLabel, mt as postSlackMessage, ot as resolveBaseUrl, u as finalizeMcpAuthorization } from "./agent-runner-9g6TQk9y.js";
2
+ import { m as escapeXml } from "./client-B27ufhzl.js";
3
+ import { f as logWarn, m as runBestEffort, u as logException } from "./logging-iY0ssa2q.js";
4
+ import { n as botConfig, o as getRuntimeMetadata } from "./config-BAhl7-RC.js";
5
+ import { g as parseSlackUserId } from "./context-Bplcgypl.js";
6
+ import { Jt as getSqlExecutor, U as getSlackClient, Wt as getConversationEventStore, ln as upsertIdentity, un as upsertLinkedIdentity } from "./agent-hooks-DNLBwmsh.js";
7
+ import { i as homeDir } from "./package-discovery-7a9YqLhE.js";
8
+ import { n as buildOAuthTokenRequest, r as parseOAuthTokenResponse, u as hasRequiredOAuthScope } from "./registry-Ctgdjt-W.js";
9
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-CVZ9TEuK.js";
10
+ import { t as discoverSkills } from "./skills-c2bMDVRS.js";
11
+ import { B as recordAuthorizationCompleted, n as loadTurnCheckpoint, o as failTurnRecord, s as getTurnRecord, z as recordAuthenticationLinked } from "./checkpoint-Ceck_fLO.js";
12
+ import { E as acquireActiveLock, w as getStateAdapter } from "./profile-6KWhg_-U.js";
13
+ import { Pt as requestConversationWork, wt as ensureConversationWake } from "./stats-DP9zl3Jg.js";
14
+ import { t as getVercelConversationWorkQueue } from "./vercel-queue-DIkVcfK4.js";
15
+ import { r as getPersistedThreadState, s as coerceThreadConversationState } from "./thread-state-Wfr0GdKS.js";
16
+ import { n as resolveTurnSessionRouting } from "./turn-session-routing-CQaysLoe.js";
17
+ import { n as relayLocalOAuthCallback } from "./oauth-relay-CdCnu80W.js";
18
+ import { n as deleteWebAuthorization } from "./web-authorization-Bbx999_q.js";
19
+ import path from "node:path";
20
+ import { z } from "zod";
21
+ import { createHmac, createSecretKey, timingSafeEqual } from "node:crypto";
22
+ import { pluginStoredTokensSchema } from "@sentry/junior-plugin-api";
23
+ import fs from "node:fs";
24
+ //#region src/chat/task-execution/authorized-turn.ts
25
+ /**
26
+ * OAuth callbacks save readiness, not agent history. The conversation worker
27
+ * consumes this event under its lease before it resumes the paused Turn.
28
+ */
29
+ const authorizationSchema = z.object({
30
+ kind: z.enum(["mcp", "plugin"]),
31
+ provider: z.string().min(1),
32
+ actorId: z.string().min(1),
33
+ authorizationId: z.string().min(1),
34
+ authSessionId: z.string().optional(),
35
+ scope: z.string().optional(),
36
+ configuration: z.record(z.string(), z.unknown()).optional(),
37
+ toolChannelId: z.string().optional()
38
+ });
39
+ function authorizationKey(turn) {
40
+ return `turn:${turn.turnId}:authorization:${turn.version}`;
41
+ }
42
+ /** Save authorization for this parked boundary without changing model history. */
43
+ async function recordTurnAuthorization(args) {
44
+ const turn = await getTurnRecord(args.conversationId, args.turnId);
45
+ if (!turn || turn.state !== "paused" || turn.resumeReason !== "auth") return;
46
+ await getConversationEventStore().append(args.conversationId, [{
47
+ idempotencyKey: authorizationKey(turn),
48
+ createdAtMs: Date.now(),
49
+ data: {
50
+ type: "structured_event",
51
+ namespace: "junior",
52
+ name: "turn_authorized",
53
+ version: 1,
54
+ turnId: turn.turnId,
55
+ content: authorizationSchema.parse(args)
56
+ }
57
+ }], { activity: "preserve" });
58
+ return {
59
+ conversationId: args.conversationId,
60
+ destination: args.destination,
61
+ turnId: turn.turnId,
62
+ expectedVersion: turn.version
63
+ };
64
+ }
65
+ /** Read authorization for the exact pause; a later auth pause needs its own callback. */
66
+ async function getTurnAuthorization(turn) {
67
+ const event = await getConversationEventStore().loadByIdempotencyKey(turn.conversationId, authorizationKey(turn));
68
+ if (event?.data.type !== "structured_event") return void 0;
69
+ return authorizationSchema.parse(event.data.content);
70
+ }
71
+ //#endregion
72
+ //#region src/chat/task-execution/turn-wake.ts
73
+ /** Build the worker input for a paused turn. */
74
+ async function getPausedTurnRequest(args) {
75
+ const turn = (await loadTurnCheckpoint({
76
+ conversationId: args.conversationId,
77
+ turnId: args.turnId
78
+ })).record;
79
+ if (!turn || turn.state !== "paused" || turn.resumeReason !== "timeout" && turn.resumeReason !== "yield" && turn.resumeReason !== "retry" && !(turn.resumeReason === "auth" && await getTurnAuthorization(turn)) || turn.resumeReason === "timeout" && turn.sliceId < 2) return;
80
+ let routing;
81
+ try {
82
+ routing = await resolveTurnSessionRouting({
83
+ conversationId: args.conversationId,
84
+ conversationStore: args.conversationStore
85
+ });
86
+ } catch (error) {
87
+ await failTurnRecord({
88
+ conversationId: args.conversationId,
89
+ expectedVersion: turn.version,
90
+ turnId: args.turnId,
91
+ errorMessage: error instanceof Error ? error.message : String(error)
92
+ });
93
+ return;
94
+ }
95
+ return {
96
+ conversationId: args.conversationId,
97
+ destination: routing.destination,
98
+ turnId: args.turnId,
99
+ expectedVersion: turn.version
100
+ };
101
+ }
102
+ /** Wake the conversation worker for a paused turn. */
103
+ async function wakePausedTurn(request, options = {}) {
104
+ const nowMs = options.nowMs ?? Date.now();
105
+ await requestConversationWork({
106
+ conversationId: request.conversationId,
107
+ destination: request.destination,
108
+ nowMs,
109
+ state: options.state
110
+ });
111
+ const queue = options.queue ?? getVercelConversationWorkQueue();
112
+ await ensureConversationWake({
113
+ conversationId: request.conversationId,
114
+ idempotencyKey: [
115
+ "agent-continue",
116
+ request.conversationId,
117
+ request.turnId,
118
+ request.expectedVersion,
119
+ nowMs
120
+ ].join(":"),
121
+ nowMs,
122
+ queue,
123
+ state: options.state
124
+ });
125
+ }
126
+ /** Create paused Turn operations for one app. */
127
+ function createPausedTurns(options = {}) {
128
+ return {
129
+ get: (args) => getPausedTurnRequest({
130
+ ...args,
131
+ conversationStore: options.conversationStore
132
+ }),
133
+ wake: (request) => wakePausedTurn(request, options)
134
+ };
135
+ }
136
+ /** Record completed authorization and wake the sole execution owner. */
137
+ async function wakeAuthorizedTurn(args, queue) {
138
+ const request = await recordTurnAuthorization(args);
139
+ if (request) await wakePausedTurn(request, { queue });
140
+ }
141
+ //#endregion
142
+ //#region src/handlers/oauth-html.ts
143
+ /** Build pre-escaped callback HTML with the shared response security policy. */
144
+ function htmlCallbackResponse(title, message, status, options = {}) {
145
+ const footerMessage = options.footerMessage ?? `You can close this tab and return to ${botConfig.userName}.`;
146
+ const isSuccess = status >= 200 && status < 300;
147
+ const statusClass = isSuccess ? "success" : "error";
148
+ const statusIcon = isSuccess ? "" : "!";
149
+ const html = `<!DOCTYPE html>
150
+ <html lang="en">
151
+ <head>
152
+ <meta charset="utf-8">
153
+ <meta name="viewport" content="width=device-width, initial-scale=1">
154
+ <meta name="color-scheme" content="light dark">
155
+ <title>${title}</title>
156
+ <style>
157
+ :root {
158
+ color-scheme: light dark;
159
+ --canvas: #f4f1fa;
160
+ --surface: #ffffff;
161
+ --text: #211a2c;
162
+ --muted: #6e6479;
163
+ --border: #ded7e8;
164
+ --accent: #6c5fc7;
165
+ --success: #16835c;
166
+ --success-soft: #ddf5ea;
167
+ --error: #bc3d50;
168
+ --error-soft: #fce7eb;
169
+ --shadow: 0 24px 64px rgba(45, 31, 63, 0.14);
170
+ }
171
+
172
+ * { box-sizing: border-box; }
173
+
174
+ body {
175
+ min-height: 100vh;
176
+ margin: 0;
177
+ display: grid;
178
+ place-items: center;
179
+ padding: 24px;
180
+ background: var(--canvas);
181
+ color: var(--text);
182
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
183
+ }
184
+
185
+ main {
186
+ width: min(100%, 480px);
187
+ padding: 48px 40px 40px;
188
+ text-align: center;
189
+ background: var(--surface);
190
+ border: 1px solid var(--border);
191
+ border-radius: 24px;
192
+ box-shadow: var(--shadow);
193
+ }
194
+
195
+ .brand {
196
+ display: inline-flex;
197
+ align-items: center;
198
+ gap: 8px;
199
+ margin-bottom: 32px;
200
+ color: var(--muted);
201
+ font-size: 14px;
202
+ font-weight: 700;
203
+ letter-spacing: 0.08em;
204
+ text-transform: uppercase;
205
+ }
206
+
207
+ .brand-mark {
208
+ width: 10px;
209
+ height: 10px;
210
+ background: var(--accent);
211
+ border-radius: 3px;
212
+ transform: rotate(12deg);
213
+ }
214
+
215
+ .status {
216
+ width: 64px;
217
+ height: 64px;
218
+ margin: 0 auto 24px;
219
+ display: grid;
220
+ place-items: center;
221
+ border-radius: 20px;
222
+ font-size: 32px;
223
+ font-weight: 750;
224
+ line-height: 1;
225
+ }
226
+
227
+ .status.success { color: var(--success); background: var(--success-soft); }
228
+ .status.success::before {
229
+ content: "";
230
+ width: 12px;
231
+ height: 24px;
232
+ margin-top: -5px;
233
+ border-right: 4px solid currentColor;
234
+ border-bottom: 4px solid currentColor;
235
+ transform: rotate(45deg);
236
+ }
237
+ .status.error { color: var(--error); background: var(--error-soft); }
238
+
239
+ h1 {
240
+ margin: 0;
241
+ font-size: clamp(28px, 7vw, 36px);
242
+ line-height: 1.12;
243
+ letter-spacing: -0.035em;
244
+ }
245
+
246
+ .message {
247
+ margin: 16px auto 0;
248
+ color: var(--muted);
249
+ font-size: 17px;
250
+ line-height: 1.6;
251
+ }
252
+
253
+ .footer {
254
+ margin: 32px 0 0;
255
+ padding-top: 24px;
256
+ color: var(--muted);
257
+ border-top: 1px solid var(--border);
258
+ font-size: 14px;
259
+ line-height: 1.5;
260
+ }
261
+
262
+ @media (prefers-color-scheme: dark) {
263
+ :root {
264
+ --canvas: #15121a;
265
+ --surface: #211c28;
266
+ --text: #f6f1fb;
267
+ --muted: #b8adbf;
268
+ --border: #3a3243;
269
+ --accent: #a99af0;
270
+ --success: #72d6ad;
271
+ --success-soft: #193c30;
272
+ --error: #ff9aaa;
273
+ --error-soft: #48262e;
274
+ --shadow: 0 24px 64px rgba(0, 0, 0, 0.36);
275
+ }
276
+ }
277
+
278
+ @media (max-width: 520px) {
279
+ body { padding: 16px; }
280
+ main { padding: 40px 24px 32px; border-radius: 20px; }
281
+ }
282
+ </style>
283
+ </head>
284
+ <body>
285
+ <main>
286
+ <div class="brand"><span class="brand-mark" aria-hidden="true"></span>${botConfig.userName}</div>
287
+ <div class="status ${statusClass}" aria-hidden="true">${statusIcon}</div>
288
+ <h1>${title}</h1>
289
+ <p class="message">${message}</p>
290
+ <p class="footer">${footerMessage}</p>
291
+ </main>
292
+ </body>
293
+ </html>`;
294
+ return new Response(html, {
295
+ status,
296
+ headers: {
297
+ "Cache-Control": "no-store",
298
+ "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
299
+ "Content-Type": "text/html; charset=utf-8",
300
+ "Referrer-Policy": "no-referrer",
301
+ "X-Content-Type-Options": "nosniff"
302
+ }
303
+ });
304
+ }
305
+ //#endregion
306
+ //#region src/handlers/mcp-oauth-callback.ts
307
+ /**
308
+ * MCP OAuth callback handler.
309
+ *
310
+ * Public signed callbacks relay to the owning CLI. The loopback callback
311
+ * finalizes local MCP authorization and leaves continuation to that CLI.
312
+ * Slack callbacks save authorization readiness and wake the conversation
313
+ * worker. Only that worker can change agent history or resume the Turn.
314
+ */
315
+ function callbackPages(botName) {
316
+ return {
317
+ missing_state: {
318
+ title: "Authorization failed",
319
+ message: "Missing state parameter.",
320
+ status: 400
321
+ },
322
+ provider_error: {
323
+ title: "Authorization failed",
324
+ message: "The provider returned an authorization error.",
325
+ status: 400
326
+ },
327
+ missing_code: {
328
+ title: "Authorization failed",
329
+ message: "Missing code parameter.",
330
+ status: 400
331
+ },
332
+ expired: {
333
+ title: "Authorization expired",
334
+ message: `This authorization link is no longer active. Return to ${botName} and retry the original request.`,
335
+ status: 400
336
+ },
337
+ success: {
338
+ title: "Authorization complete",
339
+ message: `Your MCP access is connected. ${botName} will continue the paused request.`,
340
+ status: 200
341
+ },
342
+ failure: {
343
+ title: "Authorization failed",
344
+ message: `${botName} could not finish the authorization callback. Return to ${botName} and retry the original request.`,
345
+ status: 500
346
+ }
347
+ };
348
+ }
349
+ var McpOAuthAttemptExpiredError = class extends Error {
350
+ constructor() {
351
+ super("MCP OAuth authorization attempt is no longer current");
352
+ this.name = "McpOAuthAttemptExpiredError";
353
+ }
354
+ };
355
+ function mcpAuthorizationId(args) {
356
+ return `${args.sessionId}:mcp:${args.provider}`;
357
+ }
358
+ function htmlResponse(kind, options) {
359
+ const botName = botConfig.userName;
360
+ const pages = callbackPages(botName);
361
+ const page = kind === "success" && options?.local ? {
362
+ ...pages.success,
363
+ message: `Your MCP access is connected. ${botName} will continue the paused request in the local client.`
364
+ } : pages[kind];
365
+ const footerMessage = kind === "success" && !options?.local ? `You can close this tab and return to ${botName}.` : void 0;
366
+ return htmlCallbackResponse(page.title, page.message, page.status, { footerMessage });
367
+ }
368
+ /** Queue the exact MCP authorization attempt without changing thread state. */
369
+ async function wakeAuthorizedMcpTurn(args) {
370
+ const { authSession, provider } = args;
371
+ if (!authSession.destination) return;
372
+ await wakeAuthorizedTurn({
373
+ conversationId: authSession.conversationId,
374
+ destination: authSession.destination,
375
+ turnId: authSession.sessionId,
376
+ kind: "mcp",
377
+ provider,
378
+ actorId: authSession.userId,
379
+ authSessionId: authSession.authSessionId,
380
+ configuration: authSession.configuration,
381
+ toolChannelId: authSession.toolChannelId,
382
+ authorizationId: mcpAuthorizationId({
383
+ provider,
384
+ sessionId: authSession.sessionId
385
+ })
386
+ }, args.queue);
387
+ }
388
+ async function isCurrentMcpAuthorizationAttempt(authSession, provider) {
389
+ const conversationId = mcpConversationId(authSession);
390
+ if (!conversationId) return false;
391
+ const currentState = await getPersistedThreadState(conversationId);
392
+ const conversation = coerceThreadConversationState(currentState);
393
+ const pendingAuth = getConversationPendingAuth({
394
+ conversation,
395
+ kind: "mcp",
396
+ provider,
397
+ actorId: authSession.userId
398
+ });
399
+ return pendingAuth?.authSessionId === authSession.authSessionId && pendingAuth.sessionId === authSession.sessionId;
400
+ }
401
+ function mcpConversationId(authSession) {
402
+ if (authSession.destination?.platform === "local" || authSession.source?.kind === "web") return authSession.conversationId;
403
+ if (authSession.channelId && authSession.threadTs) return `slack:${authSession.channelId}:${authSession.threadTs}`;
404
+ }
405
+ /** Commit one shared credential mutation only while this attempt owns the thread. */
406
+ async function runCurrentMcpCredentialMutation(authSession, provider, mutation) {
407
+ const conversationId = mcpConversationId(authSession);
408
+ if (!conversationId) throw new McpOAuthAttemptExpiredError();
409
+ const stateAdapter = getStateAdapter();
410
+ await stateAdapter.connect();
411
+ const lock = await acquireActiveLock(stateAdapter, conversationId);
412
+ if (!lock) throw new Error(`Could not acquire MCP OAuth callback lock for ${conversationId}`);
413
+ try {
414
+ if (!await isCurrentMcpAuthorizationAttempt(authSession, provider)) throw new McpOAuthAttemptExpiredError();
415
+ return await mutation();
416
+ } finally {
417
+ await stateAdapter.releaseLock(lock);
418
+ }
419
+ }
420
+ /** Exchange MCP credentials and queue the authorized Turn before reporting success. */
421
+ async function GET$1(request, provider, waitUntil, options) {
422
+ const localRelay = await relayLocalOAuthCallback(request);
423
+ if (localRelay) return localRelay;
424
+ const url = new URL(request.url);
425
+ const state = url.searchParams.get("state")?.trim();
426
+ const code = url.searchParams.get("code")?.trim();
427
+ const error = url.searchParams.get("error")?.trim();
428
+ if (!state) return htmlResponse("missing_state");
429
+ if (error) {
430
+ try {
431
+ const pendingSession = await getMcpAuthSession(state);
432
+ if (pendingSession && pendingSession.provider === provider) {
433
+ try {
434
+ await runCurrentMcpCredentialMutation(pendingSession, provider, async () => {
435
+ const credentials = await getMcpStoredOAuthCredentials(pendingSession.userId, provider);
436
+ if (!credentials?.clientInformation && !credentials?.discoveryState) return;
437
+ const nextCredentials = credentials.tokens ? { tokens: credentials.tokens } : {};
438
+ await putMcpStoredOAuthCredentials(pendingSession.userId, provider, nextCredentials);
439
+ });
440
+ } catch (cleanupError) {
441
+ if (!(cleanupError instanceof McpOAuthAttemptExpiredError)) logException(cleanupError, "mcp.oauth_callback.provider_error_cleanup.failed", { "app.credential.provider": provider });
442
+ }
443
+ await deleteMcpAuthSession(pendingSession.authSessionId);
444
+ }
445
+ } catch (cleanupError) {
446
+ logException(cleanupError, "mcp.oauth_callback.provider_error_cleanup.failed", { "app.credential.provider": provider });
447
+ }
448
+ return htmlResponse("provider_error");
449
+ }
450
+ if (!code) return htmlResponse("missing_code");
451
+ try {
452
+ const pendingSession = await getMcpAuthSession(state);
453
+ if (!pendingSession || pendingSession.provider !== provider || !await isCurrentMcpAuthorizationAttempt(pendingSession, provider)) {
454
+ if (pendingSession) await deleteMcpAuthSession(pendingSession.authSessionId);
455
+ return htmlResponse("expired");
456
+ }
457
+ const authSession = await finalizeMcpAuthorization(provider, state, code, async (mutation) => await runCurrentMcpCredentialMutation(pendingSession, provider, mutation));
458
+ let providerLabel;
459
+ try {
460
+ providerLabel = formatProviderLabel(provider);
461
+ } catch {
462
+ providerLabel = void 0;
463
+ }
464
+ try {
465
+ await recordAuthenticationLinked({
466
+ conversationId: authSession.conversationId,
467
+ provider,
468
+ actorId: authSession.userId,
469
+ authorizationId: mcpAuthorizationId({
470
+ provider,
471
+ sessionId: authSession.sessionId
472
+ }),
473
+ turnId: authSession.sessionId,
474
+ ...providerLabel ? { providerLabel } : void 0
475
+ });
476
+ } catch (error) {
477
+ logException(error, "mcp.oauth_callback.authentication_event.failed", { "app.credential.provider": provider });
478
+ }
479
+ try {
480
+ await deleteMcpAuthSession(authSession.authSessionId);
481
+ } catch (cleanupError) {
482
+ logException(cleanupError, "mcp.oauth_callback.session_cleanup.failed", { "app.credential.provider": provider });
483
+ }
484
+ if (authSession.source?.kind === "web" && authSession.destination) waitUntil(async () => {
485
+ const turn = await getTurnRecord(authSession.conversationId, authSession.sessionId);
486
+ await deleteWebAuthorization({
487
+ actorId: authSession.userId,
488
+ conversationId: authSession.conversationId
489
+ });
490
+ if (!turn || turn.state !== "paused" || turn.resumeReason !== "auth") return;
491
+ await recordAuthorizationCompleted({
492
+ conversationId: authSession.conversationId,
493
+ kind: "mcp",
494
+ provider,
495
+ actorId: authSession.userId,
496
+ authorizationId: mcpAuthorizationId({
497
+ provider,
498
+ sessionId: authSession.sessionId
499
+ })
500
+ });
501
+ await wakePausedTurn({
502
+ conversationId: authSession.conversationId,
503
+ destination: authSession.destination,
504
+ turnId: authSession.sessionId,
505
+ expectedVersion: turn.version
506
+ }, options.conversationWorkQueue ? { queue: options.conversationWorkQueue } : void 0);
507
+ });
508
+ else if (authSession.destination?.platform !== "local") await wakeAuthorizedMcpTurn({
509
+ authSession,
510
+ queue: options.conversationWorkQueue,
511
+ provider
512
+ });
513
+ return htmlResponse("success", { local: authSession.destination?.platform === "local" && authSession.source?.kind !== "web" });
514
+ } catch (callbackError) {
515
+ if (callbackError instanceof McpOAuthAttemptExpiredError) {
516
+ await deleteMcpAuthSession(state);
517
+ return htmlResponse("expired");
518
+ }
519
+ logException(callbackError, "mcp.oauth_callback.failed", {
520
+ "app.credential.provider": provider,
521
+ ...getMcpProviderErrorAttributes(callbackError)
522
+ });
523
+ return htmlResponse("failure");
524
+ }
525
+ }
526
+ //#endregion
527
+ //#region src/chat/slack/app-home.ts
528
+ const DEFAULT_DESCRIPTION_TEXT = "I help your team investigate, summarize, and act on work in Slack.";
529
+ const MAX_HOME_SKILLS = 6;
530
+ const MAX_SECTION_TEXT_CHARS = 3e3;
531
+ const HIDDEN_HOME_SKILLS = /* @__PURE__ */ new Set(["jr-rpc"]);
532
+ function clampSectionText(text) {
533
+ if (text.length <= MAX_SECTION_TEXT_CHARS) return text;
534
+ return `${text.slice(0, 2999)}…`;
535
+ }
536
+ function loadDescriptionText() {
537
+ const descriptionPath = path.join(homeDir(), "DESCRIPTION.md");
538
+ try {
539
+ const raw = fs.readFileSync(descriptionPath, "utf8").trim();
540
+ if (raw.length > 0) return clampSectionText(raw);
541
+ } catch {}
542
+ return DEFAULT_DESCRIPTION_TEXT;
543
+ }
544
+ async function buildSkillsSummaryText() {
545
+ const skills = (await discoverSkills()).filter((skill) => !HIDDEN_HOME_SKILLS.has(skill.name));
546
+ if (skills.length === 0) return "No skills installed.";
547
+ const visible = skills.slice(0, MAX_HOME_SKILLS);
548
+ const lines = visible.map((skill) => `• *${skill.name}* — ${skill.description}`);
549
+ if (skills.length > visible.length) lines.push(`• …and ${skills.length - visible.length} more`);
550
+ return lines.join("\n");
551
+ }
552
+ function accountLabel(account) {
553
+ const label = account.label ?? account.id;
554
+ return account.url ? `<${account.url}|${label}>` : label;
555
+ }
556
+ function connectedAccountText(plugin, account) {
557
+ return account ? `*${plugin.manifest.name}*\nConnected as ${accountLabel(account)}` : `*${plugin.manifest.name}*\n${plugin.manifest.description}`;
558
+ }
559
+ async function connectedOAuthTokens(userId, plugin, userTokenStore) {
560
+ if (plugin.manifest.oauth || plugin.manifest.credentials) {
561
+ const stored = await userTokenStore.get(userId, plugin.manifest.name);
562
+ return stored && hasRequiredOAuthScope(stored.scope, plugin.manifest.oauth?.scope) ? stored : void 0;
563
+ }
564
+ }
565
+ async function hasConnectedMcpAccount(userId, plugin) {
566
+ if (!plugin.manifest.mcp) return false;
567
+ const credentials = await getMcpStoredOAuthCredentials(userId, plugin.manifest.name);
568
+ return Boolean(credentials?.tokens || credentials?.clientInformation || credentials?.discoveryState);
569
+ }
570
+ /** Build the Slack App Home tab view showing skills, connected accounts, and version. */
571
+ async function buildHomeView(userId, userTokenStore) {
572
+ const runtimeMetadata = getRuntimeMetadata();
573
+ const descriptionText = loadDescriptionText();
574
+ const skillsSummaryText = await buildSkillsSummaryText();
575
+ const providers = pluginCatalogRuntime.getProviders();
576
+ const connectedSections = [];
577
+ for (const plugin of providers) {
578
+ const tokens = await connectedOAuthTokens(userId, plugin, userTokenStore);
579
+ if (!tokens && !await hasConnectedMcpAccount(userId, plugin)) continue;
580
+ connectedSections.push({
581
+ type: "section",
582
+ text: {
583
+ type: "mrkdwn",
584
+ text: connectedAccountText(plugin, tokens?.account)
585
+ },
586
+ accessory: {
587
+ type: "button",
588
+ text: {
589
+ type: "plain_text",
590
+ text: "Unlink"
591
+ },
592
+ action_id: "app_home_disconnect",
593
+ value: plugin.manifest.name,
594
+ style: "danger"
595
+ }
596
+ });
597
+ }
598
+ const accountBlocks = connectedSections.length > 0 ? connectedSections : [{
599
+ type: "section",
600
+ text: {
601
+ type: "mrkdwn",
602
+ text: "No connected accounts"
603
+ }
604
+ }];
605
+ return {
606
+ type: "home",
607
+ blocks: [
608
+ {
609
+ type: "header",
610
+ text: {
611
+ type: "plain_text",
612
+ text: "Junior"
613
+ }
614
+ },
615
+ {
616
+ type: "section",
617
+ text: {
618
+ type: "mrkdwn",
619
+ text: descriptionText
620
+ }
621
+ },
622
+ { type: "divider" },
623
+ {
624
+ type: "header",
625
+ text: {
626
+ type: "plain_text",
627
+ text: "What I can help with"
628
+ }
629
+ },
630
+ {
631
+ type: "section",
632
+ text: {
633
+ type: "mrkdwn",
634
+ text: skillsSummaryText
635
+ }
636
+ },
637
+ { type: "divider" },
638
+ {
639
+ type: "header",
640
+ text: {
641
+ type: "plain_text",
642
+ text: "Connected accounts"
643
+ }
644
+ },
645
+ ...accountBlocks,
646
+ {
647
+ type: "context",
648
+ elements: [{
649
+ type: "mrkdwn",
650
+ text: `*junior version:* \`${runtimeMetadata.version ?? "unknown"}\``
651
+ }]
652
+ }
653
+ ]
654
+ };
655
+ }
656
+ /** Publish the App Home view to a specific Slack user. */
657
+ async function publishAppHomeView(slackClient, userId, userTokenStore) {
658
+ const view = await buildHomeView(userId, userTokenStore);
659
+ await slackClient.views.publish({
660
+ user_id: userId,
661
+ view
662
+ });
663
+ }
664
+ //#endregion
665
+ //#region src/handlers/oauth-callback.ts
666
+ /**
667
+ * OAuth callbacks exchange and store credentials. Public signed callbacks
668
+ * relay to the owning CLI, which controls local continuation. Slack callbacks
669
+ * save readiness and wake the conversation worker. They do not run the agent
670
+ * or change its history.
671
+ */
672
+ function htmlErrorResponse(title, message, status) {
673
+ return htmlCallbackResponse(escapeXml(title), escapeXml(message), status);
674
+ }
675
+ function oauthTokenErrorAttributes(provider, endpoint, status) {
676
+ return {
677
+ "app.credential.provider": provider,
678
+ "app.oauth.error.phase": "token_exchange",
679
+ "server.address": new URL(endpoint).hostname,
680
+ ...status !== void 0 ? { "http.response.status_code": status } : void 0
681
+ };
682
+ }
683
+ function pluginAuthorizationId(args) {
684
+ return `${args.sessionId}:plugin:${args.provider}`;
685
+ }
686
+ /** Queue the pending Turn, including a newer Turn that reused this auth link. */
687
+ async function wakeAuthorizedPluginTurn(stored, options) {
688
+ if (!stored.resumeConversationId || !stored.resumeSessionId || !stored.destination) return;
689
+ const conversation = coerceThreadConversationState(await getPersistedThreadState(stored.resumeConversationId));
690
+ const turnId = getConversationPendingAuth({
691
+ conversation,
692
+ kind: "plugin",
693
+ provider: stored.provider,
694
+ actorId: stored.userId,
695
+ ...stored.scope ? { scope: stored.scope } : void 0
696
+ })?.sessionId ?? stored.resumeSessionId;
697
+ await wakeAuthorizedTurn({
698
+ conversationId: stored.resumeConversationId,
699
+ destination: stored.destination,
700
+ turnId,
701
+ kind: "plugin",
702
+ provider: stored.provider,
703
+ actorId: stored.userId,
704
+ scope: stored.scope,
705
+ authorizationId: pluginAuthorizationId({
706
+ provider: stored.provider,
707
+ sessionId: turnId
708
+ })
709
+ }, options.conversationWorkQueue);
710
+ }
711
+ /** Complete provider authorization and queue any paused Slack Turn. */
712
+ async function GET(request, provider, waitUntil, options) {
713
+ const localRelay = await relayLocalOAuthCallback(request);
714
+ if (localRelay) return localRelay;
715
+ const providerConfig = pluginCatalogRuntime.getOAuthConfig(provider);
716
+ if (!providerConfig) return htmlErrorResponse("Unknown provider", "The OAuth provider in this link is not recognized.", 404);
717
+ const providerLabel = formatProviderLabel(provider);
718
+ const url = new URL(request.url);
719
+ const errorParam = url.searchParams.get("error");
720
+ const code = url.searchParams.get("code");
721
+ const state = url.searchParams.get("state");
722
+ if (errorParam) {
723
+ if (state) await getStateAdapter().delete(`oauth-state:${state}`);
724
+ if (errorParam === "access_denied") return htmlErrorResponse("Authorization declined", `You declined the ${providerLabel} authorization request. Return to Junior and connect your ${providerLabel} account again if you change your mind.`, 400);
725
+ return htmlErrorResponse("Authorization failed", `${providerLabel} returned an error: ${errorParam}. Return to Junior and try again.`, 400);
726
+ }
727
+ if (!code || !state) return htmlErrorResponse("Invalid request", "This authorization link is missing required parameters.", 400);
728
+ const stateAdapter = getStateAdapter();
729
+ const stateKey = `oauth-state:${state}`;
730
+ const stored = parseOAuthStatePayload(await stateAdapter.get(stateKey));
731
+ if (!stored) return htmlErrorResponse("Link expired", `This authorization link has expired (links are valid for 10 minutes). Return to Junior and connect your ${providerLabel} account again to get a new link.`, 400);
732
+ if (stored.provider !== provider) return htmlErrorResponse("Provider mismatch", "This authorization link does not match the expected provider.", 400);
733
+ await stateAdapter.delete(stateKey);
734
+ const clientId = process.env[providerConfig.clientIdEnv]?.trim();
735
+ const clientSecret = process.env[providerConfig.clientSecretEnv]?.trim();
736
+ if (!clientId || !clientSecret) return htmlErrorResponse("Configuration error", "OAuth client credentials are not configured on the server.", 500);
737
+ const baseUrl = resolveBaseUrl();
738
+ if (!baseUrl) return htmlErrorResponse("Configuration error", "The server cannot determine its base URL.", 500);
739
+ const redirectUri = `${baseUrl}${providerConfig.callbackPath}`;
740
+ const requestedScope = stored.scope ?? providerConfig.scope;
741
+ let tokenResponse;
742
+ try {
743
+ const tokenRequest = buildOAuthTokenRequest({
744
+ clientId,
745
+ clientSecret,
746
+ payload: {
747
+ grant_type: "authorization_code",
748
+ code,
749
+ redirect_uri: redirectUri
750
+ },
751
+ tokenAuthMethod: providerConfig.tokenAuthMethod,
752
+ tokenExtraHeaders: providerConfig.tokenExtraHeaders
753
+ });
754
+ tokenResponse = await fetch(providerConfig.tokenEndpoint, {
755
+ method: "POST",
756
+ headers: tokenRequest.headers,
757
+ body: tokenRequest.body
758
+ });
759
+ } catch {
760
+ logWarn("oauth.token_exchange.failed", oauthTokenErrorAttributes(provider, providerConfig.tokenEndpoint));
761
+ return htmlErrorResponse("Connection failed", "Failed to exchange the authorization code. Please try again.", 500);
762
+ }
763
+ if (!tokenResponse.ok) {
764
+ logWarn("oauth.token_exchange.failed", oauthTokenErrorAttributes(provider, providerConfig.tokenEndpoint, tokenResponse.status));
765
+ return htmlErrorResponse("Connection failed", "The token exchange with the provider failed. Please try again.", 500);
766
+ }
767
+ let parsedTokenResponse;
768
+ try {
769
+ const tokenData = await tokenResponse.json();
770
+ parsedTokenResponse = parseOAuthTokenResponse(tokenData, requestedScope, { treatEmptyScopeAsUnreported: providerConfig.treatEmptyScopeAsUnreported });
771
+ } catch {
772
+ logWarn("oauth.token_exchange.failed", oauthTokenErrorAttributes(provider, providerConfig.tokenEndpoint, tokenResponse.status));
773
+ return htmlErrorResponse("Connection failed", "The provider returned an incomplete token response. Please try again.", 500);
774
+ }
775
+ if (!hasRequiredOAuthScope(parsedTokenResponse.scope, requestedScope)) return htmlErrorResponse("Connection failed", `The ${providerLabel} authorization did not grant the access Junior requires. Return to Junior and connect your ${providerLabel} account again.`, 400);
776
+ const userTokenStore = createUserTokenStore();
777
+ let account;
778
+ try {
779
+ account = await resolvePluginOAuthAccount({
780
+ provider,
781
+ tokens: parsedTokenResponse
782
+ });
783
+ } catch {
784
+ return htmlErrorResponse("Connection failed", `Junior could not verify the connected ${providerLabel} account. Please try again.`, 500);
785
+ }
786
+ await userTokenStore.set(stored.userId, provider, {
787
+ ...parsedTokenResponse,
788
+ ...account ? { account } : void 0
789
+ });
790
+ const slackActor = stored.actor?.platform === "slack" ? stored.actor : void 0;
791
+ if (account && slackActor) await runBestEffort(async () => {
792
+ const slackUserId = parseSlackUserId(slackActor.userId);
793
+ if (!slackUserId) throw new Error("OAuth Slack actor user id is invalid");
794
+ const profile = await lookupSlackUserProfile(slackUserId);
795
+ const slackIdentity = await upsertIdentity(getSqlExecutor(), {
796
+ kind: "user",
797
+ provider: "slack",
798
+ providerTenantId: slackActor.teamId,
799
+ providerSubjectId: profile.id,
800
+ displayName: profile.real_name || profile.display_name,
801
+ handle: profile.name,
802
+ ...profile.email ? {
803
+ email: profile.email,
804
+ emailVerified: true
805
+ } : void 0
806
+ });
807
+ if (!slackIdentity.userId) throw new Error("OAuth Slack identity is not linked to a user");
808
+ await upsertLinkedIdentity(getSqlExecutor(), slackIdentity.userId, {
809
+ kind: "user",
810
+ provider,
811
+ providerSubjectId: account.id,
812
+ displayName: account.displayName,
813
+ handle: account.handle ?? account.label
814
+ });
815
+ }, "oauth.callback.identity_link.failed", { "app.credential.provider": provider });
816
+ const resumeConversationId = stored.resumeConversationId;
817
+ if (resumeConversationId) await runBestEffort(async () => {
818
+ await recordAuthenticationLinked({
819
+ conversationId: resumeConversationId,
820
+ provider,
821
+ actorId: stored.userId,
822
+ providerLabel,
823
+ ...account?.label ? { accountLabel: account.label } : void 0,
824
+ ...stored.resumeSessionId ? {
825
+ authorizationId: pluginAuthorizationId({
826
+ provider,
827
+ sessionId: stored.resumeSessionId
828
+ }),
829
+ turnId: stored.resumeSessionId
830
+ } : void 0
831
+ });
832
+ }, "oauth.callback.authentication_event.failed", { "app.credential.provider": provider });
833
+ if (stored.destination?.platform !== "local" && stored.source?.kind !== "web") waitUntil(async () => {
834
+ try {
835
+ await publishAppHomeView(getSlackClient(), stored.userId, userTokenStore);
836
+ } catch {}
837
+ });
838
+ const resumesWebTurn = Boolean(stored.source?.kind === "web" && stored.destination && stored.resumeConversationId && stored.resumeSessionId);
839
+ const resumesAgentTurn = Boolean(stored.destination?.platform !== "local" && stored.resumeConversationId && stored.resumeSessionId);
840
+ if (resumesWebTurn) waitUntil(async () => {
841
+ const turn = await getTurnRecord(stored.resumeConversationId, stored.resumeSessionId);
842
+ await deleteWebAuthorization({
843
+ actorId: stored.userId,
844
+ conversationId: stored.resumeConversationId
845
+ });
846
+ if (!turn || turn.state !== "paused" || turn.resumeReason !== "auth") return;
847
+ await recordAuthorizationCompleted({
848
+ conversationId: stored.resumeConversationId,
849
+ kind: "plugin",
850
+ provider: stored.provider,
851
+ actorId: stored.userId,
852
+ authorizationId: pluginAuthorizationId({
853
+ provider: stored.provider,
854
+ sessionId: stored.resumeSessionId
855
+ })
856
+ });
857
+ await wakePausedTurn({
858
+ conversationId: stored.resumeConversationId,
859
+ destination: stored.destination,
860
+ turnId: stored.resumeSessionId,
861
+ expectedVersion: turn.version
862
+ }, options.conversationWorkQueue ? { queue: options.conversationWorkQueue } : void 0);
863
+ });
864
+ else if (resumesAgentTurn) await wakeAuthorizedPluginTurn(stored, options);
865
+ else if (stored.channelId && stored.threadTs) {
866
+ const { channelId, threadTs } = stored;
867
+ waitUntil(() => postSlackMessage({
868
+ channelId,
869
+ threadTs,
870
+ text: `Your ${providerLabel} account is now connected. You can start using ${providerLabel} commands.`
871
+ }));
872
+ }
873
+ const botName = botConfig.userName;
874
+ const statusMessage = stored.destination?.platform === "local" && !resumesWebTurn ? `Your request is continuing in the local ${botName} client.` : resumesWebTurn ? `Your request is continuing in ${botName}.` : resumesAgentTurn ? "Your request is being processed in Slack." : `Your ${providerLabel} account is connected.`;
875
+ const footerMessage = stored.destination?.platform === "local" || resumesWebTurn ? `You can close this tab and return to ${botName}.` : "You can close this tab and return to Slack.";
876
+ return htmlCallbackResponse(escapeXml(`${providerLabel} account connected`), escapeXml(statusMessage), 200, { footerMessage: escapeXml(footerMessage) });
877
+ }
878
+ //#endregion
879
+ //#region src/chat/local/credential-sync.ts
880
+ const LOCAL_CREDENTIAL_SYNC_CONTEXT = "junior.local-credential-sync.v1";
881
+ const LOCAL_CREDENTIAL_SYNC_MAX_AGE_MS = 6e4;
882
+ const localCredentialSyncSchema = z.object({
883
+ createdAtMs: z.number().finite(),
884
+ provider: z.string().regex(/^[a-z][a-z0-9-]*$/),
885
+ tokens: pluginStoredTokensSchema
886
+ }).strict();
887
+ function syncSecret() {
888
+ const secret = process.env.JUNIOR_SECRET?.trim();
889
+ if (!secret) throw new Error("Local credential sync requires JUNIOR_SECRET");
890
+ return secret;
891
+ }
892
+ /** Sign one local credential payload in its own HMAC domain. */
893
+ function signature(body) {
894
+ const signingKey = createSecretKey(Buffer.from(syncSecret(), "utf8"));
895
+ return createHmac("sha256", signingKey).update(`${LOCAL_CREDENTIAL_SYNC_CONTEXT}:${body}`).digest("base64url");
896
+ }
897
+ /** Compare credential signatures without leaking a matching prefix by timing. */
898
+ function signaturesMatch(expected, actual) {
899
+ const expectedBuffer = Buffer.from(expected);
900
+ const actualBuffer = Buffer.from(actual);
901
+ return expectedBuffer.length === actualBuffer.length && timingSafeEqual(expectedBuffer, actualBuffer);
902
+ }
903
+ /** Copy a local user's provider credential to the loopback dev-server broker. */
904
+ async function syncLocalOAuthCredential(provider, tokens) {
905
+ const body = JSON.stringify({
906
+ createdAtMs: Date.now(),
907
+ provider,
908
+ tokens
909
+ });
910
+ const port = process.env.PORT?.trim() || "3000";
911
+ const response = await fetch(`http://127.0.0.1:${port}/api/internal/local-oauth-credentials`, {
912
+ method: "POST",
913
+ headers: {
914
+ "content-type": "application/json",
915
+ "x-junior-local-credential-signature": signature(body)
916
+ },
917
+ body
918
+ });
919
+ if (!response.ok) throw new Error(`Could not sync local OAuth credential: HTTP ${response.status}`);
920
+ }
921
+ /** Verify and store one credential sent by the local CLI over loopback. */
922
+ async function receiveLocalOAuthCredential(request) {
923
+ const hostname = new URL(request.url).hostname;
924
+ if (process.env.NODE_ENV !== "development" || hostname !== "127.0.0.1" && hostname !== "localhost") return new Response(null, { status: 404 });
925
+ const body = await request.text();
926
+ const actualSignature = request.headers.get("x-junior-local-credential-signature");
927
+ if (!actualSignature || !signaturesMatch(signature(body), actualSignature)) return Response.json({ error: "Unauthorized" }, { status: 401 });
928
+ let value;
929
+ try {
930
+ value = JSON.parse(body);
931
+ } catch {
932
+ return Response.json({ error: "Invalid request" }, { status: 400 });
933
+ }
934
+ const payload = localCredentialSyncSchema.safeParse(value);
935
+ if (!payload.success || Math.abs(Date.now() - payload.data.createdAtMs) > LOCAL_CREDENTIAL_SYNC_MAX_AGE_MS) return Response.json({ error: "Invalid request" }, { status: 400 });
936
+ await createUserTokenStore().set("local-cli", payload.data.provider, payload.data.tokens);
937
+ return new Response(null, { status: 204 });
938
+ }
939
+ //#endregion
940
+ export { GET$1 as a, wakePausedTurn as c, publishAppHomeView as i, getTurnAuthorization as l, syncLocalOAuthCredential as n, createPausedTurns as o, GET as r, getPausedTurnRequest as s, receiveLocalOAuthCredential as t };