@sentry/junior 0.114.0 → 0.115.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 (88) hide show
  1. package/dist/{agent-hooks-WNBUY4OB.js → agent-hooks-4EUUXP2F.js} +12 -11
  2. package/dist/api/conversations/detail.d.ts +2 -0
  3. package/dist/api/conversations/event-list.d.ts +2 -0
  4. package/dist/api/health.d.ts +7 -0
  5. package/dist/api/plugin-reports.d.ts +62 -0
  6. package/dist/api/plugins.d.ts +9 -0
  7. package/dist/api/runtime.d.ts +24 -0
  8. package/dist/api/schema/conversation.d.ts +8 -0
  9. package/dist/api/schema.d.ts +2 -2
  10. package/dist/api/schema.js +6 -6
  11. package/dist/api/skills.d.ts +6 -0
  12. package/dist/api.js +123 -99
  13. package/dist/app.js +1104 -5006
  14. package/dist/catalog-runtime-Y32UEQLB.js +18 -0
  15. package/dist/chat/agent/request.d.ts +6 -0
  16. package/dist/chat/agent/sandbox.d.ts +2 -0
  17. package/dist/chat/agent/tools.d.ts +2 -0
  18. package/dist/chat/config.d.ts +2 -0
  19. package/dist/chat/conversations/history.d.ts +6 -0
  20. package/dist/chat/local/credential-sync.d.ts +5 -0
  21. package/dist/chat/local/oauth-relay.d.ts +4 -0
  22. package/dist/chat/mcp/oauth.d.ts +1 -0
  23. package/dist/chat/oauth-authorization.d.ts +17 -0
  24. package/dist/chat/oauth-flow.d.ts +9 -0
  25. package/dist/chat/sandbox/egress/schemas.d.ts +46 -0
  26. package/dist/chat/sandbox/egress/signals.d.ts +9 -0
  27. package/dist/chat/sandbox/sandbox.d.ts +2 -0
  28. package/dist/chat/sentry-payload-filter.d.ts +4 -4
  29. package/dist/chat/services/mcp-auth-orchestration.d.ts +5 -3
  30. package/dist/chat/services/plugin-auth-orchestration.d.ts +2 -0
  31. package/dist/chat/slack/message-control.d.ts +2 -0
  32. package/dist/chat/slack/oauth-authorization-message.d.ts +3 -0
  33. package/dist/chat/task-execution/slack-work.d.ts +2 -0
  34. package/dist/chat/tool-support/zod-tool.d.ts +1 -1
  35. package/dist/chat/tools/definition.d.ts +5 -0
  36. package/dist/{chunk-TLGM5X2M.js → chunk-2QG4MZQ7.js} +2 -2
  37. package/dist/{chunk-WV4UJBAS.js → chunk-43RYMWJ2.js} +523 -2373
  38. package/dist/chunk-4CODA7P5.js +43 -0
  39. package/dist/{chunk-YU6PO42Q.js → chunk-67CVNDJ7.js} +1 -1
  40. package/dist/{chunk-OSKEC4XU.js → chunk-6FKLPVF3.js} +1 -1
  41. package/dist/{chunk-2ZTTXTSZ.js → chunk-6OPTF4BQ.js} +10 -4
  42. package/dist/{chunk-W6RDUAO3.js → chunk-6V762AB4.js} +1 -1
  43. package/dist/{chunk-ABPSDO6M.js → chunk-AGLQYELI.js} +77 -8
  44. package/dist/{chunk-6QWPVTMO.js → chunk-CEYOL62C.js} +9 -7
  45. package/dist/{chunk-RD7EJI7H.js → chunk-CWEZEPD4.js} +3 -3
  46. package/dist/chunk-DIB4GX3G.js +8 -0
  47. package/dist/{chunk-K2W4CK7K.js → chunk-EW5MLKT5.js} +10 -75
  48. package/dist/{chunk-42WRQLJ6.js → chunk-F3B2YJ76.js} +0 -14
  49. package/dist/chunk-FA6POGLH.js +70 -0
  50. package/dist/{chunk-PM3PX42K.js → chunk-FHC4JUSF.js} +1 -1
  51. package/dist/{chunk-6Y5L4BOM.js → chunk-FXOYURO4.js} +5 -3
  52. package/dist/{chunk-S4QDH23A.js → chunk-G36V2YGB.js} +83 -81
  53. package/dist/{chunk-O4GT7LYB.js → chunk-G5SX4UPR.js} +3 -43
  54. package/dist/chunk-HDSDCEPK.js +80 -0
  55. package/dist/{chunk-FIGAF6KL.js → chunk-HWCQRGLL.js} +5 -3
  56. package/dist/{chunk-C4MZ3GP5.js → chunk-HZF5P5UN.js} +1 -1
  57. package/dist/chunk-LYA2AB5G.js +1914 -0
  58. package/dist/{chunk-4FUEG3QY.js → chunk-NVUQSFWX.js} +2 -2
  59. package/dist/chunk-OB42YVAE.js +16 -0
  60. package/dist/{chunk-XLFBQJUO.js → chunk-PVZHG2XZ.js} +4 -4
  61. package/dist/{chunk-VNNBSKCO.js → chunk-RAVKCKXA.js} +7 -7
  62. package/dist/{chunk-ZGXH3REA.js → chunk-SYJT7AKB.js} +12 -2
  63. package/dist/{chunk-5CZ7RETL.js → chunk-TVE25LPW.js} +1 -1
  64. package/dist/chunk-W3U3YQYI.js +71 -0
  65. package/dist/chunk-YCLO7D7M.js +4267 -0
  66. package/dist/cli/chat.js +72 -40
  67. package/dist/cli/check.js +10 -8
  68. package/dist/cli/init.js +1 -0
  69. package/dist/cli/plugins.js +16 -14
  70. package/dist/cli/snapshot-warmup.js +13 -11
  71. package/dist/cli/upgrade.js +10 -8
  72. package/dist/{db-4VLP7KBF.js → db-KBSJHAZU.js} +8 -7
  73. package/dist/handlers/sandbox-egress-signals.d.ts +4 -0
  74. package/dist/instrumentation.js +72 -4
  75. package/dist/nitro.js +2 -2
  76. package/dist/oauth-callback-server-EYRMJTCS.js +231 -0
  77. package/dist/oauth-relay-3ZU72UGL.js +9 -0
  78. package/dist/reporting-schema.d.ts +12 -4
  79. package/dist/reporting.d.ts +5 -5
  80. package/dist/{runner-IPXLWQOF.js → runner-RQGZ5Z2S.js} +139 -72
  81. package/dist/sandbox-egress-signals-CDFAP67O.js +75 -0
  82. package/dist/{skills-QCNZISNF.js → skills-JJQIE3BT.js} +10 -8
  83. package/dist/validation-MZ47PZP5.js +21 -0
  84. package/dist/version.js +1 -1
  85. package/package.json +5 -5
  86. package/dist/catalog-runtime-6O4M6JGA.js +0 -16
  87. package/dist/chat/oauth-authorization-message.d.ts +0 -6
  88. package/dist/validation-I2BW3UHD.js +0 -19
@@ -0,0 +1,1914 @@
1
+ import {
2
+ getInterruptionMarker
3
+ } from "./chunk-CEYOL62C.js";
4
+ import {
5
+ createPluginLogger,
6
+ getPlugins
7
+ } from "./chunk-RAVKCKXA.js";
8
+ import {
9
+ acquireActiveLock,
10
+ getStateAdapter
11
+ } from "./chunk-67CVNDJ7.js";
12
+ import {
13
+ SlackActionError,
14
+ getSlackClient,
15
+ isDmChannel,
16
+ normalizeSlackConversationId,
17
+ withSlackRetries
18
+ } from "./chunk-CWEZEPD4.js";
19
+ import {
20
+ getDb,
21
+ parseDestination
22
+ } from "./chunk-HWCQRGLL.js";
23
+ import {
24
+ sleep
25
+ } from "./chunk-4ZNGQH7C.js";
26
+ import {
27
+ normalizeSlackEmojiName
28
+ } from "./chunk-SYJT7AKB.js";
29
+ import {
30
+ pluginCatalogRuntime
31
+ } from "./chunk-6FKLPVF3.js";
32
+ import {
33
+ logInfo,
34
+ logWarn
35
+ } from "./chunk-NVUQSFWX.js";
36
+ import {
37
+ isRecord
38
+ } from "./chunk-OB42YVAE.js";
39
+ import {
40
+ parseSlackMessageTs,
41
+ parseSlackUserId
42
+ } from "./chunk-F3B2YJ76.js";
43
+
44
+ // src/chat/capabilities/catalog.ts
45
+ var cachedCatalog;
46
+ function getCapabilityCatalog() {
47
+ const signature = pluginCatalogRuntime.getSignature();
48
+ if (cachedCatalog?.signature === signature) return cachedCatalog;
49
+ const providers = pluginCatalogRuntime.getCapabilityProviders();
50
+ const capabilityToProvider = /* @__PURE__ */ new Map();
51
+ for (const provider of providers) {
52
+ for (const capability of provider.capabilities) {
53
+ if (capabilityToProvider.has(capability)) {
54
+ throw new Error(
55
+ `Duplicate capability registration for "${capability}"`
56
+ );
57
+ }
58
+ capabilityToProvider.set(capability, provider);
59
+ }
60
+ }
61
+ cachedCatalog = { signature, providers, capabilityToProvider };
62
+ return cachedCatalog;
63
+ }
64
+ var catalogLogged = false;
65
+ function logCapabilityCatalogLoadedOnce() {
66
+ if (catalogLogged) return;
67
+ catalogLogged = true;
68
+ const { providers } = getCapabilityCatalog();
69
+ const capabilityNames = providers.flatMap((p) => p.capabilities).sort();
70
+ const configKeys = [
71
+ ...new Set(providers.flatMap((p) => p.configKeys))
72
+ ].sort();
73
+ logInfo(
74
+ "capability_catalog_loaded",
75
+ {},
76
+ {
77
+ "app.capability.providers": providers.map((p) => p.provider),
78
+ "app.capability.count": capabilityNames.length,
79
+ "app.capability.names": capabilityNames,
80
+ "app.config.key_count": configKeys.length,
81
+ "app.config.keys": configKeys
82
+ },
83
+ "Loaded capability provider catalog"
84
+ );
85
+ }
86
+
87
+ // src/chat/capabilities/router.ts
88
+ var ProviderCredentialRouter = class {
89
+ brokersByProvider;
90
+ constructor(input) {
91
+ this.brokersByProvider = input.brokersByProvider;
92
+ }
93
+ async issue(input) {
94
+ const broker = this.brokersByProvider[input.provider];
95
+ if (!broker) {
96
+ throw new Error(
97
+ `No credential broker registered for provider: ${input.provider}`
98
+ );
99
+ }
100
+ return await broker.issue({
101
+ context: input.context,
102
+ reason: input.reason
103
+ });
104
+ }
105
+ };
106
+
107
+ // src/chat/credentials/user-token-store.ts
108
+ import {
109
+ pluginStoredTokensSchema
110
+ } from "@sentry/junior-plugin-api";
111
+ var storedTokensSchema = pluginStoredTokensSchema;
112
+
113
+ // src/chat/credentials/state-adapter-token-store.ts
114
+ var KEY_PREFIX = "oauth-token";
115
+ var BUFFER_MS = 24 * 60 * 60 * 1e3;
116
+ var LONG_LIVED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
117
+ var REFRESH_LOCK_WAIT_MS = 3e4;
118
+ var REFRESH_LOCK_RETRY_MS = 100;
119
+ function tokenKey(userId, provider) {
120
+ return `${KEY_PREFIX}:${userId}:${provider}`;
121
+ }
122
+ function refreshLockKey(userId, provider) {
123
+ return `${tokenKey(userId, provider)}:refresh`;
124
+ }
125
+ var StateAdapterTokenStore = class {
126
+ state;
127
+ constructor(stateAdapter) {
128
+ this.state = stateAdapter;
129
+ }
130
+ async get(userId, provider) {
131
+ const stored = await this.state.get(tokenKey(userId, provider));
132
+ return stored === null || stored === void 0 ? void 0 : storedTokensSchema.parse(stored);
133
+ }
134
+ async set(userId, provider, tokens) {
135
+ const parsed = storedTokensSchema.parse(tokens);
136
+ const expiresAt = parsed.refreshTokenExpiresAt ?? parsed.expiresAt;
137
+ const ttlMs = expiresAt ? Math.max(expiresAt - Date.now() + BUFFER_MS, BUFFER_MS) : LONG_LIVED_TTL_MS;
138
+ await this.state.set(tokenKey(userId, provider), parsed, ttlMs);
139
+ }
140
+ async delete(userId, provider) {
141
+ await this.state.delete(tokenKey(userId, provider));
142
+ }
143
+ /** Wait for the per-slot refresh gate so rotated refresh tokens are used once. */
144
+ async withRefresh(userId, provider, callback) {
145
+ const lockKey = refreshLockKey(userId, provider);
146
+ const deadline = Date.now() + REFRESH_LOCK_WAIT_MS;
147
+ while (true) {
148
+ const lock = await acquireActiveLock(this.state, lockKey);
149
+ if (lock) {
150
+ try {
151
+ return await callback();
152
+ } finally {
153
+ await this.state.releaseLock(lock);
154
+ }
155
+ }
156
+ if (Date.now() >= deadline) {
157
+ throw new Error(`Could not acquire OAuth token refresh lock`);
158
+ }
159
+ await sleep(REFRESH_LOCK_RETRY_MS);
160
+ }
161
+ }
162
+ };
163
+
164
+ // src/chat/capabilities/factory.ts
165
+ var sandboxEgressRouters = /* @__PURE__ */ new WeakMap();
166
+ function createUserTokenStore() {
167
+ return new StateAdapterTokenStore(getStateAdapter());
168
+ }
169
+ function createProviderCredentialRouter(userTokenStore) {
170
+ logCapabilityCatalogLoadedOnce();
171
+ const brokersByProvider = {};
172
+ for (const plugin of pluginCatalogRuntime.getProviders()) {
173
+ const { name } = plugin.manifest;
174
+ if (!plugin.manifest.credentials && !plugin.manifest.apiHeaders) {
175
+ continue;
176
+ }
177
+ brokersByProvider[name] = pluginCatalogRuntime.createBroker(name, {
178
+ userTokenStore
179
+ });
180
+ }
181
+ return new ProviderCredentialRouter({ brokersByProvider });
182
+ }
183
+ function getSandboxEgressRouter() {
184
+ const stateAdapter = getStateAdapter();
185
+ let router = sandboxEgressRouters.get(stateAdapter);
186
+ if (!router) {
187
+ router = createProviderCredentialRouter(
188
+ new StateAdapterTokenStore(stateAdapter)
189
+ );
190
+ sandboxEgressRouters.set(stateAdapter, router);
191
+ }
192
+ return router;
193
+ }
194
+ async function issueProviderCredentialLease(input) {
195
+ return await getSandboxEgressRouter().issue(input);
196
+ }
197
+
198
+ // src/chat/agent/request.ts
199
+ var RetryableDeliveryError = class extends Error {
200
+ constructor(cause) {
201
+ super("Assistant delivery was transient or ambiguous", { cause });
202
+ this.name = "RetryableDeliveryError";
203
+ }
204
+ };
205
+ function actorFromRouting(routing) {
206
+ if (routing.dispatch?.actor) {
207
+ return routing.dispatch.actor;
208
+ }
209
+ if (routing.actor) {
210
+ return routing.actor;
211
+ }
212
+ if (routing.credentialContext && !("type" in routing.credentialContext.actor)) {
213
+ return routing.credentialContext.actor;
214
+ }
215
+ return void 0;
216
+ }
217
+ function assertRunRoutingConsistency(request) {
218
+ const { destination, source } = request.routing;
219
+ if (source.platform !== destination.platform) {
220
+ throw new TypeError("Run source and destination platforms do not match");
221
+ }
222
+ if (source.platform === "slack" && destination.platform === "slack") {
223
+ if (source.teamId !== destination.teamId) {
224
+ throw new TypeError("Slack source and destination teams do not match");
225
+ }
226
+ } else if (source.platform === "local" && destination.platform === "local" && (source.conversationId !== request.conversationId || destination.conversationId !== request.conversationId)) {
227
+ throw new TypeError(
228
+ "Local source, destination, and run conversation IDs do not match"
229
+ );
230
+ }
231
+ const actor = request.routing.dispatch?.actor ?? request.routing.actor;
232
+ if (!actor || actor.platform === "system") {
233
+ return;
234
+ }
235
+ if (actor.platform !== destination.platform) {
236
+ throw new TypeError(
237
+ `Actor platform "${actor.platform}" does not match destination platform "${destination.platform}"`
238
+ );
239
+ }
240
+ if (actor.platform === "slack" && destination.platform === "slack" && actor.teamId !== destination.teamId) {
241
+ throw new TypeError("Slack actor team does not match destination team");
242
+ }
243
+ }
244
+ function toolInvocationDestination(routing) {
245
+ if (routing.destination.platform !== "slack" || !routing.toolChannelId) {
246
+ return routing.destination;
247
+ }
248
+ return {
249
+ platform: "slack",
250
+ teamId: routing.destination.teamId,
251
+ channelId: routing.toolChannelId
252
+ };
253
+ }
254
+ function surfaceFromRouting(routing) {
255
+ if (routing.surface) {
256
+ return routing.surface;
257
+ }
258
+ return routing.source.platform === "slack" ? "slack" : "internal";
259
+ }
260
+
261
+ // src/chat/slack/outbound.ts
262
+ var MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
263
+ function requireSlackConversationId(channelId, action) {
264
+ const normalized = normalizeSlackConversationId(channelId);
265
+ if (!normalized) {
266
+ throw new Error(`${action} requires a valid channel ID`);
267
+ }
268
+ return normalized;
269
+ }
270
+ function requireSlackThreadTimestamp(threadTs, action) {
271
+ const normalized = threadTs.trim();
272
+ if (!normalized) {
273
+ throw new Error(`${action} requires a thread timestamp`);
274
+ }
275
+ return normalized;
276
+ }
277
+ function requireSlackMessageTimestamp(timestamp, action) {
278
+ const normalized = parseSlackMessageTs(timestamp);
279
+ if (!normalized) {
280
+ throw new Error(`${action} requires a target message timestamp`);
281
+ }
282
+ return normalized;
283
+ }
284
+ function requireSlackMessageText(text, action) {
285
+ if (text.trim().length === 0) {
286
+ throw new Error(`${action} requires non-empty text`);
287
+ }
288
+ if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) {
289
+ throw new Error(
290
+ `${action} text exceeds Slack's 40000 character truncation limit`
291
+ );
292
+ }
293
+ return text;
294
+ }
295
+ async function getSlackMessagePermalink(args) {
296
+ try {
297
+ const response = await withSlackRetries(
298
+ () => getSlackClient().chat.getPermalink({
299
+ channel: args.channelId,
300
+ message_ts: args.messageTs
301
+ }),
302
+ 3,
303
+ {
304
+ action: "chat.getPermalink",
305
+ idempotent: true,
306
+ spanAttributes: {
307
+ "app.slack.channel_id": args.channelId,
308
+ "app.slack.message_ts": args.messageTs
309
+ }
310
+ }
311
+ );
312
+ return response.permalink;
313
+ } catch {
314
+ return void 0;
315
+ }
316
+ }
317
+ async function postSlackMessage(input) {
318
+ const channelId = requireSlackConversationId(
319
+ input.channelId,
320
+ "Slack message posting"
321
+ );
322
+ const text = requireSlackMessageText(input.text, "Slack message posting");
323
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(
324
+ input.threadTs,
325
+ "Slack thread message posting"
326
+ ) : void 0;
327
+ const response = await withSlackRetries(
328
+ () => getSlackClient().chat.postMessage({
329
+ channel: channelId,
330
+ text,
331
+ ...input.blocks?.length ? {
332
+ blocks: input.blocks
333
+ } : {},
334
+ ...threadTs ? { thread_ts: threadTs } : {}
335
+ }),
336
+ 3,
337
+ {
338
+ action: "chat.postMessage",
339
+ spanAttributes: {
340
+ "app.slack.channel_id": channelId,
341
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : {}
342
+ }
343
+ }
344
+ );
345
+ const messageTs = parseSlackMessageTs(response.ts);
346
+ if (!messageTs) {
347
+ throw new Error("Slack message posted without ts");
348
+ }
349
+ return {
350
+ ts: messageTs,
351
+ ...input.includePermalink ? {
352
+ permalink: await getSlackMessagePermalink({
353
+ channelId,
354
+ messageTs
355
+ })
356
+ } : {}
357
+ };
358
+ }
359
+ async function postSlackEphemeralMessage(input) {
360
+ const channelId = requireSlackConversationId(
361
+ input.channelId,
362
+ "Slack ephemeral message posting"
363
+ );
364
+ const userId = parseSlackUserId(input.userId);
365
+ if (!userId) {
366
+ throw new Error("Slack ephemeral message posting requires a user ID");
367
+ }
368
+ const text = requireSlackMessageText(
369
+ input.text,
370
+ "Slack ephemeral message posting"
371
+ );
372
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(
373
+ input.threadTs,
374
+ "Slack ephemeral thread message posting"
375
+ ) : void 0;
376
+ const response = await withSlackRetries(
377
+ () => getSlackClient().chat.postEphemeral({
378
+ channel: channelId,
379
+ user: userId,
380
+ text,
381
+ ...threadTs ? { thread_ts: threadTs } : {}
382
+ }),
383
+ 3,
384
+ {
385
+ action: "chat.postEphemeral",
386
+ spanAttributes: {
387
+ "app.slack.channel_id": channelId,
388
+ "app.slack.user_id": userId,
389
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : {}
390
+ }
391
+ }
392
+ );
393
+ return {
394
+ messageTs: response.message_ts
395
+ };
396
+ }
397
+ async function uploadFilesToConversation(input) {
398
+ const channelId = requireSlackConversationId(
399
+ input.channelId,
400
+ "Slack file upload"
401
+ );
402
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack file upload") : void 0;
403
+ if (input.files.length === 0) {
404
+ throw new Error("Slack file upload requires at least one file");
405
+ }
406
+ const fileUploads = input.files.map((file) => {
407
+ const filename = file.filename.trim();
408
+ if (!filename) {
409
+ throw new Error(
410
+ "Slack file upload requires every file to have a filename"
411
+ );
412
+ }
413
+ return {
414
+ file: file.data,
415
+ filename
416
+ };
417
+ });
418
+ const response = await withSlackRetries(
419
+ () => getSlackClient().filesUploadV2({
420
+ channel_id: channelId,
421
+ ...threadTs ? { thread_ts: threadTs } : {},
422
+ file_uploads: fileUploads
423
+ }),
424
+ 3,
425
+ {
426
+ action: "filesUploadV2",
427
+ spanAttributes: {
428
+ "app.slack.channel_id": channelId,
429
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : {}
430
+ }
431
+ }
432
+ );
433
+ return {
434
+ files: (response.files ?? []).flatMap(
435
+ (completion) => (completion.files ?? []).map((file) => file.id ? { id: file.id } : {})
436
+ )
437
+ };
438
+ }
439
+ async function addReactionToMessage(input) {
440
+ const channelId = requireSlackConversationId(
441
+ input.channelId,
442
+ "Slack reaction"
443
+ );
444
+ const timestamp = requireSlackMessageTimestamp(
445
+ input.timestamp,
446
+ "Slack reaction"
447
+ );
448
+ const emoji = normalizeSlackEmojiName(input.emoji);
449
+ if (!emoji) {
450
+ throw new Error("Slack reaction requires a valid emoji alias name");
451
+ }
452
+ try {
453
+ await withSlackRetries(
454
+ () => getSlackClient().reactions.add({
455
+ channel: channelId,
456
+ timestamp,
457
+ name: emoji
458
+ }),
459
+ 3,
460
+ {
461
+ action: "reactions.add",
462
+ idempotent: true,
463
+ spanAttributes: {
464
+ "app.slack.channel_id": channelId,
465
+ "app.slack.message_ts": timestamp,
466
+ "app.slack.reaction": emoji
467
+ }
468
+ }
469
+ );
470
+ } catch (error) {
471
+ if (error instanceof SlackActionError && error.code === "already_reacted") {
472
+ return { ok: true };
473
+ }
474
+ throw error;
475
+ }
476
+ return { ok: true };
477
+ }
478
+ async function removeReactionFromMessage(input) {
479
+ const channelId = requireSlackConversationId(
480
+ input.channelId,
481
+ "Slack reaction removal"
482
+ );
483
+ const timestamp = requireSlackMessageTimestamp(
484
+ input.timestamp,
485
+ "Slack reaction removal"
486
+ );
487
+ const emoji = normalizeSlackEmojiName(input.emoji);
488
+ if (!emoji) {
489
+ throw new Error("Slack reaction removal requires a valid emoji alias name");
490
+ }
491
+ try {
492
+ await withSlackRetries(
493
+ () => getSlackClient().reactions.remove({
494
+ channel: channelId,
495
+ timestamp,
496
+ name: emoji
497
+ }),
498
+ 3,
499
+ {
500
+ action: "reactions.remove",
501
+ idempotent: true,
502
+ spanAttributes: {
503
+ "app.slack.channel_id": channelId,
504
+ "app.slack.message_ts": timestamp,
505
+ "app.slack.reaction": emoji
506
+ }
507
+ }
508
+ );
509
+ } catch (error) {
510
+ if (error instanceof SlackActionError && error.code === "no_reaction") {
511
+ return { ok: true };
512
+ }
513
+ throw error;
514
+ }
515
+ return { ok: true };
516
+ }
517
+
518
+ // src/chat/oauth-flow.ts
519
+ import { randomBytes } from "crypto";
520
+ import {
521
+ sourceSchema
522
+ } from "@sentry/junior-plugin-api";
523
+
524
+ // src/chat/slack/status-format.ts
525
+ var SLACK_STATUS_MAX_LENGTH = 50;
526
+ function truncateStatusText(text) {
527
+ const trimmed = text.trim();
528
+ if (!trimmed) {
529
+ return "";
530
+ }
531
+ if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) {
532
+ return trimmed;
533
+ }
534
+ return `${trimmed.slice(0, SLACK_STATUS_MAX_LENGTH - 3).trimEnd()}...`;
535
+ }
536
+
537
+ // src/chat/slack/mrkdwn.ts
538
+ function escapeSlackMrkdwnText(text) {
539
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
540
+ }
541
+ function escapeSlackLinkUrl(url) {
542
+ return url.replaceAll("&", "&amp;").replaceAll("<", "%3C").replaceAll(">", "%3E");
543
+ }
544
+ function formatSlackLink(url, label) {
545
+ return `<${escapeSlackLinkUrl(url)}|${escapeSlackMrkdwnText(label)}>`;
546
+ }
547
+ function readInlineCodeSpan(line, start) {
548
+ if (line[start] !== "`") {
549
+ return void 0;
550
+ }
551
+ let n = 1;
552
+ while (line[start + n] === "`") {
553
+ n++;
554
+ }
555
+ const marker = "`".repeat(n);
556
+ let search = start + n;
557
+ while (search < line.length) {
558
+ const close = line.indexOf(marker, search);
559
+ if (close === -1) {
560
+ return void 0;
561
+ }
562
+ const after = close + n;
563
+ if (line[after] !== "`") {
564
+ return { text: line.slice(start, after), end: after };
565
+ }
566
+ search = after + 1;
567
+ }
568
+ return void 0;
569
+ }
570
+ function readExistingSlackAngleToken(line, start) {
571
+ if (line[start] !== "<") {
572
+ return void 0;
573
+ }
574
+ const close = line.indexOf(">", start + 1);
575
+ if (close === -1) {
576
+ return void 0;
577
+ }
578
+ const body = line.slice(start + 1, close);
579
+ if (/^(?:https?:\/\/|@|#|!)/.test(body)) {
580
+ return { text: line.slice(start, close + 1), end: close + 1 };
581
+ }
582
+ return void 0;
583
+ }
584
+ function readMarkdownLink(line, start) {
585
+ if (line[start] !== "[") {
586
+ return void 0;
587
+ }
588
+ const labelEnd = line.indexOf("](", start + 1);
589
+ if (labelEnd === -1) {
590
+ return void 0;
591
+ }
592
+ const destStart = labelEnd + 2;
593
+ if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) {
594
+ return void 0;
595
+ }
596
+ const closeParens = line.indexOf(")", destStart);
597
+ if (closeParens === -1) {
598
+ return void 0;
599
+ }
600
+ return { text: line.slice(start, closeParens + 1), end: closeParens + 1 };
601
+ }
602
+ function hasUnmatchedClosingParen(text) {
603
+ let balance = 0;
604
+ for (const ch of text) {
605
+ if (ch === "(") balance++;
606
+ else if (ch === ")") balance--;
607
+ }
608
+ return balance < 0;
609
+ }
610
+ function readBareUrl(line, start) {
611
+ let end = start;
612
+ while (end < line.length) {
613
+ const ch = line[end];
614
+ if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === '"' || ch === "`" || ch === "|" || ch === "*") {
615
+ break;
616
+ }
617
+ end++;
618
+ }
619
+ if (end === start) {
620
+ return void 0;
621
+ }
622
+ let raw = line.slice(start, end);
623
+ let suffix = "";
624
+ const peel = () => {
625
+ suffix = raw.slice(-1) + suffix;
626
+ raw = raw.slice(0, -1);
627
+ };
628
+ const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
629
+ while (raw.length > 0 && shouldPeel()) {
630
+ peel();
631
+ }
632
+ if (!/^https?:\/\/.+/.test(raw)) {
633
+ return void 0;
634
+ }
635
+ return { url: raw, suffix, end };
636
+ }
637
+ function wrapBareUrlsOnLine(line) {
638
+ let result = "";
639
+ let i = 0;
640
+ while (i < line.length) {
641
+ const codeSpan = readInlineCodeSpan(line, i);
642
+ if (codeSpan) {
643
+ result += codeSpan.text;
644
+ i = codeSpan.end;
645
+ continue;
646
+ }
647
+ const angleToken = readExistingSlackAngleToken(line, i);
648
+ if (angleToken) {
649
+ result += angleToken.text;
650
+ i = angleToken.end;
651
+ continue;
652
+ }
653
+ const mdLink = readMarkdownLink(line, i);
654
+ if (mdLink) {
655
+ result += mdLink.text;
656
+ i = mdLink.end;
657
+ continue;
658
+ }
659
+ if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
660
+ const parsed = readBareUrl(line, i);
661
+ if (parsed) {
662
+ result += `<${parsed.url}>${parsed.suffix}`;
663
+ i = parsed.end;
664
+ continue;
665
+ }
666
+ }
667
+ result += line[i];
668
+ i++;
669
+ }
670
+ return result;
671
+ }
672
+ function wrapBareUrls(text) {
673
+ const lines = text.split("\n");
674
+ const out = [];
675
+ let inCodeBlock = false;
676
+ for (const line of lines) {
677
+ if (line.trimStart().startsWith("```")) {
678
+ inCodeBlock = !inCodeBlock;
679
+ out.push(line);
680
+ continue;
681
+ }
682
+ out.push(inCodeBlock ? line : wrapBareUrlsOnLine(line));
683
+ }
684
+ return out.join("\n");
685
+ }
686
+ function ensureBlockSpacing(text) {
687
+ const codeBlockPattern = /^```/;
688
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
689
+ const lines = text.split("\n");
690
+ const result = [];
691
+ let inCodeBlock = false;
692
+ for (let i = 0; i < lines.length; i++) {
693
+ const line = lines[i];
694
+ const isCodeFence = codeBlockPattern.test(line.trimStart());
695
+ if (isCodeFence) {
696
+ if (!inCodeBlock) {
697
+ const prev2 = result.length > 0 ? result[result.length - 1] : void 0;
698
+ if (prev2 !== void 0 && prev2.trim() !== "") {
699
+ result.push("");
700
+ }
701
+ }
702
+ inCodeBlock = !inCodeBlock;
703
+ result.push(line);
704
+ continue;
705
+ }
706
+ if (inCodeBlock) {
707
+ result.push(line);
708
+ continue;
709
+ }
710
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
711
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) {
712
+ result.push("");
713
+ }
714
+ result.push(line);
715
+ }
716
+ return result.join("\n");
717
+ }
718
+ function normalizeSlackReplyMarkdown(text) {
719
+ let normalized = text.replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
720
+ normalized = wrapBareUrls(normalized);
721
+ normalized = ensureBlockSpacing(normalized);
722
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
723
+ }
724
+ function normalizeSlackStatusText(text) {
725
+ const trimmed = text.trim();
726
+ if (!trimmed) {
727
+ return "";
728
+ }
729
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
730
+ }
731
+
732
+ // src/chat/slack/oauth-authorization-message.ts
733
+ function formatOAuthAuthorizationMessage(args) {
734
+ return `${formatSlackLink(args.authorizationUrl, args.label)}. ${args.completionText}`;
735
+ }
736
+
737
+ // src/chat/oauth-flow.ts
738
+ var OAUTH_STATE_TTL_MS = 10 * 60 * 1e3;
739
+ function optionalString(value) {
740
+ return typeof value === "string" ? value : void 0;
741
+ }
742
+ function parseOAuthStatePayload(value) {
743
+ if (!isRecord(value)) {
744
+ return void 0;
745
+ }
746
+ if (typeof value.userId !== "string" || typeof value.provider !== "string") {
747
+ return void 0;
748
+ }
749
+ const destination = parseDestination(value.destination);
750
+ if (value.destination !== void 0 && !destination) {
751
+ return void 0;
752
+ }
753
+ const source = value.source === void 0 ? void 0 : sourceSchema.safeParse(value.source);
754
+ if (value.source !== void 0 && (!source || !source.success)) {
755
+ return void 0;
756
+ }
757
+ return {
758
+ userId: value.userId,
759
+ provider: value.provider,
760
+ ...optionalString(value.channelId) ? { channelId: optionalString(value.channelId) } : {},
761
+ ...destination ? { destination } : {},
762
+ ...source?.success ? { source: source.data } : {},
763
+ ...optionalString(value.threadTs) ? { threadTs: optionalString(value.threadTs) } : {},
764
+ ...optionalString(value.resumeConversationId) ? { resumeConversationId: optionalString(value.resumeConversationId) } : {},
765
+ ...optionalString(value.resumeSessionId) ? { resumeSessionId: optionalString(value.resumeSessionId) } : {},
766
+ ...optionalString(value.scope) ? { scope: optionalString(value.scope) } : {}
767
+ };
768
+ }
769
+ function formatProviderLabel(provider) {
770
+ const displayName = pluginCatalogRuntime.getDisplayName(provider);
771
+ if (!displayName) {
772
+ throw new Error(`Unknown plugin provider display name: "${provider}"`);
773
+ }
774
+ return displayName;
775
+ }
776
+ function resolveBaseUrl() {
777
+ const explicit = process.env.JUNIOR_BASE_URL?.trim();
778
+ if (explicit) return explicit;
779
+ const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
780
+ if (vercelProd) return `https://${vercelProd}`;
781
+ const vercelUrl = process.env.VERCEL_URL?.trim();
782
+ if (vercelUrl) return `https://${vercelUrl}`;
783
+ return void 0;
784
+ }
785
+ async function deliverPrivateMessage(input) {
786
+ let client;
787
+ try {
788
+ client = getSlackClient();
789
+ } catch {
790
+ logWarn(
791
+ "oauth_private_delivery_skip",
792
+ {},
793
+ { "app.reason": "missing_bot_token" },
794
+ "Skipped private message delivery \u2014 no SLACK_BOT_TOKEN"
795
+ );
796
+ return false;
797
+ }
798
+ if (input.channelId) {
799
+ try {
800
+ if (isDmChannel(input.channelId)) {
801
+ await postSlackMessage({
802
+ channelId: input.channelId,
803
+ text: input.text,
804
+ threadTs: input.threadTs
805
+ });
806
+ } else {
807
+ await postSlackEphemeralMessage({
808
+ channelId: input.channelId,
809
+ userId: input.userId,
810
+ text: input.text,
811
+ threadTs: input.threadTs
812
+ });
813
+ }
814
+ return "in_context";
815
+ } catch (error) {
816
+ logWarn(
817
+ "oauth_private_delivery_failed",
818
+ {},
819
+ {
820
+ "app.slack.error": error instanceof Error ? error.message : String(error),
821
+ "app.slack.channel": input.channelId
822
+ },
823
+ "Private message delivery failed, falling back to DM"
824
+ );
825
+ }
826
+ }
827
+ try {
828
+ const dmChannelId = (await withSlackRetries(
829
+ () => client.conversations.open({ users: input.userId }),
830
+ 3,
831
+ {
832
+ action: "conversations.open",
833
+ spanAttributes: { "app.slack.user_id": input.userId }
834
+ }
835
+ )).channel?.id;
836
+ if (!dmChannelId) {
837
+ logWarn(
838
+ "oauth_dm_fallback_failed",
839
+ {},
840
+ { "app.reason": "no_dm_channel_id" },
841
+ "conversations.open returned no channel ID"
842
+ );
843
+ return false;
844
+ }
845
+ await postSlackMessage({ channelId: dmChannelId, text: input.text });
846
+ return "fallback_dm";
847
+ } catch (error) {
848
+ logWarn(
849
+ "oauth_dm_fallback_failed",
850
+ {},
851
+ {
852
+ "app.slack.error": error instanceof Error ? error.message : String(error)
853
+ },
854
+ "DM fallback delivery failed"
855
+ );
856
+ return false;
857
+ }
858
+ }
859
+ async function deliverOAuthAuthorization(request, input) {
860
+ if (input.authorization) {
861
+ await input.authorization.deliver(request);
862
+ return "in_context";
863
+ }
864
+ return await deliverPrivateMessage({
865
+ channelId: input.channelId,
866
+ threadTs: input.threadTs,
867
+ userId: input.userId,
868
+ text: formatOAuthAuthorizationMessage(request)
869
+ });
870
+ }
871
+ async function startOAuthFlow(provider, input) {
872
+ const providerConfig = pluginCatalogRuntime.getOAuthConfig(provider);
873
+ if (!providerConfig) {
874
+ return {
875
+ ok: false,
876
+ error: `Provider "${provider}" does not support OAuth authorization`
877
+ };
878
+ }
879
+ const clientId = process.env[providerConfig.clientIdEnv]?.trim();
880
+ if (!clientId) {
881
+ return {
882
+ ok: false,
883
+ error: `Missing ${providerConfig.clientIdEnv} environment variable`
884
+ };
885
+ }
886
+ const baseUrl = resolveBaseUrl();
887
+ if (!baseUrl) {
888
+ return {
889
+ ok: false,
890
+ error: "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)"
891
+ };
892
+ }
893
+ const state = input.authorization ? await input.authorization.createState() : randomBytes(32).toString("hex");
894
+ const requestedScope = input.scope ?? providerConfig.scope;
895
+ await getStateAdapter().set(
896
+ `oauth-state:${state}`,
897
+ {
898
+ userId: input.actorId,
899
+ provider,
900
+ ...input.channelId ? { channelId: input.channelId } : {},
901
+ ...input.destination ? { destination: input.destination } : {},
902
+ ...input.source ? { source: input.source } : {},
903
+ ...input.threadTs ? { threadTs: input.threadTs } : {},
904
+ ...input.resumeConversationId ? { resumeConversationId: input.resumeConversationId } : {},
905
+ ...input.resumeSessionId ? { resumeSessionId: input.resumeSessionId } : {},
906
+ ...requestedScope ? { scope: requestedScope } : {}
907
+ },
908
+ OAUTH_STATE_TTL_MS
909
+ );
910
+ const authorizeParams = new URLSearchParams({
911
+ client_id: clientId,
912
+ state,
913
+ redirect_uri: `${baseUrl}${providerConfig.callbackPath}`,
914
+ response_type: "code"
915
+ });
916
+ if (requestedScope) {
917
+ authorizeParams.set("scope", requestedScope);
918
+ }
919
+ for (const [key, value] of Object.entries(
920
+ providerConfig.authorizeParams ?? {}
921
+ )) {
922
+ authorizeParams.set(key, value);
923
+ }
924
+ logInfo(
925
+ "jr_rpc_oauth_start",
926
+ {},
927
+ {
928
+ "app.credential.provider": provider,
929
+ ...input.activeSkillName ? { "app.skill.name": input.activeSkillName } : {}
930
+ },
931
+ "Initiated OAuth authorization code flow"
932
+ );
933
+ const authorizationUrl = `${providerConfig.authorizeEndpoint}?${authorizeParams.toString()}`;
934
+ const authorizationRequest = {
935
+ authorizationUrl,
936
+ label: `Click here to link your ${formatProviderLabel(provider)} account`,
937
+ completionText: input.resumeSessionId ? "Once you've authorized, Junior will continue automatically." : "Once you've authorized, you'll see a confirmation in Slack."
938
+ };
939
+ return {
940
+ ok: true,
941
+ delivery: await deliverOAuthAuthorization(authorizationRequest, {
942
+ authorization: input.authorization,
943
+ channelId: input.channelId,
944
+ threadTs: input.threadTs,
945
+ userId: input.actorId
946
+ })
947
+ };
948
+ }
949
+
950
+ // src/chat/mcp/auth-store.ts
951
+ import {
952
+ sourceSchema as sourceSchema2
953
+ } from "@sentry/junior-plugin-api";
954
+ var MCP_AUTH_SESSION_PREFIX = "junior:mcp_oauth_attempt:v2";
955
+ var MCP_AUTH_CREDENTIALS_PREFIX = "junior:mcp_auth_credentials";
956
+ var MCP_AUTH_SESSION_INDEX_PREFIX = "junior:mcp_oauth_attempt_index:v2";
957
+ var MCP_SERVER_SESSION_PREFIX = "junior:mcp_server_session";
958
+ var MCP_AUTH_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
959
+ var MCP_AUTH_CREDENTIALS_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
960
+ var MCP_SERVER_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
961
+ function sessionKey(authSessionId) {
962
+ return `${MCP_AUTH_SESSION_PREFIX}:${authSessionId}`;
963
+ }
964
+ function credentialsKey(userId, provider) {
965
+ return `${MCP_AUTH_CREDENTIALS_PREFIX}:${userId}:${provider}`;
966
+ }
967
+ function sessionIndexKey(userId, provider) {
968
+ return `${MCP_AUTH_SESSION_INDEX_PREFIX}:${userId}:${provider}`;
969
+ }
970
+ function serverSessionKey(userId, provider) {
971
+ return `${MCP_SERVER_SESSION_PREFIX}:${userId}:${provider}`;
972
+ }
973
+ function parseSessionIndex(value) {
974
+ if (typeof value !== "string") {
975
+ return [];
976
+ }
977
+ try {
978
+ const parsed = JSON.parse(value);
979
+ if (!Array.isArray(parsed)) {
980
+ return [];
981
+ }
982
+ return [
983
+ ...new Set(parsed.filter((id) => typeof id === "string"))
984
+ ];
985
+ } catch {
986
+ return [];
987
+ }
988
+ }
989
+ function parseMcpAuthSession(value) {
990
+ if (typeof value !== "string") {
991
+ return void 0;
992
+ }
993
+ try {
994
+ const parsed = JSON.parse(value);
995
+ if (!isRecord(parsed)) {
996
+ return void 0;
997
+ }
998
+ if (parsed.schemaVersion !== 2 || typeof parsed.authSessionId !== "string" || typeof parsed.provider !== "string" || typeof parsed.userId !== "string" || typeof parsed.conversationId !== "string" || typeof parsed.sessionId !== "string" || typeof parsed.userMessage !== "string" || typeof parsed.createdAtMs !== "number" || typeof parsed.updatedAtMs !== "number") {
999
+ return void 0;
1000
+ }
1001
+ const destination = parsed.destination === void 0 ? void 0 : parseDestination(parsed.destination);
1002
+ if (parsed.destination !== void 0 && !destination) {
1003
+ return void 0;
1004
+ }
1005
+ const source = parsed.source === void 0 ? void 0 : sourceSchema2.safeParse(parsed.source);
1006
+ if (parsed.source !== void 0 && (!source || !source.success)) {
1007
+ return void 0;
1008
+ }
1009
+ return {
1010
+ schemaVersion: 2,
1011
+ authSessionId: parsed.authSessionId,
1012
+ provider: parsed.provider,
1013
+ userId: parsed.userId,
1014
+ conversationId: parsed.conversationId,
1015
+ ...destination ? { destination } : {},
1016
+ ...source?.success ? { source: source.data } : {},
1017
+ sessionId: parsed.sessionId,
1018
+ userMessage: parsed.userMessage,
1019
+ createdAtMs: parsed.createdAtMs,
1020
+ updatedAtMs: parsed.updatedAtMs,
1021
+ ...typeof parsed.channelId === "string" ? { channelId: parsed.channelId } : {},
1022
+ ...typeof parsed.threadTs === "string" ? { threadTs: parsed.threadTs } : {},
1023
+ ...typeof parsed.toolChannelId === "string" ? { toolChannelId: parsed.toolChannelId } : {},
1024
+ ...isRecord(parsed.configuration) ? { configuration: parsed.configuration } : {},
1025
+ ...isRecord(parsed.artifactState) ? { artifactState: parsed.artifactState } : {},
1026
+ ...typeof parsed.authorizationUrl === "string" ? { authorizationUrl: parsed.authorizationUrl } : {},
1027
+ ...typeof parsed.codeVerifier === "string" ? { codeVerifier: parsed.codeVerifier } : {}
1028
+ };
1029
+ } catch {
1030
+ return void 0;
1031
+ }
1032
+ }
1033
+ function parseServerSession(value) {
1034
+ if (typeof value !== "string") {
1035
+ return void 0;
1036
+ }
1037
+ try {
1038
+ const parsed = JSON.parse(value);
1039
+ if (!isRecord(parsed) || typeof parsed.sessionId !== "string" || typeof parsed.updatedAtMs !== "number") {
1040
+ return void 0;
1041
+ }
1042
+ return {
1043
+ sessionId: parsed.sessionId,
1044
+ updatedAtMs: parsed.updatedAtMs
1045
+ };
1046
+ } catch {
1047
+ return void 0;
1048
+ }
1049
+ }
1050
+ function parseStoredCredentials(value) {
1051
+ if (typeof value !== "string") {
1052
+ return void 0;
1053
+ }
1054
+ try {
1055
+ const parsed = JSON.parse(value);
1056
+ if (!isRecord(parsed)) {
1057
+ return void 0;
1058
+ }
1059
+ return {
1060
+ ...isRecord(parsed.clientInformation) ? {
1061
+ clientInformation: parsed.clientInformation
1062
+ } : {},
1063
+ ...isRecord(parsed.discoveryState) ? {
1064
+ discoveryState: parsed.discoveryState
1065
+ } : {},
1066
+ ...isRecord(parsed.tokens) ? { tokens: parsed.tokens } : {}
1067
+ };
1068
+ } catch {
1069
+ return void 0;
1070
+ }
1071
+ }
1072
+ async function getConnectedStateAdapter() {
1073
+ const stateAdapter = getStateAdapter();
1074
+ await stateAdapter.connect();
1075
+ return stateAdapter;
1076
+ }
1077
+ async function getMcpAuthSession(authSessionId) {
1078
+ const stateAdapter = await getConnectedStateAdapter();
1079
+ return parseMcpAuthSession(await stateAdapter.get(sessionKey(authSessionId)));
1080
+ }
1081
+ async function putMcpAuthSession(session, ttlMs = MCP_AUTH_SESSION_TTL_MS) {
1082
+ const stateAdapter = await getConnectedStateAdapter();
1083
+ await stateAdapter.set(
1084
+ sessionKey(session.authSessionId),
1085
+ JSON.stringify(session),
1086
+ ttlMs
1087
+ );
1088
+ const nextIndex = parseSessionIndex(
1089
+ await stateAdapter.get(sessionIndexKey(session.userId, session.provider))
1090
+ );
1091
+ if (!nextIndex.includes(session.authSessionId)) {
1092
+ nextIndex.push(session.authSessionId);
1093
+ }
1094
+ await stateAdapter.set(
1095
+ sessionIndexKey(session.userId, session.provider),
1096
+ JSON.stringify(nextIndex),
1097
+ ttlMs
1098
+ );
1099
+ }
1100
+ async function patchMcpAuthSession(authSessionId, patch) {
1101
+ const current = await getMcpAuthSession(authSessionId);
1102
+ if (!current) {
1103
+ throw new Error(`Unknown MCP auth session: ${authSessionId}`);
1104
+ }
1105
+ const next = {
1106
+ ...current,
1107
+ ...patch,
1108
+ schemaVersion: 2,
1109
+ authSessionId: current.authSessionId,
1110
+ provider: current.provider,
1111
+ userId: current.userId,
1112
+ conversationId: current.conversationId,
1113
+ ...current.destination ? { destination: current.destination } : {},
1114
+ sessionId: current.sessionId,
1115
+ userMessage: current.userMessage,
1116
+ createdAtMs: current.createdAtMs,
1117
+ updatedAtMs: Date.now()
1118
+ };
1119
+ await putMcpAuthSession(next);
1120
+ return next;
1121
+ }
1122
+ async function deleteMcpAuthSession(authSessionId) {
1123
+ const stateAdapter = await getConnectedStateAdapter();
1124
+ const current = parseMcpAuthSession(
1125
+ await stateAdapter.get(sessionKey(authSessionId))
1126
+ );
1127
+ await stateAdapter.delete(sessionKey(authSessionId));
1128
+ if (!current) {
1129
+ return;
1130
+ }
1131
+ const nextIndex = parseSessionIndex(
1132
+ await stateAdapter.get(sessionIndexKey(current.userId, current.provider))
1133
+ ).filter((id) => id !== authSessionId);
1134
+ if (nextIndex.length > 0) {
1135
+ await stateAdapter.set(
1136
+ sessionIndexKey(current.userId, current.provider),
1137
+ JSON.stringify(nextIndex),
1138
+ MCP_AUTH_SESSION_TTL_MS
1139
+ );
1140
+ return;
1141
+ }
1142
+ await stateAdapter.delete(sessionIndexKey(current.userId, current.provider));
1143
+ }
1144
+ async function deleteMcpAuthSessionsForUserProvider(userId, provider) {
1145
+ const stateAdapter = await getConnectedStateAdapter();
1146
+ const indexKey = sessionIndexKey(userId, provider);
1147
+ const authSessionIds = parseSessionIndex(await stateAdapter.get(indexKey));
1148
+ for (const authSessionId of authSessionIds) {
1149
+ await stateAdapter.delete(sessionKey(authSessionId));
1150
+ }
1151
+ await stateAdapter.delete(indexKey);
1152
+ }
1153
+ async function getMcpStoredOAuthCredentials(userId, provider) {
1154
+ const stateAdapter = await getConnectedStateAdapter();
1155
+ return parseStoredCredentials(
1156
+ await stateAdapter.get(credentialsKey(userId, provider))
1157
+ );
1158
+ }
1159
+ async function putMcpStoredOAuthCredentials(userId, provider, value, ttlMs = MCP_AUTH_CREDENTIALS_TTL_MS) {
1160
+ const stateAdapter = await getConnectedStateAdapter();
1161
+ await stateAdapter.set(
1162
+ credentialsKey(userId, provider),
1163
+ JSON.stringify(value),
1164
+ ttlMs
1165
+ );
1166
+ }
1167
+ async function deleteMcpStoredOAuthCredentials(userId, provider) {
1168
+ const stateAdapter = await getConnectedStateAdapter();
1169
+ await stateAdapter.delete(credentialsKey(userId, provider));
1170
+ }
1171
+ async function getMcpServerSessionId(userId, provider) {
1172
+ const stateAdapter = await getConnectedStateAdapter();
1173
+ return parseServerSession(
1174
+ await stateAdapter.get(serverSessionKey(userId, provider))
1175
+ )?.sessionId;
1176
+ }
1177
+ async function putMcpServerSessionId(userId, provider, sessionId, ttlMs = MCP_SERVER_SESSION_TTL_MS) {
1178
+ const stateAdapter = await getConnectedStateAdapter();
1179
+ await stateAdapter.set(
1180
+ serverSessionKey(userId, provider),
1181
+ JSON.stringify({
1182
+ sessionId,
1183
+ updatedAtMs: Date.now()
1184
+ }),
1185
+ ttlMs
1186
+ );
1187
+ }
1188
+ async function deleteMcpServerSessionId(userId, provider) {
1189
+ const stateAdapter = await getConnectedStateAdapter();
1190
+ await stateAdapter.delete(serverSessionKey(userId, provider));
1191
+ }
1192
+
1193
+ // src/chat/mcp/oauth.ts
1194
+ import { randomUUID } from "crypto";
1195
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1196
+
1197
+ // src/chat/mcp/oauth-provider.ts
1198
+ function createClientMetadata(callbackUrl) {
1199
+ return {
1200
+ client_name: "Junior MCP Client",
1201
+ redirect_uris: [callbackUrl],
1202
+ grant_types: ["authorization_code", "refresh_token"],
1203
+ response_types: ["code"],
1204
+ token_endpoint_auth_method: "none"
1205
+ };
1206
+ }
1207
+ function clientAllowsRedirectUri(clientInformation, callbackUrl) {
1208
+ const redirectUris = clientInformation.redirect_uris;
1209
+ if (!Array.isArray(redirectUris)) {
1210
+ return true;
1211
+ }
1212
+ return redirectUris.includes(callbackUrl);
1213
+ }
1214
+ var StateBackedMcpOAuthClientProvider = class {
1215
+ constructor(authSessionId, callbackUrl, sessionContext, runCredentialMutation) {
1216
+ this.authSessionId = authSessionId;
1217
+ this.callbackUrl = callbackUrl;
1218
+ this.sessionContext = sessionContext;
1219
+ this.runCredentialMutation = runCredentialMutation;
1220
+ this.clientMetadata = createClientMetadata(callbackUrl);
1221
+ }
1222
+ authSessionId;
1223
+ callbackUrl;
1224
+ sessionContext;
1225
+ runCredentialMutation;
1226
+ clientMetadata;
1227
+ get redirectUrl() {
1228
+ return this.callbackUrl;
1229
+ }
1230
+ async state() {
1231
+ return this.authSessionId;
1232
+ }
1233
+ async clientInformation() {
1234
+ const session = await this.getCredentialContext();
1235
+ const credentials = await getMcpStoredOAuthCredentials(
1236
+ session.userId,
1237
+ session.provider
1238
+ );
1239
+ const clientInformation = credentials?.clientInformation;
1240
+ if (!clientInformation) {
1241
+ return void 0;
1242
+ }
1243
+ if (clientAllowsRedirectUri(clientInformation, this.callbackUrl)) {
1244
+ return clientInformation;
1245
+ }
1246
+ return void 0;
1247
+ }
1248
+ async saveClientInformation(clientInformation) {
1249
+ await this.mutateCredentials(async () => {
1250
+ const session = await this.getCredentialContext();
1251
+ const credentials = await getMcpStoredOAuthCredentials(
1252
+ session.userId,
1253
+ session.provider
1254
+ ) ?? {};
1255
+ await putMcpStoredOAuthCredentials(session.userId, session.provider, {
1256
+ ...credentials,
1257
+ clientInformation
1258
+ });
1259
+ });
1260
+ }
1261
+ async tokens() {
1262
+ const session = await this.getCredentialContext();
1263
+ const credentials = await getMcpStoredOAuthCredentials(
1264
+ session.userId,
1265
+ session.provider
1266
+ );
1267
+ return credentials?.tokens;
1268
+ }
1269
+ async saveTokens(tokens) {
1270
+ await this.mutateCredentials(async () => {
1271
+ const session = await this.getCredentialContext();
1272
+ const credentials = await getMcpStoredOAuthCredentials(
1273
+ session.userId,
1274
+ session.provider
1275
+ ) ?? {};
1276
+ await putMcpStoredOAuthCredentials(session.userId, session.provider, {
1277
+ ...credentials,
1278
+ tokens
1279
+ });
1280
+ });
1281
+ }
1282
+ async redirectToAuthorization(authorizationUrl) {
1283
+ const existing = await getMcpAuthSession(this.authSessionId);
1284
+ if (existing?.authorizationUrl && existing.authorizationUrl !== authorizationUrl.toString()) {
1285
+ throw new Error("MCP OAuth authorization attempt is already initialized");
1286
+ }
1287
+ await this.ensureSession({
1288
+ authorizationUrl: authorizationUrl.toString()
1289
+ });
1290
+ }
1291
+ async saveCodeVerifier(codeVerifier) {
1292
+ const existing = await getMcpAuthSession(this.authSessionId);
1293
+ if (existing?.codeVerifier && existing.codeVerifier !== codeVerifier) {
1294
+ throw new Error("MCP OAuth authorization attempt is already initialized");
1295
+ }
1296
+ await this.ensureSession({ codeVerifier });
1297
+ }
1298
+ async codeVerifier() {
1299
+ const session = await this.requireSession();
1300
+ if (!session.codeVerifier) {
1301
+ throw new Error("Missing MCP OAuth code verifier");
1302
+ }
1303
+ return session.codeVerifier;
1304
+ }
1305
+ async saveDiscoveryState(state) {
1306
+ await this.mutateCredentials(async () => {
1307
+ const session = await this.getCredentialContext();
1308
+ const credentials = await getMcpStoredOAuthCredentials(
1309
+ session.userId,
1310
+ session.provider
1311
+ ) ?? {};
1312
+ await putMcpStoredOAuthCredentials(session.userId, session.provider, {
1313
+ ...credentials,
1314
+ discoveryState: state
1315
+ });
1316
+ });
1317
+ }
1318
+ async discoveryState() {
1319
+ const session = await this.getCredentialContext();
1320
+ const credentials = await getMcpStoredOAuthCredentials(
1321
+ session.userId,
1322
+ session.provider
1323
+ );
1324
+ return credentials?.discoveryState;
1325
+ }
1326
+ async invalidateCredentials(scope) {
1327
+ await this.mutateCredentials(async () => {
1328
+ const session = await this.getCredentialContext();
1329
+ const credentials = await getMcpStoredOAuthCredentials(
1330
+ session.userId,
1331
+ session.provider
1332
+ ) ?? {};
1333
+ await putMcpStoredOAuthCredentials(session.userId, session.provider, {
1334
+ ...scope === "tokens" || scope === "all" ? {} : credentials.tokens ? { tokens: credentials.tokens } : {},
1335
+ ...scope === "client" || scope === "all" ? {} : credentials.clientInformation ? { clientInformation: credentials.clientInformation } : {},
1336
+ ...scope === "discovery" || scope === "all" ? {} : credentials.discoveryState ? { discoveryState: credentials.discoveryState } : {}
1337
+ });
1338
+ });
1339
+ }
1340
+ async getMcpServerSessionId() {
1341
+ const session = await this.getCredentialContext();
1342
+ return await getMcpServerSessionId(session.userId, session.provider);
1343
+ }
1344
+ async saveMcpServerSessionId(sessionId) {
1345
+ await this.mutateCredentials(async () => {
1346
+ const session = await this.getCredentialContext();
1347
+ if (!sessionId) {
1348
+ await deleteMcpServerSessionId(session.userId, session.provider);
1349
+ return;
1350
+ }
1351
+ await putMcpServerSessionId(session.userId, session.provider, sessionId);
1352
+ });
1353
+ }
1354
+ /** Route shared credential writes through callback-owned freshness control. */
1355
+ async mutateCredentials(mutation) {
1356
+ return this.runCredentialMutation ? await this.runCredentialMutation(mutation) : await mutation();
1357
+ }
1358
+ async getCredentialContext() {
1359
+ return this.sessionContext ?? await this.requireSession();
1360
+ }
1361
+ async ensureSession(patch) {
1362
+ const existing = await getMcpAuthSession(this.authSessionId);
1363
+ if (existing) {
1364
+ return await patchMcpAuthSession(this.authSessionId, patch);
1365
+ }
1366
+ if (!this.sessionContext) {
1367
+ throw new Error(`Unknown MCP auth session: ${this.authSessionId}`);
1368
+ }
1369
+ const now = Date.now();
1370
+ const nextSession = {
1371
+ schemaVersion: 2,
1372
+ authSessionId: this.authSessionId,
1373
+ ...this.sessionContext,
1374
+ ...patch,
1375
+ createdAtMs: now,
1376
+ updatedAtMs: now
1377
+ };
1378
+ await putMcpAuthSession(nextSession);
1379
+ return nextSession;
1380
+ }
1381
+ async requireSession() {
1382
+ const session = await getMcpAuthSession(this.authSessionId);
1383
+ if (!session) {
1384
+ throw new Error(`Unknown MCP auth session: ${this.authSessionId}`);
1385
+ }
1386
+ return session;
1387
+ }
1388
+ };
1389
+
1390
+ // src/chat/mcp/oauth.ts
1391
+ function getMcpOAuthCallbackPath(provider) {
1392
+ return `/api/oauth/callback/mcp/${provider}`;
1393
+ }
1394
+ function requirePluginWithMcp(provider) {
1395
+ const plugin = pluginCatalogRuntime.getDefinition(provider);
1396
+ if (!plugin?.manifest.mcp) {
1397
+ throw new Error(`Plugin "${provider}" does not support MCP`);
1398
+ }
1399
+ return plugin;
1400
+ }
1401
+ async function createMcpOAuthClientProvider(input) {
1402
+ requirePluginWithMcp(input.provider);
1403
+ const baseUrl = resolveBaseUrl();
1404
+ if (!baseUrl) {
1405
+ throw new Error(
1406
+ "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)"
1407
+ );
1408
+ }
1409
+ const authSessionId = input.createAuthorizationState ? await input.createAuthorizationState() : randomUUID();
1410
+ return new StateBackedMcpOAuthClientProvider(
1411
+ authSessionId,
1412
+ `${baseUrl}${getMcpOAuthCallbackPath(input.provider)}`,
1413
+ {
1414
+ provider: input.provider,
1415
+ userId: input.userId,
1416
+ conversationId: input.conversationId,
1417
+ ...input.destination ? { destination: input.destination } : {},
1418
+ ...input.source ? { source: input.source } : {},
1419
+ sessionId: input.sessionId,
1420
+ userMessage: input.userMessage,
1421
+ ...input.channelId ? { channelId: input.channelId } : {},
1422
+ ...input.threadTs ? { threadTs: input.threadTs } : {},
1423
+ ...input.toolChannelId ? { toolChannelId: input.toolChannelId } : {},
1424
+ ...input.configuration ? { configuration: input.configuration } : {},
1425
+ ...input.artifactState ? { artifactState: input.artifactState } : {}
1426
+ }
1427
+ );
1428
+ }
1429
+ async function finalizeMcpAuthorization(provider, authSessionId, authorizationCode, runCredentialMutation) {
1430
+ const plugin = requirePluginWithMcp(provider);
1431
+ const mcp = plugin.manifest.mcp;
1432
+ if (!mcp) {
1433
+ throw new Error(`Plugin "${provider}" does not support MCP`);
1434
+ }
1435
+ const session = await getMcpAuthSession(authSessionId);
1436
+ if (!session) {
1437
+ throw new Error(`Unknown MCP auth session: ${authSessionId}`);
1438
+ }
1439
+ if (session.provider !== provider) {
1440
+ throw new Error(
1441
+ `MCP auth session provider mismatch: expected "${provider}", got "${session.provider}"`
1442
+ );
1443
+ }
1444
+ const baseUrl = resolveBaseUrl();
1445
+ if (!baseUrl) {
1446
+ throw new Error(
1447
+ "Cannot determine base URL (set JUNIOR_BASE_URL or deploy to Vercel)"
1448
+ );
1449
+ }
1450
+ const callbackUrl = `${baseUrl}${getMcpOAuthCallbackPath(provider)}`;
1451
+ const authProvider = new StateBackedMcpOAuthClientProvider(
1452
+ authSessionId,
1453
+ callbackUrl,
1454
+ void 0,
1455
+ runCredentialMutation
1456
+ );
1457
+ const requestInit = {};
1458
+ if (mcp.headers && Object.keys(mcp.headers).length > 0) {
1459
+ requestInit.headers = new Headers(mcp.headers);
1460
+ }
1461
+ const transport = new StreamableHTTPClientTransport(new URL(mcp.url), {
1462
+ ...Object.keys(requestInit).length > 0 ? { requestInit } : {},
1463
+ authProvider
1464
+ });
1465
+ try {
1466
+ await transport.finishAuth(authorizationCode);
1467
+ } finally {
1468
+ await transport.close();
1469
+ }
1470
+ const nextSession = await getMcpAuthSession(authSessionId);
1471
+ if (!nextSession) {
1472
+ throw new Error(`Unknown MCP auth session: ${authSessionId}`);
1473
+ }
1474
+ return nextSession;
1475
+ }
1476
+
1477
+ // src/chat/plugins/credential-hooks.ts
1478
+ import {
1479
+ pluginAuthorizationSchema,
1480
+ pluginCredentialResultSchema,
1481
+ pluginGrantSchema,
1482
+ pluginProviderAccountSchema
1483
+ } from "@sentry/junior-plugin-api";
1484
+ function parseSchema(schema, value, message) {
1485
+ const result = schema.safeParse(value);
1486
+ if (!result.success) {
1487
+ throw new Error(message);
1488
+ }
1489
+ return result.data;
1490
+ }
1491
+ function parseAuthorization(value, pluginName) {
1492
+ if (value === void 0) {
1493
+ return void 0;
1494
+ }
1495
+ const authorization = parseSchema(
1496
+ pluginAuthorizationSchema,
1497
+ value,
1498
+ `Plugin "${pluginName}" grant authorization is invalid`
1499
+ );
1500
+ if (authorization.provider !== pluginName) {
1501
+ throw new Error(
1502
+ `Plugin "${pluginName}" grant authorization provider must match the issuing plugin`
1503
+ );
1504
+ }
1505
+ return authorization;
1506
+ }
1507
+ function parseGrant(value, pluginName) {
1508
+ return parseSchema(
1509
+ pluginGrantSchema,
1510
+ value,
1511
+ `Plugin "${pluginName}" grantForEgress returned an invalid grant`
1512
+ );
1513
+ }
1514
+ function pluginFor(provider) {
1515
+ return getPlugins().find((candidate) => candidate.manifest.name === provider);
1516
+ }
1517
+ function basePluginContext(plugin) {
1518
+ const pluginName = plugin.manifest.name;
1519
+ return {
1520
+ plugin: { name: pluginName },
1521
+ log: createPluginLogger(pluginName),
1522
+ db: getDb()
1523
+ };
1524
+ }
1525
+ function parseCredentialResult(value, pluginName) {
1526
+ const result = parseSchema(
1527
+ pluginCredentialResultSchema,
1528
+ value,
1529
+ `Plugin "${pluginName}" issueCredential result is invalid`
1530
+ );
1531
+ if (result.type === "lease") {
1532
+ parseAuthorization(result.lease.authorization, pluginName);
1533
+ return result;
1534
+ }
1535
+ if (result.type === "unavailable") {
1536
+ return result;
1537
+ }
1538
+ parseAuthorization(result.authorization, pluginName);
1539
+ return result;
1540
+ }
1541
+ async function selectPluginGrant(input) {
1542
+ const plugin = pluginFor(input.provider);
1543
+ const hook = plugin?.hooks?.grantForEgress;
1544
+ if (!plugin || !hook) {
1545
+ return void 0;
1546
+ }
1547
+ const result = await hook({
1548
+ ...basePluginContext(plugin),
1549
+ request: {
1550
+ ...input.bodyText !== void 0 ? { bodyText: input.bodyText } : {},
1551
+ method: input.method,
1552
+ ...input.operation ? { operation: input.operation } : {},
1553
+ url: input.upstreamUrl.toString()
1554
+ }
1555
+ });
1556
+ return result === void 0 ? void 0 : parseGrant(result, plugin.manifest.name);
1557
+ }
1558
+ async function onPluginEgressResponse(input) {
1559
+ const plugin = pluginFor(input.provider);
1560
+ const hook = plugin?.hooks?.onEgressResponse;
1561
+ if (!plugin || !hook) {
1562
+ return {};
1563
+ }
1564
+ let permissionDenied;
1565
+ await hook({
1566
+ ...basePluginContext(plugin),
1567
+ grant: input.grant,
1568
+ permissionDenied(message) {
1569
+ const trimmed = message.trim();
1570
+ if (!trimmed) {
1571
+ throw new Error(
1572
+ `Plugin "${plugin.manifest.name}" onEgressResponse permissionDenied message is empty`
1573
+ );
1574
+ }
1575
+ permissionDenied = { message: trimmed };
1576
+ },
1577
+ request: {
1578
+ method: input.method,
1579
+ ...input.operation ? { operation: input.operation } : {},
1580
+ url: input.upstreamUrl.toString()
1581
+ },
1582
+ response: input.response
1583
+ });
1584
+ return permissionDenied ? { permissionDenied } : {};
1585
+ }
1586
+ function hasEgressCredentialHooks(provider) {
1587
+ const hooks = pluginFor(provider)?.hooks;
1588
+ return Boolean(hooks?.grantForEgress || hooks?.issueCredential);
1589
+ }
1590
+ async function resolvePluginOAuthAccount(input) {
1591
+ const plugin = pluginFor(input.provider);
1592
+ const hook = plugin?.hooks?.resolveOAuthAccount;
1593
+ if (!plugin || !hook) {
1594
+ return void 0;
1595
+ }
1596
+ const account = await hook({
1597
+ ...basePluginContext(plugin),
1598
+ tokens: input.tokens
1599
+ });
1600
+ return account === void 0 ? void 0 : parseSchema(
1601
+ pluginProviderAccountSchema,
1602
+ account,
1603
+ `Plugin "${plugin.manifest.name}" resolveOAuthAccount returned an invalid account`
1604
+ );
1605
+ }
1606
+ async function issuePluginCredential(input) {
1607
+ const plugin = pluginFor(input.provider);
1608
+ const hook = plugin?.hooks?.issueCredential;
1609
+ if (!plugin || !hook) {
1610
+ throw new Error(`Plugin "${input.provider}" has no issueCredential hook`);
1611
+ }
1612
+ const currentUserId = "type" in input.actor ? input.actor.userId : void 0;
1613
+ const credentialSubjectUserId = input.credentialSubject?.userId;
1614
+ const result = await hook({
1615
+ ...basePluginContext(plugin),
1616
+ actor: input.actor,
1617
+ grant: input.grant,
1618
+ ...input.credentialSubject ? { credentialSubject: input.credentialSubject } : {},
1619
+ tokens: {
1620
+ ...currentUserId ? {
1621
+ currentUser: {
1622
+ userId: currentUserId,
1623
+ get: async () => await input.userTokenStore.get(
1624
+ currentUserId,
1625
+ plugin.manifest.name
1626
+ ),
1627
+ set: async (tokens) => {
1628
+ await input.userTokenStore.set(
1629
+ currentUserId,
1630
+ plugin.manifest.name,
1631
+ tokens
1632
+ );
1633
+ },
1634
+ withRefresh: async (callback) => await input.userTokenStore.withRefresh(
1635
+ currentUserId,
1636
+ plugin.manifest.name,
1637
+ callback
1638
+ )
1639
+ }
1640
+ } : {},
1641
+ ...credentialSubjectUserId ? {
1642
+ credentialSubject: {
1643
+ userId: credentialSubjectUserId,
1644
+ get: async () => await input.userTokenStore.get(
1645
+ credentialSubjectUserId,
1646
+ plugin.manifest.name
1647
+ ),
1648
+ set: async (tokens) => {
1649
+ await input.userTokenStore.set(
1650
+ credentialSubjectUserId,
1651
+ plugin.manifest.name,
1652
+ tokens
1653
+ );
1654
+ },
1655
+ withRefresh: async (callback) => await input.userTokenStore.withRefresh(
1656
+ credentialSubjectUserId,
1657
+ plugin.manifest.name,
1658
+ callback
1659
+ )
1660
+ }
1661
+ } : {}
1662
+ }
1663
+ });
1664
+ return parseCredentialResult(result, plugin.manifest.name);
1665
+ }
1666
+
1667
+ // src/chat/slack/output.ts
1668
+ var MAX_INLINE_CHARS = 2200;
1669
+ var MAX_INLINE_LINES = 45;
1670
+ var CONTINUED_MARKER = "\n\n[Continued below]";
1671
+ function countSlackLines(text) {
1672
+ if (!text) {
1673
+ return 0;
1674
+ }
1675
+ return text.split("\n").length;
1676
+ }
1677
+ function fitsInlineBudget(text, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1678
+ return text.length <= maxChars && countSlackLines(text) <= maxLines;
1679
+ }
1680
+ function findSplitIndex(text, maxChars) {
1681
+ if (text.length <= maxChars) {
1682
+ return text.length;
1683
+ }
1684
+ const bounded = text.slice(0, maxChars);
1685
+ const newlineIndex = bounded.lastIndexOf("\n");
1686
+ if (newlineIndex > 0) {
1687
+ return newlineIndex;
1688
+ }
1689
+ const spaceIndex = bounded.lastIndexOf(" ");
1690
+ if (spaceIndex > 0) {
1691
+ return spaceIndex;
1692
+ }
1693
+ return maxChars;
1694
+ }
1695
+ function splitByLineBudget(text, maxLines) {
1696
+ if (maxLines <= 0) {
1697
+ return "";
1698
+ }
1699
+ const lines = text.split("\n");
1700
+ if (lines.length <= maxLines) {
1701
+ return text;
1702
+ }
1703
+ return lines.slice(0, maxLines).join("\n");
1704
+ }
1705
+ function reserveInlineBudgetForSuffix(suffix, maxChars = MAX_INLINE_CHARS, maxLines = MAX_INLINE_LINES) {
1706
+ return {
1707
+ maxChars: Math.max(1, maxChars - suffix.length),
1708
+ maxLines: Math.max(1, maxLines - Math.max(0, countSlackLines(suffix) - 1))
1709
+ };
1710
+ }
1711
+ function forceSplitBudget(text, budget) {
1712
+ const lineCount = countSlackLines(text);
1713
+ return {
1714
+ maxChars: text.length <= budget.maxChars ? Math.max(1, text.length - 1) : budget.maxChars,
1715
+ maxLines: lineCount <= budget.maxLines ? Math.max(1, lineCount - 1) : budget.maxLines
1716
+ };
1717
+ }
1718
+ function getFenceContinuation(text) {
1719
+ let open;
1720
+ for (const line of text.split("\n")) {
1721
+ const trimmed = line.trimStart();
1722
+ const openerMatch = trimmed.match(/^(`{3,}|~{3,})(.*)$/);
1723
+ if (!openerMatch) {
1724
+ continue;
1725
+ }
1726
+ if (!open) {
1727
+ open = {
1728
+ fence: openerMatch[1],
1729
+ openerLine: trimmed
1730
+ };
1731
+ continue;
1732
+ }
1733
+ if (new RegExp(`^${open.fence}\\s*$`).test(trimmed)) {
1734
+ open = void 0;
1735
+ }
1736
+ }
1737
+ if (!open) {
1738
+ return null;
1739
+ }
1740
+ return {
1741
+ closeSuffix: text.endsWith("\n") ? open.fence : `
1742
+ ${open.fence}`,
1743
+ reopenPrefix: `${open.openerLine}
1744
+ `
1745
+ };
1746
+ }
1747
+ function appendSlackSuffix(text, marker) {
1748
+ const carryover = getFenceContinuation(text);
1749
+ return `${text}${carryover?.closeSuffix ?? ""}${marker}`;
1750
+ }
1751
+ function stripTrailingContinuationMarker(text) {
1752
+ return text.endsWith(CONTINUED_MARKER) ? text.slice(0, -CONTINUED_MARKER.length) : text;
1753
+ }
1754
+ function takeSlackContinuationChunk(text, budget) {
1755
+ let { prefix, rest } = takeSlackInlinePrefix(text, budget);
1756
+ if (!rest) {
1757
+ ({ prefix, rest } = takeSlackInlinePrefix(
1758
+ text,
1759
+ forceSplitBudget(text, budget)
1760
+ ));
1761
+ }
1762
+ let carryover = rest ? getFenceContinuation(prefix) : null;
1763
+ if (!carryover) {
1764
+ return { prefix, rest };
1765
+ }
1766
+ const carryoverBudget = reserveInlineBudgetForSuffix(
1767
+ `${carryover.closeSuffix}${CONTINUED_MARKER}`
1768
+ );
1769
+ ({ prefix, rest } = takeSlackInlinePrefix(text, carryoverBudget));
1770
+ if (!rest) {
1771
+ ({ prefix, rest } = takeSlackInlinePrefix(
1772
+ text,
1773
+ forceSplitBudget(text, carryoverBudget)
1774
+ ));
1775
+ }
1776
+ carryover = rest ? getFenceContinuation(prefix) : null;
1777
+ if (!carryover) {
1778
+ return { prefix, rest };
1779
+ }
1780
+ return {
1781
+ prefix,
1782
+ rest: `${carryover.reopenPrefix}${rest}`
1783
+ };
1784
+ }
1785
+ function takeSlackContinuationPrefix(text, options) {
1786
+ const budget = {
1787
+ maxChars: options?.maxChars ?? getSlackContinuationBudget().maxChars,
1788
+ maxLines: options?.maxLines ?? getSlackContinuationBudget().maxLines
1789
+ };
1790
+ const { prefix, rest } = (() => {
1791
+ if (options?.forceSplit) {
1792
+ return takeSlackContinuationChunk(text, budget);
1793
+ }
1794
+ const initial = takeSlackInlinePrefix(text, budget);
1795
+ return initial.rest ? takeSlackContinuationChunk(text, budget) : initial;
1796
+ })();
1797
+ return {
1798
+ prefix,
1799
+ renderedPrefix: rest ? appendSlackSuffix(prefix, CONTINUED_MARKER) : prefix,
1800
+ rest
1801
+ };
1802
+ }
1803
+ function takeSlackInlinePrefix(text, options) {
1804
+ const maxChars = options?.maxChars ?? MAX_INLINE_CHARS;
1805
+ const maxLines = options?.maxLines ?? MAX_INLINE_LINES;
1806
+ const normalized = text.replace(/\r\n?/g, "\n");
1807
+ if (!normalized) {
1808
+ return { prefix: "", rest: "" };
1809
+ }
1810
+ if (fitsInlineBudget(normalized, maxChars, maxLines)) {
1811
+ return { prefix: normalized, rest: "" };
1812
+ }
1813
+ const lineBounded = splitByLineBudget(normalized, maxLines);
1814
+ const cutIndex = findSplitIndex(lineBounded, maxChars);
1815
+ const prefix = lineBounded.slice(0, cutIndex).trimEnd();
1816
+ if (prefix) {
1817
+ return {
1818
+ prefix,
1819
+ rest: normalized.slice(prefix.length).trimStart()
1820
+ };
1821
+ }
1822
+ const hardPrefix = normalized.slice(0, Math.max(1, maxChars)).trimEnd();
1823
+ return {
1824
+ prefix: hardPrefix || normalized.slice(0, Math.max(1, maxChars)),
1825
+ rest: normalized.slice(hardPrefix.length || Math.max(1, maxChars)).trimStart()
1826
+ };
1827
+ }
1828
+ function splitSlackReplyText(text, options) {
1829
+ const normalized = normalizeSlackReplyMarkdown(text);
1830
+ if (!normalized) {
1831
+ return [];
1832
+ }
1833
+ const chunks = [];
1834
+ const continuationBudget = reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1835
+ let remaining = normalized;
1836
+ while (remaining) {
1837
+ const fitsFinalChunk = options?.interrupted ? fitsInlineBudget(appendSlackSuffix(remaining, getInterruptionMarker())) : fitsInlineBudget(remaining);
1838
+ if (fitsFinalChunk) {
1839
+ chunks.push(
1840
+ options?.interrupted ? appendSlackSuffix(remaining, getInterruptionMarker()) : remaining
1841
+ );
1842
+ break;
1843
+ }
1844
+ const { renderedPrefix, rest } = takeSlackContinuationPrefix(remaining, {
1845
+ ...continuationBudget,
1846
+ forceSplit: true
1847
+ });
1848
+ chunks.push(renderedPrefix);
1849
+ remaining = rest;
1850
+ }
1851
+ if (chunks.length === 2) {
1852
+ chunks[0] = stripTrailingContinuationMarker(chunks[0] ?? "");
1853
+ }
1854
+ return chunks;
1855
+ }
1856
+ function getSlackContinuationBudget() {
1857
+ return reserveInlineBudgetForSuffix(CONTINUED_MARKER);
1858
+ }
1859
+ function buildSlackOutputMessage(text) {
1860
+ const normalized = normalizeSlackReplyMarkdown(text);
1861
+ if (!normalized) {
1862
+ throw new Error(
1863
+ `Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`
1864
+ );
1865
+ }
1866
+ return {
1867
+ markdown: normalized
1868
+ };
1869
+ }
1870
+ var slackOutputPolicy = {
1871
+ maxInlineChars: MAX_INLINE_CHARS,
1872
+ maxInlineLines: MAX_INLINE_LINES
1873
+ };
1874
+
1875
+ export {
1876
+ RetryableDeliveryError,
1877
+ actorFromRouting,
1878
+ assertRunRoutingConsistency,
1879
+ toolInvocationDestination,
1880
+ surfaceFromRouting,
1881
+ truncateStatusText,
1882
+ escapeSlackMrkdwnText,
1883
+ formatSlackLink,
1884
+ normalizeSlackStatusText,
1885
+ splitSlackReplyText,
1886
+ buildSlackOutputMessage,
1887
+ slackOutputPolicy,
1888
+ createUserTokenStore,
1889
+ issueProviderCredentialLease,
1890
+ getSlackMessagePermalink,
1891
+ postSlackMessage,
1892
+ uploadFilesToConversation,
1893
+ addReactionToMessage,
1894
+ removeReactionFromMessage,
1895
+ parseOAuthStatePayload,
1896
+ formatProviderLabel,
1897
+ resolveBaseUrl,
1898
+ deliverOAuthAuthorization,
1899
+ startOAuthFlow,
1900
+ getMcpAuthSession,
1901
+ patchMcpAuthSession,
1902
+ deleteMcpAuthSession,
1903
+ deleteMcpAuthSessionsForUserProvider,
1904
+ getMcpStoredOAuthCredentials,
1905
+ deleteMcpStoredOAuthCredentials,
1906
+ deleteMcpServerSessionId,
1907
+ createMcpOAuthClientProvider,
1908
+ finalizeMcpAuthorization,
1909
+ selectPluginGrant,
1910
+ onPluginEgressResponse,
1911
+ hasEgressCredentialHooks,
1912
+ resolvePluginOAuthAccount,
1913
+ issuePluginCredential
1914
+ };