@sentry/junior 0.184.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.
Files changed (111) hide show
  1. package/dist/{agent-hooks-XTZVALGQ.js → agent-hooks-NGZQLL5O.js} +13 -11
  2. package/dist/api/acp/schema.d.ts +20 -2
  3. package/dist/api/code/overview.d.ts +39 -0
  4. package/dist/api/locations/detail.d.ts +1 -1
  5. package/dist/api/people/code.d.ts +22 -0
  6. package/dist/api/schema/code.d.ts +46 -0
  7. package/dist/api/schema/conversation.d.ts +100 -20
  8. package/dist/api/schema/location.d.ts +1 -1
  9. package/dist/api/schema/person.d.ts +1 -1
  10. package/dist/api/schema.d.ts +4 -4
  11. package/dist/api/schema.js +12 -1
  12. package/dist/api.js +358 -76
  13. package/dist/app.js +406 -377
  14. package/dist/{catalog-runtime-5357XR4F.js → catalog-runtime-AAEAO2RL.js} +4 -4
  15. package/dist/chat/api-turns/accepted-reply.d.ts +11 -0
  16. package/dist/chat/api-turns/cancellation.d.ts +3 -5
  17. package/dist/chat/api-turns/work.d.ts +4 -9
  18. package/dist/chat/app/conversation-work.d.ts +1 -1
  19. package/dist/chat/app/factory.d.ts +4 -0
  20. package/dist/chat/app/services.d.ts +4 -3
  21. package/dist/chat/conversations/history.d.ts +83 -9
  22. package/dist/chat/conversations/turn-lifecycle.d.ts +4 -3
  23. package/dist/chat/providers/slack/input.d.ts +32 -0
  24. package/dist/chat/{runtime → providers/slack}/processing-reaction.d.ts +6 -8
  25. package/dist/chat/{runtime/slack-resume.d.ts → providers/slack/resume.d.ts} +16 -19
  26. package/dist/chat/providers/slack/runtime.d.ts +1 -10
  27. package/dist/chat/providers/slack/turn.d.ts +11 -24
  28. package/dist/chat/resource-events/store.d.ts +1 -1
  29. package/dist/chat/resource-events/tool-support.d.ts +0 -5
  30. package/dist/chat/runtime/turn-execution.d.ts +7 -2
  31. package/dist/chat/sandbox/egress/credentials.d.ts +3 -3
  32. package/dist/chat/sandbox/egress/session.d.ts +5 -6
  33. package/dist/chat/scheduled-tasks/tool-support.d.ts +5 -5
  34. package/dist/chat/scheduled-tasks/tools/create.d.ts +2 -2
  35. package/dist/chat/scheduled-tasks/tools/delete.d.ts +2 -2
  36. package/dist/chat/scheduled-tasks/tools/list.d.ts +2 -2
  37. package/dist/chat/scheduled-tasks/tools/run-now.d.ts +2 -2
  38. package/dist/chat/scheduled-tasks/tools/update.d.ts +2 -2
  39. package/dist/chat/scheduled-tasks/types.d.ts +1 -1
  40. package/dist/chat/services/provider-retry.d.ts +2 -0
  41. package/dist/chat/services/turn-failure-response.d.ts +4 -0
  42. package/dist/chat/task-execution/paused-turn.d.ts +0 -2
  43. package/dist/chat/task-execution/state.d.ts +2 -2
  44. package/dist/chat/task-execution/store.d.ts +1 -1
  45. package/dist/chat/task-execution/turn-wake.d.ts +12 -0
  46. package/dist/chat/tool-support/action-review.d.ts +1 -1
  47. package/dist/chat/tools/resource-events.d.ts +1 -1
  48. package/dist/chat/tools/search-conversation-events.d.ts +1 -1
  49. package/dist/chat/tools/types.d.ts +1 -1
  50. package/dist/chat/tools/watch-resource-events.d.ts +2 -2
  51. package/dist/{chunk-FKEZZMXS.js → chunk-4HA36IWS.js} +9 -94
  52. package/dist/{chunk-WVIA7V3M.js → chunk-4K25ODAO.js} +27 -1
  53. package/dist/{chunk-UF6BUA5W.js → chunk-4S47VHJR.js} +3 -3
  54. package/dist/{chunk-JNO2GUDS.js → chunk-6DOAUXLD.js} +231 -198
  55. package/dist/{chunk-M3QYTBQ6.js → chunk-6Q7YZLCL.js} +2 -1
  56. package/dist/{chunk-6VOYP4M6.js → chunk-7GAP2XG4.js} +1 -1
  57. package/dist/{chunk-LHXIZLFF.js → chunk-7MHNIGHP.js} +1 -1
  58. package/dist/{chunk-IZDYYYGP.js → chunk-ABAX75GG.js} +10 -384
  59. package/dist/{chunk-WQFEWULF.js → chunk-ATUWN6Z3.js} +2 -2
  60. package/dist/{chunk-4RONS3WR.js → chunk-C6TS47CQ.js} +4 -4
  61. package/dist/chunk-D4YV5TDV.js +45 -0
  62. package/dist/{chunk-Z3QAXJR7.js → chunk-D63IUQQE.js} +4 -4
  63. package/dist/{chunk-M5YWNMTE.js → chunk-DAPC2HTM.js} +24 -6
  64. package/dist/{chunk-BHYRYYLY.js → chunk-EIJCPJOY.js} +1 -1
  65. package/dist/{chunk-SHJK75Z2.js → chunk-EJYP3NQ4.js} +4 -4
  66. package/dist/{chunk-K7VDC2QW.js → chunk-EWEH2PJW.js} +6 -6
  67. package/dist/{chunk-6WGC3SO4.js → chunk-FFHXG2FX.js} +13 -11
  68. package/dist/{chunk-MLSQX2EX.js → chunk-FTVMXDNN.js} +1 -1
  69. package/dist/chunk-GFBR4S3Q.js +406 -0
  70. package/dist/{chunk-PJPRXO2T.js → chunk-GJTIZQGJ.js} +1 -1
  71. package/dist/{chunk-BT53RIGJ.js → chunk-HJPC32TQ.js} +12 -50
  72. package/dist/{chunk-RF6J3DIO.js → chunk-INQHUE72.js} +84 -1
  73. package/dist/{chunk-6UDPINOX.js → chunk-LRI5IRGY.js} +19 -44
  74. package/dist/{chunk-BCHYQS4B.js → chunk-LVK4WJKM.js} +1 -1
  75. package/dist/{chunk-UBOIHNTH.js → chunk-MWOH6CIK.js} +178 -148
  76. package/dist/{chunk-3S7ULUWW.js → chunk-OMQ2J6F6.js} +1 -1
  77. package/dist/{chunk-DUKIDPF6.js → chunk-PNYJOEVV.js} +1 -1
  78. package/dist/{chunk-MAPJFUMV.js → chunk-QCT4226Q.js} +3 -3
  79. package/dist/{chunk-HIPUIPJM.js → chunk-QZKHHG3B.js} +34 -8
  80. package/dist/{chunk-QABCY33S.js → chunk-TVVAMOLS.js} +2 -2
  81. package/dist/{chunk-JA43DV4Y.js → chunk-U4CTAVWN.js} +1 -1
  82. package/dist/{chunk-LCP4EDKR.js → chunk-UUULGCKK.js} +1 -1
  83. package/dist/{chunk-4CNDGK25.js → chunk-VH4IU3UC.js} +928 -984
  84. package/dist/chunk-W7GCLNIV.js +58 -0
  85. package/dist/{chunk-2ZQPVX7C.js → chunk-WLKWN4CD.js} +22 -12
  86. package/dist/{chunk-ACPDVEJO.js → chunk-X2SS6BF7.js} +198 -209
  87. package/dist/{chunk-YKX6MTLU.js → chunk-X7NCJQAA.js} +1 -1
  88. package/dist/{chunk-E57WPXWY.js → chunk-Y5G23T2D.js} +5 -5
  89. package/dist/{chunk-HUXFMDBZ.js → chunk-ZIF2S464.js} +1 -1
  90. package/dist/{chunk-JM2COLGC.js → chunk-ZKKGFUWM.js} +6 -6
  91. package/dist/cli/chat.js +35 -33
  92. package/dist/cli/check.js +5 -5
  93. package/dist/cli/plugins.js +14 -12
  94. package/dist/cli/snapshot-create.js +10 -9
  95. package/dist/cli/upgrade.js +7 -6
  96. package/dist/{db-MA52H62H.js → db-TRJ77FAU.js} +8 -6
  97. package/dist/instrumentation.js +3 -3
  98. package/dist/nitro.js +7 -6
  99. package/dist/{oauth-callback-server-DXGHKN6S.js → oauth-callback-server-XD5SV3MS.js} +30 -27
  100. package/dist/{runner-4IXUAQNK.js → runner-KFX7NG6D.js} +24 -19
  101. package/dist/{skills-X3DY7AXO.js → skills-J2AH26CW.js} +5 -5
  102. package/dist/spawn-VU3CSRI6.js +41 -0
  103. package/dist/task-runner-5JG3VIIY.js +34 -0
  104. package/dist/{validation-Z6JXSTJS.js → validation-O5J4VOPF.js} +5 -5
  105. package/dist/version.js +1 -1
  106. package/dist/{work-OKWRFBDW.js → work-ECPQTG3U.js} +24 -19
  107. package/dist/worker-RS3DXUDO.js +24 -0
  108. package/package.json +4 -4
  109. package/dist/spawn-43WGS6CH.js +0 -37
  110. package/dist/task-runner-VDRKITPV.js +0 -32
  111. package/dist/worker-B236AE72.js +0 -22
@@ -5,7 +5,7 @@ import {
5
5
  getConversationEventStore,
6
6
  getDb,
7
7
  getSqlExecutor
8
- } from "./chunk-IZDYYYGP.js";
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-BCHYQS4B.js";
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-SHJK75Z2.js";
27
+ } from "./chunk-EJYP3NQ4.js";
28
28
  import {
29
29
  getStateAdapter
30
- } from "./chunk-DUKIDPF6.js";
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-6UDPINOX.js";
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-3S7ULUWW.js";
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/slack/client.ts
472
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
473
- import { WebClient } from "@slack/web-api";
474
-
475
- // src/chat/slack/workspace-context.ts
476
- import { AsyncLocalStorage } from "async_hooks";
477
- var workspaceTeamIdStorage = new AsyncLocalStorage();
478
- function runWithWorkspaceTeamId(teamId, fn) {
479
- if (!teamId) return fn();
480
- return workspaceTeamIdStorage.run(teamId, fn);
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 getWorkspaceTeamId() {
483
- return workspaceTeamIdStorage.getStore();
510
+ function subscriptionKey(id) {
511
+ return `${RESOURCE_EVENT_PREFIX}:record:${id}`;
484
512
  }
485
-
486
- // src/chat/slack/client.ts
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 getHeaderString(headers, name) {
535
- if (!headers || typeof headers !== "object") {
536
- return void 0;
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
- const key = name.toLowerCase();
539
- const entries = headers;
540
- for (const [entryKey, value] of Object.entries(entries)) {
541
- if (entryKey.toLowerCase() !== key) continue;
542
- if (typeof value === "string") return value;
543
- if (Array.isArray(value)) {
544
- const first = value.find((entry) => typeof entry === "string");
545
- return typeof first === "string" ? first : void 0;
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 parseSlackCanvasDetail(detail) {
551
- if (typeof detail !== "string") {
552
- return {};
553
- }
554
- const trimmed = detail.trim();
555
- if (!trimmed) {
556
- return {};
557
- }
558
- const parsed = {
559
- detail: trimmed
560
- };
561
- const lineMatch = trimmed.match(/line\s+(\d+):/i);
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
- if (/unsupported heading depth/i.test(trimmed)) {
569
- parsed.detailRule = "unsupported_heading_depth";
590
+ const lockError = lockLostError ?? heartbeatError;
591
+ if (lockError) {
592
+ throw lockError;
570
593
  }
571
- return parsed;
594
+ return result;
572
595
  }
573
- var installationTokenStorage = new AsyncLocalStorage2();
574
- var clientsByToken = /* @__PURE__ */ new Map();
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 resolveSlackToken() {
586
- const ambientToken = installationTokenStorage.getStore()?.token;
587
- if (ambientToken) {
588
- return ambientToken;
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
- const envToken = getSlackBotToken();
591
- if (envToken) {
592
- return envToken;
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
- const teamId = getWorkspaceTeamId();
595
- if (teamId) {
596
- throw new SlackActionError(
597
- `Slack call is scoped to workspace ${teamId} but no installation token is bound and no default bot token is configured`,
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 normalizeSlackConversationId(channelId) {
607
- return parseSlackChannelReferenceId(channelId);
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 getClient() {
610
- const token = resolveSlackToken();
611
- let cached = clientsByToken.get(token);
612
- if (!cached) {
613
- cached = new WebClient(token, {
614
- retryConfig: { retries: 0 },
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 mapSlackError(error) {
622
- if (error instanceof SlackActionError) {
623
- return error;
624
- }
625
- const candidate = error;
626
- const apiError = candidate.data?.error;
627
- const message = candidate.message ?? "Slack action failed";
628
- const baseOptions = {
629
- apiError,
630
- statusCode: candidate.statusCode,
631
- requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
632
- errorData: serializeSlackErrorData(candidate.data),
633
- ...parseSlackCanvasDetail(candidate.data?.detail)
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
- var MAX_RETRY_DELAY_MS = 1e4;
684
- var MAX_TOTAL_RETRY_DELAY_MS = 2e4;
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
- const candidate = error;
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 hasSocketHangUpMessage(error) {
707
- return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
657
+ function activeAt(subscription, nowMs) {
658
+ return subscription.status === "active" && subscription.expiresAtMs > nowMs;
708
659
  }
709
- function classifySlackRetry(raw, mapped) {
710
- if (mapped.code === "rate_limited") {
711
- return "rate_limited";
712
- }
713
- if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) {
714
- return "server_error";
715
- }
716
- const networkCode = findNetworkErrorCode(raw);
717
- if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) {
718
- return "connection";
719
- }
720
- if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) {
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 (hasSocketHangUpMessage(raw)) {
724
- return "connection";
673
+ if (input.expiresAtMs <= nowMs) {
674
+ throw new Error("Resource event subscription expiry must be in the future");
725
675
  }
726
- return "none";
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 withSlackRetries(task, maxAttempts = 3, context = {}) {
729
- let attempt = 0;
730
- let totalDelayMs = 0;
731
- const action = context.action ?? "unknown";
732
- while (attempt < maxAttempts) {
733
- attempt += 1;
734
- const attemptNumber = attempt;
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 getSlackClient() {
821
- return getClient();
822
- }
823
- function isDmChannel(channelId) {
824
- const normalized = normalizeSlackConversationId(channelId);
825
- return Boolean(normalized && normalized.startsWith("D"));
826
- }
827
- function isConversationScopedChannel(channelId) {
828
- const normalized = normalizeSlackConversationId(channelId);
829
- if (!normalized) return false;
830
- return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
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 isConversationChannel(channelId) {
833
- const normalized = normalizeSlackConversationId(channelId);
834
- if (!normalized) return false;
835
- return normalized.startsWith("C") || normalized.startsWith("G");
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 getFilePermalink(fileId) {
838
- const client = getClient();
839
- const response = await withSlackRetries(
840
- () => client.files.info({
841
- file: fileId
842
- }),
843
- 3,
844
- {
845
- action: "files.info",
846
- idempotent: true,
847
- spanAttributes: { "app.slack.file_id": fileId }
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 downloadPrivateSlackFile(url) {
853
- const token = resolveSlackToken();
854
- return withSpan(
855
- "GET files.slack.com",
856
- "http.client",
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 response = await fetch(url, {
860
- headers: {
861
- Authorization: `Bearer ${token}`
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
- setSpanAttributes({ "http.response.status_code": response.status });
865
- if (!response.ok) {
866
- setSpanAttributes({ "error.type": String(response.status) });
867
- setSpanStatus("error");
868
- throw new Error(`Slack file download failed: ${response.status}`);
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 Buffer.from(await response.arrayBuffer());
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/workspace.ts
882
- function canRouteResourceEvents() {
883
- return Boolean(getSlackBotToken());
884
- }
885
- function createResourceEventTeamIdResolver() {
886
- let pending;
887
- return async () => {
888
- if (!canRouteResourceEvents()) return void 0;
889
- pending ??= getSlackClient().auth.test().then((result) => {
890
- const teamId = result.team_id?.trim();
891
- if (!teamId) {
892
- throw new Error("Slack auth.test did not return a workspace team id");
893
- }
894
- return teamId;
895
- }).catch((error) => {
896
- pending = void 0;
897
- throw error;
898
- });
899
- return await pending;
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/resource-events/store.ts
904
- import { createHash as createHash2 } from "crypto";
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
- destinationSchema,
907
- resourceEventMatchSchema,
908
- resourceEventMatches,
909
- resourceEventTypeSchema,
910
- stableResourceEventMatchKey
958
+ identitySchema,
959
+ userSchema
911
960
  } from "@sentry/junior-plugin-api";
912
- import { z as z2 } from "zod";
913
- var RESOURCE_EVENT_PREFIX = "junior:resource_event_subscription:v3";
914
- var INDEX_LOCK_TTL_MS = 1e4;
915
- var SUBSCRIPTION_LOCK_TTL_MS = 1e4;
916
- var SUBSCRIPTION_LOCK_WAIT_MS = 1e4;
917
- var SUBSCRIPTION_LOCK_RETRY_MS = 25;
918
- var SUBSCRIPTION_LOCK_HEARTBEAT_MS = 3e3;
919
- var subscriptionStatusSchema = z2.enum(["active", "cancelled", "completed"]);
920
- var subscriptionIdIndexSchema = z2.array(z2.string().min(1));
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 subscriptionKey(id) {
941
- return `${RESOURCE_EVENT_PREFIX}:record:${id}`;
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 subscriptionLockKey(id) {
944
- return `${RESOURCE_EVENT_PREFIX}:lock:${id}`;
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 resourceIndexKey(teamId, namespace, identifier) {
947
- return `${RESOURCE_EVENT_PREFIX}:resource:${digest(`${teamId}\0${namespace}\0${identifier}`)}`;
1004
+ async function resolveViewerUser(email) {
1005
+ return await resolveViewerUserFromSql(getDb(), email);
948
1006
  }
949
- function subscriptionTeamId(destination) {
950
- if (destination.platform !== "slack") {
951
- throw new Error("Resource event subscriptions require a Slack destination");
952
- }
953
- return destination.teamId;
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 conversationIndexKey(conversationId) {
956
- return `${RESOURCE_EVENT_PREFIX}:conversation:${digest(conversationId)}`;
1013
+ async function findUserByEmail(email) {
1014
+ return await findUserByEmailFromSql(getDb(), email);
957
1015
  }
958
- function indexLockKey(key) {
959
- return `${key}:lock`;
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 sleep2(ms) {
962
- await new Promise((resolve) => {
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 acquireSubscriptionLock(state, subscriptionId, waitDeadlineMs = Date.now() + SUBSCRIPTION_LOCK_WAIT_MS) {
968
- while (true) {
969
- const lock = await state.acquireLock(
970
- subscriptionLockKey(subscriptionId),
971
- SUBSCRIPTION_LOCK_TTL_MS
972
- );
973
- if (lock) {
974
- return lock;
975
- }
976
- if (Date.now() >= waitDeadlineMs) {
977
- throw new Error(
978
- `Could not acquire resource event subscription lock for ${subscriptionId}`
979
- );
980
- }
981
- await sleep2(SUBSCRIPTION_LOCK_RETRY_MS);
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 withSubscriptionLock(state, subscriptionId, callback, waitDeadlineMs) {
985
- const lock = await acquireSubscriptionLock(
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
- function ttlUntil(expiresAtMs, nowMs) {
1027
- return Math.max(1, expiresAtMs - nowMs);
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
- async function readSubscriptionIdIndex(state, key) {
1030
- const value = await state.get(key);
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
- async function withIndexLock(state, key, callback) {
1043
- const lock = await state.acquireLock(indexLockKey(key), INDEX_LOCK_TTL_MS);
1044
- if (!lock) {
1045
- throw new Error(`Could not acquire resource event index lock for ${key}`);
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
- try {
1048
- return await callback();
1049
- } finally {
1050
- await state.releaseLock(lock);
1095
+ };
1096
+ function serializeSlackErrorData(data) {
1097
+ if (!data || typeof data !== "object") {
1098
+ return void 0;
1051
1099
  }
1052
- }
1053
- async function addToIndex(state, key, subscriptionId, nowMs) {
1054
- await withIndexLock(state, key, async () => {
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
- const latestExpiresAtMs = Math.max(
1074
- nowMs,
1075
- ...records.filter(
1076
- (record) => record !== void 0 && activeAt(record, nowMs)
1077
- ).map((record) => record.expiresAtMs)
1078
- );
1079
- return ttlUntil(latestExpiresAtMs, nowMs);
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 matchesEvent(subscription, input) {
1091
- 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);
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
- async function createResourceEventSubscription(input, options = {}) {
1094
- const state = options.state ?? getStateAdapter();
1095
- await state.connect();
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
- if (input.expiresAtMs <= nowMs) {
1104
- throw new Error("Resource event subscription expiry must be in the future");
1135
+ const trimmed = detail.trim();
1136
+ if (!trimmed) {
1137
+ return {};
1105
1138
  }
1106
- const match = input.match && Object.keys(input.match).length > 0 ? input.match : void 0;
1107
- const id = buildSubscriptionId({
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 parsed = subscriptionSchema.parse(record);
1132
- await state.set(
1133
- subscriptionKey(id),
1134
- parsed,
1135
- ttlUntil(parsed.expiresAtMs, nowMs)
1136
- );
1137
- await addToIndex(
1138
- state,
1139
- resourceIndexKey(
1140
- subscriptionTeamId(parsed.destination),
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
- async function listResourceEventSubscriptions(input) {
1156
- const state = input.state ?? getStateAdapter();
1157
- await state.connect();
1158
- const nowMs = input.nowMs ?? Date.now();
1159
- const ids = await readSubscriptionIdIndex(
1160
- state,
1161
- conversationIndexKey(input.conversationId)
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
- return next;
1210
- });
1163
+ }
1164
+ return installationTokenStorage.run({ token: trimmed }, fn);
1211
1165
  }
1212
- async function cancelSubscriptions(input) {
1213
- const subscriptions = await listResourceEventSubscriptions(input);
1214
- for (const subscription of subscriptions) {
1215
- await cancelResourceEventSubscription({
1216
- conversationId: input.conversationId,
1217
- id: subscription.id,
1218
- nowMs: input.nowMs,
1219
- state: input.state
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
- async function collectResourceEventMatchKeys(input) {
1224
- const state = input.state ?? getStateAdapter();
1225
- await state.connect();
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
- return [...keys].sort();
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
- async function findMatchingResourceEventSubscriptions(input) {
1260
- const state = input.state ?? getStateAdapter();
1261
- await state.connect();
1262
- const nowMs = input.nowMs ?? Date.now();
1263
- const ids = await readSubscriptionIdIndex(
1264
- state,
1265
- resourceIndexKey(input.teamId, input.namespace, input.identifier)
1266
- );
1267
- const records = await Promise.all(
1268
- ids.map(
1269
- async (id) => parseSubscription(await state.get(subscriptionKey(id)))
1270
- )
1271
- );
1272
- return records.filter(
1273
- (record) => record !== void 0 && matchesEvent(record, {
1274
- data: input.data,
1275
- eventType: input.eventType,
1276
- nowMs,
1277
- namespace: input.namespace,
1278
- identifier: input.identifier,
1279
- teamId: input.teamId
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
- async function deliverResourceEventSubscription(input) {
1284
- const state = input.state ?? getStateAdapter();
1285
- await state.connect();
1286
- return await withSubscriptionLock(
1287
- state,
1288
- input.subscription.id,
1289
- async () => {
1290
- const nowMs = input.nowMs ?? Date.now();
1291
- const current = parseSubscription(
1292
- await state.get(subscriptionKey(input.subscription.id))
1293
- );
1294
- if (!current || !matchesEvent(current, {
1295
- data: input.data,
1296
- eventType: input.eventType,
1297
- nowMs,
1298
- namespace: input.namespace,
1299
- identifier: input.identifier,
1300
- teamId: input.teamId
1301
- })) {
1302
- return false;
1303
- }
1304
- const delivered = await input.deliver(current);
1305
- if (input.terminal) {
1306
- const latest = parseSubscription(
1307
- await state.get(subscriptionKey(current.id))
1308
- );
1309
- if (!latest || latest.status !== current.status || latest.updatedAtMs !== current.updatedAtMs) {
1310
- return delivered;
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
- const next = {
1313
- ...current,
1314
- status: "completed",
1315
- updatedAtMs: nowMs
1316
- };
1317
- await state.set(
1318
- subscriptionKey(current.id),
1319
- next,
1320
- JUNIOR_THREAD_STATE_TTL_MS
1321
- );
1322
- await removeFromIndex(
1323
- state,
1324
- resourceIndexKey(
1325
- subscriptionTeamId(current.destination),
1326
- current.namespace,
1327
- current.identifier
1328
- ),
1329
- current.id,
1330
- nowMs
1331
- );
1332
- await removeFromIndex(
1333
- state,
1334
- conversationIndexKey(current.conversationId),
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
- return delivered;
1340
- },
1341
- input.waitDeadlineMs
1342
- );
1343
- }
1344
-
1345
- // src/chat/tools/execution/tool-input-error.ts
1346
- var ToolInputError = class extends Error {
1347
- constructor(message, options) {
1348
- super(message, options);
1349
- this.name = "ToolInputError";
1350
- }
1351
- };
1352
-
1353
- // src/chat/resource-events/tool-support.ts
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
- return {
1408
- destination: context.destination,
1409
- source: context.source,
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
- async function resolveViewerUser(email) {
1470
- return await resolveViewerUserFromSql(getDb(), email);
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
- async function findUserByEmail(email) {
1479
- return await findUserByEmailFromSql(getDb(), email);
1404
+ function isDmChannel(channelId) {
1405
+ const normalized = normalizeSlackConversationId(channelId);
1406
+ return Boolean(normalized && normalized.startsWith("D"));
1480
1407
  }
1481
- async function updateViewerDisplayNameFromSql(db, userId, displayName) {
1482
- const rows = await db.update(juniorUsers).set({ displayName, updatedAt: /* @__PURE__ */ new Date() }).where(eq3(juniorUsers.id, userId)).returning();
1483
- const userRow = rows[0];
1484
- return userRow ? await readUserById(db, userRow) : void 0;
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
- async function updateViewerDisplayName(userId, displayName) {
1487
- return await updateViewerDisplayNameFromSql(getDb(), userId, displayName);
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 readActorIdentityFromSql(db, actor) {
1490
- if (actor.platform === "system") return void 0;
1491
- const providerTenantId = actor.platform === "slack" ? actor.teamId : "";
1492
- const rows = await db.select({
1493
- identity: juniorIdentities,
1494
- user: juniorUsers
1495
- }).from(juniorIdentities).leftJoin(juniorUsers, eq3(juniorUsers.id, juniorIdentities.userId)).where(
1496
- and3(
1497
- eq3(juniorIdentities.kind, "user"),
1498
- eq3(juniorIdentities.provider, actor.platform),
1499
- eq3(juniorIdentities.providerTenantId, providerTenantId),
1500
- eq3(juniorIdentities.providerSubjectId, actor.userId)
1501
- )
1502
- ).limit(1);
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 readActorIdentity(actor) {
1511
- return await readActorIdentityFromSql(getDb(), actor);
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 = context.conversationId ? getDashboardConversationLink(context.conversationId) : void 0;
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 = context.conversationId ? createPluginAnnotations({
2686
+ const annotations = createPluginAnnotations({
2738
2687
  conversationId: context.conversationId,
2739
2688
  db: getDb(),
2740
2689
  plugin: pluginName
2741
- }) : void 0;
2690
+ });
2742
2691
  const mcp = pluginMcpContext(plugin, context);
2743
- const canSubscribe = context.source.platform === "slack" && context.destination.platform === "slack" && Boolean(context.conversationId) && canRouteResourceEvents() && Boolean(plugin.resourceEvents) && plugin.resourceEvents?.isEnabled?.() !== false;
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 || context.destination.platform !== "slack") {
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
- ...annotations ? { annotations } : void 0,
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,