@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.
- package/dist/{agent-hooks-WNBUY4OB.js → agent-hooks-4EUUXP2F.js} +12 -11
- package/dist/api/conversations/detail.d.ts +2 -0
- package/dist/api/conversations/event-list.d.ts +2 -0
- package/dist/api/health.d.ts +7 -0
- package/dist/api/plugin-reports.d.ts +62 -0
- package/dist/api/plugins.d.ts +9 -0
- package/dist/api/runtime.d.ts +24 -0
- package/dist/api/schema/conversation.d.ts +8 -0
- package/dist/api/schema.d.ts +2 -2
- package/dist/api/schema.js +6 -6
- package/dist/api/skills.d.ts +6 -0
- package/dist/api.js +123 -99
- package/dist/app.js +1104 -5006
- package/dist/catalog-runtime-Y32UEQLB.js +18 -0
- package/dist/chat/agent/request.d.ts +6 -0
- package/dist/chat/agent/sandbox.d.ts +2 -0
- package/dist/chat/agent/tools.d.ts +2 -0
- package/dist/chat/config.d.ts +2 -0
- package/dist/chat/conversations/history.d.ts +6 -0
- package/dist/chat/local/credential-sync.d.ts +5 -0
- package/dist/chat/local/oauth-relay.d.ts +4 -0
- package/dist/chat/mcp/oauth.d.ts +1 -0
- package/dist/chat/oauth-authorization.d.ts +17 -0
- package/dist/chat/oauth-flow.d.ts +9 -0
- package/dist/chat/sandbox/egress/schemas.d.ts +46 -0
- package/dist/chat/sandbox/egress/signals.d.ts +9 -0
- package/dist/chat/sandbox/sandbox.d.ts +2 -0
- package/dist/chat/sentry-payload-filter.d.ts +4 -4
- package/dist/chat/services/mcp-auth-orchestration.d.ts +5 -3
- package/dist/chat/services/plugin-auth-orchestration.d.ts +2 -0
- package/dist/chat/slack/message-control.d.ts +2 -0
- package/dist/chat/slack/oauth-authorization-message.d.ts +3 -0
- package/dist/chat/task-execution/slack-work.d.ts +2 -0
- package/dist/chat/tool-support/zod-tool.d.ts +1 -1
- package/dist/chat/tools/definition.d.ts +5 -0
- package/dist/{chunk-TLGM5X2M.js → chunk-2QG4MZQ7.js} +2 -2
- package/dist/{chunk-WV4UJBAS.js → chunk-43RYMWJ2.js} +523 -2373
- package/dist/chunk-4CODA7P5.js +43 -0
- package/dist/{chunk-YU6PO42Q.js → chunk-67CVNDJ7.js} +1 -1
- package/dist/{chunk-OSKEC4XU.js → chunk-6FKLPVF3.js} +1 -1
- package/dist/{chunk-2ZTTXTSZ.js → chunk-6OPTF4BQ.js} +10 -4
- package/dist/{chunk-W6RDUAO3.js → chunk-6V762AB4.js} +1 -1
- package/dist/{chunk-ABPSDO6M.js → chunk-AGLQYELI.js} +77 -8
- package/dist/{chunk-6QWPVTMO.js → chunk-CEYOL62C.js} +9 -7
- package/dist/{chunk-RD7EJI7H.js → chunk-CWEZEPD4.js} +3 -3
- package/dist/chunk-DIB4GX3G.js +8 -0
- package/dist/{chunk-K2W4CK7K.js → chunk-EW5MLKT5.js} +10 -75
- package/dist/{chunk-42WRQLJ6.js → chunk-F3B2YJ76.js} +0 -14
- package/dist/chunk-FA6POGLH.js +70 -0
- package/dist/{chunk-PM3PX42K.js → chunk-FHC4JUSF.js} +1 -1
- package/dist/{chunk-6Y5L4BOM.js → chunk-FXOYURO4.js} +5 -3
- package/dist/{chunk-S4QDH23A.js → chunk-G36V2YGB.js} +83 -81
- package/dist/{chunk-O4GT7LYB.js → chunk-G5SX4UPR.js} +3 -43
- package/dist/chunk-HDSDCEPK.js +80 -0
- package/dist/{chunk-FIGAF6KL.js → chunk-HWCQRGLL.js} +5 -3
- package/dist/{chunk-C4MZ3GP5.js → chunk-HZF5P5UN.js} +1 -1
- package/dist/chunk-LYA2AB5G.js +1914 -0
- package/dist/{chunk-4FUEG3QY.js → chunk-NVUQSFWX.js} +2 -2
- package/dist/chunk-OB42YVAE.js +16 -0
- package/dist/{chunk-XLFBQJUO.js → chunk-PVZHG2XZ.js} +4 -4
- package/dist/{chunk-VNNBSKCO.js → chunk-RAVKCKXA.js} +7 -7
- package/dist/{chunk-ZGXH3REA.js → chunk-SYJT7AKB.js} +12 -2
- package/dist/{chunk-5CZ7RETL.js → chunk-TVE25LPW.js} +1 -1
- package/dist/chunk-W3U3YQYI.js +71 -0
- package/dist/chunk-YCLO7D7M.js +4267 -0
- package/dist/cli/chat.js +72 -40
- package/dist/cli/check.js +10 -8
- package/dist/cli/init.js +1 -0
- package/dist/cli/plugins.js +16 -14
- package/dist/cli/snapshot-warmup.js +13 -11
- package/dist/cli/upgrade.js +10 -8
- package/dist/{db-4VLP7KBF.js → db-KBSJHAZU.js} +8 -7
- package/dist/handlers/sandbox-egress-signals.d.ts +4 -0
- package/dist/instrumentation.js +72 -4
- package/dist/nitro.js +2 -2
- package/dist/oauth-callback-server-EYRMJTCS.js +231 -0
- package/dist/oauth-relay-3ZU72UGL.js +9 -0
- package/dist/reporting-schema.d.ts +12 -4
- package/dist/reporting.d.ts +5 -5
- package/dist/{runner-IPXLWQOF.js → runner-RQGZ5Z2S.js} +139 -72
- package/dist/sandbox-egress-signals-CDFAP67O.js +75 -0
- package/dist/{skills-QCNZISNF.js → skills-JJQIE3BT.js} +10 -8
- package/dist/validation-MZ47PZP5.js +21 -0
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/dist/catalog-runtime-6O4M6JGA.js +0 -16
- package/dist/chat/oauth-authorization-message.d.ts +0 -6
- 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("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
540
|
+
}
|
|
541
|
+
function escapeSlackLinkUrl(url) {
|
|
542
|
+
return url.replaceAll("&", "&").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
|
+
};
|