@sentry/junior 0.255.0 → 0.257.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{admin-CeJh4SuR.js → admin-DgIkm6Vy.js} +1 -1
- package/dist/{agent-hooks-CDVx26Uu.js → agent-hooks-BjtTzvru.js} +13 -13
- package/dist/{agent-invocations-BY60iNw-.js → agent-invocations-Bdu3ROfo.js} +2 -1
- package/dist/{agent-runner-Caiof09f.js → agent-runner-D7OboVb3.js} +41 -33
- package/dist/api/schema.d.ts +41 -1
- package/dist/api/schema.js +2 -2
- package/dist/api.js +121 -33
- package/dist/{app-B0gT1Slp.d.ts → app-BsU5QNBd.d.ts} +7 -1
- package/dist/app.d.ts +1 -1
- package/dist/app.js +587 -210
- package/dist/{assistant-reply-BddmM66R.js → assistant-reply-CbTPebzL.js} +1 -1
- package/dist/{briefs-iVq_8-ft.js → briefs-oLOldnDM.js} +4 -4
- package/dist/{catalog-runtime-hKmI-iZa.js → catalog-runtime-CZzhGQ7F.js} +1 -1
- package/dist/cli/chat.js +16 -16
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +2 -2
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +3 -3
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-Clj7onnT.js → client-D8HnkOQX.js} +2 -2
- package/dist/{config-BDAfLTI9.js → config-CJRnWK9H.js} +2 -2
- package/dist/{connections-BC0RTejT.js → connections-ClYrKzpX.js} +16 -11
- package/dist/{conversation-T18Xlkx9.js → conversation-CKNLYBJt.js} +3 -2
- package/dist/{conversation-privacy-1ERtieuM.js → conversation-privacy-C8b1nphZ.js} +1 -1
- package/dist/{credential-sync-BZuqqIHk.js → credential-sync-BXfohNmJ.js} +82 -40
- package/dist/{executor-Dh4Ofsnx.js → executor-Do0THDRR.js} +33 -23
- package/dist/{experimental-Qn9rKLgx.js → experimental-CQIvMUO7.js} +1 -0
- package/dist/{images-C-WOnTc_.js → images-BN_BiBXF.js} +3 -3
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-BaCL36FY.js → job-queue-B6SV9uVa.js} +1 -1
- package/dist/{logging-D3aYP0aG.js → logging-CWxiIuCF.js} +50 -15
- package/dist/{message-B9xqD2BP.js → message-TUOdbSBW.js} +3 -3
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-i5qdAou6.js → oauth-callback-server-Dj5UJODo.js} +2 -2
- package/dist/{pending-auth-D2t1Moxf.js → pending-auth-s_QFribU.js} +3 -3
- package/dist/{profile-DnkX3e2S.js → profile-h-72wM78.js} +4 -4
- package/dist/{prompt-C04mP1hu.js → prompt-DyoEj9c2.js} +1 -1
- package/dist/{registry-CmKmKHpp.js → registry-sLTr-sV9.js} +43 -3
- package/dist/{resolve-jfNHtEBx.js → resolve-ncQPZROQ.js} +2 -2
- package/dist/{result-Df5nEDX2.js → result-C883vngT.js} +7 -7
- package/dist/{runner-CmzoXEI4.js → runner-CzYImdOF.js} +10 -10
- package/dist/{sandbox-egress-signals-DjMNKLZz.js → sandbox-egress-signals-CZbMINHI.js} +1 -1
- package/dist/{schema-BBgv0MNf.js → schema-Dy69D4Ge.js} +4 -3
- package/dist/{session-stop-BEBfE97J.js → session-stop-CQjKd4a2.js} +46 -20
- package/dist/{skills-Dc-AMYi4.js → skills-C22SLruK.js} +2 -2
- package/dist/{spawn-OgTwBUj0.js → spawn-DY9CLXDL.js} +2 -2
- package/dist/{store-BashBewg.js → store-DXFIQy-G.js} +6 -6
- package/dist/{task-queue-VvIKqlQ4.js → task-queue-B17vV0La.js} +3 -3
- package/dist/{task-runner-D2CpD6GN.js → task-runner-DUjvzYXj.js} +11 -11
- package/dist/{tasks-dJooqiip.js → tasks-D_WIJtvO.js} +25 -10
- package/dist/{thread-state-CsMC-ob0.js → thread-state-Ch4yuCqG.js} +7 -7
- package/dist/{turn-failure-response-zB364t7_.js → turn-failure-response-C5nBKC4S.js} +8 -8
- package/dist/{turn-lifecycle-BfodfmiY.js → turn-lifecycle-CO-ylUmV.js} +1 -1
- package/dist/{turn-session-routing-BAh_qsBJ.js → turn-session-routing-B3LWooK5.js} +1 -1
- package/dist/{validation-LzIilOb6.js → validation-CXYxaNYx.js} +1 -1
- package/dist/{version-CDM8Rq6E.js → version-BcdZy2zg.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-Rnim7alE.js → viewer-DSZXygMU.js} +53 -5
- package/dist/{web-input-Dc1VwcT4.js → web-input-B9Bv0_lR.js} +73 -75
- package/dist/{work-BAn43SsN.js → work-nAfDSUFP.js} +9 -9
- package/dist/{worker-mBXq4_zg.js → worker-vGXZ6XFT.js} +5 -5
- package/migrations/0059_automation_run_version.sql +2 -0
- package/migrations/0060_identity_signed_in_at.sql +1 -0
- package/migrations/meta/0059_snapshot.json +3619 -0
- package/migrations/meta/0060_snapshot.json +3465 -0
- package/migrations/meta/_journal.json +15 -1
- package/package.json +6 -5
package/dist/app.js
CHANGED
|
@@ -1,38 +1,40 @@
|
|
|
1
|
-
import { At as consumeSandboxEgressPermissionDeniedSignal, B as effectiveTaskOutcomes, Ct as ingestEvent, Dt as SANDBOX_EGRESS_PROXY_PATH, E as requestAttachmentDeletion, Et as shouldPropagateSandboxEgressTrace, F as logScheduledAutomationRunSkipped, I as scheduledAutomationRunAttributes, It as
|
|
2
|
-
import {
|
|
1
|
+
import { At as consumeSandboxEgressPermissionDeniedSignal, B as effectiveTaskOutcomes, Ct as ingestEvent, Dt as SANDBOX_EGRESS_PROXY_PATH, E as requestAttachmentDeletion, Et as shouldPropagateSandboxEgressTrace, F as logScheduledAutomationRunSkipped, I as scheduledAutomationRunAttributes, It as resolveBaseUrl, Jt as setConfigDefaults, Lt as startOAuthFlow, Nt as deliverPrivateMessage, O as parseSlackThreadId, Ot as clearSandboxEgressSignals, Pt as formatProviderLabel, Rt as createUserTokenStore, St as processWorkspaceSnapshotJob, Tt as normalizeSandboxEgressTracePropagationDomains, U as stopConversationSession, W as recordSubagentEnded, b as unpublishedArtifactHeaders, i as executeCredentialedEgressRequest, jt as parseSandboxEgressCredentialToken, kt as consumeSandboxEgressAuthRequiredSignal, lt as listPendingAgentInvocationMailboxAppends, n as executeAgentRun, qt as getConfigDefaults, t as createAgentRunner, ut as listPendingAgentInvocationResults, v as publicArtifactHeaders, w as collectAttachmentGarbage, wt as resolveSandboxEgressProviderForHost, x as lookupSlackUserProfile, y as readPublicArtifact } from "./agent-runner-D7OboVb3.js";
|
|
2
|
+
import { f as escapeXml } from "./client-D8HnkOQX.js";
|
|
3
|
+
import { C as withContext, M as JUNIOR_WORKSPACE_SNAPSHOT_JOB_CALLBACK_ROUTE, R as normalizeIdentityEmail, T as withSpan, b as setSpanStatus, f as logException, k as JUNIOR_PLUGIN_TASK_CALLBACK_ROUTE, m as logWarn, p as logInfo, r as createRequestContext, v as setSentryUser, w as withLogContext, y as setSpanAttributes } from "./logging-CWxiIuCF.js";
|
|
3
4
|
import { n as captureMessage, r as continueTrace } from "./sentry-CgWZM_kV.js";
|
|
4
|
-
import { t as JUNIOR_VERSION } from "./version-
|
|
5
|
-
import { _ as snapshotRuntimeConfig, d as restoreRuntimeConfig, f as setBotModelConfig, g as setSlackReactionConfig, h as setRuntimeLimits, i as getChatConfig, m as setProfiles, n as botConfig, o as getSlackBotToken, p as setCrossActorMidRunMode, u as resetRuntimeConfig } from "./config-
|
|
5
|
+
import { t as JUNIOR_VERSION } from "./version-BcdZy2zg.js";
|
|
6
|
+
import { _ as snapshotRuntimeConfig, d as restoreRuntimeConfig, f as setBotModelConfig, g as setSlackReactionConfig, h as setRuntimeLimits, i as getChatConfig, m as setProfiles, n as botConfig, o as getSlackBotToken, p as setCrossActorMidRunMode, u as resetRuntimeConfig } from "./config-CJRnWK9H.js";
|
|
6
7
|
import { _ as slackMessageTsSchema, d as parseSlackTeamId, f as parseSlackUserId, h as slackUserIdSchema, m as slackTeamIdSchema, o as parseActorUserId, p as slackChannelIdSchema } from "./actor-BTtLaeWy.js";
|
|
7
|
-
import {
|
|
8
|
-
import { a as juniorAgentInvocations, d as juniorDestinations, i as juniorAgentBindings, s as juniorConversations } from "./agent-invocations-
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
8
|
+
import { F as recordGoogleSignIn, L as slackUserHasGoogleSignIn, M as findSlackIdentityUserId, R as upsertIdentity, d as getConversationStore, f as getDb, h as withConversationEventLock, i as readActorIdentity, l as getConversationEventStore, n as findUserByEmailFromSql, p as getSqlExecutor, v as withConversationMutationLock } from "./viewer-DSZXygMU.js";
|
|
9
|
+
import { a as juniorAgentInvocations, d as juniorDestinations, i as juniorAgentBindings, s as juniorConversations } from "./agent-invocations-Bdu3ROfo.js";
|
|
10
|
+
import { a as juniorAutomationRuns, f as juniorConversationBriefs, h as juniorConversationEvents, s as juniorAutomations } from "./executor-Do0THDRR.js";
|
|
11
|
+
import { m as credentialLogContext } from "./registry-sLTr-sV9.js";
|
|
12
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-CZzhGQ7F.js";
|
|
13
|
+
import { C as automationVersionOf, Kt as projectConversationMessages, P as slackMention, U as slackEntitySchema, Ut as appendConversationMessages, W as slackExternalRefIdSchema, Yt as settleConversationTitleWork, b as admitEventAutomationRun, c as withScheduledAutomationLock, i as readScheduledAutomation, j as formatSlackLink, l as blockEventAutomation, n as listScheduledAutomationsForTeam, o as saveScheduledAutomationInLock, p as findMatchingEventAutomations, s as scheduledRunId, u as collectEventAutomationMatchKeys, x as eventAutomationRunLimitReason } from "./tasks-D_WIJtvO.js";
|
|
14
|
+
import { $ as bindSlackDirectCredentialSubject, At as resolveRootVisibility, Ht as getSlackClient, Jt as getWorkspaceTeamId, Nt as listConversationAnnotations, Q as bindScheduledAutomationCredentialSubject, U as collectEventMatchKeys, Wt as isDmChannel, X as takeDueTimerWatches, Yt as runWithWorkspaceTeamId, Z as bindEventAutomationCredentialSubject, a as getPluginRoutes, at as getDashboardConversationLink, ct as setDashboardConversationLinkOptions, d as setPlugins, et as verifyEventAutomationCredentialSubject, f as validatePlugins, ft as createPluginLogger, h as setBriefsConfig, i as getPluginApiRoutes, lt as createPluginState, nt as verifySlackDirectCredentialSubject, pt as readConversationAccessFromSql, q as findMatchingWatches, st as getDashboardTaskLink, tt as verifyScheduledAutomationCredentialSubject, u as getPlugins } from "./agent-hooks-BjtTzvru.js";
|
|
15
|
+
import { W as annotationCard } from "./conversation-CKNLYBJt.js";
|
|
16
|
+
import { O as withLock$1, w as getStateAdapter } from "./profile-h-72wM78.js";
|
|
17
|
+
import { c as getThreadStopDecision } from "./pending-auth-s_QFribU.js";
|
|
18
|
+
import { b as eventGuidance, f as slackChannelMessageEvent, k as renderTaskInput, l as getEventCatalog, m as slackChannelMessageRunsAutomation, p as slackChannelMessageReplyThreadTs, u as SLACK_EVENT_NAMESPACE } from "./session-stop-CQjKd4a2.js";
|
|
17
19
|
import { n as getNextRunAtMs } from "./cadence-D00SCzT1.js";
|
|
18
|
-
import { f as setSandboxResourceConfig, u as getSandboxResourceConfig } from "./resolve-
|
|
19
|
-
import { a as getVercelConversationWorkQueue, g as isConversationQueueMessageRejectedError, h as conversationQueueMessageSchema, i as verifyWorkspaceSnapshotJobMessage, o as resolveConversationWorkQueueTopic, s as verifyConversationQueueMessage, t as WORKSPACE_SNAPSHOT_JOB_QUEUE_TOPIC } from "./job-queue-
|
|
20
|
-
import { A as hasRunnableConversationWork$1, M as isInvalidConversationRecordError, O as CONVERSATION_WORK_STALE_ENQUEUE_MS, S as removeActiveConversation, _ as hasRunnableConversationWork, c as clearExpiredConversationLease, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, v as listActiveConversationIds } from "./store-
|
|
20
|
+
import { f as setSandboxResourceConfig, u as getSandboxResourceConfig } from "./resolve-ncQPZROQ.js";
|
|
21
|
+
import { a as getVercelConversationWorkQueue, g as isConversationQueueMessageRejectedError, h as conversationQueueMessageSchema, i as verifyWorkspaceSnapshotJobMessage, o as resolveConversationWorkQueueTopic, s as verifyConversationQueueMessage, t as WORKSPACE_SNAPSHOT_JOB_QUEUE_TOPIC } from "./job-queue-B6SV9uVa.js";
|
|
22
|
+
import { A as hasRunnableConversationWork$1, M as isInvalidConversationRecordError, O as CONVERSATION_WORK_STALE_ENQUEUE_MS, S as removeActiveConversation, _ as hasRunnableConversationWork, c as clearExpiredConversationLease, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, v as listActiveConversationIds } from "./store-DXFIQy-G.js";
|
|
21
23
|
import { D as scheduledRunSchema } from "./automation-CHdlHVZP.js";
|
|
22
|
-
import { i as setExperimentalFeatures, n as getExperimentalFeatures, r as isExperimentalFeatureEnabled } from "./experimental-
|
|
23
|
-
import { n as validatePluginRegistrations, t as validatePluginEgressCredentialHooks } from "./validation-
|
|
24
|
+
import { i as setExperimentalFeatures, n as getExperimentalFeatures, r as isExperimentalFeatureEnabled } from "./experimental-CQIvMUO7.js";
|
|
25
|
+
import { n as validatePluginRegistrations, t as validatePluginEgressCredentialHooks } from "./validation-CXYxaNYx.js";
|
|
24
26
|
import { a as pluginRuntimeRegistrationsFromPluginSet, n as pluginCatalogConfigFromEnv, r as pluginCatalogConfigFromPluginSet, t as defineJuniorPlugins } from "./plugins-egKWjgue.js";
|
|
25
|
-
import { A as
|
|
26
|
-
import { M as recordAutomationExecution, S as existingConversationId, a as confirmDispatchMailboxAppend, b as notifyAutomationBlocked, f as getPluginDispatchProjection, m as listPendingDispatchMailboxAppends, o as createOrGetDispatch, p as isTerminalDispatchStatus, s as getDispatchConversationId, t as processPluginTask, u as getDispatchRecord, v as markDispatchFailed } from "./task-runner-
|
|
27
|
-
import { o as getAuthPausedConversationTurnId, t as enqueueAgentInvocationResult } from "./result-
|
|
28
|
-
import { r as enqueueAgentInvocation } from "./work-
|
|
29
|
-
import { s as coerceThreadConversationState } from "./thread-state-
|
|
30
|
-
import { o as unlinkProvider } from "./connections-
|
|
31
|
-
import { a as GET$4, i as publishAppHomeView, r as GET$5, t as receiveLocalOAuthCredential } from "./credential-sync-
|
|
32
|
-
import { a as registerVercelPluginTaskDevConsumer, n as createVercelPluginTaskCallback, s as queueCallback } from "./task-queue-
|
|
33
|
-
import { t as processConversationWork } from "./worker-
|
|
34
|
-
import { t as bindMessageAgent } from "./message-
|
|
35
|
-
import { t as bindSpawnAgent } from "./spawn-
|
|
27
|
+
import { A as THREAD_OPTOUT_ACK, C as buildSlackInboundMessage, D as normalizeIncomingSlackThreadId, E as stopSlackThread, F as renderSlackObjectCard, I as createVercelAttachmentStorage, M as parseContent, N as runWithSlackInstallation, O as withNormalizedThreadId, P as verifySlackSignature, R as enqueueAgentDispatch, S as recordSkippedConversationMessage, T as isSlackMessageStopped, a as recordWebConversationActivity, c as createProductionSlackWebhookServices, f as createSlackWebApiStatusSender, j as startProcessingReaction, k as stopConversationTurn, l as getProductionSlackAdapter, n as conversationTurnIdForMessage, o as webActorFromEmail, s as createProductionConversationWorkOptions, t as appendAndEnqueueWebMessage, v as readViewerAutomationSummary, w as clearSlackPendingReactions, z as GET$3 } from "./web-input-B9Bv0_lR.js";
|
|
28
|
+
import { M as recordAutomationExecution, S as existingConversationId, a as confirmDispatchMailboxAppend, b as notifyAutomationBlocked, f as getPluginDispatchProjection, m as listPendingDispatchMailboxAppends, o as createOrGetDispatch, p as isTerminalDispatchStatus, s as getDispatchConversationId, t as processPluginTask, u as getDispatchRecord, v as markDispatchFailed } from "./task-runner-DUjvzYXj.js";
|
|
29
|
+
import { o as getAuthPausedConversationTurnId, t as enqueueAgentInvocationResult } from "./result-C883vngT.js";
|
|
30
|
+
import { r as enqueueAgentInvocation } from "./work-nAfDSUFP.js";
|
|
31
|
+
import { s as coerceThreadConversationState } from "./thread-state-Ch4yuCqG.js";
|
|
32
|
+
import { o as unlinkProvider } from "./connections-ClYrKzpX.js";
|
|
33
|
+
import { a as GET$4, i as publishAppHomeView, o as htmlCallbackResponse, r as GET$5, t as receiveLocalOAuthCredential } from "./credential-sync-BXfohNmJ.js";
|
|
34
|
+
import { a as registerVercelPluginTaskDevConsumer, n as createVercelPluginTaskCallback, s as queueCallback } from "./task-queue-B17vV0La.js";
|
|
35
|
+
import { t as processConversationWork } from "./worker-vGXZ6XFT.js";
|
|
36
|
+
import { t as bindMessageAgent } from "./message-TUOdbSBW.js";
|
|
37
|
+
import { t as bindSpawnAgent } from "./spawn-DY9CLXDL.js";
|
|
36
38
|
import { createRequire } from "node:module";
|
|
37
39
|
import { Hono } from "hono";
|
|
38
40
|
import { z } from "zod";
|
|
@@ -44,6 +46,8 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
|
44
46
|
import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
45
47
|
import { ChannelImpl, ThreadImpl } from "chat";
|
|
46
48
|
import * as acp from "@agentclientprotocol/sdk";
|
|
49
|
+
import { createAuthMiddleware } from "better-auth/api";
|
|
50
|
+
import { betterAuth } from "better-auth/minimal";
|
|
47
51
|
//#region src/chat/events/timers.ts
|
|
48
52
|
/** Publish a bounded batch of due timers; the mailbox owns agent execution. */
|
|
49
53
|
async function runTimerWatchHeartbeat(args) {
|
|
@@ -345,14 +349,16 @@ async function getScheduledAutomationDispatch(id) {
|
|
|
345
349
|
//#endregion
|
|
346
350
|
//#region src/chat/event-automations/runs.ts
|
|
347
351
|
/**
|
|
348
|
-
* Record a dispatched event automation run
|
|
349
|
-
* exists. The dispatch can finish before
|
|
350
|
-
* retry of the same Event, keeps the row it
|
|
352
|
+
* Record a dispatched event automation run with the Automation version it ran,
|
|
353
|
+
* and link its Conversation when it exists. The dispatch can finish before
|
|
354
|
+
* this write, so a stored run, or a retry of the same Event, keeps the row it
|
|
355
|
+
* has.
|
|
351
356
|
*/
|
|
352
357
|
async function recordEventAutomationRunStarted(db, args) {
|
|
353
358
|
await db.insert(juniorAutomationRuns).values({
|
|
354
359
|
id: args.dispatchId,
|
|
355
|
-
automationId: args.
|
|
360
|
+
automationId: args.automation.id,
|
|
361
|
+
automationVersion: automationVersionOf(args.automation),
|
|
356
362
|
conversationId: existingConversationId(getDispatchConversationId({ id: args.dispatchId })),
|
|
357
363
|
startedAtMs: args.nowMs,
|
|
358
364
|
status: "running"
|
|
@@ -419,9 +425,14 @@ async function readRun(db, runId) {
|
|
|
419
425
|
const rows = await db.select().from(juniorAutomationRuns).where(eq(juniorAutomationRuns.id, runId)).limit(1);
|
|
420
426
|
return rows[0] ? parseScheduledRunRow(rows[0]) : void 0;
|
|
421
427
|
}
|
|
422
|
-
|
|
428
|
+
/**
|
|
429
|
+
* Save a Scheduled automation run. Pass `automationVersion` only when the run
|
|
430
|
+
* is dispatched. Later writes keep the stored version.
|
|
431
|
+
*/
|
|
432
|
+
async function writeRun(db, run, automationVersion) {
|
|
423
433
|
const stored = scheduledRunSchema.parse(run);
|
|
424
434
|
const columns = {
|
|
435
|
+
...automationVersion ? { automationVersion } : void 0,
|
|
425
436
|
attempt: stored.attempt,
|
|
426
437
|
automationId: stored.taskId,
|
|
427
438
|
claimedAtMs: stored.claimedAtMs,
|
|
@@ -588,14 +599,14 @@ async function claimDueScheduledRun(db, args) {
|
|
|
588
599
|
for (const skipped of skippedRuns) logScheduledAutomationRunSkipped(skipped.task, skipped.run, { "app.task.run.error": skipped.errorMessage });
|
|
589
600
|
return claimed;
|
|
590
601
|
}
|
|
591
|
-
async function updateRun(db, runId, update) {
|
|
602
|
+
async function updateRun(db, runId, update, automationVersion) {
|
|
592
603
|
return await db.transaction(async (tx) => {
|
|
593
604
|
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${`junior:scheduler:run:${runId}:lock`}))`);
|
|
594
605
|
const current = await readRun(tx, runId);
|
|
595
606
|
if (!current) return void 0;
|
|
596
607
|
const next = update(current);
|
|
597
608
|
if (!next) return void 0;
|
|
598
|
-
await writeRun(tx, next);
|
|
609
|
+
await writeRun(tx, next, automationVersion);
|
|
599
610
|
return next;
|
|
600
611
|
});
|
|
601
612
|
}
|
|
@@ -603,14 +614,17 @@ function canFinishRun(run, startedAtMs) {
|
|
|
603
614
|
if (run.status === "pending") return startedAtMs === void 0;
|
|
604
615
|
return run.status === "running" && run.startedAtMs === startedAtMs;
|
|
605
616
|
}
|
|
606
|
-
/**
|
|
617
|
+
/**
|
|
618
|
+
* Mark a pending run dispatched only for the active claim, and store the
|
|
619
|
+
* version of `task` that the dispatch ran.
|
|
620
|
+
*/
|
|
607
621
|
async function markScheduledRunDispatched(db, args) {
|
|
608
622
|
return await updateRun(db, args.runId, (run) => run.status === "pending" && run.claimedAtMs === args.claimedAtMs ? {
|
|
609
623
|
...run,
|
|
610
624
|
dispatchId: args.dispatchId,
|
|
611
625
|
startedAtMs: args.nowMs,
|
|
612
626
|
status: "running"
|
|
613
|
-
} : void 0);
|
|
627
|
+
} : void 0, automationVersionOf(args.task));
|
|
614
628
|
}
|
|
615
629
|
/** Complete a run only for its active start timestamp. */
|
|
616
630
|
async function markScheduledRunCompleted(db, args) {
|
|
@@ -972,7 +986,8 @@ async function runScheduledAutomationHeartbeat(args) {
|
|
|
972
986
|
claimedAtMs: run.claimedAtMs,
|
|
973
987
|
dispatchId: dispatch.id,
|
|
974
988
|
nowMs: args.nowMs,
|
|
975
|
-
runId: run.id
|
|
989
|
+
runId: run.id,
|
|
990
|
+
task
|
|
976
991
|
});
|
|
977
992
|
logInfo("scheduled_automation.run.dispatched", scheduledAutomationRunAttributes(task, dispatched ?? {
|
|
978
993
|
...run,
|
|
@@ -1604,11 +1619,26 @@ function buildUpstreamUrl(request) {
|
|
|
1604
1619
|
};
|
|
1605
1620
|
}
|
|
1606
1621
|
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Name the Conversation and the accountable person from the verified token.
|
|
1624
|
+
* Each proxy request is a new HTTP request with no run log context, so the
|
|
1625
|
+
* proxy binds the same fields that the run binds.
|
|
1626
|
+
*/
|
|
1627
|
+
function sandboxEgressLogContext(context) {
|
|
1628
|
+
return {
|
|
1629
|
+
...context.conversationId ? { conversationId: context.conversationId } : void 0,
|
|
1630
|
+
...credentialLogContext(context.credentials)
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1607
1633
|
/** Continue inbound trace context only after sandbox egress verification succeeds. */
|
|
1608
|
-
function continueSandboxEgressTrace(request, upstreamHost, tracePropagation, callback) {
|
|
1634
|
+
function continueSandboxEgressTrace(request, upstreamHost, tracePropagation, credentialContext, callback) {
|
|
1609
1635
|
const sentryTrace = request.headers.get("sentry-trace") ?? void 0;
|
|
1610
1636
|
const baggage = request.headers.get("baggage") ?? void 0;
|
|
1611
|
-
const
|
|
1637
|
+
const logContext = sandboxEgressLogContext(credentialContext);
|
|
1638
|
+
const run = () => withSpan("sandbox.egress", "http.server", logContext, async () => {
|
|
1639
|
+
setSentryUser(logContext.userId ? { id: logContext.userId } : void 0);
|
|
1640
|
+
return await callback();
|
|
1641
|
+
}, {
|
|
1612
1642
|
"http.request.method": request.method,
|
|
1613
1643
|
"url.path": redactedProxyPath(new URL(request.url).pathname)
|
|
1614
1644
|
});
|
|
@@ -1697,7 +1727,7 @@ async function proxySandboxEgressRequest(request, deps = {}) {
|
|
|
1697
1727
|
});
|
|
1698
1728
|
return jsonError("Sandbox egress credential context is not authorized", 403);
|
|
1699
1729
|
}
|
|
1700
|
-
return await continueSandboxEgressTrace(request, upstreamUrl.hostname, deps.tracePropagation ?? {}, async () => await executeCredentialedEgressRequest({
|
|
1730
|
+
return await continueSandboxEgressTrace(request, upstreamUrl.hostname, deps.tracePropagation ?? {}, credentialContext, async () => await executeCredentialedEgressRequest({
|
|
1701
1731
|
activeEgressId,
|
|
1702
1732
|
credentialContext,
|
|
1703
1733
|
deps,
|
|
@@ -1933,6 +1963,174 @@ const slackInteractivePayloadSchema = z.object({
|
|
|
1933
1963
|
}).optional()
|
|
1934
1964
|
});
|
|
1935
1965
|
//#endregion
|
|
1966
|
+
//#region src/chat/ingress/slash-command.ts
|
|
1967
|
+
async function postEphemeral(event, text) {
|
|
1968
|
+
await event.channel.postEphemeral(event.userId, text, { fallbackToDM: false });
|
|
1969
|
+
}
|
|
1970
|
+
function getCommandName() {
|
|
1971
|
+
return getChatConfig().slack.slashCommand;
|
|
1972
|
+
}
|
|
1973
|
+
async function handleLink(event, provider) {
|
|
1974
|
+
if (!pluginCatalogRuntime.isProvider(provider)) {
|
|
1975
|
+
await postEphemeral(event, `Unknown provider: \`${provider}\``);
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
if (!pluginCatalogRuntime.getOAuthConfig(provider)) {
|
|
1979
|
+
await postEphemeral(event, `${formatProviderLabel(provider)} doesn't support account linking.`);
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
const result = await startOAuthFlow(provider, {
|
|
1983
|
+
actorId: event.userId,
|
|
1984
|
+
actor: {
|
|
1985
|
+
platform: "slack",
|
|
1986
|
+
teamId: event.teamId,
|
|
1987
|
+
userId: event.userId
|
|
1988
|
+
},
|
|
1989
|
+
channelId: event.channelId
|
|
1990
|
+
});
|
|
1991
|
+
if (!result.ok) {
|
|
1992
|
+
await postEphemeral(event, `Failed to start linking: ${result.error}`);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
if (result.delivery === "fallback_dm") await postEphemeral(event, `Check your DMs for a ${formatProviderLabel(provider)} authorization link.`);
|
|
1996
|
+
else if (result.delivery === false) await postEphemeral(event, "I wasn't able to send you a private authorization link. Please try again in a direct message.");
|
|
1997
|
+
}
|
|
1998
|
+
async function handleUnlink(event, provider) {
|
|
1999
|
+
if (!pluginCatalogRuntime.isProvider(provider)) {
|
|
2000
|
+
await postEphemeral(event, `Unknown provider: \`${provider}\``);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
if (!(Boolean(pluginCatalogRuntime.getOAuthConfig(provider)) || Boolean(pluginCatalogRuntime.getDefinition(provider)?.manifest.mcp))) {
|
|
2004
|
+
await postEphemeral(event, `${formatProviderLabel(provider)} doesn't support account unlinking.`);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
const tokenStore = createUserTokenStore();
|
|
2008
|
+
await unlinkProvider(event.userId, provider, tokenStore, event.teamId);
|
|
2009
|
+
logInfo("slash_command.credential.unlinked", { "app.credential.provider": provider });
|
|
2010
|
+
await postEphemeral(event, `Your ${formatProviderLabel(provider)} account has been unlinked.`);
|
|
2011
|
+
}
|
|
2012
|
+
/** Route link and unlink slash commands to the appropriate OAuth flow. */
|
|
2013
|
+
async function handleSlashCommand(event) {
|
|
2014
|
+
const [subcommand, provider, ...rest] = event.text.trim().split(/\s+/);
|
|
2015
|
+
if (!subcommand || !["link", "unlink"].includes(subcommand)) {
|
|
2016
|
+
await postEphemeral(event, `Usage: \`${getCommandName()} link <provider>\` or \`${getCommandName()} unlink <provider>\``);
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
if (!provider || rest.length > 0) {
|
|
2020
|
+
await postEphemeral(event, `Usage: \`${getCommandName()} ${subcommand} <provider>\``);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
const normalized = provider.toLowerCase();
|
|
2024
|
+
if (subcommand === "link") await handleLink(event, normalized);
|
|
2025
|
+
else await handleUnlink(event, normalized);
|
|
2026
|
+
}
|
|
2027
|
+
//#endregion
|
|
2028
|
+
//#region src/chat/ingress/slack-form.ts
|
|
2029
|
+
function parseJson$1(body) {
|
|
2030
|
+
try {
|
|
2031
|
+
return JSON.parse(body);
|
|
2032
|
+
} catch {
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
function requireSlackPayloadUserId(value, source) {
|
|
2037
|
+
const userId = parseActorUserId(value);
|
|
2038
|
+
if (!userId) throw new Error(`${source} is missing a Slack user id`);
|
|
2039
|
+
return userId;
|
|
2040
|
+
}
|
|
2041
|
+
async function handleSlashCommandForm(args) {
|
|
2042
|
+
const { channel_id: channelId, team_id: teamId, user_id: userId } = args.form;
|
|
2043
|
+
const channel = new ChannelImpl({
|
|
2044
|
+
id: `slack:${channelId}`,
|
|
2045
|
+
adapter: args.adapter,
|
|
2046
|
+
stateAdapter: args.state
|
|
2047
|
+
});
|
|
2048
|
+
await withSpan("chat.slash_command", "chat.slash_command", { userId }, async () => {
|
|
2049
|
+
await handleSlashCommand({
|
|
2050
|
+
channel,
|
|
2051
|
+
channelId,
|
|
2052
|
+
teamId,
|
|
2053
|
+
text: args.form.text,
|
|
2054
|
+
userId
|
|
2055
|
+
});
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
async function handleInteractivePayload(args) {
|
|
2059
|
+
if (args.payload.type !== "block_actions") return;
|
|
2060
|
+
const action = args.payload.actions?.find((candidate) => candidate.action_id === "app_home_disconnect");
|
|
2061
|
+
const provider = action?.selected_option?.value ?? action?.value;
|
|
2062
|
+
if (!provider) return;
|
|
2063
|
+
const userId = requireSlackPayloadUserId(args.payload.user?.id, "Slack app home disconnect payload");
|
|
2064
|
+
await withSpan("chat.app_home_disconnect", "chat.app_home_disconnect", { userId }, async () => {
|
|
2065
|
+
try {
|
|
2066
|
+
await unlinkProvider(userId, provider, args.userTokenStore, getWorkspaceTeamId());
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
logException(error, "app_home.disconnect_unlink.failed", { "app.credential.provider": provider });
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
await publishAppHomeView(getSlackClient(), userId, args.userTokenStore);
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
logException(error, "app_home.disconnect_publish.failed", { "app.credential.provider": provider });
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
function installationFromInteractive(payload) {
|
|
2078
|
+
return { teamId: payload.team?.id ?? payload.user?.team_id };
|
|
2079
|
+
}
|
|
2080
|
+
/** Handle a Slack slash command or interactive payload form. */
|
|
2081
|
+
async function handleSlackForm(args) {
|
|
2082
|
+
const params = new URLSearchParams(args.body);
|
|
2083
|
+
const { adapter, state } = args;
|
|
2084
|
+
await state.connect();
|
|
2085
|
+
if (params.has("command") && !params.has("payload")) {
|
|
2086
|
+
const result = slackSlashCommandSchema.safeParse(Object.fromEntries(params));
|
|
2087
|
+
if (!result.success) return new Response("Invalid slash command payload", { status: 400 });
|
|
2088
|
+
const form = result.data;
|
|
2089
|
+
const installation = {
|
|
2090
|
+
teamId: form.team_id,
|
|
2091
|
+
enterpriseId: form.enterprise_id,
|
|
2092
|
+
isEnterpriseInstall: form.is_enterprise_install === "true"
|
|
2093
|
+
};
|
|
2094
|
+
args.waitUntil(withLogContext({
|
|
2095
|
+
platform: "slack",
|
|
2096
|
+
userId: form.user_id
|
|
2097
|
+
}, () => runWithWorkspaceTeamId(installation.teamId, () => runWithSlackInstallation({
|
|
2098
|
+
adapter,
|
|
2099
|
+
installation,
|
|
2100
|
+
state,
|
|
2101
|
+
task: () => handleSlashCommandForm({
|
|
2102
|
+
adapter,
|
|
2103
|
+
form,
|
|
2104
|
+
state
|
|
2105
|
+
})
|
|
2106
|
+
})).catch((error) => {
|
|
2107
|
+
logException(error, "slash_command.failed");
|
|
2108
|
+
})));
|
|
2109
|
+
return new Response("", { status: 200 });
|
|
2110
|
+
}
|
|
2111
|
+
const rawPayload = params.get("payload");
|
|
2112
|
+
if (!rawPayload) return new Response("Missing payload", { status: 400 });
|
|
2113
|
+
const result = slackInteractivePayloadSchema.safeParse(parseJson$1(rawPayload));
|
|
2114
|
+
if (!result.success) return new Response("Invalid payload JSON", { status: 400 });
|
|
2115
|
+
const payload = result.data;
|
|
2116
|
+
const installation = installationFromInteractive(payload);
|
|
2117
|
+
args.waitUntil(withLogContext({
|
|
2118
|
+
platform: "slack",
|
|
2119
|
+
userId: payload.user?.id?.trim() || void 0
|
|
2120
|
+
}, () => runWithWorkspaceTeamId(installation.teamId, () => runWithSlackInstallation({
|
|
2121
|
+
adapter,
|
|
2122
|
+
installation,
|
|
2123
|
+
state,
|
|
2124
|
+
task: () => handleInteractivePayload({
|
|
2125
|
+
payload,
|
|
2126
|
+
userTokenStore: args.getUserTokenStore()
|
|
2127
|
+
})
|
|
2128
|
+
})).catch((error) => {
|
|
2129
|
+
logException(error, "slack.interactive.payload.failed");
|
|
2130
|
+
})));
|
|
2131
|
+
return new Response("", { status: 200 });
|
|
2132
|
+
}
|
|
2133
|
+
//#endregion
|
|
1936
2134
|
//#region src/chat/ingress/bot-mention.ts
|
|
1937
2135
|
/**
|
|
1938
2136
|
* Detect Slack bot @mentions that should activate Junior.
|
|
@@ -2043,7 +2241,7 @@ async function ingestEventAutomations(input, options) {
|
|
|
2043
2241
|
const event = eventSchema.parse(input);
|
|
2044
2242
|
const db = getDb();
|
|
2045
2243
|
const nowMs = options.nowMs ?? Date.now();
|
|
2046
|
-
const tasks = await findMatchingEventAutomations(db, event, options.teamId);
|
|
2244
|
+
const tasks = (await findMatchingEventAutomations(db, event, options.teamId)).filter((task) => slackChannelMessageRunsAutomation(event, task));
|
|
2047
2245
|
let dispatched = 0;
|
|
2048
2246
|
const errors = [];
|
|
2049
2247
|
for (const task of tasks) try {
|
|
@@ -2108,7 +2306,7 @@ async function ingestEventAutomations(input, options) {
|
|
|
2108
2306
|
}
|
|
2109
2307
|
});
|
|
2110
2308
|
await recordEventAutomationRunStarted(db, {
|
|
2111
|
-
|
|
2309
|
+
automation: task,
|
|
2112
2310
|
dispatchId: dispatch.id,
|
|
2113
2311
|
nowMs
|
|
2114
2312
|
});
|
|
@@ -2167,6 +2365,170 @@ async function isSlackWorkspaceMember(raw, state) {
|
|
|
2167
2365
|
return result.value;
|
|
2168
2366
|
}
|
|
2169
2367
|
//#endregion
|
|
2368
|
+
//#region src/chat/sign-in/allowlist.ts
|
|
2369
|
+
/** Google accounts that may sign in. The dashboard uses the same list. */
|
|
2370
|
+
/**
|
|
2371
|
+
* Read a list env var the same way as the dashboard: a JSON string array or
|
|
2372
|
+
* comma-separated values.
|
|
2373
|
+
*/
|
|
2374
|
+
function readEnvList(name) {
|
|
2375
|
+
const value = process.env[name]?.trim();
|
|
2376
|
+
if (!value) return void 0;
|
|
2377
|
+
if (!value.startsWith("[")) return value.split(",");
|
|
2378
|
+
let parsed;
|
|
2379
|
+
try {
|
|
2380
|
+
parsed = JSON.parse(value);
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
throw new Error(`${name} must be a JSON string array`, { cause: error });
|
|
2383
|
+
}
|
|
2384
|
+
const list = z.array(z.string()).safeParse(parsed);
|
|
2385
|
+
if (!list.success) throw new Error(`${name} must be a JSON string array`);
|
|
2386
|
+
return list.data;
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Resolve the sign-in allowlist. Use the same values as the dashboard: its
|
|
2390
|
+
* options when set, else its environment variables.
|
|
2391
|
+
*/
|
|
2392
|
+
function resolveSignInAllowlist(options = {}) {
|
|
2393
|
+
const list = (values, name) => (values ?? readEnvList(name) ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
2394
|
+
return {
|
|
2395
|
+
domains: list(options.allowedGoogleDomains, "JUNIOR_DASHBOARD_GOOGLE_DOMAINS"),
|
|
2396
|
+
emails: list(options.allowedEmails, "JUNIOR_DASHBOARD_ALLOWED_EMAILS")
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
/** Return whether a normalized Google email is on the allowlist. */
|
|
2400
|
+
function isSignInAllowed(email, allowlist) {
|
|
2401
|
+
const domain = email.slice(email.lastIndexOf("@") + 1);
|
|
2402
|
+
return allowlist.emails.includes(email) || allowlist.domains.includes(domain);
|
|
2403
|
+
}
|
|
2404
|
+
//#endregion
|
|
2405
|
+
//#region src/chat/sign-in/slack.ts
|
|
2406
|
+
/**
|
|
2407
|
+
* Slack sign-in through the shared Google sign-in session.
|
|
2408
|
+
*
|
|
2409
|
+
* Slack sends a person here when they have no recent Google sign-in. The page
|
|
2410
|
+
* uses the browser session that the dashboard also uses. When the person has
|
|
2411
|
+
* no session, or an old one, it starts a Google sign-in and comes back here.
|
|
2412
|
+
* The sign-in counts only when its Google email belongs to the user of the
|
|
2413
|
+
* Slack identity that got the link.
|
|
2414
|
+
*/
|
|
2415
|
+
const SLACK_SIGN_IN_PATH = "/api/sign-in/slack";
|
|
2416
|
+
const SIGN_IN_STATE_TTL_MS = 6e5;
|
|
2417
|
+
const signInStateSchema = z.object({
|
|
2418
|
+
redirected: z.boolean(),
|
|
2419
|
+
slackTeamId: z.string().min(1),
|
|
2420
|
+
slackUserId: z.string().min(1)
|
|
2421
|
+
});
|
|
2422
|
+
function stateKey(state) {
|
|
2423
|
+
return `sign-in:v2:${state}`;
|
|
2424
|
+
}
|
|
2425
|
+
function page(title, message, status) {
|
|
2426
|
+
return htmlCallbackResponse(escapeXml(title), escapeXml(message), status);
|
|
2427
|
+
}
|
|
2428
|
+
/** Store one Slack sign-in request and return its link. */
|
|
2429
|
+
async function createSlackSignInUrl(args) {
|
|
2430
|
+
const state = randomBytes(24).toString("base64url");
|
|
2431
|
+
await args.state.set(stateKey(state), {
|
|
2432
|
+
redirected: false,
|
|
2433
|
+
slackTeamId: args.slackTeamId,
|
|
2434
|
+
slackUserId: args.slackUserId
|
|
2435
|
+
}, SIGN_IN_STATE_TTL_MS);
|
|
2436
|
+
const url = new URL(SLACK_SIGN_IN_PATH, args.baseURL);
|
|
2437
|
+
url.searchParams.set("state", state);
|
|
2438
|
+
return url.toString();
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Finish a Slack sign-in when the browser has a recent Google sign-in for the
|
|
2442
|
+
* user of the Slack identity. Otherwise start one Google sign-in.
|
|
2443
|
+
*/
|
|
2444
|
+
async function handleSlackSignIn(args) {
|
|
2445
|
+
const token = new URL(args.request.url).searchParams.get("state");
|
|
2446
|
+
const parsed = token ? signInStateSchema.safeParse(await args.state.get(stateKey(token))) : void 0;
|
|
2447
|
+
if (!token || !parsed?.success) return page("Sign-in link expired", `Mention ${botConfig.userName} in Slack again to get a new link.`, 400);
|
|
2448
|
+
const stored = parsed.data;
|
|
2449
|
+
const session = await args.auth.getSession(args.request);
|
|
2450
|
+
const email = session?.user.emailVerified ? normalizeIdentityEmail(session.user.email) : void 0;
|
|
2451
|
+
let failure;
|
|
2452
|
+
if (email && !isSignInAllowed(email, args.allowlist)) failure = page("Account not allowed", `${email} cannot use ${botConfig.userName}. Sign in with your work account.`, 403);
|
|
2453
|
+
else if (email) {
|
|
2454
|
+
const executor = getSqlExecutor();
|
|
2455
|
+
const userId = await findSlackIdentityUserId(executor, stored.slackTeamId, stored.slackUserId);
|
|
2456
|
+
const user = await findUserByEmailFromSql(executor.db(), email);
|
|
2457
|
+
if (!userId || user?.id !== userId) failure = page("Slack account does not match", `Your Slack email is not ${email}. Sign in with the account that matches your Slack email.`, 403);
|
|
2458
|
+
else if (await slackUserHasGoogleSignIn(executor, {
|
|
2459
|
+
signedInSince: new Date(Date.now() - args.signIn.maxAgeMs),
|
|
2460
|
+
slackTeamId: stored.slackTeamId,
|
|
2461
|
+
slackUserId: stored.slackUserId
|
|
2462
|
+
})) {
|
|
2463
|
+
await args.state.delete(stateKey(token));
|
|
2464
|
+
return page("You are signed in", "Return to Slack and send your message again.", 200);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
if (stored.redirected) return failure ?? page("Sign-in failed", "Google sign-in did not finish. Sign in with your work account.", 400);
|
|
2468
|
+
await args.state.set(stateKey(token), {
|
|
2469
|
+
...stored,
|
|
2470
|
+
redirected: true
|
|
2471
|
+
}, SIGN_IN_STATE_TTL_MS);
|
|
2472
|
+
const returnUrl = new URL(SLACK_SIGN_IN_PATH, args.signIn.baseURL);
|
|
2473
|
+
returnUrl.searchParams.set("state", token);
|
|
2474
|
+
return await args.auth.signInWithGoogle(args.request, returnUrl.toString(), returnUrl.toString());
|
|
2475
|
+
}
|
|
2476
|
+
//#endregion
|
|
2477
|
+
//#region src/chat/ingress/slack-sign-in.ts
|
|
2478
|
+
/** Require a recent Google sign-in before a Slack user can start a Turn. */
|
|
2479
|
+
/**
|
|
2480
|
+
* Return whether this Slack user may start work.
|
|
2481
|
+
*
|
|
2482
|
+
* The user of the Slack identity must have signed in with Google, in Slack or
|
|
2483
|
+
* in the dashboard, within `signIn.maxAgeMs`. When it has not, save the Slack
|
|
2484
|
+
* identity with the email from the Slack profile, so the sign-in page can match
|
|
2485
|
+
* it, and check again. Then send the user a private sign-in link and drop the
|
|
2486
|
+
* message.
|
|
2487
|
+
*/
|
|
2488
|
+
async function hasSlackSignIn(args) {
|
|
2489
|
+
if (!args.signIn) return true;
|
|
2490
|
+
const teamId = args.installation.teamId;
|
|
2491
|
+
const channelId = args.event.channel;
|
|
2492
|
+
const userId = parseSlackUserId(args.event.user);
|
|
2493
|
+
if (!teamId || !channelId || !userId) return false;
|
|
2494
|
+
const executor = getSqlExecutor();
|
|
2495
|
+
const signedInSince = new Date(Date.now() - args.signIn.maxAgeMs);
|
|
2496
|
+
const hasSignIn = () => slackUserHasGoogleSignIn(executor, {
|
|
2497
|
+
signedInSince,
|
|
2498
|
+
slackTeamId: teamId,
|
|
2499
|
+
slackUserId: userId
|
|
2500
|
+
});
|
|
2501
|
+
if (await hasSignIn()) return true;
|
|
2502
|
+
const profile = await lookupSlackUserProfile(userId);
|
|
2503
|
+
await upsertIdentity(executor, {
|
|
2504
|
+
kind: "user",
|
|
2505
|
+
provider: "slack",
|
|
2506
|
+
providerTenantId: teamId,
|
|
2507
|
+
providerSubjectId: userId,
|
|
2508
|
+
...profile.real_name ? { displayName: profile.real_name } : void 0,
|
|
2509
|
+
...profile.name ? { handle: profile.name } : void 0,
|
|
2510
|
+
...profile.email ? {
|
|
2511
|
+
email: profile.email,
|
|
2512
|
+
emailVerified: true
|
|
2513
|
+
} : void 0,
|
|
2514
|
+
metadata: { platform: "slack" }
|
|
2515
|
+
});
|
|
2516
|
+
if (await hasSignIn()) return true;
|
|
2517
|
+
const link = await createSlackSignInUrl({
|
|
2518
|
+
baseURL: args.signIn.baseURL,
|
|
2519
|
+
slackTeamId: teamId,
|
|
2520
|
+
slackUserId: userId,
|
|
2521
|
+
state: args.state
|
|
2522
|
+
});
|
|
2523
|
+
await deliverPrivateMessage({
|
|
2524
|
+
channelId,
|
|
2525
|
+
...args.event.thread_ts ? { threadTs: args.event.thread_ts } : void 0,
|
|
2526
|
+
userId,
|
|
2527
|
+
text: `${formatSlackLink(link, `Sign in to use ${botConfig.userName}`)}. Sign in with your work Google account, then send your message again.`
|
|
2528
|
+
});
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
//#endregion
|
|
2170
2532
|
//#region src/chat/ingress/slack-message-telemetry.ts
|
|
2171
2533
|
function field(value, pattern) {
|
|
2172
2534
|
if (value === void 0) return "missing";
|
|
@@ -2192,68 +2554,6 @@ function slackMessageAttributes(body) {
|
|
|
2192
2554
|
};
|
|
2193
2555
|
}
|
|
2194
2556
|
//#endregion
|
|
2195
|
-
//#region src/chat/ingress/slash-command.ts
|
|
2196
|
-
async function postEphemeral(event, text) {
|
|
2197
|
-
await event.channel.postEphemeral(event.userId, text, { fallbackToDM: false });
|
|
2198
|
-
}
|
|
2199
|
-
function getCommandName() {
|
|
2200
|
-
return getChatConfig().slack.slashCommand;
|
|
2201
|
-
}
|
|
2202
|
-
async function handleLink(event, provider) {
|
|
2203
|
-
if (!pluginCatalogRuntime.isProvider(provider)) {
|
|
2204
|
-
await postEphemeral(event, `Unknown provider: \`${provider}\``);
|
|
2205
|
-
return;
|
|
2206
|
-
}
|
|
2207
|
-
if (!pluginCatalogRuntime.getOAuthConfig(provider)) {
|
|
2208
|
-
await postEphemeral(event, `${formatProviderLabel(provider)} doesn't support account linking.`);
|
|
2209
|
-
return;
|
|
2210
|
-
}
|
|
2211
|
-
const result = await startOAuthFlow(provider, {
|
|
2212
|
-
actorId: event.userId,
|
|
2213
|
-
actor: {
|
|
2214
|
-
platform: "slack",
|
|
2215
|
-
teamId: event.teamId,
|
|
2216
|
-
userId: event.userId
|
|
2217
|
-
},
|
|
2218
|
-
channelId: event.channelId
|
|
2219
|
-
});
|
|
2220
|
-
if (!result.ok) {
|
|
2221
|
-
await postEphemeral(event, `Failed to start linking: ${result.error}`);
|
|
2222
|
-
return;
|
|
2223
|
-
}
|
|
2224
|
-
if (result.delivery === "fallback_dm") await postEphemeral(event, `Check your DMs for a ${formatProviderLabel(provider)} authorization link.`);
|
|
2225
|
-
else if (result.delivery === false) await postEphemeral(event, "I wasn't able to send you a private authorization link. Please try again in a direct message.");
|
|
2226
|
-
}
|
|
2227
|
-
async function handleUnlink(event, provider) {
|
|
2228
|
-
if (!pluginCatalogRuntime.isProvider(provider)) {
|
|
2229
|
-
await postEphemeral(event, `Unknown provider: \`${provider}\``);
|
|
2230
|
-
return;
|
|
2231
|
-
}
|
|
2232
|
-
if (!(Boolean(pluginCatalogRuntime.getOAuthConfig(provider)) || Boolean(pluginCatalogRuntime.getDefinition(provider)?.manifest.mcp))) {
|
|
2233
|
-
await postEphemeral(event, `${formatProviderLabel(provider)} doesn't support account unlinking.`);
|
|
2234
|
-
return;
|
|
2235
|
-
}
|
|
2236
|
-
const tokenStore = createUserTokenStore();
|
|
2237
|
-
await unlinkProvider(event.userId, provider, tokenStore, event.teamId);
|
|
2238
|
-
logInfo("slash_command.credential.unlinked", { "app.credential.provider": provider });
|
|
2239
|
-
await postEphemeral(event, `Your ${formatProviderLabel(provider)} account has been unlinked.`);
|
|
2240
|
-
}
|
|
2241
|
-
/** Route link and unlink slash commands to the appropriate OAuth flow. */
|
|
2242
|
-
async function handleSlashCommand(event) {
|
|
2243
|
-
const [subcommand, provider, ...rest] = event.text.trim().split(/\s+/);
|
|
2244
|
-
if (!subcommand || !["link", "unlink"].includes(subcommand)) {
|
|
2245
|
-
await postEphemeral(event, `Usage: \`${getCommandName()} link <provider>\` or \`${getCommandName()} unlink <provider>\``);
|
|
2246
|
-
return;
|
|
2247
|
-
}
|
|
2248
|
-
if (!provider || rest.length > 0) {
|
|
2249
|
-
await postEphemeral(event, `Usage: \`${getCommandName()} ${subcommand} <provider>\``);
|
|
2250
|
-
return;
|
|
2251
|
-
}
|
|
2252
|
-
const normalized = provider.toLowerCase();
|
|
2253
|
-
if (subcommand === "link") await handleLink(event, normalized);
|
|
2254
|
-
else await handleUnlink(event, normalized);
|
|
2255
|
-
}
|
|
2256
|
-
//#endregion
|
|
2257
2557
|
//#region src/chat/slack/automation-details.ts
|
|
2258
2558
|
function textField(key, label, value) {
|
|
2259
2559
|
return {
|
|
@@ -2651,6 +2951,7 @@ async function routeParsedMessage(args) {
|
|
|
2651
2951
|
});
|
|
2652
2952
|
return;
|
|
2653
2953
|
}
|
|
2954
|
+
if (route === "mention" && !await hasSlackSignIn(args)) return;
|
|
2654
2955
|
if (isSubscribed && !isExperimentalFeatureEnabled("passive-routing")) {
|
|
2655
2956
|
const content = parseContent(message);
|
|
2656
2957
|
const conversation = coerceThreadConversationState(void 0);
|
|
@@ -2767,6 +3068,7 @@ async function handleSlackEvent(args) {
|
|
|
2767
3068
|
conversationStore: args.services.conversationStore,
|
|
2768
3069
|
queue: args.services.queue,
|
|
2769
3070
|
receivedAtMs,
|
|
3071
|
+
signIn: args.services.signIn,
|
|
2770
3072
|
state
|
|
2771
3073
|
}), {
|
|
2772
3074
|
keepAlive: true,
|
|
@@ -2776,103 +3078,6 @@ async function handleSlackEvent(args) {
|
|
|
2776
3078
|
}
|
|
2777
3079
|
}));
|
|
2778
3080
|
}
|
|
2779
|
-
function requireSlackPayloadUserId(value, source) {
|
|
2780
|
-
const userId = parseActorUserId(value);
|
|
2781
|
-
if (!userId) throw new Error(`${source} is missing a Slack user id`);
|
|
2782
|
-
return userId;
|
|
2783
|
-
}
|
|
2784
|
-
async function handleSlashCommandForm(args) {
|
|
2785
|
-
const { channel_id: channelId, team_id: teamId, user_id: userId } = args.form;
|
|
2786
|
-
const channel = new ChannelImpl({
|
|
2787
|
-
id: `slack:${channelId}`,
|
|
2788
|
-
adapter: args.adapter,
|
|
2789
|
-
stateAdapter: args.state
|
|
2790
|
-
});
|
|
2791
|
-
await withSpan("chat.slash_command", "chat.slash_command", { userId }, async () => {
|
|
2792
|
-
await handleSlashCommand({
|
|
2793
|
-
channel,
|
|
2794
|
-
channelId,
|
|
2795
|
-
teamId,
|
|
2796
|
-
text: args.form.text,
|
|
2797
|
-
userId
|
|
2798
|
-
});
|
|
2799
|
-
});
|
|
2800
|
-
}
|
|
2801
|
-
async function handleInteractivePayload(args) {
|
|
2802
|
-
if (args.payload.type !== "block_actions") return;
|
|
2803
|
-
const action = args.payload.actions?.find((candidate) => candidate.action_id === "app_home_disconnect");
|
|
2804
|
-
const provider = action?.selected_option?.value ?? action?.value;
|
|
2805
|
-
if (!provider) return;
|
|
2806
|
-
const userId = requireSlackPayloadUserId(args.payload.user?.id, "Slack app home disconnect payload");
|
|
2807
|
-
await withSpan("chat.app_home_disconnect", "chat.app_home_disconnect", { userId }, async () => {
|
|
2808
|
-
try {
|
|
2809
|
-
await unlinkProvider(userId, provider, args.userTokenStore, getWorkspaceTeamId());
|
|
2810
|
-
} catch (error) {
|
|
2811
|
-
logException(error, "app_home.disconnect_unlink.failed", { "app.credential.provider": provider });
|
|
2812
|
-
}
|
|
2813
|
-
try {
|
|
2814
|
-
await publishAppHomeView(getSlackClient(), userId, args.userTokenStore);
|
|
2815
|
-
} catch (error) {
|
|
2816
|
-
logException(error, "app_home.disconnect_publish.failed", { "app.credential.provider": provider });
|
|
2817
|
-
}
|
|
2818
|
-
});
|
|
2819
|
-
}
|
|
2820
|
-
function installationFromInteractive(payload) {
|
|
2821
|
-
return { teamId: payload.team?.id ?? payload.user?.team_id };
|
|
2822
|
-
}
|
|
2823
|
-
async function handleSlackForm(args) {
|
|
2824
|
-
const params = new URLSearchParams(args.body);
|
|
2825
|
-
const adapter = args.services.getSlackAdapter();
|
|
2826
|
-
const state = args.services.state ?? getStateAdapter();
|
|
2827
|
-
await state.connect();
|
|
2828
|
-
if (params.has("command") && !params.has("payload")) {
|
|
2829
|
-
const result = slackSlashCommandSchema.safeParse(Object.fromEntries(params));
|
|
2830
|
-
if (!result.success) return new Response("Invalid slash command payload", { status: 400 });
|
|
2831
|
-
const form = result.data;
|
|
2832
|
-
const installation = {
|
|
2833
|
-
teamId: form.team_id,
|
|
2834
|
-
enterpriseId: form.enterprise_id,
|
|
2835
|
-
isEnterpriseInstall: form.is_enterprise_install === "true"
|
|
2836
|
-
};
|
|
2837
|
-
enqueue(args.waitUntil, withLogContext({
|
|
2838
|
-
platform: "slack",
|
|
2839
|
-
userId: form.user_id
|
|
2840
|
-
}, () => runWithWorkspaceTeamId(installation.teamId, () => runWithSlackInstallation({
|
|
2841
|
-
adapter,
|
|
2842
|
-
installation,
|
|
2843
|
-
state,
|
|
2844
|
-
task: () => handleSlashCommandForm({
|
|
2845
|
-
adapter,
|
|
2846
|
-
form,
|
|
2847
|
-
state
|
|
2848
|
-
})
|
|
2849
|
-
})).catch((error) => {
|
|
2850
|
-
logException(error, "slash_command.failed");
|
|
2851
|
-
})));
|
|
2852
|
-
return new Response("", { status: 200 });
|
|
2853
|
-
}
|
|
2854
|
-
const rawPayload = params.get("payload");
|
|
2855
|
-
if (!rawPayload) return new Response("Missing payload", { status: 400 });
|
|
2856
|
-
const result = slackInteractivePayloadSchema.safeParse(parseJson(rawPayload));
|
|
2857
|
-
if (!result.success) return new Response("Invalid payload JSON", { status: 400 });
|
|
2858
|
-
const payload = result.data;
|
|
2859
|
-
const installation = installationFromInteractive(payload);
|
|
2860
|
-
enqueue(args.waitUntil, withLogContext({
|
|
2861
|
-
platform: "slack",
|
|
2862
|
-
userId: payload.user?.id?.trim() || void 0
|
|
2863
|
-
}, () => runWithWorkspaceTeamId(installation.teamId, () => runWithSlackInstallation({
|
|
2864
|
-
adapter,
|
|
2865
|
-
installation,
|
|
2866
|
-
state,
|
|
2867
|
-
task: () => handleInteractivePayload({
|
|
2868
|
-
payload,
|
|
2869
|
-
userTokenStore: getUserTokenStore(args.services)
|
|
2870
|
-
})
|
|
2871
|
-
})).catch((error) => {
|
|
2872
|
-
logException(error, "slack.interactive.payload.failed");
|
|
2873
|
-
})));
|
|
2874
|
-
return new Response("", { status: 200 });
|
|
2875
|
-
}
|
|
2876
3081
|
/** Handle Slack webhooks by enqueueing durable conversation work. */
|
|
2877
3082
|
async function handleSlackWebhook(args) {
|
|
2878
3083
|
const adapter = args.services.getSlackAdapter();
|
|
@@ -2883,8 +3088,10 @@ async function handleSlackWebhook(args) {
|
|
|
2883
3088
|
request: args.request
|
|
2884
3089
|
})) return new Response("Invalid signature", { status: 401 });
|
|
2885
3090
|
if ((args.request.headers.get("content-type") || "").includes("application/x-www-form-urlencoded")) return await handleSlackForm({
|
|
3091
|
+
adapter,
|
|
2886
3092
|
body,
|
|
2887
|
-
|
|
3093
|
+
getUserTokenStore: () => getUserTokenStore(args.services),
|
|
3094
|
+
state: args.services.state ?? getStateAdapter(),
|
|
2888
3095
|
waitUntil: args.waitUntil
|
|
2889
3096
|
});
|
|
2890
3097
|
const result = slackEventEnvelopeSchema.safeParse(parseJson(body));
|
|
@@ -4945,6 +5152,139 @@ async function handleAcpAuthorizationPage(args) {
|
|
|
4945
5152
|
return renderResult(args.agentName, result);
|
|
4946
5153
|
}
|
|
4947
5154
|
//#endregion
|
|
5155
|
+
//#region src/chat/sign-in/auth.ts
|
|
5156
|
+
/**
|
|
5157
|
+
* Google sign-in session shared by the dashboard and Slack.
|
|
5158
|
+
*
|
|
5159
|
+
* Better Auth runs the Google OAuth flow and keeps the browser session in an
|
|
5160
|
+
* encrypted cookie. Each sign-in also saves the native Google identity with its
|
|
5161
|
+
* sign-in time, so Slack can check a recent sign-in without the browser.
|
|
5162
|
+
*/
|
|
5163
|
+
const DEFAULT_AUTH_PATH = "/api/auth";
|
|
5164
|
+
const DEFAULT_SESSION_MAX_AGE_SECONDS = 28800;
|
|
5165
|
+
function trimSlashes(value, leading) {
|
|
5166
|
+
let start = 0;
|
|
5167
|
+
let end = value.length;
|
|
5168
|
+
while (leading && start < end && value[start] === "/") start += 1;
|
|
5169
|
+
while (end > start && value[end - 1] === "/") end -= 1;
|
|
5170
|
+
return value.slice(start, end);
|
|
5171
|
+
}
|
|
5172
|
+
function withHttps(host) {
|
|
5173
|
+
return /^https?:\/\//.test(host) ? host : `https://${host}`;
|
|
5174
|
+
}
|
|
5175
|
+
/** Resolve sign-in settings from dashboard options and environment. */
|
|
5176
|
+
function resolveSignInAuthConfig(agentName, options = {}) {
|
|
5177
|
+
const authPath = `/${trimSlashes(options.authPath?.trim() || DEFAULT_AUTH_PATH, true)}`;
|
|
5178
|
+
const baseURL = trimSlashes(withHttps(options.baseURL?.trim() || resolveBaseUrl() || "http://localhost:3000"), false);
|
|
5179
|
+
return {
|
|
5180
|
+
agentName,
|
|
5181
|
+
allowlist: resolveSignInAllowlist(options),
|
|
5182
|
+
authPath,
|
|
5183
|
+
baseURL,
|
|
5184
|
+
sessionMaxAgeSeconds: options.sessionMaxAgeSeconds,
|
|
5185
|
+
trustedOrigins: options.trustedOrigins ?? readEnvList("JUNIOR_DASHBOARD_TRUSTED_ORIGINS") ?? []
|
|
5186
|
+
};
|
|
5187
|
+
}
|
|
5188
|
+
function required(value, name) {
|
|
5189
|
+
if (!value?.trim()) throw new Error(`${name} is required for Junior sign-in`);
|
|
5190
|
+
return value.trim();
|
|
5191
|
+
}
|
|
5192
|
+
function createBetterAuth(config) {
|
|
5193
|
+
const secret = required(config.secret ?? process.env.BETTER_AUTH_SECRET ?? process.env.JUNIOR_SECRET, "JUNIOR_SECRET or BETTER_AUTH_SECRET");
|
|
5194
|
+
const sessionMaxAgeSeconds = config.sessionMaxAgeSeconds ?? DEFAULT_SESSION_MAX_AGE_SECONDS;
|
|
5195
|
+
const { domains, emails } = config.allowlist;
|
|
5196
|
+
const hostedDomain = domains.length === 1 && emails.length === 0 ? domains[0] : void 0;
|
|
5197
|
+
return betterAuth({
|
|
5198
|
+
appName: config.agentName,
|
|
5199
|
+
baseURL: config.baseURL,
|
|
5200
|
+
basePath: config.authPath,
|
|
5201
|
+
secret,
|
|
5202
|
+
trustedOrigins: config.trustedOrigins,
|
|
5203
|
+
socialProviders: { google: {
|
|
5204
|
+
clientId: required(config.googleClientId ?? process.env.GOOGLE_CLIENT_ID, "GOOGLE_CLIENT_ID"),
|
|
5205
|
+
clientSecret: required(config.googleClientSecret ?? process.env.GOOGLE_CLIENT_SECRET, "GOOGLE_CLIENT_SECRET"),
|
|
5206
|
+
hd: hostedDomain,
|
|
5207
|
+
prompt: "select_account",
|
|
5208
|
+
mapProfileToUser(profile) {
|
|
5209
|
+
return {
|
|
5210
|
+
email: profile.email,
|
|
5211
|
+
emailVerified: profile.email_verified,
|
|
5212
|
+
image: profile.picture,
|
|
5213
|
+
name: profile.name
|
|
5214
|
+
};
|
|
5215
|
+
}
|
|
5216
|
+
} },
|
|
5217
|
+
account: {
|
|
5218
|
+
storeStateStrategy: "cookie",
|
|
5219
|
+
storeAccountCookie: false,
|
|
5220
|
+
updateAccountOnSignIn: false
|
|
5221
|
+
},
|
|
5222
|
+
session: {
|
|
5223
|
+
expiresIn: sessionMaxAgeSeconds,
|
|
5224
|
+
disableSessionRefresh: true,
|
|
5225
|
+
cookieCache: {
|
|
5226
|
+
enabled: true,
|
|
5227
|
+
strategy: "jwe",
|
|
5228
|
+
maxAge: sessionMaxAgeSeconds,
|
|
5229
|
+
refreshCache: false
|
|
5230
|
+
}
|
|
5231
|
+
},
|
|
5232
|
+
hooks: { after: createAuthMiddleware(async (ctx) => {
|
|
5233
|
+
const created = ctx.context.newSession;
|
|
5234
|
+
if (!created || !ctx.path.startsWith("/callback/")) return;
|
|
5235
|
+
const email = normalizeIdentityEmail(created.user.email);
|
|
5236
|
+
if (!created.user.emailVerified || !email || !isSignInAllowed(email, config.allowlist)) return;
|
|
5237
|
+
const google = (await ctx.context.internalAdapter.findAccounts(created.user.id)).find((account) => account.providerId === "google");
|
|
5238
|
+
if (!google) throw new Error("Google sign-in has no Google account");
|
|
5239
|
+
await recordGoogleSignIn(getSqlExecutor(), {
|
|
5240
|
+
email,
|
|
5241
|
+
subject: google.accountId,
|
|
5242
|
+
displayName: created.user.name || void 0
|
|
5243
|
+
});
|
|
5244
|
+
}) }
|
|
5245
|
+
});
|
|
5246
|
+
}
|
|
5247
|
+
/**
|
|
5248
|
+
* Create the shared Google sign-in session. Better Auth starts on first use,
|
|
5249
|
+
* so apps that never sign in need no Google credentials.
|
|
5250
|
+
*/
|
|
5251
|
+
function createSignInAuth(config) {
|
|
5252
|
+
let auth;
|
|
5253
|
+
const getAuth = () => auth ??= createBetterAuth(config);
|
|
5254
|
+
return {
|
|
5255
|
+
handler(request) {
|
|
5256
|
+
return getAuth().handler(request);
|
|
5257
|
+
},
|
|
5258
|
+
async getSession(request) {
|
|
5259
|
+
const session = await getAuth().api.getSession({ headers: request.headers });
|
|
5260
|
+
if (!session) return null;
|
|
5261
|
+
const { email, emailVerified, name } = session.user;
|
|
5262
|
+
return { user: {
|
|
5263
|
+
email,
|
|
5264
|
+
emailVerified,
|
|
5265
|
+
name
|
|
5266
|
+
} };
|
|
5267
|
+
},
|
|
5268
|
+
async signInWithGoogle(request, callbackURL, errorCallbackURL) {
|
|
5269
|
+
const result = await getAuth().api.signInSocial({
|
|
5270
|
+
body: {
|
|
5271
|
+
provider: "google",
|
|
5272
|
+
callbackURL,
|
|
5273
|
+
errorCallbackURL
|
|
5274
|
+
},
|
|
5275
|
+
headers: request.headers,
|
|
5276
|
+
returnHeaders: true
|
|
5277
|
+
});
|
|
5278
|
+
if (!("url" in result.response) || !result.response.url) throw new Error("Google sign-in did not return a redirect URL");
|
|
5279
|
+
result.headers.set("location", result.response.url);
|
|
5280
|
+
return new Response(null, {
|
|
5281
|
+
status: 302,
|
|
5282
|
+
headers: result.headers
|
|
5283
|
+
});
|
|
5284
|
+
}
|
|
5285
|
+
};
|
|
5286
|
+
}
|
|
5287
|
+
//#endregion
|
|
4948
5288
|
//#region src/dashboard-routes.ts
|
|
4949
5289
|
const DASHBOARD_PACKAGE_NAME = "@sentry/junior-dashboard";
|
|
4950
5290
|
/** Mount the dashboard app on the dashboard-owned host paths. */
|
|
@@ -4952,6 +5292,7 @@ async function createDashboardRouteRegistrations(args) {
|
|
|
4952
5292
|
if (!args.dashboard || args.dashboard.disabled) return [];
|
|
4953
5293
|
const createDashboardApp = args.createDashboardApp ?? await loadDashboardAppFactory();
|
|
4954
5294
|
return dashboardRouteRegistrations({
|
|
5295
|
+
auth: args.auth,
|
|
4955
5296
|
authenticatedRoutes: args.authenticatedRoutes,
|
|
4956
5297
|
conversationWorkQueue: args.conversationWorkQueue,
|
|
4957
5298
|
dashboard: args.dashboard,
|
|
@@ -5100,6 +5441,7 @@ function dashboardRouteRegistrations(args) {
|
|
|
5100
5441
|
const dashboardOptions = {
|
|
5101
5442
|
...args.dashboard,
|
|
5102
5443
|
agentName: botConfig.userName,
|
|
5444
|
+
...args.auth ? { auth: args.auth } : void 0,
|
|
5103
5445
|
authenticatedRoutes: args.authenticatedRoutes,
|
|
5104
5446
|
conversationWorkQueue: args.conversationWorkQueue,
|
|
5105
5447
|
pluginRoutes: args.pluginRoutes
|
|
@@ -5236,6 +5578,8 @@ async function createApp(options) {
|
|
|
5236
5578
|
queue: conversationWorkQueue
|
|
5237
5579
|
}) });
|
|
5238
5580
|
let sandboxEgressTracePropagationDomains = [];
|
|
5581
|
+
let signInConfig;
|
|
5582
|
+
let slackSignIn;
|
|
5239
5583
|
try {
|
|
5240
5584
|
sandboxEgressTracePropagationDomains = normalizeSandboxEgressTracePropagationDomains(options?.sandbox?.egressTracePropagationDomains);
|
|
5241
5585
|
setSandboxResourceConfig(options?.sandbox);
|
|
@@ -5248,6 +5592,18 @@ async function createApp(options) {
|
|
|
5248
5592
|
setRuntimeLimits(options?.limits);
|
|
5249
5593
|
setSlackReactionConfig(options?.slack ?? {});
|
|
5250
5594
|
if (options?.slack?.crossActorMidRunMode) setCrossActorMidRunMode(options.slack.crossActorMidRunMode);
|
|
5595
|
+
const dashboardMounted = Boolean(dashboard && !dashboard.disabled);
|
|
5596
|
+
if (isExperimentalFeatureEnabled("slack-sign-in") || dashboardMounted) signInConfig = resolveSignInAuthConfig(botConfig.userName, dashboard);
|
|
5597
|
+
if (signInConfig && isExperimentalFeatureEnabled("slack-sign-in")) {
|
|
5598
|
+
const { allowlist } = signInConfig;
|
|
5599
|
+
if (!allowlist.domains.length && !allowlist.emails.length) throw new Error("experimental.slack-sign-in requires allowed Google domains or emails");
|
|
5600
|
+
const maxAgeDays = options?.slack?.signInMaxAgeDays ?? 7;
|
|
5601
|
+
if (!(maxAgeDays > 0)) throw new Error("slack.signInMaxAgeDays must be greater than 0");
|
|
5602
|
+
slackSignIn = {
|
|
5603
|
+
baseURL: signInConfig.baseURL,
|
|
5604
|
+
maxAgeMs: maxAgeDays * 24 * 60 * 60 * 1e3
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
5251
5607
|
if (shouldValidatePluginCatalog) {
|
|
5252
5608
|
pluginCatalogRuntime.getSignature();
|
|
5253
5609
|
validatePluginRegistrations(configuredPlugins?.registrations ?? []);
|
|
@@ -5335,10 +5691,14 @@ async function createApp(options) {
|
|
|
5335
5691
|
restoreRuntimeConfig$1();
|
|
5336
5692
|
throw error;
|
|
5337
5693
|
}
|
|
5338
|
-
const
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5694
|
+
const signInAuth = signInConfig ? createSignInAuth(signInConfig) : void 0;
|
|
5695
|
+
const slackWebhookServices = {
|
|
5696
|
+
...createProductionSlackWebhookServices({
|
|
5697
|
+
queue: conversationWorkQueue,
|
|
5698
|
+
services: runtimeServiceOverrides
|
|
5699
|
+
}),
|
|
5700
|
+
...slackSignIn ? { signIn: slackSignIn } : void 0
|
|
5701
|
+
};
|
|
5342
5702
|
const app = new Hono();
|
|
5343
5703
|
app.onError((err, c) => {
|
|
5344
5704
|
logException(err, "route.unhandled.exception");
|
|
@@ -5354,9 +5714,13 @@ async function createApp(options) {
|
|
|
5354
5714
|
], "/api/acp", (c) => getAcpRuntime().handleRequest(c.req.raw));
|
|
5355
5715
|
mountRoutes(app, pluginRoutes);
|
|
5356
5716
|
mountRoutes(app, await createDashboardRouteRegistrations({
|
|
5717
|
+
auth: signInAuth,
|
|
5357
5718
|
authenticatedRoutes,
|
|
5358
5719
|
conversationWorkQueue,
|
|
5359
|
-
dashboard
|
|
5720
|
+
dashboard: dashboard && signInConfig ? {
|
|
5721
|
+
...dashboard,
|
|
5722
|
+
baseURL: signInConfig.baseURL
|
|
5723
|
+
} : dashboard,
|
|
5360
5724
|
createDashboardApp: virtualConfig?.createDashboardApp,
|
|
5361
5725
|
pluginRoutes: pluginApiRoutes
|
|
5362
5726
|
}));
|
|
@@ -5372,6 +5736,19 @@ async function createApp(options) {
|
|
|
5372
5736
|
app.get("/api/oauth/callback/:provider", (c) => {
|
|
5373
5737
|
return GET$5(c.req.raw, c.req.param("provider"), waitUntil, { conversationWorkQueue });
|
|
5374
5738
|
});
|
|
5739
|
+
if (signInAuth && signInConfig && slackSignIn) {
|
|
5740
|
+
const auth = signInAuth;
|
|
5741
|
+
if (!dashboard || dashboard.disabled) app.on(["GET", "POST"], `${signInConfig.authPath}/*`, (c) => auth.handler(c.req.raw));
|
|
5742
|
+
const { allowlist } = signInConfig;
|
|
5743
|
+
const signIn = slackSignIn;
|
|
5744
|
+
app.get(SLACK_SIGN_IN_PATH, (c) => handleSlackSignIn({
|
|
5745
|
+
allowlist,
|
|
5746
|
+
auth,
|
|
5747
|
+
signIn,
|
|
5748
|
+
request: c.req.raw,
|
|
5749
|
+
state: getStateAdapter()
|
|
5750
|
+
}));
|
|
5751
|
+
}
|
|
5375
5752
|
app.get("/api/internal/sandbox-egress/signals", (c) => {
|
|
5376
5753
|
return GET(c.req.raw);
|
|
5377
5754
|
});
|