@opengeni/api-router 2.1.0-canary.0 → 2.2.0-canary.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.
@@ -26394,6 +26394,8 @@ import {
26394
26394
  DocumentAuthorityReclassification,
26395
26395
  DocumentBase,
26396
26396
  DocumentDefaultCollectionBackfill,
26397
+ DocumentDefaultCollectionBackfillAudit,
26398
+ GetDocumentDefaultCollectionBackfillAuditQuery,
26397
26399
  DocumentSearchRequest,
26398
26400
  DocumentSearchResponse,
26399
26401
  FileAsset as FileAsset2,
@@ -26402,6 +26404,9 @@ import {
26402
26404
  KnowledgeMemorySearchRequest,
26403
26405
  ListDocumentAuthorityReclassificationsQuery,
26404
26406
  ListDocumentAuthorityReclassificationsResponse,
26407
+ ListDocumentDefaultCollectionBackfillRunsResponse,
26408
+ ListDocumentMigrationAuditQuery,
26409
+ ListOrganizationDocumentAuthorityReclassificationsResponse,
26405
26410
  MoveDocumentRequest,
26406
26411
  ReclassifyDocumentAuthorityRequest,
26407
26412
  RunDocumentDefaultCollectionBackfillRequest,
@@ -26427,10 +26432,13 @@ import {
26427
26432
  getDocument,
26428
26433
  getDocumentOriginalFile,
26429
26434
  getDocumentBase,
26435
+ getDocumentDefaultCollectionBackfillAudit,
26430
26436
  listAccessibleDocuments,
26431
26437
  listDocumentAuthorityReclassifications,
26438
+ listDocumentDefaultCollectionBackfillRuns,
26432
26439
  listDocumentBasesEnsuringDefault,
26433
26440
  listDocuments,
26441
+ listOrganizationDocumentAuthorityReclassifications,
26434
26442
  moveDocumentToBase,
26435
26443
  queueDocumentForReindex,
26436
26444
  reclassifyDocumentAuthority,
@@ -26886,6 +26894,103 @@ function registerDocumentRoutes(app, deps) {
26886
26894
  throw documentHttpException(error);
26887
26895
  }
26888
26896
  });
26897
+ app.get("/v1/workspaces/:workspaceId/document-default-collection-backfills", async (c) => {
26898
+ const workspaceId = c.req.param("workspaceId");
26899
+ const authorization = await requireAccessGrantAuthorization2(
26900
+ c,
26901
+ deps,
26902
+ workspaceId,
26903
+ "documents:manage"
26904
+ );
26905
+ if (!hasAccountAdminAuthority(authorization)) {
26906
+ throw new HTTPException26(403, { message: "missing permission: account:admin" });
26907
+ }
26908
+ const query = ListDocumentMigrationAuditQuery.safeParse(c.req.query());
26909
+ if (!query.success) {
26910
+ throw new HTTPException26(400, { message: "invalid document migration audit query" });
26911
+ }
26912
+ try {
26913
+ return c.json(
26914
+ ListDocumentDefaultCollectionBackfillRunsResponse.parse(
26915
+ await listDocumentDefaultCollectionBackfillRuns(db, {
26916
+ accountId: authorization.grant.accountId,
26917
+ workspaceId,
26918
+ actorSubjectId: authorization.grant.subjectId,
26919
+ accountAdminAuthorization: requireAccountAdminAuthorizationStamp(authorization),
26920
+ ...query.data
26921
+ })
26922
+ )
26923
+ );
26924
+ } catch (error) {
26925
+ throw documentHttpException(error);
26926
+ }
26927
+ });
26928
+ app.get("/v1/workspaces/:workspaceId/document-default-collection-backfills/:runId", async (c) => {
26929
+ const workspaceId = c.req.param("workspaceId");
26930
+ const authorization = await requireAccessGrantAuthorization2(
26931
+ c,
26932
+ deps,
26933
+ workspaceId,
26934
+ "documents:manage"
26935
+ );
26936
+ if (!hasAccountAdminAuthority(authorization)) {
26937
+ throw new HTTPException26(403, { message: "missing permission: account:admin" });
26938
+ }
26939
+ const runId = RunDocumentDefaultCollectionBackfillRequest.shape.runId.safeParse(
26940
+ c.req.param("runId")
26941
+ );
26942
+ const query = GetDocumentDefaultCollectionBackfillAuditQuery.safeParse(c.req.query());
26943
+ if (!runId.success || !query.success) {
26944
+ throw new HTTPException26(400, { message: "invalid document migration audit query" });
26945
+ }
26946
+ try {
26947
+ return c.json(
26948
+ DocumentDefaultCollectionBackfillAudit.parse(
26949
+ await getDocumentDefaultCollectionBackfillAudit(db, {
26950
+ accountId: authorization.grant.accountId,
26951
+ workspaceId,
26952
+ actorSubjectId: authorization.grant.subjectId,
26953
+ accountAdminAuthorization: requireAccountAdminAuthorizationStamp(authorization),
26954
+ runId: runId.data,
26955
+ ...query.data
26956
+ })
26957
+ )
26958
+ );
26959
+ } catch (error) {
26960
+ throw documentHttpException(error);
26961
+ }
26962
+ });
26963
+ app.get("/v1/workspaces/:workspaceId/document-authority-reclassifications", async (c) => {
26964
+ const workspaceId = c.req.param("workspaceId");
26965
+ const authorization = await requireAccessGrantAuthorization2(
26966
+ c,
26967
+ deps,
26968
+ workspaceId,
26969
+ "documents:manage"
26970
+ );
26971
+ if (!hasAccountAdminAuthority(authorization)) {
26972
+ throw new HTTPException26(403, { message: "missing permission: account:admin" });
26973
+ }
26974
+ const query = ListDocumentMigrationAuditQuery.safeParse(c.req.query());
26975
+ if (!query.success) {
26976
+ throw new HTTPException26(400, { message: "invalid document migration audit query" });
26977
+ }
26978
+ try {
26979
+ return c.json(
26980
+ ListOrganizationDocumentAuthorityReclassificationsResponse.parse(
26981
+ await listOrganizationDocumentAuthorityReclassifications(db, {
26982
+ accountId: authorization.grant.accountId,
26983
+ workspaceId,
26984
+ actorSubjectId: authorization.grant.subjectId,
26985
+ accountAdminAuthorization: requireAccountAdminAuthorizationStamp(authorization),
26986
+ ...query.data
26987
+ })
26988
+ )
26989
+ );
26990
+ } catch (error) {
26991
+ throw documentHttpException(error);
26992
+ }
26993
+ });
26889
26994
  app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
26890
26995
  const workspaceId = c.req.param("workspaceId");
26891
26996
  const access = await requireAccessGrantAuthorization2(c, deps, workspaceId, "documents:manage");
@@ -27569,23 +27674,26 @@ function dropFilename(preferred) {
27569
27674
  return /\.[A-Za-z0-9]{1,8}$/.test(stem) ? stem : `${stem}.txt`;
27570
27675
  }
27571
27676
  function documentHttpException(error) {
27572
- const message = error instanceof Error ? error.message : String(error);
27677
+ const message = documentDomainErrorMessage(error);
27573
27678
  if (message.includes("organization document") && message.includes("exact account authority")) {
27574
27679
  return new HTTPException26(403, { message: "missing permission: account:admin" });
27575
27680
  }
27576
27681
  if (message.includes("not found")) {
27577
27682
  return new HTTPException26(404, { message });
27578
27683
  }
27684
+ if (message.includes("document Default collection backfill audit run is unavailable")) {
27685
+ return new HTTPException26(404, { message });
27686
+ }
27579
27687
  if (message.includes("already exists")) {
27580
27688
  return new HTTPException26(409, { message });
27581
27689
  }
27582
27690
  if (message.includes("operation id was reused") || message.includes("authority changed before reclassification") || message.includes("run identity mismatch")) {
27583
27691
  return new HTTPException26(409, { message });
27584
27692
  }
27585
- if (message.includes("reclassification requires") || message.includes("backfill requires organization administration")) {
27693
+ if (message.includes("reclassification requires") || message.includes("backfill requires organization administration") || message.includes("document migration audit requires organization administration") || message.includes("document migration audit scope is invalid")) {
27586
27694
  return new HTTPException26(403, { message });
27587
27695
  }
27588
- if (message.includes("reclassification input is invalid") || message.includes("reclassification authority is invalid") || message.includes("invalid document authority receipt cursor") || message.includes("document authority receipt limit")) {
27696
+ if (message.includes("reclassification input is invalid") || message.includes("reclassification authority is invalid") || message.includes("invalid document authority receipt cursor") || message.includes("document authority receipt limit") || message.includes("invalid document migration audit cursor") || message.includes("document migration audit cursor is invalid") || message.includes("document migration audit limit") || message.includes("document migration audit authority is incomplete") || message.includes("document Default collection backfill audit run id is required")) {
27589
27697
  return new HTTPException26(400, { message });
27590
27698
  }
27591
27699
  if (message.includes("no suggested base")) {
@@ -27599,6 +27707,18 @@ function documentHttpException(error) {
27599
27707
  }
27600
27708
  return new HTTPException26(500, { message });
27601
27709
  }
27710
+ function documentDomainErrorMessage(error) {
27711
+ let current = error;
27712
+ let message = error instanceof Error ? error.message : String(error);
27713
+ const seen = /* @__PURE__ */ new Set();
27714
+ while (current && typeof current === "object" && !seen.has(current) && seen.size < 8) {
27715
+ seen.add(current);
27716
+ const record7 = current;
27717
+ if (typeof record7.message === "string") message = record7.message;
27718
+ current = record7.cause;
27719
+ }
27720
+ return message;
27721
+ }
27602
27722
  function hasAccountAdminAuthority(authorization) {
27603
27723
  return authorization.accountGrant?.permissions.includes("account:admin") === true;
27604
27724
  }
@@ -55146,7 +55266,9 @@ import {
55146
55266
  evaluateSlackTaskPolicy,
55147
55267
  resolveWorkspaceSlackOrchestrationNoticeSettings,
55148
55268
  resolveWorkspaceSlackReactionSummonSettings,
55269
+ SlackChannelRouteListResponse,
55149
55270
  SlackReactionChannelListResponse,
55271
+ UpdateSlackChannelRoutesRequest,
55150
55272
  SlackUserLinkAccessMutationRequest,
55151
55273
  SlackUserLinkAccessRequest,
55152
55274
  workspaceSlackReactionChannelAllowed
@@ -55188,7 +55310,14 @@ import {
55188
55310
  getSlackInteractionById,
55189
55311
  getSlackInteractionByConnectionRoute,
55190
55312
  probeSlackActionHandleTenancy,
55313
+ openSlackRoutePrompt,
55314
+ getSlackRoutePromptByOption,
55315
+ settleSlackRoutePrompt,
55316
+ answerSlackRoutePrompt,
55191
55317
  getSlackChannelRoute,
55318
+ listSlackChannelRoutes,
55319
+ upsertSlackChannelRoute,
55320
+ deleteSlackChannelRoute,
55192
55321
  getSlackUserDmRoute,
55193
55322
  listNamedSubjectSlackRoutableWorkspaces,
55194
55323
  getActiveSlackTaskPolicy,
@@ -55196,11 +55325,14 @@ import {
55196
55325
  getSessionEventByClientEventId,
55197
55326
  getWorkspace as getWorkspace6,
55198
55327
  getWorkspaceGrant as getWorkspaceGrant9,
55328
+ namedSubjectPersonalWorkspaceId,
55199
55329
  resolveSlackTargetAuthority,
55200
55330
  listSlackInteractionProgressDeliveryEvidence,
55201
55331
  listSessionEventPage as listSessionEventPage3,
55202
55332
  listSessionHumanInputRequests as listSessionHumanInputRequests2,
55203
55333
  listPendingSlackUserLinkAccessRequests,
55334
+ listSlackInstallationBindings as listSlackInstallationBindings2,
55335
+ slackUserLinkAccessRequestTeam,
55204
55336
  rekeySlackInteractionRoute,
55205
55337
  reopenSlackInteractionDelivery,
55206
55338
  reserveSlackInteractionActionHandles,
@@ -55273,11 +55405,13 @@ function labelFor(candidates, workspaceId) {
55273
55405
  return candidates.find((candidate) => candidate.workspaceId === workspaceId)?.label ?? null;
55274
55406
  }
55275
55407
  function resolveSlackWorkspaceRoute(input) {
55408
+ const directMessage = isSlackDirectMessageConversation(input.entry);
55409
+ const candidates = directMessage ? input.candidates : input.candidates.filter((candidate) => !candidate.personal);
55276
55410
  const installation = {
55277
55411
  kind: "resolved",
55278
55412
  accountId: input.home.accountId,
55279
55413
  workspaceId: input.home.workspaceId,
55280
- label: labelFor(input.candidates, input.home.workspaceId),
55414
+ label: labelFor(candidates, input.home.workspaceId),
55281
55415
  source: "installation"
55282
55416
  };
55283
55417
  if (!input.routingEnabled) {
@@ -55294,19 +55428,19 @@ function resolveSlackWorkspaceRoute(input) {
55294
55428
  kind: "resolved",
55295
55429
  accountId: input.threadTenancy.accountId,
55296
55430
  workspaceId: input.threadTenancy.workspaceId,
55297
- label: labelFor(input.candidates, input.threadTenancy.workspaceId),
55431
+ label: labelFor(candidates, input.threadTenancy.workspaceId),
55298
55432
  source: "thread"
55299
55433
  };
55300
55434
  }
55301
55435
  const prefix = AUTHORED_BY_INVOKER.has(input.entry.triggerKind) ? parseSlackWorkspacePrefix(splitSlackLeadingMention(input.entry.text, input.botUserId).rest) : null;
55302
55436
  if (prefix) {
55303
- const named = matchCandidate(input.candidates, prefix.requested);
55437
+ const named = matchCandidate(candidates, prefix.requested);
55304
55438
  if (named === "ambiguous" || !named) {
55305
55439
  return {
55306
55440
  kind: "denied",
55307
55441
  reason: "no_access_to_named",
55308
55442
  requested: prefix.requested,
55309
- candidates: input.candidates
55443
+ candidates
55310
55444
  };
55311
55445
  }
55312
55446
  return {
@@ -55322,17 +55456,17 @@ function resolveSlackWorkspaceRoute(input) {
55322
55456
  kind: "resolved",
55323
55457
  accountId: input.channelRoute.targetAccountId,
55324
55458
  workspaceId: input.channelRoute.targetWorkspaceId,
55325
- label: labelFor(input.candidates, input.channelRoute.targetWorkspaceId),
55459
+ label: labelFor(candidates, input.channelRoute.targetWorkspaceId),
55326
55460
  source: "channel"
55327
55461
  };
55328
55462
  }
55329
- if (isSlackDirectMessageConversation(input.entry)) {
55463
+ if (directMessage) {
55330
55464
  if (input.dmRoute) {
55331
55465
  return {
55332
55466
  kind: "resolved",
55333
55467
  accountId: input.dmRoute.targetAccountId,
55334
55468
  workspaceId: input.dmRoute.targetWorkspaceId,
55335
- label: labelFor(input.candidates, input.dmRoute.targetWorkspaceId),
55469
+ label: labelFor(candidates, input.dmRoute.targetWorkspaceId),
55336
55470
  source: "dm_route"
55337
55471
  };
55338
55472
  }
@@ -55341,13 +55475,13 @@ function resolveSlackWorkspaceRoute(input) {
55341
55475
  kind: "resolved",
55342
55476
  accountId: input.home.accountId,
55343
55477
  workspaceId: input.personalWorkspaceId,
55344
- label: labelFor(input.candidates, input.personalWorkspaceId),
55478
+ label: labelFor(candidates, input.personalWorkspaceId),
55345
55479
  source: "dm_personal"
55346
55480
  };
55347
55481
  }
55348
55482
  }
55349
- const sole = input.candidates[0];
55350
- if (input.candidates.length === 1 && sole) {
55483
+ const sole = candidates[0];
55484
+ if (candidates.length === 1 && sole) {
55351
55485
  return {
55352
55486
  kind: "resolved",
55353
55487
  accountId: sole.accountId,
@@ -55356,10 +55490,10 @@ function resolveSlackWorkspaceRoute(input) {
55356
55490
  source: "sole_candidate"
55357
55491
  };
55358
55492
  }
55359
- if (input.candidates.length === 0) {
55493
+ if (candidates.length === 0) {
55360
55494
  return { kind: "denied", reason: "no_candidates", requested: null, candidates: [] };
55361
55495
  }
55362
- return input.askEnabled ? { kind: "ask", candidates: input.candidates } : installation;
55496
+ return input.askEnabled ? { kind: "ask", candidates } : installation;
55363
55497
  }
55364
55498
  function slackRoutedRequestText(text, resolution, botUserId) {
55365
55499
  if (resolution.kind !== "resolved" || resolution.source !== "prefix") return text;
@@ -55883,6 +56017,20 @@ function slackReactionInboxEntry(payload, bot, settings) {
55883
56017
  hasFiles: false
55884
56018
  };
55885
56019
  }
56020
+ async function resolveSlackLinkTargetInstallation(deps, link, workspaceId) {
56021
+ const route = await resolveSlackInstallationRoute(deps.db, link.slackTeamId);
56022
+ if (!route || route.connectionId !== link.connectionId || route.accountId.length === 0) {
56023
+ return null;
56024
+ }
56025
+ const workspace = await getWorkspace6(deps.db, workspaceId);
56026
+ if (!workspace || workspace.accountId !== route.accountId) return null;
56027
+ return {
56028
+ accountId: route.accountId,
56029
+ connectionId: route.connectionId,
56030
+ workspaceName: workspace.name,
56031
+ identityHome: { accountId: route.accountId, workspaceId: route.workspaceId }
56032
+ };
56033
+ }
55886
56034
  function registerSlackInteractionRoutes(app, deps) {
55887
56035
  app.post("/v1/integrations/slack/events", async (c) => {
55888
56036
  const signed = await readSignedSlackRequest(c, deps);
@@ -56035,17 +56183,13 @@ function registerSlackInteractionRoutes(app, deps) {
56035
56183
  if (!link || link.workspaceId !== workspaceId) {
56036
56184
  throw freshSlackLinkRequired();
56037
56185
  }
56038
- const route = await resolveSlackInstallationRoute(deps.db, link.slackTeamId);
56039
- if (!route || route.workspaceId !== workspaceId || route.connectionId !== link.connectionId || route.accountId.length === 0) {
56040
- throw freshSlackLinkRequired();
56041
- }
56042
- const workspace = await getWorkspace6(deps.db, workspaceId);
56043
- if (!workspace || workspace.accountId !== route.accountId) {
56186
+ const installation = await resolveSlackLinkTargetInstallation(deps, link, workspaceId);
56187
+ if (!installation) {
56044
56188
  throw freshSlackLinkRequired();
56045
56189
  }
56046
56190
  try {
56047
56191
  const prepared = await prepareSlackUserLinkAccessRequest(deps.db, {
56048
- accountId: route.accountId,
56192
+ accountId: installation.accountId,
56049
56193
  workspaceId,
56050
56194
  tokenDigest: createHash22("sha256").update(payload.linkToken).digest("hex"),
56051
56195
  connectionId: link.connectionId,
@@ -56058,13 +56202,14 @@ function registerSlackInteractionRoutes(app, deps) {
56058
56202
  const completed = await completeSlackUserLinkAccessIfGranted(deps.db, {
56059
56203
  workspaceId,
56060
56204
  requestId: prepared.id,
56061
- subjectId: context.subjectId
56205
+ subjectId: context.subjectId,
56206
+ identityHome: installation.identityHome
56062
56207
  });
56063
56208
  if (!completed) throw freshSlackLinkRequired();
56064
56209
  return c.json(
56065
56210
  SlackUserLinkAccessRequest.parse({
56066
56211
  ...completed,
56067
- workspaceDisplayName: workspace.name
56212
+ workspaceDisplayName: installation.workspaceName
56068
56213
  }),
56069
56214
  201
56070
56215
  );
@@ -56167,14 +56312,26 @@ function registerSlackInteractionRoutes(app, deps) {
56167
56312
  const workspaceId = c.req.param("workspaceId");
56168
56313
  const grant = await requireAccessGrant44(c, deps, workspaceId, "members:manage");
56169
56314
  const payload = ApproveSlackUserLinkAccessRequest.parse(await c.req.json());
56315
+ const requestId = c.req.param("requestId");
56316
+ const requestTeam = await slackUserLinkAccessRequestTeam(deps.db, {
56317
+ workspaceId,
56318
+ requestId
56319
+ });
56320
+ const installation = requestTeam ? await resolveSlackInstallationRoute(deps.db, requestTeam.slackTeamId) : null;
56170
56321
  try {
56171
56322
  const request = await approveSlackUserLinkAccessRequest(deps.db, {
56172
56323
  workspaceId,
56173
- requestId: c.req.param("requestId"),
56324
+ requestId,
56174
56325
  actorSubjectId: grant.subjectId,
56175
56326
  expectedVersion: payload.expectedVersion,
56176
56327
  idempotencyKey: payload.idempotencyKey,
56177
56328
  permissions: payload.permissions,
56329
+ ...installation ? {
56330
+ identityHome: {
56331
+ accountId: installation.accountId,
56332
+ workspaceId: installation.workspaceId
56333
+ }
56334
+ } : {},
56178
56335
  ...payload.role !== void 0 ? { role: payload.role } : {}
56179
56336
  });
56180
56337
  const workspace = await getWorkspace6(deps.db, workspaceId);
@@ -56231,8 +56388,8 @@ function registerSlackInteractionRoutes(app, deps) {
56231
56388
  message: "invalid or expired Slack identity link"
56232
56389
  });
56233
56390
  }
56234
- const route = await resolveSlackInstallationRoute(deps.db, link.slackTeamId);
56235
- if (!route || route.workspaceId !== workspaceId || route.connectionId !== link.connectionId) {
56391
+ const installation = await resolveSlackLinkTargetInstallation(deps, link, workspaceId);
56392
+ if (!installation) {
56236
56393
  throw new HTTPException70(404, {
56237
56394
  message: "Slack installation not found"
56238
56395
  });
@@ -56252,7 +56409,8 @@ function registerSlackInteractionRoutes(app, deps) {
56252
56409
  const completed = await completeSlackUserLinkAccessIfGranted(deps.db, {
56253
56410
  workspaceId,
56254
56411
  requestId: prepared.id,
56255
- subjectId: grant.subjectId
56412
+ subjectId: grant.subjectId,
56413
+ identityHome: installation.identityHome
56256
56414
  });
56257
56415
  if (completed?.status !== "completed") throw freshSlackLinkRequired();
56258
56416
  const saved = await getSlackBotUserLink(
@@ -56284,6 +56442,92 @@ function registerSlackInteractionRoutes(app, deps) {
56284
56442
  });
56285
56443
  }
56286
56444
  );
56445
+ app.get("/v1/workspaces/:workspaceId/integrations/slack/channel-routes", async (c) => {
56446
+ const workspaceId = c.req.param("workspaceId");
56447
+ const grant = await requireAccessGrant44(c, deps, workspaceId, "workspace:admin");
56448
+ const connectionId = boundedString(c.req.query("connectionId"), 64);
56449
+ if (!connectionId) throw new HTTPException70(400, { message: "connectionId is required" });
56450
+ const routes = await listSlackChannelRoutes(
56451
+ deps.db,
56452
+ { accountId: grant.accountId, workspaceId },
56453
+ { connectionId }
56454
+ );
56455
+ const names = /* @__PURE__ */ new Map();
56456
+ for (const route of routes) {
56457
+ if (names.has(route.targetWorkspaceId)) continue;
56458
+ names.set(
56459
+ route.targetWorkspaceId,
56460
+ (await getWorkspace6(deps.db, route.targetWorkspaceId))?.name ?? null
56461
+ );
56462
+ }
56463
+ return c.json(
56464
+ SlackChannelRouteListResponse.parse({
56465
+ routingEnabled: deps.settings.slackWorkspaceRoutingEnabled,
56466
+ routes: routes.map((route) => ({
56467
+ slackChannelId: route.slackChannelId,
56468
+ targetWorkspaceId: route.targetWorkspaceId,
56469
+ targetWorkspaceName: names.get(route.targetWorkspaceId) ?? null,
56470
+ source: route.source,
56471
+ updatedAt: route.updatedAt.toISOString()
56472
+ }))
56473
+ })
56474
+ );
56475
+ });
56476
+ app.put("/v1/workspaces/:workspaceId/integrations/slack/channel-routes", async (c) => {
56477
+ const workspaceId = c.req.param("workspaceId");
56478
+ const grant = await requireAccessGrant44(c, deps, workspaceId, "workspace:admin");
56479
+ const payload = UpdateSlackChannelRoutesRequest.parse(await c.req.json());
56480
+ const home = { accountId: grant.accountId, workspaceId };
56481
+ const installation = await listSlackInstallationBindings2(deps.db, home);
56482
+ const binding = installation.find(
56483
+ (candidate) => candidate.connectionId === payload.connectionId
56484
+ );
56485
+ if (!binding) {
56486
+ throw new HTTPException70(404, { message: "Slack installation not found" });
56487
+ }
56488
+ const callerPersonalWorkspaceId = await namedSubjectPersonalWorkspaceId(deps.db, {
56489
+ accountId: grant.accountId,
56490
+ subjectId: grant.subjectId
56491
+ });
56492
+ for (const route of payload.routes) {
56493
+ if (route.targetWorkspaceId === null) continue;
56494
+ const authorized = await resolveSlackTargetAuthority(deps.db, {
56495
+ subjectId: grant.subjectId,
56496
+ targetAccountId: grant.accountId,
56497
+ targetWorkspaceId: route.targetWorkspaceId
56498
+ });
56499
+ if (!authorized || !hasPermission22(authorized.permissions, "sessions:create")) {
56500
+ throw new HTTPException70(403, {
56501
+ message: `you cannot start work in the workspace chosen for ${route.slackChannelId}`
56502
+ });
56503
+ }
56504
+ if (callerPersonalWorkspaceId !== null && route.targetWorkspaceId === callerPersonalWorkspaceId) {
56505
+ throw new HTTPException70(422, {
56506
+ message: `a channel cannot be routed to a personal workspace: work started in ${route.slackChannelId} would be invisible to everyone else in it`
56507
+ });
56508
+ }
56509
+ }
56510
+ for (const route of payload.routes) {
56511
+ if (route.targetWorkspaceId === null) {
56512
+ await deleteSlackChannelRoute(deps.db, home, {
56513
+ connectionId: payload.connectionId,
56514
+ slackChannelId: route.slackChannelId
56515
+ });
56516
+ continue;
56517
+ }
56518
+ await upsertSlackChannelRoute(deps.db, home, {
56519
+ connectionId: payload.connectionId,
56520
+ slackTeamId: binding.slackTeamId,
56521
+ slackChannelId: route.slackChannelId,
56522
+ targetAccountId: grant.accountId,
56523
+ targetWorkspaceId: route.targetWorkspaceId,
56524
+ decidedBySubjectId: grant.subjectId,
56525
+ decidedBySlackUserId: "admin",
56526
+ source: "admin"
56527
+ });
56528
+ }
56529
+ return c.json({ ok: true });
56530
+ });
56287
56531
  app.get("/v1/workspaces/:workspaceId/integrations/slack/reaction-channels", async (c) => {
56288
56532
  const workspaceId = c.req.param("workspaceId");
56289
56533
  const grant = await requireAccessGrant44(c, deps, workspaceId, "workspace:admin");
@@ -56680,13 +56924,222 @@ async function resolveSlackRouteForEntry(deps, home, entry2, subjectId, options)
56680
56924
  }
56681
56925
  async function postSlackRouteRefusal(deps, client, entry2, refusal) {
56682
56926
  const available = refusal.candidates.map((candidate) => candidate.label);
56683
- const text = refusal.reason === "no_access_to_named" ? `OpenGeni does not see a workspace named ${JSON.stringify(refusal.requested ?? "")} that you can start work in.${available.length > 0 ? ` You can use: ${available.join(", ")}.` : ""} No session was created.` : refusal.reason === "no_access_to_route" ? `OpenGeni starts work from this conversation in ${refusal.requested ?? "another workspace"}, and you do not have access to it. Ask an OpenGeni administrator for access to that workspace, or point this conversation somewhere else in OpenGeni under Capabilities, then Slack. No session was created.` : "OpenGeni has no workspace it can start this task in for you. Ask an OpenGeni administrator to give you access to one. No session was created.";
56927
+ const text = refusal.reason === "no_access_to_named" ? `OpenGeni does not see a workspace named ${JSON.stringify(refusal.requested ?? "")} that you can start work in from here.${available.length > 0 ? ` You can use: ${available.join(", ")}.` : ""} No session was created.` : refusal.reason === "no_access_to_route" ? `OpenGeni starts work from this conversation in ${refusal.requested ?? "another workspace"}, and you do not have access to it. Request access: ${linkUrl(
56928
+ deps,
56929
+ {
56930
+ // Minted for the workspace they actually need, not the
56931
+ // installation's. The token only decides which workspace an
56932
+ // access request may be raised for; approval is unchanged.
56933
+ workspaceId: refusal.workspaceId ?? entry2.workspaceId,
56934
+ connectionId: entry2.connectionId,
56935
+ slackTeamId: entry2.slackTeamId,
56936
+ slackUserId: entry2.slackUserId
56937
+ },
56938
+ entry2.createdAt.getTime()
56939
+ )}. No session was created.` : (
56940
+ // Deliberately "from this conversation" rather than "for you": in a
56941
+ // channel this also fires for someone whose only workspace is their
56942
+ // own personal one, and telling that person they have no workspace
56943
+ // at all would be false. Their bot DM still works.
56944
+ "OpenGeni has no workspace it can start this task in from this conversation. Ask an OpenGeni administrator to give you access to one, or message OpenGeni directly to work in your own workspace. No session was created."
56945
+ );
56684
56946
  await client.postMessage({
56685
56947
  operationId: deterministicUuid2(`slack-route-denied:${entry2.id}:${refusal.reason}`),
56686
56948
  userId: entry2.slackUserId,
56687
56949
  text: boundedOutput(text)
56688
56950
  });
56689
56951
  }
56952
+ function boundedSlackRouteLabel(label) {
56953
+ const encoder3 = new TextEncoder();
56954
+ if (encoder3.encode(label).length <= 72) return label;
56955
+ let bounded = label;
56956
+ while (bounded.length > 0 && encoder3.encode(`${bounded}...`).length > 72) {
56957
+ bounded = bounded.slice(0, -1);
56958
+ }
56959
+ return `${bounded}...`;
56960
+ }
56961
+ function withWorkspaceLine(label, text, blocks) {
56962
+ if (!label) return blocks ? { text, blocks } : { text };
56963
+ const suffix = `
56964
+ -> ${label}`;
56965
+ const body4 = boundedOutput(text, MAX_SLACK_TEXT_CHARS - suffix.length);
56966
+ const line = {
56967
+ type: "context",
56968
+ elements: [{ type: "mrkdwn", text: `-> ${label}` }]
56969
+ };
56970
+ return {
56971
+ text: `${body4}${suffix}`,
56972
+ ...blocks ? { blocks: [...blocks, line] } : {}
56973
+ };
56974
+ }
56975
+ function slackPostSeed(interaction, seed) {
56976
+ return interaction.routedWorkspaceLabel ? `${seed}:v2` : seed;
56977
+ }
56978
+ var SLACK_ROUTE_SOURCES_WORTH_NAMING = /* @__PURE__ */ new Set([
56979
+ // Not "thread": a mapped thread reuses the interaction that already exists,
56980
+ // so it inherits the label frozen when that interaction was created and never
56981
+ // reaches the writer below.
56982
+ "prefix",
56983
+ "channel",
56984
+ "dm_route",
56985
+ "dm_personal"
56986
+ ]);
56987
+ async function routedWorkspaceLabelFor(deps, resolution) {
56988
+ if (!SLACK_ROUTE_SOURCES_WORTH_NAMING.has(resolution.source)) return null;
56989
+ const label = resolution.label ?? (await getWorkspace6(deps.db, resolution.workspaceId))?.name;
56990
+ if (!label) return null;
56991
+ const bounded = boundedSlackRouteLabel(label);
56992
+ return bounded.length > 0 ? bounded : null;
56993
+ }
56994
+ var SLACK_ROUTE_PROMPT_TTL_MS = 24 * 60 * 60 * 1e3;
56995
+ var MAX_SLACK_ROUTE_PROMPT_BUTTONS = 5;
56996
+ async function askSlackRouteChoice(deps, client, home, entry2, candidates) {
56997
+ const offered = candidates.slice(0, MAX_SLACK_ROUTE_PROMPT_BUTTONS);
56998
+ const bounded = offered.map((candidate) => ({
56999
+ ...candidate,
57000
+ label: boundedSlackRouteLabel(candidate.label)
57001
+ }));
57002
+ const opened = await openSlackRoutePrompt(deps.db, home, {
57003
+ connectionId: entry2.connectionId,
57004
+ inboxId: entry2.id,
57005
+ slackTeamId: entry2.slackTeamId,
57006
+ slackUserId: entry2.slackUserId,
57007
+ slackChannelId: entry2.slackChannelId,
57008
+ slackMessageTs: entry2.slackMessageTs,
57009
+ providerEventId: entry2.providerEventId,
57010
+ triggerKind: entry2.triggerKind,
57011
+ requestText: entry2.text,
57012
+ hasFiles: entry2.hasFiles,
57013
+ slackThreadTs: entry2.slackThreadTs,
57014
+ messageOperationId: deterministicUuid2(`slack-route-prompt:${entry2.id}`),
57015
+ expiresAt: new Date(Date.now() + SLACK_ROUTE_PROMPT_TTL_MS),
57016
+ candidates: bounded
57017
+ });
57018
+ if (!opened) return;
57019
+ const where = opened.prompt.slackChannelId.startsWith("D") ? "your messages with OpenGeni" : `<#${opened.prompt.slackChannelId}>`;
57020
+ const blocks = [
57021
+ {
57022
+ type: "section",
57023
+ text: {
57024
+ type: "mrkdwn",
57025
+ text: `OpenGeni can start this task in more than one workspace. Pick where ${where} should work. This choice is remembered.`
57026
+ }
57027
+ },
57028
+ {
57029
+ type: "actions",
57030
+ block_id: "opengeni-route-choice",
57031
+ elements: [
57032
+ ...opened.prompt.options.map((option) => ({
57033
+ type: "button",
57034
+ action_id: "opengeni.route.select",
57035
+ value: option.id,
57036
+ text: { type: "plain_text", text: option.candidateLabel }
57037
+ })),
57038
+ {
57039
+ type: "button",
57040
+ action_id: "opengeni.route.cancel",
57041
+ value: opened.prompt.options[0]?.id ?? opened.prompt.id,
57042
+ text: { type: "plain_text", text: "Not now" }
57043
+ }
57044
+ ]
57045
+ }
57046
+ ];
57047
+ const url = slackWorkspaceUrl(deps, home.workspaceId);
57048
+ blocks.push({
57049
+ type: "section",
57050
+ text: {
57051
+ type: "mrkdwn",
57052
+ text: url ? `You can change this later in <${url}|OpenGeni>, under Capabilities, then Slack.` : "You can change this later in OpenGeni, under Capabilities, then Slack."
57053
+ }
57054
+ });
57055
+ await client.postMessage({
57056
+ operationId: opened.prompt.messageOperationId,
57057
+ userId: opened.prompt.slackUserId,
57058
+ text: boundedOutput(
57059
+ `OpenGeni can start this task in more than one workspace. Pick where ${where} should work.`
57060
+ ),
57061
+ blocks
57062
+ });
57063
+ }
57064
+ async function handleSlackRouteChoice(deps, home, entry2, actionId, optionId) {
57065
+ if (!deps.settings.slackWorkspaceRoutingEnabled) {
57066
+ throw new SlackInteractionPermanentError("slack_route_choice_unavailable");
57067
+ }
57068
+ const found = await getSlackRoutePromptByOption(deps.db, home, optionId);
57069
+ if (!found) throw new SlackInteractionPermanentError("slack_route_prompt_invalid");
57070
+ const { prompt, chosen } = found;
57071
+ if (prompt.slackUserId !== entry2.slackUserId) {
57072
+ throw new SlackInteractionPermanentError("slack_route_prompt_user_mismatch");
57073
+ }
57074
+ const client = await createOpenGeniSlackBotInteractionClient(deps, {
57075
+ ...home,
57076
+ connectionId: entry2.connectionId,
57077
+ subjectId: SLACK_INTERACTION_BOT_SUBJECT_ID
57078
+ });
57079
+ const replaceCard = async (outcome, text) => {
57080
+ await client.updateMessage({
57081
+ operationId: deterministicUuid2(
57082
+ `slack-route-answered:${prompt.id}:${actionId}:${chosen.id}:${outcome}`
57083
+ ),
57084
+ channelId: entry2.slackChannelId,
57085
+ timestamp: entry2.slackMessageTs,
57086
+ text,
57087
+ // Every button goes with the card, so a sibling option cannot be pressed
57088
+ // after the question is settled.
57089
+ blocks: [{ type: "section", text: { type: "mrkdwn", text } }]
57090
+ });
57091
+ };
57092
+ if (actionId === "opengeni.route.cancel") {
57093
+ const cancelled = await settleSlackRoutePrompt(deps.db, home, {
57094
+ promptId: prompt.id,
57095
+ status: "cancelled"
57096
+ });
57097
+ await replaceCard(
57098
+ cancelled ? "cancelled" : "already_settled",
57099
+ cancelled ? "OpenGeni did not start this task. No session was created." : "OpenGeni already settled this choice."
57100
+ );
57101
+ return;
57102
+ }
57103
+ const link = await getSlackBotUserLink(
57104
+ deps.db,
57105
+ home.workspaceId,
57106
+ entry2.connectionId,
57107
+ entry2.slackUserId
57108
+ );
57109
+ const grant = link ? await resolveSlackTargetAuthority(deps.db, {
57110
+ subjectId: link.subjectId,
57111
+ targetAccountId: chosen.candidateAccountId,
57112
+ targetWorkspaceId: chosen.candidateWorkspaceId
57113
+ }) : null;
57114
+ if (!grant || !hasPermission22(grant.permissions, "sessions:create")) {
57115
+ await client.postMessage({
57116
+ operationId: deterministicUuid2(`slack-route-denied:${prompt.id}:${chosen.id}`),
57117
+ userId: entry2.slackUserId,
57118
+ text: boundedOutput(
57119
+ `OpenGeni cannot start work in ${chosen.candidateLabel}: you do not have access to it. No session was created. Pick another workspace on the card, or ask an OpenGeni administrator for access.`
57120
+ )
57121
+ });
57122
+ return;
57123
+ }
57124
+ const answered = await answerSlackRoutePrompt(deps.db, home, {
57125
+ promptId: prompt.id,
57126
+ targetAccountId: chosen.candidateAccountId,
57127
+ targetWorkspaceId: chosen.candidateWorkspaceId,
57128
+ answeredBySubjectId: grant.subjectId,
57129
+ decidedBySlackUserId: entry2.slackUserId
57130
+ });
57131
+ if (!answered.answered) {
57132
+ await replaceCard(
57133
+ "settled",
57134
+ "OpenGeni already settled this choice. No new session was created."
57135
+ );
57136
+ return;
57137
+ }
57138
+ await replaceCard(
57139
+ "answered",
57140
+ `OpenGeni will use *${chosen.candidateLabel}* here. You can change this in OpenGeni under Capabilities, then Slack.`
57141
+ );
57142
+ }
56690
57143
  async function processSlackInboxEntry(deps, entry2) {
56691
57144
  if (entry2.triggerKind === "block_action") {
56692
57145
  await processSlackBlockAction(deps, entry2);
@@ -56743,19 +57196,18 @@ async function processSlackInboxEntry(deps, entry2) {
56743
57196
  await client.postMessage({
56744
57197
  operationId: deterministicUuid2(`slack-link:${entry2.id}`),
56745
57198
  userId: entry2.slackUserId,
56746
- text: `Link your Slack identity to OpenGeni before starting work: ${linkUrl(deps, entry2)}. No session was created.`
57199
+ text: `Link your Slack identity to OpenGeni before starting work: ${linkUrl(deps, entry2, entry2.createdAt.getTime())}. No session was created.`
56747
57200
  });
56748
57201
  return;
56749
57202
  }
56750
57203
  const resolution = await resolveSlackRouteForEntry(deps, home, entry2, link.subjectId, {
56751
57204
  botUserId: installation.botUserId,
56752
57205
  threadTenancy: existing ? { accountId: existing.accountId, workspaceId: existing.workspaceId } : null,
56753
- // The first-use picker does not exist yet, so genuine ambiguity keeps the
56754
- // installation's workspace rather than inventing an answer.
56755
- askEnabled: false
57206
+ askEnabled: true
56756
57207
  });
56757
57208
  if (resolution.kind === "ask") {
56758
- throw new SlackInteractionPermanentError("slack_route_choice_unavailable");
57209
+ await askSlackRouteChoice(deps, client, home, entry2, resolution.candidates);
57210
+ return;
56759
57211
  }
56760
57212
  if (resolution.kind === "denied") {
56761
57213
  await postSlackRouteRefusal(deps, client, entry2, resolution);
@@ -56862,6 +57314,11 @@ async function processSlackInboxEntry(deps, entry2) {
56862
57314
  }
56863
57315
  const { interaction } = await getOrCreateSlackInteraction(deps.db, {
56864
57316
  ...target,
57317
+ // Frozen here, after authorization, so a workspace name is never shown to
57318
+ // someone who could not reach it. Null when the routing decision was not a
57319
+ // genuine choice, which is what keeps the line off every message in an
57320
+ // install that only ever had one workspace.
57321
+ routedWorkspaceLabel: await routedWorkspaceLabelFor(deps, resolution),
56865
57322
  connectionId: entry2.connectionId,
56866
57323
  slackTeamId: entry2.slackTeamId,
56867
57324
  slackChannelId: entry2.slackChannelId,
@@ -57087,7 +57544,7 @@ async function acknowledgeSlackSession(deps, client, interaction, entry2) {
57087
57544
  const directMessageShortcut = isDirectMessageShortcut(entry2);
57088
57545
  const privateHandoff = !directMessageShortcut && interaction.visibility === "private" && entry2.triggerKind !== "dm";
57089
57546
  const privateBotDm = directMessageShortcut || privateHandoff;
57090
- const operationId = deterministicUuid2(`slack-ack:${interaction.id}`);
57547
+ const operationId = deterministicUuid2(slackPostSeed(interaction, `slack-ack:${interaction.id}`));
57091
57548
  const started = directMessageShortcut ? `OpenGeni started a private task from the selected DM message. ${openSessionText(deps, interaction.workspaceId, interaction.sessionId)} The source DM was not opened to the bot or made workspace-visible.` : privateHandoff ? `OpenGeni started a private task from the selected Slack conversation. ${openSessionText(deps, interaction.workspaceId, interaction.sessionId)} Results stay private unless a separate authorized publication is approved.` : `OpenGeni started this task. ${openSessionText(deps, interaction.workspaceId, interaction.sessionId)}`;
57092
57549
  const showHint = await resolveSlackInteractionFirstTaskHint(deps.db, {
57093
57550
  // The identity link is HOME; the interaction row is TARGET.
@@ -57103,19 +57560,19 @@ async function acknowledgeSlackSession(deps, client, interaction, entry2) {
57103
57560
  sessionEventSequence: 0,
57104
57561
  state: "active"
57105
57562
  });
57563
+ const acknowledgement = withWorkspaceLine(
57564
+ interaction.routedWorkspaceLabel,
57565
+ text,
57566
+ controls.length > 0 ? [{ type: "section", text: { type: "mrkdwn", text } }, ...controls] : void 0
57567
+ );
57106
57568
  const ack = await client.postMessage({
57107
57569
  operationId,
57108
57570
  ...privateBotDm ? { userId: entry2.slackUserId } : {
57109
57571
  channelId: entry2.slackChannelId,
57110
57572
  ...entry2.triggerKind === "slash_command" ? {} : { threadTimestamp: entry2.slackThreadTs ?? entry2.slackMessageTs }
57111
57573
  },
57112
- text,
57113
- ...controls.length > 0 ? {
57114
- blocks: [
57115
- { type: "section", text: { type: "mrkdwn", text } },
57116
- ...controls
57117
- ]
57118
- } : {}
57574
+ text: acknowledgement.text,
57575
+ ...acknowledgement.blocks ? { blocks: acknowledgement.blocks } : {}
57119
57576
  });
57120
57577
  if (entry2.triggerKind === "slash_command" || privateBotDm) {
57121
57578
  const rekeyed = await rekeySlackInteractionRoute(deps.db, {
@@ -57283,6 +57740,10 @@ async function processSlackReactionInboxEntry(deps, entry2) {
57283
57740
  }
57284
57741
  const { interaction } = await getOrCreateSlackInteraction(deps.db, {
57285
57742
  ...target,
57743
+ // A reaction routes exactly like a mention, so it names its workspace the
57744
+ // same way. Resolved after `authorizeTarget`, so a name never reaches
57745
+ // somebody who could not reach the workspace.
57746
+ routedWorkspaceLabel: await routedWorkspaceLabelFor(deps, routeResolution),
57286
57747
  connectionId: entry2.connectionId,
57287
57748
  slackTeamId: entry2.slackTeamId,
57288
57749
  slackChannelId: entry2.slackChannelId,
@@ -57360,11 +57821,15 @@ async function acknowledgeSlackReactionSession(deps, client, interaction, emoji)
57360
57821
  if (!interaction.sessionId) {
57361
57822
  throw new Error("Slack reaction acknowledgement requires a bound session");
57362
57823
  }
57824
+ const started = `OpenGeni started from the :${emoji}: reaction. ${openSessionText(deps, interaction.workspaceId, interaction.sessionId)} If the intended action is unclear, OpenGeni will ask in this thread. Reply here to continue, or reply \`stop\` to stop.`;
57825
+ const rendered = withWorkspaceLine(interaction.routedWorkspaceLabel, started);
57363
57826
  await client.postMessage({
57364
- operationId: deterministicUuid2(`slack-reaction-ack:${interaction.id}`),
57827
+ operationId: deterministicUuid2(
57828
+ slackPostSeed(interaction, `slack-reaction-ack:${interaction.id}`)
57829
+ ),
57365
57830
  channelId: interaction.slackChannelId,
57366
57831
  threadTimestamp: interaction.slackThreadTs,
57367
- text: `OpenGeni started from the :${emoji}: reaction. ${openSessionText(deps, interaction.workspaceId, interaction.sessionId)} If the intended action is unclear, OpenGeni will ask in this thread. Reply here to continue, or reply \`stop\` to stop.`
57832
+ text: rendered.text
57368
57833
  });
57369
57834
  }
57370
57835
  function slackReactionPreparedEntry(entry2, context, prepared) {
@@ -57715,6 +58180,10 @@ async function processSlackBlockAction(deps, entry2) {
57715
58180
  if (!route || route.accountId !== entry2.accountId || route.workspaceId !== entry2.workspaceId || route.connectionId !== entry2.connectionId) {
57716
58181
  throw new SlackInteractionPermanentError("slack_action_installation_changed");
57717
58182
  }
58183
+ if (actionId === "opengeni.route.select" || actionId === "opengeni.route.cancel") {
58184
+ await handleSlackRouteChoice(deps, home, entry2, actionId, handleId);
58185
+ return;
58186
+ }
57718
58187
  const handleTenancy = await probeSlackActionHandleTenancy(deps.db, {
57719
58188
  accountId: home.accountId,
57720
58189
  connectionId: entry2.connectionId,
@@ -57774,14 +58243,21 @@ async function processSlackBlockAction(deps, entry2) {
57774
58243
  sessionId: handle.sessionId
57775
58244
  });
57776
58245
  const outcome = await executeSlackAction(deps, grant, interaction, handle);
57777
- const acknowledgementOperationId = deterministicUuid2(`slack-ack:${interaction.id}`);
58246
+ const acknowledgementOperationId = deterministicUuid2(
58247
+ slackPostSeed(interaction, `slack-ack:${interaction.id}`)
58248
+ );
57778
58249
  const updateText = interaction.firstTaskHint === true && handle.messageOperationId === acknowledgementOperationId ? `${outcome.text}${slackFirstTaskHintText(deps)}` : outcome.text;
58250
+ const rendered = withWorkspaceLine(interaction.routedWorkspaceLabel, updateText, [
58251
+ { type: "section", text: { type: "mrkdwn", text: updateText } }
58252
+ ]);
57779
58253
  await client.updateMessage({
57780
- operationId: deterministicUuid2(`slack-action-update:${handle.id}:${outcome.result}`),
58254
+ operationId: deterministicUuid2(
58255
+ slackPostSeed(interaction, `slack-action-update:${handle.id}:${outcome.result}`)
58256
+ ),
57781
58257
  channelId: entry2.slackChannelId,
57782
58258
  timestamp: entry2.slackMessageTs,
57783
- text: updateText,
57784
- blocks: [{ type: "section", text: { type: "mrkdwn", text: updateText } }]
58259
+ text: rendered.text,
58260
+ ...rendered.blocks ? { blocks: rendered.blocks } : {}
57785
58261
  });
57786
58262
  await settleSlackInteractionActionHandles(deps.db, {
57787
58263
  ...target,
@@ -58125,7 +58601,7 @@ function slackApprovalSummaries(payload) {
58125
58601
  }
58126
58602
  async function slackApprovalCard(deps, interaction, event, mention, requesterAuthorized) {
58127
58603
  const operationId = deterministicUuid2(
58128
- `slack-delivery:${interaction.id}:${event.sequence}:approval`
58604
+ slackPostSeed(interaction, `slack-delivery:${interaction.id}:${event.sequence}:approval`)
58129
58605
  );
58130
58606
  const approvals = slackApprovalSummaries(event.payload);
58131
58607
  const fallback = approvals.length ? `${mention}OpenGeni needs approval for ${approvals.map((approval) => approval.name).join(", ")}.` : `${mention}OpenGeni needs approval. Open the task to review it.`;
@@ -58253,7 +58729,7 @@ async function slackHumanInputCard(deps, interaction, event, mention, requesterA
58253
58729
  );
58254
58730
  if (!request || request.status !== "pending") return null;
58255
58731
  const operationId = deterministicUuid2(
58256
- `slack-delivery:${interaction.id}:${event.sequence}:human-input`
58732
+ slackPostSeed(interaction, `slack-delivery:${interaction.id}:${event.sequence}:human-input`)
58257
58733
  );
58258
58734
  const text = `${mention}OpenGeni needs your input:
58259
58735
  ${formatQuestions(request.questions)}
@@ -58558,30 +59034,32 @@ async function deliverSlackSessionEvents(deps, interaction, claimHolderId) {
58558
59034
  );
58559
59035
  if (posted?.slackChannelId && posted.slackMessageTimestamp) {
58560
59036
  const text = boundedOutput(`${requester.mention}${existingProgress.text}`);
59037
+ const rendered = withWorkspaceLine(
59038
+ interaction.routedWorkspaceLabel,
59039
+ text,
59040
+ publicationBlocks.length > 0 ? [
59041
+ { type: "section", text: { type: "mrkdwn", text } },
59042
+ ...publicationBlocks
59043
+ ] : void 0
59044
+ );
58561
59045
  await client.updateMessage({
58562
59046
  operationId: deterministicUuid2(
58563
- `slack-terminal-update:${interaction.id}:${event.sequence}`
59047
+ slackPostSeed(
59048
+ interaction,
59049
+ `slack-terminal-update:${interaction.id}:${event.sequence}`
59050
+ )
58564
59051
  ),
58565
59052
  channelId: posted.slackChannelId,
58566
59053
  timestamp: posted.slackMessageTimestamp,
58567
- text,
58568
- ...publicationBlocks.length > 0 ? {
58569
- blocks: [
58570
- {
58571
- type: "section",
58572
- text: {
58573
- type: "mrkdwn",
58574
- text
58575
- }
58576
- },
58577
- ...publicationBlocks
58578
- ]
59054
+ text: rendered.text,
59055
+ ...rendered.blocks ? {
59056
+ blocks: rendered.blocks
58579
59057
  } : {}
58580
59058
  });
58581
59059
  }
58582
59060
  } else {
58583
59061
  const operationId = deterministicUuid2(
58584
- `slack-delivery:${interaction.id}:${event.sequence}:final`
59062
+ slackPostSeed(interaction, `slack-delivery:${interaction.id}:${event.sequence}:final`)
58585
59063
  );
58586
59064
  const text = boundedOutput(
58587
59065
  `${requester.mention}${output || "OpenGeni finished this task."}`
@@ -58658,13 +59136,16 @@ function slackTerminalProgressSameTurn(event, delivery, interaction, samePageTer
58658
59136
  if (event.turnId || delivery.turnId) return false;
58659
59137
  return delivery.sessionEventSequence === interaction.lastDeliveredSessionEventSequence || samePageTerminalSequences.has(delivery.sessionEventSequence);
58660
59138
  }
58661
- async function postDelivery(client, interaction, event, text, kind, operationId = deterministicUuid2(`slack-delivery:${interaction.id}:${event.sequence}:${kind}`), blocks) {
59139
+ async function postDelivery(client, interaction, event, text, kind, operationId = deterministicUuid2(
59140
+ slackPostSeed(interaction, `slack-delivery:${interaction.id}:${event.sequence}:${kind}`)
59141
+ ), blocks) {
59142
+ const rendered = withWorkspaceLine(interaction.routedWorkspaceLabel, boundedOutput(text), blocks);
58662
59143
  await client.postMessage({
58663
59144
  operationId,
58664
59145
  channelId: interaction.slackChannelId,
58665
59146
  threadTimestamp: interaction.slackThreadTs,
58666
- text: boundedOutput(text),
58667
- ...blocks ? { blocks } : {}
59147
+ text: rendered.text,
59148
+ ...rendered.blocks ? { blocks: rendered.blocks } : {}
58668
59149
  });
58669
59150
  }
58670
59151
  async function enqueueNormalizedSlackInteraction(deps, route, entry2) {
@@ -58730,7 +59211,9 @@ var SLACK_BLOCK_ACTION_IDS = /* @__PURE__ */ new Set([
58730
59211
  "opengeni.session.status",
58731
59212
  "opengeni.session.pause",
58732
59213
  "opengeni.session.resume",
58733
- "opengeni.shared_result.publish"
59214
+ "opengeni.shared_result.publish",
59215
+ "opengeni.route.select",
59216
+ "opengeni.route.cancel"
58734
59217
  ]);
58735
59218
  function normalizedBlockActionInteraction(payload) {
58736
59219
  const team = record6(payload.team);
@@ -58926,13 +59409,13 @@ function makeRecurringText(deps, workspaceId, sessionId) {
58926
59409
  function slackSchedulesUrl(deps, workspaceId) {
58927
59410
  return safeSlackActionUrl(deps, `/workspaces/${encodeURIComponent(workspaceId)}/schedules`);
58928
59411
  }
58929
- function linkUrl(deps, entry2) {
59412
+ function linkUrl(deps, entry2, mintedAtMs) {
58930
59413
  const base = deps.settings.webBaseUrl ?? deps.settings.publicBaseUrl;
58931
59414
  const signingSecret = deps.settings.slackSigningSecret;
58932
59415
  if (!base || !signingSecret) return "OpenGeni Settings \u2192 Integrations \u2192 Slack";
58933
59416
  const url = new URL(`/workspaces/${entry2.workspaceId}/capabilities`, base);
58934
59417
  url.hash = new URLSearchParams({
58935
- slack_link: createSlackUserLinkToken(signingSecret, entry2)
59418
+ slack_link: createSlackUserLinkToken(signingSecret, entry2, mintedAtMs)
58936
59419
  }).toString();
58937
59420
  return url.toString();
58938
59421
  }
@@ -59046,8 +59529,8 @@ function boundedSlackChildDetail(value) {
59046
59529
  const collapsed = value.replace(/\s+/gu, " ").trim();
59047
59530
  return collapsed.length <= MAX_SLACK_CHILD_DETAIL_CHARS ? collapsed : `${collapsed.slice(0, MAX_SLACK_CHILD_DETAIL_CHARS - 1)}\u2026`;
59048
59531
  }
59049
- function boundedOutput(value) {
59050
- return value.length <= MAX_SLACK_TEXT_CHARS ? value : `${value.slice(0, MAX_SLACK_TEXT_CHARS - 20)}
59532
+ function boundedOutput(value, limit = MAX_SLACK_TEXT_CHARS) {
59533
+ return value.length <= limit ? value : `${value.slice(0, limit - 20)}
59051
59534
  \u2026 output truncated`;
59052
59535
  }
59053
59536
  function safePayloadText(payload, field) {
@@ -60220,6 +60703,10 @@ var routeLabelPatterns = [
60220
60703
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events$/,
60221
60704
  label: "/v1/workspaces/:workspaceId/sessions/:id/events"
60222
60705
  },
60706
+ {
60707
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/turns$/,
60708
+ label: "/v1/workspaces/:workspaceId/sessions/:id/turns"
60709
+ },
60223
60710
  {
60224
60711
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue\/[^/]+\/(move|edit|steer|delete)$/,
60225
60712
  label: "/v1/workspaces/:workspaceId/sessions/:id/queue/:turnId/:action"
@@ -60673,6 +61160,10 @@ var routeLabelPatterns = [
60673
61160
  {
60674
61161
  pattern: /^\/v1\/github\/oauth\/callback$/,
60675
61162
  label: "/v1/github/oauth/callback"
61163
+ },
61164
+ {
61165
+ pattern: /^\/v1\/workspaces\/[^/]+$/,
61166
+ label: "/v1/workspaces/:workspaceId"
60676
61167
  }
60677
61168
  ];
60678
61169
  function routeLabel(pathname) {
@@ -60729,4 +61220,4 @@ export {
60729
61220
  withDefaultEnabledCapabilityMcpTools,
60730
61221
  workflowIdForSession3 as workflowIdForSession
60731
61222
  };
60732
- //# sourceMappingURL=chunk-QKDFBBUE.js.map
61223
+ //# sourceMappingURL=chunk-3TP54PPX.js.map