@sentry/junior 0.184.0 → 0.186.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 (129) hide show
  1. package/dist/{agent-hooks-XTZVALGQ.js → agent-hooks-4B3Z4WRK.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 +108 -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 +368 -78
  13. package/dist/app.js +1412 -538
  14. package/dist/{catalog-runtime-5357XR4F.js → catalog-runtime-AFJCAOHC.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/mailbox-input.d.ts +7 -0
  18. package/dist/chat/api-turns/routing.d.ts +18 -3
  19. package/dist/chat/api-turns/work.d.ts +4 -9
  20. package/dist/chat/app/conversation-work.d.ts +1 -1
  21. package/dist/chat/app/factory.d.ts +4 -0
  22. package/dist/chat/app/services.d.ts +4 -3
  23. package/dist/chat/conversations/history.d.ts +83 -9
  24. package/dist/chat/conversations/turn-lifecycle.d.ts +6 -3
  25. package/dist/chat/providers/slack/input.d.ts +32 -0
  26. package/dist/chat/{runtime → providers/slack}/processing-reaction.d.ts +6 -8
  27. package/dist/chat/{runtime/slack-resume.d.ts → providers/slack/resume.d.ts} +16 -19
  28. package/dist/chat/providers/slack/runtime.d.ts +1 -12
  29. package/dist/chat/providers/slack/turn.d.ts +10 -25
  30. package/dist/chat/resource-events/actor.d.ts +7 -2
  31. package/dist/chat/resource-events/app-publisher.d.ts +18 -0
  32. package/dist/chat/resource-events/catalog.d.ts +19 -0
  33. package/dist/chat/resource-events/ingest.d.ts +0 -1
  34. package/dist/chat/resource-events/notification.d.ts +28 -3
  35. package/dist/chat/resource-events/runtime-catalog.d.ts +7 -2
  36. package/dist/chat/resource-events/store.d.ts +2 -14
  37. package/dist/chat/resource-events/tool-support.d.ts +0 -5
  38. package/dist/chat/resource-events/workspace.d.ts +7 -3
  39. package/dist/chat/runtime/turn-execution.d.ts +7 -2
  40. package/dist/chat/sandbox/egress/credentials.d.ts +5 -4
  41. package/dist/chat/sandbox/egress/session.d.ts +10 -11
  42. package/dist/chat/sandbox/errors.d.ts +1 -1
  43. package/dist/chat/sandbox/ref.d.ts +1 -1
  44. package/dist/chat/sandbox/snapshot/events.d.ts +8 -1
  45. package/dist/chat/sandbox/snapshot/not-ready-error.d.ts +6 -1
  46. package/dist/chat/scheduled-tasks/tool-support.d.ts +5 -5
  47. package/dist/chat/scheduled-tasks/tools/create.d.ts +2 -2
  48. package/dist/chat/scheduled-tasks/tools/delete.d.ts +2 -2
  49. package/dist/chat/scheduled-tasks/tools/list.d.ts +2 -2
  50. package/dist/chat/scheduled-tasks/tools/run-now.d.ts +2 -2
  51. package/dist/chat/scheduled-tasks/tools/update.d.ts +2 -2
  52. package/dist/chat/scheduled-tasks/types.d.ts +1 -1
  53. package/dist/chat/sentry-links.d.ts +2 -0
  54. package/dist/chat/services/provider-retry.d.ts +2 -0
  55. package/dist/chat/services/turn-failure-response.d.ts +4 -0
  56. package/dist/chat/services/turn-session-routing.d.ts +20 -5
  57. package/dist/chat/task-execution/paused-turn.d.ts +0 -2
  58. package/dist/chat/task-execution/slack-resource-event.d.ts +47 -0
  59. package/dist/chat/task-execution/slack-work.d.ts +0 -21
  60. package/dist/chat/task-execution/state.d.ts +2 -2
  61. package/dist/chat/task-execution/store.d.ts +1 -1
  62. package/dist/chat/task-execution/turn-wake.d.ts +12 -0
  63. package/dist/chat/tool-support/action-review.d.ts +1 -1
  64. package/dist/chat/tools/event-tasks.d.ts +1 -1
  65. package/dist/chat/tools/resource-events.d.ts +2 -2
  66. package/dist/chat/tools/search-conversation-events.d.ts +1 -1
  67. package/dist/chat/tools/types.d.ts +1 -1
  68. package/dist/chat/tools/watch-resource-events.d.ts +2 -2
  69. package/dist/{chunk-Z3QAXJR7.js → chunk-2IBDVE62.js} +12 -9
  70. package/dist/{chunk-4RONS3WR.js → chunk-2U7EPTRZ.js} +11 -5
  71. package/dist/{chunk-3S7ULUWW.js → chunk-3CVEE6LA.js} +1 -1
  72. package/dist/{chunk-IZDYYYGP.js → chunk-43LODHJM.js} +24 -384
  73. package/dist/{chunk-LHXIZLFF.js → chunk-64WF2GQ3.js} +1 -1
  74. package/dist/{chunk-YKX6MTLU.js → chunk-7EOWT62G.js} +1 -1
  75. package/dist/{chunk-UBOIHNTH.js → chunk-7EU4YSZ4.js} +362 -188
  76. package/dist/{chunk-UF6BUA5W.js → chunk-ACGWTJ74.js} +3 -3
  77. package/dist/{chunk-6UDPINOX.js → chunk-AD64YMEC.js} +19 -44
  78. package/dist/{chunk-MLSQX2EX.js → chunk-BRKB5R7P.js} +1 -1
  79. package/dist/{chunk-E57WPXWY.js → chunk-CM6RTYSO.js} +5 -5
  80. package/dist/{chunk-MAPJFUMV.js → chunk-CV5CPRVG.js} +3 -3
  81. package/dist/chunk-D4YV5TDV.js +45 -0
  82. package/dist/{chunk-HIPUIPJM.js → chunk-DHYZ4VKG.js} +383 -254
  83. package/dist/{chunk-DUKIDPF6.js → chunk-DOP3FIQC.js} +1 -1
  84. package/dist/{chunk-BT53RIGJ.js → chunk-EWRPUXE3.js} +12 -50
  85. package/dist/{chunk-BHYRYYLY.js → chunk-FFYDRLXW.js} +1 -1
  86. package/dist/chunk-GFBR4S3Q.js +406 -0
  87. package/dist/chunk-GQMQCY7H.js +58 -0
  88. package/dist/{chunk-JNO2GUDS.js → chunk-GSB3NTNS.js} +926 -1856
  89. package/dist/{chunk-2ZQPVX7C.js → chunk-H7X2G2KS.js} +25 -12
  90. package/dist/{chunk-WQFEWULF.js → chunk-HKFLRIBD.js} +2 -2
  91. package/dist/{chunk-SHJK75Z2.js → chunk-HWXP6RHG.js} +38 -4
  92. package/dist/{chunk-RF6J3DIO.js → chunk-INQHUE72.js} +84 -1
  93. package/dist/{chunk-4CNDGK25.js → chunk-JFPY6XFF.js} +911 -993
  94. package/dist/{chunk-6WGC3SO4.js → chunk-MCLWOBPN.js} +80 -18
  95. package/dist/{chunk-PJPRXO2T.js → chunk-NRMAOGRS.js} +1 -1
  96. package/dist/{chunk-ACPDVEJO.js → chunk-O6GQO7VP.js} +201 -218
  97. package/dist/{chunk-M5YWNMTE.js → chunk-OZG7E22Y.js} +24 -6
  98. package/dist/{chunk-WVIA7V3M.js → chunk-PFAZSRGR.js} +29 -1
  99. package/dist/{chunk-LCP4EDKR.js → chunk-PZZOGHVR.js} +14 -4
  100. package/dist/{chunk-QABCY33S.js → chunk-QBL2AVFF.js} +2 -2
  101. package/dist/{chunk-FKEZZMXS.js → chunk-QU3URVWG.js} +9 -94
  102. package/dist/{chunk-M3QYTBQ6.js → chunk-STOCCPWH.js} +11 -2
  103. package/dist/{chunk-K7VDC2QW.js → chunk-TXLBLGNQ.js} +6 -6
  104. package/dist/{chunk-JA43DV4Y.js → chunk-VOGOTEW7.js} +1 -1
  105. package/dist/{chunk-HUXFMDBZ.js → chunk-VQIWBJOP.js} +1 -1
  106. package/dist/{chunk-JM2COLGC.js → chunk-VQZYIVYN.js} +6 -6
  107. package/dist/{chunk-BCHYQS4B.js → chunk-VXL2LBPK.js} +1 -1
  108. package/dist/{chunk-6VOYP4M6.js → chunk-YXSBZRP7.js} +1 -1
  109. package/dist/cli/chat.js +35 -33
  110. package/dist/cli/check.js +5 -5
  111. package/dist/cli/plugins.js +14 -12
  112. package/dist/cli/snapshot-create.js +10 -9
  113. package/dist/cli/upgrade.js +7 -6
  114. package/dist/{db-MA52H62H.js → db-7XL4UPAE.js} +8 -6
  115. package/dist/instrumentation.js +3 -3
  116. package/dist/nitro.js +7 -6
  117. package/dist/{oauth-callback-server-DXGHKN6S.js → oauth-callback-server-RAOYHWDI.js} +30 -27
  118. package/dist/{runner-4IXUAQNK.js → runner-KMPBWRPM.js} +24 -19
  119. package/dist/{skills-X3DY7AXO.js → skills-IPCALGLQ.js} +5 -5
  120. package/dist/spawn-OSOWTMBG.js +41 -0
  121. package/dist/task-runner-IQNBA3RY.js +34 -0
  122. package/dist/{validation-Z6JXSTJS.js → validation-BSRCCX6N.js} +5 -5
  123. package/dist/version.js +1 -1
  124. package/dist/{work-OKWRFBDW.js → work-T6UFTOJY.js} +24 -19
  125. package/dist/worker-L35OG7VA.js +24 -0
  126. package/package.json +5 -5
  127. package/dist/spawn-43WGS6CH.js +0 -37
  128. package/dist/task-runner-VDRKITPV.js +0 -32
  129. 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-43LODHJM.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-VXL2LBPK.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-HWXP6RHG.js";
28
28
  import {
29
29
  getStateAdapter
30
- } from "./chunk-DUKIDPF6.js";
30
+ } from "./chunk-DOP3FIQC.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-AD64YMEC.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-3CVEE6LA.js";
54
56
 
55
57
  // src/chat/plugins/agent-hooks.ts
56
58
  import {
@@ -468,1051 +470,971 @@ 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
+ resourceEventMatchSchema,
477
+ resourceEventMatches,
478
+ resourceEventTypeSchema,
479
+ stableResourceEventMatchKey
480
+ } from "@sentry/junior-plugin-api";
481
+ import { z as z2 } from "zod";
482
+ var RESOURCE_EVENT_PREFIX = "junior:resource_event_subscription:v5";
483
+ var INDEX_LOCK_TTL_MS = 1e4;
484
+ var SUBSCRIPTION_LOCK_TTL_MS = 1e4;
485
+ var SUBSCRIPTION_LOCK_WAIT_MS = 1e4;
486
+ var SUBSCRIPTION_LOCK_RETRY_MS = 25;
487
+ var SUBSCRIPTION_LOCK_HEARTBEAT_MS = 3e3;
488
+ var subscriptionStatusSchema = z2.enum(["active", "cancelled", "completed"]);
489
+ var subscriptionIdIndexSchema = z2.array(z2.string().min(1));
490
+ var subscriptionSchema = z2.object({
491
+ conversationId: z2.string().min(1),
492
+ createdAtMs: z2.number().finite(),
493
+ events: z2.array(resourceEventTypeSchema).min(1),
494
+ expiresAtMs: z2.number().finite(),
495
+ id: z2.string().min(1),
496
+ intent: z2.string().min(1),
497
+ label: z2.string().min(1),
498
+ match: resourceEventMatchSchema.optional(),
499
+ namespace: z2.string().min(1),
500
+ identifier: z2.string().min(1),
501
+ resourceType: z2.string().min(1),
502
+ status: subscriptionStatusSchema,
503
+ updatedAtMs: z2.number().finite()
504
+ }).strict();
505
+ function digest(value) {
506
+ return createHash2("sha256").update(value).digest("hex").slice(0, 32);
481
507
  }
482
- function getWorkspaceTeamId() {
483
- return workspaceTeamIdStorage.getStore();
508
+ function subscriptionKey(id) {
509
+ return `${RESOURCE_EVENT_PREFIX}:record:${id}`;
484
510
  }
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
- }
511
+ function subscriptionLockKey(id) {
512
+ return `${RESOURCE_EVENT_PREFIX}:lock:${id}`;
533
513
  }
534
- function getHeaderString(headers, name) {
535
- if (!headers || typeof headers !== "object") {
536
- return void 0;
537
- }
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;
514
+ function resourceIndexKey(namespace, identifier) {
515
+ return `${RESOURCE_EVENT_PREFIX}:resource:${digest(`${namespace}\0${identifier}`)}`;
516
+ }
517
+ function conversationIndexKey(conversationId) {
518
+ return `${RESOURCE_EVENT_PREFIX}:conversation:${digest(conversationId)}`;
519
+ }
520
+ function indexLockKey(key) {
521
+ return `${key}:lock`;
522
+ }
523
+ async function sleep2(ms) {
524
+ await new Promise((resolve) => {
525
+ const timer = setTimeout(resolve, ms);
526
+ timer.unref?.();
527
+ });
528
+ }
529
+ async function acquireSubscriptionLock(state, subscriptionId, waitDeadlineMs = Date.now() + SUBSCRIPTION_LOCK_WAIT_MS) {
530
+ while (true) {
531
+ const lock = await state.acquireLock(
532
+ subscriptionLockKey(subscriptionId),
533
+ SUBSCRIPTION_LOCK_TTL_MS
534
+ );
535
+ if (lock) {
536
+ return lock;
537
+ }
538
+ if (Date.now() >= waitDeadlineMs) {
539
+ throw new Error(
540
+ `Could not acquire resource event subscription lock for ${subscriptionId}`
541
+ );
546
542
  }
543
+ await sleep2(SUBSCRIPTION_LOCK_RETRY_MS);
547
544
  }
548
- return void 0;
549
545
  }
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;
546
+ async function withSubscriptionLock(state, subscriptionId, callback, waitDeadlineMs) {
547
+ const lock = await acquireSubscriptionLock(
548
+ state,
549
+ subscriptionId,
550
+ waitDeadlineMs
551
+ );
552
+ let lockLostError;
553
+ let heartbeatError;
554
+ let heartbeat;
555
+ const timer = setInterval(() => {
556
+ if (heartbeat) {
557
+ return;
566
558
  }
559
+ heartbeat = state.extendLock(lock, SUBSCRIPTION_LOCK_TTL_MS).then((extended) => {
560
+ if (extended) {
561
+ heartbeatError = void 0;
562
+ } else {
563
+ lockLostError = new Error(
564
+ `Resource event subscription lock was lost for ${subscriptionId}`
565
+ );
566
+ }
567
+ }).catch((error) => {
568
+ heartbeatError = error instanceof Error ? error : new Error(`Failed to extend subscription lock: ${String(error)}`);
569
+ }).finally(() => {
570
+ heartbeat = void 0;
571
+ });
572
+ }, SUBSCRIPTION_LOCK_HEARTBEAT_MS);
573
+ timer.unref?.();
574
+ let result;
575
+ try {
576
+ result = await callback();
577
+ } finally {
578
+ clearInterval(timer);
579
+ await heartbeat;
580
+ await state.releaseLock(lock);
567
581
  }
568
- if (/unsupported heading depth/i.test(trimmed)) {
569
- parsed.detailRule = "unsupported_heading_depth";
582
+ const lockError = lockLostError ?? heartbeatError;
583
+ if (lockError) {
584
+ throw lockError;
570
585
  }
571
- return parsed;
586
+ return result;
572
587
  }
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);
588
+ function ttlUntil(expiresAtMs, nowMs) {
589
+ return Math.max(1, expiresAtMs - nowMs);
584
590
  }
585
- function resolveSlackToken() {
586
- const ambientToken = installationTokenStorage.getStore()?.token;
587
- if (ambientToken) {
588
- return ambientToken;
591
+ async function readSubscriptionIdIndex(state, key) {
592
+ const value = await state.get(key);
593
+ if (value === void 0 || value === null) {
594
+ return [];
589
595
  }
590
- const envToken = getSlackBotToken();
591
- if (envToken) {
592
- return envToken;
596
+ return subscriptionIdIndexSchema.parse(value);
597
+ }
598
+ function buildSubscriptionId(input) {
599
+ const eventKey = [...new Set(input.events)].sort().join("\0");
600
+ const matchKey = stableResourceEventMatchKey(input.match);
601
+ const material = matchKey ? `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}\0${matchKey}` : `${input.namespace}\0${input.identifier}\0${input.conversationId}\0${eventKey}`;
602
+ return `resub_${digest(material)}`;
603
+ }
604
+ async function withIndexLock(state, key, callback) {
605
+ const lock = await state.acquireLock(indexLockKey(key), INDEX_LOCK_TTL_MS);
606
+ if (!lock) {
607
+ throw new Error(`Could not acquire resource event index lock for ${key}`);
593
608
  }
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
- );
609
+ try {
610
+ return await callback();
611
+ } finally {
612
+ await state.releaseLock(lock);
600
613
  }
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
614
  }
606
- function normalizeSlackConversationId(channelId) {
607
- return parseSlackChannelReferenceId(channelId);
615
+ async function addToIndex(state, key, subscriptionId, nowMs) {
616
+ await withIndexLock(state, key, async () => {
617
+ const ids = [...new Set(await readSubscriptionIdIndex(state, key))];
618
+ const next = ids.includes(subscriptionId) ? ids : [...ids, subscriptionId];
619
+ await state.set(key, next, await indexTtlMs(state, next, nowMs));
620
+ });
608
621
  }
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;
622
+ async function removeFromIndex(state, key, subscriptionId, nowMs) {
623
+ await withIndexLock(state, key, async () => {
624
+ const existing = await readSubscriptionIdIndex(state, key);
625
+ const next = existing.filter((id) => id !== subscriptionId);
626
+ await state.set(key, next, await indexTtlMs(state, next, nowMs));
627
+ });
620
628
  }
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);
629
+ async function indexTtlMs(state, subscriptionIds, nowMs) {
630
+ const records = await Promise.all(
631
+ subscriptionIds.map(
632
+ async (id) => parseSubscription(await state.get(subscriptionKey(id)))
633
+ )
634
+ );
635
+ const latestExpiresAtMs = Math.max(
636
+ nowMs,
637
+ ...records.filter(
638
+ (record) => record !== void 0 && activeAt(record, nowMs)
639
+ ).map((record) => record.expiresAtMs)
640
+ );
641
+ return ttlUntil(latestExpiresAtMs, nowMs);
682
642
  }
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) {
643
+ function parseSubscription(value) {
644
+ if (value === void 0 || value === null) {
698
645
  return void 0;
699
646
  }
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);
647
+ return subscriptionSchema.parse(value);
705
648
  }
706
- function hasSocketHangUpMessage(error) {
707
- return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
649
+ function activeAt(subscription, nowMs) {
650
+ return subscription.status === "active" && subscription.expiresAtMs > nowMs;
708
651
  }
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";
652
+ function matchesEvent(subscription, input) {
653
+ return subscription.namespace === input.namespace && subscription.identifier === input.identifier && subscription.events.includes(input.eventType) && activeAt(subscription, input.nowMs) && resourceEventMatches(subscription.match, input.data);
654
+ }
655
+ async function createResourceEventSubscription(input, options = {}) {
656
+ const state = options.state ?? getStateAdapter();
657
+ await state.connect();
658
+ const nowMs = options.nowMs ?? Date.now();
659
+ const events = [...new Set(input.events.map((event) => event.trim()))].filter(
660
+ Boolean
661
+ );
662
+ if (events.length === 0) {
663
+ throw new Error("Resource event subscription requires at least one event");
719
664
  }
720
- if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) {
721
- return "timeout";
665
+ if (input.expiresAtMs <= nowMs) {
666
+ throw new Error("Resource event subscription expiry must be in the future");
722
667
  }
723
- if (hasSocketHangUpMessage(raw)) {
724
- return "connection";
668
+ const match = input.match && Object.keys(input.match).length > 0 ? input.match : void 0;
669
+ const id = buildSubscriptionId({
670
+ conversationId: input.conversationId,
671
+ events,
672
+ match,
673
+ namespace: input.namespace,
674
+ identifier: input.identifier
675
+ });
676
+ const record = {
677
+ conversationId: input.conversationId,
678
+ createdAtMs: nowMs,
679
+ events,
680
+ expiresAtMs: input.expiresAtMs,
681
+ id,
682
+ intent: input.intent,
683
+ label: input.label,
684
+ ...match ? { match } : void 0,
685
+ namespace: input.namespace,
686
+ identifier: input.identifier,
687
+ resourceType: input.resourceType,
688
+ status: "active",
689
+ updatedAtMs: nowMs
690
+ };
691
+ const parsed = subscriptionSchema.parse(record);
692
+ await state.set(
693
+ subscriptionKey(id),
694
+ parsed,
695
+ ttlUntil(parsed.expiresAtMs, nowMs)
696
+ );
697
+ await addToIndex(
698
+ state,
699
+ resourceIndexKey(input.namespace, input.identifier),
700
+ id,
701
+ nowMs
702
+ );
703
+ await addToIndex(
704
+ state,
705
+ conversationIndexKey(input.conversationId),
706
+ id,
707
+ nowMs
708
+ );
709
+ return parsed;
710
+ }
711
+ async function listResourceEventSubscriptions(input) {
712
+ const state = input.state ?? getStateAdapter();
713
+ await state.connect();
714
+ const nowMs = input.nowMs ?? Date.now();
715
+ const ids = await readSubscriptionIdIndex(
716
+ state,
717
+ conversationIndexKey(input.conversationId)
718
+ );
719
+ const records = await Promise.all(
720
+ ids.map(
721
+ async (id) => parseSubscription(await state.get(subscriptionKey(id)))
722
+ )
723
+ );
724
+ return records.filter(
725
+ (record) => record !== void 0 && record.conversationId === input.conversationId && activeAt(record, nowMs)
726
+ ).sort((left, right) => left.createdAtMs - right.createdAtMs);
727
+ }
728
+ async function cancelResourceEventSubscription(input) {
729
+ const state = input.state ?? getStateAdapter();
730
+ await state.connect();
731
+ return await withSubscriptionLock(state, input.id, async () => {
732
+ const current = parseSubscription(
733
+ await state.get(subscriptionKey(input.id))
734
+ );
735
+ if (!current || current.conversationId !== input.conversationId) {
736
+ return void 0;
737
+ }
738
+ const nowMs = input.nowMs ?? Date.now();
739
+ const next = {
740
+ ...current,
741
+ status: "cancelled",
742
+ updatedAtMs: nowMs
743
+ };
744
+ await state.set(
745
+ subscriptionKey(input.id),
746
+ next,
747
+ JUNIOR_THREAD_STATE_TTL_MS
748
+ );
749
+ await removeFromIndex(
750
+ state,
751
+ resourceIndexKey(current.namespace, current.identifier),
752
+ input.id,
753
+ nowMs
754
+ );
755
+ await removeFromIndex(
756
+ state,
757
+ conversationIndexKey(current.conversationId),
758
+ input.id,
759
+ nowMs
760
+ );
761
+ return next;
762
+ });
763
+ }
764
+ async function cancelSubscriptions(input) {
765
+ const subscriptions = await listResourceEventSubscriptions(input);
766
+ for (const subscription of subscriptions) {
767
+ await cancelResourceEventSubscription({
768
+ conversationId: input.conversationId,
769
+ id: subscription.id,
770
+ nowMs: input.nowMs,
771
+ state: input.state
772
+ });
725
773
  }
726
- return "none";
727
774
  }
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;
775
+ async function collectResourceEventMatchKeys(input) {
776
+ const state = input.state ?? getStateAdapter();
777
+ await state.connect();
778
+ const nowMs = input.nowMs ?? Date.now();
779
+ const eventTypes = new Set(
780
+ input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean)
781
+ );
782
+ if (eventTypes.size === 0) return [];
783
+ const keys = /* @__PURE__ */ new Set();
784
+ const identifiers = new Set(
785
+ input.identifiers.map((value) => value.trim()).filter(Boolean)
786
+ );
787
+ for (const identifier of identifiers) {
788
+ const ids = await readSubscriptionIdIndex(
789
+ state,
790
+ resourceIndexKey(input.namespace, identifier)
791
+ );
792
+ const records = await Promise.all(
793
+ ids.map(
794
+ async (id) => parseSubscription(await state.get(subscriptionKey(id)))
795
+ )
796
+ );
797
+ for (const record of records) {
798
+ if (!record) continue;
799
+ if (record.namespace !== input.namespace) continue;
800
+ if (record.identifier !== identifier) continue;
801
+ if (!activeAt(record, nowMs)) continue;
802
+ if (!record.events.some((eventType) => eventTypes.has(eventType))) continue;
803
+ for (const key of Object.keys(record.match ?? {})) {
804
+ keys.add(key);
798
805
  }
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
806
  }
814
807
  }
815
- throw new SlackActionError(
816
- "Slack action exhausted retries",
817
- "internal_error"
818
- );
819
- }
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");
831
- }
832
- function isConversationChannel(channelId) {
833
- const normalized = normalizeSlackConversationId(channelId);
834
- if (!normalized) return false;
835
- return normalized.startsWith("C") || normalized.startsWith("G");
808
+ return [...keys].sort();
836
809
  }
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 }
848
- }
810
+ async function findMatchingResourceEventSubscriptions(input) {
811
+ const state = input.state ?? getStateAdapter();
812
+ await state.connect();
813
+ const nowMs = input.nowMs ?? Date.now();
814
+ const ids = await readSubscriptionIdIndex(
815
+ state,
816
+ resourceIndexKey(input.namespace, input.identifier)
817
+ );
818
+ const records = await Promise.all(
819
+ ids.map(
820
+ async (id) => parseSubscription(await state.get(subscriptionKey(id)))
821
+ )
822
+ );
823
+ return records.filter(
824
+ (record) => record !== void 0 && matchesEvent(record, {
825
+ data: input.data,
826
+ eventType: input.eventType,
827
+ nowMs,
828
+ namespace: input.namespace,
829
+ identifier: input.identifier
830
+ })
849
831
  );
850
- return response.file?.permalink;
851
832
  }
852
- async function downloadPrivateSlackFile(url) {
853
- const token = resolveSlackToken();
854
- return withSpan(
855
- "GET files.slack.com",
856
- "http.client",
857
- {},
833
+ async function deliverResourceEventSubscription(input) {
834
+ const state = input.state ?? getStateAdapter();
835
+ await state.connect();
836
+ return await withSubscriptionLock(
837
+ state,
838
+ input.subscription.id,
858
839
  async () => {
859
- const response = await fetch(url, {
860
- headers: {
861
- Authorization: `Bearer ${token}`
840
+ const nowMs = input.nowMs ?? Date.now();
841
+ const current = parseSubscription(
842
+ await state.get(subscriptionKey(input.subscription.id))
843
+ );
844
+ if (!current || !matchesEvent(current, {
845
+ data: input.data,
846
+ eventType: input.eventType,
847
+ nowMs,
848
+ namespace: input.namespace,
849
+ identifier: input.identifier
850
+ })) {
851
+ return false;
852
+ }
853
+ const delivered = await input.deliver(current);
854
+ if (input.terminal) {
855
+ const latest = parseSubscription(
856
+ await state.get(subscriptionKey(current.id))
857
+ );
858
+ if (!latest || latest.status !== current.status || latest.updatedAtMs !== current.updatedAtMs) {
859
+ return delivered;
862
860
  }
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}`);
861
+ const next = {
862
+ ...current,
863
+ status: "completed",
864
+ updatedAtMs: nowMs
865
+ };
866
+ await state.set(
867
+ subscriptionKey(current.id),
868
+ next,
869
+ JUNIOR_THREAD_STATE_TTL_MS
870
+ );
871
+ await removeFromIndex(
872
+ state,
873
+ resourceIndexKey(current.namespace, current.identifier),
874
+ current.id,
875
+ nowMs
876
+ );
877
+ await removeFromIndex(
878
+ state,
879
+ conversationIndexKey(current.conversationId),
880
+ current.id,
881
+ nowMs
882
+ );
869
883
  }
870
- return Buffer.from(await response.arrayBuffer());
884
+ return delivered;
871
885
  },
872
- {
873
- "http.request.method": "GET",
874
- "server.address": "files.slack.com",
875
- "url.scheme": "https",
876
- "app.slack.method": "files.download"
877
- }
886
+ input.waitDeadlineMs
878
887
  );
879
888
  }
880
889
 
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;
900
- };
901
- }
890
+ // src/chat/resource-events/tool-support.ts
891
+ var RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
892
+ var RESOURCE_SUBSCRIPTION_MAX_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
893
+ var STOP_WATCHING_TOOL_NAME = "stopWatchingResources";
894
+ var RESOURCE_WATCH_TOOL_SOURCE = {
895
+ id: "resource-watches",
896
+ description: "Inspect or stop resource watches for the current conversation."
897
+ };
902
898
 
903
- // src/chat/resource-events/store.ts
904
- import { createHash as createHash2 } from "crypto";
905
- import {
906
- destinationSchema,
907
- resourceEventMatchSchema,
908
- resourceEventMatches,
909
- resourceEventTypeSchema,
910
- stableResourceEventMatchKey
911
- } 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);
939
- }
940
- function subscriptionKey(id) {
941
- return `${RESOURCE_EVENT_PREFIX}:record:${id}`;
942
- }
943
- function subscriptionLockKey(id) {
944
- return `${RESOURCE_EVENT_PREFIX}:lock:${id}`;
945
- }
946
- function resourceIndexKey(teamId, namespace, identifier) {
947
- return `${RESOURCE_EVENT_PREFIX}:resource:${digest(`${teamId}\0${namespace}\0${identifier}`)}`;
948
- }
949
- function subscriptionTeamId(destination) {
950
- if (destination.platform !== "slack") {
951
- throw new Error("Resource event subscriptions require a Slack destination");
899
+ // src/chat/slack/tool-support/context.ts
900
+ function getSlackToolContext(context) {
901
+ if (context.source.platform !== "slack") {
902
+ return void 0;
952
903
  }
953
- return destination.teamId;
954
- }
955
- function conversationIndexKey(conversationId) {
956
- return `${RESOURCE_EVENT_PREFIX}:conversation:${digest(conversationId)}`;
957
- }
958
- function indexLockKey(key) {
959
- return `${key}:lock`;
960
- }
961
- async function sleep2(ms) {
962
- await new Promise((resolve) => {
963
- const timer = setTimeout(resolve, ms);
964
- timer.unref?.();
965
- });
966
- }
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);
904
+ if (context.destination.platform !== "slack") {
905
+ throw new TypeError("Slack source requires a Slack destination");
982
906
  }
983
- }
984
- async function withSubscriptionLock(state, subscriptionId, callback, waitDeadlineMs) {
985
- const lock = await acquireSubscriptionLock(
986
- state,
987
- subscriptionId,
988
- waitDeadlineMs
907
+ const destinationChannelId = parseSlackChannelReferenceId(
908
+ context.destination.channelId
989
909
  );
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;
910
+ const sourceChannelId = parseSlackChannelReferenceId(
911
+ context.source.channelId
912
+ );
913
+ const teamId = parseSlackTeamId(context.source.teamId);
914
+ if (!destinationChannelId || !sourceChannelId || !teamId) {
915
+ return void 0;
1023
916
  }
1024
- return result;
917
+ return {
918
+ destination: context.destination,
919
+ source: context.source,
920
+ actor: context.actor?.platform === "slack" ? context.actor : void 0,
921
+ destinationChannelId,
922
+ messageTs: parseSlackMessageTs(context.source.messageTs),
923
+ sourceChannelId,
924
+ teamId,
925
+ threadTs: parseSlackMessageTs(context.source.threadTs)
926
+ };
1025
927
  }
1026
- function ttlUntil(expiresAtMs, nowMs) {
1027
- return Math.max(1, expiresAtMs - nowMs);
928
+
929
+ // src/chat/plugins/viewer.ts
930
+ import { randomUUID } from "crypto";
931
+ import { and as and3, asc, eq as eq3 } from "drizzle-orm";
932
+ import {
933
+ identitySchema,
934
+ userSchema
935
+ } from "@sentry/junior-plugin-api";
936
+ function identityFromRow(row) {
937
+ return identitySchema.parse({
938
+ id: row.id,
939
+ provider: row.provider,
940
+ providerSubjectId: row.providerSubjectId,
941
+ ...row.providerTenantId ? { providerTenantId: row.providerTenantId } : void 0,
942
+ ...row.displayName ? { displayName: row.displayName } : void 0,
943
+ ...row.handle ? { handle: row.handle } : void 0
944
+ });
1028
945
  }
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);
946
+ async function readUserById(db, userRow) {
947
+ const identityRows = await db.select().from(juniorIdentities).where(
948
+ and3(
949
+ eq3(juniorIdentities.userId, userRow.id),
950
+ eq3(juniorIdentities.kind, "user")
951
+ )
952
+ ).orderBy(
953
+ asc(juniorIdentities.provider),
954
+ asc(juniorIdentities.providerTenantId),
955
+ asc(juniorIdentities.providerSubjectId)
956
+ );
957
+ return userSchema.parse({
958
+ email: userRow.primaryEmail,
959
+ id: userRow.id,
960
+ identities: identityRows.map(identityFromRow),
961
+ ...userRow.displayName ? { displayName: userRow.displayName } : void 0
962
+ });
1035
963
  }
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)}`;
964
+ async function resolveViewerUserFromSql(db, email) {
965
+ const normalizedEmail = normalizeIdentityEmail(email);
966
+ if (!normalizedEmail) return void 0;
967
+ const now = /* @__PURE__ */ new Date();
968
+ const rows = await db.insert(juniorUsers).values({
969
+ id: randomUUID(),
970
+ primaryEmail: email.trim(),
971
+ primaryEmailNormalized: normalizedEmail,
972
+ displayName: null,
973
+ createdAt: now,
974
+ updatedAt: now
975
+ }).onConflictDoNothing({ target: juniorUsers.primaryEmailNormalized }).returning();
976
+ const userRow = rows[0] ?? (await db.select().from(juniorUsers).where(eq3(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
977
+ return userRow ? await readUserById(db, userRow) : void 0;
1041
978
  }
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}`);
1046
- }
1047
- try {
1048
- return await callback();
1049
- } finally {
1050
- await state.releaseLock(lock);
1051
- }
979
+ async function resolveViewerUser(email) {
980
+ return await resolveViewerUserFromSql(getDb(), email);
1052
981
  }
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
- });
982
+ async function findUserByEmailFromSql(db, email) {
983
+ const normalizedEmail = normalizeIdentityEmail(email);
984
+ if (!normalizedEmail) return void 0;
985
+ const userRow = (await db.select().from(juniorUsers).where(eq3(juniorUsers.primaryEmailNormalized, normalizedEmail)).limit(1))[0];
986
+ return userRow ? await readUserById(db, userRow) : void 0;
1059
987
  }
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
- });
988
+ async function findUserByEmail(email) {
989
+ return await findUserByEmailFromSql(getDb(), email);
1066
990
  }
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)))
1071
- )
1072
- );
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);
991
+ async function updateViewerDisplayNameFromSql(db, userId, displayName) {
992
+ const rows = await db.update(juniorUsers).set({ displayName, updatedAt: /* @__PURE__ */ new Date() }).where(eq3(juniorUsers.id, userId)).returning();
993
+ const userRow = rows[0];
994
+ return userRow ? await readUserById(db, userRow) : void 0;
1080
995
  }
1081
- function parseSubscription(value) {
1082
- if (value === void 0 || value === null) {
1083
- return void 0;
1084
- }
1085
- return subscriptionSchema.parse(value);
996
+ async function updateViewerDisplayName(userId, displayName) {
997
+ return await updateViewerDisplayNameFromSql(getDb(), userId, displayName);
1086
998
  }
1087
- function activeAt(subscription, nowMs) {
1088
- return subscription.status === "active" && subscription.expiresAtMs > nowMs;
999
+ async function readActorIdentityFromSql(db, actor) {
1000
+ if (actor.platform === "system") return void 0;
1001
+ const providerTenantId = actor.platform === "slack" ? actor.teamId : "";
1002
+ const rows = await db.select({
1003
+ identity: juniorIdentities,
1004
+ user: juniorUsers
1005
+ }).from(juniorIdentities).leftJoin(juniorUsers, eq3(juniorUsers.id, juniorIdentities.userId)).where(
1006
+ and3(
1007
+ eq3(juniorIdentities.kind, "user"),
1008
+ eq3(juniorIdentities.provider, actor.platform),
1009
+ eq3(juniorIdentities.providerTenantId, providerTenantId),
1010
+ eq3(juniorIdentities.providerSubjectId, actor.userId)
1011
+ )
1012
+ ).limit(1);
1013
+ const row = rows[0];
1014
+ if (!row) return void 0;
1015
+ return {
1016
+ identity: identityFromRow(row.identity),
1017
+ ...row.user ? { user: await readUserById(db, row.user) } : void 0
1018
+ };
1089
1019
  }
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);
1020
+ async function readActorIdentity(actor) {
1021
+ return await readActorIdentityFromSql(getDb(), actor);
1092
1022
  }
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");
1023
+
1024
+ // src/chat/credentials/subject.ts
1025
+ import { createHmac, timingSafeEqual } from "crypto";
1026
+
1027
+ // src/chat/slack/client.ts
1028
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
1029
+ import { WebClient } from "@slack/web-api";
1030
+
1031
+ // src/chat/slack/workspace-context.ts
1032
+ import { AsyncLocalStorage } from "async_hooks";
1033
+ var workspaceTeamIdStorage = new AsyncLocalStorage();
1034
+ function runWithWorkspaceTeamId(teamId, fn) {
1035
+ if (!teamId) return fn();
1036
+ return workspaceTeamIdStorage.run(teamId, fn);
1037
+ }
1038
+ function getWorkspaceTeamId() {
1039
+ return workspaceTeamIdStorage.getStore();
1040
+ }
1041
+
1042
+ // src/chat/slack/client.ts
1043
+ var SlackActionError = class extends Error {
1044
+ code;
1045
+ apiError;
1046
+ needed;
1047
+ provided;
1048
+ statusCode;
1049
+ requestId;
1050
+ errorData;
1051
+ retryAfterSeconds;
1052
+ detail;
1053
+ detailLine;
1054
+ detailRule;
1055
+ constructor(message, code, options = {}) {
1056
+ super(message);
1057
+ this.name = "SlackActionError";
1058
+ this.code = code;
1059
+ this.apiError = options.apiError;
1060
+ this.needed = options.needed;
1061
+ this.provided = options.provided;
1062
+ this.statusCode = options.statusCode;
1063
+ this.requestId = options.requestId;
1064
+ this.errorData = options.errorData;
1065
+ this.retryAfterSeconds = options.retryAfterSeconds;
1066
+ this.detail = options.detail;
1067
+ this.detailLine = options.detailLine;
1068
+ this.detailRule = options.detailRule;
1102
1069
  }
1103
- if (input.expiresAtMs <= nowMs) {
1104
- throw new Error("Resource event subscription expiry must be in the future");
1070
+ };
1071
+ function serializeSlackErrorData(data) {
1072
+ if (!data || typeof data !== "object") {
1073
+ return void 0;
1105
1074
  }
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
1130
- };
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
- );
1153
- return parsed;
1154
- }
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)))
1075
+ const filtered = Object.fromEntries(
1076
+ Object.entries(data).filter(
1077
+ ([key]) => key !== "error"
1166
1078
  )
1167
1079
  );
1168
- return records.filter(
1169
- (record) => record !== void 0 && record.conversationId === input.conversationId && activeAt(record, nowMs)
1170
- ).sort((left, right) => left.createdAtMs - right.createdAtMs);
1080
+ if (Object.keys(filtered).length === 0) {
1081
+ return void 0;
1082
+ }
1083
+ try {
1084
+ const serialized = JSON.stringify(filtered);
1085
+ return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
1086
+ } catch {
1087
+ return void 0;
1088
+ }
1171
1089
  }
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;
1090
+ function getHeaderString(headers, name) {
1091
+ if (!headers || typeof headers !== "object") {
1092
+ return void 0;
1093
+ }
1094
+ const key = name.toLowerCase();
1095
+ const entries = headers;
1096
+ for (const [entryKey, value] of Object.entries(entries)) {
1097
+ if (entryKey.toLowerCase() !== key) continue;
1098
+ if (typeof value === "string") return value;
1099
+ if (Array.isArray(value)) {
1100
+ const first = value.find((entry) => typeof entry === "string");
1101
+ return typeof first === "string" ? first : void 0;
1181
1102
  }
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
1103
+ }
1104
+ return void 0;
1105
+ }
1106
+ function parseSlackCanvasDetail(detail) {
1107
+ if (typeof detail !== "string") {
1108
+ return {};
1109
+ }
1110
+ const trimmed = detail.trim();
1111
+ if (!trimmed) {
1112
+ return {};
1113
+ }
1114
+ const parsed = {
1115
+ detail: trimmed
1116
+ };
1117
+ const lineMatch = trimmed.match(/line\s+(\d+):/i);
1118
+ if (lineMatch) {
1119
+ const line = Number.parseInt(lineMatch[1] ?? "", 10);
1120
+ if (Number.isFinite(line)) {
1121
+ parsed.detailLine = line;
1122
+ }
1123
+ }
1124
+ if (/unsupported heading depth/i.test(trimmed)) {
1125
+ parsed.detailRule = "unsupported_heading_depth";
1126
+ }
1127
+ return parsed;
1128
+ }
1129
+ var installationTokenStorage = new AsyncLocalStorage2();
1130
+ var clientsByToken = /* @__PURE__ */ new Map();
1131
+ function runWithSlackInstallationToken(token, fn) {
1132
+ const trimmed = token.trim();
1133
+ if (!trimmed) {
1134
+ throw new SlackActionError(
1135
+ "Slack installation token binding requires a non-empty token",
1136
+ "missing_token"
1202
1137
  );
1203
- await removeFromIndex(
1204
- state,
1205
- conversationIndexKey(current.conversationId),
1206
- input.id,
1207
- nowMs
1138
+ }
1139
+ return installationTokenStorage.run({ token: trimmed }, fn);
1140
+ }
1141
+ function resolveSlackToken() {
1142
+ const ambientToken = installationTokenStorage.getStore()?.token;
1143
+ if (ambientToken) {
1144
+ return ambientToken;
1145
+ }
1146
+ const envToken = getSlackBotToken();
1147
+ if (envToken) {
1148
+ return envToken;
1149
+ }
1150
+ const teamId = getWorkspaceTeamId();
1151
+ if (teamId) {
1152
+ throw new SlackActionError(
1153
+ `Slack call is scoped to workspace ${teamId} but no installation token is bound and no default bot token is configured`,
1154
+ "missing_token"
1208
1155
  );
1209
- return next;
1210
- });
1156
+ }
1157
+ throw new SlackActionError(
1158
+ "SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service",
1159
+ "missing_token"
1160
+ );
1211
1161
  }
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
1162
+ function normalizeSlackConversationId(channelId) {
1163
+ return parseSlackChannelReferenceId(channelId);
1164
+ }
1165
+ function getClient() {
1166
+ const token = resolveSlackToken();
1167
+ let cached = clientsByToken.get(token);
1168
+ if (!cached) {
1169
+ cached = new WebClient(token, {
1170
+ retryConfig: { retries: 0 },
1171
+ rejectRateLimitedCalls: true
1220
1172
  });
1173
+ clientsByToken.set(token, cached);
1221
1174
  }
1175
+ return cached;
1222
1176
  }
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
- }
1177
+ function mapSlackError(error) {
1178
+ if (error instanceof SlackActionError) {
1179
+ return error;
1256
1180
  }
1257
- return [...keys].sort();
1181
+ const candidate = error;
1182
+ const apiError = candidate.data?.error;
1183
+ const message = candidate.message ?? "Slack action failed";
1184
+ const baseOptions = {
1185
+ apiError,
1186
+ statusCode: candidate.statusCode,
1187
+ requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
1188
+ errorData: serializeSlackErrorData(candidate.data),
1189
+ ...parseSlackCanvasDetail(candidate.data?.detail)
1190
+ };
1191
+ if (apiError === "missing_scope") {
1192
+ return new SlackActionError(message, "missing_scope", {
1193
+ ...baseOptions,
1194
+ needed: candidate.data?.needed,
1195
+ provided: candidate.data?.provided
1196
+ });
1197
+ }
1198
+ if (apiError === "not_in_channel") {
1199
+ return new SlackActionError(message, "not_in_channel", baseOptions);
1200
+ }
1201
+ if (apiError === "restricted_action_read_only_channel") {
1202
+ return new SlackActionError(message, "read_only_channel", baseOptions);
1203
+ }
1204
+ if (apiError === "already_reacted") {
1205
+ return new SlackActionError(message, "already_reacted", baseOptions);
1206
+ }
1207
+ if (apiError === "no_reaction") {
1208
+ return new SlackActionError(message, "no_reaction", baseOptions);
1209
+ }
1210
+ if (apiError === "invalid_arguments") {
1211
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
1212
+ }
1213
+ if (apiError === "invalid_cursor") {
1214
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
1215
+ }
1216
+ if (apiError === "invalid_name") {
1217
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
1218
+ }
1219
+ if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") {
1220
+ return new SlackActionError(message, "not_found", baseOptions);
1221
+ }
1222
+ if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") {
1223
+ return new SlackActionError(message, "feature_unavailable", baseOptions);
1224
+ }
1225
+ if (apiError === "canvas_creation_failed") {
1226
+ return new SlackActionError(message, "canvas_creation_failed", baseOptions);
1227
+ }
1228
+ if (apiError === "canvas_editing_failed") {
1229
+ return new SlackActionError(message, "canvas_editing_failed", baseOptions);
1230
+ }
1231
+ if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) {
1232
+ return new SlackActionError(message, "rate_limited", {
1233
+ ...baseOptions,
1234
+ retryAfterSeconds: candidate.retryAfter
1235
+ });
1236
+ }
1237
+ return new SlackActionError(message, "internal_error", baseOptions);
1258
1238
  }
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
- );
1239
+ var MAX_RETRY_DELAY_MS = 1e4;
1240
+ var MAX_TOTAL_RETRY_DELAY_MS = 2e4;
1241
+ var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
1242
+ "ECONNREFUSED",
1243
+ "ECONNRESET",
1244
+ "ENOTFOUND",
1245
+ "EAI_AGAIN"
1246
+ ]);
1247
+ var TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
1248
+ "ETIMEDOUT",
1249
+ "ECONNABORTED",
1250
+ "ESOCKETTIMEDOUT"
1251
+ ]);
1252
+ function findNetworkErrorCode(error, depth = 0) {
1253
+ if (!error || typeof error !== "object" || depth > 4) {
1254
+ return void 0;
1255
+ }
1256
+ const candidate = error;
1257
+ if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) {
1258
+ return candidate.code;
1259
+ }
1260
+ return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
1282
1261
  }
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;
1262
+ function hasSocketHangUpMessage(error) {
1263
+ return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
1264
+ }
1265
+ function classifySlackRetry(raw, mapped) {
1266
+ if (mapped.code === "rate_limited") {
1267
+ return "rate_limited";
1268
+ }
1269
+ if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) {
1270
+ return "server_error";
1271
+ }
1272
+ const networkCode = findNetworkErrorCode(raw);
1273
+ if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) {
1274
+ return "connection";
1275
+ }
1276
+ if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) {
1277
+ return "timeout";
1278
+ }
1279
+ if (hasSocketHangUpMessage(raw)) {
1280
+ return "connection";
1281
+ }
1282
+ return "none";
1283
+ }
1284
+ async function withSlackRetries(task, maxAttempts = 3, context = {}) {
1285
+ let attempt = 0;
1286
+ let totalDelayMs = 0;
1287
+ const action = context.action ?? "unknown";
1288
+ while (attempt < maxAttempts) {
1289
+ attempt += 1;
1290
+ const attemptNumber = attempt;
1291
+ try {
1292
+ return await withSpan(
1293
+ `POST slack.com/api/${action}`,
1294
+ "http.client",
1295
+ {},
1296
+ async () => {
1297
+ try {
1298
+ return await task();
1299
+ } catch (error) {
1300
+ const mapped = mapSlackError(error);
1301
+ const errorAttrs = {
1302
+ "error.type": mapped.code
1303
+ };
1304
+ if (mapped.apiError) {
1305
+ errorAttrs["app.slack.api_error_code"] = mapped.apiError;
1306
+ }
1307
+ if (mapped.code === "rate_limited") {
1308
+ errorAttrs["http.response.status_code"] = 429;
1309
+ if (mapped.retryAfterSeconds) {
1310
+ errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
1311
+ }
1312
+ } else if (mapped.statusCode != null) {
1313
+ errorAttrs["http.response.status_code"] = mapped.statusCode;
1314
+ }
1315
+ setSpanAttributes(errorAttrs);
1316
+ setSpanStatus("error");
1317
+ throw error;
1318
+ }
1319
+ },
1320
+ {
1321
+ "http.request.method": "POST",
1322
+ "server.address": "slack.com",
1323
+ "url.scheme": "https",
1324
+ "url.path": `/api/${action}`,
1325
+ "app.slack.method": action,
1326
+ "app.retry.max_attempts": maxAttempts,
1327
+ ...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
1328
+ ...context.attributes ?? {},
1329
+ ...context.spanAttributes ?? {}
1311
1330
  }
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
- );
1331
+ );
1332
+ } catch (error) {
1333
+ const mapped = mapSlackError(error);
1334
+ const retryClass = classifySlackRetry(error, mapped);
1335
+ const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
1336
+ const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
1337
+ const baseLogAttributes = {
1338
+ "app.slack.action": action,
1339
+ "app.slack.error_code": mapped.code,
1340
+ ...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
1341
+ ...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
1342
+ ...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
1343
+ ...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
1344
+ ...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
1345
+ ...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
1346
+ ...context.attributes ?? {}
1347
+ };
1348
+ if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
1349
+ logWarn("slack.action.failed", {
1350
+ ...baseLogAttributes,
1351
+ ...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
1352
+ });
1353
+ throw mapped;
1338
1354
  }
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;
1355
+ logWarn("slack.action.retrying", {
1356
+ ...baseLogAttributes,
1357
+ "app.slack.retry_attempt": attempt,
1358
+ "app.slack.retry_class": retryClass
1359
+ });
1360
+ const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
1361
+ const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
1362
+ const delayMs = Math.min(
1363
+ retryAfterMs ?? backoffMs,
1364
+ MAX_RETRY_DELAY_MS,
1365
+ remainingDelayBudgetMs
1366
+ );
1367
+ totalDelayMs += delayMs;
1368
+ await sleep(delayMs);
1369
+ }
1406
1370
  }
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)
1371
+ throw new SlackActionError(
1372
+ "Slack action exhausted retries",
1373
+ "internal_error"
1446
1374
  );
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
- }
1469
- async function resolveViewerUser(email) {
1470
- return await resolveViewerUserFromSql(getDb(), email);
1471
1375
  }
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;
1376
+ function getSlackClient() {
1377
+ return getClient();
1477
1378
  }
1478
- async function findUserByEmail(email) {
1479
- return await findUserByEmailFromSql(getDb(), email);
1379
+ function isDmChannel(channelId) {
1380
+ const normalized = normalizeSlackConversationId(channelId);
1381
+ return Boolean(normalized && normalized.startsWith("D"));
1480
1382
  }
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;
1383
+ function isConversationScopedChannel(channelId) {
1384
+ const normalized = normalizeSlackConversationId(channelId);
1385
+ if (!normalized) return false;
1386
+ return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
1485
1387
  }
1486
- async function updateViewerDisplayName(userId, displayName) {
1487
- return await updateViewerDisplayNameFromSql(getDb(), userId, displayName);
1388
+ function isConversationChannel(channelId) {
1389
+ const normalized = normalizeSlackConversationId(channelId);
1390
+ if (!normalized) return false;
1391
+ return normalized.startsWith("C") || normalized.startsWith("G");
1488
1392
  }
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
- };
1393
+ async function getFilePermalink(fileId) {
1394
+ const client = getClient();
1395
+ const response = await withSlackRetries(
1396
+ () => client.files.info({
1397
+ file: fileId
1398
+ }),
1399
+ 3,
1400
+ {
1401
+ action: "files.info",
1402
+ idempotent: true,
1403
+ spanAttributes: { "app.slack.file_id": fileId }
1404
+ }
1405
+ );
1406
+ return response.file?.permalink;
1509
1407
  }
1510
- async function readActorIdentity(actor) {
1511
- return await readActorIdentityFromSql(getDb(), actor);
1408
+ async function downloadPrivateSlackFile(url) {
1409
+ const token = resolveSlackToken();
1410
+ return withSpan(
1411
+ "GET files.slack.com",
1412
+ "http.client",
1413
+ {},
1414
+ async () => {
1415
+ const response = await fetch(url, {
1416
+ headers: {
1417
+ Authorization: `Bearer ${token}`
1418
+ }
1419
+ });
1420
+ setSpanAttributes({ "http.response.status_code": response.status });
1421
+ if (!response.ok) {
1422
+ setSpanAttributes({ "error.type": String(response.status) });
1423
+ setSpanStatus("error");
1424
+ throw new Error(`Slack file download failed: ${response.status}`);
1425
+ }
1426
+ return Buffer.from(await response.arrayBuffer());
1427
+ },
1428
+ {
1429
+ "http.request.method": "GET",
1430
+ "server.address": "files.slack.com",
1431
+ "url.scheme": "https",
1432
+ "app.slack.method": "files.download"
1433
+ }
1434
+ );
1512
1435
  }
1513
1436
 
1514
1437
  // src/chat/credentials/subject.ts
1515
- import { createHmac, timingSafeEqual } from "crypto";
1516
1438
  var CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
1517
1439
  var CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
1518
1440
  function getCredentialSubjectSecret() {
@@ -2726,7 +2648,9 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
2726
2648
  teamId: slackToolContext.teamId,
2727
2649
  userId: slackToolContext.actor?.userId
2728
2650
  }) : void 0;
2729
- const dashboardConversationUrl = context.conversationId ? getDashboardConversationLink(context.conversationId) : void 0;
2651
+ const dashboardConversationUrl = getDashboardConversationLink(
2652
+ context.conversationId
2653
+ );
2730
2654
  const slackContext = slackToolContext ? {
2731
2655
  channelCapabilities: resolveChannelCapabilities(
2732
2656
  slackToolContext.sourceChannelId
@@ -2734,17 +2658,17 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
2734
2658
  ...dashboardConversationUrl ? { conversationLink: { url: dashboardConversationUrl } } : void 0,
2735
2659
  ...credentialSubject ? { credentialSubject } : void 0
2736
2660
  } : void 0;
2737
- const annotations = context.conversationId ? createPluginAnnotations({
2661
+ const annotations = createPluginAnnotations({
2738
2662
  conversationId: context.conversationId,
2739
2663
  db: getDb(),
2740
2664
  plugin: pluginName
2741
- }) : void 0;
2665
+ });
2742
2666
  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;
2667
+ const canSubscribe = Boolean(plugin.resourceEvents) && plugin.resourceEvents?.isEnabled?.() !== false;
2744
2668
  const resourceEvents = {
2745
2669
  canSubscribe,
2746
2670
  async subscribe(input) {
2747
- if (!canSubscribe || context.destination.platform !== "slack") {
2671
+ if (!canSubscribe) {
2748
2672
  throw new Error(
2749
2673
  "Resource subscriptions are not available in this conversation."
2750
2674
  );
@@ -2762,7 +2686,6 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
2762
2686
  }
2763
2687
  const subscription = await createResourceEventSubscription({
2764
2688
  conversationId: context.conversationId,
2765
- destination: context.destination,
2766
2689
  events: input.events,
2767
2690
  expiresAtMs: Date.now() + RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS,
2768
2691
  intent: input.intent,
@@ -2783,7 +2706,7 @@ function getPluginTools(context, sandbox = createSandboxCapability(context.works
2783
2706
  const resolveActor = context.resolveActorIdentity ?? (async () => void 0);
2784
2707
  const common = {
2785
2708
  ...basePluginContext(plugin),
2786
- ...annotations ? { annotations } : void 0,
2709
+ annotations,
2787
2710
  conversationId: context.conversationId,
2788
2711
  locationId: context.locationId,
2789
2712
  userText: context.userText,
@@ -3575,7 +3498,6 @@ function createPluginHookRunner(input = {}) {
3575
3498
  }
3576
3499
 
3577
3500
  export {
3578
- ToolInputError,
3579
3501
  listConversationAnnotations,
3580
3502
  listConversationAnnotationsById,
3581
3503
  createPluginConversationEvents,
@@ -3587,20 +3509,6 @@ export {
3587
3509
  setDashboardConversationLinkOptions,
3588
3510
  getDashboardConversationLink,
3589
3511
  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
3512
  createResourceEventSubscription,
3605
3513
  listResourceEventSubscriptions,
3606
3514
  cancelResourceEventSubscription,
@@ -3612,13 +3520,23 @@ export {
3612
3520
  RESOURCE_SUBSCRIPTION_MAX_TTL_MS,
3613
3521
  STOP_WATCHING_TOOL_NAME,
3614
3522
  RESOURCE_WATCH_TOOL_SOURCE,
3615
- requireResourceWatchConversation,
3616
- canUseResourceEventSubscriptionTools,
3617
3523
  getSlackToolContext,
3618
3524
  resolveViewerUser,
3619
3525
  findUserByEmail,
3620
3526
  updateViewerDisplayName,
3621
3527
  readActorIdentity,
3528
+ runWithWorkspaceTeamId,
3529
+ getWorkspaceTeamId,
3530
+ SlackActionError,
3531
+ getHeaderString,
3532
+ runWithSlackInstallationToken,
3533
+ normalizeSlackConversationId,
3534
+ withSlackRetries,
3535
+ getSlackClient,
3536
+ isDmChannel,
3537
+ isConversationScopedChannel,
3538
+ getFilePermalink,
3539
+ downloadPrivateSlackFile,
3622
3540
  bindSlackDirectCredentialSubject,
3623
3541
  verifySlackDirectCredentialSubject,
3624
3542
  bindScheduledTaskCredentialSubject,