@sentry/junior 0.183.0 → 0.185.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-SNRBZYAU.js → agent-hooks-NGZQLL5O.js} +13 -11
- package/dist/api/acp/auth.d.ts +46 -0
- package/dist/api/acp/authorization-page.d.ts +11 -0
- package/dist/api/acp/conversations.d.ts +70 -0
- package/dist/api/acp/errors.d.ts +6 -0
- package/dist/api/acp/route.d.ts +16 -0
- package/dist/api/acp/schema.d.ts +115 -0
- package/dist/api/acp/sleep.d.ts +2 -0
- package/dist/api/acp/state.d.ts +22 -0
- package/dist/api/acp/transport.d.ts +113 -0
- package/dist/api/code/overview.d.ts +39 -0
- package/dist/api/locations/detail.d.ts +1 -1
- package/dist/api/people/code.d.ts +22 -0
- package/dist/api/schema/code.d.ts +46 -0
- package/dist/api/schema/conversation.d.ts +100 -20
- package/dist/api/schema/location.d.ts +1 -1
- package/dist/api/schema/person.d.ts +1 -1
- package/dist/api/schema.d.ts +4 -4
- package/dist/api/schema.js +12 -1
- package/dist/api.js +358 -76
- package/dist/app.d.ts +0 -4
- package/dist/app.js +2450 -493
- package/dist/{catalog-runtime-3I3JOIIH.js → catalog-runtime-AAEAO2RL.js} +4 -4
- package/dist/chat/api-turns/accepted-reply.d.ts +11 -0
- package/dist/chat/api-turns/cancellation.d.ts +3 -5
- package/dist/chat/api-turns/work.d.ts +4 -9
- package/dist/chat/app/conversation-work.d.ts +1 -1
- package/dist/chat/app/factory.d.ts +4 -0
- package/dist/chat/app/services.d.ts +4 -3
- package/dist/chat/conversations/history.d.ts +83 -9
- package/dist/chat/conversations/turn-lifecycle.d.ts +4 -3
- package/dist/chat/providers/slack/input.d.ts +32 -0
- package/dist/chat/{runtime → providers/slack}/processing-reaction.d.ts +6 -8
- package/dist/chat/{runtime/slack-resume.d.ts → providers/slack/resume.d.ts} +16 -19
- package/dist/chat/providers/slack/runtime.d.ts +1 -10
- package/dist/chat/providers/slack/turn.d.ts +11 -24
- package/dist/chat/resource-events/store.d.ts +1 -1
- package/dist/chat/resource-events/tool-support.d.ts +0 -5
- package/dist/chat/runtime/turn-execution.d.ts +7 -2
- package/dist/chat/sandbox/egress/credentials.d.ts +3 -3
- package/dist/chat/sandbox/egress/session.d.ts +5 -6
- package/dist/chat/scheduled-tasks/tool-support.d.ts +5 -5
- package/dist/chat/scheduled-tasks/tools/create.d.ts +2 -2
- package/dist/chat/scheduled-tasks/tools/delete.d.ts +2 -2
- package/dist/chat/scheduled-tasks/tools/list.d.ts +2 -2
- package/dist/chat/scheduled-tasks/tools/run-now.d.ts +2 -2
- package/dist/chat/scheduled-tasks/tools/update.d.ts +2 -2
- package/dist/chat/scheduled-tasks/types.d.ts +1 -1
- package/dist/chat/services/provider-retry.d.ts +2 -0
- package/dist/chat/services/turn-failure-response.d.ts +4 -0
- package/dist/chat/task-execution/paused-turn.d.ts +0 -2
- package/dist/chat/task-execution/state.d.ts +2 -2
- package/dist/chat/task-execution/store.d.ts +1 -1
- package/dist/chat/task-execution/turn-wake.d.ts +12 -0
- package/dist/chat/tool-support/action-review.d.ts +1 -1
- package/dist/chat/tools/resource-events.d.ts +1 -1
- package/dist/chat/tools/search-conversation-events.d.ts +1 -1
- package/dist/chat/tools/types.d.ts +1 -1
- package/dist/chat/tools/watch-resource-events.d.ts +2 -2
- package/dist/{chunk-2EPYYVJC.js → chunk-4HA36IWS.js} +9 -94
- package/dist/{chunk-WVIA7V3M.js → chunk-4K25ODAO.js} +27 -1
- package/dist/{chunk-V64JCZUV.js → chunk-4S47VHJR.js} +3 -3
- package/dist/{chunk-QZV76CM4.js → chunk-6DOAUXLD.js} +231 -198
- package/dist/{chunk-M3QYTBQ6.js → chunk-6Q7YZLCL.js} +2 -1
- package/dist/{chunk-W46NIB5H.js → chunk-7GAP2XG4.js} +2 -3
- package/dist/{chunk-EENQGEIP.js → chunk-7MHNIGHP.js} +1 -1
- package/dist/{chunk-AX3UDIMA.js → chunk-ABAX75GG.js} +10 -384
- package/dist/{chunk-KTXOGLNE.js → chunk-ATUWN6Z3.js} +2 -2
- package/dist/{chunk-3HJ4F4L7.js → chunk-C6TS47CQ.js} +4 -4
- package/dist/chunk-D4YV5TDV.js +45 -0
- package/dist/{chunk-HUK5XIZL.js → chunk-D63IUQQE.js} +4 -4
- package/dist/{chunk-WOKJRLM2.js → chunk-DAPC2HTM.js} +24 -6
- package/dist/{chunk-2ADFGSUS.js → chunk-EIJCPJOY.js} +1 -1
- package/dist/{chunk-X52ESOBP.js → chunk-EJYP3NQ4.js} +4 -4
- package/dist/{chunk-35PM4YY5.js → chunk-EWEH2PJW.js} +6 -6
- package/dist/{chunk-W5JMPEBO.js → chunk-FFHXG2FX.js} +13 -11
- package/dist/{chunk-FJMTMGBC.js → chunk-FTVMXDNN.js} +1 -1
- package/dist/chunk-GFBR4S3Q.js +406 -0
- package/dist/{chunk-E3OBGRHN.js → chunk-GJTIZQGJ.js} +1 -1
- package/dist/{chunk-IO6KFXYE.js → chunk-HJPC32TQ.js} +12 -50
- package/dist/{chunk-RF6J3DIO.js → chunk-INQHUE72.js} +84 -1
- package/dist/{chunk-LMNMMESF.js → chunk-LRI5IRGY.js} +19 -44
- package/dist/{chunk-6UXDTPEN.js → chunk-LVK4WJKM.js} +1 -1
- package/dist/{chunk-AYW7J6K4.js → chunk-MWOH6CIK.js} +178 -148
- package/dist/{chunk-LZBPGX5A.js → chunk-OMQ2J6F6.js} +1 -1
- package/dist/{chunk-G2B2XZQW.js → chunk-PNYJOEVV.js} +1 -1
- package/dist/{chunk-37IQ6DGU.js → chunk-QCT4226Q.js} +3 -3
- package/dist/{chunk-R767Y7QV.js → chunk-QZKHHG3B.js} +34 -8
- package/dist/{chunk-VOV3SCM3.js → chunk-TVVAMOLS.js} +2 -2
- package/dist/{chunk-56XONJPF.js → chunk-U4CTAVWN.js} +1 -1
- package/dist/{chunk-4WGFWHWP.js → chunk-UUULGCKK.js} +1 -1
- package/dist/{chunk-AHMWPFIW.js → chunk-VH4IU3UC.js} +928 -984
- package/dist/chunk-W7GCLNIV.js +58 -0
- package/dist/{chunk-24OJZ5OU.js → chunk-WLKWN4CD.js} +22 -12
- package/dist/{chunk-T5MTGX2E.js → chunk-X2SS6BF7.js} +201 -210
- package/dist/{chunk-SQHFVIMZ.js → chunk-X7NCJQAA.js} +1 -1
- package/dist/{chunk-PLZT45VC.js → chunk-Y5G23T2D.js} +5 -5
- package/dist/{chunk-3L7CWDQQ.js → chunk-ZIF2S464.js} +1 -1
- package/dist/{chunk-QI5JW5AG.js → chunk-ZKKGFUWM.js} +6 -6
- package/dist/cli/chat.js +35 -33
- package/dist/cli/check.js +5 -5
- package/dist/cli/plugins.js +14 -12
- package/dist/cli/snapshot-create.js +10 -9
- package/dist/cli/upgrade.js +7 -6
- package/dist/{db-K2VMNLHQ.js → db-TRJ77FAU.js} +8 -6
- package/dist/instrumentation.js +3 -3
- package/dist/nitro.js +7 -6
- package/dist/{oauth-callback-server-DZZMTKSS.js → oauth-callback-server-XD5SV3MS.js} +30 -27
- package/dist/{runner-IIAKWDXI.js → runner-KFX7NG6D.js} +24 -19
- package/dist/{skills-EXIQ7BP5.js → skills-J2AH26CW.js} +5 -5
- package/dist/spawn-VU3CSRI6.js +41 -0
- package/dist/task-runner-5JG3VIIY.js +34 -0
- package/dist/{validation-XUGIWBJF.js → validation-O5J4VOPF.js} +5 -5
- package/dist/version.js +1 -1
- package/dist/{work-G4RLJ634.js → work-ECPQTG3U.js} +24 -19
- package/dist/worker-RS3DXUDO.js +24 -0
- package/package.json +5 -6
- package/dist/app-adapter.d.ts +0 -85
- package/dist/chat/api-turns/adapter-conversations.d.ts +0 -15
- package/dist/spawn-ZR6OOEAN.js +0 -37
- package/dist/task-runner-C52HYCVD.js +0 -32
- package/dist/worker-R2JEHK67.js +0 -22
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
getConversationEventStore,
|
|
6
6
|
getDb,
|
|
7
7
|
getSqlExecutor
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ABAX75GG.js";
|
|
9
9
|
import {
|
|
10
10
|
juniorCodeChanges,
|
|
11
11
|
juniorCodeRepositories,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
juniorUsers,
|
|
17
17
|
juniorWorkspaceRepos,
|
|
18
18
|
juniorWorkspaces
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-LVK4WJKM.js";
|
|
20
20
|
import {
|
|
21
21
|
SANDBOX_WORKSPACE_ROOT,
|
|
22
22
|
getVercelSandboxCredentials,
|
|
@@ -24,17 +24,19 @@ import {
|
|
|
24
24
|
isSandboxMissingError,
|
|
25
25
|
runNonInteractiveCommand,
|
|
26
26
|
sleep
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-EJYP3NQ4.js";
|
|
28
28
|
import {
|
|
29
29
|
getStateAdapter
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-PNYJOEVV.js";
|
|
31
31
|
import {
|
|
32
32
|
botConfig,
|
|
33
33
|
completeObject,
|
|
34
|
-
defaultModelId,
|
|
35
34
|
embedTexts,
|
|
36
35
|
getSlackBotToken
|
|
37
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-LRI5IRGY.js";
|
|
37
|
+
import {
|
|
38
|
+
defaultModelId
|
|
39
|
+
} from "./chunk-D4YV5TDV.js";
|
|
38
40
|
import {
|
|
39
41
|
isActorUserId,
|
|
40
42
|
parseActorUserId,
|
|
@@ -50,7 +52,7 @@ import {
|
|
|
50
52
|
setSpanAttributes,
|
|
51
53
|
setSpanStatus,
|
|
52
54
|
withSpan
|
|
53
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-OMQ2J6F6.js";
|
|
54
56
|
|
|
55
57
|
// src/chat/plugins/agent-hooks.ts
|
|
56
58
|
import {
|
|
@@ -468,1051 +470,996 @@ function getDashboardTaskLink(taskId) {
|
|
|
468
470
|
return resolveDashboardPath(`/tasks/${encodeURIComponent(taskId)}`);
|
|
469
471
|
}
|
|
470
472
|
|
|
471
|
-
// src/chat/
|
|
472
|
-
import {
|
|
473
|
-
import {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
473
|
+
// src/chat/resource-events/store.ts
|
|
474
|
+
import { createHash as createHash2 } from "crypto";
|
|
475
|
+
import {
|
|
476
|
+
destinationSchema,
|
|
477
|
+
resourceEventMatchSchema,
|
|
478
|
+
resourceEventMatches,
|
|
479
|
+
resourceEventTypeSchema,
|
|
480
|
+
stableResourceEventMatchKey
|
|
481
|
+
} from "@sentry/junior-plugin-api";
|
|
482
|
+
import { z as z2 } from "zod";
|
|
483
|
+
var RESOURCE_EVENT_PREFIX = "junior:resource_event_subscription:v3";
|
|
484
|
+
var INDEX_LOCK_TTL_MS = 1e4;
|
|
485
|
+
var SUBSCRIPTION_LOCK_TTL_MS = 1e4;
|
|
486
|
+
var SUBSCRIPTION_LOCK_WAIT_MS = 1e4;
|
|
487
|
+
var SUBSCRIPTION_LOCK_RETRY_MS = 25;
|
|
488
|
+
var SUBSCRIPTION_LOCK_HEARTBEAT_MS = 3e3;
|
|
489
|
+
var subscriptionStatusSchema = z2.enum(["active", "cancelled", "completed"]);
|
|
490
|
+
var subscriptionIdIndexSchema = z2.array(z2.string().min(1));
|
|
491
|
+
var subscriptionSchema = z2.object({
|
|
492
|
+
conversationId: z2.string().min(1),
|
|
493
|
+
createdAtMs: z2.number().finite(),
|
|
494
|
+
destination: destinationSchema,
|
|
495
|
+
events: z2.array(resourceEventTypeSchema).min(1),
|
|
496
|
+
expiresAtMs: z2.number().finite(),
|
|
497
|
+
id: z2.string().min(1),
|
|
498
|
+
intent: z2.string().min(1),
|
|
499
|
+
label: z2.string().min(1),
|
|
500
|
+
match: resourceEventMatchSchema.optional(),
|
|
501
|
+
namespace: z2.string().min(1),
|
|
502
|
+
identifier: z2.string().min(1),
|
|
503
|
+
resourceType: z2.string().min(1),
|
|
504
|
+
status: subscriptionStatusSchema,
|
|
505
|
+
updatedAtMs: z2.number().finite()
|
|
506
|
+
}).strict();
|
|
507
|
+
function digest(value) {
|
|
508
|
+
return createHash2("sha256").update(value).digest("hex").slice(0, 32);
|
|
481
509
|
}
|
|
482
|
-
function
|
|
483
|
-
return
|
|
510
|
+
function subscriptionKey(id) {
|
|
511
|
+
return `${RESOURCE_EVENT_PREFIX}:record:${id}`;
|
|
484
512
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
var SlackActionError = class extends Error {
|
|
488
|
-
code;
|
|
489
|
-
apiError;
|
|
490
|
-
needed;
|
|
491
|
-
provided;
|
|
492
|
-
statusCode;
|
|
493
|
-
requestId;
|
|
494
|
-
errorData;
|
|
495
|
-
retryAfterSeconds;
|
|
496
|
-
detail;
|
|
497
|
-
detailLine;
|
|
498
|
-
detailRule;
|
|
499
|
-
constructor(message, code, options = {}) {
|
|
500
|
-
super(message);
|
|
501
|
-
this.name = "SlackActionError";
|
|
502
|
-
this.code = code;
|
|
503
|
-
this.apiError = options.apiError;
|
|
504
|
-
this.needed = options.needed;
|
|
505
|
-
this.provided = options.provided;
|
|
506
|
-
this.statusCode = options.statusCode;
|
|
507
|
-
this.requestId = options.requestId;
|
|
508
|
-
this.errorData = options.errorData;
|
|
509
|
-
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
510
|
-
this.detail = options.detail;
|
|
511
|
-
this.detailLine = options.detailLine;
|
|
512
|
-
this.detailRule = options.detailRule;
|
|
513
|
-
}
|
|
514
|
-
};
|
|
515
|
-
function serializeSlackErrorData(data) {
|
|
516
|
-
if (!data || typeof data !== "object") {
|
|
517
|
-
return void 0;
|
|
518
|
-
}
|
|
519
|
-
const filtered = Object.fromEntries(
|
|
520
|
-
Object.entries(data).filter(
|
|
521
|
-
([key]) => key !== "error"
|
|
522
|
-
)
|
|
523
|
-
);
|
|
524
|
-
if (Object.keys(filtered).length === 0) {
|
|
525
|
-
return void 0;
|
|
526
|
-
}
|
|
527
|
-
try {
|
|
528
|
-
const serialized = JSON.stringify(filtered);
|
|
529
|
-
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
530
|
-
} catch {
|
|
531
|
-
return void 0;
|
|
532
|
-
}
|
|
513
|
+
function subscriptionLockKey(id) {
|
|
514
|
+
return `${RESOURCE_EVENT_PREFIX}:lock:${id}`;
|
|
533
515
|
}
|
|
534
|
-
function
|
|
535
|
-
|
|
536
|
-
|
|
516
|
+
function resourceIndexKey(teamId, namespace, identifier) {
|
|
517
|
+
return `${RESOURCE_EVENT_PREFIX}:resource:${digest(`${teamId}\0${namespace}\0${identifier}`)}`;
|
|
518
|
+
}
|
|
519
|
+
function subscriptionTeamId(destination) {
|
|
520
|
+
if (destination.platform !== "slack") {
|
|
521
|
+
throw new Error("Resource event subscriptions require a Slack destination");
|
|
537
522
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
523
|
+
return destination.teamId;
|
|
524
|
+
}
|
|
525
|
+
function conversationIndexKey(conversationId) {
|
|
526
|
+
return `${RESOURCE_EVENT_PREFIX}:conversation:${digest(conversationId)}`;
|
|
527
|
+
}
|
|
528
|
+
function indexLockKey(key) {
|
|
529
|
+
return `${key}:lock`;
|
|
530
|
+
}
|
|
531
|
+
async function sleep2(ms) {
|
|
532
|
+
await new Promise((resolve) => {
|
|
533
|
+
const timer = setTimeout(resolve, ms);
|
|
534
|
+
timer.unref?.();
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
async function acquireSubscriptionLock(state, subscriptionId, waitDeadlineMs = Date.now() + SUBSCRIPTION_LOCK_WAIT_MS) {
|
|
538
|
+
while (true) {
|
|
539
|
+
const lock = await state.acquireLock(
|
|
540
|
+
subscriptionLockKey(subscriptionId),
|
|
541
|
+
SUBSCRIPTION_LOCK_TTL_MS
|
|
542
|
+
);
|
|
543
|
+
if (lock) {
|
|
544
|
+
return lock;
|
|
545
|
+
}
|
|
546
|
+
if (Date.now() >= waitDeadlineMs) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`Could not acquire resource event subscription lock for ${subscriptionId}`
|
|
549
|
+
);
|
|
546
550
|
}
|
|
551
|
+
await sleep2(SUBSCRIPTION_LOCK_RETRY_MS);
|
|
547
552
|
}
|
|
548
|
-
return void 0;
|
|
549
553
|
}
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (lineMatch) {
|
|
563
|
-
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
564
|
-
if (Number.isFinite(line)) {
|
|
565
|
-
parsed.detailLine = line;
|
|
554
|
+
async function withSubscriptionLock(state, subscriptionId, callback, waitDeadlineMs) {
|
|
555
|
+
const lock = await acquireSubscriptionLock(
|
|
556
|
+
state,
|
|
557
|
+
subscriptionId,
|
|
558
|
+
waitDeadlineMs
|
|
559
|
+
);
|
|
560
|
+
let lockLostError;
|
|
561
|
+
let heartbeatError;
|
|
562
|
+
let heartbeat;
|
|
563
|
+
const timer = setInterval(() => {
|
|
564
|
+
if (heartbeat) {
|
|
565
|
+
return;
|
|
566
566
|
}
|
|
567
|
+
heartbeat = state.extendLock(lock, SUBSCRIPTION_LOCK_TTL_MS).then((extended) => {
|
|
568
|
+
if (extended) {
|
|
569
|
+
heartbeatError = void 0;
|
|
570
|
+
} else {
|
|
571
|
+
lockLostError = new Error(
|
|
572
|
+
`Resource event subscription lock was lost for ${subscriptionId}`
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}).catch((error) => {
|
|
576
|
+
heartbeatError = error instanceof Error ? error : new Error(`Failed to extend subscription lock: ${String(error)}`);
|
|
577
|
+
}).finally(() => {
|
|
578
|
+
heartbeat = void 0;
|
|
579
|
+
});
|
|
580
|
+
}, SUBSCRIPTION_LOCK_HEARTBEAT_MS);
|
|
581
|
+
timer.unref?.();
|
|
582
|
+
let result;
|
|
583
|
+
try {
|
|
584
|
+
result = await callback();
|
|
585
|
+
} finally {
|
|
586
|
+
clearInterval(timer);
|
|
587
|
+
await heartbeat;
|
|
588
|
+
await state.releaseLock(lock);
|
|
567
589
|
}
|
|
568
|
-
|
|
569
|
-
|
|
590
|
+
const lockError = lockLostError ?? heartbeatError;
|
|
591
|
+
if (lockError) {
|
|
592
|
+
throw lockError;
|
|
570
593
|
}
|
|
571
|
-
return
|
|
594
|
+
return result;
|
|
572
595
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
function runWithSlackInstallationToken(token, fn) {
|
|
576
|
-
const trimmed = token.trim();
|
|
577
|
-
if (!trimmed) {
|
|
578
|
-
throw new SlackActionError(
|
|
579
|
-
"Slack installation token binding requires a non-empty token",
|
|
580
|
-
"missing_token"
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
return installationTokenStorage.run({ token: trimmed }, fn);
|
|
596
|
+
function ttlUntil(expiresAtMs, nowMs) {
|
|
597
|
+
return Math.max(1, expiresAtMs - nowMs);
|
|
584
598
|
}
|
|
585
|
-
function
|
|
586
|
-
const
|
|
587
|
-
if (
|
|
588
|
-
return
|
|
599
|
+
async function readSubscriptionIdIndex(state, key) {
|
|
600
|
+
const value = await state.get(key);
|
|
601
|
+
if (value === void 0 || value === null) {
|
|
602
|
+
return [];
|
|
589
603
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
604
|
+
return subscriptionIdIndexSchema.parse(value);
|
|
605
|
+
}
|
|
606
|
+
function buildSubscriptionId(input) {
|
|
607
|
+
const eventKey = [...new Set(input.events)].sort().join("\0");
|
|
608
|
+
const matchKey = stableResourceEventMatchKey(input.match);
|
|
609
|
+
const material = matchKey ? `${input.teamId}\0${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}\0${matchKey}` : `${input.teamId}\0${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}`;
|
|
610
|
+
return `resub_${digest(material)}`;
|
|
611
|
+
}
|
|
612
|
+
async function withIndexLock(state, key, callback) {
|
|
613
|
+
const lock = await state.acquireLock(indexLockKey(key), INDEX_LOCK_TTL_MS);
|
|
614
|
+
if (!lock) {
|
|
615
|
+
throw new Error(`Could not acquire resource event index lock for ${key}`);
|
|
593
616
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
"missing_token"
|
|
599
|
-
);
|
|
617
|
+
try {
|
|
618
|
+
return await callback();
|
|
619
|
+
} finally {
|
|
620
|
+
await state.releaseLock(lock);
|
|
600
621
|
}
|
|
601
|
-
throw new SlackActionError(
|
|
602
|
-
"SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service",
|
|
603
|
-
"missing_token"
|
|
604
|
-
);
|
|
605
622
|
}
|
|
606
|
-
function
|
|
607
|
-
|
|
623
|
+
async function addToIndex(state, key, subscriptionId, nowMs) {
|
|
624
|
+
await withIndexLock(state, key, async () => {
|
|
625
|
+
const ids = [...new Set(await readSubscriptionIdIndex(state, key))];
|
|
626
|
+
const next = ids.includes(subscriptionId) ? ids : [...ids, subscriptionId];
|
|
627
|
+
await state.set(key, next, await indexTtlMs(state, next, nowMs));
|
|
628
|
+
});
|
|
608
629
|
}
|
|
609
|
-
function
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
rejectRateLimitedCalls: true
|
|
616
|
-
});
|
|
617
|
-
clientsByToken.set(token, cached);
|
|
618
|
-
}
|
|
619
|
-
return cached;
|
|
630
|
+
async function removeFromIndex(state, key, subscriptionId, nowMs) {
|
|
631
|
+
await withIndexLock(state, key, async () => {
|
|
632
|
+
const existing = await readSubscriptionIdIndex(state, key);
|
|
633
|
+
const next = existing.filter((id) => id !== subscriptionId);
|
|
634
|
+
await state.set(key, next, await indexTtlMs(state, next, nowMs));
|
|
635
|
+
});
|
|
620
636
|
}
|
|
621
|
-
function
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
};
|
|
635
|
-
if (apiError === "missing_scope") {
|
|
636
|
-
return new SlackActionError(message, "missing_scope", {
|
|
637
|
-
...baseOptions,
|
|
638
|
-
needed: candidate.data?.needed,
|
|
639
|
-
provided: candidate.data?.provided
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
if (apiError === "not_in_channel") {
|
|
643
|
-
return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
644
|
-
}
|
|
645
|
-
if (apiError === "restricted_action_read_only_channel") {
|
|
646
|
-
return new SlackActionError(message, "read_only_channel", baseOptions);
|
|
647
|
-
}
|
|
648
|
-
if (apiError === "already_reacted") {
|
|
649
|
-
return new SlackActionError(message, "already_reacted", baseOptions);
|
|
650
|
-
}
|
|
651
|
-
if (apiError === "no_reaction") {
|
|
652
|
-
return new SlackActionError(message, "no_reaction", baseOptions);
|
|
653
|
-
}
|
|
654
|
-
if (apiError === "invalid_arguments") {
|
|
655
|
-
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
656
|
-
}
|
|
657
|
-
if (apiError === "invalid_cursor") {
|
|
658
|
-
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
659
|
-
}
|
|
660
|
-
if (apiError === "invalid_name") {
|
|
661
|
-
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
662
|
-
}
|
|
663
|
-
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
|
|
664
|
-
return new SlackActionError(message, "not_found", baseOptions);
|
|
665
|
-
}
|
|
666
|
-
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
|
|
667
|
-
return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
668
|
-
}
|
|
669
|
-
if (apiError === "canvas_creation_failed") {
|
|
670
|
-
return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
671
|
-
}
|
|
672
|
-
if (apiError === "canvas_editing_failed") {
|
|
673
|
-
return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
674
|
-
}
|
|
675
|
-
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
|
|
676
|
-
return new SlackActionError(message, "rate_limited", {
|
|
677
|
-
...baseOptions,
|
|
678
|
-
retryAfterSeconds: candidate.retryAfter
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
return new SlackActionError(message, "internal_error", baseOptions);
|
|
637
|
+
async function indexTtlMs(state, subscriptionIds, nowMs) {
|
|
638
|
+
const records = await Promise.all(
|
|
639
|
+
subscriptionIds.map(
|
|
640
|
+
async (id) => parseSubscription(await state.get(subscriptionKey(id)))
|
|
641
|
+
)
|
|
642
|
+
);
|
|
643
|
+
const latestExpiresAtMs = Math.max(
|
|
644
|
+
nowMs,
|
|
645
|
+
...records.filter(
|
|
646
|
+
(record) => record !== void 0 && activeAt(record, nowMs)
|
|
647
|
+
).map((record) => record.expiresAtMs)
|
|
648
|
+
);
|
|
649
|
+
return ttlUntil(latestExpiresAtMs, nowMs);
|
|
682
650
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
686
|
-
"ECONNREFUSED",
|
|
687
|
-
"ECONNRESET",
|
|
688
|
-
"ENOTFOUND",
|
|
689
|
-
"EAI_AGAIN"
|
|
690
|
-
]);
|
|
691
|
-
var TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
692
|
-
"ETIMEDOUT",
|
|
693
|
-
"ECONNABORTED",
|
|
694
|
-
"ESOCKETTIMEDOUT"
|
|
695
|
-
]);
|
|
696
|
-
function findNetworkErrorCode(error, depth = 0) {
|
|
697
|
-
if (!error || typeof error !== "object" || depth > 4) {
|
|
651
|
+
function parseSubscription(value) {
|
|
652
|
+
if (value === void 0 || value === null) {
|
|
698
653
|
return void 0;
|
|
699
654
|
}
|
|
700
|
-
|
|
701
|
-
if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) {
|
|
702
|
-
return candidate.code;
|
|
703
|
-
}
|
|
704
|
-
return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
|
|
655
|
+
return subscriptionSchema.parse(value);
|
|
705
656
|
}
|
|
706
|
-
function
|
|
707
|
-
return
|
|
657
|
+
function activeAt(subscription, nowMs) {
|
|
658
|
+
return subscription.status === "active" && subscription.expiresAtMs > nowMs;
|
|
708
659
|
}
|
|
709
|
-
function
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
return "timeout";
|
|
660
|
+
function matchesEvent(subscription, input) {
|
|
661
|
+
return subscription.namespace === input.namespace && subscription.identifier === input.identifier && subscriptionTeamId(subscription.destination) === input.teamId && subscription.events.includes(input.eventType) && activeAt(subscription, input.nowMs) && resourceEventMatches(subscription.match, input.data);
|
|
662
|
+
}
|
|
663
|
+
async function createResourceEventSubscription(input, options = {}) {
|
|
664
|
+
const state = options.state ?? getStateAdapter();
|
|
665
|
+
await state.connect();
|
|
666
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
667
|
+
const events = [...new Set(input.events.map((event) => event.trim()))].filter(
|
|
668
|
+
Boolean
|
|
669
|
+
);
|
|
670
|
+
if (events.length === 0) {
|
|
671
|
+
throw new Error("Resource event subscription requires at least one event");
|
|
722
672
|
}
|
|
723
|
-
if (
|
|
724
|
-
|
|
673
|
+
if (input.expiresAtMs <= nowMs) {
|
|
674
|
+
throw new Error("Resource event subscription expiry must be in the future");
|
|
725
675
|
}
|
|
726
|
-
|
|
676
|
+
const match = input.match && Object.keys(input.match).length > 0 ? input.match : void 0;
|
|
677
|
+
const id = buildSubscriptionId({
|
|
678
|
+
conversationId: input.conversationId,
|
|
679
|
+
events,
|
|
680
|
+
match,
|
|
681
|
+
namespace: input.namespace,
|
|
682
|
+
identifier: input.identifier,
|
|
683
|
+
teamId: subscriptionTeamId(input.destination)
|
|
684
|
+
});
|
|
685
|
+
const record = {
|
|
686
|
+
conversationId: input.conversationId,
|
|
687
|
+
createdAtMs: nowMs,
|
|
688
|
+
destination: input.destination,
|
|
689
|
+
events,
|
|
690
|
+
expiresAtMs: input.expiresAtMs,
|
|
691
|
+
id,
|
|
692
|
+
intent: input.intent,
|
|
693
|
+
label: input.label,
|
|
694
|
+
...match ? { match } : void 0,
|
|
695
|
+
namespace: input.namespace,
|
|
696
|
+
identifier: input.identifier,
|
|
697
|
+
resourceType: input.resourceType,
|
|
698
|
+
status: "active",
|
|
699
|
+
updatedAtMs: nowMs
|
|
700
|
+
};
|
|
701
|
+
const parsed = subscriptionSchema.parse(record);
|
|
702
|
+
await state.set(
|
|
703
|
+
subscriptionKey(id),
|
|
704
|
+
parsed,
|
|
705
|
+
ttlUntil(parsed.expiresAtMs, nowMs)
|
|
706
|
+
);
|
|
707
|
+
await addToIndex(
|
|
708
|
+
state,
|
|
709
|
+
resourceIndexKey(
|
|
710
|
+
subscriptionTeamId(parsed.destination),
|
|
711
|
+
input.namespace,
|
|
712
|
+
input.identifier
|
|
713
|
+
),
|
|
714
|
+
id,
|
|
715
|
+
nowMs
|
|
716
|
+
);
|
|
717
|
+
await addToIndex(
|
|
718
|
+
state,
|
|
719
|
+
conversationIndexKey(input.conversationId),
|
|
720
|
+
id,
|
|
721
|
+
nowMs
|
|
722
|
+
);
|
|
723
|
+
return parsed;
|
|
727
724
|
}
|
|
728
|
-
async function
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
try {
|
|
736
|
-
return await withSpan(
|
|
737
|
-
`POST slack.com/api/${action}`,
|
|
738
|
-
"http.client",
|
|
739
|
-
{},
|
|
740
|
-
async () => {
|
|
741
|
-
try {
|
|
742
|
-
return await task();
|
|
743
|
-
} catch (error) {
|
|
744
|
-
const mapped = mapSlackError(error);
|
|
745
|
-
const errorAttrs = {
|
|
746
|
-
"error.type": mapped.code
|
|
747
|
-
};
|
|
748
|
-
if (mapped.apiError) {
|
|
749
|
-
errorAttrs["app.slack.api_error_code"] = mapped.apiError;
|
|
750
|
-
}
|
|
751
|
-
if (mapped.code === "rate_limited") {
|
|
752
|
-
errorAttrs["http.response.status_code"] = 429;
|
|
753
|
-
if (mapped.retryAfterSeconds) {
|
|
754
|
-
errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
|
|
755
|
-
}
|
|
756
|
-
} else if (mapped.statusCode != null) {
|
|
757
|
-
errorAttrs["http.response.status_code"] = mapped.statusCode;
|
|
758
|
-
}
|
|
759
|
-
setSpanAttributes(errorAttrs);
|
|
760
|
-
setSpanStatus("error");
|
|
761
|
-
throw error;
|
|
762
|
-
}
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
"http.request.method": "POST",
|
|
766
|
-
"server.address": "slack.com",
|
|
767
|
-
"url.scheme": "https",
|
|
768
|
-
"url.path": `/api/${action}`,
|
|
769
|
-
"app.slack.method": action,
|
|
770
|
-
"app.retry.max_attempts": maxAttempts,
|
|
771
|
-
...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
|
|
772
|
-
...context.attributes ?? {},
|
|
773
|
-
...context.spanAttributes ?? {}
|
|
774
|
-
}
|
|
775
|
-
);
|
|
776
|
-
} catch (error) {
|
|
777
|
-
const mapped = mapSlackError(error);
|
|
778
|
-
const retryClass = classifySlackRetry(error, mapped);
|
|
779
|
-
const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
|
|
780
|
-
const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
|
|
781
|
-
const baseLogAttributes = {
|
|
782
|
-
"app.slack.action": action,
|
|
783
|
-
"app.slack.error_code": mapped.code,
|
|
784
|
-
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
|
|
785
|
-
...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
|
|
786
|
-
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
|
|
787
|
-
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
|
|
788
|
-
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
|
|
789
|
-
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
|
|
790
|
-
...context.attributes ?? {}
|
|
791
|
-
};
|
|
792
|
-
if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
|
|
793
|
-
logWarn("slack.action.failed", {
|
|
794
|
-
...baseLogAttributes,
|
|
795
|
-
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
|
|
796
|
-
});
|
|
797
|
-
throw mapped;
|
|
798
|
-
}
|
|
799
|
-
logWarn("slack.action.retrying", {
|
|
800
|
-
...baseLogAttributes,
|
|
801
|
-
"app.slack.retry_attempt": attempt,
|
|
802
|
-
"app.slack.retry_class": retryClass
|
|
803
|
-
});
|
|
804
|
-
const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
805
|
-
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
806
|
-
const delayMs = Math.min(
|
|
807
|
-
retryAfterMs ?? backoffMs,
|
|
808
|
-
MAX_RETRY_DELAY_MS,
|
|
809
|
-
remainingDelayBudgetMs
|
|
810
|
-
);
|
|
811
|
-
totalDelayMs += delayMs;
|
|
812
|
-
await sleep(delayMs);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
throw new SlackActionError(
|
|
816
|
-
"Slack action exhausted retries",
|
|
817
|
-
"internal_error"
|
|
725
|
+
async function listResourceEventSubscriptions(input) {
|
|
726
|
+
const state = input.state ?? getStateAdapter();
|
|
727
|
+
await state.connect();
|
|
728
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
729
|
+
const ids = await readSubscriptionIdIndex(
|
|
730
|
+
state,
|
|
731
|
+
conversationIndexKey(input.conversationId)
|
|
818
732
|
);
|
|
733
|
+
const records = await Promise.all(
|
|
734
|
+
ids.map(
|
|
735
|
+
async (id) => parseSubscription(await state.get(subscriptionKey(id)))
|
|
736
|
+
)
|
|
737
|
+
);
|
|
738
|
+
return records.filter(
|
|
739
|
+
(record) => record !== void 0 && record.conversationId === input.conversationId && activeAt(record, nowMs)
|
|
740
|
+
).sort((left, right) => left.createdAtMs - right.createdAtMs);
|
|
819
741
|
}
|
|
820
|
-
function
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
742
|
+
async function cancelResourceEventSubscription(input) {
|
|
743
|
+
const state = input.state ?? getStateAdapter();
|
|
744
|
+
await state.connect();
|
|
745
|
+
return await withSubscriptionLock(state, input.id, async () => {
|
|
746
|
+
const current = parseSubscription(
|
|
747
|
+
await state.get(subscriptionKey(input.id))
|
|
748
|
+
);
|
|
749
|
+
if (!current || current.conversationId !== input.conversationId) {
|
|
750
|
+
return void 0;
|
|
751
|
+
}
|
|
752
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
753
|
+
const next = {
|
|
754
|
+
...current,
|
|
755
|
+
status: "cancelled",
|
|
756
|
+
updatedAtMs: nowMs
|
|
757
|
+
};
|
|
758
|
+
await state.set(
|
|
759
|
+
subscriptionKey(input.id),
|
|
760
|
+
next,
|
|
761
|
+
JUNIOR_THREAD_STATE_TTL_MS
|
|
762
|
+
);
|
|
763
|
+
await removeFromIndex(
|
|
764
|
+
state,
|
|
765
|
+
resourceIndexKey(
|
|
766
|
+
subscriptionTeamId(current.destination),
|
|
767
|
+
current.namespace,
|
|
768
|
+
current.identifier
|
|
769
|
+
),
|
|
770
|
+
input.id,
|
|
771
|
+
nowMs
|
|
772
|
+
);
|
|
773
|
+
await removeFromIndex(
|
|
774
|
+
state,
|
|
775
|
+
conversationIndexKey(current.conversationId),
|
|
776
|
+
input.id,
|
|
777
|
+
nowMs
|
|
778
|
+
);
|
|
779
|
+
return next;
|
|
780
|
+
});
|
|
831
781
|
}
|
|
832
|
-
function
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
782
|
+
async function cancelSubscriptions(input) {
|
|
783
|
+
const subscriptions = await listResourceEventSubscriptions(input);
|
|
784
|
+
for (const subscription of subscriptions) {
|
|
785
|
+
await cancelResourceEventSubscription({
|
|
786
|
+
conversationId: input.conversationId,
|
|
787
|
+
id: subscription.id,
|
|
788
|
+
nowMs: input.nowMs,
|
|
789
|
+
state: input.state
|
|
790
|
+
});
|
|
791
|
+
}
|
|
836
792
|
}
|
|
837
|
-
async function
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
793
|
+
async function collectResourceEventMatchKeys(input) {
|
|
794
|
+
const state = input.state ?? getStateAdapter();
|
|
795
|
+
await state.connect();
|
|
796
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
797
|
+
const eventTypes = new Set(
|
|
798
|
+
input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean)
|
|
799
|
+
);
|
|
800
|
+
if (eventTypes.size === 0) return [];
|
|
801
|
+
const keys = /* @__PURE__ */ new Set();
|
|
802
|
+
const identifiers = new Set(
|
|
803
|
+
input.identifiers.map((value) => value.trim()).filter(Boolean)
|
|
804
|
+
);
|
|
805
|
+
for (const identifier of identifiers) {
|
|
806
|
+
const ids = await readSubscriptionIdIndex(
|
|
807
|
+
state,
|
|
808
|
+
resourceIndexKey(input.teamId, input.namespace, identifier)
|
|
809
|
+
);
|
|
810
|
+
const records = await Promise.all(
|
|
811
|
+
ids.map(
|
|
812
|
+
async (id) => parseSubscription(await state.get(subscriptionKey(id)))
|
|
813
|
+
)
|
|
814
|
+
);
|
|
815
|
+
for (const record of records) {
|
|
816
|
+
if (!record) continue;
|
|
817
|
+
if (record.namespace !== input.namespace) continue;
|
|
818
|
+
if (record.identifier !== identifier) continue;
|
|
819
|
+
if (subscriptionTeamId(record.destination) !== input.teamId) continue;
|
|
820
|
+
if (!activeAt(record, nowMs)) continue;
|
|
821
|
+
if (!record.events.some((eventType) => eventTypes.has(eventType))) continue;
|
|
822
|
+
for (const key of Object.keys(record.match ?? {})) {
|
|
823
|
+
keys.add(key);
|
|
824
|
+
}
|
|
848
825
|
}
|
|
826
|
+
}
|
|
827
|
+
return [...keys].sort();
|
|
828
|
+
}
|
|
829
|
+
async function findMatchingResourceEventSubscriptions(input) {
|
|
830
|
+
const state = input.state ?? getStateAdapter();
|
|
831
|
+
await state.connect();
|
|
832
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
833
|
+
const ids = await readSubscriptionIdIndex(
|
|
834
|
+
state,
|
|
835
|
+
resourceIndexKey(input.teamId, input.namespace, input.identifier)
|
|
836
|
+
);
|
|
837
|
+
const records = await Promise.all(
|
|
838
|
+
ids.map(
|
|
839
|
+
async (id) => parseSubscription(await state.get(subscriptionKey(id)))
|
|
840
|
+
)
|
|
841
|
+
);
|
|
842
|
+
return records.filter(
|
|
843
|
+
(record) => record !== void 0 && matchesEvent(record, {
|
|
844
|
+
data: input.data,
|
|
845
|
+
eventType: input.eventType,
|
|
846
|
+
nowMs,
|
|
847
|
+
namespace: input.namespace,
|
|
848
|
+
identifier: input.identifier,
|
|
849
|
+
teamId: input.teamId
|
|
850
|
+
})
|
|
849
851
|
);
|
|
850
|
-
return response.file?.permalink;
|
|
851
852
|
}
|
|
852
|
-
async function
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
853
|
+
async function deliverResourceEventSubscription(input) {
|
|
854
|
+
const state = input.state ?? getStateAdapter();
|
|
855
|
+
await state.connect();
|
|
856
|
+
return await withSubscriptionLock(
|
|
857
|
+
state,
|
|
858
|
+
input.subscription.id,
|
|
858
859
|
async () => {
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
860
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
861
|
+
const current = parseSubscription(
|
|
862
|
+
await state.get(subscriptionKey(input.subscription.id))
|
|
863
|
+
);
|
|
864
|
+
if (!current || !matchesEvent(current, {
|
|
865
|
+
data: input.data,
|
|
866
|
+
eventType: input.eventType,
|
|
867
|
+
nowMs,
|
|
868
|
+
namespace: input.namespace,
|
|
869
|
+
identifier: input.identifier,
|
|
870
|
+
teamId: input.teamId
|
|
871
|
+
})) {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
const delivered = await input.deliver(current);
|
|
875
|
+
if (input.terminal) {
|
|
876
|
+
const latest = parseSubscription(
|
|
877
|
+
await state.get(subscriptionKey(current.id))
|
|
878
|
+
);
|
|
879
|
+
if (!latest || latest.status !== current.status || latest.updatedAtMs !== current.updatedAtMs) {
|
|
880
|
+
return delivered;
|
|
862
881
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
882
|
+
const next = {
|
|
883
|
+
...current,
|
|
884
|
+
status: "completed",
|
|
885
|
+
updatedAtMs: nowMs
|
|
886
|
+
};
|
|
887
|
+
await state.set(
|
|
888
|
+
subscriptionKey(current.id),
|
|
889
|
+
next,
|
|
890
|
+
JUNIOR_THREAD_STATE_TTL_MS
|
|
891
|
+
);
|
|
892
|
+
await removeFromIndex(
|
|
893
|
+
state,
|
|
894
|
+
resourceIndexKey(
|
|
895
|
+
subscriptionTeamId(current.destination),
|
|
896
|
+
current.namespace,
|
|
897
|
+
current.identifier
|
|
898
|
+
),
|
|
899
|
+
current.id,
|
|
900
|
+
nowMs
|
|
901
|
+
);
|
|
902
|
+
await removeFromIndex(
|
|
903
|
+
state,
|
|
904
|
+
conversationIndexKey(current.conversationId),
|
|
905
|
+
current.id,
|
|
906
|
+
nowMs
|
|
907
|
+
);
|
|
869
908
|
}
|
|
870
|
-
return
|
|
909
|
+
return delivered;
|
|
871
910
|
},
|
|
872
|
-
|
|
873
|
-
"http.request.method": "GET",
|
|
874
|
-
"server.address": "files.slack.com",
|
|
875
|
-
"url.scheme": "https",
|
|
876
|
-
"app.slack.method": "files.download"
|
|
877
|
-
}
|
|
911
|
+
input.waitDeadlineMs
|
|
878
912
|
);
|
|
879
913
|
}
|
|
880
914
|
|
|
881
|
-
// src/chat/resource-events/
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
915
|
+
// src/chat/resource-events/tool-support.ts
|
|
916
|
+
var RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
917
|
+
var RESOURCE_SUBSCRIPTION_MAX_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
918
|
+
var STOP_WATCHING_TOOL_NAME = "stopWatchingResources";
|
|
919
|
+
var RESOURCE_WATCH_TOOL_SOURCE = {
|
|
920
|
+
id: "resource-watches",
|
|
921
|
+
description: "Inspect or stop resource watches for the current conversation."
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// src/chat/slack/tool-support/context.ts
|
|
925
|
+
function getSlackToolContext(context) {
|
|
926
|
+
if (context.source.platform !== "slack") {
|
|
927
|
+
return void 0;
|
|
928
|
+
}
|
|
929
|
+
if (context.destination.platform !== "slack") {
|
|
930
|
+
throw new TypeError("Slack source requires a Slack destination");
|
|
931
|
+
}
|
|
932
|
+
const destinationChannelId = parseSlackChannelReferenceId(
|
|
933
|
+
context.destination.channelId
|
|
934
|
+
);
|
|
935
|
+
const sourceChannelId = parseSlackChannelReferenceId(
|
|
936
|
+
context.source.channelId
|
|
937
|
+
);
|
|
938
|
+
const teamId = parseSlackTeamId(context.source.teamId);
|
|
939
|
+
if (!destinationChannelId || !sourceChannelId || !teamId) {
|
|
940
|
+
return void 0;
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
destination: context.destination,
|
|
944
|
+
source: context.source,
|
|
945
|
+
actor: context.actor?.platform === "slack" ? context.actor : void 0,
|
|
946
|
+
destinationChannelId,
|
|
947
|
+
messageTs: parseSlackMessageTs(context.source.messageTs),
|
|
948
|
+
sourceChannelId,
|
|
949
|
+
teamId,
|
|
950
|
+
threadTs: parseSlackMessageTs(context.source.threadTs)
|
|
900
951
|
};
|
|
901
952
|
}
|
|
902
953
|
|
|
903
|
-
// src/chat/
|
|
904
|
-
import {
|
|
954
|
+
// src/chat/plugins/viewer.ts
|
|
955
|
+
import { randomUUID } from "crypto";
|
|
956
|
+
import { and as and3, asc, eq as eq3 } from "drizzle-orm";
|
|
905
957
|
import {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
resourceEventMatches,
|
|
909
|
-
resourceEventTypeSchema,
|
|
910
|
-
stableResourceEventMatchKey
|
|
958
|
+
identitySchema,
|
|
959
|
+
userSchema
|
|
911
960
|
} from "@sentry/junior-plugin-api";
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
var subscriptionSchema = z2.object({
|
|
922
|
-
conversationId: z2.string().min(1),
|
|
923
|
-
createdAtMs: z2.number().finite(),
|
|
924
|
-
destination: destinationSchema,
|
|
925
|
-
events: z2.array(resourceEventTypeSchema).min(1),
|
|
926
|
-
expiresAtMs: z2.number().finite(),
|
|
927
|
-
id: z2.string().min(1),
|
|
928
|
-
intent: z2.string().min(1),
|
|
929
|
-
label: z2.string().min(1),
|
|
930
|
-
match: resourceEventMatchSchema.optional(),
|
|
931
|
-
namespace: z2.string().min(1),
|
|
932
|
-
identifier: z2.string().min(1),
|
|
933
|
-
resourceType: z2.string().min(1),
|
|
934
|
-
status: subscriptionStatusSchema,
|
|
935
|
-
updatedAtMs: z2.number().finite()
|
|
936
|
-
}).strict();
|
|
937
|
-
function digest(value) {
|
|
938
|
-
return createHash2("sha256").update(value).digest("hex").slice(0, 32);
|
|
961
|
+
function identityFromRow(row) {
|
|
962
|
+
return identitySchema.parse({
|
|
963
|
+
id: row.id,
|
|
964
|
+
provider: row.provider,
|
|
965
|
+
providerSubjectId: row.providerSubjectId,
|
|
966
|
+
...row.providerTenantId ? { providerTenantId: row.providerTenantId } : void 0,
|
|
967
|
+
...row.displayName ? { displayName: row.displayName } : void 0,
|
|
968
|
+
...row.handle ? { handle: row.handle } : void 0
|
|
969
|
+
});
|
|
939
970
|
}
|
|
940
|
-
function
|
|
941
|
-
|
|
971
|
+
async function readUserById(db, userRow) {
|
|
972
|
+
const identityRows = await db.select().from(juniorIdentities).where(
|
|
973
|
+
and3(
|
|
974
|
+
eq3(juniorIdentities.userId, userRow.id),
|
|
975
|
+
eq3(juniorIdentities.kind, "user")
|
|
976
|
+
)
|
|
977
|
+
).orderBy(
|
|
978
|
+
asc(juniorIdentities.provider),
|
|
979
|
+
asc(juniorIdentities.providerTenantId),
|
|
980
|
+
asc(juniorIdentities.providerSubjectId)
|
|
981
|
+
);
|
|
982
|
+
return userSchema.parse({
|
|
983
|
+
email: userRow.primaryEmail,
|
|
984
|
+
id: userRow.id,
|
|
985
|
+
identities: identityRows.map(identityFromRow),
|
|
986
|
+
...userRow.displayName ? { displayName: userRow.displayName } : void 0
|
|
987
|
+
});
|
|
942
988
|
}
|
|
943
|
-
function
|
|
944
|
-
|
|
989
|
+
async function resolveViewerUserFromSql(db, email) {
|
|
990
|
+
const normalizedEmail = normalizeIdentityEmail(email);
|
|
991
|
+
if (!normalizedEmail) return void 0;
|
|
992
|
+
const now = /* @__PURE__ */ new Date();
|
|
993
|
+
const rows = await db.insert(juniorUsers).values({
|
|
994
|
+
id: randomUUID(),
|
|
995
|
+
primaryEmail: email.trim(),
|
|
996
|
+
primaryEmailNormalized: normalizedEmail,
|
|
997
|
+
displayName: null,
|
|
998
|
+
createdAt: now,
|
|
999
|
+
updatedAt: now
|
|
1000
|
+
}).onConflictDoNothing({ target: juniorUsers.primaryEmailNormalized }).returning();
|
|
1001
|
+
const userRow = rows[0] ?? (await db.select().from(juniorUsers).where(eq3(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
|
|
1002
|
+
return userRow ? await readUserById(db, userRow) : void 0;
|
|
945
1003
|
}
|
|
946
|
-
function
|
|
947
|
-
return
|
|
1004
|
+
async function resolveViewerUser(email) {
|
|
1005
|
+
return await resolveViewerUserFromSql(getDb(), email);
|
|
948
1006
|
}
|
|
949
|
-
function
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
return
|
|
1007
|
+
async function findUserByEmailFromSql(db, email) {
|
|
1008
|
+
const normalizedEmail = normalizeIdentityEmail(email);
|
|
1009
|
+
if (!normalizedEmail) return void 0;
|
|
1010
|
+
const userRow = (await db.select().from(juniorUsers).where(eq3(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
|
|
1011
|
+
return userRow ? await readUserById(db, userRow) : void 0;
|
|
954
1012
|
}
|
|
955
|
-
function
|
|
956
|
-
return
|
|
1013
|
+
async function findUserByEmail(email) {
|
|
1014
|
+
return await findUserByEmailFromSql(getDb(), email);
|
|
957
1015
|
}
|
|
958
|
-
function
|
|
959
|
-
|
|
1016
|
+
async function updateViewerDisplayNameFromSql(db, userId, displayName) {
|
|
1017
|
+
const rows = await db.update(juniorUsers).set({ displayName, updatedAt: /* @__PURE__ */ new Date() }).where(eq3(juniorUsers.id, userId)).returning();
|
|
1018
|
+
const userRow = rows[0];
|
|
1019
|
+
return userRow ? await readUserById(db, userRow) : void 0;
|
|
960
1020
|
}
|
|
961
|
-
async function
|
|
962
|
-
await
|
|
963
|
-
const timer = setTimeout(resolve, ms);
|
|
964
|
-
timer.unref?.();
|
|
965
|
-
});
|
|
1021
|
+
async function updateViewerDisplayName(userId, displayName) {
|
|
1022
|
+
return await updateViewerDisplayNameFromSql(getDb(), userId, displayName);
|
|
966
1023
|
}
|
|
967
|
-
async function
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1024
|
+
async function readActorIdentityFromSql(db, actor) {
|
|
1025
|
+
if (actor.platform === "system") return void 0;
|
|
1026
|
+
const providerTenantId = actor.platform === "slack" ? actor.teamId : "";
|
|
1027
|
+
const rows = await db.select({
|
|
1028
|
+
identity: juniorIdentities,
|
|
1029
|
+
user: juniorUsers
|
|
1030
|
+
}).from(juniorIdentities).leftJoin(juniorUsers, eq3(juniorUsers.id, juniorIdentities.userId)).where(
|
|
1031
|
+
and3(
|
|
1032
|
+
eq3(juniorIdentities.kind, "user"),
|
|
1033
|
+
eq3(juniorIdentities.provider, actor.platform),
|
|
1034
|
+
eq3(juniorIdentities.providerTenantId, providerTenantId),
|
|
1035
|
+
eq3(juniorIdentities.providerSubjectId, actor.userId)
|
|
1036
|
+
)
|
|
1037
|
+
).limit(1);
|
|
1038
|
+
const row = rows[0];
|
|
1039
|
+
if (!row) return void 0;
|
|
1040
|
+
return {
|
|
1041
|
+
identity: identityFromRow(row.identity),
|
|
1042
|
+
...row.user ? { user: await readUserById(db, row.user) } : void 0
|
|
1043
|
+
};
|
|
983
1044
|
}
|
|
984
|
-
async function
|
|
985
|
-
|
|
986
|
-
state,
|
|
987
|
-
subscriptionId,
|
|
988
|
-
waitDeadlineMs
|
|
989
|
-
);
|
|
990
|
-
let lockLostError;
|
|
991
|
-
let heartbeatError;
|
|
992
|
-
let heartbeat;
|
|
993
|
-
const timer = setInterval(() => {
|
|
994
|
-
if (heartbeat) {
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
heartbeat = state.extendLock(lock, SUBSCRIPTION_LOCK_TTL_MS).then((extended) => {
|
|
998
|
-
if (extended) {
|
|
999
|
-
heartbeatError = void 0;
|
|
1000
|
-
} else {
|
|
1001
|
-
lockLostError = new Error(
|
|
1002
|
-
`Resource event subscription lock was lost for ${subscriptionId}`
|
|
1003
|
-
);
|
|
1004
|
-
}
|
|
1005
|
-
}).catch((error) => {
|
|
1006
|
-
heartbeatError = error instanceof Error ? error : new Error(`Failed to extend subscription lock: ${String(error)}`);
|
|
1007
|
-
}).finally(() => {
|
|
1008
|
-
heartbeat = void 0;
|
|
1009
|
-
});
|
|
1010
|
-
}, SUBSCRIPTION_LOCK_HEARTBEAT_MS);
|
|
1011
|
-
timer.unref?.();
|
|
1012
|
-
let result;
|
|
1013
|
-
try {
|
|
1014
|
-
result = await callback();
|
|
1015
|
-
} finally {
|
|
1016
|
-
clearInterval(timer);
|
|
1017
|
-
await heartbeat;
|
|
1018
|
-
await state.releaseLock(lock);
|
|
1019
|
-
}
|
|
1020
|
-
const lockError = lockLostError ?? heartbeatError;
|
|
1021
|
-
if (lockError) {
|
|
1022
|
-
throw lockError;
|
|
1023
|
-
}
|
|
1024
|
-
return result;
|
|
1045
|
+
async function readActorIdentity(actor) {
|
|
1046
|
+
return await readActorIdentityFromSql(getDb(), actor);
|
|
1025
1047
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1048
|
+
|
|
1049
|
+
// src/chat/credentials/subject.ts
|
|
1050
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
1051
|
+
|
|
1052
|
+
// src/chat/slack/client.ts
|
|
1053
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
1054
|
+
import { WebClient } from "@slack/web-api";
|
|
1055
|
+
|
|
1056
|
+
// src/chat/slack/workspace-context.ts
|
|
1057
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
1058
|
+
var workspaceTeamIdStorage = new AsyncLocalStorage();
|
|
1059
|
+
function runWithWorkspaceTeamId(teamId, fn) {
|
|
1060
|
+
if (!teamId) return fn();
|
|
1061
|
+
return workspaceTeamIdStorage.run(teamId, fn);
|
|
1028
1062
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
if (value === void 0 || value === null) {
|
|
1032
|
-
return [];
|
|
1033
|
-
}
|
|
1034
|
-
return subscriptionIdIndexSchema.parse(value);
|
|
1035
|
-
}
|
|
1036
|
-
function buildSubscriptionId(input) {
|
|
1037
|
-
const eventKey = [...new Set(input.events)].sort().join("\0");
|
|
1038
|
-
const matchKey = stableResourceEventMatchKey(input.match);
|
|
1039
|
-
const material = matchKey ? `${input.teamId}\0${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}\0${matchKey}` : `${input.teamId}\0${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}`;
|
|
1040
|
-
return `resub_${digest(material)}`;
|
|
1063
|
+
function getWorkspaceTeamId() {
|
|
1064
|
+
return workspaceTeamIdStorage.getStore();
|
|
1041
1065
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1066
|
+
|
|
1067
|
+
// src/chat/slack/client.ts
|
|
1068
|
+
var SlackActionError = class extends Error {
|
|
1069
|
+
code;
|
|
1070
|
+
apiError;
|
|
1071
|
+
needed;
|
|
1072
|
+
provided;
|
|
1073
|
+
statusCode;
|
|
1074
|
+
requestId;
|
|
1075
|
+
errorData;
|
|
1076
|
+
retryAfterSeconds;
|
|
1077
|
+
detail;
|
|
1078
|
+
detailLine;
|
|
1079
|
+
detailRule;
|
|
1080
|
+
constructor(message, code, options = {}) {
|
|
1081
|
+
super(message);
|
|
1082
|
+
this.name = "SlackActionError";
|
|
1083
|
+
this.code = code;
|
|
1084
|
+
this.apiError = options.apiError;
|
|
1085
|
+
this.needed = options.needed;
|
|
1086
|
+
this.provided = options.provided;
|
|
1087
|
+
this.statusCode = options.statusCode;
|
|
1088
|
+
this.requestId = options.requestId;
|
|
1089
|
+
this.errorData = options.errorData;
|
|
1090
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
1091
|
+
this.detail = options.detail;
|
|
1092
|
+
this.detailLine = options.detailLine;
|
|
1093
|
+
this.detailRule = options.detailRule;
|
|
1046
1094
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1095
|
+
};
|
|
1096
|
+
function serializeSlackErrorData(data) {
|
|
1097
|
+
if (!data || typeof data !== "object") {
|
|
1098
|
+
return void 0;
|
|
1051
1099
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const ids = [...new Set(await readSubscriptionIdIndex(state, key))];
|
|
1056
|
-
const next = ids.includes(subscriptionId) ? ids : [...ids, subscriptionId];
|
|
1057
|
-
await state.set(key, next, await indexTtlMs(state, next, nowMs));
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
async function removeFromIndex(state, key, subscriptionId, nowMs) {
|
|
1061
|
-
await withIndexLock(state, key, async () => {
|
|
1062
|
-
const existing = await readSubscriptionIdIndex(state, key);
|
|
1063
|
-
const next = existing.filter((id) => id !== subscriptionId);
|
|
1064
|
-
await state.set(key, next, await indexTtlMs(state, next, nowMs));
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
async function indexTtlMs(state, subscriptionIds, nowMs) {
|
|
1068
|
-
const records = await Promise.all(
|
|
1069
|
-
subscriptionIds.map(
|
|
1070
|
-
async (id) => parseSubscription(await state.get(subscriptionKey(id)))
|
|
1100
|
+
const filtered = Object.fromEntries(
|
|
1101
|
+
Object.entries(data).filter(
|
|
1102
|
+
([key]) => key !== "error"
|
|
1071
1103
|
)
|
|
1072
1104
|
);
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
}
|
|
1081
|
-
function parseSubscription(value) {
|
|
1082
|
-
if (value === void 0 || value === null) {
|
|
1105
|
+
if (Object.keys(filtered).length === 0) {
|
|
1106
|
+
return void 0;
|
|
1107
|
+
}
|
|
1108
|
+
try {
|
|
1109
|
+
const serialized = JSON.stringify(filtered);
|
|
1110
|
+
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
1111
|
+
} catch {
|
|
1083
1112
|
return void 0;
|
|
1084
1113
|
}
|
|
1085
|
-
return subscriptionSchema.parse(value);
|
|
1086
|
-
}
|
|
1087
|
-
function activeAt(subscription, nowMs) {
|
|
1088
|
-
return subscription.status === "active" && subscription.expiresAtMs > nowMs;
|
|
1089
1114
|
}
|
|
1090
|
-
function
|
|
1091
|
-
|
|
1115
|
+
function getHeaderString(headers, name) {
|
|
1116
|
+
if (!headers || typeof headers !== "object") {
|
|
1117
|
+
return void 0;
|
|
1118
|
+
}
|
|
1119
|
+
const key = name.toLowerCase();
|
|
1120
|
+
const entries = headers;
|
|
1121
|
+
for (const [entryKey, value] of Object.entries(entries)) {
|
|
1122
|
+
if (entryKey.toLowerCase() !== key) continue;
|
|
1123
|
+
if (typeof value === "string") return value;
|
|
1124
|
+
if (Array.isArray(value)) {
|
|
1125
|
+
const first = value.find((entry) => typeof entry === "string");
|
|
1126
|
+
return typeof first === "string" ? first : void 0;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return void 0;
|
|
1092
1130
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const nowMs = options.nowMs ?? Date.now();
|
|
1097
|
-
const events = [...new Set(input.events.map((event) => event.trim()))].filter(
|
|
1098
|
-
Boolean
|
|
1099
|
-
);
|
|
1100
|
-
if (events.length === 0) {
|
|
1101
|
-
throw new Error("Resource event subscription requires at least one event");
|
|
1131
|
+
function parseSlackCanvasDetail(detail) {
|
|
1132
|
+
if (typeof detail !== "string") {
|
|
1133
|
+
return {};
|
|
1102
1134
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1135
|
+
const trimmed = detail.trim();
|
|
1136
|
+
if (!trimmed) {
|
|
1137
|
+
return {};
|
|
1105
1138
|
}
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
conversationId: input.conversationId,
|
|
1109
|
-
events,
|
|
1110
|
-
match,
|
|
1111
|
-
namespace: input.namespace,
|
|
1112
|
-
identifier: input.identifier,
|
|
1113
|
-
teamId: subscriptionTeamId(input.destination)
|
|
1114
|
-
});
|
|
1115
|
-
const record = {
|
|
1116
|
-
conversationId: input.conversationId,
|
|
1117
|
-
createdAtMs: nowMs,
|
|
1118
|
-
destination: input.destination,
|
|
1119
|
-
events,
|
|
1120
|
-
expiresAtMs: input.expiresAtMs,
|
|
1121
|
-
id,
|
|
1122
|
-
intent: input.intent,
|
|
1123
|
-
label: input.label,
|
|
1124
|
-
...match ? { match } : void 0,
|
|
1125
|
-
namespace: input.namespace,
|
|
1126
|
-
identifier: input.identifier,
|
|
1127
|
-
resourceType: input.resourceType,
|
|
1128
|
-
status: "active",
|
|
1129
|
-
updatedAtMs: nowMs
|
|
1139
|
+
const parsed = {
|
|
1140
|
+
detail: trimmed
|
|
1130
1141
|
};
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
input.namespace,
|
|
1142
|
-
input.identifier
|
|
1143
|
-
),
|
|
1144
|
-
id,
|
|
1145
|
-
nowMs
|
|
1146
|
-
);
|
|
1147
|
-
await addToIndex(
|
|
1148
|
-
state,
|
|
1149
|
-
conversationIndexKey(input.conversationId),
|
|
1150
|
-
id,
|
|
1151
|
-
nowMs
|
|
1152
|
-
);
|
|
1142
|
+
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
1143
|
+
if (lineMatch) {
|
|
1144
|
+
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
1145
|
+
if (Number.isFinite(line)) {
|
|
1146
|
+
parsed.detailLine = line;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (/unsupported heading depth/i.test(trimmed)) {
|
|
1150
|
+
parsed.detailRule = "unsupported_heading_depth";
|
|
1151
|
+
}
|
|
1153
1152
|
return parsed;
|
|
1154
1153
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const records = await Promise.all(
|
|
1164
|
-
ids.map(
|
|
1165
|
-
async (id) => parseSubscription(await state.get(subscriptionKey(id)))
|
|
1166
|
-
)
|
|
1167
|
-
);
|
|
1168
|
-
return records.filter(
|
|
1169
|
-
(record) => record !== void 0 && record.conversationId === input.conversationId && activeAt(record, nowMs)
|
|
1170
|
-
).sort((left, right) => left.createdAtMs - right.createdAtMs);
|
|
1171
|
-
}
|
|
1172
|
-
async function cancelResourceEventSubscription(input) {
|
|
1173
|
-
const state = input.state ?? getStateAdapter();
|
|
1174
|
-
await state.connect();
|
|
1175
|
-
return await withSubscriptionLock(state, input.id, async () => {
|
|
1176
|
-
const current = parseSubscription(
|
|
1177
|
-
await state.get(subscriptionKey(input.id))
|
|
1178
|
-
);
|
|
1179
|
-
if (!current || current.conversationId !== input.conversationId) {
|
|
1180
|
-
return void 0;
|
|
1181
|
-
}
|
|
1182
|
-
const nowMs = input.nowMs ?? Date.now();
|
|
1183
|
-
const next = {
|
|
1184
|
-
...current,
|
|
1185
|
-
status: "cancelled",
|
|
1186
|
-
updatedAtMs: nowMs
|
|
1187
|
-
};
|
|
1188
|
-
await state.set(
|
|
1189
|
-
subscriptionKey(input.id),
|
|
1190
|
-
next,
|
|
1191
|
-
JUNIOR_THREAD_STATE_TTL_MS
|
|
1192
|
-
);
|
|
1193
|
-
await removeFromIndex(
|
|
1194
|
-
state,
|
|
1195
|
-
resourceIndexKey(
|
|
1196
|
-
subscriptionTeamId(current.destination),
|
|
1197
|
-
current.namespace,
|
|
1198
|
-
current.identifier
|
|
1199
|
-
),
|
|
1200
|
-
input.id,
|
|
1201
|
-
nowMs
|
|
1202
|
-
);
|
|
1203
|
-
await removeFromIndex(
|
|
1204
|
-
state,
|
|
1205
|
-
conversationIndexKey(current.conversationId),
|
|
1206
|
-
input.id,
|
|
1207
|
-
nowMs
|
|
1154
|
+
var installationTokenStorage = new AsyncLocalStorage2();
|
|
1155
|
+
var clientsByToken = /* @__PURE__ */ new Map();
|
|
1156
|
+
function runWithSlackInstallationToken(token, fn) {
|
|
1157
|
+
const trimmed = token.trim();
|
|
1158
|
+
if (!trimmed) {
|
|
1159
|
+
throw new SlackActionError(
|
|
1160
|
+
"Slack installation token binding requires a non-empty token",
|
|
1161
|
+
"missing_token"
|
|
1208
1162
|
);
|
|
1209
|
-
|
|
1210
|
-
});
|
|
1163
|
+
}
|
|
1164
|
+
return installationTokenStorage.run({ token: trimmed }, fn);
|
|
1211
1165
|
}
|
|
1212
|
-
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1166
|
+
function resolveSlackToken() {
|
|
1167
|
+
const ambientToken = installationTokenStorage.getStore()?.token;
|
|
1168
|
+
if (ambientToken) {
|
|
1169
|
+
return ambientToken;
|
|
1170
|
+
}
|
|
1171
|
+
const envToken = getSlackBotToken();
|
|
1172
|
+
if (envToken) {
|
|
1173
|
+
return envToken;
|
|
1174
|
+
}
|
|
1175
|
+
const teamId = getWorkspaceTeamId();
|
|
1176
|
+
if (teamId) {
|
|
1177
|
+
throw new SlackActionError(
|
|
1178
|
+
`Slack call is scoped to workspace ${teamId} but no installation token is bound and no default bot token is configured`,
|
|
1179
|
+
"missing_token"
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
throw new SlackActionError(
|
|
1183
|
+
"SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service",
|
|
1184
|
+
"missing_token"
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
function normalizeSlackConversationId(channelId) {
|
|
1188
|
+
return parseSlackChannelReferenceId(channelId);
|
|
1189
|
+
}
|
|
1190
|
+
function getClient() {
|
|
1191
|
+
const token = resolveSlackToken();
|
|
1192
|
+
let cached = clientsByToken.get(token);
|
|
1193
|
+
if (!cached) {
|
|
1194
|
+
cached = new WebClient(token, {
|
|
1195
|
+
retryConfig: { retries: 0 },
|
|
1196
|
+
rejectRateLimitedCalls: true
|
|
1220
1197
|
});
|
|
1198
|
+
clientsByToken.set(token, cached);
|
|
1221
1199
|
}
|
|
1200
|
+
return cached;
|
|
1222
1201
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const nowMs = input.nowMs ?? Date.now();
|
|
1227
|
-
const eventTypes = new Set(
|
|
1228
|
-
input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean)
|
|
1229
|
-
);
|
|
1230
|
-
if (eventTypes.size === 0) return [];
|
|
1231
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1232
|
-
const identifiers = new Set(
|
|
1233
|
-
input.identifiers.map((value) => value.trim()).filter(Boolean)
|
|
1234
|
-
);
|
|
1235
|
-
for (const identifier of identifiers) {
|
|
1236
|
-
const ids = await readSubscriptionIdIndex(
|
|
1237
|
-
state,
|
|
1238
|
-
resourceIndexKey(input.teamId, input.namespace, identifier)
|
|
1239
|
-
);
|
|
1240
|
-
const records = await Promise.all(
|
|
1241
|
-
ids.map(
|
|
1242
|
-
async (id) => parseSubscription(await state.get(subscriptionKey(id)))
|
|
1243
|
-
)
|
|
1244
|
-
);
|
|
1245
|
-
for (const record of records) {
|
|
1246
|
-
if (!record) continue;
|
|
1247
|
-
if (record.namespace !== input.namespace) continue;
|
|
1248
|
-
if (record.identifier !== identifier) continue;
|
|
1249
|
-
if (subscriptionTeamId(record.destination) !== input.teamId) continue;
|
|
1250
|
-
if (!activeAt(record, nowMs)) continue;
|
|
1251
|
-
if (!record.events.some((eventType) => eventTypes.has(eventType))) continue;
|
|
1252
|
-
for (const key of Object.keys(record.match ?? {})) {
|
|
1253
|
-
keys.add(key);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1202
|
+
function mapSlackError(error) {
|
|
1203
|
+
if (error instanceof SlackActionError) {
|
|
1204
|
+
return error;
|
|
1256
1205
|
}
|
|
1257
|
-
|
|
1206
|
+
const candidate = error;
|
|
1207
|
+
const apiError = candidate.data?.error;
|
|
1208
|
+
const message = candidate.message ?? "Slack action failed";
|
|
1209
|
+
const baseOptions = {
|
|
1210
|
+
apiError,
|
|
1211
|
+
statusCode: candidate.statusCode,
|
|
1212
|
+
requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
|
|
1213
|
+
errorData: serializeSlackErrorData(candidate.data),
|
|
1214
|
+
...parseSlackCanvasDetail(candidate.data?.detail)
|
|
1215
|
+
};
|
|
1216
|
+
if (apiError === "missing_scope") {
|
|
1217
|
+
return new SlackActionError(message, "missing_scope", {
|
|
1218
|
+
...baseOptions,
|
|
1219
|
+
needed: candidate.data?.needed,
|
|
1220
|
+
provided: candidate.data?.provided
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
if (apiError === "not_in_channel") {
|
|
1224
|
+
return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
1225
|
+
}
|
|
1226
|
+
if (apiError === "restricted_action_read_only_channel") {
|
|
1227
|
+
return new SlackActionError(message, "read_only_channel", baseOptions);
|
|
1228
|
+
}
|
|
1229
|
+
if (apiError === "already_reacted") {
|
|
1230
|
+
return new SlackActionError(message, "already_reacted", baseOptions);
|
|
1231
|
+
}
|
|
1232
|
+
if (apiError === "no_reaction") {
|
|
1233
|
+
return new SlackActionError(message, "no_reaction", baseOptions);
|
|
1234
|
+
}
|
|
1235
|
+
if (apiError === "invalid_arguments") {
|
|
1236
|
+
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1237
|
+
}
|
|
1238
|
+
if (apiError === "invalid_cursor") {
|
|
1239
|
+
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1240
|
+
}
|
|
1241
|
+
if (apiError === "invalid_name") {
|
|
1242
|
+
return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
1243
|
+
}
|
|
1244
|
+
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
|
|
1245
|
+
return new SlackActionError(message, "not_found", baseOptions);
|
|
1246
|
+
}
|
|
1247
|
+
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
|
|
1248
|
+
return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
1249
|
+
}
|
|
1250
|
+
if (apiError === "canvas_creation_failed") {
|
|
1251
|
+
return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
1252
|
+
}
|
|
1253
|
+
if (apiError === "canvas_editing_failed") {
|
|
1254
|
+
return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
1255
|
+
}
|
|
1256
|
+
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
|
|
1257
|
+
return new SlackActionError(message, "rate_limited", {
|
|
1258
|
+
...baseOptions,
|
|
1259
|
+
retryAfterSeconds: candidate.retryAfter
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
return new SlackActionError(message, "internal_error", baseOptions);
|
|
1258
1263
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
);
|
|
1264
|
+
var MAX_RETRY_DELAY_MS = 1e4;
|
|
1265
|
+
var MAX_TOTAL_RETRY_DELAY_MS = 2e4;
|
|
1266
|
+
var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1267
|
+
"ECONNREFUSED",
|
|
1268
|
+
"ECONNRESET",
|
|
1269
|
+
"ENOTFOUND",
|
|
1270
|
+
"EAI_AGAIN"
|
|
1271
|
+
]);
|
|
1272
|
+
var TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1273
|
+
"ETIMEDOUT",
|
|
1274
|
+
"ECONNABORTED",
|
|
1275
|
+
"ESOCKETTIMEDOUT"
|
|
1276
|
+
]);
|
|
1277
|
+
function findNetworkErrorCode(error, depth = 0) {
|
|
1278
|
+
if (!error || typeof error !== "object" || depth > 4) {
|
|
1279
|
+
return void 0;
|
|
1280
|
+
}
|
|
1281
|
+
const candidate = error;
|
|
1282
|
+
if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) {
|
|
1283
|
+
return candidate.code;
|
|
1284
|
+
}
|
|
1285
|
+
return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
|
|
1282
1286
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1287
|
+
function hasSocketHangUpMessage(error) {
|
|
1288
|
+
return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
|
|
1289
|
+
}
|
|
1290
|
+
function classifySlackRetry(raw, mapped) {
|
|
1291
|
+
if (mapped.code === "rate_limited") {
|
|
1292
|
+
return "rate_limited";
|
|
1293
|
+
}
|
|
1294
|
+
if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) {
|
|
1295
|
+
return "server_error";
|
|
1296
|
+
}
|
|
1297
|
+
const networkCode = findNetworkErrorCode(raw);
|
|
1298
|
+
if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) {
|
|
1299
|
+
return "connection";
|
|
1300
|
+
}
|
|
1301
|
+
if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) {
|
|
1302
|
+
return "timeout";
|
|
1303
|
+
}
|
|
1304
|
+
if (hasSocketHangUpMessage(raw)) {
|
|
1305
|
+
return "connection";
|
|
1306
|
+
}
|
|
1307
|
+
return "none";
|
|
1308
|
+
}
|
|
1309
|
+
async function withSlackRetries(task, maxAttempts = 3, context = {}) {
|
|
1310
|
+
let attempt = 0;
|
|
1311
|
+
let totalDelayMs = 0;
|
|
1312
|
+
const action = context.action ?? "unknown";
|
|
1313
|
+
while (attempt < maxAttempts) {
|
|
1314
|
+
attempt += 1;
|
|
1315
|
+
const attemptNumber = attempt;
|
|
1316
|
+
try {
|
|
1317
|
+
return await withSpan(
|
|
1318
|
+
`POST slack.com/api/${action}`,
|
|
1319
|
+
"http.client",
|
|
1320
|
+
{},
|
|
1321
|
+
async () => {
|
|
1322
|
+
try {
|
|
1323
|
+
return await task();
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
const mapped = mapSlackError(error);
|
|
1326
|
+
const errorAttrs = {
|
|
1327
|
+
"error.type": mapped.code
|
|
1328
|
+
};
|
|
1329
|
+
if (mapped.apiError) {
|
|
1330
|
+
errorAttrs["app.slack.api_error_code"] = mapped.apiError;
|
|
1331
|
+
}
|
|
1332
|
+
if (mapped.code === "rate_limited") {
|
|
1333
|
+
errorAttrs["http.response.status_code"] = 429;
|
|
1334
|
+
if (mapped.retryAfterSeconds) {
|
|
1335
|
+
errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
|
|
1336
|
+
}
|
|
1337
|
+
} else if (mapped.statusCode != null) {
|
|
1338
|
+
errorAttrs["http.response.status_code"] = mapped.statusCode;
|
|
1339
|
+
}
|
|
1340
|
+
setSpanAttributes(errorAttrs);
|
|
1341
|
+
setSpanStatus("error");
|
|
1342
|
+
throw error;
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
"http.request.method": "POST",
|
|
1347
|
+
"server.address": "slack.com",
|
|
1348
|
+
"url.scheme": "https",
|
|
1349
|
+
"url.path": `/api/${action}`,
|
|
1350
|
+
"app.slack.method": action,
|
|
1351
|
+
"app.retry.max_attempts": maxAttempts,
|
|
1352
|
+
...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
|
|
1353
|
+
...context.attributes ?? {},
|
|
1354
|
+
...context.spanAttributes ?? {}
|
|
1311
1355
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
current.id,
|
|
1336
|
-
nowMs
|
|
1337
|
-
);
|
|
1356
|
+
);
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
const mapped = mapSlackError(error);
|
|
1359
|
+
const retryClass = classifySlackRetry(error, mapped);
|
|
1360
|
+
const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
|
|
1361
|
+
const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
|
|
1362
|
+
const baseLogAttributes = {
|
|
1363
|
+
"app.slack.action": action,
|
|
1364
|
+
"app.slack.error_code": mapped.code,
|
|
1365
|
+
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
|
|
1366
|
+
...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
|
|
1367
|
+
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
|
|
1368
|
+
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
|
|
1369
|
+
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
|
|
1370
|
+
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
|
|
1371
|
+
...context.attributes ?? {}
|
|
1372
|
+
};
|
|
1373
|
+
if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
|
|
1374
|
+
logWarn("slack.action.failed", {
|
|
1375
|
+
...baseLogAttributes,
|
|
1376
|
+
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
|
|
1377
|
+
});
|
|
1378
|
+
throw mapped;
|
|
1338
1379
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
var RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
1355
|
-
var RESOURCE_SUBSCRIPTION_MAX_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1356
|
-
var STOP_WATCHING_TOOL_NAME = "stopWatchingResources";
|
|
1357
|
-
var RESOURCE_WATCH_TOOL_SOURCE = {
|
|
1358
|
-
id: "resource-watches",
|
|
1359
|
-
description: "Inspect or stop resource watches for the current conversation."
|
|
1360
|
-
};
|
|
1361
|
-
function requireResourceWatchConversation(context) {
|
|
1362
|
-
if (!context.conversationId) {
|
|
1363
|
-
throw new ToolInputError(
|
|
1364
|
-
"Resource event subscriptions require a conversation"
|
|
1365
|
-
);
|
|
1366
|
-
}
|
|
1367
|
-
if (context.destination.platform !== "slack") {
|
|
1368
|
-
throw new ToolInputError(
|
|
1369
|
-
"Resource event subscriptions currently require Slack delivery"
|
|
1370
|
-
);
|
|
1371
|
-
}
|
|
1372
|
-
if (!isSlackThreadConversationId(context.conversationId)) {
|
|
1373
|
-
throw new ToolInputError(
|
|
1374
|
-
"Resource event subscriptions require a Slack thread conversation"
|
|
1375
|
-
);
|
|
1376
|
-
}
|
|
1377
|
-
return context.conversationId;
|
|
1378
|
-
}
|
|
1379
|
-
function canUseResourceEventSubscriptionTools(context) {
|
|
1380
|
-
return context.destination.platform === "slack" && Boolean(
|
|
1381
|
-
context.conversationId && isSlackThreadConversationId(context.conversationId)
|
|
1382
|
-
);
|
|
1383
|
-
}
|
|
1384
|
-
function isSlackThreadConversationId(conversationId) {
|
|
1385
|
-
const parts = conversationId.split(":");
|
|
1386
|
-
return parts.length === 3 && parts[0] === "slack" && Boolean(parts[1]) && Boolean(parts[2]);
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
// src/chat/slack/tool-support/context.ts
|
|
1390
|
-
function getSlackToolContext(context) {
|
|
1391
|
-
if (context.source.platform !== "slack") {
|
|
1392
|
-
return void 0;
|
|
1393
|
-
}
|
|
1394
|
-
if (context.destination.platform !== "slack") {
|
|
1395
|
-
throw new TypeError("Slack source requires a Slack destination");
|
|
1396
|
-
}
|
|
1397
|
-
const destinationChannelId = parseSlackChannelReferenceId(
|
|
1398
|
-
context.destination.channelId
|
|
1399
|
-
);
|
|
1400
|
-
const sourceChannelId = parseSlackChannelReferenceId(
|
|
1401
|
-
context.source.channelId
|
|
1402
|
-
);
|
|
1403
|
-
const teamId = parseSlackTeamId(context.source.teamId);
|
|
1404
|
-
if (!destinationChannelId || !sourceChannelId || !teamId) {
|
|
1405
|
-
return void 0;
|
|
1380
|
+
logWarn("slack.action.retrying", {
|
|
1381
|
+
...baseLogAttributes,
|
|
1382
|
+
"app.slack.retry_attempt": attempt,
|
|
1383
|
+
"app.slack.retry_class": retryClass
|
|
1384
|
+
});
|
|
1385
|
+
const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
1386
|
+
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
1387
|
+
const delayMs = Math.min(
|
|
1388
|
+
retryAfterMs ?? backoffMs,
|
|
1389
|
+
MAX_RETRY_DELAY_MS,
|
|
1390
|
+
remainingDelayBudgetMs
|
|
1391
|
+
);
|
|
1392
|
+
totalDelayMs += delayMs;
|
|
1393
|
+
await sleep(delayMs);
|
|
1394
|
+
}
|
|
1406
1395
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
actor: context.actor?.platform === "slack" ? context.actor : void 0,
|
|
1411
|
-
destinationChannelId,
|
|
1412
|
-
messageTs: parseSlackMessageTs(context.source.messageTs),
|
|
1413
|
-
sourceChannelId,
|
|
1414
|
-
teamId,
|
|
1415
|
-
threadTs: parseSlackMessageTs(context.source.threadTs)
|
|
1416
|
-
};
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
// src/chat/plugins/viewer.ts
|
|
1420
|
-
import { randomUUID } from "crypto";
|
|
1421
|
-
import { and as and3, asc, eq as eq3 } from "drizzle-orm";
|
|
1422
|
-
import {
|
|
1423
|
-
identitySchema,
|
|
1424
|
-
userSchema
|
|
1425
|
-
} from "@sentry/junior-plugin-api";
|
|
1426
|
-
function identityFromRow(row) {
|
|
1427
|
-
return identitySchema.parse({
|
|
1428
|
-
id: row.id,
|
|
1429
|
-
provider: row.provider,
|
|
1430
|
-
providerSubjectId: row.providerSubjectId,
|
|
1431
|
-
...row.providerTenantId ? { providerTenantId: row.providerTenantId } : void 0,
|
|
1432
|
-
...row.displayName ? { displayName: row.displayName } : void 0,
|
|
1433
|
-
...row.handle ? { handle: row.handle } : void 0
|
|
1434
|
-
});
|
|
1435
|
-
}
|
|
1436
|
-
async function readUserById(db, userRow) {
|
|
1437
|
-
const identityRows = await db.select().from(juniorIdentities).where(
|
|
1438
|
-
and3(
|
|
1439
|
-
eq3(juniorIdentities.userId, userRow.id),
|
|
1440
|
-
eq3(juniorIdentities.kind, "user")
|
|
1441
|
-
)
|
|
1442
|
-
).orderBy(
|
|
1443
|
-
asc(juniorIdentities.provider),
|
|
1444
|
-
asc(juniorIdentities.providerTenantId),
|
|
1445
|
-
asc(juniorIdentities.providerSubjectId)
|
|
1396
|
+
throw new SlackActionError(
|
|
1397
|
+
"Slack action exhausted retries",
|
|
1398
|
+
"internal_error"
|
|
1446
1399
|
);
|
|
1447
|
-
return userSchema.parse({
|
|
1448
|
-
email: userRow.primaryEmail,
|
|
1449
|
-
id: userRow.id,
|
|
1450
|
-
identities: identityRows.map(identityFromRow),
|
|
1451
|
-
...userRow.displayName ? { displayName: userRow.displayName } : void 0
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
async function resolveViewerUserFromSql(db, email) {
|
|
1455
|
-
const normalizedEmail = normalizeIdentityEmail(email);
|
|
1456
|
-
if (!normalizedEmail) return void 0;
|
|
1457
|
-
const now = /* @__PURE__ */ new Date();
|
|
1458
|
-
const rows = await db.insert(juniorUsers).values({
|
|
1459
|
-
id: randomUUID(),
|
|
1460
|
-
primaryEmail: email.trim(),
|
|
1461
|
-
primaryEmailNormalized: normalizedEmail,
|
|
1462
|
-
displayName: null,
|
|
1463
|
-
createdAt: now,
|
|
1464
|
-
updatedAt: now
|
|
1465
|
-
}).onConflictDoNothing({ target: juniorUsers.primaryEmailNormalized }).returning();
|
|
1466
|
-
const userRow = rows[0] ?? (await db.select().from(juniorUsers).where(eq3(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
|
|
1467
|
-
return userRow ? await readUserById(db, userRow) : void 0;
|
|
1468
1400
|
}
|
|
1469
|
-
|
|
1470
|
-
return
|
|
1471
|
-
}
|
|
1472
|
-
async function findUserByEmailFromSql(db, email) {
|
|
1473
|
-
const normalizedEmail = normalizeIdentityEmail(email);
|
|
1474
|
-
if (!normalizedEmail) return void 0;
|
|
1475
|
-
const userRow = (await db.select().from(juniorUsers).where(eq3(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
|
|
1476
|
-
return userRow ? await readUserById(db, userRow) : void 0;
|
|
1401
|
+
function getSlackClient() {
|
|
1402
|
+
return getClient();
|
|
1477
1403
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
1404
|
+
function isDmChannel(channelId) {
|
|
1405
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
1406
|
+
return Boolean(normalized && normalized.startsWith("D"));
|
|
1480
1407
|
}
|
|
1481
|
-
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
return
|
|
1408
|
+
function isConversationScopedChannel(channelId) {
|
|
1409
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
1410
|
+
if (!normalized) return false;
|
|
1411
|
+
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
1485
1412
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1413
|
+
function isConversationChannel(channelId) {
|
|
1414
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
1415
|
+
if (!normalized) return false;
|
|
1416
|
+
return normalized.startsWith("C") || normalized.startsWith("G");
|
|
1488
1417
|
}
|
|
1489
|
-
async function
|
|
1490
|
-
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
const row = rows[0];
|
|
1504
|
-
if (!row) return void 0;
|
|
1505
|
-
return {
|
|
1506
|
-
identity: identityFromRow(row.identity),
|
|
1507
|
-
...row.user ? { user: await readUserById(db, row.user) } : void 0
|
|
1508
|
-
};
|
|
1418
|
+
async function getFilePermalink(fileId) {
|
|
1419
|
+
const client = getClient();
|
|
1420
|
+
const response = await withSlackRetries(
|
|
1421
|
+
() => client.files.info({
|
|
1422
|
+
file: fileId
|
|
1423
|
+
}),
|
|
1424
|
+
3,
|
|
1425
|
+
{
|
|
1426
|
+
action: "files.info",
|
|
1427
|
+
idempotent: true,
|
|
1428
|
+
spanAttributes: { "app.slack.file_id": fileId }
|
|
1429
|
+
}
|
|
1430
|
+
);
|
|
1431
|
+
return response.file?.permalink;
|
|
1509
1432
|
}
|
|
1510
|
-
async function
|
|
1511
|
-
|
|
1433
|
+
async function downloadPrivateSlackFile(url) {
|
|
1434
|
+
const token = resolveSlackToken();
|
|
1435
|
+
return withSpan(
|
|
1436
|
+
"GET files.slack.com",
|
|
1437
|
+
"http.client",
|
|
1438
|
+
{},
|
|
1439
|
+
async () => {
|
|
1440
|
+
const response = await fetch(url, {
|
|
1441
|
+
headers: {
|
|
1442
|
+
Authorization: `Bearer ${token}`
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
setSpanAttributes({ "http.response.status_code": response.status });
|
|
1446
|
+
if (!response.ok) {
|
|
1447
|
+
setSpanAttributes({ "error.type": String(response.status) });
|
|
1448
|
+
setSpanStatus("error");
|
|
1449
|
+
throw new Error(`Slack file download failed: ${response.status}`);
|
|
1450
|
+
}
|
|
1451
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
"http.request.method": "GET",
|
|
1455
|
+
"server.address": "files.slack.com",
|
|
1456
|
+
"url.scheme": "https",
|
|
1457
|
+
"app.slack.method": "files.download"
|
|
1458
|
+
}
|
|
1459
|
+
);
|
|
1512
1460
|
}
|
|
1513
1461
|
|
|
1514
1462
|
// src/chat/credentials/subject.ts
|
|
1515
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
1516
1463
|
var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
|
|
1517
1464
|
var CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
|
|
1518
1465
|
function getCredentialSubjectSecret() {
|
|
@@ -2726,7 +2673,9 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
|
|
|
2726
2673
|
teamId: slackToolContext.teamId,
|
|
2727
2674
|
userId: slackToolContext.actor?.userId
|
|
2728
2675
|
}) : void 0;
|
|
2729
|
-
const dashboardConversationUrl =
|
|
2676
|
+
const dashboardConversationUrl = getDashboardConversationLink(
|
|
2677
|
+
context.conversationId
|
|
2678
|
+
);
|
|
2730
2679
|
const slackContext = slackToolContext ? {
|
|
2731
2680
|
channelCapabilities: resolveChannelCapabilities(
|
|
2732
2681
|
slackToolContext.sourceChannelId
|
|
@@ -2734,17 +2683,17 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
|
|
|
2734
2683
|
...dashboardConversationUrl ? { conversationLink: { url: dashboardConversationUrl } } : void 0,
|
|
2735
2684
|
...credentialSubject ? { credentialSubject } : void 0
|
|
2736
2685
|
} : void 0;
|
|
2737
|
-
const annotations =
|
|
2686
|
+
const annotations = createPluginAnnotations({
|
|
2738
2687
|
conversationId: context.conversationId,
|
|
2739
2688
|
db: getDb(),
|
|
2740
2689
|
plugin: pluginName
|
|
2741
|
-
})
|
|
2690
|
+
});
|
|
2742
2691
|
const mcp = pluginMcpContext(plugin, context);
|
|
2743
|
-
const canSubscribe =
|
|
2692
|
+
const canSubscribe = Boolean(plugin.resourceEvents) && plugin.resourceEvents?.isEnabled?.() !== false;
|
|
2744
2693
|
const resourceEvents = {
|
|
2745
2694
|
canSubscribe,
|
|
2746
2695
|
async subscribe(input) {
|
|
2747
|
-
if (!canSubscribe
|
|
2696
|
+
if (!canSubscribe) {
|
|
2748
2697
|
throw new Error(
|
|
2749
2698
|
"Resource subscriptions are not available in this conversation."
|
|
2750
2699
|
);
|
|
@@ -2783,7 +2732,7 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
|
|
|
2783
2732
|
const resolveActor = context.resolveActorIdentity ?? (async () => void 0);
|
|
2784
2733
|
const common = {
|
|
2785
2734
|
...basePluginContext(plugin),
|
|
2786
|
-
|
|
2735
|
+
annotations,
|
|
2787
2736
|
conversationId: context.conversationId,
|
|
2788
2737
|
locationId: context.locationId,
|
|
2789
2738
|
userText: context.userText,
|
|
@@ -3575,7 +3524,6 @@ function createPluginHookRunner(input = {}) {
|
|
|
3575
3524
|
}
|
|
3576
3525
|
|
|
3577
3526
|
export {
|
|
3578
|
-
ToolInputError,
|
|
3579
3527
|
listConversationAnnotations,
|
|
3580
3528
|
listConversationAnnotationsById,
|
|
3581
3529
|
createPluginConversationEvents,
|
|
@@ -3587,20 +3535,6 @@ export {
|
|
|
3587
3535
|
setDashboardConversationLinkOptions,
|
|
3588
3536
|
getDashboardConversationLink,
|
|
3589
3537
|
getDashboardTaskLink,
|
|
3590
|
-
runWithWorkspaceTeamId,
|
|
3591
|
-
getWorkspaceTeamId,
|
|
3592
|
-
SlackActionError,
|
|
3593
|
-
getHeaderString,
|
|
3594
|
-
runWithSlackInstallationToken,
|
|
3595
|
-
normalizeSlackConversationId,
|
|
3596
|
-
withSlackRetries,
|
|
3597
|
-
getSlackClient,
|
|
3598
|
-
isDmChannel,
|
|
3599
|
-
isConversationScopedChannel,
|
|
3600
|
-
getFilePermalink,
|
|
3601
|
-
downloadPrivateSlackFile,
|
|
3602
|
-
canRouteResourceEvents,
|
|
3603
|
-
createResourceEventTeamIdResolver,
|
|
3604
3538
|
createResourceEventSubscription,
|
|
3605
3539
|
listResourceEventSubscriptions,
|
|
3606
3540
|
cancelResourceEventSubscription,
|
|
@@ -3612,13 +3546,23 @@ export {
|
|
|
3612
3546
|
RESOURCE_SUBSCRIPTION_MAX_TTL_MS,
|
|
3613
3547
|
STOP_WATCHING_TOOL_NAME,
|
|
3614
3548
|
RESOURCE_WATCH_TOOL_SOURCE,
|
|
3615
|
-
requireResourceWatchConversation,
|
|
3616
|
-
canUseResourceEventSubscriptionTools,
|
|
3617
3549
|
getSlackToolContext,
|
|
3618
3550
|
resolveViewerUser,
|
|
3619
3551
|
findUserByEmail,
|
|
3620
3552
|
updateViewerDisplayName,
|
|
3621
3553
|
readActorIdentity,
|
|
3554
|
+
runWithWorkspaceTeamId,
|
|
3555
|
+
getWorkspaceTeamId,
|
|
3556
|
+
SlackActionError,
|
|
3557
|
+
getHeaderString,
|
|
3558
|
+
runWithSlackInstallationToken,
|
|
3559
|
+
normalizeSlackConversationId,
|
|
3560
|
+
withSlackRetries,
|
|
3561
|
+
getSlackClient,
|
|
3562
|
+
isDmChannel,
|
|
3563
|
+
isConversationScopedChannel,
|
|
3564
|
+
getFilePermalink,
|
|
3565
|
+
downloadPrivateSlackFile,
|
|
3622
3566
|
bindSlackDirectCredentialSubject,
|
|
3623
3567
|
verifySlackDirectCredentialSubject,
|
|
3624
3568
|
bindScheduledTaskCredentialSubject,
|