@mastra/factory 0.18.0 → 0.18.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/integrations/github/default-rules.d.ts.map +1 -1
  2. package/dist/integrations/github/default-rules.js +3 -3
  3. package/dist/integrations/github/default-rules.js.map +1 -1
  4. package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
  5. package/dist/integrations/github/reconcile-worker.js +1 -2
  6. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  7. package/dist/integrations/github/routes.d.ts.map +1 -1
  8. package/dist/integrations/github/routes.js +1 -14
  9. package/dist/integrations/github/routes.js.map +1 -1
  10. package/dist/integrations/gitlab/version-control.d.ts.map +1 -1
  11. package/dist/integrations/gitlab/version-control.js +4 -5
  12. package/dist/integrations/gitlab/version-control.js.map +1 -1
  13. package/dist/integrations/gitlab/webhook.d.ts.map +1 -1
  14. package/dist/integrations/gitlab/webhook.js +2 -2
  15. package/dist/integrations/gitlab/webhook.js.map +1 -1
  16. package/dist/integrations/issue-reconcile-worker.d.ts.map +1 -1
  17. package/dist/integrations/issue-reconcile-worker.js +1 -2
  18. package/dist/integrations/issue-reconcile-worker.js.map +1 -1
  19. package/dist/integrations/linear/integration.d.ts.map +1 -1
  20. package/dist/integrations/linear/integration.js +10 -30
  21. package/dist/integrations/linear/integration.js.map +1 -1
  22. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
  23. package/dist/integrations/platform/github/event-worker.js +1 -2
  24. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  25. package/dist/integrations/platform/gitlab/event-worker.d.ts.map +1 -1
  26. package/dist/integrations/platform/gitlab/event-worker.js +1 -2
  27. package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
  28. package/dist/integrations/platform/linear/event-worker.d.ts.map +1 -1
  29. package/dist/integrations/platform/linear/event-worker.js +1 -2
  30. package/dist/integrations/platform/linear/event-worker.js.map +1 -1
  31. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  32. package/dist/integrations/platform/linear/integration.js +9 -9
  33. package/dist/integrations/platform/linear/integration.js.map +1 -1
  34. package/dist/integrations/slack/slack.d.ts.map +1 -1
  35. package/dist/integrations/slack/slack.js +1 -2
  36. package/dist/integrations/slack/slack.js.map +1 -1
  37. package/dist/review-verdict.d.ts +6 -0
  38. package/dist/review-verdict.d.ts.map +1 -0
  39. package/dist/review-verdict.js +14 -0
  40. package/dist/review-verdict.js.map +1 -0
  41. package/dist/routes/oauth.d.ts.map +1 -1
  42. package/dist/routes/oauth.js +3 -3
  43. package/dist/routes/oauth.js.map +1 -1
  44. package/dist/routes/source-control-sessions.d.ts.map +1 -1
  45. package/dist/routes/source-control-sessions.js +1 -2
  46. package/dist/routes/source-control-sessions.js.map +1 -1
  47. package/dist/rules/dispatcher.d.ts.map +1 -1
  48. package/dist/rules/dispatcher.js +1 -2
  49. package/dist/rules/dispatcher.js.map +1 -1
  50. package/dist/rules/processor.d.ts.map +1 -1
  51. package/dist/rules/processor.js +5 -5
  52. package/dist/rules/processor.js.map +1 -1
  53. package/dist/rules/transition-service.d.ts.map +1 -1
  54. package/dist/rules/transition-service.js +1 -2
  55. package/dist/rules/transition-service.js.map +1 -1
  56. package/dist/secret-encryption.js +2 -2
  57. package/dist/secret-encryption.js.map +1 -1
  58. package/dist/session/factory-session.d.ts.map +1 -1
  59. package/dist/session/factory-session.js +1 -2
  60. package/dist/session/factory-session.js.map +1 -1
  61. package/dist/session/source-control-tools.d.ts +9 -0
  62. package/dist/session/source-control-tools.d.ts.map +1 -1
  63. package/dist/session/source-control-tools.js +51 -2
  64. package/dist/session/source-control-tools.js.map +1 -1
  65. package/dist/state-signing.js +3 -3
  66. package/dist/state-signing.js.map +1 -1
  67. package/dist/storage/domains/audit/base.d.ts.map +1 -1
  68. package/dist/storage/domains/audit/base.js +2 -3
  69. package/dist/storage/domains/audit/base.js.map +1 -1
  70. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  71. package/dist/storage/domains/source-control/inmemory.js +5 -6
  72. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  73. package/factory-skills/factory-gitlab-rereview/SKILL.md +1 -1
  74. package/factory-skills/factory-gitlab-review/SKILL.md +1 -1
  75. package/factory-skills/factory-rereview/SKILL.md +12 -3
  76. package/factory-skills/factory-review/SKILL.md +12 -3
  77. package/package.json +9 -9
@@ -1 +1 @@
1
- {"version":3,"file":"slack.js","names":[],"sources":["../../../src/integrations/slack/slack.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { ChannelSessionRejectedError } from '@mastra/core/channels';\nimport type {\n ChannelHandler,\n ChannelHandlerContext,\n ChannelHandlers,\n ChannelSessionResolve,\n ChannelSessionStart,\n ResolveResourceId,\n ResolveThreadId,\n} from '@mastra/core/channels';\nimport type { Mastra } from '@mastra/core/mastra';\nimport { createSlackAdapter } from '@mastra/slack';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\nimport { Card, CardText, Actions, LinkButton } from 'chat';\n\nimport {\n createSourceControlSessionLookup,\n FactorySourceControlConflictError,\n hydrateFactorySession,\n resolveFactoryDefaultModelId,\n resolveFactoryProjectForSession,\n resolveFactorySourceControl,\n resolveFactorySourceRepository,\n} from '../../session/factory-session.js';\nimport { applyPersonalMemorySettings } from '../../session/memory-settings-hydration.js';\nimport { readRequestContextOrgId, seedSessionOrg } from '../../session/org-seed.js';\nimport type {\n ChannelAccountLink,\n ChannelAccountLinkKey,\n ChannelIdentityStorage,\n} from '../../storage/domains/channel-identity/base.js';\nimport type { FactoryActorExternalIdentity } from '../../storage/domains/comments/actor.js';\nimport { actorFromChannelAuthor } from '../../storage/domains/comments/actor.js';\nimport type { CommentsDomain } from '../../storage/domains/comments/domain.js';\nimport type { MemorySettingsStorage } from '../../storage/domains/memory-settings/base.js';\nimport type { ActiveModelPackRecord, ModelPacksStorage } from '../../storage/domains/model-packs/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { ExternalWorkItemSource, WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { FactoryChannelsConfig } from '../base.js';\nimport { prepareSessionRunContext } from '../subscription-session.js';\n\nimport { resolveEmojiShortcodes } from './emoji.js';\nimport { slackCommentSource } from './feed-publisher.js';\n\n// Derive the thread/message types from the core handler signature rather than\n// importing them from `chat` directly: mc-web can resolve a different `chat`\n// version than @mastra/core, and the two `Thread`/`Message` declarations are\n// structurally incompatible (private fields). Using the handler's own types\n// keeps everything on one version.\ntype HandlerThread = Parameters<ChannelHandler>[0];\ntype HandlerMessage = Parameters<ChannelHandler>[1];\n\nconst SLACK_REQUEST_TIMEOUT_MS = 15_000;\nconst MAX_WORK_ITEM_TITLE_CHARS = 80;\nconst graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n\nclass SlackSessionStartError extends Error {}\n\n/**\n * A card is titled with what the sender wrote. Cutting counts graphemes, not\n * code points: `👍🏼` is a base plus a skin-tone modifier, and a cut between\n * them leaves the wrong emoji.\n */\nfunction workItemTitleFrom(messageText: string): string {\n const resolved = resolveEmojiShortcodes(messageText);\n const characters = [...graphemes.segment(resolved)].map(({ segment }) => segment);\n if (characters.length <= MAX_WORK_ITEM_TITLE_CHARS) return resolved;\n return `${characters.slice(0, MAX_WORK_ITEM_TITLE_CHARS - 1).join('')}…`;\n}\n\n/** Dependencies the Slack channel handlers close over, injected from the web entry. */\ninterface SlackChannelDeps {\n /**\n * The factory's reverse-index store mapping a Slack sender to a Mastra\n * tenant. When provided, inbound messages from an unlinked sender are not\n * dispatched — the run only proceeds (with the sender's tenant stamped on\n * the request context) once they've linked their account. Unlinked senders\n * get an ephemeral \"connect your account\" card instead.\n */\n accountLinks?: ChannelIdentityStorage;\n /**\n * Factory projects domain. When provided (alongside `accountLinks`), a\n * linked sender's run must also resolve to a Factory project before it\n * dispatches: their link's default factory, else their tenant's only\n * factory (stamped back onto the link), else a \"pick a default factory\"\n * card posted in the thread and no run. Unset → no factory routing (runs\n * dispatch as before).\n */\n projects?: FactoryProjectsStorage;\n /**\n * Storage handle of the integration that owns source control\n * (`IntegrationContext.storage.sourceControlOwner`). Used to make new Slack\n * threads repo-backed: when the sender is linked and their factory has a\n * repository, the thread's resourceId becomes a Factory user-session id (repo\n * cloned on a `slack/{threadTs}` branch) instead of the chat-only\n * `channel:...` id. It also lets a started session read back the project it\n * belongs to. Nothing here is provider-specific — the connection is matched\n * by the handle's own `integrationId`. Absent (no source-control integration\n * registered) → chat-only sessions as before.\n */\n sourceControl?: SourceControlStorageHandle;\n /** Registered source-control partitions used to select the provider linked to each Factory project. */\n sourceControls?: readonly SourceControlStorageHandle[];\n /**\n * Observational-memory settings domain. When provided, a repo-backed session\n * adopts its factory project's shared memory settings on start, matching the\n * web kickoff — and the linked sender's own settings win over them for every\n * knob the sender has saved.\n */\n memorySettings?: MemorySettingsStorage;\n /**\n * Model-packs domain. When provided, a new repo-backed session starts on the\n * linked sender's active pack build model — the model that user picked for\n * themselves — before the factory's shared default. Read once, when the\n * thread's build model is first chosen; the choice is persisted on the thread\n * and never re-resolved. Unset → the factory default, as before.\n */\n modelPacks?: ModelPacksStorage;\n /**\n * Factory work-items domain. When provided, a dispatched new-session thread\n * (DM or mention) upserts a Work-board card in Building (`execute`) carrying\n * the Slack thread as its external source and binding the repo-backed\n * session. Best-effort — a failure never blocks the run. Unset → no card.\n */\n workItems?: WorkItemsStorage;\n /**\n * Work-item feed. With it (and `workItems`), an `aside` lands as a comment on\n * the card the thread created. Unset → asides stay ignored, as before.\n */\n feed?: Pick<CommentsDomain, 'createComment'>;\n /** Overrides applied to the Slack channel adapter entry. */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\nfunction configuredSourceControls(deps: SlackChannelDeps): readonly SourceControlStorageHandle[] {\n return deps.sourceControls ?? (deps.sourceControl ? [deps.sourceControl] : []);\n}\n\n/**\n * Read the Slack team id off a raw platform payload (Events API envelope or\n * slash-command body — both carry `team_id`), duck-typed to build the\n * workspace-scoped account-link key.\n */\nfunction rawTeamId(rawPayload: unknown): string | undefined {\n if (!rawPayload || typeof rawPayload !== 'object') return undefined;\n const raw = rawPayload as { team_id?: unknown; team?: unknown };\n if (typeof raw.team_id === 'string' && raw.team_id) return raw.team_id;\n if (typeof raw.team === 'string' && raw.team) return raw.team;\n if (raw.team && typeof raw.team === 'object') {\n const id = (raw.team as { id?: unknown }).id;\n if (typeof id === 'string' && id) return id;\n }\n return undefined;\n}\n\n/**\n * The Slack team id survives onto a normalized chat Message only on\n * `message.raw` (the Slack Events API envelope).\n */\nfunction slackTeamId(message: HandlerMessage): string | undefined {\n return rawTeamId(message.raw);\n}\n\n/**\n * Resolve the web-UI origin for links humans open in a browser (Connect card,\n * session deep links). Prefers `MASTRACODE_PUBLIC_URL` — the origin auth\n * cookies and OAuth redirect allow-lists are registered against — over the\n * channels tunnel, which only Slack's servers need to reach.\n */\nfunction webPublicUrl(): string | undefined {\n return process.env.MASTRACODE_PUBLIC_URL ?? process.env.MASTRACODE_CHANNELS_PUBLIC_URL;\n}\n\n/** Outcome of the sender-link gate for one inbound message. */\ntype LinkedSenderResult =\n /** Gating not configured — dispatch as before account linking existed. */\n | { status: 'ungated' }\n /** Sender unlinked — Connect card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** Sender linked — their tenant plus the sender key the link lives under. */\n | { status: 'linked'; link: ChannelAccountLink; key: ChannelAccountLinkKey };\n\n/**\n * Resolve the sender's account link, posting an ephemeral \"connect your\n * account\" card (visible only to the sender) linking into the web UI's\n * Slack-connect flow when they're unlinked.\n */\nexport async function resolveLinkedSender({\n thread,\n message,\n accountLinks,\n}: {\n thread: HandlerThread;\n message: HandlerMessage;\n accountLinks?: ChannelIdentityStorage;\n}): Promise<LinkedSenderResult> {\n if (!accountLinks) return { status: 'ungated' };\n const platform = thread.adapter.name;\n const externalUserId = message.author.userId;\n const externalTeamId = slackTeamId(message);\n // Without a team id we can't identify the workspace-scoped link; treat as\n // unlinked so a run never proceeds tenant-less.\n const key = externalTeamId ? { platform, externalTeamId, externalUserId } : undefined;\n const link = key ? await accountLinks.getAccountLink(key) : null;\n if (link && key) return { status: 'linked', link, key };\n\n const publicUrl = webPublicUrl();\n // A public origin is all the card needs. The link carries no identity: the\n // web app authenticates the visitor, then Slack's OIDC flow proves which\n // Slack account they control. Without an origin, still block, just no card.\n if (publicUrl) {\n await thread.postEphemeral(message.author, buildConnectCard(publicUrl, thread), { fallbackToDM: true });\n }\n return { status: 'blocked' };\n}\n\nconst retryHint = (thread: HandlerThread) => (thread.isDM ? 'message me again' : 'mention me again');\n\n/**\n * The \"connect your account\" card. The link is deliberately identity-free —\n * `/connect/slack` sends the visitor to Connections, where \"Connect Slack\"\n * runs the OIDC flow and Slack itself asserts the (team, user) pair.\n */\nfunction buildConnectCard(publicUrl: string, thread: HandlerThread) {\n return Card({\n title: 'Connect your account',\n children: [\n CardText(`Connect your account to use this agent, then ${retryHint(thread)}.`),\n Actions([\n LinkButton({\n url: `${publicUrl}/connect/slack`,\n label: 'Connect account',\n }),\n ]),\n ],\n });\n}\n\n/** Outcome of factory routing for one linked sender's inbound message. */\ntype FactoryRouteResult =\n /** Factory routing not configured — dispatch without a factory. */\n | { status: 'ungated' }\n /** No factory resolved — prompt card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** The Factory project this sender's runs route to. */\n | { status: 'resolved'; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n\n/**\n * Decide which Factory project a linked sender's run belongs to:\n *\n * 1. The link's `defaultFactoryProjectId`, when it still exists (a stale id —\n * deleted factory — falls through as if unset).\n * 2. Else, the tenant's only factory, stamped back onto the link so it shows\n * up (and stays editable) in Connected Accounts settings.\n * 3. Else — zero or several factories — a \"pick a default factory\" card\n * posted publicly in the thread so Slack notifies the sender, and the run is\n * blocked.\n */\nexport async function resolveFactoryForLink({\n thread,\n link,\n key,\n accountLinks,\n projects,\n}: {\n thread: HandlerThread;\n link: ChannelAccountLink;\n key: ChannelAccountLinkKey;\n accountLinks: ChannelIdentityStorage;\n projects?: FactoryProjectsStorage;\n}): Promise<FactoryRouteResult> {\n if (!projects) return { status: 'ungated' };\n // Factories are org-scoped; a personal account (no org) has none and lands\n // on the prompt below.\n const orgId = link.orgId ?? '';\n\n if (link.defaultFactoryProjectId) {\n const existing = await projects.get({ orgId, id: link.defaultFactoryProjectId });\n if (existing) {\n return {\n status: 'resolved',\n factoryProjectId: existing.id,\n slackWorkItemsEnabled: existing.slackWorkItemsEnabled,\n };\n }\n }\n\n const factories = orgId ? await projects.list({ orgId }) : [];\n if (factories.length === 1) {\n const only = factories[0]!;\n await accountLinks.setDefaultFactory({ ...key, userId: link.userId, factoryProjectId: only.id });\n return {\n status: 'resolved',\n factoryProjectId: only.id,\n slackWorkItemsEnabled: only.slackWorkItemsEnabled,\n };\n }\n\n const publicUrl = webPublicUrl();\n if (publicUrl) {\n await thread.post(\n Card({\n title: 'Pick a default factory',\n children: [\n CardText(\n factories.length === 0\n ? `Your account has no factory yet. Create one in the web app, then ${retryHint(thread)}.`\n : `Your account has several factories. Pick which one Slack sessions should go to, then ${retryHint(thread)}.`,\n ),\n Actions([\n LinkButton({\n url: `${publicUrl}/settings/connections`,\n label: 'Open settings',\n }),\n ]),\n ],\n }),\n );\n }\n return { status: 'blocked' };\n}\n\n/**\n * Deterministic per-thread branch name: `slack/{threadTs}` with characters\n * outside the sandbox git-ref allow-list (`[A-Za-z0-9_./-]`, and `.` for\n * readability) mapped to `-`. `thread.id` is `{channelId}:{threadTs}`\n * (platform-prefixed on handler threads) — the trailing segment is the ts.\n *\n * Top-level DM and channel conversations use the empty-threadTs thread form,\n * so the trailing segment can be empty; a bare `slack/` is not a valid git\n * ref. Fall back to the last non-empty segment (the channel id): one\n * deterministic branch per top-level conversation.\n */\nfunction threadBranch(threadId: string): string {\n const segments = threadId.split(':');\n const tail = segments.findLast(segment => segment.length > 0) ?? threadId;\n return `slack/${tail.replace(/[^A-Za-z0-9_/-]/g, '-')}`;\n}\n\n/**\n * Resolve the resourceId for a NEW Slack channel thread. A linked sender whose\n * factory has a repository gets a Factory user-session id — the controller\n * session then materializes the repo sandbox via the factory's dynamic\n * workspace (clone + PAT), the session shows up in the web Sessions list, and\n * View Session deep-links land on the normal workspace route.\n *\n * A gated deployment refuses a thread it cannot place, rather than falling back\n * to a chat-only session: a repo-backed thread is the whole point of the\n * integration, and answering in a chat-only one would run the sender's request\n * in no project, on the SDK's built-in defaults. A linked sender whose project\n * cannot start a session gets an actionable error in Slack; the dispatch gate\n * handles unlinked or unrouted senders before this hook runs.\n * The hook only runs when a NEW thread is actually created (`getOrCreateThread`\n * resolves the owner lazily), so an established conversation is never touched:\n * it keeps its session, its model, and its history.\n *\n * The chat-only `channel:...` id survives for two cases where refusing would be\n * wrong: a deployment that cannot produce a repo-backed thread at all (no\n * account linking, no projects, or no source-control integration registered),\n * and a message carrying no sender id. In both, a `channel:` thread is the only\n * shape a session can take.\n *\n * Pure lookups only — cards for unlinked/unrouted senders are the dispatch\n * gate's job; this hook must never post.\n */\nexport function createChannelResourceIdResolver(deps: SlackChannelDeps): ResolveResourceId {\n const { accountLinks, projects } = deps;\n const sourceControls = configuredSourceControls(deps);\n return async ({ platform, thread, message }) => {\n // NOT the hook's `defaultResourceId`: configuring a custom resolver\n // bypasses AgentControllerChannels' own `channel:{thread.id}` derivation\n // (agent-controller-channels.ts `resolveChannelResourceId`), and the base\n // default is the per-USER memory key. Chat-only fallbacks must stay\n // per-thread, so reproduce the controller default here.\n const chatOnlyResourceId = `channel:${thread.id}`;\n // A deployment with no account linking, no projects, or no source-control\n // integration cannot produce a repo-backed thread at all, so a `channel:`\n // thread is the only shape it has. Every refusal below is a different thing\n // entirely: a sender the bot cannot place, in a deployment that can.\n if (!accountLinks || !projects || sourceControls.length === 0) return chatOnlyResourceId;\n // No sender id at all — a malformed message, not a sender the host turned\n // away. There is nothing to place, so give it the per-thread memory key.\n if (!message.author.userId) return chatOnlyResourceId;\n\n const externalTeamId = rawTeamId(message.raw);\n if (!externalTeamId) {\n throw new ChannelSessionRejectedError('No Slack workspace id on the message — the sender cannot be identified');\n }\n try {\n const link = await accountLinks.getAccountLink({\n platform,\n externalTeamId,\n externalUserId: message.author.userId,\n });\n if (!link) {\n throw new ChannelSessionRejectedError(\n `Slack sender ${message.author.userId} is not linked to a Factory account`,\n );\n }\n\n // Same chain as `resolveFactoryForLink`, minus prompts/stamping: the\n // dispatch gate has already run (and stamped a lone factory) by the\n // time a new thread is created, so this is a read-only re-resolve.\n const orgId = link.orgId ?? '';\n let factoryProjectId: string | undefined;\n if (link.defaultFactoryProjectId && (await projects.get({ orgId, id: link.defaultFactoryProjectId }))) {\n factoryProjectId = link.defaultFactoryProjectId;\n } else if (orgId) {\n const factories = await projects.list({ orgId });\n if (factories.length === 1) factoryProjectId = factories[0]!.id;\n }\n if (!factoryProjectId) {\n throw new ChannelSessionRejectedError(`Linked Slack sender ${message.author.userId} has no Factory project`);\n }\n\n // Linked and routed, so a repo-backed session is not a preference: with no\n // repository to work in there is no thread to start, and starting one\n // anyway would answer in a project the sender never picked, on the SDK's\n // built-in defaults.\n const sourceControl = await resolveFactorySourceControl({ sourceControls, orgId, factoryProjectId });\n if (!sourceControl) {\n const connections = await Promise.all(\n sourceControls.map(sourceControl => sourceControl.connections.list({ orgId, factoryProjectId })),\n );\n throw new SlackSessionStartError(\n connections.some(rows => rows.length > 0)\n ? 'Could not start a session: link a repository to this Factory project.'\n : 'Could not start a session: connect source control to this Factory project.',\n );\n }\n const repo = await resolveFactorySourceRepository({\n sourceControl,\n orgId,\n factoryProjectId,\n firstLinkedRepository: true,\n });\n if (!repo.found) {\n throw new SlackSessionStartError(\n repo.reason === 'connection'\n ? 'Could not start a session: connect source control to this Factory project.'\n : 'Could not start a session: link a repository to this Factory project.',\n );\n }\n\n const branch = threadBranch(thread.id);\n // Attributed to the Slack sender, not to whoever connected the repository:\n // unlike an autonomous rule run, a Slack thread has a real interactive user.\n const existing = await sourceControl.sessions.getForBranch({\n projectRepositoryId: repo.projectRepositoryId,\n userId: link.userId,\n branch,\n });\n if (existing) return existing.sessionId;\n const session = await sourceControl.sessions.create({\n sessionId: randomUUID(),\n projectRepositoryId: repo.projectRepositoryId,\n orgId,\n userId: link.userId,\n branch,\n baseBranch: repo.baseBranch,\n // DMs are the only private origin; channel threads are org-visible.\n visibility: thread.isDM ? 'private' : 'org',\n });\n return session.sessionId;\n } catch (error) {\n if (error instanceof ChannelSessionRejectedError || error instanceof SlackSessionStartError) throw error;\n if (error instanceof FactorySourceControlConflictError) {\n throw new SlackSessionStartError(\n 'Could not start a session: this Factory project has repositories from more than one source-control provider.',\n { cause: error },\n );\n }\n console.error('[slack] failed to start repo-backed session for thread', thread.id, error);\n throw new SlackSessionStartError('Could not start a session right now. Please try again later.', {\n cause: error,\n });\n }\n };\n}\n\n/**\n * Thread id for a NEW Slack channel thread. Repo-backed threads take the\n * user-session id AS their thread id, matching the web convention\n * (FactoryStartCoordinator seeds threads with threadId = sessionId) so\n * `/workspaces/{sessionId}/threads/{sessionId}` resolves Slack-created\n * sessions exactly like web-created ones — no `?resourceId=` override needed.\n * Chat-only threads keep the default random id: their `channel:...`\n * resourceId is a memory key, not a unique thread id.\n */\nexport const resolveChannelThreadId: ResolveThreadId = ({ resourceId, defaultThreadId }) =>\n resourceId.startsWith('channel:') ? defaultThreadId : resourceId;\n\n/** Create channel sessions with their Factory ownership present in initial controller state. */\nexport function createChannelSessionResolver(deps: SlackChannelDeps): ChannelSessionResolve {\n const sourceControlSessions = createSourceControlSessionLookup(configuredSourceControls(deps));\n return async ({ controller, thread, requestContext }) => {\n const sourceControl = thread.resourceId.startsWith('channel:')\n ? null\n : await sourceControlSessions.getSourceControlBySessionId(thread.resourceId);\n const owner = sourceControl\n ? await resolveFactoryProjectForSession({ sourceControl, sessionId: thread.resourceId })\n : null;\n return controller.createSession({\n id: thread.resourceId,\n ownerId: controller.id,\n resourceId: thread.resourceId,\n requestContext,\n ...(owner ? { tags: { factoryProjectId: owner.factoryProjectId } } : {}),\n });\n };\n}\n\n/**\n * Apply the factory's configuration to a Slack-created session the first time\n * its thread reaches the controller.\n *\n * Without this a Slack session runs on the SDK's built-in mode default\n * (`openai/gpt-5.5`), so a factory configured for any other provider fails every\n * message with a missing-credentials error. The web kickoff has always applied\n * the factory default; this brings Slack to the same footing.\n *\n * Only repo-backed threads are configured. Their resourceId IS the Factory\n * session id, which the source-control rows turn back into a project — a\n * chat-only `channel:...` id names no project, so there is nothing to read.\n *\n * The model this session starts on is picked here, once, in the order of who\n * chose it: the linked sender's active model pack, else the factory project's\n * default, else the SDK's built-in mode default. The choice is persisted on the\n * thread as `modeModelId_<mode>`, so it outlives the process that made it.\n *\n * Observational memory is configured here too, in the same order of who chose\n * it: the project's shared settings first, then the linked sender's own row,\n * which wins for every knob they have saved. The pair is re-applied on every\n * start rather than once — memory settings are stored preference, not a choice\n * made on this thread, and a restarted process re-resolves the project's row\n * before this hook runs.\n *\n * The model resolution is skipped on a session whose mode already has a model\n * persisted on the thread. That is the durable record of a deliberate choice —\n * either an earlier start or a user's own switch — and re-applying a preference\n * over it would undo the user's selection every time the process restarts.\n * Memory settings have no such per-thread record, so they are re-applied on\n * every start.\n */\nexport function createChannelSessionStartHook(deps: SlackChannelDeps): ChannelSessionStart {\n const { projects, memorySettings, modelPacks } = deps;\n const sourceControlSessions = createSourceControlSessionLookup(configuredSourceControls(deps));\n return async ({ session, thread, requestContext }) => {\n // Seed the tenant org above every guard below. `gateDispatch` stamps it on\n // the message's request context before the session exists, so this needs no\n // storage read — which matters, because the model lookups below deliberately\n // skip storage on a restarted session. A channel-only thread and a thread\n // whose dispatch was ungated both land here with no org and are marked\n // unresolved rather than being left to look like a local session.\n await seedSessionOrg(session, readRequestContextOrgId(requestContext));\n\n if (!projects) return;\n if (thread.resourceId.startsWith('channel:')) return;\n\n const sourceControl = await sourceControlSessions.getSourceControlBySessionId(thread.resourceId);\n if (!sourceControl) return;\n const owner = await resolveFactoryProjectForSession({ sourceControl, sessionId: thread.resourceId });\n if (!owner) return;\n\n // Repo-backed Slack sessions are factory sessions: stamp the owning\n // project onto controller state so downstream reads (org-first credential\n // resolution, authority gates) recognize them, same as board runs.\n await session.state.set({ factoryProjectId: owner.factoryProjectId });\n // Route the org through the seed helper rather than stamping it directly:\n // an ungated dispatch marked this session unresolved above, and owner\n // recovery is the resolution that has to clear that marker with it.\n await seedSessionOrg(session, owner.orgId);\n\n const modeModelKey = `modeModelId_${session.mode.get()}`;\n const persistedModelId = await session.thread.getSetting({ key: modeModelKey });\n if (typeof persistedModelId === 'string') {\n // A restarted session restores its generation model from the thread, but\n // still needs the project memory row and a provider-compatible fallback.\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n observationalMemoryModelId: persistedModelId,\n memorySettings,\n });\n // Subagent models live in session state only, so restore the ones this\n // thread pinned at its first start instead of re-resolving them.\n for (const agentType of SUBAGENT_TYPES) {\n const modelId = await session.thread.getSetting({ key: pinnedSubagentModelKey(agentType) });\n if (typeof modelId === 'string') await applySubagentModel(session, agentType, modelId);\n }\n } else {\n const factoryModelId = await resolveFactoryDefaultModelId(projects, owner.factoryProjectId);\n const userPackModels = await resolveActivePackModels(modelPacks, owner);\n const userModelId = userPackModels?.build || undefined;\n const selectedModelId = userModelId ?? factoryModelId;\n\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n defaultModelId: factoryModelId,\n // Slack runs with the linked sender's credentials. Derive OM's fallback\n // from that sender's selected model rather than the factory model, which\n // may belong to a provider the sender cannot access.\n observationalMemoryModelId: selectedModelId,\n memorySettings,\n });\n\n if (selectedModelId && selectedModelId !== factoryModelId) {\n // The sender's own choice beats the factory's. `switch` applies the model\n // and persists it as this mode's model on the thread in one step — which\n // is what makes the choice outlive this process.\n try {\n await session.model.switch({ modelId: selectedModelId });\n } catch (error) {\n console.warn(\"[slack] Failed to apply the sender's model pack model\", {\n modelId: selectedModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n const currentModelId = session.model.get();\n // The message continues on the factory/SDK model. Realign the OM\n // fallback with that model while preserving explicit project settings.\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n observationalMemoryModelId: currentModelId,\n memorySettings,\n });\n if (currentModelId && !factoryModelId) {\n try {\n await session.model.saveForMode({ modeId: session.mode.get(), modelId: currentModelId });\n } catch (saveError) {\n console.warn(\"[slack] Failed to persist the sender's model pack model\", {\n modelId: currentModelId,\n error: saveError instanceof Error ? saveError.message : String(saveError),\n });\n }\n }\n }\n } else if (!selectedModelId) {\n // Neither preference exists, so the SDK's built-in mode default is this\n // thread's model of record. Pin it too: a later SDK upgrade that moves\n // that default must not silently retarget a thread that already started.\n const currentModelId = session.model.get();\n if (currentModelId) {\n await session.model.saveForMode({ modeId: session.mode.get(), modelId: currentModelId });\n }\n }\n\n // Subagents follow the sender's pack like the TUI does (explore→fast,\n // plan→plan, execute→build); roles the pack leaves empty use the factory\n // default. Pinned on the thread like the main model, so a restart\n // restores these rather than whatever pack or default exists by then.\n const packSubagentModels = {\n explore: userPackModels?.fast,\n plan: userPackModels?.plan,\n execute: userPackModels?.build,\n };\n for (const agentType of SUBAGENT_TYPES) {\n const modelId = packSubagentModels[agentType] || factoryModelId;\n if (!modelId) continue;\n if (await applySubagentModel(session, agentType, modelId)) {\n await session.thread.setSetting({ key: pinnedSubagentModelKey(agentType), value: modelId });\n }\n }\n }\n\n // The sender's own observational-memory settings, applied last so they beat\n // the project's — and on EVERY start, not just the first. Unlike the model,\n // this is stored preference rather than a choice made on this thread: a\n // restarted process re-resolves the project row before this hook runs, so\n // skipping it here would quietly put a thread back on the project's OM\n // configuration. Chat-only threads never reach this point.\n await applyPersonalMemorySettings(session, {\n memorySettings,\n orgId: owner.orgId,\n userId: owner.userId,\n });\n };\n}\n\nconst SUBAGENT_TYPES = ['explore', 'plan', 'execute'] as const;\nconst pinnedSubagentModelKey = (agentType: string) => `slackSubagentModelId_${agentType}`;\n\n/** Best-effort: a subagent model that can't be applied must not fail the message. */\nasync function applySubagentModel(\n session: Parameters<ChannelSessionStart>[0]['session'],\n agentType: (typeof SUBAGENT_TYPES)[number],\n modelId: string,\n): Promise<boolean> {\n try {\n await session.subagents.model.set({ modelId, agentType });\n return true;\n } catch (error) {\n console.warn('[slack] Failed to apply the subagent model', {\n agentType,\n modelId,\n error: error instanceof Error ? error.message : String(error),\n });\n return false;\n }\n}\n\n/**\n * The models of the sender's active model pack — the models that user chose\n * for themselves — or `undefined` when they have no pack.\n *\n * Best-effort by design: an uninitialized model-packs domain, a read failure, or\n * a pack saved without a build model all mean \"no personal preference\", which\n * falls through to the factory default rather than failing the dispatch.\n */\nasync function resolveActivePackModels(\n modelPacks: ModelPacksStorage | undefined,\n owner: { orgId: string; userId: string },\n): Promise<ActiveModelPackRecord['models'] | undefined> {\n if (!modelPacks) return undefined;\n try {\n const active = await modelPacks.getActive({ orgId: owner.orgId, userId: owner.userId });\n return active?.models;\n } catch (error) {\n console.warn('[slack] model pack lookup failed for a new session', {\n orgId: owner.orgId,\n userId: owner.userId,\n error: error instanceof Error ? error.message : String(error),\n });\n return undefined;\n }\n}\n\n/**\n * The internal Mastra thread the framework created for a channel conversation.\n * The handler's `thread.id` is the platform thread id (e.g. `slack:C123:ts`),\n * NOT the internal UUID — the mapping lives in the stored thread's channel\n * metadata.\n */\nasync function findInternalThread(mastra: Mastra | undefined, thread: HandlerThread) {\n const store = await mastra?.getStorage()?.getStore('memory');\n const { threads } = (await store?.listThreads({\n filter: {\n metadata: {\n channel_platform: thread.adapter.name,\n channel_externalThreadId: thread.id,\n channel_externalChannelId: thread.channelId,\n },\n },\n perPage: 1,\n })) ?? { threads: [] };\n return threads[0];\n}\n\nasync function prepareExistingSessionOwnerContext(\n thread: HandlerThread,\n deps: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n options: { expectedOrgId?: string; requireInternalThread: boolean },\n): Promise<'ready' | 'organization-mismatch'> {\n const sourceControls = configuredSourceControls(deps);\n if (sourceControls.length === 0 && !options.requireInternalThread) return 'ready';\n\n const internalThread = await findInternalThread(ctx.mastra, thread);\n if (!internalThread) {\n if (options.requireInternalThread) {\n throw new Error(`Could not resolve the internal Slack thread for ${thread.id}.`);\n }\n return 'ready';\n }\n if (sourceControls.length === 0 || internalThread.resourceId.startsWith('channel:')) return 'ready';\n if (!options.expectedOrgId) {\n throw new Error(`Could not authorize the owner of Slack Factory session ${internalThread.resourceId}.`);\n }\n\n const preparation = await prepareSessionRunContext(\n ctx.requestContext,\n internalThread.resourceId,\n { sessions: createSourceControlSessionLookup(sourceControls) },\n { expectedOrgId: options.expectedOrgId },\n );\n if (preparation === 'organization-mismatch') return preparation;\n if (preparation === 'unavailable') {\n throw new Error(`Could not authorize the owner of Slack Factory session ${internalThread.resourceId}.`);\n }\n return 'ready';\n}\n\n/**\n * Build the \"new session\" handler for mention / direct-message events. A mention or\n * DM on a not-yet-subscribed thread starts a NEW session; once subscribed, later\n * events are follow-ups and don't re-announce.\n */\n/**\n * Run the account-link + factory-routing gates for one inbound message.\n * Returns `null` when the run must not dispatch (a prompt card was posted\n * where possible); otherwise the dispatch context — with `routed` present\n * only when a linked sender resolved to a factory.\n */\nasync function gateDispatch(\n thread: HandlerThread,\n message: HandlerMessage,\n deps: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n options: { requireInternalThread: boolean } = { requireInternalThread: false },\n): Promise<{\n routed?: { link: ChannelAccountLink; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n} | null> {\n const { accountLinks, projects } = deps;\n const sender = await resolveLinkedSender({ thread, message, accountLinks });\n if (sender.status === 'blocked') return null;\n // Linked senders must also route to a Factory project before a run starts.\n if (sender.status === 'linked' && accountLinks) {\n // Stamp the tenant on the run's request context — the single seam\n // `resolveCredentialStore` reads to load this sender's model credentials.\n // This belongs to the link, not to the routing: a linked sender whose\n // factory routing comes back `ungated` still exits below and dispatches, so\n // stamping only in the routed branch would silently run them on default\n // credentials.\n ctx.requestContext.set('user', { id: sender.link.userId, organizationId: sender.link.orgId });\n const ownerContext = await prepareExistingSessionOwnerContext(thread, deps, ctx, {\n expectedOrgId: sender.link.orgId,\n requireInternalThread: options.requireInternalThread,\n });\n if (ownerContext === 'organization-mismatch') {\n await thread.post('This thread belongs to a Factory session in another organization.');\n return null;\n }\n\n const route = await resolveFactoryForLink({ thread, ...sender, accountLinks, projects });\n if (route.status === 'blocked') return null;\n if (route.status === 'resolved') {\n return {\n routed: {\n link: sender.link,\n factoryProjectId: route.factoryProjectId,\n slackWorkItemsEnabled: route.slackWorkItemsEnabled,\n },\n };\n }\n }\n return {};\n}\n\n/**\n * A channel id and a `ts` name a thread only inside the workspace that issued\n * them, so the team scopes the card's key — without it two workspaces could\n * hold one key and an aside would land on another tenant's card.\n */\nexport function slackThreadSource(thread: HandlerThread, teamId?: string): ExternalWorkItemSource {\n return {\n integrationId: thread.adapter.name,\n type: 'slack-thread',\n ...(teamId ? { workspaceId: teamId } : {}),\n externalId: thread.id,\n };\n}\n\n/** Cards written before the key carried a team are still keyed bare. */\nasync function findThreadWorkItem(\n workItems: WorkItemsStorage,\n thread: HandlerThread,\n teamId?: string,\n): Promise<WorkItemRow | null> {\n const scoped = await workItems.getBySource(slackThreadSource(thread, teamId));\n if (scoped || !teamId) return scoped;\n return workItems.getBySource(slackThreadSource(thread));\n}\n\n/**\n * Upsert the Work-board card for a dispatched Slack-thread run. Keyed on the\n * thread via `externalSource` — the work-items domain's unique\n * `(factory_project_id, source_key)` index makes repeat messages reuse the\n * same card, and `reuseMode: 'preserve'` keeps a card a human already dragged\n * across stages untouched. The card lands in Building (`execute`) for every\n * dispatched thread (DM or mention) — there is deliberately no per-origin\n * stage split; smart routing is a follow-up.\n *\n * The session id / branch / threadId and the workspace deep-link are resolved\n * by the caller (which already looked up the internal thread), so this helper\n * just shapes and writes. Best-effort: the run is already dispatched, so a\n * failure logs instead of throwing — work-item creation must never abort a Slack run.\n */\nexport async function upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link,\n factoryProjectId,\n session,\n url,\n}: {\n workItems: WorkItemsStorage;\n thread: HandlerThread;\n message: HandlerMessage;\n link: ChannelAccountLink;\n factoryProjectId: string;\n /**\n * The repo-backed Factory session to bind under the `chat` role, or\n * `undefined` for a chat-only thread (no Factory session to bind).\n */\n session?: { sessionId: string; branch: string; threadId: string };\n /** Workspace deep-link to the running session; omitted when no public URL. */\n url?: string;\n}): Promise<void> {\n try {\n const title = workItemTitleFrom(message.text);\n\n await workItems.upsert({\n orgId: link.orgId ?? '',\n userId: link.userId,\n factoryProjectId,\n reuseMode: 'preserve',\n input: {\n title: title || 'Slack thread',\n externalSource: { ...slackThreadSource(thread, slackTeamId(message)), ...(url ? { url } : {}) },\n stages: ['execute'],\n ...(session ? { sessions: { chat: session } } : {}),\n },\n });\n } catch (error) {\n console.warn('[slack] work-item creation failed for thread', thread.id, error);\n }\n}\n\nfunction preDispatchErrorDetails(error: unknown): { message: string; cause?: { message: string } } {\n const readMessage = (value: unknown) => {\n const message =\n typeof value === 'string'\n ? value\n : value && typeof value === 'object' && 'message' in value\n ? value.message\n : undefined;\n return typeof message === 'string' && message.length > 0 ? message : undefined;\n };\n try {\n const message = readMessage(error) ?? 'Unknown error';\n const cause = error && typeof error === 'object' && 'cause' in error ? readMessage(error.cause) : undefined;\n return { message, ...(cause ? { cause: { message: cause } } : {}) };\n } catch {\n return { message: 'Error details unavailable' };\n }\n}\n\nasync function reportPreDispatchError(\n thread: HandlerThread,\n message: HandlerMessage,\n ctx: ChannelHandlerContext,\n error: unknown,\n): Promise<void> {\n try {\n if (error instanceof ChannelSessionRejectedError) return;\n } catch {\n // A thrown proxy can fail even the refusal check; still report the failure.\n }\n const correlation = {\n platform: thread.adapter.name,\n threadId: thread.id,\n messageId: message.id,\n authorId: message.author.userId,\n };\n const logger = typeof ctx.mastra?.getLogger === 'function' ? ctx.mastra.getLogger() : undefined;\n const logError = (line: string) => (logger ? logger.error(line) : console.error(line));\n logError(`[slack] Pre-dispatch failure ${JSON.stringify({ ...correlation, error: preDispatchErrorDetails(error) })}`);\n try {\n // The message id is the lookup key for the diagnostic above.\n await thread.post(`Couldn’t start processing your message. Please try again.\\n\\`messageId: ${message.id}\\``);\n } catch (deliveryError) {\n logError(\n `[slack] Failed to deliver pre-dispatch error reply ${JSON.stringify({ ...correlation, error: preDispatchErrorDetails(deliveryError) })}`,\n );\n }\n}\n\nfunction createNewSessionChatHandler(deps: SlackChannelDeps): ChannelHandler {\n const { workItems } = deps;\n return async (thread, message, defaultHandler, ctx) => {\n // Gate on the sender having linked their Slack account to a Mastra tenant.\n // Unlinked → post the ephemeral Connect card and stop; no session/run is\n // created (which would otherwise be tenant-less and fail credential\n // resolution). This handler is the only gate — core dispatches whatever\n // reaches it — so every slot that can start a run must call it.\n let gate: Awaited<ReturnType<typeof gateDispatch>>;\n let isNewSession: boolean;\n try {\n gate = await gateDispatch(thread, message, deps, ctx);\n if (!gate) return;\n\n // A mention on a not-yet-subscribed thread is a NEW session. The\n // default handler auto-subscribes, so once subscribed this is a\n // follow-up mention — don't re-announce.\n isNewSession = !(await thread.isSubscribed());\n } catch (error) {\n await reportPreDispatchError(thread, message, ctx, error);\n return;\n }\n\n // Run the framework handler first so the internal Mastra thread and\n // controller session are created before we build the deep link.\n await defaultHandler(thread, message);\n\n if (!isNewSession) return;\n\n // The internal-thread lookup and deep-link are needed by BOTH the\n // announcement card AND work-item creation, so they run BEFORE the\n // card-only `MASTRACODE_PUBLIC_URL` gate — a deployment without a public\n // origin should still create board cards, just without a clickable link.\n const internalThread = await findInternalThread(ctx.mastra, thread);\n if (!internalThread) {\n console.warn('[onMention] no internal thread found for', thread.id);\n return;\n }\n\n // When the sender routed to a factory we know exactly which workspace the\n // session belongs to — deep-link straight into it. A repo-backed thread's\n // resourceId IS the Factory user-session id, so the link lands on the same\n // route a web-started run navigates to; chat-only threads keep the literal\n // `channel` segment (the real resource rides the `?resourceId=` override).\n // Unrouted senders fall back to the factory-agnostic /threads/ redirect.\n // One predicate drives both the path segment and the query param, so the\n // two can never disagree about what the URL already carries.\n const isChatOnly = internalThread.resourceId.startsWith('channel:');\n const workspaceSegment = isChatOnly ? 'channel' : encodeURIComponent(internalThread.resourceId);\n const threadPath = gate.routed\n ? `/factories/${encodeURIComponent(gate.routed.factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(internalThread.id)}`\n : `/threads/${internalThread.id}`;\n\n // The param is an override for a URL that can't otherwise name its\n // resource. A routed repo-backed thread already spells the resourceId out\n // as its workspace segment, so appending it again is duplication the app\n // ignores. Chat-only threads need it (their segment is the literal string\n // `channel`), and so does the unrouted fallback, which has no workspace\n // segment at all — `ChannelThreadRedirect` forwards the search through.\n //\n // One shared deep-link: the card's button and the work-item `url` read the\n // SAME value so they can never drift. Undefined without a public origin —\n // the card is then skipped, but the work item is still created (url omitted).\n const needsResourceParam = isChatOnly || !gate.routed;\n const deepLink = process.env.MASTRACODE_PUBLIC_URL\n ? needsResourceParam\n ? `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}?resourceId=${encodeURIComponent(internalThread.resourceId)}`\n : `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}`\n : undefined;\n\n // A dispatched, routed new-session thread becomes a Work-board card in\n // Building. Only routed senders (linked → factory) have the org/user/factory\n // a work item needs. Bind the repo-backed Factory session under the `chat`\n // role; a chat-only `channel:` resourceId is NOT a session id, so bind\n // nothing rather than a bad id. Best-effort (the helper swallows failures).\n if (workItems && gate.routed?.slackWorkItemsEnabled) {\n const session = isChatOnly\n ? undefined\n : { sessionId: internalThread.resourceId, branch: threadBranch(thread.id), threadId: internalThread.id };\n await upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link: gate.routed.link,\n factoryProjectId: gate.routed.factoryProjectId,\n session,\n url: deepLink,\n });\n }\n\n // The announcement card is only useful with a public origin to deep-link\n // to — otherwise the link would be `undefined/threads/...`. Without one the\n // session (and now the work item) still exist; we just skip the broken card.\n if (!deepLink) return;\n\n await thread.post(\n Card({\n title: 'New session started',\n children: [Actions([LinkButton({ url: deepLink, label: 'View session' })])],\n }),\n );\n };\n}\n\n/**\n * An `aside` never reaches the agent, but it is still part of the conversation:\n * record it on the card the thread created. The link is resolved here rather\n * than through `gateDispatch`, which posts a Connect card an aside must not\n * trigger. Best-effort — nothing here may surface in the Slack thread.\n */\nasync function ingestAside(\n thread: HandlerThread,\n message: HandlerMessage,\n { feed, workItems, accountLinks }: SlackChannelDeps,\n): Promise<void> {\n if (!feed || !workItems) return;\n const body = resolveEmojiShortcodes(message.text.replace(/^aside\\b[,:]?\\s*/i, '').trim());\n if (!body) return;\n try {\n const teamId = slackTeamId(message);\n const workItem = await findThreadWorkItem(workItems, thread, teamId);\n if (!workItem) return;\n\n const external: FactoryActorExternalIdentity = {\n platform: thread.adapter.name,\n ...(teamId ? { teamId } : {}),\n messageId: message.id,\n userId: message.author.userId,\n userName: message.author.userName,\n fullName: message.author.fullName,\n isBot: message.author.isBot,\n };\n const link =\n accountLinks && teamId\n ? await accountLinks.getAccountLink({\n platform: external.platform,\n externalTeamId: teamId,\n externalUserId: external.userId,\n })\n : null;\n\n const result = await feed.createComment({\n orgId: workItem.orgId,\n workItemId: workItem.id,\n author: actorFromChannelAuthor(external, link),\n body,\n occurredAt: message.metadata.dateSent,\n externalSource: slackCommentSource(thread.id, message.id, teamId),\n });\n if (result.status !== 'created') {\n console.warn('[slack] aside not ingested', { thread: thread.id, messageTs: message.id, status: result.status });\n }\n } catch (error) {\n // Slack was acked before this handler ran, so a lost aside is never redelivered.\n console.warn('[slack] aside ingest failed', { thread: thread.id, messageTs: message.id, error });\n }\n}\n\nexport const createHandlers = (deps: SlackChannelDeps): ChannelHandlers => {\n const newSessionChatHandler = createNewSessionChatHandler(deps);\n\n return {\n onSubscribedMessage: async (thread, message, defaultHandler, ctx) => {\n // `aside` as its own leading word lets humans talk in a subscribed\n // thread without the bot replying. Word boundary so messages that\n // merely start with \"aside...\" (e.g. \"asides can wait\") still route.\n if (/^aside\\b/i.test(message.text)) return ingestAside(thread, message, deps);\n // A subscribed follow-up from an unlinked sender must not run either\n // (e.g. the link was removed mid-conversation), and it must still\n // resolve a factory (e.g. the default was cleared or its factory\n // deleted mid-conversation).\n try {\n const gate = await gateDispatch(thread, message, deps, ctx, { requireInternalThread: true });\n if (!gate) return;\n } catch (error) {\n await reportPreDispatchError(thread, message, ctx, error);\n return;\n }\n await defaultHandler(thread, message);\n },\n onMention: newSessionChatHandler,\n onDirectMessage: newSessionChatHandler,\n };\n};\n\n/** Slack app credentials, passed in explicitly rather than read from env here. */\ninterface SlackCredentials {\n clientId?: string;\n clientSecret?: string;\n signingSecret: string;\n botToken?: string;\n}\n\nexport function createSlackChannelsConfig(deps: SlackChannelDeps & { slack: SlackCredentials }): FactoryChannelsConfig {\n // Per HTTP attempt, not per call: the WebClient's own retry policy still\n // waits out a 429. Without it a dead socket hangs a post forever.\n const adapter = createSlackAdapter({ ...deps.slack, webClientOptions: { timeout: SLACK_REQUEST_TIMEOUT_MS } });\n const slack =\n deps.adapterOptions?.streaming === false\n ? { adapter, ...deps.adapterOptions }\n : { adapter, ...deps.adapterOptions, streaming: deps.adapterOptions?.streaming ?? true };\n\n return {\n adapters: { slack },\n handlers: createHandlers(deps),\n // New linked+repo-backed threads own a Factory user-session id as their\n // resourceId, which is what makes the controller session repo-backed.\n resolveResourceId: createChannelResourceIdResolver(deps),\n resolveThreadId: resolveChannelThreadId,\n resolveSession: createChannelSessionResolver(deps),\n // Those sessions are created by the channel machinery, not the web kickoff,\n // so this is where they pick up the factory's configuration.\n onSessionStart: createChannelSessionStartHook(deps),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"slack.js","names":[],"sources":["../../../src/integrations/slack/slack.ts"],"sourcesContent":["import { ChannelSessionRejectedError } from '@mastra/core/channels';\nimport type {\n ChannelHandler,\n ChannelHandlerContext,\n ChannelHandlers,\n ChannelSessionResolve,\n ChannelSessionStart,\n ResolveResourceId,\n ResolveThreadId,\n} from '@mastra/core/channels';\nimport type { Mastra } from '@mastra/core/mastra';\nimport { createSlackAdapter } from '@mastra/slack';\nimport type { SlackAdapterChannelConfig } from '@mastra/slack';\nimport { Card, CardText, Actions, LinkButton } from 'chat';\n\nimport {\n createSourceControlSessionLookup,\n FactorySourceControlConflictError,\n hydrateFactorySession,\n resolveFactoryDefaultModelId,\n resolveFactoryProjectForSession,\n resolveFactorySourceControl,\n resolveFactorySourceRepository,\n} from '../../session/factory-session.js';\nimport { applyPersonalMemorySettings } from '../../session/memory-settings-hydration.js';\nimport { readRequestContextOrgId, seedSessionOrg } from '../../session/org-seed.js';\nimport type {\n ChannelAccountLink,\n ChannelAccountLinkKey,\n ChannelIdentityStorage,\n} from '../../storage/domains/channel-identity/base.js';\nimport type { FactoryActorExternalIdentity } from '../../storage/domains/comments/actor.js';\nimport { actorFromChannelAuthor } from '../../storage/domains/comments/actor.js';\nimport type { CommentsDomain } from '../../storage/domains/comments/domain.js';\nimport type { MemorySettingsStorage } from '../../storage/domains/memory-settings/base.js';\nimport type { ActiveModelPackRecord, ModelPacksStorage } from '../../storage/domains/model-packs/base.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { ExternalWorkItemSource, WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { FactoryChannelsConfig } from '../base.js';\nimport { prepareSessionRunContext } from '../subscription-session.js';\n\nimport { resolveEmojiShortcodes } from './emoji.js';\nimport { slackCommentSource } from './feed-publisher.js';\n\n// Derive the thread/message types from the core handler signature rather than\n// importing them from `chat` directly: mc-web can resolve a different `chat`\n// version than @mastra/core, and the two `Thread`/`Message` declarations are\n// structurally incompatible (private fields). Using the handler's own types\n// keeps everything on one version.\ntype HandlerThread = Parameters<ChannelHandler>[0];\ntype HandlerMessage = Parameters<ChannelHandler>[1];\n\nconst SLACK_REQUEST_TIMEOUT_MS = 15_000;\nconst MAX_WORK_ITEM_TITLE_CHARS = 80;\nconst graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\n\nclass SlackSessionStartError extends Error {}\n\n/**\n * A card is titled with what the sender wrote. Cutting counts graphemes, not\n * code points: `👍🏼` is a base plus a skin-tone modifier, and a cut between\n * them leaves the wrong emoji.\n */\nfunction workItemTitleFrom(messageText: string): string {\n const resolved = resolveEmojiShortcodes(messageText);\n const characters = [...graphemes.segment(resolved)].map(({ segment }) => segment);\n if (characters.length <= MAX_WORK_ITEM_TITLE_CHARS) return resolved;\n return `${characters.slice(0, MAX_WORK_ITEM_TITLE_CHARS - 1).join('')}…`;\n}\n\n/** Dependencies the Slack channel handlers close over, injected from the web entry. */\ninterface SlackChannelDeps {\n /**\n * The factory's reverse-index store mapping a Slack sender to a Mastra\n * tenant. When provided, inbound messages from an unlinked sender are not\n * dispatched — the run only proceeds (with the sender's tenant stamped on\n * the request context) once they've linked their account. Unlinked senders\n * get an ephemeral \"connect your account\" card instead.\n */\n accountLinks?: ChannelIdentityStorage;\n /**\n * Factory projects domain. When provided (alongside `accountLinks`), a\n * linked sender's run must also resolve to a Factory project before it\n * dispatches: their link's default factory, else their tenant's only\n * factory (stamped back onto the link), else a \"pick a default factory\"\n * card posted in the thread and no run. Unset → no factory routing (runs\n * dispatch as before).\n */\n projects?: FactoryProjectsStorage;\n /**\n * Storage handle of the integration that owns source control\n * (`IntegrationContext.storage.sourceControlOwner`). Used to make new Slack\n * threads repo-backed: when the sender is linked and their factory has a\n * repository, the thread's resourceId becomes a Factory user-session id (repo\n * cloned on a `slack/{threadTs}` branch) instead of the chat-only\n * `channel:...` id. It also lets a started session read back the project it\n * belongs to. Nothing here is provider-specific — the connection is matched\n * by the handle's own `integrationId`. Absent (no source-control integration\n * registered) → chat-only sessions as before.\n */\n sourceControl?: SourceControlStorageHandle;\n /** Registered source-control partitions used to select the provider linked to each Factory project. */\n sourceControls?: readonly SourceControlStorageHandle[];\n /**\n * Observational-memory settings domain. When provided, a repo-backed session\n * adopts its factory project's shared memory settings on start, matching the\n * web kickoff — and the linked sender's own settings win over them for every\n * knob the sender has saved.\n */\n memorySettings?: MemorySettingsStorage;\n /**\n * Model-packs domain. When provided, a new repo-backed session starts on the\n * linked sender's active pack build model — the model that user picked for\n * themselves — before the factory's shared default. Read once, when the\n * thread's build model is first chosen; the choice is persisted on the thread\n * and never re-resolved. Unset → the factory default, as before.\n */\n modelPacks?: ModelPacksStorage;\n /**\n * Factory work-items domain. When provided, a dispatched new-session thread\n * (DM or mention) upserts a Work-board card in Building (`execute`) carrying\n * the Slack thread as its external source and binding the repo-backed\n * session. Best-effort — a failure never blocks the run. Unset → no card.\n */\n workItems?: WorkItemsStorage;\n /**\n * Work-item feed. With it (and `workItems`), an `aside` lands as a comment on\n * the card the thread created. Unset → asides stay ignored, as before.\n */\n feed?: Pick<CommentsDomain, 'createComment'>;\n /** Overrides applied to the Slack channel adapter entry. */\n adapterOptions?: SlackAdapterChannelConfig;\n}\n\nfunction configuredSourceControls(deps: SlackChannelDeps): readonly SourceControlStorageHandle[] {\n return deps.sourceControls ?? (deps.sourceControl ? [deps.sourceControl] : []);\n}\n\n/**\n * Read the Slack team id off a raw platform payload (Events API envelope or\n * slash-command body — both carry `team_id`), duck-typed to build the\n * workspace-scoped account-link key.\n */\nfunction rawTeamId(rawPayload: unknown): string | undefined {\n if (!rawPayload || typeof rawPayload !== 'object') return undefined;\n const raw = rawPayload as { team_id?: unknown; team?: unknown };\n if (typeof raw.team_id === 'string' && raw.team_id) return raw.team_id;\n if (typeof raw.team === 'string' && raw.team) return raw.team;\n if (raw.team && typeof raw.team === 'object') {\n const id = (raw.team as { id?: unknown }).id;\n if (typeof id === 'string' && id) return id;\n }\n return undefined;\n}\n\n/**\n * The Slack team id survives onto a normalized chat Message only on\n * `message.raw` (the Slack Events API envelope).\n */\nfunction slackTeamId(message: HandlerMessage): string | undefined {\n return rawTeamId(message.raw);\n}\n\n/**\n * Resolve the web-UI origin for links humans open in a browser (Connect card,\n * session deep links). Prefers `MASTRACODE_PUBLIC_URL` — the origin auth\n * cookies and OAuth redirect allow-lists are registered against — over the\n * channels tunnel, which only Slack's servers need to reach.\n */\nfunction webPublicUrl(): string | undefined {\n return process.env.MASTRACODE_PUBLIC_URL ?? process.env.MASTRACODE_CHANNELS_PUBLIC_URL;\n}\n\n/** Outcome of the sender-link gate for one inbound message. */\ntype LinkedSenderResult =\n /** Gating not configured — dispatch as before account linking existed. */\n | { status: 'ungated' }\n /** Sender unlinked — Connect card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** Sender linked — their tenant plus the sender key the link lives under. */\n | { status: 'linked'; link: ChannelAccountLink; key: ChannelAccountLinkKey };\n\n/**\n * Resolve the sender's account link, posting an ephemeral \"connect your\n * account\" card (visible only to the sender) linking into the web UI's\n * Slack-connect flow when they're unlinked.\n */\nexport async function resolveLinkedSender({\n thread,\n message,\n accountLinks,\n}: {\n thread: HandlerThread;\n message: HandlerMessage;\n accountLinks?: ChannelIdentityStorage;\n}): Promise<LinkedSenderResult> {\n if (!accountLinks) return { status: 'ungated' };\n const platform = thread.adapter.name;\n const externalUserId = message.author.userId;\n const externalTeamId = slackTeamId(message);\n // Without a team id we can't identify the workspace-scoped link; treat as\n // unlinked so a run never proceeds tenant-less.\n const key = externalTeamId ? { platform, externalTeamId, externalUserId } : undefined;\n const link = key ? await accountLinks.getAccountLink(key) : null;\n if (link && key) return { status: 'linked', link, key };\n\n const publicUrl = webPublicUrl();\n // A public origin is all the card needs. The link carries no identity: the\n // web app authenticates the visitor, then Slack's OIDC flow proves which\n // Slack account they control. Without an origin, still block, just no card.\n if (publicUrl) {\n await thread.postEphemeral(message.author, buildConnectCard(publicUrl, thread), { fallbackToDM: true });\n }\n return { status: 'blocked' };\n}\n\nconst retryHint = (thread: HandlerThread) => (thread.isDM ? 'message me again' : 'mention me again');\n\n/**\n * The \"connect your account\" card. The link is deliberately identity-free —\n * `/connect/slack` sends the visitor to Connections, where \"Connect Slack\"\n * runs the OIDC flow and Slack itself asserts the (team, user) pair.\n */\nfunction buildConnectCard(publicUrl: string, thread: HandlerThread) {\n return Card({\n title: 'Connect your account',\n children: [\n CardText(`Connect your account to use this agent, then ${retryHint(thread)}.`),\n Actions([\n LinkButton({\n url: `${publicUrl}/connect/slack`,\n label: 'Connect account',\n }),\n ]),\n ],\n });\n}\n\n/** Outcome of factory routing for one linked sender's inbound message. */\ntype FactoryRouteResult =\n /** Factory routing not configured — dispatch without a factory. */\n | { status: 'ungated' }\n /** No factory resolved — prompt card posted (when possible), do not dispatch. */\n | { status: 'blocked' }\n /** The Factory project this sender's runs route to. */\n | { status: 'resolved'; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n\n/**\n * Decide which Factory project a linked sender's run belongs to:\n *\n * 1. The link's `defaultFactoryProjectId`, when it still exists (a stale id —\n * deleted factory — falls through as if unset).\n * 2. Else, the tenant's only factory, stamped back onto the link so it shows\n * up (and stays editable) in Connected Accounts settings.\n * 3. Else — zero or several factories — a \"pick a default factory\" card\n * posted publicly in the thread so Slack notifies the sender, and the run is\n * blocked.\n */\nexport async function resolveFactoryForLink({\n thread,\n link,\n key,\n accountLinks,\n projects,\n}: {\n thread: HandlerThread;\n link: ChannelAccountLink;\n key: ChannelAccountLinkKey;\n accountLinks: ChannelIdentityStorage;\n projects?: FactoryProjectsStorage;\n}): Promise<FactoryRouteResult> {\n if (!projects) return { status: 'ungated' };\n // Factories are org-scoped; a personal account (no org) has none and lands\n // on the prompt below.\n const orgId = link.orgId ?? '';\n\n if (link.defaultFactoryProjectId) {\n const existing = await projects.get({ orgId, id: link.defaultFactoryProjectId });\n if (existing) {\n return {\n status: 'resolved',\n factoryProjectId: existing.id,\n slackWorkItemsEnabled: existing.slackWorkItemsEnabled,\n };\n }\n }\n\n const factories = orgId ? await projects.list({ orgId }) : [];\n if (factories.length === 1) {\n const only = factories[0]!;\n await accountLinks.setDefaultFactory({ ...key, userId: link.userId, factoryProjectId: only.id });\n return {\n status: 'resolved',\n factoryProjectId: only.id,\n slackWorkItemsEnabled: only.slackWorkItemsEnabled,\n };\n }\n\n const publicUrl = webPublicUrl();\n if (publicUrl) {\n await thread.post(\n Card({\n title: 'Pick a default factory',\n children: [\n CardText(\n factories.length === 0\n ? `Your account has no factory yet. Create one in the web app, then ${retryHint(thread)}.`\n : `Your account has several factories. Pick which one Slack sessions should go to, then ${retryHint(thread)}.`,\n ),\n Actions([\n LinkButton({\n url: `${publicUrl}/settings/connections`,\n label: 'Open settings',\n }),\n ]),\n ],\n }),\n );\n }\n return { status: 'blocked' };\n}\n\n/**\n * Deterministic per-thread branch name: `slack/{threadTs}` with characters\n * outside the sandbox git-ref allow-list (`[A-Za-z0-9_./-]`, and `.` for\n * readability) mapped to `-`. `thread.id` is `{channelId}:{threadTs}`\n * (platform-prefixed on handler threads) — the trailing segment is the ts.\n *\n * Top-level DM and channel conversations use the empty-threadTs thread form,\n * so the trailing segment can be empty; a bare `slack/` is not a valid git\n * ref. Fall back to the last non-empty segment (the channel id): one\n * deterministic branch per top-level conversation.\n */\nfunction threadBranch(threadId: string): string {\n const segments = threadId.split(':');\n const tail = segments.findLast(segment => segment.length > 0) ?? threadId;\n return `slack/${tail.replace(/[^A-Za-z0-9_/-]/g, '-')}`;\n}\n\n/**\n * Resolve the resourceId for a NEW Slack channel thread. A linked sender whose\n * factory has a repository gets a Factory user-session id — the controller\n * session then materializes the repo sandbox via the factory's dynamic\n * workspace (clone + PAT), the session shows up in the web Sessions list, and\n * View Session deep-links land on the normal workspace route.\n *\n * A gated deployment refuses a thread it cannot place, rather than falling back\n * to a chat-only session: a repo-backed thread is the whole point of the\n * integration, and answering in a chat-only one would run the sender's request\n * in no project, on the SDK's built-in defaults. A linked sender whose project\n * cannot start a session gets an actionable error in Slack; the dispatch gate\n * handles unlinked or unrouted senders before this hook runs.\n * The hook only runs when a NEW thread is actually created (`getOrCreateThread`\n * resolves the owner lazily), so an established conversation is never touched:\n * it keeps its session, its model, and its history.\n *\n * The chat-only `channel:...` id survives for two cases where refusing would be\n * wrong: a deployment that cannot produce a repo-backed thread at all (no\n * account linking, no projects, or no source-control integration registered),\n * and a message carrying no sender id. In both, a `channel:` thread is the only\n * shape a session can take.\n *\n * Pure lookups only — cards for unlinked/unrouted senders are the dispatch\n * gate's job; this hook must never post.\n */\nexport function createChannelResourceIdResolver(deps: SlackChannelDeps): ResolveResourceId {\n const { accountLinks, projects } = deps;\n const sourceControls = configuredSourceControls(deps);\n return async ({ platform, thread, message }) => {\n // NOT the hook's `defaultResourceId`: configuring a custom resolver\n // bypasses AgentControllerChannels' own `channel:{thread.id}` derivation\n // (agent-controller-channels.ts `resolveChannelResourceId`), and the base\n // default is the per-USER memory key. Chat-only fallbacks must stay\n // per-thread, so reproduce the controller default here.\n const chatOnlyResourceId = `channel:${thread.id}`;\n // A deployment with no account linking, no projects, or no source-control\n // integration cannot produce a repo-backed thread at all, so a `channel:`\n // thread is the only shape it has. Every refusal below is a different thing\n // entirely: a sender the bot cannot place, in a deployment that can.\n if (!accountLinks || !projects || sourceControls.length === 0) return chatOnlyResourceId;\n // No sender id at all — a malformed message, not a sender the host turned\n // away. There is nothing to place, so give it the per-thread memory key.\n if (!message.author.userId) return chatOnlyResourceId;\n\n const externalTeamId = rawTeamId(message.raw);\n if (!externalTeamId) {\n throw new ChannelSessionRejectedError('No Slack workspace id on the message — the sender cannot be identified');\n }\n try {\n const link = await accountLinks.getAccountLink({\n platform,\n externalTeamId,\n externalUserId: message.author.userId,\n });\n if (!link) {\n throw new ChannelSessionRejectedError(\n `Slack sender ${message.author.userId} is not linked to a Factory account`,\n );\n }\n\n // Same chain as `resolveFactoryForLink`, minus prompts/stamping: the\n // dispatch gate has already run (and stamped a lone factory) by the\n // time a new thread is created, so this is a read-only re-resolve.\n const orgId = link.orgId ?? '';\n let factoryProjectId: string | undefined;\n if (link.defaultFactoryProjectId && (await projects.get({ orgId, id: link.defaultFactoryProjectId }))) {\n factoryProjectId = link.defaultFactoryProjectId;\n } else if (orgId) {\n const factories = await projects.list({ orgId });\n if (factories.length === 1) factoryProjectId = factories[0]!.id;\n }\n if (!factoryProjectId) {\n throw new ChannelSessionRejectedError(`Linked Slack sender ${message.author.userId} has no Factory project`);\n }\n\n // Linked and routed, so a repo-backed session is not a preference: with no\n // repository to work in there is no thread to start, and starting one\n // anyway would answer in a project the sender never picked, on the SDK's\n // built-in defaults.\n const sourceControl = await resolveFactorySourceControl({ sourceControls, orgId, factoryProjectId });\n if (!sourceControl) {\n const connections = await Promise.all(\n sourceControls.map(sourceControl => sourceControl.connections.list({ orgId, factoryProjectId })),\n );\n throw new SlackSessionStartError(\n connections.some(rows => rows.length > 0)\n ? 'Could not start a session: link a repository to this Factory project.'\n : 'Could not start a session: connect source control to this Factory project.',\n );\n }\n const repo = await resolveFactorySourceRepository({\n sourceControl,\n orgId,\n factoryProjectId,\n firstLinkedRepository: true,\n });\n if (!repo.found) {\n throw new SlackSessionStartError(\n repo.reason === 'connection'\n ? 'Could not start a session: connect source control to this Factory project.'\n : 'Could not start a session: link a repository to this Factory project.',\n );\n }\n\n const branch = threadBranch(thread.id);\n // Attributed to the Slack sender, not to whoever connected the repository:\n // unlike an autonomous rule run, a Slack thread has a real interactive user.\n const existing = await sourceControl.sessions.getForBranch({\n projectRepositoryId: repo.projectRepositoryId,\n userId: link.userId,\n branch,\n });\n if (existing) return existing.sessionId;\n const session = await sourceControl.sessions.create({\n sessionId: globalThis.crypto.randomUUID(),\n projectRepositoryId: repo.projectRepositoryId,\n orgId,\n userId: link.userId,\n branch,\n baseBranch: repo.baseBranch,\n // DMs are the only private origin; channel threads are org-visible.\n visibility: thread.isDM ? 'private' : 'org',\n });\n return session.sessionId;\n } catch (error) {\n if (error instanceof ChannelSessionRejectedError || error instanceof SlackSessionStartError) throw error;\n if (error instanceof FactorySourceControlConflictError) {\n throw new SlackSessionStartError(\n 'Could not start a session: this Factory project has repositories from more than one source-control provider.',\n { cause: error },\n );\n }\n console.error('[slack] failed to start repo-backed session for thread', thread.id, error);\n throw new SlackSessionStartError('Could not start a session right now. Please try again later.', {\n cause: error,\n });\n }\n };\n}\n\n/**\n * Thread id for a NEW Slack channel thread. Repo-backed threads take the\n * user-session id AS their thread id, matching the web convention\n * (FactoryStartCoordinator seeds threads with threadId = sessionId) so\n * `/workspaces/{sessionId}/threads/{sessionId}` resolves Slack-created\n * sessions exactly like web-created ones — no `?resourceId=` override needed.\n * Chat-only threads keep the default random id: their `channel:...`\n * resourceId is a memory key, not a unique thread id.\n */\nexport const resolveChannelThreadId: ResolveThreadId = ({ resourceId, defaultThreadId }) =>\n resourceId.startsWith('channel:') ? defaultThreadId : resourceId;\n\n/** Create channel sessions with their Factory ownership present in initial controller state. */\nexport function createChannelSessionResolver(deps: SlackChannelDeps): ChannelSessionResolve {\n const sourceControlSessions = createSourceControlSessionLookup(configuredSourceControls(deps));\n return async ({ controller, thread, requestContext }) => {\n const sourceControl = thread.resourceId.startsWith('channel:')\n ? null\n : await sourceControlSessions.getSourceControlBySessionId(thread.resourceId);\n const owner = sourceControl\n ? await resolveFactoryProjectForSession({ sourceControl, sessionId: thread.resourceId })\n : null;\n return controller.createSession({\n id: thread.resourceId,\n ownerId: controller.id,\n resourceId: thread.resourceId,\n requestContext,\n ...(owner ? { tags: { factoryProjectId: owner.factoryProjectId } } : {}),\n });\n };\n}\n\n/**\n * Apply the factory's configuration to a Slack-created session the first time\n * its thread reaches the controller.\n *\n * Without this a Slack session runs on the SDK's built-in mode default\n * (`openai/gpt-5.5`), so a factory configured for any other provider fails every\n * message with a missing-credentials error. The web kickoff has always applied\n * the factory default; this brings Slack to the same footing.\n *\n * Only repo-backed threads are configured. Their resourceId IS the Factory\n * session id, which the source-control rows turn back into a project — a\n * chat-only `channel:...` id names no project, so there is nothing to read.\n *\n * The model this session starts on is picked here, once, in the order of who\n * chose it: the linked sender's active model pack, else the factory project's\n * default, else the SDK's built-in mode default. The choice is persisted on the\n * thread as `modeModelId_<mode>`, so it outlives the process that made it.\n *\n * Observational memory is configured here too, in the same order of who chose\n * it: the project's shared settings first, then the linked sender's own row,\n * which wins for every knob they have saved. The pair is re-applied on every\n * start rather than once — memory settings are stored preference, not a choice\n * made on this thread, and a restarted process re-resolves the project's row\n * before this hook runs.\n *\n * The model resolution is skipped on a session whose mode already has a model\n * persisted on the thread. That is the durable record of a deliberate choice —\n * either an earlier start or a user's own switch — and re-applying a preference\n * over it would undo the user's selection every time the process restarts.\n * Memory settings have no such per-thread record, so they are re-applied on\n * every start.\n */\nexport function createChannelSessionStartHook(deps: SlackChannelDeps): ChannelSessionStart {\n const { projects, memorySettings, modelPacks } = deps;\n const sourceControlSessions = createSourceControlSessionLookup(configuredSourceControls(deps));\n return async ({ session, thread, requestContext }) => {\n // Seed the tenant org above every guard below. `gateDispatch` stamps it on\n // the message's request context before the session exists, so this needs no\n // storage read — which matters, because the model lookups below deliberately\n // skip storage on a restarted session. A channel-only thread and a thread\n // whose dispatch was ungated both land here with no org and are marked\n // unresolved rather than being left to look like a local session.\n await seedSessionOrg(session, readRequestContextOrgId(requestContext));\n\n if (!projects) return;\n if (thread.resourceId.startsWith('channel:')) return;\n\n const sourceControl = await sourceControlSessions.getSourceControlBySessionId(thread.resourceId);\n if (!sourceControl) return;\n const owner = await resolveFactoryProjectForSession({ sourceControl, sessionId: thread.resourceId });\n if (!owner) return;\n\n // Repo-backed Slack sessions are factory sessions: stamp the owning\n // project onto controller state so downstream reads (org-first credential\n // resolution, authority gates) recognize them, same as board runs.\n await session.state.set({ factoryProjectId: owner.factoryProjectId });\n // Route the org through the seed helper rather than stamping it directly:\n // an ungated dispatch marked this session unresolved above, and owner\n // recovery is the resolution that has to clear that marker with it.\n await seedSessionOrg(session, owner.orgId);\n\n const modeModelKey = `modeModelId_${session.mode.get()}`;\n const persistedModelId = await session.thread.getSetting({ key: modeModelKey });\n if (typeof persistedModelId === 'string') {\n // A restarted session restores its generation model from the thread, but\n // still needs the project memory row and a provider-compatible fallback.\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n observationalMemoryModelId: persistedModelId,\n memorySettings,\n });\n // Subagent models live in session state only, so restore the ones this\n // thread pinned at its first start instead of re-resolving them.\n for (const agentType of SUBAGENT_TYPES) {\n const modelId = await session.thread.getSetting({ key: pinnedSubagentModelKey(agentType) });\n if (typeof modelId === 'string') await applySubagentModel(session, agentType, modelId);\n }\n } else {\n const factoryModelId = await resolveFactoryDefaultModelId(projects, owner.factoryProjectId);\n const userPackModels = await resolveActivePackModels(modelPacks, owner);\n const userModelId = userPackModels?.build || undefined;\n const selectedModelId = userModelId ?? factoryModelId;\n\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n defaultModelId: factoryModelId,\n // Slack runs with the linked sender's credentials. Derive OM's fallback\n // from that sender's selected model rather than the factory model, which\n // may belong to a provider the sender cannot access.\n observationalMemoryModelId: selectedModelId,\n memorySettings,\n });\n\n if (selectedModelId && selectedModelId !== factoryModelId) {\n // The sender's own choice beats the factory's. `switch` applies the model\n // and persists it as this mode's model on the thread in one step — which\n // is what makes the choice outlive this process.\n try {\n await session.model.switch({ modelId: selectedModelId });\n } catch (error) {\n console.warn(\"[slack] Failed to apply the sender's model pack model\", {\n modelId: selectedModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n const currentModelId = session.model.get();\n // The message continues on the factory/SDK model. Realign the OM\n // fallback with that model while preserving explicit project settings.\n await hydrateFactorySession(session, {\n orgId: owner.orgId,\n factoryProjectId: owner.factoryProjectId,\n observationalMemoryModelId: currentModelId,\n memorySettings,\n });\n if (currentModelId && !factoryModelId) {\n try {\n await session.model.saveForMode({ modeId: session.mode.get(), modelId: currentModelId });\n } catch (saveError) {\n console.warn(\"[slack] Failed to persist the sender's model pack model\", {\n modelId: currentModelId,\n error: saveError instanceof Error ? saveError.message : String(saveError),\n });\n }\n }\n }\n } else if (!selectedModelId) {\n // Neither preference exists, so the SDK's built-in mode default is this\n // thread's model of record. Pin it too: a later SDK upgrade that moves\n // that default must not silently retarget a thread that already started.\n const currentModelId = session.model.get();\n if (currentModelId) {\n await session.model.saveForMode({ modeId: session.mode.get(), modelId: currentModelId });\n }\n }\n\n // Subagents follow the sender's pack like the TUI does (explore→fast,\n // plan→plan, execute→build); roles the pack leaves empty use the factory\n // default. Pinned on the thread like the main model, so a restart\n // restores these rather than whatever pack or default exists by then.\n const packSubagentModels = {\n explore: userPackModels?.fast,\n plan: userPackModels?.plan,\n execute: userPackModels?.build,\n };\n for (const agentType of SUBAGENT_TYPES) {\n const modelId = packSubagentModels[agentType] || factoryModelId;\n if (!modelId) continue;\n if (await applySubagentModel(session, agentType, modelId)) {\n await session.thread.setSetting({ key: pinnedSubagentModelKey(agentType), value: modelId });\n }\n }\n }\n\n // The sender's own observational-memory settings, applied last so they beat\n // the project's — and on EVERY start, not just the first. Unlike the model,\n // this is stored preference rather than a choice made on this thread: a\n // restarted process re-resolves the project row before this hook runs, so\n // skipping it here would quietly put a thread back on the project's OM\n // configuration. Chat-only threads never reach this point.\n await applyPersonalMemorySettings(session, {\n memorySettings,\n orgId: owner.orgId,\n userId: owner.userId,\n });\n };\n}\n\nconst SUBAGENT_TYPES = ['explore', 'plan', 'execute'] as const;\nconst pinnedSubagentModelKey = (agentType: string) => `slackSubagentModelId_${agentType}`;\n\n/** Best-effort: a subagent model that can't be applied must not fail the message. */\nasync function applySubagentModel(\n session: Parameters<ChannelSessionStart>[0]['session'],\n agentType: (typeof SUBAGENT_TYPES)[number],\n modelId: string,\n): Promise<boolean> {\n try {\n await session.subagents.model.set({ modelId, agentType });\n return true;\n } catch (error) {\n console.warn('[slack] Failed to apply the subagent model', {\n agentType,\n modelId,\n error: error instanceof Error ? error.message : String(error),\n });\n return false;\n }\n}\n\n/**\n * The models of the sender's active model pack — the models that user chose\n * for themselves — or `undefined` when they have no pack.\n *\n * Best-effort by design: an uninitialized model-packs domain, a read failure, or\n * a pack saved without a build model all mean \"no personal preference\", which\n * falls through to the factory default rather than failing the dispatch.\n */\nasync function resolveActivePackModels(\n modelPacks: ModelPacksStorage | undefined,\n owner: { orgId: string; userId: string },\n): Promise<ActiveModelPackRecord['models'] | undefined> {\n if (!modelPacks) return undefined;\n try {\n const active = await modelPacks.getActive({ orgId: owner.orgId, userId: owner.userId });\n return active?.models;\n } catch (error) {\n console.warn('[slack] model pack lookup failed for a new session', {\n orgId: owner.orgId,\n userId: owner.userId,\n error: error instanceof Error ? error.message : String(error),\n });\n return undefined;\n }\n}\n\n/**\n * The internal Mastra thread the framework created for a channel conversation.\n * The handler's `thread.id` is the platform thread id (e.g. `slack:C123:ts`),\n * NOT the internal UUID — the mapping lives in the stored thread's channel\n * metadata.\n */\nasync function findInternalThread(mastra: Mastra | undefined, thread: HandlerThread) {\n const store = await mastra?.getStorage()?.getStore('memory');\n const { threads } = (await store?.listThreads({\n filter: {\n metadata: {\n channel_platform: thread.adapter.name,\n channel_externalThreadId: thread.id,\n channel_externalChannelId: thread.channelId,\n },\n },\n perPage: 1,\n })) ?? { threads: [] };\n return threads[0];\n}\n\nasync function prepareExistingSessionOwnerContext(\n thread: HandlerThread,\n deps: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n options: { expectedOrgId?: string; requireInternalThread: boolean },\n): Promise<'ready' | 'organization-mismatch'> {\n const sourceControls = configuredSourceControls(deps);\n if (sourceControls.length === 0 && !options.requireInternalThread) return 'ready';\n\n const internalThread = await findInternalThread(ctx.mastra, thread);\n if (!internalThread) {\n if (options.requireInternalThread) {\n throw new Error(`Could not resolve the internal Slack thread for ${thread.id}.`);\n }\n return 'ready';\n }\n if (sourceControls.length === 0 || internalThread.resourceId.startsWith('channel:')) return 'ready';\n if (!options.expectedOrgId) {\n throw new Error(`Could not authorize the owner of Slack Factory session ${internalThread.resourceId}.`);\n }\n\n const preparation = await prepareSessionRunContext(\n ctx.requestContext,\n internalThread.resourceId,\n { sessions: createSourceControlSessionLookup(sourceControls) },\n { expectedOrgId: options.expectedOrgId },\n );\n if (preparation === 'organization-mismatch') return preparation;\n if (preparation === 'unavailable') {\n throw new Error(`Could not authorize the owner of Slack Factory session ${internalThread.resourceId}.`);\n }\n return 'ready';\n}\n\n/**\n * Build the \"new session\" handler for mention / direct-message events. A mention or\n * DM on a not-yet-subscribed thread starts a NEW session; once subscribed, later\n * events are follow-ups and don't re-announce.\n */\n/**\n * Run the account-link + factory-routing gates for one inbound message.\n * Returns `null` when the run must not dispatch (a prompt card was posted\n * where possible); otherwise the dispatch context — with `routed` present\n * only when a linked sender resolved to a factory.\n */\nasync function gateDispatch(\n thread: HandlerThread,\n message: HandlerMessage,\n deps: SlackChannelDeps,\n ctx: ChannelHandlerContext,\n options: { requireInternalThread: boolean } = { requireInternalThread: false },\n): Promise<{\n routed?: { link: ChannelAccountLink; factoryProjectId: string; slackWorkItemsEnabled: boolean };\n} | null> {\n const { accountLinks, projects } = deps;\n const sender = await resolveLinkedSender({ thread, message, accountLinks });\n if (sender.status === 'blocked') return null;\n // Linked senders must also route to a Factory project before a run starts.\n if (sender.status === 'linked' && accountLinks) {\n // Stamp the tenant on the run's request context — the single seam\n // `resolveCredentialStore` reads to load this sender's model credentials.\n // This belongs to the link, not to the routing: a linked sender whose\n // factory routing comes back `ungated` still exits below and dispatches, so\n // stamping only in the routed branch would silently run them on default\n // credentials.\n ctx.requestContext.set('user', { id: sender.link.userId, organizationId: sender.link.orgId });\n const ownerContext = await prepareExistingSessionOwnerContext(thread, deps, ctx, {\n expectedOrgId: sender.link.orgId,\n requireInternalThread: options.requireInternalThread,\n });\n if (ownerContext === 'organization-mismatch') {\n await thread.post('This thread belongs to a Factory session in another organization.');\n return null;\n }\n\n const route = await resolveFactoryForLink({ thread, ...sender, accountLinks, projects });\n if (route.status === 'blocked') return null;\n if (route.status === 'resolved') {\n return {\n routed: {\n link: sender.link,\n factoryProjectId: route.factoryProjectId,\n slackWorkItemsEnabled: route.slackWorkItemsEnabled,\n },\n };\n }\n }\n return {};\n}\n\n/**\n * A channel id and a `ts` name a thread only inside the workspace that issued\n * them, so the team scopes the card's key — without it two workspaces could\n * hold one key and an aside would land on another tenant's card.\n */\nexport function slackThreadSource(thread: HandlerThread, teamId?: string): ExternalWorkItemSource {\n return {\n integrationId: thread.adapter.name,\n type: 'slack-thread',\n ...(teamId ? { workspaceId: teamId } : {}),\n externalId: thread.id,\n };\n}\n\n/** Cards written before the key carried a team are still keyed bare. */\nasync function findThreadWorkItem(\n workItems: WorkItemsStorage,\n thread: HandlerThread,\n teamId?: string,\n): Promise<WorkItemRow | null> {\n const scoped = await workItems.getBySource(slackThreadSource(thread, teamId));\n if (scoped || !teamId) return scoped;\n return workItems.getBySource(slackThreadSource(thread));\n}\n\n/**\n * Upsert the Work-board card for a dispatched Slack-thread run. Keyed on the\n * thread via `externalSource` — the work-items domain's unique\n * `(factory_project_id, source_key)` index makes repeat messages reuse the\n * same card, and `reuseMode: 'preserve'` keeps a card a human already dragged\n * across stages untouched. The card lands in Building (`execute`) for every\n * dispatched thread (DM or mention) — there is deliberately no per-origin\n * stage split; smart routing is a follow-up.\n *\n * The session id / branch / threadId and the workspace deep-link are resolved\n * by the caller (which already looked up the internal thread), so this helper\n * just shapes and writes. Best-effort: the run is already dispatched, so a\n * failure logs instead of throwing — work-item creation must never abort a Slack run.\n */\nexport async function upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link,\n factoryProjectId,\n session,\n url,\n}: {\n workItems: WorkItemsStorage;\n thread: HandlerThread;\n message: HandlerMessage;\n link: ChannelAccountLink;\n factoryProjectId: string;\n /**\n * The repo-backed Factory session to bind under the `chat` role, or\n * `undefined` for a chat-only thread (no Factory session to bind).\n */\n session?: { sessionId: string; branch: string; threadId: string };\n /** Workspace deep-link to the running session; omitted when no public URL. */\n url?: string;\n}): Promise<void> {\n try {\n const title = workItemTitleFrom(message.text);\n\n await workItems.upsert({\n orgId: link.orgId ?? '',\n userId: link.userId,\n factoryProjectId,\n reuseMode: 'preserve',\n input: {\n title: title || 'Slack thread',\n externalSource: { ...slackThreadSource(thread, slackTeamId(message)), ...(url ? { url } : {}) },\n stages: ['execute'],\n ...(session ? { sessions: { chat: session } } : {}),\n },\n });\n } catch (error) {\n console.warn('[slack] work-item creation failed for thread', thread.id, error);\n }\n}\n\nfunction preDispatchErrorDetails(error: unknown): { message: string; cause?: { message: string } } {\n const readMessage = (value: unknown) => {\n const message =\n typeof value === 'string'\n ? value\n : value && typeof value === 'object' && 'message' in value\n ? value.message\n : undefined;\n return typeof message === 'string' && message.length > 0 ? message : undefined;\n };\n try {\n const message = readMessage(error) ?? 'Unknown error';\n const cause = error && typeof error === 'object' && 'cause' in error ? readMessage(error.cause) : undefined;\n return { message, ...(cause ? { cause: { message: cause } } : {}) };\n } catch {\n return { message: 'Error details unavailable' };\n }\n}\n\nasync function reportPreDispatchError(\n thread: HandlerThread,\n message: HandlerMessage,\n ctx: ChannelHandlerContext,\n error: unknown,\n): Promise<void> {\n try {\n if (error instanceof ChannelSessionRejectedError) return;\n } catch {\n // A thrown proxy can fail even the refusal check; still report the failure.\n }\n const correlation = {\n platform: thread.adapter.name,\n threadId: thread.id,\n messageId: message.id,\n authorId: message.author.userId,\n };\n const logger = typeof ctx.mastra?.getLogger === 'function' ? ctx.mastra.getLogger() : undefined;\n const logError = (line: string) => (logger ? logger.error(line) : console.error(line));\n logError(`[slack] Pre-dispatch failure ${JSON.stringify({ ...correlation, error: preDispatchErrorDetails(error) })}`);\n try {\n // The message id is the lookup key for the diagnostic above.\n await thread.post(`Couldn’t start processing your message. Please try again.\\n\\`messageId: ${message.id}\\``);\n } catch (deliveryError) {\n logError(\n `[slack] Failed to deliver pre-dispatch error reply ${JSON.stringify({ ...correlation, error: preDispatchErrorDetails(deliveryError) })}`,\n );\n }\n}\n\nfunction createNewSessionChatHandler(deps: SlackChannelDeps): ChannelHandler {\n const { workItems } = deps;\n return async (thread, message, defaultHandler, ctx) => {\n // Gate on the sender having linked their Slack account to a Mastra tenant.\n // Unlinked → post the ephemeral Connect card and stop; no session/run is\n // created (which would otherwise be tenant-less and fail credential\n // resolution). This handler is the only gate — core dispatches whatever\n // reaches it — so every slot that can start a run must call it.\n let gate: Awaited<ReturnType<typeof gateDispatch>>;\n let isNewSession: boolean;\n try {\n gate = await gateDispatch(thread, message, deps, ctx);\n if (!gate) return;\n\n // A mention on a not-yet-subscribed thread is a NEW session. The\n // default handler auto-subscribes, so once subscribed this is a\n // follow-up mention — don't re-announce.\n isNewSession = !(await thread.isSubscribed());\n } catch (error) {\n await reportPreDispatchError(thread, message, ctx, error);\n return;\n }\n\n // Run the framework handler first so the internal Mastra thread and\n // controller session are created before we build the deep link.\n await defaultHandler(thread, message);\n\n if (!isNewSession) return;\n\n // The internal-thread lookup and deep-link are needed by BOTH the\n // announcement card AND work-item creation, so they run BEFORE the\n // card-only `MASTRACODE_PUBLIC_URL` gate — a deployment without a public\n // origin should still create board cards, just without a clickable link.\n const internalThread = await findInternalThread(ctx.mastra, thread);\n if (!internalThread) {\n console.warn('[onMention] no internal thread found for', thread.id);\n return;\n }\n\n // When the sender routed to a factory we know exactly which workspace the\n // session belongs to — deep-link straight into it. A repo-backed thread's\n // resourceId IS the Factory user-session id, so the link lands on the same\n // route a web-started run navigates to; chat-only threads keep the literal\n // `channel` segment (the real resource rides the `?resourceId=` override).\n // Unrouted senders fall back to the factory-agnostic /threads/ redirect.\n // One predicate drives both the path segment and the query param, so the\n // two can never disagree about what the URL already carries.\n const isChatOnly = internalThread.resourceId.startsWith('channel:');\n const workspaceSegment = isChatOnly ? 'channel' : encodeURIComponent(internalThread.resourceId);\n const threadPath = gate.routed\n ? `/factories/${encodeURIComponent(gate.routed.factoryProjectId)}/workspaces/${workspaceSegment}/threads/${encodeURIComponent(internalThread.id)}`\n : `/threads/${internalThread.id}`;\n\n // The param is an override for a URL that can't otherwise name its\n // resource. A routed repo-backed thread already spells the resourceId out\n // as its workspace segment, so appending it again is duplication the app\n // ignores. Chat-only threads need it (their segment is the literal string\n // `channel`), and so does the unrouted fallback, which has no workspace\n // segment at all — `ChannelThreadRedirect` forwards the search through.\n //\n // One shared deep-link: the card's button and the work-item `url` read the\n // SAME value so they can never drift. Undefined without a public origin —\n // the card is then skipped, but the work item is still created (url omitted).\n const needsResourceParam = isChatOnly || !gate.routed;\n const deepLink = process.env.MASTRACODE_PUBLIC_URL\n ? needsResourceParam\n ? `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}?resourceId=${encodeURIComponent(internalThread.resourceId)}`\n : `${process.env.MASTRACODE_PUBLIC_URL}${threadPath}`\n : undefined;\n\n // A dispatched, routed new-session thread becomes a Work-board card in\n // Building. Only routed senders (linked → factory) have the org/user/factory\n // a work item needs. Bind the repo-backed Factory session under the `chat`\n // role; a chat-only `channel:` resourceId is NOT a session id, so bind\n // nothing rather than a bad id. Best-effort (the helper swallows failures).\n if (workItems && gate.routed?.slackWorkItemsEnabled) {\n const session = isChatOnly\n ? undefined\n : { sessionId: internalThread.resourceId, branch: threadBranch(thread.id), threadId: internalThread.id };\n await upsertThreadWorkItem({\n workItems,\n thread,\n message,\n link: gate.routed.link,\n factoryProjectId: gate.routed.factoryProjectId,\n session,\n url: deepLink,\n });\n }\n\n // The announcement card is only useful with a public origin to deep-link\n // to — otherwise the link would be `undefined/threads/...`. Without one the\n // session (and now the work item) still exist; we just skip the broken card.\n if (!deepLink) return;\n\n await thread.post(\n Card({\n title: 'New session started',\n children: [Actions([LinkButton({ url: deepLink, label: 'View session' })])],\n }),\n );\n };\n}\n\n/**\n * An `aside` never reaches the agent, but it is still part of the conversation:\n * record it on the card the thread created. The link is resolved here rather\n * than through `gateDispatch`, which posts a Connect card an aside must not\n * trigger. Best-effort — nothing here may surface in the Slack thread.\n */\nasync function ingestAside(\n thread: HandlerThread,\n message: HandlerMessage,\n { feed, workItems, accountLinks }: SlackChannelDeps,\n): Promise<void> {\n if (!feed || !workItems) return;\n const body = resolveEmojiShortcodes(message.text.replace(/^aside\\b[,:]?\\s*/i, '').trim());\n if (!body) return;\n try {\n const teamId = slackTeamId(message);\n const workItem = await findThreadWorkItem(workItems, thread, teamId);\n if (!workItem) return;\n\n const external: FactoryActorExternalIdentity = {\n platform: thread.adapter.name,\n ...(teamId ? { teamId } : {}),\n messageId: message.id,\n userId: message.author.userId,\n userName: message.author.userName,\n fullName: message.author.fullName,\n isBot: message.author.isBot,\n };\n const link =\n accountLinks && teamId\n ? await accountLinks.getAccountLink({\n platform: external.platform,\n externalTeamId: teamId,\n externalUserId: external.userId,\n })\n : null;\n\n const result = await feed.createComment({\n orgId: workItem.orgId,\n workItemId: workItem.id,\n author: actorFromChannelAuthor(external, link),\n body,\n occurredAt: message.metadata.dateSent,\n externalSource: slackCommentSource(thread.id, message.id, teamId),\n });\n if (result.status !== 'created') {\n console.warn('[slack] aside not ingested', { thread: thread.id, messageTs: message.id, status: result.status });\n }\n } catch (error) {\n // Slack was acked before this handler ran, so a lost aside is never redelivered.\n console.warn('[slack] aside ingest failed', { thread: thread.id, messageTs: message.id, error });\n }\n}\n\nexport const createHandlers = (deps: SlackChannelDeps): ChannelHandlers => {\n const newSessionChatHandler = createNewSessionChatHandler(deps);\n\n return {\n onSubscribedMessage: async (thread, message, defaultHandler, ctx) => {\n // `aside` as its own leading word lets humans talk in a subscribed\n // thread without the bot replying. Word boundary so messages that\n // merely start with \"aside...\" (e.g. \"asides can wait\") still route.\n if (/^aside\\b/i.test(message.text)) return ingestAside(thread, message, deps);\n // A subscribed follow-up from an unlinked sender must not run either\n // (e.g. the link was removed mid-conversation), and it must still\n // resolve a factory (e.g. the default was cleared or its factory\n // deleted mid-conversation).\n try {\n const gate = await gateDispatch(thread, message, deps, ctx, { requireInternalThread: true });\n if (!gate) return;\n } catch (error) {\n await reportPreDispatchError(thread, message, ctx, error);\n return;\n }\n await defaultHandler(thread, message);\n },\n onMention: newSessionChatHandler,\n onDirectMessage: newSessionChatHandler,\n };\n};\n\n/** Slack app credentials, passed in explicitly rather than read from env here. */\ninterface SlackCredentials {\n clientId?: string;\n clientSecret?: string;\n signingSecret: string;\n botToken?: string;\n}\n\nexport function createSlackChannelsConfig(deps: SlackChannelDeps & { slack: SlackCredentials }): FactoryChannelsConfig {\n // Per HTTP attempt, not per call: the WebClient's own retry policy still\n // waits out a 429. Without it a dead socket hangs a post forever.\n const adapter = createSlackAdapter({ ...deps.slack, webClientOptions: { timeout: SLACK_REQUEST_TIMEOUT_MS } });\n const slack =\n deps.adapterOptions?.streaming === false\n ? { adapter, ...deps.adapterOptions }\n : { adapter, ...deps.adapterOptions, streaming: deps.adapterOptions?.streaming ?? true };\n\n return {\n adapters: { slack },\n handlers: createHandlers(deps),\n // New linked+repo-backed threads own a Factory user-session id as their\n // resourceId, which is what makes the controller session repo-backed.\n resolveResourceId: createChannelResourceIdResolver(deps),\n resolveThreadId: resolveChannelThreadId,\n resolveSession: createChannelSessionResolver(deps),\n // Those sessions are created by the channel machinery, not the web kickoff,\n // so this is where they pick up the factory's configuration.\n onSessionStart: createChannelSessionStartHook(deps),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Normalizes the first non-empty line of a review body so its `Verdict:` line can be read
3
+ * regardless of the markdown it is wrapped in (`**Verdict:** request changes`, `# Verdict: ...`).
4
+ */
5
+ export declare function normalizedVerdictLine(body: string | undefined): string | undefined;
6
+ //# sourceMappingURL=review-verdict.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../src/review-verdict.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAUlF"}
@@ -0,0 +1,14 @@
1
+ //#region src/review-verdict.ts
2
+ /**
3
+ * Normalizes the first non-empty line of a review body so its `Verdict:` line can be read
4
+ * regardless of the markdown it is wrapped in (`**Verdict:** request changes`, `# Verdict: ...`).
5
+ */
6
+ function normalizedVerdictLine(body) {
7
+ const firstLine = body?.split("\n").map((line) => line.trim()).find((line) => line.length > 0);
8
+ if (!firstLine) return void 0;
9
+ return firstLine.replaceAll(/[*_`#>\s]+/g, " ").trim().toLowerCase();
10
+ }
11
+ //#endregion
12
+ export { normalizedVerdictLine };
13
+
14
+ //# sourceMappingURL=review-verdict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-verdict.js","names":[],"sources":["../src/review-verdict.ts"],"sourcesContent":["/**\n * Normalizes the first non-empty line of a review body so its `Verdict:` line can be read\n * regardless of the markdown it is wrapped in (`**Verdict:** request changes`, `# Verdict: ...`).\n */\nexport function normalizedVerdictLine(body: string | undefined): string | undefined {\n const firstLine = body\n ?.split('\\n')\n .map(line => line.trim())\n .find(line => line.length > 0);\n if (!firstLine) return undefined;\n return firstLine\n .replaceAll(/[*_`#>\\s]+/g, ' ')\n .trim()\n .toLowerCase();\n}\n"],"mappings":";;;;;AAIA,SAAgB,sBAAsB,MAA8C;CAClF,MAAM,YAAY,MACd,MAAM,IAAI,CAAC,CACZ,KAAI,SAAQ,KAAK,KAAK,CAAC,CAAC,CACxB,MAAK,SAAQ,KAAK,SAAS,CAAC;CAC/B,IAAI,CAAC,WAAW,OAAO,KAAA;CACvB,OAAO,UACJ,WAAW,eAAe,GAAG,CAAC,CAC9B,KAAK,CAAC,CACN,YAAY;AACjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/routes/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAgBH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AAEjF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAIxF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA6MpD,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,kEAAkE;IAClE,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,wFAAwF;IACxF,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC7E;AAED;;;;;;;GAOG;AACH,qBAAa,WAAY,SAAQ,KAAK,CAAC,eAAe,CAAC;IACrD,MAAM,IAAI,QAAQ,EAAE;CAiQrB"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/routes/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAcH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AAEjF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAIxF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA6MpD,MAAM,WAAW,eAAgB,SAAQ,iBAAiB;IACxD,kEAAkE;IAClE,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,wFAAwF;IACxF,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC7E;AAED;;;;;;;GAOG;AACH,qBAAa,WAAY,SAAQ,KAAK,CAAC,eAAe,CAAC;IACrD,MAAM,IAAI,QAAQ,EAAE;CAqQrB"}
@@ -3,7 +3,6 @@ import { getAuthProviderId, resolveCredentialContext } from "./provider-credenti
3
3
  import { Route } from "./route.js";
4
4
  import { seedPersonalOmDefaults } from "./om-seed.js";
5
5
  import { registerApiRoute } from "@mastra/core/server";
6
- import { randomUUID } from "crypto";
7
6
  import { nextPollDelayMs } from "@mastra/code-sdk/auth/device-code";
8
7
  import { completeAnthropicLogin, startAnthropicLogin } from "@mastra/code-sdk/auth/providers/anthropic";
9
8
  import { copilotNextPollDelayMs, pollGitHubCopilotDeviceLogin, startGitHubCopilotDeviceLogin } from "@mastra/code-sdk/auth/providers/github-copilot";
@@ -257,7 +256,7 @@ var OAuthRoutes = class extends Route {
257
256
  } catch (error) {
258
257
  return c.json({ error: error instanceof Error ? error.message : String(error) }, 502);
259
258
  }
260
- const sessionId = randomUUID();
259
+ const sessionId = globalThis.crypto.randomUUID();
261
260
  const tenant = sessionTenant(ctx);
262
261
  await (await sessionStore(ctx)).createLoginSession({
263
262
  sessionId,
@@ -368,7 +367,8 @@ var OAuthRoutes = class extends Route {
368
367
  }, 403);
369
368
  const now = Date.now();
370
369
  const nextPollAt = session.nextPollAt?.getTime();
371
- if (nextPollAt != null && now < nextPollAt) return c.json({
370
+ const parkedByClaim = nextPollAt != null && nextPollAt >= session.expiresAt.getTime();
371
+ if (nextPollAt != null && now < nextPollAt && !parkedByClaim) return c.json({
372
372
  status: "pending",
373
373
  nextPollMs: nextPollAt - now
374
374
  });
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.js","names":[],"sources":["../../src/routes/oauth.ts"],"sourcesContent":["/**\n * Web OAuth sign-in routes for model providers (Settings › Providers).\n *\n * Wraps the SDK's step-based OAuth primitives (start/complete for Anthropic's\n * paste-code PKCE flow, start/poll for the Codex/Copilot/xAI device flows) in\n * HTTP routes. Flow state lives in login sessions — the `model-credentials`\n * domain's `oauth_login_sessions` table in tenant mode (any replica can\n * complete/poll), an in-memory store in local mode — so a flow can span\n * requests. Completed credentials are **user-scoped by default**; org admins\n * can start a flow with `{ scope: 'org' }` to share the credential with the\n * whole org. Local mode writes to the file-backed `AuthStorage`.\n *\n * Tokens never leave the server; responses only carry flow metadata (URLs,\n * user codes, poll delays).\n */\n\nimport { randomUUID } from 'node:crypto';\n\nimport { nextPollDelayMs } from '@mastra/code-sdk/auth/device-code';\nimport { completeAnthropicLogin, startAnthropicLogin } from '@mastra/code-sdk/auth/providers/anthropic';\nimport {\n copilotNextPollDelayMs,\n pollGitHubCopilotDeviceLogin,\n startGitHubCopilotDeviceLogin,\n} from '@mastra/code-sdk/auth/providers/github-copilot';\nimport type { CopilotDeviceLoginPending } from '@mastra/code-sdk/auth/providers/github-copilot';\nimport { pollCodexDeviceLogin, startCodexDeviceLogin } from '@mastra/code-sdk/auth/providers/openai-codex';\nimport type { CodexDeviceLoginPending } from '@mastra/code-sdk/auth/providers/openai-codex';\nimport { pollXAIDeviceLogin, startXAIDeviceLogin } from '@mastra/code-sdk/auth/providers/xai';\nimport type { XAIDeviceLoginPending } from '@mastra/code-sdk/auth/providers/xai';\nimport type { AuthStorage } from '@mastra/code-sdk/auth/storage';\nimport type { OAuthCredentials } from '@mastra/code-sdk/auth/types';\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { LoginCredentialScope, LoginSessionKind, LoginSessionRow } from '../storage/domains/credentials/base.js';\nimport type { MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport { seedPersonalOmDefaults } from './om-seed.js';\nimport { getAuthProviderId, resolveCredentialContext } from './provider-credentials.js';\nimport type { CredentialContext } from './provider-credentials.js';\nimport { Route } from './route.js';\nimport type { RouteDependencies } from './route.js';\n\n/** Lifetime of a paste-code session (Anthropic gives no explicit expiry). */\nconst PASTE_CODE_TTL_MS = 10 * 60 * 1000;\n\ninterface OAuthFlowStart {\n url: string;\n userCode?: string;\n instructions: string;\n /** ms epoch after which the flow expires. */\n expiresAt: number;\n /** Delay before the first upstream poll (device-code flows only). */\n nextPollMs?: number;\n /** Serializable flow state persisted in the login session. */\n pending: Record<string, unknown>;\n}\n\ntype OAuthFlowPoll =\n | { status: 'complete'; credentials: OAuthCredentials }\n | { status: 'pending'; nextPollMs: number; pending?: Record<string, unknown> }\n | { status: 'failed'; error: string };\n\ninterface OAuthFlow {\n kind: LoginSessionKind;\n start(): Promise<OAuthFlowStart>;\n /** Paste-code flows: exchange the pasted code for credentials. Throws on bad input. */\n complete?(pending: Record<string, unknown>, code: string): Promise<OAuthCredentials>;\n /** Device-code flows: perform exactly one upstream poll. */\n poll?(pending: Record<string, unknown>): Promise<OAuthFlowPoll>;\n}\n\n/** Web OAuth flows keyed by *catalog* provider id (mirrors {@link WEB_OAUTH_FLOW_KINDS}). */\nconst OAUTH_FLOWS: Record<string, OAuthFlow> = {\n anthropic: {\n kind: 'paste-code',\n start: async () => {\n const { url, verifier } = await startAnthropicLogin();\n return {\n url,\n instructions: 'Open the link, authorize, then paste the code shown on the final page.',\n expiresAt: Date.now() + PASTE_CODE_TTL_MS,\n pending: { verifier },\n };\n },\n complete: (pending, code) => completeAnthropicLogin(code, String(pending.verifier ?? '')),\n },\n openai: {\n kind: 'device-code',\n start: async () => {\n const p = await startCodexDeviceLogin();\n return {\n url: p.url,\n userCode: p.userCode,\n instructions: p.instructions,\n expiresAt: p.deadlineAt,\n nextPollMs: p.intervalMs,\n pending: { ...p },\n };\n },\n poll: async pending => {\n const r = await pollCodexDeviceLogin(pending as unknown as CodexDeviceLoginPending);\n if (r.status === 'complete') return { status: 'complete', credentials: r.credentials };\n if (r.status === 'pending') return { status: 'pending', nextPollMs: r.nextPollMs };\n return { status: 'failed', error: r.error };\n },\n },\n 'github-copilot': {\n kind: 'device-code',\n start: async () => {\n const p = await startGitHubCopilotDeviceLogin();\n return {\n url: p.url,\n userCode: p.userCode,\n instructions: p.instructions,\n expiresAt: p.deadlineAt,\n nextPollMs: copilotNextPollDelayMs(p),\n pending: { ...p },\n };\n },\n poll: async pending => {\n const p = pending as unknown as CopilotDeviceLoginPending;\n // Web flows are always started against github.com (no Enterprise input).\n // Never let deserialized session state redirect server-side polling to\n // an arbitrary hostname.\n if (p.domain !== 'github.com' || p.enterpriseDomain !== undefined) {\n return { status: 'failed', error: 'Unsupported GitHub host' };\n }\n const r = await pollGitHubCopilotDeviceLogin(p);\n if (r.status === 'complete') return { status: 'complete', credentials: r.credentials };\n if (r.status === 'pending') return { status: 'pending', nextPollMs: r.nextPollMs, pending: { ...r.pending } };\n return { status: 'failed', error: r.error };\n },\n },\n xai: {\n kind: 'device-code',\n start: async () => {\n const p = await startXAIDeviceLogin();\n return {\n url: p.url,\n userCode: p.userCode,\n instructions: p.instructions,\n expiresAt: p.state.deadlineAt,\n nextPollMs: nextPollDelayMs(p.state),\n pending: { ...p },\n };\n },\n poll: async pending => {\n const r = await pollXAIDeviceLogin(pending as unknown as XAIDeviceLoginPending);\n if (r.status === 'complete') return { status: 'complete', credentials: r.credentials };\n if (r.status === 'pending') return { status: 'pending', nextPollMs: r.nextPollMs, pending: { ...r.pending } };\n return { status: 'failed', error: r.error };\n },\n },\n};\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\n/**\n * Local-mode login sessions. Flows in local mode are single-process, so a\n * process-local libsql `:memory:` database (which already handles TTL\n * cleanup) is sufficient.\n */\nlet localSessionsPromise: Promise<ModelCredentialsStorage> | undefined;\nfunction localSessions(): Promise<ModelCredentialsStorage> {\n localSessionsPromise ??= (async () => {\n const { LibSQLFactoryStorage } = await import('@mastra/libsql');\n const storage = new LibSQLFactoryStorage({ id: 'local-oauth-sessions', url: ':memory:' });\n const store = storage.registerDomain(new ModelCredentialsStorage());\n await storage.init();\n return store;\n })();\n return localSessionsPromise;\n}\nconst LOCAL_TENANT = { orgId: 'local', userId: 'local' } as const;\n\nasync function sessionStore(ctx: CredentialContext): Promise<ModelCredentialsStorage> {\n return ctx.mode === 'tenant' ? ctx.storage : localSessions();\n}\n\nfunction sessionTenant(ctx: CredentialContext): { orgId: string; userId: string } {\n return ctx.mode === 'tenant' ? { orgId: ctx.orgId, userId: ctx.userId } : LOCAL_TENANT;\n}\n\n/** Load a session and verify it belongs to the caller + provider (else undefined). */\nasync function loadOwnedSession({\n ctx,\n provider,\n sessionId,\n}: {\n ctx: CredentialContext;\n provider: string;\n sessionId: string;\n}): Promise<LoginSessionRow | undefined> {\n const session = await (await sessionStore(ctx)).getLoginSession(sessionId);\n if (!session) return undefined;\n const tenant = sessionTenant(ctx);\n if (session.orgId !== tenant.orgId || session.userId !== tenant.userId) return undefined;\n if (session.provider !== provider) return undefined;\n return session;\n}\n\n/** Persist completed OAuth credentials — user-scoped unless the flow was started org-wide. */\nasync function persistOAuthCredential({\n ctx,\n provider,\n scope,\n credentials,\n authStorage,\n memorySettings,\n onCredentialsChanged,\n}: {\n ctx: CredentialContext;\n provider: string;\n scope: LoginCredentialScope | undefined;\n credentials: OAuthCredentials;\n authStorage: AuthStorage | undefined;\n memorySettings: MemorySettingsStorage | undefined;\n onCredentialsChanged: (tenant: { orgId: string; userId?: string }) => void;\n}): Promise<void> {\n const authProviderId = getAuthProviderId(provider);\n if (ctx.mode === 'tenant') {\n const tenant = { orgId: ctx.orgId, ...(scope === 'org' ? {} : { userId: ctx.userId }) };\n await ctx.storage.setCredential(tenant, authProviderId, {\n type: 'oauth',\n ...credentials,\n });\n onCredentialsChanged(tenant);\n await seedPersonalOmDefaults({ memorySettings, tenant, provider });\n return;\n }\n if (!authStorage) throw new Error('Credential storage is not available');\n await authStorage.addAccount(authProviderId, credentials);\n}\n\nasync function readJsonBody(c: Context): Promise<Record<string, unknown>> {\n try {\n const body = (await c.req.json()) as unknown;\n return body && typeof body === 'object' ? (body as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n}\n\nexport interface OAuthRoutesDeps extends RouteDependencies {\n /** File-backed credential store; used in local (no-auth) mode. */\n authStorage?: AuthStorage;\n /** Tenant credential domain handle; absent in local (no-DB) mode. */\n modelCredentials?: ModelCredentialsStorage;\n /** Personal OM settings; seeded from the login provider after a user-scoped sign-in. */\n memorySettings?: MemorySettingsStorage;\n /** Notifies the host after tenant credentials change so caches can be dropped. */\n onCredentialsChanged?: (tenant: { orgId: string; userId?: string }) => void;\n}\n\n/**\n * Provider OAuth sign-in routes as Mastra `apiRoutes`:\n * - `POST /web/config/providers/:provider/oauth/start` — begin a sign-in flow\n * - `POST /web/config/providers/:provider/oauth/complete` — paste-code exchange\n * - `POST /web/config/providers/:provider/oauth/poll` — one device-code poll\n * - `DELETE /web/config/providers/:provider/oauth/session/:sessionId` — cancel a flow\n * - `DELETE /web/config/providers/:provider/oauth` — sign out (caller only)\n */\nexport class OAuthRoutes extends Route<OAuthRoutesDeps> {\n routes(): ApiRoute[] {\n const { auth, authStorage, modelCredentials, memorySettings } = this.deps;\n const onCredentialsChanged = this.deps.onCredentialsChanged ?? (() => {});\n\n return [\n registerApiRoute('/web/config/providers/:provider/oauth/start', {\n method: 'POST',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const flow = OAUTH_FLOWS[provider];\n if (!flow) {\n return c.json({ error: 'oauth_not_supported', message: `Provider does not support web sign-in` }, 404);\n }\n // Body may carry `{ mode }` for future multi-mode providers; each web\n // flow currently has exactly one mode, so an unknown mode is rejected.\n const body = await readJsonBody(loose(c));\n if (typeof body.mode === 'string' && body.mode !== flow.kind) {\n return c.json({ error: 'invalid_mode', message: `Unsupported mode for ${provider}: ${body.mode}` }, 400);\n }\n\n // Optional `{ scope: 'org' }` shares the completed credential with\n // the whole org (admin-only, tenant mode). Anything else is personal.\n const scope: LoginCredentialScope = body.scope === 'org' ? 'org' : 'user';\n if (scope === 'org' && ctx.mode === 'tenant' && !(await auth.isOrganizationAdmin(loose(c), ctx.orgId))) {\n return c.json({ error: 'forbidden', message: 'Only org admins can sign in for the whole org' }, 403);\n }\n\n let started: OAuthFlowStart;\n try {\n started = await flow.start();\n } catch (error) {\n return c.json({ error: error instanceof Error ? error.message : String(error) }, 502);\n }\n\n const sessionId = randomUUID();\n const tenant = sessionTenant(ctx);\n await (\n await sessionStore(ctx)\n ).createLoginSession({\n sessionId,\n orgId: tenant.orgId,\n userId: tenant.userId,\n provider,\n kind: flow.kind,\n ...(scope === 'org' && ctx.mode === 'tenant' ? { credentialScope: scope } : {}),\n pending: started.pending,\n expiresAt: new Date(started.expiresAt),\n nextPollAt: started.nextPollMs != null ? new Date(Date.now() + started.nextPollMs) : null,\n });\n\n return c.json({\n sessionId,\n kind: flow.kind,\n url: started.url,\n ...(started.userCode ? { userCode: started.userCode } : {}),\n instructions: started.instructions,\n expiresAt: started.expiresAt,\n ...(started.nextPollMs != null ? { nextPollMs: started.nextPollMs } : {}),\n });\n },\n }),\n\n registerApiRoute('/web/config/providers/:provider/oauth/complete', {\n method: 'POST',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const flow = OAUTH_FLOWS[provider];\n if (!flow?.complete) return c.json({ error: 'oauth_not_supported' }, 404);\n\n const body = await readJsonBody(loose(c));\n const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';\n const code = typeof body.code === 'string' ? body.code.trim() : '';\n if (!sessionId || !code) return c.json({ error: 'Missing required fields: sessionId, code' }, 400);\n\n const session = await loadOwnedSession({ ctx, provider, sessionId });\n if (!session) return c.json({ error: 'session_not_found' }, 404);\n if (session.kind !== 'paste-code') return c.json({ error: 'wrong_session_kind' }, 400);\n // Org-scoped flows recheck admin access at completion time — the\n // caller may have lost admin since the flow was started.\n if (\n session.credentialScope === 'org' &&\n ctx.mode === 'tenant' &&\n !(await auth.isOrganizationAdmin(loose(c), ctx.orgId))\n ) {\n return c.json({ error: 'forbidden', message: 'Only org admins can sign in for the whole org' }, 403);\n }\n\n const claimed = await (\n await sessionStore(ctx)\n ).claimLoginSession(sessionId, {\n orgId: session.orgId,\n userId: session.userId,\n provider,\n kind: 'paste-code',\n });\n if (!claimed) return c.json({ error: 'oauth_in_progress' }, 409);\n\n let credentials: OAuthCredentials;\n try {\n credentials = await flow.complete(claimed.pending, code);\n } catch (error) {\n await (await sessionStore(ctx)).touchLoginSession(sessionId, { nextPollAt: null });\n return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);\n }\n\n await persistOAuthCredential({\n ctx,\n provider,\n scope: session.credentialScope,\n credentials,\n authStorage,\n memorySettings,\n onCredentialsChanged,\n });\n await (await sessionStore(ctx)).deleteLoginSession(sessionId);\n return c.json({ status: 'complete', ok: true });\n },\n }),\n\n registerApiRoute('/web/config/providers/:provider/oauth/poll', {\n method: 'POST',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const flow = OAUTH_FLOWS[provider];\n if (!flow?.poll) return c.json({ error: 'oauth_not_supported' }, 404);\n\n const body = await readJsonBody(loose(c));\n const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';\n if (!sessionId) return c.json({ error: 'Missing required field: sessionId' }, 400);\n\n const session = await loadOwnedSession({ ctx, provider, sessionId });\n if (!session) return c.json({ error: 'session_not_found' }, 404);\n if (session.kind !== 'device-code') return c.json({ error: 'wrong_session_kind' }, 400);\n // Org-scoped flows recheck admin access before the credential write —\n // the caller may have lost admin since the flow was started.\n if (\n session.credentialScope === 'org' &&\n ctx.mode === 'tenant' &&\n !(await auth.isOrganizationAdmin(loose(c), ctx.orgId))\n ) {\n return c.json({ error: 'forbidden', message: 'Only org admins can sign in for the whole org' }, 403);\n }\n\n // Server-side rate limit: at most one upstream poll per interval,\n // regardless of how eagerly the client calls this route.\n const now = Date.now();\n const nextPollAt = session.nextPollAt?.getTime();\n if (nextPollAt != null && now < nextPollAt) {\n return c.json({ status: 'pending', nextPollMs: nextPollAt - now });\n }\n\n const claimed = await (\n await sessionStore(ctx)\n ).claimLoginSession(sessionId, {\n orgId: session.orgId,\n userId: session.userId,\n provider,\n kind: 'device-code',\n });\n if (!claimed) return c.json({ status: 'pending', nextPollMs: 250 });\n\n let result: OAuthFlowPoll;\n try {\n result = await flow.poll(claimed.pending);\n } catch (error) {\n await (await sessionStore(ctx)).touchLoginSession(sessionId, { nextPollAt: null });\n throw error;\n }\n\n if (result.status === 'complete') {\n await persistOAuthCredential({\n ctx,\n provider,\n scope: session.credentialScope,\n credentials: result.credentials,\n authStorage,\n memorySettings,\n onCredentialsChanged,\n });\n await (await sessionStore(ctx)).deleteLoginSession(sessionId);\n return c.json({ status: 'complete', ok: true });\n }\n\n if (result.status === 'failed') {\n await (await sessionStore(ctx)).deleteLoginSession(sessionId);\n return c.json({ status: 'failed', error: result.error });\n }\n\n await (\n await sessionStore(ctx)\n ).touchLoginSession(sessionId, {\n ...(result.pending ? { pending: result.pending } : {}),\n nextPollAt: new Date(now + result.nextPollMs),\n });\n return c.json({ status: 'pending', nextPollMs: result.nextPollMs });\n },\n }),\n\n registerApiRoute('/web/config/providers/:provider/oauth/session/:sessionId', {\n method: 'DELETE',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const sessionId = c.req.param('sessionId');\n const session = await loadOwnedSession({ ctx, provider, sessionId });\n if (session) await (await sessionStore(ctx)).deleteLoginSession(sessionId);\n return c.json({ ok: true });\n },\n }),\n\n registerApiRoute('/web/config/providers/:provider/oauth', {\n method: 'DELETE',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const authProviderId = getAuthProviderId(provider);\n // `?scope=org` removes the shared org credential (admin-only);\n // default removes the caller's personal credential only.\n const scope = c.req.query('scope') === 'org' ? 'org' : 'user';\n try {\n if (ctx.mode === 'tenant') {\n if (scope === 'org' && !(await auth.isOrganizationAdmin(loose(c), ctx.orgId))) {\n return c.json({ error: 'forbidden', message: 'Only org admins can remove an org-wide sign-in' }, 403);\n }\n const tenant = { orgId: ctx.orgId, ...(scope === 'org' ? {} : { userId: ctx.userId }) };\n await ctx.storage.removeCredential(tenant, authProviderId);\n onCredentialsChanged(tenant);\n } else {\n if (!authStorage) return c.json({ error: 'Credential storage is not available' }, 503);\n authStorage.logout(authProviderId);\n }\n return c.json({ ok: true });\n } catch (error) {\n return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);\n }\n },\n }),\n ];\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"oauth.js","names":[],"sources":["../../src/routes/oauth.ts"],"sourcesContent":["/**\n * Web OAuth sign-in routes for model providers (Settings › Providers).\n *\n * Wraps the SDK's step-based OAuth primitives (start/complete for Anthropic's\n * paste-code PKCE flow, start/poll for the Codex/Copilot/xAI device flows) in\n * HTTP routes. Flow state lives in login sessions — the `model-credentials`\n * domain's `oauth_login_sessions` table in tenant mode (any replica can\n * complete/poll), an in-memory store in local mode — so a flow can span\n * requests. Completed credentials are **user-scoped by default**; org admins\n * can start a flow with `{ scope: 'org' }` to share the credential with the\n * whole org. Local mode writes to the file-backed `AuthStorage`.\n *\n * Tokens never leave the server; responses only carry flow metadata (URLs,\n * user codes, poll delays).\n */\n\nimport { nextPollDelayMs } from '@mastra/code-sdk/auth/device-code';\nimport { completeAnthropicLogin, startAnthropicLogin } from '@mastra/code-sdk/auth/providers/anthropic';\nimport {\n copilotNextPollDelayMs,\n pollGitHubCopilotDeviceLogin,\n startGitHubCopilotDeviceLogin,\n} from '@mastra/code-sdk/auth/providers/github-copilot';\nimport type { CopilotDeviceLoginPending } from '@mastra/code-sdk/auth/providers/github-copilot';\nimport { pollCodexDeviceLogin, startCodexDeviceLogin } from '@mastra/code-sdk/auth/providers/openai-codex';\nimport type { CodexDeviceLoginPending } from '@mastra/code-sdk/auth/providers/openai-codex';\nimport { pollXAIDeviceLogin, startXAIDeviceLogin } from '@mastra/code-sdk/auth/providers/xai';\nimport type { XAIDeviceLoginPending } from '@mastra/code-sdk/auth/providers/xai';\nimport type { AuthStorage } from '@mastra/code-sdk/auth/storage';\nimport type { OAuthCredentials } from '@mastra/code-sdk/auth/types';\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { LoginCredentialScope, LoginSessionKind, LoginSessionRow } from '../storage/domains/credentials/base.js';\nimport type { MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport { seedPersonalOmDefaults } from './om-seed.js';\nimport { getAuthProviderId, resolveCredentialContext } from './provider-credentials.js';\nimport type { CredentialContext } from './provider-credentials.js';\nimport { Route } from './route.js';\nimport type { RouteDependencies } from './route.js';\n\n/** Lifetime of a paste-code session (Anthropic gives no explicit expiry). */\nconst PASTE_CODE_TTL_MS = 10 * 60 * 1000;\n\ninterface OAuthFlowStart {\n url: string;\n userCode?: string;\n instructions: string;\n /** ms epoch after which the flow expires. */\n expiresAt: number;\n /** Delay before the first upstream poll (device-code flows only). */\n nextPollMs?: number;\n /** Serializable flow state persisted in the login session. */\n pending: Record<string, unknown>;\n}\n\ntype OAuthFlowPoll =\n | { status: 'complete'; credentials: OAuthCredentials }\n | { status: 'pending'; nextPollMs: number; pending?: Record<string, unknown> }\n | { status: 'failed'; error: string };\n\ninterface OAuthFlow {\n kind: LoginSessionKind;\n start(): Promise<OAuthFlowStart>;\n /** Paste-code flows: exchange the pasted code for credentials. Throws on bad input. */\n complete?(pending: Record<string, unknown>, code: string): Promise<OAuthCredentials>;\n /** Device-code flows: perform exactly one upstream poll. */\n poll?(pending: Record<string, unknown>): Promise<OAuthFlowPoll>;\n}\n\n/** Web OAuth flows keyed by *catalog* provider id (mirrors {@link WEB_OAUTH_FLOW_KINDS}). */\nconst OAUTH_FLOWS: Record<string, OAuthFlow> = {\n anthropic: {\n kind: 'paste-code',\n start: async () => {\n const { url, verifier } = await startAnthropicLogin();\n return {\n url,\n instructions: 'Open the link, authorize, then paste the code shown on the final page.',\n expiresAt: Date.now() + PASTE_CODE_TTL_MS,\n pending: { verifier },\n };\n },\n complete: (pending, code) => completeAnthropicLogin(code, String(pending.verifier ?? '')),\n },\n openai: {\n kind: 'device-code',\n start: async () => {\n const p = await startCodexDeviceLogin();\n return {\n url: p.url,\n userCode: p.userCode,\n instructions: p.instructions,\n expiresAt: p.deadlineAt,\n nextPollMs: p.intervalMs,\n pending: { ...p },\n };\n },\n poll: async pending => {\n const r = await pollCodexDeviceLogin(pending as unknown as CodexDeviceLoginPending);\n if (r.status === 'complete') return { status: 'complete', credentials: r.credentials };\n if (r.status === 'pending') return { status: 'pending', nextPollMs: r.nextPollMs };\n return { status: 'failed', error: r.error };\n },\n },\n 'github-copilot': {\n kind: 'device-code',\n start: async () => {\n const p = await startGitHubCopilotDeviceLogin();\n return {\n url: p.url,\n userCode: p.userCode,\n instructions: p.instructions,\n expiresAt: p.deadlineAt,\n nextPollMs: copilotNextPollDelayMs(p),\n pending: { ...p },\n };\n },\n poll: async pending => {\n const p = pending as unknown as CopilotDeviceLoginPending;\n // Web flows are always started against github.com (no Enterprise input).\n // Never let deserialized session state redirect server-side polling to\n // an arbitrary hostname.\n if (p.domain !== 'github.com' || p.enterpriseDomain !== undefined) {\n return { status: 'failed', error: 'Unsupported GitHub host' };\n }\n const r = await pollGitHubCopilotDeviceLogin(p);\n if (r.status === 'complete') return { status: 'complete', credentials: r.credentials };\n if (r.status === 'pending') return { status: 'pending', nextPollMs: r.nextPollMs, pending: { ...r.pending } };\n return { status: 'failed', error: r.error };\n },\n },\n xai: {\n kind: 'device-code',\n start: async () => {\n const p = await startXAIDeviceLogin();\n return {\n url: p.url,\n userCode: p.userCode,\n instructions: p.instructions,\n expiresAt: p.state.deadlineAt,\n nextPollMs: nextPollDelayMs(p.state),\n pending: { ...p },\n };\n },\n poll: async pending => {\n const r = await pollXAIDeviceLogin(pending as unknown as XAIDeviceLoginPending);\n if (r.status === 'complete') return { status: 'complete', credentials: r.credentials };\n if (r.status === 'pending') return { status: 'pending', nextPollMs: r.nextPollMs, pending: { ...r.pending } };\n return { status: 'failed', error: r.error };\n },\n },\n};\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\n/**\n * Local-mode login sessions. Flows in local mode are single-process, so a\n * process-local libsql `:memory:` database (which already handles TTL\n * cleanup) is sufficient.\n */\nlet localSessionsPromise: Promise<ModelCredentialsStorage> | undefined;\nfunction localSessions(): Promise<ModelCredentialsStorage> {\n localSessionsPromise ??= (async () => {\n const { LibSQLFactoryStorage } = await import('@mastra/libsql');\n const storage = new LibSQLFactoryStorage({ id: 'local-oauth-sessions', url: ':memory:' });\n const store = storage.registerDomain(new ModelCredentialsStorage());\n await storage.init();\n return store;\n })();\n return localSessionsPromise;\n}\nconst LOCAL_TENANT = { orgId: 'local', userId: 'local' } as const;\n\nasync function sessionStore(ctx: CredentialContext): Promise<ModelCredentialsStorage> {\n return ctx.mode === 'tenant' ? ctx.storage : localSessions();\n}\n\nfunction sessionTenant(ctx: CredentialContext): { orgId: string; userId: string } {\n return ctx.mode === 'tenant' ? { orgId: ctx.orgId, userId: ctx.userId } : LOCAL_TENANT;\n}\n\n/** Load a session and verify it belongs to the caller + provider (else undefined). */\nasync function loadOwnedSession({\n ctx,\n provider,\n sessionId,\n}: {\n ctx: CredentialContext;\n provider: string;\n sessionId: string;\n}): Promise<LoginSessionRow | undefined> {\n const session = await (await sessionStore(ctx)).getLoginSession(sessionId);\n if (!session) return undefined;\n const tenant = sessionTenant(ctx);\n if (session.orgId !== tenant.orgId || session.userId !== tenant.userId) return undefined;\n if (session.provider !== provider) return undefined;\n return session;\n}\n\n/** Persist completed OAuth credentials — user-scoped unless the flow was started org-wide. */\nasync function persistOAuthCredential({\n ctx,\n provider,\n scope,\n credentials,\n authStorage,\n memorySettings,\n onCredentialsChanged,\n}: {\n ctx: CredentialContext;\n provider: string;\n scope: LoginCredentialScope | undefined;\n credentials: OAuthCredentials;\n authStorage: AuthStorage | undefined;\n memorySettings: MemorySettingsStorage | undefined;\n onCredentialsChanged: (tenant: { orgId: string; userId?: string }) => void;\n}): Promise<void> {\n const authProviderId = getAuthProviderId(provider);\n if (ctx.mode === 'tenant') {\n const tenant = { orgId: ctx.orgId, ...(scope === 'org' ? {} : { userId: ctx.userId }) };\n await ctx.storage.setCredential(tenant, authProviderId, {\n type: 'oauth',\n ...credentials,\n });\n onCredentialsChanged(tenant);\n await seedPersonalOmDefaults({ memorySettings, tenant, provider });\n return;\n }\n if (!authStorage) throw new Error('Credential storage is not available');\n await authStorage.addAccount(authProviderId, credentials);\n}\n\nasync function readJsonBody(c: Context): Promise<Record<string, unknown>> {\n try {\n const body = (await c.req.json()) as unknown;\n return body && typeof body === 'object' ? (body as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n}\n\nexport interface OAuthRoutesDeps extends RouteDependencies {\n /** File-backed credential store; used in local (no-auth) mode. */\n authStorage?: AuthStorage;\n /** Tenant credential domain handle; absent in local (no-DB) mode. */\n modelCredentials?: ModelCredentialsStorage;\n /** Personal OM settings; seeded from the login provider after a user-scoped sign-in. */\n memorySettings?: MemorySettingsStorage;\n /** Notifies the host after tenant credentials change so caches can be dropped. */\n onCredentialsChanged?: (tenant: { orgId: string; userId?: string }) => void;\n}\n\n/**\n * Provider OAuth sign-in routes as Mastra `apiRoutes`:\n * - `POST /web/config/providers/:provider/oauth/start` — begin a sign-in flow\n * - `POST /web/config/providers/:provider/oauth/complete` — paste-code exchange\n * - `POST /web/config/providers/:provider/oauth/poll` — one device-code poll\n * - `DELETE /web/config/providers/:provider/oauth/session/:sessionId` — cancel a flow\n * - `DELETE /web/config/providers/:provider/oauth` — sign out (caller only)\n */\nexport class OAuthRoutes extends Route<OAuthRoutesDeps> {\n routes(): ApiRoute[] {\n const { auth, authStorage, modelCredentials, memorySettings } = this.deps;\n const onCredentialsChanged = this.deps.onCredentialsChanged ?? (() => {});\n\n return [\n registerApiRoute('/web/config/providers/:provider/oauth/start', {\n method: 'POST',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const flow = OAUTH_FLOWS[provider];\n if (!flow) {\n return c.json({ error: 'oauth_not_supported', message: `Provider does not support web sign-in` }, 404);\n }\n // Body may carry `{ mode }` for future multi-mode providers; each web\n // flow currently has exactly one mode, so an unknown mode is rejected.\n const body = await readJsonBody(loose(c));\n if (typeof body.mode === 'string' && body.mode !== flow.kind) {\n return c.json({ error: 'invalid_mode', message: `Unsupported mode for ${provider}: ${body.mode}` }, 400);\n }\n\n // Optional `{ scope: 'org' }` shares the completed credential with\n // the whole org (admin-only, tenant mode). Anything else is personal.\n const scope: LoginCredentialScope = body.scope === 'org' ? 'org' : 'user';\n if (scope === 'org' && ctx.mode === 'tenant' && !(await auth.isOrganizationAdmin(loose(c), ctx.orgId))) {\n return c.json({ error: 'forbidden', message: 'Only org admins can sign in for the whole org' }, 403);\n }\n\n let started: OAuthFlowStart;\n try {\n started = await flow.start();\n } catch (error) {\n return c.json({ error: error instanceof Error ? error.message : String(error) }, 502);\n }\n\n const sessionId = globalThis.crypto.randomUUID();\n const tenant = sessionTenant(ctx);\n await (\n await sessionStore(ctx)\n ).createLoginSession({\n sessionId,\n orgId: tenant.orgId,\n userId: tenant.userId,\n provider,\n kind: flow.kind,\n ...(scope === 'org' && ctx.mode === 'tenant' ? { credentialScope: scope } : {}),\n pending: started.pending,\n expiresAt: new Date(started.expiresAt),\n nextPollAt: started.nextPollMs != null ? new Date(Date.now() + started.nextPollMs) : null,\n });\n\n return c.json({\n sessionId,\n kind: flow.kind,\n url: started.url,\n ...(started.userCode ? { userCode: started.userCode } : {}),\n instructions: started.instructions,\n expiresAt: started.expiresAt,\n ...(started.nextPollMs != null ? { nextPollMs: started.nextPollMs } : {}),\n });\n },\n }),\n\n registerApiRoute('/web/config/providers/:provider/oauth/complete', {\n method: 'POST',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const flow = OAUTH_FLOWS[provider];\n if (!flow?.complete) return c.json({ error: 'oauth_not_supported' }, 404);\n\n const body = await readJsonBody(loose(c));\n const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';\n const code = typeof body.code === 'string' ? body.code.trim() : '';\n if (!sessionId || !code) return c.json({ error: 'Missing required fields: sessionId, code' }, 400);\n\n const session = await loadOwnedSession({ ctx, provider, sessionId });\n if (!session) return c.json({ error: 'session_not_found' }, 404);\n if (session.kind !== 'paste-code') return c.json({ error: 'wrong_session_kind' }, 400);\n // Org-scoped flows recheck admin access at completion time — the\n // caller may have lost admin since the flow was started.\n if (\n session.credentialScope === 'org' &&\n ctx.mode === 'tenant' &&\n !(await auth.isOrganizationAdmin(loose(c), ctx.orgId))\n ) {\n return c.json({ error: 'forbidden', message: 'Only org admins can sign in for the whole org' }, 403);\n }\n\n const claimed = await (\n await sessionStore(ctx)\n ).claimLoginSession(sessionId, {\n orgId: session.orgId,\n userId: session.userId,\n provider,\n kind: 'paste-code',\n });\n if (!claimed) return c.json({ error: 'oauth_in_progress' }, 409);\n\n let credentials: OAuthCredentials;\n try {\n credentials = await flow.complete(claimed.pending, code);\n } catch (error) {\n await (await sessionStore(ctx)).touchLoginSession(sessionId, { nextPollAt: null });\n return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);\n }\n\n await persistOAuthCredential({\n ctx,\n provider,\n scope: session.credentialScope,\n credentials,\n authStorage,\n memorySettings,\n onCredentialsChanged,\n });\n await (await sessionStore(ctx)).deleteLoginSession(sessionId);\n return c.json({ status: 'complete', ok: true });\n },\n }),\n\n registerApiRoute('/web/config/providers/:provider/oauth/poll', {\n method: 'POST',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const flow = OAUTH_FLOWS[provider];\n if (!flow?.poll) return c.json({ error: 'oauth_not_supported' }, 404);\n\n const body = await readJsonBody(loose(c));\n const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';\n if (!sessionId) return c.json({ error: 'Missing required field: sessionId' }, 400);\n\n const session = await loadOwnedSession({ ctx, provider, sessionId });\n if (!session) return c.json({ error: 'session_not_found' }, 404);\n if (session.kind !== 'device-code') return c.json({ error: 'wrong_session_kind' }, 400);\n // Org-scoped flows recheck admin access before the credential write —\n // the caller may have lost admin since the flow was started.\n if (\n session.credentialScope === 'org' &&\n ctx.mode === 'tenant' &&\n !(await auth.isOrganizationAdmin(loose(c), ctx.orgId))\n ) {\n return c.json({ error: 'forbidden', message: 'Only org admins can sign in for the whole org' }, 403);\n }\n\n // Server-side rate limit: at most one upstream poll per interval,\n // regardless of how eagerly the client calls this route.\n const now = Date.now();\n const nextPollAt = session.nextPollAt?.getTime();\n // claimLoginSession parks next_poll_at at expires_at while another\n // request owns the flow. That is a lock, not a schedule, so fall\n // through to the claim and take its short retry instead.\n const parkedByClaim = nextPollAt != null && nextPollAt >= session.expiresAt.getTime();\n if (nextPollAt != null && now < nextPollAt && !parkedByClaim) {\n return c.json({ status: 'pending', nextPollMs: nextPollAt - now });\n }\n\n const claimed = await (\n await sessionStore(ctx)\n ).claimLoginSession(sessionId, {\n orgId: session.orgId,\n userId: session.userId,\n provider,\n kind: 'device-code',\n });\n if (!claimed) return c.json({ status: 'pending', nextPollMs: 250 });\n\n let result: OAuthFlowPoll;\n try {\n result = await flow.poll(claimed.pending);\n } catch (error) {\n await (await sessionStore(ctx)).touchLoginSession(sessionId, { nextPollAt: null });\n throw error;\n }\n\n if (result.status === 'complete') {\n await persistOAuthCredential({\n ctx,\n provider,\n scope: session.credentialScope,\n credentials: result.credentials,\n authStorage,\n memorySettings,\n onCredentialsChanged,\n });\n await (await sessionStore(ctx)).deleteLoginSession(sessionId);\n return c.json({ status: 'complete', ok: true });\n }\n\n if (result.status === 'failed') {\n await (await sessionStore(ctx)).deleteLoginSession(sessionId);\n return c.json({ status: 'failed', error: result.error });\n }\n\n await (\n await sessionStore(ctx)\n ).touchLoginSession(sessionId, {\n ...(result.pending ? { pending: result.pending } : {}),\n nextPollAt: new Date(now + result.nextPollMs),\n });\n return c.json({ status: 'pending', nextPollMs: result.nextPollMs });\n },\n }),\n\n registerApiRoute('/web/config/providers/:provider/oauth/session/:sessionId', {\n method: 'DELETE',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const sessionId = c.req.param('sessionId');\n const session = await loadOwnedSession({ ctx, provider, sessionId });\n if (session) await (await sessionStore(ctx)).deleteLoginSession(sessionId);\n return c.json({ ok: true });\n },\n }),\n\n registerApiRoute('/web/config/providers/:provider/oauth', {\n method: 'DELETE',\n requiresAuth: false,\n handler: async c => {\n const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: modelCredentials });\n if ('response' in ctx) return ctx.response;\n\n const provider = c.req.param('provider');\n const authProviderId = getAuthProviderId(provider);\n // `?scope=org` removes the shared org credential (admin-only);\n // default removes the caller's personal credential only.\n const scope = c.req.query('scope') === 'org' ? 'org' : 'user';\n try {\n if (ctx.mode === 'tenant') {\n if (scope === 'org' && !(await auth.isOrganizationAdmin(loose(c), ctx.orgId))) {\n return c.json({ error: 'forbidden', message: 'Only org admins can remove an org-wide sign-in' }, 403);\n }\n const tenant = { orgId: ctx.orgId, ...(scope === 'org' ? {} : { userId: ctx.userId }) };\n await ctx.storage.removeCredential(tenant, authProviderId);\n onCredentialsChanged(tenant);\n } else {\n if (!authStorage) return c.json({ error: 'Credential storage is not available' }, 503);\n authStorage.logout(authProviderId);\n }\n return c.json({ ok: true });\n } catch (error) {\n return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);\n }\n },\n }),\n ];\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,MAAM,oBAAoB,MAAU;;AA6BpC,MAAM,cAAyC;CAC7C,WAAW;EACT,MAAM;EACN,OAAO,YAAY;GACjB,MAAM,EAAE,KAAK,aAAa,MAAM,oBAAoB;GACpD,OAAO;IACL;IACA,cAAc;IACd,WAAW,KAAK,IAAI,IAAI;IACxB,SAAS,EAAE,SAAS;GACtB;EACF;EACA,WAAW,SAAS,SAAS,uBAAuB,MAAM,OAAO,QAAQ,YAAY,EAAE,CAAC;CAC1F;CACA,QAAQ;EACN,MAAM;EACN,OAAO,YAAY;GACjB,MAAM,IAAI,MAAM,sBAAsB;GACtC,OAAO;IACL,KAAK,EAAE;IACP,UAAU,EAAE;IACZ,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,YAAY,EAAE;IACd,SAAS,EAAE,GAAG,EAAE;GAClB;EACF;EACA,MAAM,OAAM,YAAW;GACrB,MAAM,IAAI,MAAM,qBAAqB,OAA6C;GAClF,IAAI,EAAE,WAAW,YAAY,OAAO;IAAE,QAAQ;IAAY,aAAa,EAAE;GAAY;GACrF,IAAI,EAAE,WAAW,WAAW,OAAO;IAAE,QAAQ;IAAW,YAAY,EAAE;GAAW;GACjF,OAAO;IAAE,QAAQ;IAAU,OAAO,EAAE;GAAM;EAC5C;CACF;CACA,kBAAkB;EAChB,MAAM;EACN,OAAO,YAAY;GACjB,MAAM,IAAI,MAAM,8BAA8B;GAC9C,OAAO;IACL,KAAK,EAAE;IACP,UAAU,EAAE;IACZ,cAAc,EAAE;IAChB,WAAW,EAAE;IACb,YAAY,uBAAuB,CAAC;IACpC,SAAS,EAAE,GAAG,EAAE;GAClB;EACF;EACA,MAAM,OAAM,YAAW;GACrB,MAAM,IAAI;GAIV,IAAI,EAAE,WAAW,gBAAgB,EAAE,qBAAqB,KAAA,GACtD,OAAO;IAAE,QAAQ;IAAU,OAAO;GAA0B;GAE9D,MAAM,IAAI,MAAM,6BAA6B,CAAC;GAC9C,IAAI,EAAE,WAAW,YAAY,OAAO;IAAE,QAAQ;IAAY,aAAa,EAAE;GAAY;GACrF,IAAI,EAAE,WAAW,WAAW,OAAO;IAAE,QAAQ;IAAW,YAAY,EAAE;IAAY,SAAS,EAAE,GAAG,EAAE,QAAQ;GAAE;GAC5G,OAAO;IAAE,QAAQ;IAAU,OAAO,EAAE;GAAM;EAC5C;CACF;CACA,KAAK;EACH,MAAM;EACN,OAAO,YAAY;GACjB,MAAM,IAAI,MAAM,oBAAoB;GACpC,OAAO;IACL,KAAK,EAAE;IACP,UAAU,EAAE;IACZ,cAAc,EAAE;IAChB,WAAW,EAAE,MAAM;IACnB,YAAY,gBAAgB,EAAE,KAAK;IACnC,SAAS,EAAE,GAAG,EAAE;GAClB;EACF;EACA,MAAM,OAAM,YAAW;GACrB,MAAM,IAAI,MAAM,mBAAmB,OAA2C;GAC9E,IAAI,EAAE,WAAW,YAAY,OAAO;IAAE,QAAQ;IAAY,aAAa,EAAE;GAAY;GACrF,IAAI,EAAE,WAAW,WAAW,OAAO;IAAE,QAAQ;IAAW,YAAY,EAAE;IAAY,SAAS,EAAE,GAAG,EAAE,QAAQ;GAAE;GAC5G,OAAO;IAAE,QAAQ;IAAU,OAAO,EAAE;GAAM;EAC5C;CACF;AACF;AAEA,SAAS,MAAM,GAAqB;CAClC,OAAO;AACT;;;;;;AAOA,IAAI;AACJ,SAAS,gBAAkD;CACzD,0BAA0B,YAAY;EACpC,MAAM,EAAE,yBAAyB,MAAM,OAAO;EAC9C,MAAM,UAAU,IAAI,qBAAqB;GAAE,IAAI;GAAwB,KAAK;EAAW,CAAC;EACxF,MAAM,QAAQ,QAAQ,eAAe,IAAI,wBAAwB,CAAC;EAClE,MAAM,QAAQ,KAAK;EACnB,OAAO;CACT,EAAA,CAAG;CACH,OAAO;AACT;AACA,MAAM,eAAe;CAAE,OAAO;CAAS,QAAQ;AAAQ;AAEvD,eAAe,aAAa,KAA0D;CACpF,OAAO,IAAI,SAAS,WAAW,IAAI,UAAU,cAAc;AAC7D;AAEA,SAAS,cAAc,KAA2D;CAChF,OAAO,IAAI,SAAS,WAAW;EAAE,OAAO,IAAI;EAAO,QAAQ,IAAI;CAAO,IAAI;AAC5E;;AAGA,eAAe,iBAAiB,EAC9B,KACA,UACA,aAKuC;CACvC,MAAM,UAAU,OAAO,MAAM,aAAa,GAAG,EAAA,CAAG,gBAAgB,SAAS;CACzE,IAAI,CAAC,SAAS,OAAO,KAAA;CACrB,MAAM,SAAS,cAAc,GAAG;CAChC,IAAI,QAAQ,UAAU,OAAO,SAAS,QAAQ,WAAW,OAAO,QAAQ,OAAO,KAAA;CAC/E,IAAI,QAAQ,aAAa,UAAU,OAAO,KAAA;CAC1C,OAAO;AACT;;AAGA,eAAe,uBAAuB,EACpC,KACA,UACA,OACA,aACA,aACA,gBACA,wBASgB;CAChB,MAAM,iBAAiB,kBAAkB,QAAQ;CACjD,IAAI,IAAI,SAAS,UAAU;EACzB,MAAM,SAAS;GAAE,OAAO,IAAI;GAAO,GAAI,UAAU,QAAQ,CAAC,IAAI,EAAE,QAAQ,IAAI,OAAO;EAAG;EACtF,MAAM,IAAI,QAAQ,cAAc,QAAQ,gBAAgB;GACtD,MAAM;GACN,GAAG;EACL,CAAC;EACD,qBAAqB,MAAM;EAC3B,MAAM,uBAAuB;GAAE;GAAgB;GAAQ;EAAS,CAAC;EACjE;CACF;CACA,IAAI,CAAC,aAAa,MAAM,IAAI,MAAM,qCAAqC;CACvE,MAAM,YAAY,WAAW,gBAAgB,WAAW;AAC1D;AAEA,eAAe,aAAa,GAA8C;CACxE,IAAI;EACF,MAAM,OAAQ,MAAM,EAAE,IAAI,KAAK;EAC/B,OAAO,QAAQ,OAAO,SAAS,WAAY,OAAmC,CAAC;CACjF,QAAQ;EACN,OAAO,CAAC;CACV;AACF;;;;;;;;;AAqBA,IAAa,cAAb,cAAiC,MAAuB;CACtD,SAAqB;EACnB,MAAM,EAAE,MAAM,aAAa,kBAAkB,mBAAmB,KAAK;EACrE,MAAM,uBAAuB,KAAK,KAAK,+BAA+B,CAAC;EAEvE,OAAO;GACL,iBAAiB,+CAA+C;IAC9D,QAAQ;IACR,cAAc;IACd,SAAS,OAAM,MAAK;KAClB,MAAM,MAAM,MAAM,yBAAyB;MAAE,GAAG,MAAM,CAAC;MAAG;MAAM,aAAa;KAAiB,CAAC;KAC/F,IAAI,cAAc,KAAK,OAAO,IAAI;KAElC,MAAM,WAAW,EAAE,IAAI,MAAM,UAAU;KACvC,MAAM,OAAO,YAAY;KACzB,IAAI,CAAC,MACH,OAAO,EAAE,KAAK;MAAE,OAAO;MAAuB,SAAS;KAAwC,GAAG,GAAG;KAIvG,MAAM,OAAO,MAAM,aAAa,MAAM,CAAC,CAAC;KACxC,IAAI,OAAO,KAAK,SAAS,YAAY,KAAK,SAAS,KAAK,MACtD,OAAO,EAAE,KAAK;MAAE,OAAO;MAAgB,SAAS,wBAAwB,SAAS,IAAI,KAAK;KAAO,GAAG,GAAG;KAKzG,MAAM,QAA8B,KAAK,UAAU,QAAQ,QAAQ;KACnE,IAAI,UAAU,SAAS,IAAI,SAAS,YAAY,CAAE,MAAM,KAAK,oBAAoB,MAAM,CAAC,GAAG,IAAI,KAAK,GAClG,OAAO,EAAE,KAAK;MAAE,OAAO;MAAa,SAAS;KAAgD,GAAG,GAAG;KAGrG,IAAI;KACJ,IAAI;MACF,UAAU,MAAM,KAAK,MAAM;KAC7B,SAAS,OAAO;MACd,OAAO,EAAE,KAAK,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG,GAAG;KACtF;KAEA,MAAM,YAAY,WAAW,OAAO,WAAW;KAC/C,MAAM,SAAS,cAAc,GAAG;KAChC,OACE,MAAM,aAAa,GAAG,EAAA,CACtB,mBAAmB;MACnB;MACA,OAAO,OAAO;MACd,QAAQ,OAAO;MACf;MACA,MAAM,KAAK;MACX,GAAI,UAAU,SAAS,IAAI,SAAS,WAAW,EAAE,iBAAiB,MAAM,IAAI,CAAC;MAC7E,SAAS,QAAQ;MACjB,WAAW,IAAI,KAAK,QAAQ,SAAS;MACrC,YAAY,QAAQ,cAAc,OAAO,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,UAAU,IAAI;KACvF,CAAC;KAED,OAAO,EAAE,KAAK;MACZ;MACA,MAAM,KAAK;MACX,KAAK,QAAQ;MACb,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;MACzD,cAAc,QAAQ;MACtB,WAAW,QAAQ;MACnB,GAAI,QAAQ,cAAc,OAAO,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;KACzE,CAAC;IACH;GACF,CAAC;GAED,iBAAiB,kDAAkD;IACjE,QAAQ;IACR,cAAc;IACd,SAAS,OAAM,MAAK;KAClB,MAAM,MAAM,MAAM,yBAAyB;MAAE,GAAG,MAAM,CAAC;MAAG;MAAM,aAAa;KAAiB,CAAC;KAC/F,IAAI,cAAc,KAAK,OAAO,IAAI;KAElC,MAAM,WAAW,EAAE,IAAI,MAAM,UAAU;KACvC,MAAM,OAAO,YAAY;KACzB,IAAI,CAAC,MAAM,UAAU,OAAO,EAAE,KAAK,EAAE,OAAO,sBAAsB,GAAG,GAAG;KAExE,MAAM,OAAO,MAAM,aAAa,MAAM,CAAC,CAAC;KACxC,MAAM,YAAY,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY;KACxE,MAAM,OAAO,OAAO,KAAK,SAAS,WAAW,KAAK,KAAK,KAAK,IAAI;KAChE,IAAI,CAAC,aAAa,CAAC,MAAM,OAAO,EAAE,KAAK,EAAE,OAAO,2CAA2C,GAAG,GAAG;KAEjG,MAAM,UAAU,MAAM,iBAAiB;MAAE;MAAK;MAAU;KAAU,CAAC;KACnE,IAAI,CAAC,SAAS,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;KAC/D,IAAI,QAAQ,SAAS,cAAc,OAAO,EAAE,KAAK,EAAE,OAAO,qBAAqB,GAAG,GAAG;KAGrF,IACE,QAAQ,oBAAoB,SAC5B,IAAI,SAAS,YACb,CAAE,MAAM,KAAK,oBAAoB,MAAM,CAAC,GAAG,IAAI,KAAK,GAEpD,OAAO,EAAE,KAAK;MAAE,OAAO;MAAa,SAAS;KAAgD,GAAG,GAAG;KAGrG,MAAM,UAAU,OACd,MAAM,aAAa,GAAG,EAAA,CACtB,kBAAkB,WAAW;MAC7B,OAAO,QAAQ;MACf,QAAQ,QAAQ;MAChB;MACA,MAAM;KACR,CAAC;KACD,IAAI,CAAC,SAAS,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;KAE/D,IAAI;KACJ,IAAI;MACF,cAAc,MAAM,KAAK,SAAS,QAAQ,SAAS,IAAI;KACzD,SAAS,OAAO;MACd,OAAO,MAAM,aAAa,GAAG,EAAA,CAAG,kBAAkB,WAAW,EAAE,YAAY,KAAK,CAAC;MACjF,OAAO,EAAE,KAAK,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG,GAAG;KACtF;KAEA,MAAM,uBAAuB;MAC3B;MACA;MACA,OAAO,QAAQ;MACf;MACA;MACA;MACA;KACF,CAAC;KACD,OAAO,MAAM,aAAa,GAAG,EAAA,CAAG,mBAAmB,SAAS;KAC5D,OAAO,EAAE,KAAK;MAAE,QAAQ;MAAY,IAAI;KAAK,CAAC;IAChD;GACF,CAAC;GAED,iBAAiB,8CAA8C;IAC7D,QAAQ;IACR,cAAc;IACd,SAAS,OAAM,MAAK;KAClB,MAAM,MAAM,MAAM,yBAAyB;MAAE,GAAG,MAAM,CAAC;MAAG;MAAM,aAAa;KAAiB,CAAC;KAC/F,IAAI,cAAc,KAAK,OAAO,IAAI;KAElC,MAAM,WAAW,EAAE,IAAI,MAAM,UAAU;KACvC,MAAM,OAAO,YAAY;KACzB,IAAI,CAAC,MAAM,MAAM,OAAO,EAAE,KAAK,EAAE,OAAO,sBAAsB,GAAG,GAAG;KAEpE,MAAM,OAAO,MAAM,aAAa,MAAM,CAAC,CAAC;KACxC,MAAM,YAAY,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY;KACxE,IAAI,CAAC,WAAW,OAAO,EAAE,KAAK,EAAE,OAAO,oCAAoC,GAAG,GAAG;KAEjF,MAAM,UAAU,MAAM,iBAAiB;MAAE;MAAK;MAAU;KAAU,CAAC;KACnE,IAAI,CAAC,SAAS,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;KAC/D,IAAI,QAAQ,SAAS,eAAe,OAAO,EAAE,KAAK,EAAE,OAAO,qBAAqB,GAAG,GAAG;KAGtF,IACE,QAAQ,oBAAoB,SAC5B,IAAI,SAAS,YACb,CAAE,MAAM,KAAK,oBAAoB,MAAM,CAAC,GAAG,IAAI,KAAK,GAEpD,OAAO,EAAE,KAAK;MAAE,OAAO;MAAa,SAAS;KAAgD,GAAG,GAAG;KAKrG,MAAM,MAAM,KAAK,IAAI;KACrB,MAAM,aAAa,QAAQ,YAAY,QAAQ;KAI/C,MAAM,gBAAgB,cAAc,QAAQ,cAAc,QAAQ,UAAU,QAAQ;KACpF,IAAI,cAAc,QAAQ,MAAM,cAAc,CAAC,eAC7C,OAAO,EAAE,KAAK;MAAE,QAAQ;MAAW,YAAY,aAAa;KAAI,CAAC;KAGnE,MAAM,UAAU,OACd,MAAM,aAAa,GAAG,EAAA,CACtB,kBAAkB,WAAW;MAC7B,OAAO,QAAQ;MACf,QAAQ,QAAQ;MAChB;MACA,MAAM;KACR,CAAC;KACD,IAAI,CAAC,SAAS,OAAO,EAAE,KAAK;MAAE,QAAQ;MAAW,YAAY;KAAI,CAAC;KAElE,IAAI;KACJ,IAAI;MACF,SAAS,MAAM,KAAK,KAAK,QAAQ,OAAO;KAC1C,SAAS,OAAO;MACd,OAAO,MAAM,aAAa,GAAG,EAAA,CAAG,kBAAkB,WAAW,EAAE,YAAY,KAAK,CAAC;MACjF,MAAM;KACR;KAEA,IAAI,OAAO,WAAW,YAAY;MAChC,MAAM,uBAAuB;OAC3B;OACA;OACA,OAAO,QAAQ;OACf,aAAa,OAAO;OACpB;OACA;OACA;MACF,CAAC;MACD,OAAO,MAAM,aAAa,GAAG,EAAA,CAAG,mBAAmB,SAAS;MAC5D,OAAO,EAAE,KAAK;OAAE,QAAQ;OAAY,IAAI;MAAK,CAAC;KAChD;KAEA,IAAI,OAAO,WAAW,UAAU;MAC9B,OAAO,MAAM,aAAa,GAAG,EAAA,CAAG,mBAAmB,SAAS;MAC5D,OAAO,EAAE,KAAK;OAAE,QAAQ;OAAU,OAAO,OAAO;MAAM,CAAC;KACzD;KAEA,OACE,MAAM,aAAa,GAAG,EAAA,CACtB,kBAAkB,WAAW;MAC7B,GAAI,OAAO,UAAU,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;MACpD,YAAY,IAAI,KAAK,MAAM,OAAO,UAAU;KAC9C,CAAC;KACD,OAAO,EAAE,KAAK;MAAE,QAAQ;MAAW,YAAY,OAAO;KAAW,CAAC;IACpE;GACF,CAAC;GAED,iBAAiB,4DAA4D;IAC3E,QAAQ;IACR,cAAc;IACd,SAAS,OAAM,MAAK;KAClB,MAAM,MAAM,MAAM,yBAAyB;MAAE,GAAG,MAAM,CAAC;MAAG;MAAM,aAAa;KAAiB,CAAC;KAC/F,IAAI,cAAc,KAAK,OAAO,IAAI;KAElC,MAAM,WAAW,EAAE,IAAI,MAAM,UAAU;KACvC,MAAM,YAAY,EAAE,IAAI,MAAM,WAAW;KAEzC,IAAI,MADkB,iBAAiB;MAAE;MAAK;MAAU;KAAU,CAAC,GACtD,OAAO,MAAM,aAAa,GAAG,EAAA,CAAG,mBAAmB,SAAS;KACzE,OAAO,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;IAC5B;GACF,CAAC;GAED,iBAAiB,yCAAyC;IACxD,QAAQ;IACR,cAAc;IACd,SAAS,OAAM,MAAK;KAClB,MAAM,MAAM,MAAM,yBAAyB;MAAE,GAAG,MAAM,CAAC;MAAG;MAAM,aAAa;KAAiB,CAAC;KAC/F,IAAI,cAAc,KAAK,OAAO,IAAI;KAGlC,MAAM,iBAAiB,kBADN,EAAE,IAAI,MAAM,UACmB,CAAC;KAGjD,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO,MAAM,QAAQ,QAAQ;KACvD,IAAI;MACF,IAAI,IAAI,SAAS,UAAU;OACzB,IAAI,UAAU,SAAS,CAAE,MAAM,KAAK,oBAAoB,MAAM,CAAC,GAAG,IAAI,KAAK,GACzE,OAAO,EAAE,KAAK;QAAE,OAAO;QAAa,SAAS;OAAiD,GAAG,GAAG;OAEtG,MAAM,SAAS;QAAE,OAAO,IAAI;QAAO,GAAI,UAAU,QAAQ,CAAC,IAAI,EAAE,QAAQ,IAAI,OAAO;OAAG;OACtF,MAAM,IAAI,QAAQ,iBAAiB,QAAQ,cAAc;OACzD,qBAAqB,MAAM;MAC7B,OAAO;OACL,IAAI,CAAC,aAAa,OAAO,EAAE,KAAK,EAAE,OAAO,sCAAsC,GAAG,GAAG;OACrF,YAAY,OAAO,cAAc;MACnC;MACA,OAAO,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;KAC5B,SAAS,OAAO;MACd,OAAO,EAAE,KAAK,EAAE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG,GAAG;KACtF;IACF;GACF,CAAC;EACH;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"source-control-sessions.d.ts","sourceRoot":"","sources":["../../src/routes/source-control-sessions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAOnE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AAGrF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACxF,OAAO,KAAK,EAGV,0BAA0B,EAC3B,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAoB5C,KAAK,wBAAwB,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC;AAE1G,MAAM,WAAW,iCAAiC;IAChD,IAAI,EAAE,SAAS,CAAC;IAChB,cAAc,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtD,KAAK,CAAC,EAAE,wBAAwB,CAAC;IACjC,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC7C,cAAc,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;IACnD,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;IACjD,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;CAC9D;AAqRD,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,iCAAiC,GAAG,QAAQ,EAAE,CAsHtG"}
1
+ {"version":3,"file":"source-control-sessions.d.ts","sourceRoot":"","sources":["../../src/routes/source-control-sessions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAOnE,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AAGrF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACxF,OAAO,KAAK,EAGV,0BAA0B,EAC3B,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAoB5C,KAAK,wBAAwB,GAAG,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC,CAAC;AAE1G,MAAM,WAAW,iCAAiC;IAChD,IAAI,EAAE,SAAS,CAAC;IAChB,cAAc,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtD,KAAK,CAAC,EAAE,wBAAwB,CAAC;IACjC,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC7C,cAAc,EAAE,IAAI,CAAC,qBAAqB,EAAE,KAAK,CAAC,CAAC;IACnD,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;IACjD,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;CAC9D;AAqRD,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,iCAAiC,GAAG,QAAQ,EAAE,CAsHtG"}
@@ -4,7 +4,6 @@ import { reclaimDeletedSessionSandbox } from "../integrations/github/sandbox-rel
4
4
  import { isValidGitRef } from "../sandbox/git-ref.js";
5
5
  import { RequestContext } from "@mastra/core/request-context";
6
6
  import { registerApiRoute } from "@mastra/core/server";
7
- import { randomUUID } from "crypto";
8
7
  import { UniqueViolationError } from "@mastra/core/storage";
9
8
  import { resolveModel } from "@mastra/code-sdk/agents/model";
10
9
  //#region src/routes/source-control-sessions.ts
@@ -176,7 +175,7 @@ function projectSessionRoutes(path, options, resolveOwnerProfiles) {
176
175
  if (!isValidGitRef(baseBranch)) return c.json({ error: "Invalid baseBranch" }, 400);
177
176
  if (body.sessionId !== void 0 && (typeof body.sessionId !== "string" || !UUID_PATTERN.test(body.sessionId))) return c.json({ error: "Invalid sessionId" }, 400);
178
177
  const requestedSessionId = body.sessionId;
179
- const sessionId = requestedSessionId ?? randomUUID();
178
+ const sessionId = requestedSessionId ?? globalThis.crypto.randomUUID();
180
179
  if (body.title !== void 0 && typeof body.title !== "string") return c.json({ error: "Invalid title" }, 400);
181
180
  const normalizedTitle = body.title === void 0 ? null : normalizeSessionTitle(body.title);
182
181
  let branch;