@automagik/omni 2.260922.13 → 2.260922.15

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.
@@ -136088,8 +136088,12 @@ var init_types10 = __esm(() => {
136088
136088
 
136089
136089
  // ../channel-slack/src/connection/bolt-client.ts
136090
136090
  function buildActingClients(options, botClient) {
136091
- if (options.authMode !== "user")
136091
+ if (options.authMode !== "user") {
136092
+ if (!botClient) {
136093
+ throw new SlackError(SlackErrorCode.INVALID_TOKEN, "botToken (xoxb-...) is required unless authMode is 'user'");
136094
+ }
136092
136095
  return { actingClient: botClient };
136096
+ }
136093
136097
  if (!options.userToken) {
136094
136098
  throw new SlackError(SlackErrorCode.CONNECTION_FAILED, "userToken is required when authMode is 'user'");
136095
136099
  }
@@ -136276,6 +136280,9 @@ var init_bolt_client = __esm(() => {
136276
136280
 
136277
136281
  // ../channel-slack/src/connection/app-receiver.ts
136278
136282
  import { createHash as createHash6 } from "crypto";
136283
+ function hasBotToken(attachment) {
136284
+ return Boolean(attachment.botToken);
136285
+ }
136279
136286
  function receiverKeyFor(opts) {
136280
136287
  const mode = opts.mode ?? "socket";
136281
136288
  if (mode === "http") {
@@ -136425,8 +136432,7 @@ class SlackAppReceiver {
136425
136432
  }
136426
136433
  this.attachmentMap.delete(attachment.instanceId);
136427
136434
  this.attachmentMap.set(attachment.instanceId, attachment);
136428
- const newest = this.latestAttachmentFor(attachment.teamId) ?? attachment;
136429
- this.botClients.set(newest.teamId, this.clientForBotToken(newest.botToken));
136435
+ this.refreshBotClient(attachment.teamId);
136430
136436
  this.logger.debug("Instance attached to Slack receiver", {
136431
136437
  receiver: this.key,
136432
136438
  instanceId: attachment.instanceId,
@@ -136439,21 +136445,13 @@ class SlackAppReceiver {
136439
136445
  if (!removed)
136440
136446
  return false;
136441
136447
  this.attachmentMap.delete(instanceId);
136442
- const remainingForTeam = this.latestAttachmentFor(removed.teamId);
136443
- if (remainingForTeam) {
136444
- this.botClients.set(remainingForTeam.teamId, this.clientForBotToken(remainingForTeam.botToken));
136445
- } else {
136446
- this.botClients.delete(removed.teamId);
136447
- }
136448
- let tokenStillUsed = false;
136449
- for (const attachment of this.attachmentMap.values()) {
136450
- if (attachment.botToken === removed.botToken) {
136451
- tokenStillUsed = true;
136452
- break;
136453
- }
136448
+ this.refreshBotClient(removed.teamId);
136449
+ if (hasBotToken(removed)) {
136450
+ const token = removed.botToken;
136451
+ const tokenStillUsed = [...this.attachmentMap.values()].some((attachment) => attachment.botToken === token);
136452
+ if (!tokenStillUsed)
136453
+ this.clientsByToken.delete(token);
136454
136454
  }
136455
- if (!tokenStillUsed)
136456
- this.clientsByToken.delete(removed.botToken);
136457
136455
  this.logger.debug("Instance detached from Slack receiver", {
136458
136456
  receiver: this.key,
136459
136457
  instanceId,
@@ -136471,16 +136469,25 @@ class SlackAppReceiver {
136471
136469
  return [];
136472
136470
  const teamAttachments = this.teamAttachmentsFor(teamId);
136473
136471
  if (teamAttachments.length <= 1)
136474
- return teamAttachments;
136472
+ return this.soleTargets(teamAttachments, body);
136475
136473
  this.announceNarrowing(teamId, teamAttachments.length);
136476
136474
  return selectAuthorizedTargets(teamAttachments, await this.authorizationsFor(body));
136477
136475
  }
136476
+ async soleTargets(teamAttachments, body) {
136477
+ const [only] = teamAttachments;
136478
+ if (!only || hasBotToken(only))
136479
+ return teamAttachments;
136480
+ if (selectAuthorizedTargets(teamAttachments, body.authorizations ?? []).length > 0)
136481
+ return teamAttachments;
136482
+ return selectAuthorizedTargets(teamAttachments, await this.authorizationsFor(body));
136483
+ }
136478
136484
  async targetsForActor(teamId, actorUserId) {
136479
136485
  if (!teamId)
136480
136486
  return [];
136481
136487
  const teamAttachments = this.teamAttachmentsFor(teamId);
136482
- if (teamAttachments.length <= 1)
136483
- return teamAttachments;
136488
+ if (teamAttachments.length <= 1) {
136489
+ return teamAttachments.filter((attachment) => hasBotToken(attachment) || actorUserId !== undefined && attachment.actingUserId === actorUserId);
136490
+ }
136484
136491
  const own = actorUserId ? teamAttachments.find((attachment) => attachment.authMode === "user" && attachment.actingUserId === actorUserId) : undefined;
136485
136492
  if (own) {
136486
136493
  this.announceNarrowing(teamId, teamAttachments.length);
@@ -136570,27 +136577,31 @@ class SlackAppReceiver {
136570
136577
  }
136571
136578
  async authorize(source) {
136572
136579
  const teamId = source.teamId;
136573
- const attachment = teamId ? this.latestAttachmentFor(teamId) : undefined;
136574
- if (!attachment) {
136580
+ if (!teamId || this.teamAttachmentsFor(teamId).length === 0) {
136575
136581
  throw new SlackError(SlackErrorCode.NOT_CONNECTED, `No Slack instance attached for team ${teamId ?? "(none)"} on receiver ${this.key}`, true);
136576
136582
  }
136577
- return {
136578
- botToken: attachment.botToken,
136579
- botId: attachment.botId,
136580
- botUserId: attachment.botUserId,
136581
- teamId: attachment.teamId
136582
- };
136583
+ const bot = this.latestBotAttachmentFor(teamId);
136584
+ if (!bot)
136585
+ return { teamId };
136586
+ return { botToken: bot.botToken, botId: bot.botId, botUserId: bot.botUserId, teamId: bot.teamId };
136583
136587
  }
136584
- latestAttachmentFor(teamId) {
136588
+ latestBotAttachmentFor(teamId) {
136585
136589
  let latest;
136586
136590
  for (const attachment of this.attachmentMap.values()) {
136587
- if (attachment.teamId !== teamId)
136591
+ if (attachment.teamId !== teamId || !hasBotToken(attachment))
136588
136592
  continue;
136589
136593
  if (!latest || attachment.attachedAt >= latest.attachedAt)
136590
136594
  latest = attachment;
136591
136595
  }
136592
136596
  return latest;
136593
136597
  }
136598
+ refreshBotClient(teamId) {
136599
+ const newest = this.latestBotAttachmentFor(teamId);
136600
+ if (newest)
136601
+ this.botClients.set(teamId, this.clientForBotToken(newest.botToken));
136602
+ else
136603
+ this.botClients.delete(teamId);
136604
+ }
136594
136605
  botAttachmentFor(teamId) {
136595
136606
  for (const attachment of this.attachmentMap.values()) {
136596
136607
  if (attachment.teamId === teamId && attachment.authMode !== "user")
@@ -136707,7 +136718,7 @@ class SlackAppReceiver {
136707
136718
  affected.push(attachment);
136708
136719
  continue;
136709
136720
  }
136710
- const revoked = attachment.authMode === "user" ? attachment.actingUserId !== undefined && (tokens.oauth?.includes(attachment.actingUserId) ?? false) : tokens.bot?.includes(attachment.botUserId) ?? false;
136721
+ const revoked = attachment.authMode === "user" ? attachment.actingUserId !== undefined && (tokens.oauth?.includes(attachment.actingUserId) ?? false) : attachment.botUserId !== undefined && (tokens.bot?.includes(attachment.botUserId) ?? false);
136711
136722
  if (revoked)
136712
136723
  affected.push(attachment);
136713
136724
  }
@@ -136810,7 +136821,7 @@ function headersWithOptionalAuthorization(botToken, preserveAuthorization) {
136810
136821
  }
136811
136822
  async function fetchSlackPrivateUrl(url, botToken) {
136812
136823
  let currentUrl = new URL(url);
136813
- let preserveAuthorization = true;
136824
+ let preserveAuthorization = isSlackHost(currentUrl);
136814
136825
  for (let redirects = 0;redirects <= 5; redirects++) {
136815
136826
  const response = await fetch(currentUrl.toString(), {
136816
136827
  headers: headersWithOptionalAuthorization(botToken, preserveAuthorization),
@@ -137211,7 +137222,8 @@ function setupMessageHandlers2(app, target, botUserId, callbacks, dmPolicyConfig
137211
137222
  const meta = extractMessageMeta(msg);
137212
137223
  if (await enforceDmPolicy(meta, userId, dmPolicyConfig, instanceId, callbacks, logger5))
137213
137224
  return;
137214
- await processMessage2(instanceId, msg, resolveBotUserId(), callbacks, logger5, reliability, filterConfig);
137225
+ const mentionUserId = resolveBotUserId() ?? resolveActingUserId();
137226
+ await processMessage2(instanceId, msg, mentionUserId, callbacks, logger5, reliability, filterConfig);
137215
137227
  };
137216
137228
  if (app) {
137217
137229
  app.message(async ({ message: message2 }) => handle(message2));
@@ -149754,7 +149766,7 @@ function resolveSlackTokens(slackConfig, rawOptions, rawCredentials) {
149754
149766
  const appToken = slackConfig.appToken ?? rawCredentials.appToken;
149755
149767
  const signingSecret = slackConfig.signingSecret ?? rawCredentials.signingSecret;
149756
149768
  const mode = slackConfig.mode ?? "socket";
149757
- if (!botToken) {
149769
+ if (!botToken && authMode !== "user") {
149758
149770
  throw new SlackError(SlackErrorCode.INVALID_TOKEN, "botToken (xoxb-...) is required");
149759
149771
  }
149760
149772
  if (authMode === "user" && !userToken) {
@@ -149774,6 +149786,16 @@ function resolveSlackTokens(slackConfig, rawOptions, rawCredentials) {
149774
149786
  function readConnectOverrides(rawOptions) {
149775
149787
  return { force: rawOptions.force === true };
149776
149788
  }
149789
+ function resolveAttachmentTeamId(botTeamId, userTeamId) {
149790
+ if (botTeamId && userTeamId && botTeamId !== userTeamId) {
149791
+ throw new SlackError(SlackErrorCode.CONNECTION_FAILED, `The bot token belongs to workspace ${botTeamId} but the user token to ${userTeamId}; both must come from the same workspace`);
149792
+ }
149793
+ const teamId = botTeamId ?? userTeamId;
149794
+ if (!teamId) {
149795
+ throw new SlackError(SlackErrorCode.CONNECTION_FAILED, "auth.test returned no team_id for the user token; cannot tell which workspace this instance belongs to");
149796
+ }
149797
+ return teamId;
149798
+ }
149777
149799
  function resolvePresentedIdentity(attachment) {
149778
149800
  if (attachment.authMode === "user" && attachment.actingUserId) {
149779
149801
  return {
@@ -149962,29 +149984,30 @@ var init_plugin = __esm(() => {
149962
149984
  return { key, receiver };
149963
149985
  }
149964
149986
  async buildAttachment(instanceId, receiver, options, config2, caches) {
149965
- const botClient = receiver.clientForBotToken(options.botToken);
149966
- const identity = await resolveWorkspaceIdentity(botClient);
149987
+ const botClient = options.botToken ? receiver.clientForBotToken(options.botToken) : undefined;
149988
+ const identity = botClient ? await resolveWorkspaceIdentity(botClient) : undefined;
149967
149989
  const { actingClient, userClient } = buildActingClients(options, botClient);
149968
149990
  let actingUserId;
149969
149991
  let actingUserName;
149992
+ let actingTeamId;
149970
149993
  if (userClient) {
149971
- ({ actingUserId, actingUserName } = await this.resolveActingUserId(userClient));
149994
+ ({ actingUserId, actingUserName, teamId: actingTeamId } = await this.resolveActingUserId(userClient));
149972
149995
  if (!actingUserId) {
149973
149996
  throw new SlackError(SlackErrorCode.CONNECTION_FAILED, "User mode requires a resolved acting user id, but it could not be determined from the user token. Refusing to connect.");
149974
149997
  }
149975
149998
  }
149976
149999
  return {
149977
150000
  instanceId,
149978
- teamId: identity.teamId,
150001
+ teamId: resolveAttachmentTeamId(identity?.teamId, actingTeamId),
149979
150002
  authMode: options.authMode ?? "bot",
149980
150003
  actingClient,
149981
150004
  userClient,
149982
150005
  actingUserId,
149983
150006
  actingUserName,
149984
- botUserId: identity.botUserId,
149985
- botId: identity.botId,
149986
- botToken: options.botToken,
149987
- botName: identity.botName,
150007
+ botUserId: identity?.botUserId,
150008
+ botId: identity?.botId,
150009
+ botToken: botClient ? options.botToken : undefined,
150010
+ botName: identity?.botName,
149988
150011
  config: config2,
149989
150012
  dedupeCache: caches.dedupeCache,
149990
150013
  debouncer: caches.debouncer,
@@ -150002,7 +150025,7 @@ var init_plugin = __esm(() => {
150002
150025
  const username = auth.user ?? undefined;
150003
150026
  const actingUserName = actingUserId ? await resolveActingUserName(userClient, actingUserId, username, this.logger) : username;
150004
150027
  this.logger.info("Acting user identity resolved", { actingUserId, actingUser: actingUserName });
150005
- return { actingUserId, actingUserName };
150028
+ return { actingUserId, actingUserName, teamId: auth.team_id ?? undefined };
150006
150029
  } catch (error) {
150007
150030
  this.logger.warn("Failed to resolve the acting user identity from the user token", {
150008
150031
  error: String(error)
@@ -150578,12 +150601,12 @@ var init_plugin = __esm(() => {
150578
150601
  if (!channelId || !threadTs)
150579
150602
  return { totalFetched: 0, messages: [] };
150580
150603
  const botUserId = attachment.botUserId;
150581
- const botToken = attachment.botToken;
150604
+ const downloadToken = attachment.authMode === "user" && attachment.userClient?.token || attachment.botToken;
150582
150605
  const limit = options.limit ?? 200;
150583
- const messages = await this.paginateThreadHistory(attachment, channelId, threadTs, botUserId, botToken, limit);
150606
+ const messages = await this.paginateThreadHistory(attachment, channelId, threadTs, botUserId, downloadToken, limit);
150584
150607
  return { totalFetched: messages.length, messages };
150585
150608
  }
150586
- async paginateThreadHistory(attachment, channelId, threadTs, botUserId, botToken, maxMessages) {
150609
+ async paginateThreadHistory(attachment, channelId, threadTs, botUserId, downloadToken, maxMessages) {
150587
150610
  const messages = [];
150588
150611
  let cursor;
150589
150612
  do {
@@ -150594,7 +150617,7 @@ var init_plugin = __esm(() => {
150594
150617
  cursor
150595
150618
  });
150596
150619
  for (const msg of response.messages ?? []) {
150597
- const result = await this.buildHistorySyncMessage(msg, channelId, botUserId, botToken, attachment.botId);
150620
+ const result = await this.buildHistorySyncMessage(msg, channelId, botUserId, downloadToken, attachment.botId);
150598
150621
  if (result)
150599
150622
  messages.push(result);
150600
150623
  if (messages.length >= maxMessages)
@@ -150604,13 +150627,13 @@ var init_plugin = __esm(() => {
150604
150627
  } while (cursor && messages.length < maxMessages);
150605
150628
  return messages;
150606
150629
  }
150607
- async downloadSlackMediaToTemp(file, botToken, ts) {
150630
+ async downloadSlackMediaToTemp(file, downloadToken, ts) {
150608
150631
  const mimeType = file.mimetype ?? "application/octet-stream";
150609
150632
  const urlPrivate = file.url_private_download ?? file.url_private;
150610
- if (!urlPrivate)
150633
+ if (!urlPrivate || !downloadToken)
150611
150634
  return { mimeType, localPath: undefined };
150612
150635
  try {
150613
- const { buffer: buffer2 } = await downloadSlackFile(urlPrivate, botToken, this.logger);
150636
+ const { buffer: buffer2 } = await downloadSlackFile(urlPrivate, downloadToken, this.logger);
150614
150637
  const ext = (mimeType.split("/")[1]?.split(";")[0] ?? "bin").replace(/[^a-z0-9]/gi, "");
150615
150638
  const tmpPath = join4(tmpdir(), `omni-slack-hist-${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`);
150616
150639
  await writeFile2(tmpPath, buffer2);
@@ -150620,7 +150643,7 @@ var init_plugin = __esm(() => {
150620
150643
  return { mimeType, localPath: undefined };
150621
150644
  }
150622
150645
  }
150623
- async buildHistorySyncMessage(msg, channelId, botUserId, botToken, ownBotId) {
150646
+ async buildHistorySyncMessage(msg, channelId, botUserId, downloadToken, ownBotId) {
150624
150647
  const userId = msg.user;
150625
150648
  if (!userId || shouldSkipMessage(msg, [botUserId], ownBotId))
150626
150649
  return null;
@@ -150629,7 +150652,7 @@ var init_plugin = __esm(() => {
150629
150652
  const files = msg.files;
150630
150653
  const timestamp = new Date(Number.parseFloat(ts) * 1000);
150631
150654
  if (files && files.length > 0) {
150632
- const { mimeType, localPath } = await this.downloadSlackMediaToTemp(files[0], botToken, ts);
150655
+ const { mimeType, localPath } = await this.downloadSlackMediaToTemp(files[0], downloadToken, ts);
150633
150656
  return {
150634
150657
  externalId: ts,
150635
150658
  chatId: channelId,
@@ -266412,7 +266435,7 @@ var init_sentry_scrub = __esm(() => {
266412
266435
  var require_package7 = __commonJS((exports, module) => {
266413
266436
  module.exports = {
266414
266437
  name: "@omni/api",
266415
- version: "2.260922.13",
266438
+ version: "2.260922.15",
266416
266439
  private: true,
266417
266440
  type: "module",
266418
266441
  exports: {
@@ -408999,6 +409022,484 @@ var init_media3 = __esm(() => {
408999
409022
  });
409000
409023
  });
409001
409024
 
409025
+ // ../api/src/plugins/media-processor.ts
409026
+ function getContentFieldForType(processingType, contentType) {
409027
+ switch (processingType) {
409028
+ case "transcription":
409029
+ return "transcription";
409030
+ case "description":
409031
+ return contentType === "video" ? "videoDescription" : "imageDescription";
409032
+ case "extraction":
409033
+ return "documentExtraction";
409034
+ default:
409035
+ return;
409036
+ }
409037
+ }
409038
+ function inferProcessingType(contentType) {
409039
+ switch (contentType) {
409040
+ case "audio":
409041
+ return "transcription";
409042
+ case "image":
409043
+ case "video":
409044
+ return "description";
409045
+ case "document":
409046
+ return "extraction";
409047
+ default:
409048
+ return "transcription";
409049
+ }
409050
+ }
409051
+ function shouldProcess(contentType) {
409052
+ if (!contentType)
409053
+ return false;
409054
+ return PROCESSABLE_MEDIA_TYPES.has(contentType);
409055
+ }
409056
+ function isUuid(value) {
409057
+ return typeof value === "string" && isValidUuid(value);
409058
+ }
409059
+ function envelopeTenantId(envelope2) {
409060
+ const classification = classifyEnvelope(envelope2.metadata);
409061
+ return classification.world === "tenant" ? classification.tenantId : undefined;
409062
+ }
409063
+ function runMediaDb(ctx, trustedTenantId, fn) {
409064
+ return trustedTenantId === undefined ? fn() : runInWorkerTenantScope(ctx.db, trustedTenantId, fn);
409065
+ }
409066
+ function getMimeType2(content) {
409067
+ if (content.mimeType)
409068
+ return content.mimeType;
409069
+ switch (content.type) {
409070
+ case "audio":
409071
+ return "audio/ogg";
409072
+ case "image":
409073
+ return "image/jpeg";
409074
+ case "video":
409075
+ return "video/mp4";
409076
+ case "document":
409077
+ return "application/octet-stream";
409078
+ default:
409079
+ return;
409080
+ }
409081
+ }
409082
+ function isSlackFileUrl(mediaUrl) {
409083
+ let url;
409084
+ try {
409085
+ url = new URL(mediaUrl);
409086
+ } catch {
409087
+ return false;
409088
+ }
409089
+ return url.protocol === "https:" && hostMatchesSuffix4(url.hostname, "slack.com");
409090
+ }
409091
+ function selectSlackDownloadToken(instance4, mediaUrl) {
409092
+ if (!isSlackFileUrl(mediaUrl))
409093
+ return;
409094
+ const slackAuthMode = instance4.slackAuthMode;
409095
+ const slackUserToken = instance4.slackUserToken;
409096
+ if (slackAuthMode === "user" && typeof slackUserToken === "string" && slackUserToken.length > 0) {
409097
+ return slackUserToken;
409098
+ }
409099
+ return instance4.slackBotToken;
409100
+ }
409101
+ async function buildFetchOptions2(ctx, instanceId, mediaUrl, channelType, trustedTenantId) {
409102
+ if (channelType !== "slack")
409103
+ return;
409104
+ try {
409105
+ const instance4 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.instances.getById(instanceId));
409106
+ const token = selectSlackDownloadToken(instance4, mediaUrl);
409107
+ if (token) {
409108
+ return { headers: { Authorization: `Bearer ${token}` } };
409109
+ }
409110
+ } catch {}
409111
+ return;
409112
+ }
409113
+ async function downloadMediaFromUrl(ctx, instanceId, messageId, mediaUrl, mimeType, platformTimestamp, channelType, trustedTenantId) {
409114
+ const fetchOptions = await buildFetchOptions2(ctx, instanceId, mediaUrl, channelType, trustedTenantId);
409115
+ try {
409116
+ const result = await ctx.mediaStorage.storeFromUrl(instanceId, messageId, mediaUrl, mimeType, platformTimestamp, fetchOptions, trustedTenantId);
409117
+ await runMediaDb(ctx, trustedTenantId, () => ctx.mediaStorage.updateMessageLocalPath(messageId, result.localPath));
409118
+ log125.debug("Downloaded media from URL", { messageId, filePath: result.localPath });
409119
+ return result.localPath;
409120
+ } catch (error3) {
409121
+ log125.error("Failed to download media", { error: String(error3), mediaUrl });
409122
+ return null;
409123
+ }
409124
+ }
409125
+ async function downloadMediaViaPlugin(ctx, instanceId, messageId, channelType, mediaRef, fallbackMimeType, platformTimestamp, trustedTenantId) {
409126
+ const plugin13 = await (ctx.resolveChannelPlugin ?? getPlugin)(channelType);
409127
+ if (!plugin13?.downloadInboundMedia) {
409128
+ log125.warn("Channel plugin cannot materialize deferred media", { channelType, messageId });
409129
+ return null;
409130
+ }
409131
+ try {
409132
+ const { buffer: buffer3, mimeType } = pluginMediaSchema.parse(await plugin13.downloadInboundMedia(instanceId, mediaRef));
409133
+ const result = await ctx.mediaStorage.storeFromBuffer(instanceId, messageId, buffer3, mimeType || fallbackMimeType, platformTimestamp, trustedTenantId);
409134
+ await runMediaDb(ctx, trustedTenantId, () => ctx.mediaStorage.updateMessageLocalPath(messageId, result.localPath));
409135
+ log125.debug("Downloaded inbound media via plugin", { messageId, channelType, filePath: result.localPath });
409136
+ return result.localPath;
409137
+ } catch (error3) {
409138
+ log125.error("Failed to download inbound media via plugin", {
409139
+ error: String(error3),
409140
+ channelType,
409141
+ messageId
409142
+ });
409143
+ return null;
409144
+ }
409145
+ }
409146
+ async function resolveMediaPath2(ctx, instanceId, chatId, externalId, content, mimeType, channelType, trustedTenantId) {
409147
+ const maxWaitMs = 5000;
409148
+ const pollMs = 250;
409149
+ const deadline = Date.now() + maxWaitMs;
409150
+ let chat2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.chats.findByExternalIdSmart(instanceId, chatId));
409151
+ while (!chat2 && Date.now() < deadline) {
409152
+ await new Promise((r) => setTimeout(r, pollMs));
409153
+ chat2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.chats.findByExternalIdSmart(instanceId, chatId));
409154
+ }
409155
+ if (!chat2) {
409156
+ log125.debug("Chat not found, cannot process media", { chatId, externalId });
409157
+ return null;
409158
+ }
409159
+ const chatDbId = chat2.id;
409160
+ let message2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.messages.getByExternalId(chatDbId, externalId));
409161
+ while (!message2 && Date.now() < deadline) {
409162
+ await new Promise((r) => setTimeout(r, pollMs));
409163
+ message2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.messages.getByExternalId(chatDbId, externalId));
409164
+ }
409165
+ if (!message2) {
409166
+ log125.debug("Message not found after waiting, cannot process media", { externalId });
409167
+ return null;
409168
+ }
409169
+ let filePath = message2.mediaLocalPath;
409170
+ if (!filePath && content.mediaUrl) {
409171
+ filePath = await downloadMediaFromUrl(ctx, instanceId, message2.id, content.mediaUrl, mimeType, message2.platformTimestamp ?? undefined, channelType, trustedTenantId);
409172
+ if (!filePath)
409173
+ return null;
409174
+ }
409175
+ if (!filePath && content.mediaId && channelType) {
409176
+ filePath = await downloadMediaViaPlugin(ctx, instanceId, message2.id, channelType, content.mediaId, mimeType, message2.platformTimestamp ?? undefined, trustedTenantId);
409177
+ if (!filePath)
409178
+ return null;
409179
+ }
409180
+ if (!filePath) {
409181
+ log125.debug("No media file path available", { externalId });
409182
+ return null;
409183
+ }
409184
+ return {
409185
+ messageId: message2.id,
409186
+ filePath
409187
+ };
409188
+ }
409189
+ async function resolveSafeMediaContentEventId(ctx, envelope2, eventId) {
409190
+ if (!isUuid(eventId))
409191
+ return null;
409192
+ const id = eventId;
409193
+ const maxWaitMs = 250;
409194
+ const pollMs = 50;
409195
+ const deadline = Date.now() + maxWaitMs;
409196
+ while (true) {
409197
+ let found;
409198
+ try {
409199
+ found = await runConsumerInTenantContext(ctx.db, envelope2, async () => {
409200
+ const [event] = await scopedHandle(ctx.db).select({ id: omniEvents.id }).from(omniEvents).where(eq(omniEvents.id, id)).limit(1);
409201
+ return event?.id ?? null;
409202
+ });
409203
+ } catch (error3) {
409204
+ log125.debug("Failed to validate media_content event FK", { eventId: id, error: String(error3) });
409205
+ return null;
409206
+ }
409207
+ if (found)
409208
+ return found;
409209
+ if (Date.now() >= deadline) {
409210
+ log125.debug("Skipping media_content event FK; omni_event not found", { eventId: id });
409211
+ return null;
409212
+ }
409213
+ await new Promise((resolve4) => setTimeout(resolve4, pollMs));
409214
+ }
409215
+ }
409216
+ async function persistProcessingResult(ctx, messageId, safeEventId, result, contentType) {
409217
+ if (result.content) {
409218
+ const updateField = getContentFieldForType(result.processingType, contentType);
409219
+ if (updateField) {
409220
+ await scopedHandle(ctx.db).update(messages2).set({ [updateField]: result.content }).where(eq(messages2.id, messageId));
409221
+ }
409222
+ }
409223
+ try {
409224
+ const sdb = scopedHandle(ctx.db);
409225
+ const row = {
409226
+ eventId: safeEventId ?? null,
409227
+ mediaId: messageId,
409228
+ processingType: result.processingType,
409229
+ content: result.content ?? "",
409230
+ model: result.model,
409231
+ provider: result.provider,
409232
+ language: result.language,
409233
+ duration: result.duration,
409234
+ tokensUsed: result.inputTokens ? result.inputTokens + (result.outputTokens ?? 0) : undefined,
409235
+ costUsd: result.costCents != null ? String(Math.round(result.costCents)) : null,
409236
+ processingTimeMs: result.processingTimeMs
409237
+ };
409238
+ if (currentTenantScope()) {
409239
+ await sdb.transaction(async (sp) => {
409240
+ await sp.insert(mediaContent).values(row);
409241
+ });
409242
+ } else {
409243
+ await sdb.insert(mediaContent).values(row);
409244
+ }
409245
+ } catch (error3) {
409246
+ log125.warn("Failed to insert media_content record (non-critical)", {
409247
+ messageId,
409248
+ error: String(error3)
409249
+ });
409250
+ }
409251
+ }
409252
+ function getPromptOverride(ctx, contentType) {
409253
+ if (contentType === "audio")
409254
+ return ctx.promptOverrides.audio;
409255
+ if (contentType === "image")
409256
+ return ctx.promptOverrides.image;
409257
+ if (contentType === "video")
409258
+ return ctx.promptOverrides.video;
409259
+ if (contentType === "document")
409260
+ return ctx.promptOverrides.document;
409261
+ return;
409262
+ }
409263
+ async function processMessageMedia(ctx, payload, metadata, envelope2) {
409264
+ const { instanceId, eventId } = metadata;
409265
+ const { content, externalId } = payload;
409266
+ const mimeType = getMimeType2(content);
409267
+ const trustedTenantId = envelopeTenantId(envelope2);
409268
+ if (!mimeType || !ctx.mediaService.canProcess(mimeType)) {
409269
+ log125.debug("MIME type not processable or missing", { mimeType, externalId });
409270
+ return;
409271
+ }
409272
+ const media = await resolveMediaPath2(ctx, instanceId, payload.chatId, externalId, content, mimeType, metadata.channelType, trustedTenantId);
409273
+ if (!media)
409274
+ return;
409275
+ const processable = await ctx.mediaStorage.materializeForProcessing(media.filePath);
409276
+ log125.info("Processing media", { messageId: media.messageId, mimeType, filePath: processable.path });
409277
+ let result;
409278
+ try {
409279
+ result = await ctx.mediaService.process(processable.path, mimeType, {
409280
+ language: ctx.defaultLanguage,
409281
+ caption: content.text,
409282
+ prompt: getPromptOverride(ctx, content.type)
409283
+ });
409284
+ } finally {
409285
+ await processable.cleanup();
409286
+ }
409287
+ if (!result.success) {
409288
+ const reason = result.errorMessage ?? "unknown";
409289
+ log125.warn("Media processing failed", { messageId: media.messageId, error: reason });
409290
+ const processingType = result.processingType ?? inferProcessingType(content.type);
409291
+ const errorColumn = getContentFieldForType(processingType, content.type);
409292
+ if (errorColumn) {
409293
+ const marker = `[error: media processing failed \u2014 ${reason}]`;
409294
+ await runConsumerInTenantContext(ctx.db, envelope2, async () => {
409295
+ await scopedHandle(ctx.db).update(messages2).set({ [errorColumn]: marker }).where(eq(messages2.id, media.messageId));
409296
+ });
409297
+ }
409298
+ await ctx.eventBus.publish("media.processing.failed", {
409299
+ eventId: eventId ?? media.messageId,
409300
+ mediaId: media.messageId,
409301
+ processingType,
409302
+ error: reason,
409303
+ provider: result.provider,
409304
+ model: result.model
409305
+ }, { instanceId, channelType: metadata.channelType });
409306
+ await ctx.eventBus.publish("media.processed", {
409307
+ eventId: eventId ?? media.messageId,
409308
+ mediaId: media.messageId,
409309
+ processingType,
409310
+ content: "",
409311
+ error: reason
409312
+ }, { instanceId, channelType: metadata.channelType });
409313
+ return;
409314
+ }
409315
+ const safeEventId = await resolveSafeMediaContentEventId(ctx, envelope2, eventId);
409316
+ await runConsumerInTenantContext(ctx.db, envelope2, () => persistProcessingResult(ctx, media.messageId, safeEventId, result, content.type));
409317
+ log125.info("Media processing complete", {
409318
+ messageId: media.messageId,
409319
+ processingType: result.processingType,
409320
+ provider: result.provider,
409321
+ model: result.model,
409322
+ costCents: result.costCents,
409323
+ processingTimeMs: result.processingTimeMs
409324
+ });
409325
+ await ctx.eventBus.publish("media.processed", {
409326
+ eventId: eventId ?? media.messageId,
409327
+ mediaId: media.messageId,
409328
+ processingType: result.processingType,
409329
+ content: result.content ?? "",
409330
+ model: result.model,
409331
+ provider: result.provider,
409332
+ tokensUsed: result.inputTokens ? result.inputTokens + (result.outputTokens ?? 0) : undefined
409333
+ }, { instanceId, channelType: metadata.channelType });
409334
+ }
409335
+ async function publishMediaCrashEvents(ctx, eventId, payload, metadata, error3, trustedTenantId) {
409336
+ try {
409337
+ const processingType = inferProcessingType(payload.content.type);
409338
+ const reason = `unexpected: ${String(error3)}`;
409339
+ const mediaId = await resolveMediaIdForCrash(ctx, metadata.instanceId ?? "", payload, trustedTenantId);
409340
+ const meta = { instanceId: metadata.instanceId, channelType: metadata.channelType };
409341
+ await ctx.eventBus.publish("media.processing.failed", {
409342
+ eventId,
409343
+ mediaId,
409344
+ processingType,
409345
+ error: reason,
409346
+ provider: "unknown",
409347
+ model: "unknown"
409348
+ }, meta);
409349
+ await ctx.eventBus.publish("media.processed", {
409350
+ eventId,
409351
+ mediaId,
409352
+ processingType,
409353
+ content: "",
409354
+ error: reason
409355
+ }, meta);
409356
+ } catch (publishError) {
409357
+ log125.error("Failed to publish media failure event", { error: String(publishError) });
409358
+ }
409359
+ }
409360
+ async function resolveMediaIdForCrash(ctx, instanceId, payload, trustedTenantId) {
409361
+ try {
409362
+ const msg = await runMediaDb(ctx, trustedTenantId, async () => {
409363
+ const chat2 = await ctx.services.chats.findByExternalIdSmart(instanceId, payload.chatId);
409364
+ if (!chat2)
409365
+ return null;
409366
+ return ctx.services.messages.getByExternalId(chat2.id, payload.externalId);
409367
+ });
409368
+ if (msg)
409369
+ return msg.id;
409370
+ } catch (lookupError) {
409371
+ log125.debug("Failed to resolve DB message UUID for crash handler, falling back to externalId", {
409372
+ error: String(lookupError),
409373
+ instanceId,
409374
+ chatId: payload.chatId
409375
+ });
409376
+ }
409377
+ return payload.externalId;
409378
+ }
409379
+ async function setupMediaProcessor(eventBus, db2, services) {
409380
+ const [
409381
+ groqApiKey,
409382
+ openaiApiKey,
409383
+ geminiApiKey,
409384
+ defaultLanguage,
409385
+ audioProvider,
409386
+ audioModel,
409387
+ geminiAudioModel,
409388
+ audioPrompt,
409389
+ imagePrompt,
409390
+ videoPrompt,
409391
+ documentPrompt,
409392
+ geminiVisionModel,
409393
+ openaiVisionModel
409394
+ ] = await Promise.all([
409395
+ services.settings.getSecret("groq.api_key", "GROQ_API_KEY"),
409396
+ services.settings.getSecret("openai.api_key", "OPENAI_API_KEY"),
409397
+ services.settings.getSecret("gemini.api_key", "GEMINI_API_KEY"),
409398
+ services.settings.getString("media.default_language", "DEFAULT_LANGUAGE", "pt"),
409399
+ services.settings.getString("stt.provider", "STT_PROVIDER", "openai"),
409400
+ services.settings.getString("stt.openai.model", "OPENAI_STT_MODEL", OPENAI_TRANSCRIBE_MODEL),
409401
+ services.settings.getString("stt.gemini.model", "GEMINI_STT_MODEL", GEMINI_AUDIO_MODEL),
409402
+ services.settings.getString("prompt.audio_transcription"),
409403
+ services.settings.getString("prompt.image_description"),
409404
+ services.settings.getString("prompt.video_description"),
409405
+ services.settings.getString("prompt.document_ocr"),
409406
+ services.settings.getString("media.gemini.model", "GEMINI_MEDIA_MODEL", GEMINI_MODEL),
409407
+ services.settings.getString("media.openai.vision_model", "OPENAI_VISION_MODEL", OPENAI_VISION_MODEL)
409408
+ ]);
409409
+ const mediaService = createMediaProcessingService({
409410
+ groqApiKey,
409411
+ openaiApiKey,
409412
+ geminiApiKey,
409413
+ defaultLanguage,
409414
+ audioProvider: audioProvider ?? "openai",
409415
+ audioModel: audioModel ?? OPENAI_TRANSCRIBE_MODEL,
409416
+ geminiAudioModel: geminiAudioModel ?? GEMINI_AUDIO_MODEL,
409417
+ geminiVisionModel: geminiVisionModel ?? GEMINI_MODEL,
409418
+ openaiVisionModel: openaiVisionModel ?? OPENAI_VISION_MODEL,
409419
+ audioPrompt: audioPrompt ?? undefined
409420
+ });
409421
+ const mediaStorage2 = new MediaStorageService(db2);
409422
+ const ctx = {
409423
+ db: db2,
409424
+ eventBus,
409425
+ services,
409426
+ mediaService,
409427
+ mediaStorage: mediaStorage2,
409428
+ defaultLanguage: defaultLanguage ?? "pt",
409429
+ promptOverrides: {
409430
+ audio: audioPrompt,
409431
+ image: imagePrompt,
409432
+ video: videoPrompt,
409433
+ document: documentPrompt
409434
+ }
409435
+ };
409436
+ await eventBus.subscribe("message.received", async (event) => {
409437
+ const payload = event.payload;
409438
+ const metadata = event.metadata;
409439
+ if (!metadata.instanceId)
409440
+ return;
409441
+ if (!shouldProcess(payload.content.type))
409442
+ return;
409443
+ try {
409444
+ await processMessageMedia(ctx, payload, {
409445
+ instanceId: metadata.instanceId,
409446
+ eventId: event.id,
409447
+ channelType: metadata.channelType
409448
+ }, event);
409449
+ } catch (error3) {
409450
+ log125.error("Failed to process media", {
409451
+ externalId: payload.externalId,
409452
+ error: String(error3)
409453
+ });
409454
+ await publishMediaCrashEvents(ctx, event.id, payload, metadata, error3, envelopeTenantId(event));
409455
+ }
409456
+ }, {
409457
+ durable: "media-processor",
409458
+ queue: "media-processor",
409459
+ maxRetries: 2,
409460
+ retryDelayMs: 1000,
409461
+ startFrom: "first",
409462
+ concurrency: 5
409463
+ });
409464
+ setGlobalCircuitBreakerStateChangeCallback((name, from, to) => {
409465
+ log125.warn("Circuit breaker state change", { provider: name, from, to });
409466
+ });
409467
+ await eventBus.subscribePattern("media.health.request", async (event) => {
409468
+ const tracker = getMediaHealthTracker();
409469
+ const report = tracker.getReport();
409470
+ const correlationId = event.metadata?.correlationId ?? event.id;
409471
+ await eventBus.publishGeneric("system.media.health.response", { correlationId, report }, { source: "media-processor" });
409472
+ log125.debug("Published media health report", {
409473
+ correlationId,
409474
+ totalRequests: report.overall.totalRequests,
409475
+ successRate: report.overall.successRate
409476
+ });
409477
+ }, {
409478
+ durable: "media-health-responder",
409479
+ queue: "media-health-responder"
409480
+ });
409481
+ log125.info("Media processor initialized");
409482
+ }
409483
+ var log125, PROCESSABLE_MEDIA_TYPES, pluginMediaSchema;
409484
+ var init_media_processor = __esm(() => {
409485
+ init_src();
409486
+ init_src6();
409487
+ init_src9();
409488
+ init_drizzle_orm();
409489
+ init_zod();
409490
+ init_media_storage();
409491
+ init_tenant_scope();
409492
+ init_worker_tenant_context();
409493
+ init_safe_media_fetch();
409494
+ init_loader2();
409495
+ log125 = createLogger("media-processor");
409496
+ PROCESSABLE_MEDIA_TYPES = new Set(["audio", "image", "document", "video"]);
409497
+ pluginMediaSchema = exports_external.object({
409498
+ buffer: exports_external.instanceof(Buffer),
409499
+ mimeType: exports_external.string().min(1).optional()
409500
+ });
409501
+ });
409502
+
409002
409503
  // ../api/src/routes/v2/_close-contact-config.ts
409003
409504
  function resolveCloseContactConfig(outcome, instanceSettings) {
409004
409505
  const override = instanceSettings?.closeContactConfig;
@@ -409062,7 +409563,7 @@ async function resolveReactionTarget(services, instanceId, resolvedTo, messageId
409062
409563
  recoverable: false
409063
409564
  });
409064
409565
  }
409065
- log125.warn("Reaction target chat not found in DB; deferring fromMe to channel plugin fallback (#386)", {
409566
+ log126.warn("Reaction target chat not found in DB; deferring fromMe to channel plugin fallback (#386)", {
409066
409567
  instanceId,
409067
409568
  resolvedTo,
409068
409569
  messageId,
@@ -409072,7 +409573,7 @@ async function resolveReactionTarget(services, instanceId, resolvedTo, messageId
409072
409573
  }
409073
409574
  const target = isUUID(messageId) ? await getReactionTargetByOmniId(services, instanceId, chat2.id, messageId) : await services.messages.getByExternalId(chat2.id, messageId);
409074
409575
  if (!target) {
409075
- log125.warn("Reaction target message not found in DB; deferring fromMe to channel plugin fallback (#386)", {
409576
+ log126.warn("Reaction target message not found in DB; deferring fromMe to channel plugin fallback (#386)", {
409076
409577
  instanceId,
409077
409578
  chatId: chat2.id,
409078
409579
  messageId,
@@ -409223,7 +409724,7 @@ function resolveSentByAgentId(sentBy, instance4) {
409223
409724
  if (sentBy !== "agent")
409224
409725
  return;
409225
409726
  if (!instance4.agentId) {
409226
- log125.warn("sentBy=agent could not be attributed: instance has no configured agent", { instanceId: instance4.id });
409727
+ log126.warn("sentBy=agent could not be attributed: instance has no configured agent", { instanceId: instance4.id });
409227
409728
  return;
409228
409729
  }
409229
409730
  return instance4.agentId;
@@ -409279,14 +409780,14 @@ async function resolveMessageFromRef(services, ref) {
409279
409780
  }
409280
409781
  return found;
409281
409782
  }
409282
- function buildMediaDownloadFetchOptions(instance4) {
409783
+ function buildMediaDownloadFetchOptions(instance4, mediaUrl) {
409283
409784
  if (instance4.channel !== "slack")
409284
409785
  return;
409285
- const slackBotToken = typeof instance4.slackBotToken === "string" ? instance4.slackBotToken : undefined;
409286
- if (!slackBotToken)
409786
+ const token = selectSlackDownloadToken(instance4, mediaUrl);
409787
+ if (!token)
409287
409788
  return;
409288
409789
  return {
409289
- headers: { Authorization: `Bearer ${slackBotToken}` },
409790
+ headers: { Authorization: `Bearer ${token}` },
409290
409791
  preserveAuthRedirectHostSuffixes: ["slack.com"]
409291
409792
  };
409292
409793
  }
@@ -409339,10 +409840,10 @@ async function resolveChannelMessageId(services, messageId, instanceId) {
409339
409840
  return messageId;
409340
409841
  const message2 = await services.messages.getById(messageId);
409341
409842
  await verifyMessageInstanceOwnership(services, message2, instanceId);
409342
- log125.debug("Resolved internal UUID to external ID", { messageId, externalId: message2.externalId });
409843
+ log126.debug("Resolved internal UUID to external ID", { messageId, externalId: message2.externalId });
409343
409844
  return message2.externalId;
409344
409845
  }
409345
- var log125, mediaDownloadLog, messagesRoutes, MIME_BY_EXTENSION, DEFAULT_MIME_BY_MEDIA_TYPE, UUID_REGEX2, permalinkQuerySchema, MessageSourceSchema, MessageTypeSchema, MessageStatusSchema, DeliveryStatusSchema, listQuerySchema15, createMessageSchema, updateMessageSchema, recordEditSchema, addReactionSchema, removeReactionSchema, updateDeliveryStatusSchema, sentByField, MentionSchema, sendTextSchema, sendMediaSchema, sendReactionSchema, sendStickerSchema, sendContactSchema, sendLocationSchema, messageRefSchema, _mediaStorageForDownload = null, sendTtsSchema, forwardMessageSchema, sendPresenceSchema, markMessageReadSchema, markBatchReadSchema, sendPollSchema, sendEmbedSchema, editMessageChannelSchema, deleteMessageChannelSchema, starMessageSchema;
409846
+ var log126, mediaDownloadLog, messagesRoutes, MIME_BY_EXTENSION, DEFAULT_MIME_BY_MEDIA_TYPE, UUID_REGEX2, permalinkQuerySchema, MessageSourceSchema, MessageTypeSchema, MessageStatusSchema, DeliveryStatusSchema, listQuerySchema15, createMessageSchema, updateMessageSchema, recordEditSchema, addReactionSchema, removeReactionSchema, updateDeliveryStatusSchema, sentByField, MentionSchema, sendTextSchema, sendMediaSchema, sendReactionSchema, sendStickerSchema, sendContactSchema, sendLocationSchema, messageRefSchema, _mediaStorageForDownload = null, sendTtsSchema, forwardMessageSchema, sendPresenceSchema, markMessageReadSchema, markBatchReadSchema, sendPollSchema, sendEmbedSchema, editMessageChannelSchema, deleteMessageChannelSchema, starMessageSchema;
409346
409847
  var init_messages6 = __esm(() => {
409347
409848
  init_dist6();
409348
409849
  init_src2();
@@ -409353,13 +409854,14 @@ var init_messages6 = __esm(() => {
409353
409854
  init_dist2();
409354
409855
  init_zod();
409355
409856
  init_sentry_scrub();
409857
+ init_media_processor();
409356
409858
  init_date_query();
409357
409859
  init_messages5();
409358
409860
  init_api_keys();
409359
409861
  init_media_storage();
409360
409862
  init_tenant_scope();
409361
409863
  init__close_contact_config();
409362
- log125 = createLogger("routes:messages");
409864
+ log126 = createLogger("routes:messages");
409363
409865
  mediaDownloadLog = createLogger("routes:messages:media-download");
409364
409866
  messagesRoutes = new Hono2;
409365
409867
  MIME_BY_EXTENSION = {
@@ -409668,7 +410170,7 @@ var init_messages6 = __esm(() => {
409668
410170
  }
409669
410171
  if (!cached) {
409670
410172
  try {
409671
- const result = await mediaStorage2.storeFromUrl(instanceId, message2.id, mediaUrl, message2.mediaMimeType ?? undefined, message2.platformTimestamp ?? undefined, buildMediaDownloadFetchOptions(instance4), currentTenantScope()?.tenantId ?? undefined);
410173
+ const result = await mediaStorage2.storeFromUrl(instanceId, message2.id, mediaUrl, message2.mediaMimeType ?? undefined, message2.platformTimestamp ?? undefined, buildMediaDownloadFetchOptions(instance4, mediaUrl), currentTenantScope()?.tenantId ?? undefined);
409672
410174
  mediaLocalPath = result.localPath;
409673
410175
  await mediaStorage2.updateMessageLocalPath(message2.id, result.localPath);
409674
410176
  mediaDownloadLog.info("Downloaded and cached media", {
@@ -410297,7 +410799,7 @@ var init_messages6 = __esm(() => {
410297
410799
  channelHandoffSupported: hasNativeHandoff,
410298
410800
  ...data.motivoHandoff ? { motivoHandoff: data.motivoHandoff } : {}
410299
410801
  }
410300
- }).catch((err2) => log125.warn("Failed to persist handoff log", { error: String(err2) }));
410802
+ }).catch((err2) => log126.warn("Failed to persist handoff log", { error: String(err2) }));
410301
410803
  return c.json({
410302
410804
  data: {
410303
410805
  messageId: channelSendResult?.messageId ?? null,
@@ -410400,7 +410902,7 @@ var init_messages6 = __esm(() => {
410400
410902
  closedFields: data.closeFields ?? null,
410401
410903
  closedAt: closedAt.toISOString()
410402
410904
  };
410403
- services.eventBus.publish("chat.closed", payload, { instanceId: data.instanceId }).catch((err2) => log125.debug("Failed to publish chat.closed", { error: String(err2) }));
410905
+ services.eventBus.publish("chat.closed", payload, { instanceId: data.instanceId }).catch((err2) => log126.debug("Failed to publish chat.closed", { error: String(err2) }));
410404
410906
  }
410405
410907
  return c.json({
410406
410908
  data: {
@@ -412103,9 +412605,10 @@ function buildSlackAuthorizeUrl(input) {
412103
412605
  const scopes = slackAuthorizeScopes();
412104
412606
  const url = new URL(SLACK_AUTHORIZE_URL);
412105
412607
  url.searchParams.set("client_id", input.clientId);
412106
- url.searchParams.set("scope", scopes.scope);
412107
412608
  if (input.mode === "user")
412108
412609
  url.searchParams.set("user_scope", scopes.user_scope);
412610
+ else
412611
+ url.searchParams.set("scope", scopes.scope);
412109
412612
  url.searchParams.set("redirect_uri", input.redirectUri);
412110
412613
  url.searchParams.set("state", input.state);
412111
412614
  return url.toString();
@@ -412302,7 +412805,7 @@ function matchesIdentity(row, input) {
412302
412805
  async function findExisting(instances2, input) {
412303
412806
  const page = await instances2.list({ channel: ["slack"], limit: SLACK_LOOKUP_LIMIT });
412304
412807
  if (page.hasMore) {
412305
- log126.warn("Slack OAuth identity lookup truncated; add a service finder", { limit: SLACK_LOOKUP_LIMIT });
412808
+ log127.warn("Slack OAuth identity lookup truncated; add a service finder", { limit: SLACK_LOOKUP_LIMIT });
412306
412809
  }
412307
412810
  return page.items.find((row) => matchesIdentity(row, input));
412308
412811
  }
@@ -412320,7 +412823,7 @@ function isUniqueViolation(error3) {
412320
412823
  }
412321
412824
  function credentialColumns(input) {
412322
412825
  return {
412323
- slackBotToken: input.botToken,
412826
+ slackBotToken: input.mode === "bot" ? input.botToken ?? null : null,
412324
412827
  slackUserToken: input.mode === "user" ? input.userToken ?? null : null,
412325
412828
  slackAuthMode: input.mode,
412326
412829
  slackAppToken: input.appToken,
@@ -412368,9 +412871,12 @@ function applyProfileMetadata(options, metadata) {
412368
412871
  options.dmAllowlist = metadata.dmAllowlist;
412369
412872
  }
412370
412873
  function buildConnectOptions(input, row) {
412371
- const options = { forceNewQr: false, token: input.botToken };
412874
+ const options = { forceNewQr: false };
412372
412875
  applyProfileMetadata(options, row.profileMetadata);
412373
- options.botToken = input.botToken;
412876
+ if (input.mode === "bot" && input.botToken) {
412877
+ options.token = input.botToken;
412878
+ options.botToken = input.botToken;
412879
+ }
412374
412880
  if (input.mode === "user" && input.userToken)
412375
412881
  options.userToken = input.userToken;
412376
412882
  options.authMode = input.mode;
@@ -412389,7 +412895,7 @@ async function upsertSlackOAuthInstance(deps, input) {
412389
412895
  const existing = await findExisting(deps.instances, input);
412390
412896
  const row = existing ? await deps.instances.update(existing.id, credentialColumns(input)) : await createWithUniqueName(deps.instances, input);
412391
412897
  const created = !existing;
412392
- log126.info(created ? "Slack OAuth instance created" : "Slack OAuth instance re-authorized", {
412898
+ log127.info(created ? "Slack OAuth instance created" : "Slack OAuth instance re-authorized", {
412393
412899
  instanceId: row.id,
412394
412900
  teamId: input.teamId,
412395
412901
  mode: input.mode
@@ -412398,18 +412904,18 @@ async function upsertSlackOAuthInstance(deps, input) {
412398
412904
  await plugin13.connect(row.id, { instanceId: row.id, credentials: {}, options: buildConnectOptions(input, row) });
412399
412905
  } catch (error3) {
412400
412906
  const connectError = redactSlackTokens(error3 instanceof Error ? error3.message : "Unknown error");
412401
- log126.error("Slack OAuth instance failed to connect", { instanceId: row.id, error: connectError });
412907
+ log127.error("Slack OAuth instance failed to connect", { instanceId: row.id, error: connectError });
412402
412908
  return { instanceId: row.id, created, connectError };
412403
412909
  }
412404
412910
  return { instanceId: row.id, created };
412405
412911
  }
412406
- var log126, SLACK_LOOKUP_LIMIT = 1000, NAME_MAX = 200, NAME_SUFFIX_ATTEMPTS = 20;
412912
+ var log127, SLACK_LOOKUP_LIMIT = 1000, NAME_MAX = 200, NAME_SUFFIX_ATTEMPTS = 20;
412407
412913
  var init_slack_oauth2 = __esm(() => {
412408
412914
  init_src4();
412409
412915
  init_src();
412410
412916
  init_slack_app();
412411
412917
  init_slack_oauth();
412412
- log126 = createLogger("api:slack-oauth");
412918
+ log127 = createLogger("api:slack-oauth");
412413
412919
  });
412414
412920
 
412415
412921
  // ../api/src/routes/v2/slack.ts
@@ -412501,6 +413007,17 @@ async function resolveSlackAppForCallback(c, state) {
412501
413007
  function slackError(code, message2) {
412502
413008
  return `${code}: ${message2}`;
412503
413009
  }
413010
+ function grantTokens(mode, access) {
413011
+ if (mode === "bot") {
413012
+ return access.access_token ? { ok: true, botToken: access.access_token } : { ok: false, code: "SLACK_OAUTH_EXCHANGE_FAILED", message: "Slack returned no workspace bot token" };
413013
+ }
413014
+ const userToken = access.authed_user.access_token;
413015
+ return userToken ? { ok: true, userToken } : {
413016
+ ok: false,
413017
+ code: "SLACK_USER_TOKEN_MISSING",
413018
+ message: "Slack granted no user token; the user scopes were not authorized"
413019
+ };
413020
+ }
412504
413021
  async function completeSlackInstall(deps, pending2, code, credentials) {
412505
413022
  const tenantId = pending2.tenantContext?.tenantId ?? null;
412506
413023
  let access;
@@ -412519,22 +413036,21 @@ async function completeSlackInstall(deps, pending2, code, credentials) {
412519
413036
  return errorOutcome(tenantId, "SLACK_ENTERPRISE_INSTALL_UNSUPPORTED", "Enterprise Grid (org-wide) installs are not supported; install into a single workspace");
412520
413037
  }
412521
413038
  const teamId = access.team?.id;
412522
- if (!teamId || !access.access_token) {
412523
- return errorOutcome(tenantId, "SLACK_OAUTH_EXCHANGE_FAILED", "Slack returned no workspace bot token");
412524
- }
412525
- const userToken = access.authed_user.access_token;
412526
- if (pending2.mode === "user" && !userToken) {
412527
- return errorOutcome(tenantId, "SLACK_USER_TOKEN_MISSING", "Slack granted no user token; the user scopes were not authorized");
413039
+ if (!teamId) {
413040
+ return errorOutcome(tenantId, "SLACK_OAUTH_EXCHANGE_FAILED", "Slack returned no workspace id");
412528
413041
  }
412529
- const displayName = pending2.mode === "user" ? await fetchSlackUserDisplayName(access.access_token, access.authed_user.id) : undefined;
413042
+ const tokens = grantTokens(pending2.mode, access);
413043
+ if (!tokens.ok)
413044
+ return errorOutcome(tenantId, tokens.code, tokens.message);
413045
+ const displayName = tokens.userToken ? await fetchSlackUserDisplayName(tokens.userToken, access.authed_user.id) : undefined;
412530
413046
  const input = {
412531
413047
  mode: pending2.mode,
412532
413048
  teamId,
412533
413049
  teamName: access.team?.name,
412534
413050
  userId: access.authed_user.id,
412535
413051
  displayName,
412536
- botToken: access.access_token,
412537
- userToken,
413052
+ botToken: tokens.botToken,
413053
+ userToken: tokens.userToken,
412538
413054
  appToken: credentials.appToken,
412539
413055
  signingSecret: credentials.signingSecret
412540
413056
  };
@@ -412790,7 +413306,7 @@ function makeClient(cfg) {
412790
413306
  function variablesToBodyParameters(variables) {
412791
413307
  return Object.entries(variables).map(([k2, v2]) => [Number.parseInt(k2, 10), v2]).filter(([n2]) => Number.isFinite(n2) && n2 >= 1).sort((a, b3) => a[0] - b3[0]).map(([, v2]) => v2);
412792
413308
  }
412793
- var log127, templatesRoutes, instanceAccess4, idParamSchema6, templateIdParamSchema, templateNameParamSchema, listQuerySchema20, createBodySchema, sendTestBodySchema, sendByNameBodySchema;
413309
+ var log128, templatesRoutes, instanceAccess4, idParamSchema6, templateIdParamSchema, templateNameParamSchema, listQuerySchema20, createBodySchema, sendTestBodySchema, sendByNameBodySchema;
412794
413310
  var init_templates3 = __esm(() => {
412795
413311
  init_dist6();
412796
413312
  init_src7();
@@ -412801,7 +413317,7 @@ var init_templates3 = __esm(() => {
412801
413317
  init_dist2();
412802
413318
  init_zod();
412803
413319
  init_auth();
412804
- log127 = createLogger("routes:whatsapp-templates");
413320
+ log128 = createLogger("routes:whatsapp-templates");
412805
413321
  templatesRoutes = new Hono2;
412806
413322
  instanceAccess4 = requireInstanceAccess((c) => c.req.param("id") ?? "");
412807
413323
  idParamSchema6 = exports_external.object({
@@ -412928,7 +413444,7 @@ var init_templates3 = __esm(() => {
412928
413444
  });
412929
413445
  return c.json({ data: persisted.row }, persisted.created ? 201 : 200);
412930
413446
  } catch (dbErr) {
412931
- log127.error("[whatsapp-business] local template mirror failed after Meta create", {
413447
+ log128.error("[whatsapp-business] local template mirror failed after Meta create", {
412932
413448
  instanceId,
412933
413449
  metaTemplateId: result.id,
412934
413450
  name: body.name,
@@ -413630,7 +414146,7 @@ async function readProfilePhotoUpload(c) {
413630
414146
  fileType: blob.type || "image/jpeg"
413631
414147
  };
413632
414148
  }
413633
- var log128, whatsappBusinessRoutes, instanceAccess5, analyticsQuerySchema2, updateProfileSchema;
414149
+ var log129, whatsappBusinessRoutes, instanceAccess5, analyticsQuerySchema2, updateProfileSchema;
413634
414150
  var init_whatsapp_business3 = __esm(() => {
413635
414151
  init_dist6();
413636
414152
  init_src7();
@@ -413641,7 +414157,7 @@ var init_whatsapp_business3 = __esm(() => {
413641
414157
  init_oauth_token_cache();
413642
414158
  init_whatsapp_business_connection();
413643
414159
  init_auth();
413644
- log128 = createLogger("api:whatsapp-business");
414160
+ log129 = createLogger("api:whatsapp-business");
413645
414161
  whatsappBusinessRoutes = new Hono2;
413646
414162
  instanceAccess5 = requireInstanceAccess((c) => c.req.param("id") ?? "");
413647
414163
  whatsappBusinessRoutes.use("/:id/whatsapp-business/*", instanceAccess5);
@@ -413667,7 +414183,7 @@ var init_whatsapp_business3 = __esm(() => {
413667
414183
  });
413668
414184
  } catch (err2) {
413669
414185
  const message2 = err2 instanceof Error ? err2.message : "Unknown error";
413670
- log128.error("OAuth exchange failed", { error: message2 });
414186
+ log129.error("OAuth exchange failed", { error: message2 });
413671
414187
  return c.json({ error: { code: "OAUTH_EXCHANGE_FAILED", message: message2 } }, 400);
413672
414188
  }
413673
414189
  });
@@ -413727,7 +414243,7 @@ var init_whatsapp_business3 = __esm(() => {
413727
414243
  await services.instances.update(id, { metaDisplayPhoneNumber: displayPhoneNumber });
413728
414244
  }
413729
414245
  } catch (err2) {
413730
- log128.warn("Failed to fetch phone_number info during connect \u2014 continuing", {
414246
+ log129.warn("Failed to fetch phone_number info during connect \u2014 continuing", {
413731
414247
  instanceId: id,
413732
414248
  error: err2 instanceof Error ? err2.message : String(err2)
413733
414249
  });
@@ -413753,7 +414269,7 @@ var init_whatsapp_business3 = __esm(() => {
413753
414269
  });
413754
414270
  } catch (err2) {
413755
414271
  const message2 = err2 instanceof Error ? err2.message : "Unknown error";
413756
- log128.error("plugin.connect failed for whatsapp-business", { instanceId: id, error: message2 });
414272
+ log129.error("plugin.connect failed for whatsapp-business", { instanceId: id, error: message2 });
413757
414273
  return c.json({ error: { code: "CONNECT_FAILED", message: message2 } }, 500);
413758
414274
  }
413759
414275
  return c.json({
@@ -413781,7 +414297,7 @@ var init_whatsapp_business3 = __esm(() => {
413781
414297
  return c.json({ ok: true });
413782
414298
  } catch (err2) {
413783
414299
  const message2 = err2 instanceof Error ? err2.message : "Unknown error";
413784
- log128.error("register phone number failed", { instanceId: id, error: message2 });
414300
+ log129.error("register phone number failed", { instanceId: id, error: message2 });
413785
414301
  return c.json({ error: { code: "REGISTER_FAILED", message: message2 } }, 500);
413786
414302
  }
413787
414303
  });
@@ -413800,7 +414316,7 @@ var init_whatsapp_business3 = __esm(() => {
413800
414316
  return c.json({ subscribed: result.success });
413801
414317
  } catch (err2) {
413802
414318
  const message2 = err2 instanceof Error ? err2.message : "Unknown error";
413803
- log128.error("subscribe app failed", { instanceId: id, error: message2 });
414319
+ log129.error("subscribe app failed", { instanceId: id, error: message2 });
413804
414320
  return c.json({ error: { code: "SUBSCRIBE_FAILED", message: message2 } }, 500);
413805
414321
  }
413806
414322
  });
@@ -413838,7 +414354,7 @@ var init_whatsapp_business3 = __esm(() => {
413838
414354
  try {
413839
414355
  await plugin13.disconnect(id);
413840
414356
  } catch (err2) {
413841
- log128.warn("plugin.disconnect failed during connection delete", {
414357
+ log129.warn("plugin.disconnect failed during connection delete", {
413842
414358
  instanceId: id,
413843
414359
  error: err2 instanceof Error ? err2.message : String(err2)
413844
414360
  });
@@ -414051,7 +414567,7 @@ function flowJsonInvalid(issues) {
414051
414567
  issues
414052
414568
  };
414053
414569
  }
414054
- var log129, whatsappFlowsRoutes, instanceAccess6, idParamSchema8, flowIdParamSchema, FlowCategorySchema2, createBodySchema2, updateBodySchema, sendBodySchema;
414570
+ var log130, whatsappFlowsRoutes, instanceAccess6, idParamSchema8, flowIdParamSchema, FlowCategorySchema2, createBodySchema2, updateBodySchema, sendBodySchema;
414055
414571
  var init_whatsapp_flows3 = __esm(() => {
414056
414572
  init_dist6();
414057
414573
  init_src7();
@@ -414064,7 +414580,7 @@ var init_whatsapp_flows3 = __esm(() => {
414064
414580
  init_auth();
414065
414581
  init_sealed_credentials();
414066
414582
  init_whatsapp_business3();
414067
- log129 = createLogger("routes:whatsapp-flows");
414583
+ log130 = createLogger("routes:whatsapp-flows");
414068
414584
  whatsappFlowsRoutes = new Hono2;
414069
414585
  instanceAccess6 = requireInstanceAccess((c) => c.req.param("id") ?? "");
414070
414586
  idParamSchema8 = exports_external.object({
@@ -414175,7 +414691,7 @@ var init_whatsapp_flows3 = __esm(() => {
414175
414691
  }
414176
414692
  });
414177
414693
  const status = await resolved.client.getBusinessPublicKey().catch(() => null);
414178
- log129.info("flow encryption key registered", { instanceId });
414694
+ log130.info("flow encryption key registered", { instanceId });
414179
414695
  return c.json({
414180
414696
  data: {
414181
414697
  uploaded: true,
@@ -414307,7 +414823,7 @@ var init_whatsapp_flows3 = __esm(() => {
414307
414823
  metadata: { flow: { ...flow, flowToken } }
414308
414824
  });
414309
414825
  if (!result.success) {
414310
- log129.error("flow send failed", { instanceId, to, error: result.error, errorCode: result.errorCode });
414826
+ log130.error("flow send failed", { instanceId, to, error: result.error, errorCode: result.errorCode });
414311
414827
  return c.json(jsonError2(result.error ?? "Flow send failed", result.errorCode ?? "SEND_FAILED"), 500);
414312
414828
  }
414313
414829
  return c.json({ messageId: result.messageId, flowToken }, 201);
@@ -415379,12 +415895,12 @@ async function setupEventPersistence(eventBus, db2) {
415379
415895
  const tracker = getJourneyTracker();
415380
415896
  tracker.recordCheckpoint(metadata.correlationId, "T4", JOURNEY_STAGES.T4);
415381
415897
  }
415382
- log130.debug("Persisted message.received", {
415898
+ log131.debug("Persisted message.received", {
415383
415899
  externalId: payload.externalId,
415384
415900
  instanceId: metadata.instanceId
415385
415901
  });
415386
415902
  } catch (error3) {
415387
- log130.error("Failed to persist message.received", {
415903
+ log131.error("Failed to persist message.received", {
415388
415904
  externalId: payload.externalId,
415389
415905
  error: String(error3)
415390
415906
  });
@@ -415430,12 +415946,12 @@ async function setupEventPersistence(eventBus, db2) {
415430
415946
  };
415431
415947
  await sdb.insert(omniEvents).values({ ...eventIdInsert(event.id), ...columns2 }).onConflictDoUpdate({ target: omniEvents.id, set: columns2 });
415432
415948
  });
415433
- log130.debug("Persisted message.sent", {
415949
+ log131.debug("Persisted message.sent", {
415434
415950
  externalId: payload.externalId,
415435
415951
  instanceId: metadata.instanceId
415436
415952
  });
415437
415953
  } catch (error3) {
415438
- log130.error("Failed to persist message.sent", {
415954
+ log131.error("Failed to persist message.sent", {
415439
415955
  externalId: payload.externalId,
415440
415956
  error: String(error3)
415441
415957
  });
@@ -415472,11 +415988,11 @@ async function setupEventPersistence(eventBus, db2) {
415472
415988
  await sdb.insert(omniEvents).values(newEvent).onConflictDoNothing({ target: omniEvents.id });
415473
415989
  }
415474
415990
  });
415475
- log130.debug("Persisted message.delivered", {
415991
+ log131.debug("Persisted message.delivered", {
415476
415992
  externalId: payload.externalId
415477
415993
  });
415478
415994
  } catch (error3) {
415479
- log130.error("Failed to persist message.delivered", {
415995
+ log131.error("Failed to persist message.delivered", {
415480
415996
  externalId: payload.externalId,
415481
415997
  error: String(error3)
415482
415998
  });
@@ -415512,11 +416028,11 @@ async function setupEventPersistence(eventBus, db2) {
415512
416028
  await sdb.insert(omniEvents).values(newEvent).onConflictDoNothing({ target: omniEvents.id });
415513
416029
  }
415514
416030
  });
415515
- log130.debug("Persisted message.read", {
416031
+ log131.debug("Persisted message.read", {
415516
416032
  externalId: payload.externalId
415517
416033
  });
415518
416034
  } catch (error3) {
415519
- log130.error("Failed to persist message.read", {
416035
+ log131.error("Failed to persist message.read", {
415520
416036
  externalId: payload.externalId,
415521
416037
  error: String(error3)
415522
416038
  });
@@ -415568,14 +416084,14 @@ async function setupEventPersistence(eventBus, db2) {
415568
416084
  }
415569
416085
  return updated.length;
415570
416086
  });
415571
- log130.debug("Persisted message.failed", {
416087
+ log131.debug("Persisted message.failed", {
415572
416088
  chatId: payload.chatId,
415573
416089
  externalId: payload.externalId,
415574
416090
  updatedRows,
415575
416091
  error: payload.error
415576
416092
  });
415577
416093
  } catch (error3) {
415578
- log130.error("Failed to persist message.failed", {
416094
+ log131.error("Failed to persist message.failed", {
415579
416095
  chatId: payload.chatId,
415580
416096
  error: String(error3)
415581
416097
  });
@@ -415610,9 +416126,9 @@ async function setupEventPersistence(eventBus, db2) {
415610
416126
  };
415611
416127
  await sdb.insert(omniEvents).values({ ...eventIdInsert(event.id), ...columns2 }).onConflictDoUpdate({ target: omniEvents.id, set: columns2 });
415612
416128
  });
415613
- log130.debug(`Persisted ${type}`, { messageId: payload.messageId, emoji: payload.emoji });
416129
+ log131.debug(`Persisted ${type}`, { messageId: payload.messageId, emoji: payload.emoji });
415614
416130
  } catch (error3) {
415615
- log130.error(`Failed to persist ${type}`, { messageId: payload.messageId, error: String(error3) });
416131
+ log131.error(`Failed to persist ${type}`, { messageId: payload.messageId, error: String(error3) });
415616
416132
  }
415617
416133
  }, { ...CONSUMER_OPTIONS, durable: `event-persistence-${type.replace(".", "-")}` });
415618
416134
  }
@@ -415636,9 +416152,9 @@ async function setupEventPersistence(eventBus, db2) {
415636
416152
  durable: "event-persistence-agent-run-completed",
415637
416153
  startFrom: "new"
415638
416154
  });
415639
- log130.info("Event persistence initialized - listening for message events");
416155
+ log131.info("Event persistence initialized - listening for message events");
415640
416156
  } catch (error3) {
415641
- log130.error("Failed to set up event persistence", { error: String(error3) });
416157
+ log131.error("Failed to set up event persistence", { error: String(error3) });
415642
416158
  throw error3;
415643
416159
  }
415644
416160
  }
@@ -415676,14 +416192,14 @@ async function journalInternalEvent(db2, event) {
415676
416192
  await sdb.insert(omniEvents).values(newEvent).onConflictDoNothing({ target: omniEvents.id });
415677
416193
  });
415678
416194
  } catch (error3) {
415679
- log130.error("Failed to persist custom event", {
416195
+ log131.error("Failed to persist custom event", {
415680
416196
  eventType: event.type,
415681
416197
  eventId: event.id,
415682
416198
  error: String(error3)
415683
416199
  });
415684
416200
  }
415685
416201
  }
415686
- var log130, NO_CHAT_LINK, CONSUMER_OPTIONS;
416202
+ var log131, NO_CHAT_LINK, CONSUMER_OPTIONS;
415687
416203
  var init_event_persistence = __esm(() => {
415688
416204
  init_src();
415689
416205
  init_src6();
@@ -415691,7 +416207,7 @@ var init_event_persistence = __esm(() => {
415691
416207
  init_chats();
415692
416208
  init_tenant_scope();
415693
416209
  init_worker_tenant_context();
415694
- log130 = createLogger("event-persistence");
416210
+ log131 = createLogger("event-persistence");
415695
416211
  NO_CHAT_LINK = { chatUuid: null, canonicalChatId: null };
415696
416212
  CONSUMER_OPTIONS = {
415697
416213
  queue: "event-persistence",
@@ -415702,472 +416218,6 @@ var init_event_persistence = __esm(() => {
415702
416218
  };
415703
416219
  });
415704
416220
 
415705
- // ../api/src/plugins/media-processor.ts
415706
- function getContentFieldForType(processingType, contentType) {
415707
- switch (processingType) {
415708
- case "transcription":
415709
- return "transcription";
415710
- case "description":
415711
- return contentType === "video" ? "videoDescription" : "imageDescription";
415712
- case "extraction":
415713
- return "documentExtraction";
415714
- default:
415715
- return;
415716
- }
415717
- }
415718
- function inferProcessingType(contentType) {
415719
- switch (contentType) {
415720
- case "audio":
415721
- return "transcription";
415722
- case "image":
415723
- case "video":
415724
- return "description";
415725
- case "document":
415726
- return "extraction";
415727
- default:
415728
- return "transcription";
415729
- }
415730
- }
415731
- function shouldProcess(contentType) {
415732
- if (!contentType)
415733
- return false;
415734
- return PROCESSABLE_MEDIA_TYPES.has(contentType);
415735
- }
415736
- function isUuid(value) {
415737
- return typeof value === "string" && isValidUuid(value);
415738
- }
415739
- function envelopeTenantId(envelope2) {
415740
- const classification = classifyEnvelope(envelope2.metadata);
415741
- return classification.world === "tenant" ? classification.tenantId : undefined;
415742
- }
415743
- function runMediaDb(ctx, trustedTenantId, fn) {
415744
- return trustedTenantId === undefined ? fn() : runInWorkerTenantScope(ctx.db, trustedTenantId, fn);
415745
- }
415746
- function getMimeType2(content) {
415747
- if (content.mimeType)
415748
- return content.mimeType;
415749
- switch (content.type) {
415750
- case "audio":
415751
- return "audio/ogg";
415752
- case "image":
415753
- return "image/jpeg";
415754
- case "video":
415755
- return "video/mp4";
415756
- case "document":
415757
- return "application/octet-stream";
415758
- default:
415759
- return;
415760
- }
415761
- }
415762
- function selectSlackDownloadToken(instance4) {
415763
- const slackAuthMode = instance4.slackAuthMode;
415764
- const slackUserToken = instance4.slackUserToken;
415765
- if (slackAuthMode === "user" && typeof slackUserToken === "string" && slackUserToken.length > 0) {
415766
- return slackUserToken;
415767
- }
415768
- return instance4.slackBotToken;
415769
- }
415770
- async function buildFetchOptions2(ctx, instanceId, channelType, trustedTenantId) {
415771
- if (channelType !== "slack")
415772
- return;
415773
- try {
415774
- const instance4 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.instances.getById(instanceId));
415775
- const token = selectSlackDownloadToken(instance4);
415776
- if (token) {
415777
- return { headers: { Authorization: `Bearer ${token}` } };
415778
- }
415779
- } catch {}
415780
- return;
415781
- }
415782
- async function downloadMediaFromUrl(ctx, instanceId, messageId, mediaUrl, mimeType, platformTimestamp, channelType, trustedTenantId) {
415783
- const fetchOptions = await buildFetchOptions2(ctx, instanceId, channelType, trustedTenantId);
415784
- try {
415785
- const result = await ctx.mediaStorage.storeFromUrl(instanceId, messageId, mediaUrl, mimeType, platformTimestamp, fetchOptions, trustedTenantId);
415786
- await runMediaDb(ctx, trustedTenantId, () => ctx.mediaStorage.updateMessageLocalPath(messageId, result.localPath));
415787
- log131.debug("Downloaded media from URL", { messageId, filePath: result.localPath });
415788
- return result.localPath;
415789
- } catch (error3) {
415790
- log131.error("Failed to download media", { error: String(error3), mediaUrl });
415791
- return null;
415792
- }
415793
- }
415794
- async function downloadMediaViaPlugin(ctx, instanceId, messageId, channelType, mediaRef, fallbackMimeType, platformTimestamp, trustedTenantId) {
415795
- const plugin13 = await (ctx.resolveChannelPlugin ?? getPlugin)(channelType);
415796
- if (!plugin13?.downloadInboundMedia) {
415797
- log131.warn("Channel plugin cannot materialize deferred media", { channelType, messageId });
415798
- return null;
415799
- }
415800
- try {
415801
- const { buffer: buffer3, mimeType } = pluginMediaSchema.parse(await plugin13.downloadInboundMedia(instanceId, mediaRef));
415802
- const result = await ctx.mediaStorage.storeFromBuffer(instanceId, messageId, buffer3, mimeType || fallbackMimeType, platformTimestamp, trustedTenantId);
415803
- await runMediaDb(ctx, trustedTenantId, () => ctx.mediaStorage.updateMessageLocalPath(messageId, result.localPath));
415804
- log131.debug("Downloaded inbound media via plugin", { messageId, channelType, filePath: result.localPath });
415805
- return result.localPath;
415806
- } catch (error3) {
415807
- log131.error("Failed to download inbound media via plugin", {
415808
- error: String(error3),
415809
- channelType,
415810
- messageId
415811
- });
415812
- return null;
415813
- }
415814
- }
415815
- async function resolveMediaPath2(ctx, instanceId, chatId, externalId, content, mimeType, channelType, trustedTenantId) {
415816
- const maxWaitMs = 5000;
415817
- const pollMs = 250;
415818
- const deadline = Date.now() + maxWaitMs;
415819
- let chat2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.chats.findByExternalIdSmart(instanceId, chatId));
415820
- while (!chat2 && Date.now() < deadline) {
415821
- await new Promise((r) => setTimeout(r, pollMs));
415822
- chat2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.chats.findByExternalIdSmart(instanceId, chatId));
415823
- }
415824
- if (!chat2) {
415825
- log131.debug("Chat not found, cannot process media", { chatId, externalId });
415826
- return null;
415827
- }
415828
- const chatDbId = chat2.id;
415829
- let message2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.messages.getByExternalId(chatDbId, externalId));
415830
- while (!message2 && Date.now() < deadline) {
415831
- await new Promise((r) => setTimeout(r, pollMs));
415832
- message2 = await runMediaDb(ctx, trustedTenantId, () => ctx.services.messages.getByExternalId(chatDbId, externalId));
415833
- }
415834
- if (!message2) {
415835
- log131.debug("Message not found after waiting, cannot process media", { externalId });
415836
- return null;
415837
- }
415838
- let filePath = message2.mediaLocalPath;
415839
- if (!filePath && content.mediaUrl) {
415840
- filePath = await downloadMediaFromUrl(ctx, instanceId, message2.id, content.mediaUrl, mimeType, message2.platformTimestamp ?? undefined, channelType, trustedTenantId);
415841
- if (!filePath)
415842
- return null;
415843
- }
415844
- if (!filePath && content.mediaId && channelType) {
415845
- filePath = await downloadMediaViaPlugin(ctx, instanceId, message2.id, channelType, content.mediaId, mimeType, message2.platformTimestamp ?? undefined, trustedTenantId);
415846
- if (!filePath)
415847
- return null;
415848
- }
415849
- if (!filePath) {
415850
- log131.debug("No media file path available", { externalId });
415851
- return null;
415852
- }
415853
- return {
415854
- messageId: message2.id,
415855
- filePath
415856
- };
415857
- }
415858
- async function resolveSafeMediaContentEventId(ctx, envelope2, eventId) {
415859
- if (!isUuid(eventId))
415860
- return null;
415861
- const id = eventId;
415862
- const maxWaitMs = 250;
415863
- const pollMs = 50;
415864
- const deadline = Date.now() + maxWaitMs;
415865
- while (true) {
415866
- let found;
415867
- try {
415868
- found = await runConsumerInTenantContext(ctx.db, envelope2, async () => {
415869
- const [event] = await scopedHandle(ctx.db).select({ id: omniEvents.id }).from(omniEvents).where(eq(omniEvents.id, id)).limit(1);
415870
- return event?.id ?? null;
415871
- });
415872
- } catch (error3) {
415873
- log131.debug("Failed to validate media_content event FK", { eventId: id, error: String(error3) });
415874
- return null;
415875
- }
415876
- if (found)
415877
- return found;
415878
- if (Date.now() >= deadline) {
415879
- log131.debug("Skipping media_content event FK; omni_event not found", { eventId: id });
415880
- return null;
415881
- }
415882
- await new Promise((resolve4) => setTimeout(resolve4, pollMs));
415883
- }
415884
- }
415885
- async function persistProcessingResult(ctx, messageId, safeEventId, result, contentType) {
415886
- if (result.content) {
415887
- const updateField = getContentFieldForType(result.processingType, contentType);
415888
- if (updateField) {
415889
- await scopedHandle(ctx.db).update(messages2).set({ [updateField]: result.content }).where(eq(messages2.id, messageId));
415890
- }
415891
- }
415892
- try {
415893
- const sdb = scopedHandle(ctx.db);
415894
- const row = {
415895
- eventId: safeEventId ?? null,
415896
- mediaId: messageId,
415897
- processingType: result.processingType,
415898
- content: result.content ?? "",
415899
- model: result.model,
415900
- provider: result.provider,
415901
- language: result.language,
415902
- duration: result.duration,
415903
- tokensUsed: result.inputTokens ? result.inputTokens + (result.outputTokens ?? 0) : undefined,
415904
- costUsd: result.costCents != null ? String(Math.round(result.costCents)) : null,
415905
- processingTimeMs: result.processingTimeMs
415906
- };
415907
- if (currentTenantScope()) {
415908
- await sdb.transaction(async (sp) => {
415909
- await sp.insert(mediaContent).values(row);
415910
- });
415911
- } else {
415912
- await sdb.insert(mediaContent).values(row);
415913
- }
415914
- } catch (error3) {
415915
- log131.warn("Failed to insert media_content record (non-critical)", {
415916
- messageId,
415917
- error: String(error3)
415918
- });
415919
- }
415920
- }
415921
- function getPromptOverride(ctx, contentType) {
415922
- if (contentType === "audio")
415923
- return ctx.promptOverrides.audio;
415924
- if (contentType === "image")
415925
- return ctx.promptOverrides.image;
415926
- if (contentType === "video")
415927
- return ctx.promptOverrides.video;
415928
- if (contentType === "document")
415929
- return ctx.promptOverrides.document;
415930
- return;
415931
- }
415932
- async function processMessageMedia(ctx, payload, metadata, envelope2) {
415933
- const { instanceId, eventId } = metadata;
415934
- const { content, externalId } = payload;
415935
- const mimeType = getMimeType2(content);
415936
- const trustedTenantId = envelopeTenantId(envelope2);
415937
- if (!mimeType || !ctx.mediaService.canProcess(mimeType)) {
415938
- log131.debug("MIME type not processable or missing", { mimeType, externalId });
415939
- return;
415940
- }
415941
- const media = await resolveMediaPath2(ctx, instanceId, payload.chatId, externalId, content, mimeType, metadata.channelType, trustedTenantId);
415942
- if (!media)
415943
- return;
415944
- const processable = await ctx.mediaStorage.materializeForProcessing(media.filePath);
415945
- log131.info("Processing media", { messageId: media.messageId, mimeType, filePath: processable.path });
415946
- let result;
415947
- try {
415948
- result = await ctx.mediaService.process(processable.path, mimeType, {
415949
- language: ctx.defaultLanguage,
415950
- caption: content.text,
415951
- prompt: getPromptOverride(ctx, content.type)
415952
- });
415953
- } finally {
415954
- await processable.cleanup();
415955
- }
415956
- if (!result.success) {
415957
- const reason2 = result.errorMessage ?? "unknown";
415958
- log131.warn("Media processing failed", { messageId: media.messageId, error: reason2 });
415959
- const processingType = result.processingType ?? inferProcessingType(content.type);
415960
- const errorColumn = getContentFieldForType(processingType, content.type);
415961
- if (errorColumn) {
415962
- const marker = `[error: media processing failed \u2014 ${reason2}]`;
415963
- await runConsumerInTenantContext(ctx.db, envelope2, async () => {
415964
- await scopedHandle(ctx.db).update(messages2).set({ [errorColumn]: marker }).where(eq(messages2.id, media.messageId));
415965
- });
415966
- }
415967
- await ctx.eventBus.publish("media.processing.failed", {
415968
- eventId: eventId ?? media.messageId,
415969
- mediaId: media.messageId,
415970
- processingType,
415971
- error: reason2,
415972
- provider: result.provider,
415973
- model: result.model
415974
- }, { instanceId, channelType: metadata.channelType });
415975
- await ctx.eventBus.publish("media.processed", {
415976
- eventId: eventId ?? media.messageId,
415977
- mediaId: media.messageId,
415978
- processingType,
415979
- content: "",
415980
- error: reason2
415981
- }, { instanceId, channelType: metadata.channelType });
415982
- return;
415983
- }
415984
- const safeEventId = await resolveSafeMediaContentEventId(ctx, envelope2, eventId);
415985
- await runConsumerInTenantContext(ctx.db, envelope2, () => persistProcessingResult(ctx, media.messageId, safeEventId, result, content.type));
415986
- log131.info("Media processing complete", {
415987
- messageId: media.messageId,
415988
- processingType: result.processingType,
415989
- provider: result.provider,
415990
- model: result.model,
415991
- costCents: result.costCents,
415992
- processingTimeMs: result.processingTimeMs
415993
- });
415994
- await ctx.eventBus.publish("media.processed", {
415995
- eventId: eventId ?? media.messageId,
415996
- mediaId: media.messageId,
415997
- processingType: result.processingType,
415998
- content: result.content ?? "",
415999
- model: result.model,
416000
- provider: result.provider,
416001
- tokensUsed: result.inputTokens ? result.inputTokens + (result.outputTokens ?? 0) : undefined
416002
- }, { instanceId, channelType: metadata.channelType });
416003
- }
416004
- async function publishMediaCrashEvents(ctx, eventId, payload, metadata, error3, trustedTenantId) {
416005
- try {
416006
- const processingType = inferProcessingType(payload.content.type);
416007
- const reason2 = `unexpected: ${String(error3)}`;
416008
- const mediaId = await resolveMediaIdForCrash(ctx, metadata.instanceId ?? "", payload, trustedTenantId);
416009
- const meta = { instanceId: metadata.instanceId, channelType: metadata.channelType };
416010
- await ctx.eventBus.publish("media.processing.failed", {
416011
- eventId,
416012
- mediaId,
416013
- processingType,
416014
- error: reason2,
416015
- provider: "unknown",
416016
- model: "unknown"
416017
- }, meta);
416018
- await ctx.eventBus.publish("media.processed", {
416019
- eventId,
416020
- mediaId,
416021
- processingType,
416022
- content: "",
416023
- error: reason2
416024
- }, meta);
416025
- } catch (publishError) {
416026
- log131.error("Failed to publish media failure event", { error: String(publishError) });
416027
- }
416028
- }
416029
- async function resolveMediaIdForCrash(ctx, instanceId, payload, trustedTenantId) {
416030
- try {
416031
- const msg = await runMediaDb(ctx, trustedTenantId, async () => {
416032
- const chat2 = await ctx.services.chats.findByExternalIdSmart(instanceId, payload.chatId);
416033
- if (!chat2)
416034
- return null;
416035
- return ctx.services.messages.getByExternalId(chat2.id, payload.externalId);
416036
- });
416037
- if (msg)
416038
- return msg.id;
416039
- } catch (lookupError) {
416040
- log131.debug("Failed to resolve DB message UUID for crash handler, falling back to externalId", {
416041
- error: String(lookupError),
416042
- instanceId,
416043
- chatId: payload.chatId
416044
- });
416045
- }
416046
- return payload.externalId;
416047
- }
416048
- async function setupMediaProcessor(eventBus, db2, services) {
416049
- const [
416050
- groqApiKey,
416051
- openaiApiKey,
416052
- geminiApiKey,
416053
- defaultLanguage,
416054
- audioProvider,
416055
- audioModel,
416056
- geminiAudioModel,
416057
- audioPrompt,
416058
- imagePrompt,
416059
- videoPrompt,
416060
- documentPrompt,
416061
- geminiVisionModel,
416062
- openaiVisionModel
416063
- ] = await Promise.all([
416064
- services.settings.getSecret("groq.api_key", "GROQ_API_KEY"),
416065
- services.settings.getSecret("openai.api_key", "OPENAI_API_KEY"),
416066
- services.settings.getSecret("gemini.api_key", "GEMINI_API_KEY"),
416067
- services.settings.getString("media.default_language", "DEFAULT_LANGUAGE", "pt"),
416068
- services.settings.getString("stt.provider", "STT_PROVIDER", "openai"),
416069
- services.settings.getString("stt.openai.model", "OPENAI_STT_MODEL", OPENAI_TRANSCRIBE_MODEL),
416070
- services.settings.getString("stt.gemini.model", "GEMINI_STT_MODEL", GEMINI_AUDIO_MODEL),
416071
- services.settings.getString("prompt.audio_transcription"),
416072
- services.settings.getString("prompt.image_description"),
416073
- services.settings.getString("prompt.video_description"),
416074
- services.settings.getString("prompt.document_ocr"),
416075
- services.settings.getString("media.gemini.model", "GEMINI_MEDIA_MODEL", GEMINI_MODEL),
416076
- services.settings.getString("media.openai.vision_model", "OPENAI_VISION_MODEL", OPENAI_VISION_MODEL)
416077
- ]);
416078
- const mediaService = createMediaProcessingService({
416079
- groqApiKey,
416080
- openaiApiKey,
416081
- geminiApiKey,
416082
- defaultLanguage,
416083
- audioProvider: audioProvider ?? "openai",
416084
- audioModel: audioModel ?? OPENAI_TRANSCRIBE_MODEL,
416085
- geminiAudioModel: geminiAudioModel ?? GEMINI_AUDIO_MODEL,
416086
- geminiVisionModel: geminiVisionModel ?? GEMINI_MODEL,
416087
- openaiVisionModel: openaiVisionModel ?? OPENAI_VISION_MODEL,
416088
- audioPrompt: audioPrompt ?? undefined
416089
- });
416090
- const mediaStorage2 = new MediaStorageService(db2);
416091
- const ctx = {
416092
- db: db2,
416093
- eventBus,
416094
- services,
416095
- mediaService,
416096
- mediaStorage: mediaStorage2,
416097
- defaultLanguage: defaultLanguage ?? "pt",
416098
- promptOverrides: {
416099
- audio: audioPrompt,
416100
- image: imagePrompt,
416101
- video: videoPrompt,
416102
- document: documentPrompt
416103
- }
416104
- };
416105
- await eventBus.subscribe("message.received", async (event) => {
416106
- const payload = event.payload;
416107
- const metadata = event.metadata;
416108
- if (!metadata.instanceId)
416109
- return;
416110
- if (!shouldProcess(payload.content.type))
416111
- return;
416112
- try {
416113
- await processMessageMedia(ctx, payload, {
416114
- instanceId: metadata.instanceId,
416115
- eventId: event.id,
416116
- channelType: metadata.channelType
416117
- }, event);
416118
- } catch (error3) {
416119
- log131.error("Failed to process media", {
416120
- externalId: payload.externalId,
416121
- error: String(error3)
416122
- });
416123
- await publishMediaCrashEvents(ctx, event.id, payload, metadata, error3, envelopeTenantId(event));
416124
- }
416125
- }, {
416126
- durable: "media-processor",
416127
- queue: "media-processor",
416128
- maxRetries: 2,
416129
- retryDelayMs: 1000,
416130
- startFrom: "first",
416131
- concurrency: 5
416132
- });
416133
- setGlobalCircuitBreakerStateChangeCallback((name, from, to) => {
416134
- log131.warn("Circuit breaker state change", { provider: name, from, to });
416135
- });
416136
- await eventBus.subscribePattern("media.health.request", async (event) => {
416137
- const tracker = getMediaHealthTracker();
416138
- const report = tracker.getReport();
416139
- const correlationId = event.metadata?.correlationId ?? event.id;
416140
- await eventBus.publishGeneric("system.media.health.response", { correlationId, report }, { source: "media-processor" });
416141
- log131.debug("Published media health report", {
416142
- correlationId,
416143
- totalRequests: report.overall.totalRequests,
416144
- successRate: report.overall.successRate
416145
- });
416146
- }, {
416147
- durable: "media-health-responder",
416148
- queue: "media-health-responder"
416149
- });
416150
- log131.info("Media processor initialized");
416151
- }
416152
- var log131, PROCESSABLE_MEDIA_TYPES, pluginMediaSchema;
416153
- var init_media_processor = __esm(() => {
416154
- init_src();
416155
- init_src6();
416156
- init_src9();
416157
- init_drizzle_orm();
416158
- init_zod();
416159
- init_media_storage();
416160
- init_tenant_scope();
416161
- init_worker_tenant_context();
416162
- init_loader2();
416163
- log131 = createLogger("media-processor");
416164
- PROCESSABLE_MEDIA_TYPES = new Set(["audio", "image", "document", "video"]);
416165
- pluginMediaSchema = exports_external.object({
416166
- buffer: exports_external.instanceof(Buffer),
416167
- mimeType: exports_external.string().min(1).optional()
416168
- });
416169
- });
416170
-
416171
416221
  // ../api/src/tenancy/inflight-revocation.ts
416172
416222
  function resolveInflightRecheckIntervalMs() {
416173
416223
  return Math.floor(INFLIGHT_REVOCATION_CEILING_SECONDS * 1000 / 2);