@mastra/factory 0.26.0-alpha.1 → 0.26.0-alpha.3

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 (180) hide show
  1. package/README.md +74 -7
  2. package/dist/boards/define-board.d.ts +19 -0
  3. package/dist/boards/define-board.d.ts.map +1 -1
  4. package/dist/boards/define-board.js +41 -9
  5. package/dist/boards/define-board.js.map +1 -1
  6. package/dist/boards/project-boards.d.ts +78 -0
  7. package/dist/boards/project-boards.d.ts.map +1 -0
  8. package/dist/boards/project-boards.js +194 -0
  9. package/dist/boards/project-boards.js.map +1 -0
  10. package/dist/boards/review.js +2 -2
  11. package/dist/boards/review.js.map +1 -1
  12. package/dist/boards/stored-board.d.ts +101 -0
  13. package/dist/boards/stored-board.d.ts.map +1 -0
  14. package/dist/boards/stored-board.js +139 -0
  15. package/dist/boards/stored-board.js.map +1 -0
  16. package/dist/boards/work.d.ts.map +1 -1
  17. package/dist/boards/work.js +6 -3
  18. package/dist/boards/work.js.map +1 -1
  19. package/dist/capabilities/intake.d.ts +6 -0
  20. package/dist/capabilities/intake.d.ts.map +1 -1
  21. package/dist/environment/build.d.ts +1 -1
  22. package/dist/environment/build.d.ts.map +1 -1
  23. package/dist/environment/build.js +2 -0
  24. package/dist/environment/build.js.map +1 -1
  25. package/dist/factory.d.ts +15 -2
  26. package/dist/factory.d.ts.map +1 -1
  27. package/dist/factory.js +51 -12
  28. package/dist/factory.js.map +1 -1
  29. package/dist/integrations/github/integration.d.ts +5 -4
  30. package/dist/integrations/github/integration.d.ts.map +1 -1
  31. package/dist/integrations/github/integration.js +10 -8
  32. package/dist/integrations/github/integration.js.map +1 -1
  33. package/dist/integrations/github/sandbox.d.ts +2 -2
  34. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  35. package/dist/integrations/github/sandbox.js +6 -1
  36. package/dist/integrations/github/sandbox.js.map +1 -1
  37. package/dist/integrations/jira/binding-scope.d.ts +30 -0
  38. package/dist/integrations/jira/binding-scope.d.ts.map +1 -0
  39. package/dist/integrations/jira/binding-scope.js +50 -0
  40. package/dist/integrations/jira/binding-scope.js.map +1 -0
  41. package/dist/integrations/jira/claim.d.ts +12 -0
  42. package/dist/integrations/jira/claim.d.ts.map +1 -1
  43. package/dist/integrations/jira/claim.js +24 -1
  44. package/dist/integrations/jira/claim.js.map +1 -1
  45. package/dist/integrations/jira/intake-jql.d.ts +32 -0
  46. package/dist/integrations/jira/intake-jql.d.ts.map +1 -0
  47. package/dist/integrations/jira/intake-jql.js +57 -0
  48. package/dist/integrations/jira/intake-jql.js.map +1 -0
  49. package/dist/integrations/jira/integration.d.ts +4 -3
  50. package/dist/integrations/jira/integration.d.ts.map +1 -1
  51. package/dist/integrations/jira/integration.js +14 -21
  52. package/dist/integrations/jira/integration.js.map +1 -1
  53. package/dist/integrations/jira/routes.d.ts +3 -0
  54. package/dist/integrations/jira/routes.d.ts.map +1 -1
  55. package/dist/integrations/jira/routes.js +16 -29
  56. package/dist/integrations/jira/routes.js.map +1 -1
  57. package/dist/integrations/jira/rules.d.ts.map +1 -1
  58. package/dist/integrations/jira/rules.js +8 -13
  59. package/dist/integrations/jira/rules.js.map +1 -1
  60. package/dist/integrations/platform/github/integration.d.ts +5 -0
  61. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  62. package/dist/integrations/platform/github/integration.js +7 -0
  63. package/dist/integrations/platform/github/integration.js.map +1 -1
  64. package/dist/integrations/platform/jira/integration.d.ts +7 -1
  65. package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
  66. package/dist/integrations/platform/jira/integration.js +26 -24
  67. package/dist/integrations/platform/jira/integration.js.map +1 -1
  68. package/dist/integrations/platform/jira/routes.d.ts +3 -0
  69. package/dist/integrations/platform/jira/routes.d.ts.map +1 -1
  70. package/dist/integrations/platform/jira/routes.js +17 -28
  71. package/dist/integrations/platform/jira/routes.js.map +1 -1
  72. package/dist/routes/attention-providers.d.ts +2 -1
  73. package/dist/routes/attention-providers.d.ts.map +1 -1
  74. package/dist/routes/attention-providers.js +3 -2
  75. package/dist/routes/attention-providers.js.map +1 -1
  76. package/dist/routes/attention.d.ts +2 -1
  77. package/dist/routes/attention.d.ts.map +1 -1
  78. package/dist/routes/attention.js.map +1 -1
  79. package/dist/routes/boards.d.ts +30 -0
  80. package/dist/routes/boards.d.ts.map +1 -0
  81. package/dist/routes/boards.js +272 -0
  82. package/dist/routes/boards.js.map +1 -0
  83. package/dist/routes/contracts.d.ts +249 -0
  84. package/dist/routes/contracts.d.ts.map +1 -1
  85. package/dist/routes/contracts.js +107 -35
  86. package/dist/routes/contracts.js.map +1 -1
  87. package/dist/routes/intake.d.ts +15 -10
  88. package/dist/routes/intake.d.ts.map +1 -1
  89. package/dist/routes/intake.js +109 -22
  90. package/dist/routes/intake.js.map +1 -1
  91. package/dist/routes/projects.d.ts +3 -0
  92. package/dist/routes/projects.d.ts.map +1 -1
  93. package/dist/routes/projects.js +10 -1
  94. package/dist/routes/projects.js.map +1 -1
  95. package/dist/routes/surface.d.ts +10 -3
  96. package/dist/routes/surface.d.ts.map +1 -1
  97. package/dist/routes/surface.js +13 -1
  98. package/dist/routes/surface.js.map +1 -1
  99. package/dist/routes/tenant-credentials.d.ts +9 -0
  100. package/dist/routes/tenant-credentials.d.ts.map +1 -1
  101. package/dist/routes/tenant-credentials.js +24 -2
  102. package/dist/routes/tenant-credentials.js.map +1 -1
  103. package/dist/routes/work-items.d.ts +3 -1
  104. package/dist/routes/work-items.d.ts.map +1 -1
  105. package/dist/routes/work-items.js +64 -42
  106. package/dist/routes/work-items.js.map +1 -1
  107. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  108. package/dist/rules/dispatch-errors.js +4 -0
  109. package/dist/rules/dispatch-errors.js.map +1 -1
  110. package/dist/rules/dispatcher.d.ts +7 -1
  111. package/dist/rules/dispatcher.d.ts.map +1 -1
  112. package/dist/rules/dispatcher.js +70 -34
  113. package/dist/rules/dispatcher.js.map +1 -1
  114. package/dist/rules/processor.d.ts +3 -1
  115. package/dist/rules/processor.d.ts.map +1 -1
  116. package/dist/rules/processor.js +11 -6
  117. package/dist/rules/processor.js.map +1 -1
  118. package/dist/rules/tools.d.ts +2 -1
  119. package/dist/rules/tools.d.ts.map +1 -1
  120. package/dist/rules/tools.js +2 -1
  121. package/dist/rules/tools.js.map +1 -1
  122. package/dist/rules/transition-service.d.ts +3 -1
  123. package/dist/rules/transition-service.d.ts.map +1 -1
  124. package/dist/rules/transition-service.js +12 -12
  125. package/dist/rules/transition-service.js.map +1 -1
  126. package/dist/sandbox/local-factory-sandbox.d.ts +4 -1
  127. package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
  128. package/dist/sandbox/local-factory-sandbox.js +1 -1
  129. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  130. package/dist/storage/domains/audit/actions.d.ts +1 -0
  131. package/dist/storage/domains/audit/actions.d.ts.map +1 -1
  132. package/dist/storage/domains/audit/actions.js +6 -1
  133. package/dist/storage/domains/audit/actions.js.map +1 -1
  134. package/dist/storage/domains/boards/base.d.ts +110 -0
  135. package/dist/storage/domains/boards/base.d.ts.map +1 -0
  136. package/dist/storage/domains/boards/base.js +292 -0
  137. package/dist/storage/domains/boards/base.js.map +1 -0
  138. package/dist/storage/domains/boards/schemas.d.ts +11 -0
  139. package/dist/storage/domains/boards/schemas.d.ts.map +1 -0
  140. package/dist/storage/domains/boards/schemas.js +106 -0
  141. package/dist/storage/domains/boards/schemas.js.map +1 -0
  142. package/dist/storage/domains/credentials/base.d.ts +4 -1
  143. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  144. package/dist/storage/domains/credentials/base.js +5 -2
  145. package/dist/storage/domains/credentials/base.js.map +1 -1
  146. package/dist/storage/domains/intake/base.d.ts +86 -5
  147. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  148. package/dist/storage/domains/intake/base.js +298 -59
  149. package/dist/storage/domains/intake/base.js.map +1 -1
  150. package/dist/storage/domains/projects/base.d.ts +2 -1
  151. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  152. package/dist/storage/domains/projects/base.js +15 -10
  153. package/dist/storage/domains/projects/base.js.map +1 -1
  154. package/dist/storage/domains/work-items/base.d.ts +16 -2
  155. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  156. package/dist/storage/domains/work-items/base.js +40 -11
  157. package/dist/storage/domains/work-items/base.js.map +1 -1
  158. package/dist/tools/read-tools.d.ts +3 -1
  159. package/dist/tools/read-tools.d.ts.map +1 -1
  160. package/dist/tools/read-tools.js +3 -2
  161. package/dist/tools/read-tools.js.map +1 -1
  162. package/dist/tools/session-tools.d.ts +3 -1
  163. package/dist/tools/session-tools.d.ts.map +1 -1
  164. package/dist/tools/session-tools.js +2 -1
  165. package/dist/tools/session-tools.js.map +1 -1
  166. package/dist/tools/sessions-state-processor.d.ts +3 -1
  167. package/dist/tools/sessions-state-processor.d.ts.map +1 -1
  168. package/dist/tools/sessions-state-processor.js +2 -1
  169. package/dist/tools/sessions-state-processor.js.map +1 -1
  170. package/dist/tools/work-item-tools.d.ts +3 -1
  171. package/dist/tools/work-item-tools.d.ts.map +1 -1
  172. package/dist/tools/work-item-tools.js +3 -2
  173. package/dist/tools/work-item-tools.js.map +1 -1
  174. package/dist/workspace.d.ts +2 -0
  175. package/dist/workspace.d.ts.map +1 -1
  176. package/dist/workspace.js +14 -5
  177. package/dist/workspace.js.map +1 -1
  178. package/factory-skills/factory-plan/SKILL.md +1 -1
  179. package/factory-skills/factory-triage/SKILL.md +18 -7
  180. package/package.json +9 -9
@@ -1 +1 @@
1
- {"version":3,"file":"surface.js","names":[],"sources":["../../src/routes/surface.ts"],"sourcesContent":["import type { AuthStorage } from '@mastra/code-sdk/auth/storage';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\nimport type { ApiRoute, IUserProvider } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { InlineSkill } from '@mastra/core/skills';\nimport type { FactoryStorage } from '@mastra/core/storage';\nimport type { FactorySandbox } from '@mastra/core/workspace';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { legacyPlanRoleFor, workLaneForRole } from '../boards/legacy-plan-role.js';\nimport type { FactoryIntegration, IntegrationContext, IntegrationHooks } from '../integrations/base.js';\nimport { getGithubFeatureDiagnostics } from '../integrations/github/config.js';\nimport type { GithubIntegration } from '../integrations/github/integration.js';\nimport { MaterializeError } from '../integrations/github/sandbox.js';\nimport { FactoryDispatchError } from '../rules/dispatch-errors.js';\nimport type { FactoryBindingPreparationInput } from '../rules/dispatcher.js';\nimport { FactoryStartCoordinator } from '../rules/start-coordinator.js';\nimport { FactoryTransitionService } from '../rules/transition-service.js';\nimport {\n createSourceControlSessionLookup,\n ensureFactorySourceSession,\n FactorySourceSessionResolutionError,\n resolveFactorySourceControl,\n} from '../session/factory-session.js';\nimport type { EnsuredFactorySourceSession } from '../session/factory-session.js';\nimport type { LiveSessions } from '../session/live-sessions.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { StateSigner } from '../state-signing.js';\nimport type { AuditEmitter, AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { ChannelIdentityStorage } from '../storage/domains/channel-identity/base.js';\nimport type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';\nimport type { CommentsDomain } from '../storage/domains/comments/domain.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { CustomProvidersStorage } from '../storage/domains/custom-providers/base.js';\nimport type { FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { IntegrationStorage } from '../storage/domains/integrations/base.js';\nimport type { MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { QueueHealthStorage } from '../storage/domains/queue-health/base.js';\nimport {\n SessionBranchHeldError,\n SessionOwnershipMismatchError,\n SourceControlConnectionNotFoundError,\n} from '../storage/domains/source-control/base.js';\nimport type { SourceControlStorage, SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { isAgentActor } from '../storage/domains/work-items/base.js';\nimport type { FactoryDispatchFailureCode, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport {\n legacyGithubWorkItemBranch,\n workItemBranch,\n workItemBranchSource,\n workItemThreadTitle,\n} from '../work-item-branch.js';\nimport { buildAutomationRunRoutes } from './automation-runs.js';\nimport { ConfigRoutes } from './config.js';\nimport { invalidateCustomProvidersSnapshots } from './custom-provider-source.js';\nimport { buildFsRoutes } from './fs.js';\nimport { IntakeRoutes } from './intake.js';\nimport { KnowledgeRoutes } from './knowledge.js';\nimport { OAuthRoutes } from './oauth.js';\nimport type { RouteAuth } from './route.js';\nimport { SkillRoutes } from './skills.js';\nimport { buildSourceControlSessionRoutes } from './source-control-sessions.js';\nimport { buildSourceControlSettingsRoutes } from './source-control-settings.js';\nimport { invalidateTenantCredentialSnapshots } from './tenant-credentials.js';\nimport { WorkItemRoutes } from './work-items.js';\n\nconst MATERIALIZE_FAILURE_CODE = {\n 'git-missing': 'repository_git_missing',\n 'egress-blocked': 'repository_egress_blocked',\n 'clone-failed': 'repository_clone_failed',\n 'pull-failed': 'repository_pull_failed',\n 'push-failed': 'repository_push_failed',\n 'commit-failed': 'repository_commit_failed',\n 'gh-missing': 'repository_cli_missing',\n 'pr-failed': 'repository_pr_failed',\n} satisfies Record<MaterializeError['code'], FactoryDispatchFailureCode>;\nexport interface IntegrationRegistration {\n integration: FactoryIntegration;\n ready: boolean;\n ensureReady: () => Promise<void>;\n}\n\nexport interface FactoryApiRoutesDeps {\n controllerId: string;\n controller: AgentController<MastraCodeState>;\n /** Registry of the sessions this process holds, owned by the host so it can watch them too. */\n liveSessions: LiveSessions;\n /** Request-auth seam threaded from the host (no service locator). */\n auth: RouteAuth;\n /** Optional user directory for resolving persisted owners to display profiles. */\n users?: Pick<IUserProvider, 'getUser' | 'getUsers'>;\n authStorage: AuthStorage;\n audit: AuditEmitter & AuditRecorder;\n fsRoot?: string;\n publicOrigin: string;\n stateSigner?: StateSigner;\n /** Sandbox surface (enablement, provider label, create callback). */\n sandbox?: FactorySandbox;\n /** Push hand-off for environment builds, present when the sandbox can build templates. */\n onRepositoryPush?: IntegrationHooks['onRepositoryPush'];\n /** Root factory storage backend (distributed locks, app-db diagnostics). */\n factoryStorage?: FactoryStorage;\n integrationStorage: IntegrationStorage;\n sourceControlStorage: SourceControlStorage;\n /** App-table domain handles, registered and owned by `MastraFactory.prepare()`. */\n domains: {\n intake: IntakeStorage;\n modelCredentials: ModelCredentialsStorage;\n memorySettings: MemorySettingsStorage;\n customProviders: CustomProvidersStorage;\n filesystem: FilesystemStorage;\n modelDefaults: ModelDefaultsStorage;\n projects: FactoryProjectsStorage;\n queueHealth: QueueHealthStorage;\n workItems: WorkItemsStorage;\n channelIdentity: ChannelIdentityStorage;\n comments: WorkItemCommentsStorage;\n };\n integrations?: IntegrationRegistration[];\n intakeReady: boolean;\n factoryReady: boolean;\n knowledgeEnabled: boolean;\n /** The Factory MCP endpoint is mounted; surfaced so the UI can show its URL. */\n mcpEnabled: boolean;\n /** Providers the operator opted in to run on the server process's own credentials. */\n deploymentModelProviders?: ReadonlySet<string>;\n /** App skills from `MastraFactory({ skills })`. */\n skills?: readonly InlineSkill[];\n knowledgeKey?: string;\n /** Resolved Factory rule set, threaded from the host (no service locator). */\n configVersion: string;\n /** Boards installed for this Factory instance. */\n boardRegistry: BoardRegistry;\n /** Work-item feed service, handed to integrations that ingest platform messages. */\n feed: CommentsDomain;\n factoryTransitionService?: FactoryTransitionService;\n sessionRetirement?: import('../sandbox/session-retirement.js').SessionRetirementCoordinator;\n /** Moves a thread with its observation vectors onto another memory resource. */\n moveThread?: (input: { threadId: string; resourceId: string }) => Promise<unknown>;\n onFactoryRuntime?: (runtime: {\n transitionService: FactoryTransitionService;\n startCoordinator: FactoryStartCoordinator;\n prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n }) => void;\n}\n\nfunction guardIntegrationRoutes({\n integration,\n ready,\n ensureReady,\n routes,\n}: IntegrationRegistration & { routes: ApiRoute[] }): ApiRoute[] {\n if (ready) return routes;\n return routes.map(route => {\n if ('handler' in route) {\n const handler = route.handler;\n return {\n ...route,\n handler: async (context: Parameters<typeof handler>[0]) => {\n try {\n await ensureReady();\n } catch {\n return context.json(\n { error: 'integration_unavailable', message: `${integration.id} integration is unavailable.` },\n 503,\n );\n }\n return handler(context, async () => {});\n },\n };\n }\n\n const createHandler = route.createHandler;\n return {\n ...route,\n createHandler: async (args: Parameters<typeof createHandler>[0]) => {\n const handler = await createHandler(args);\n return async (context: Parameters<typeof handler>[0]) => {\n try {\n await ensureReady();\n } catch {\n return context.json(\n { error: 'integration_unavailable', message: `${integration.id} integration is unavailable.` },\n 503,\n );\n }\n return handler(context);\n };\n },\n };\n });\n}\n\n/**\n * Resolve the source-control session the work item already holds for this role,\n * when it still resolves to the item's org and factory project. Returns\n * `undefined` when there is no ref or the ref no longer resolves, so the caller\n * falls back to minting one.\n */\nasync function reuseBoundSession(\n sourceControl: SourceControlStorageHandle,\n input: FactoryBindingPreparationInput,\n role: string = input.role,\n): Promise<EnsuredFactorySourceSession | undefined> {\n const ref = input.item.sessions[role];\n if (!ref) return undefined;\n // At least as strict as the coordinator's resolveSourceSession: a ref it\n // would reject must fall through to minting, not hard-fail the run.\n const session = await sourceControl.sessions.getBySessionId(ref.sessionId);\n if (\n !session?.projectRepositoryId ||\n session.orgId !== input.record.orgId ||\n session.factoryProjectId !== input.record.factoryProjectId\n ) {\n return undefined;\n }\n return {\n sessionId: session.sessionId,\n userId: session.userId,\n projectRepositoryId: session.projectRepositoryId,\n branch: session.branch,\n baseBranch: session.baseBranch,\n };\n}\n\n/**\n * Work cards planned before Planning moved into the `work` context hold the\n * plan's session under `plan`. Building continues in it, on the same branch and\n * repository a fresh session would use, so those cards keep the plan's context.\n */\nasync function legacyPlanSession(\n sourceControl: SourceControlStorageHandle,\n input: FactoryBindingPreparationInput,\n boardId: string,\n branch: string,\n projectRepositoryId: string,\n): Promise<EnsuredFactorySourceSession | undefined> {\n const legacyRole = legacyPlanRoleFor(boardId, input.role);\n if (!legacyRole) return undefined;\n const session = await reuseBoundSession(sourceControl, input, legacyRole);\n if (\n session &&\n (session.branch === branch || session.branch === legacyGithubWorkItemBranch(branch)) &&\n session.projectRepositoryId === projectRepositoryId\n ) {\n return session;\n }\n return undefined;\n}\n\n/**\n * The lane a run started on a resting card moves it to. Work's phases share\n * contexts, so its roles keep their own lanes: `work` builds and the old `plan`\n * role plans. Other boards use the first phase that runs in the role's context.\n */\nfunction restingExitLane(\n board: { phaseForRole(role: string): string | undefined } | undefined,\n role: string,\n): string | undefined {\n return workLaneForRole(board, role) ?? board?.phaseForRole(role);\n}\n\n/**\n * Start a factory run for a rule binding: ensure the source-control session the\n * coordinator requires, then hand it to `prepare` along with the factory's\n * default model. Exported for tests — this is the autonomous entry point with no\n * browser and no interactive user, so nothing else would catch a regression in\n * what it forwards.\n */\nexport async function prepareFactoryRuleBinding(\n sourceControlProvider: SourceControlStorageHandle | Pick<GithubIntegration, 'sourceControlStorage'>,\n coordinator: Pick<FactoryStartCoordinator, 'prepare'>,\n projects: FactoryProjectsStorage,\n boards: BoardRegistry,\n input: FactoryBindingPreparationInput,\n intake?: Pick<IntakeStorage, 'getConfig'>,\n): Promise<void> {\n try {\n const sourceControl =\n 'sourceControlStorage' in sourceControlProvider\n ? sourceControlProvider.sourceControlStorage\n : sourceControlProvider;\n const source = workItemBranchSource(input.item.externalSource);\n const branch = workItemBranch({ id: input.item.id, source, metadata: input.item.metadata });\n // Only leaving a resting phase derives a lane from the role: roles don't own lanes,\n // and the Done close-out running in the triage seat must not drag the card back.\n // Both the current phase and the role's lane come from the installed board; an\n // unknown board or phase yields no destination rather than a guessed one.\n const boardId = boardForWorkItem(input.item);\n const board = boards.get(boardId);\n const currentStage = input.item.stages.length === 1 ? input.item.stages[0] : undefined;\n const currentKind = currentStage === undefined ? undefined : board?.phaseKind(currentStage);\n const destinationStage =\n currentKind === undefined\n ? undefined\n : currentKind === 'resting'\n ? restingExitLane(board, input.role)\n : currentStage;\n if (!destinationStage) {\n throw new FactoryDispatchError(\n 'unsupported_provider_item',\n `Factory skill invocation has no destination lane (role \"${input.role}\", stages [${input.item.stages.join(', ')}]).`,\n );\n }\n const connections = await sourceControl.connections.list({\n orgId: input.record.orgId,\n factoryProjectId: input.record.factoryProjectId,\n });\n if (!connections.some(connection => connection.integrationId === sourceControl.integrationId)) {\n throw new FactoryDispatchError('source_control_missing', 'Factory source-control connection not found.');\n }\n const project = await projects.getById({ id: input.record.factoryProjectId });\n if (!project || project.orgId !== input.record.orgId) {\n throw new FactoryDispatchError(\n 'session_unavailable',\n `Factory model policy is unavailable for project ${input.record.factoryProjectId}.`,\n );\n }\n // A retry may already own a role session. Check it before treating a\n // repository-less card as ambiguous, but never override an explicit signal.\n const boundSession = await reuseBoundSession(sourceControl, input);\n const intakeConfig = await intake?.getConfig({ orgId: input.record.orgId, integrationIds: ['linear'] });\n let repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: input.record.orgId,\n factoryProjectId: input.record.factoryProjectId,\n item: input.item,\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous' && boundSession) {\n const link = await sourceControl.projectRepositories.get({\n orgId: input.record.orgId,\n id: boundSession.projectRepositoryId,\n });\n const linked =\n link && (await sourceControl.repositories.get({ orgId: input.record.orgId, id: link.repositoryId }));\n if (linked && repository.candidates.includes(linked.slug)) {\n repository = { status: 'resolved', slug: linked.slug, projectRepositoryId: link.id };\n }\n }\n if (repository.status !== 'resolved') {\n const detail =\n repository.status === 'ambiguous'\n ? `Choose one of the linked repositories: ${repository.candidates.join(', ')}.`\n : repository.hint;\n throw new FactoryDispatchError('source_repository_ambiguous', detail);\n }\n // Re-preparing a binding (server restart, retired controller session) must\n // land in the role's existing session: minting a replacement would repoint\n // the work item, flip the session's owner to the approver, and orphan the\n // previous sandbox.\n const approver = input.record.approvedBy ?? undefined;\n if (boundSession && boundSession.projectRepositoryId !== repository.projectRepositoryId) {\n throw new FactoryDispatchError(\n 'source_repository_ambiguous',\n `The existing session is bound to a different repository than ${repository.slug}. Choose the correct repository and retry.`,\n );\n }\n // The session is filed under the card's repository: the sandbox holds\n // every environment repository, but the coordinator, the PR tools and the\n // subscriptions follow the session's link, and the card names which one.\n const preparedSession =\n boundSession ??\n (await legacyPlanSession(sourceControl, input, boardId, branch, repository.projectRepositoryId)) ??\n (await ensureFactorySourceSession({\n sourceControl,\n orgId: input.record.orgId,\n factoryProjectId: input.record.factoryProjectId,\n projectRepositoryId: repository.projectRepositoryId,\n branch,\n // A person who approved the run is its interactive user: attribute it to\n // them, not the repo connector. An agent's pre-approval names no person.\n attributeToUserId: isAgentActor(approver) ? undefined : approver,\n }).catch(error => {\n if (error instanceof FactorySourceSessionResolutionError && error.reason === 'repository') {\n // The card resolved to a linked repository the sandbox will not check out.\n throw new FactoryDispatchError(\n 'source_repository_missing',\n `Repository ${repository.slug} is linked but not pre-cloned for this Factory. Turn on Pre-clone under Settings → Repositories, then retry.`,\n { cause: error },\n );\n }\n if (error instanceof SessionBranchHeldError) {\n throw new FactoryDispatchError(\n 'session_unavailable',\n `${error.message} The run cannot start until branch ${branch} is free for this repository.`,\n { cause: error },\n );\n }\n if (!(error instanceof SessionOwnershipMismatchError)) throw error;\n throw new FactoryDispatchError(\n 'session_unavailable',\n `Session ${error.sessionId} on branch ${branch} predates factory ownership and cannot be reused. Delete that session, then retry.`,\n { cause: error },\n );\n }));\n\n const modelScope = effectiveModelScope(project.modelScope, 'run');\n await coordinator.prepare({\n orgId: input.record.orgId,\n userId: preparedSession.userId,\n factoryProjectId: input.record.factoryProjectId,\n sessionId: preparedSession.sessionId,\n modelScope,\n defaultModelId: modelScope === 'factory' ? (project.defaultModelId ?? undefined) : undefined,\n threadTitle: workItemThreadTitle({ source, title: input.item.title, metadata: input.item.metadata }),\n kickoffKey: input.record.id,\n destinationStage,\n workItem: {\n id: input.item.id,\n role: input.role,\n input: {\n externalSource: input.item.externalSource,\n parentWorkItemId: input.item.parentWorkItemId,\n title: input.item.title,\n stages: ['intake'],\n sessions: input.item.sessions,\n metadata: input.item.metadata,\n },\n },\n });\n } catch (error) {\n if (error instanceof FactoryDispatchError) throw error;\n if (error instanceof FactorySourceSessionResolutionError) {\n const code = error.reason === 'connection' ? 'source_control_missing' : 'source_repository_missing';\n throw new FactoryDispatchError(code, error.message, { cause: error });\n }\n if (error instanceof SourceControlConnectionNotFoundError) {\n throw new FactoryDispatchError('source_control_missing', error.message, { cause: error });\n }\n if (error instanceof MaterializeError) {\n throw new FactoryDispatchError(MATERIALIZE_FAILURE_CODE[error.code], error.message, { cause: error });\n }\n throw error;\n }\n}\n\n/**\n * Build the {@link IntegrationContext} handed to an integration when the\n * factory collects its capabilities (routes, workers). One shape everywhere:\n * `assembleFactoryApiRoutes` uses it per registration, and `MastraFactory` uses it\n * when collecting integration workers at finalize.\n */\nexport function buildIntegrationContext(\n deps: Pick<\n FactoryApiRoutesDeps,\n | 'controller'\n | 'publicOrigin'\n | 'auth'\n | 'sandbox'\n | 'users'\n | 'factoryStorage'\n | 'integrationStorage'\n | 'sourceControlStorage'\n | 'integrations'\n > & {\n stateSigner: StateSigner;\n emitAudit?: AuditEmitter['emit'];\n onRepositoryPush?: IntegrationHooks['onRepositoryPush'];\n configVersion: string;\n boardRegistry: BoardRegistry;\n factoryReady: boolean;\n /** Work-item feed service, so a channel integration can ingest platform messages. */\n feed: CommentsDomain;\n domains: Pick<\n FactoryApiRoutesDeps['domains'],\n 'projects' | 'intake' | 'workItems' | 'channelIdentity' | 'memorySettings' | 'modelDefaults'\n >;\n /**\n * Stable id of the registered source-control-owning integration (today:\n * `'github'` when registered). Every call site must derive and pass it so\n * `routes()`, `channels()`, and `workers()` all see the same context shape.\n */\n sourceControlOwnerId?: string;\n },\n integrationId: string,\n): IntegrationContext {\n return {\n auth: deps.auth,\n sandbox: deps.sandbox,\n ...(deps.users ? { users: deps.users } : {}),\n factoryStorage: deps.factoryStorage,\n baseUrl: deps.publicOrigin,\n controller: deps.controller,\n stateSigner: deps.stateSigner,\n storage: {\n generic: deps.integrationStorage.forIntegration(integrationId),\n sourceControl: deps.sourceControlStorage.forIntegration(integrationId),\n ...(deps.sourceControlOwnerId\n ? { sourceControlOwner: deps.sourceControlStorage.forIntegration(deps.sourceControlOwnerId) }\n : {}),\n sourceControls: (deps.integrations ?? [])\n .filter(({ integration }) => integration.versionControl)\n .map(({ integration }) => deps.sourceControlStorage.forIntegration(integration.id)),\n projects: deps.domains.projects,\n intake: deps.domains.intake,\n channelIdentity: deps.domains.channelIdentity,\n memorySettings: deps.domains.memorySettings,\n modelDefaults: deps.domains.modelDefaults,\n },\n ...(deps.factoryReady ? { workItems: deps.domains.workItems, feed: deps.feed } : {}),\n ...(deps.factoryReady\n ? {\n runtime: { configVersion: deps.configVersion, workItems: deps.domains.workItems, boards: deps.boardRegistry },\n }\n : {}),\n ...(deps.emitAudit || deps.onRepositoryPush\n ? {\n hooks: {\n ...(deps.emitAudit ? { emitAudit: deps.emitAudit } : {}),\n ...(deps.onRepositoryPush ? { onRepositoryPush: deps.onRepositoryPush } : {}),\n },\n }\n : {}),\n };\n}\n\n/**\n * Disabled-status stub for the well-known integration ids. The SPA polls\n * `/web/github/status`, `/web/linear/status`, and `/web/jira/status`\n * unconditionally, so when an integration is absent (or not ready) the status\n * contract must still hold. Unknown custom ids get no stub — the SPA doesn't\n * poll them.\n */\nfunction disabledIntegrationStatusRoutes(deps: FactoryApiRoutesDeps, id: string, configured = false): ApiRoute[] {\n if (id === 'github') {\n return [\n registerApiRoute('/web/github/status', {\n method: 'GET',\n requiresAuth: false,\n handler: c =>\n c.json({\n enabled: false,\n connected: false,\n installations: [],\n reason: 'missing_config',\n diagnostics: getGithubFeatureDiagnostics({\n github: undefined,\n auth: deps.auth,\n appDbConfigured: deps.factoryStorage !== undefined,\n stateSigner: deps.stateSigner,\n sandbox: deps.sandbox,\n }),\n }),\n }),\n ];\n }\n if (id === 'linear') {\n return [\n registerApiRoute('/web/linear/status', {\n method: 'GET',\n requiresAuth: false,\n handler: c =>\n c.json({\n enabled: false,\n connected: false,\n workspace: null,\n reason: 'missing_config',\n diagnostics: {\n linearAppConfigured: configured,\n factoryAuthEnabled: deps.auth.enabled(),\n appDbConfigured: true,\n },\n }),\n }),\n ];\n }\n if (id === 'jira') {\n return [\n registerApiRoute('/web/jira/status', {\n method: 'GET',\n requiresAuth: false,\n handler: c =>\n c.json({\n enabled: false,\n configured,\n site: null,\n reason: 'missing_config',\n diagnostics: {\n jiraConfigured: configured,\n factoryAuthEnabled: deps.auth.enabled(),\n appDbConfigured: deps.factoryStorage !== undefined,\n },\n }),\n }),\n ];\n }\n return [];\n}\n\n/**\n * Stub for `GET /web/channel-accounts` when NO Slack integration is\n * registered. The SPA's Connections section polls the path unconditionally;\n * without a stub the SPA fallback serves HTML, which the UI can only read as\n * \"old server / unknown\". The machine-readable reason lets it say the truth:\n * the integration isn't registered.\n *\n * Mounted only for ABSENT slack — a registered integration owns the path via\n * its connect routes (or, when the state signer is unstable, gets no routes\n * at all and the UI falls back to the generic copy). Static payload, leaks\n * nothing → no auth needed, same posture as the github/linear stubs.\n */\nfunction absentSlackChannelAccountsRoutes(): ApiRoute[] {\n return [\n registerApiRoute('/web/channel-accounts', {\n method: 'GET',\n requiresAuth: false,\n handler: c => c.json({ accounts: [], canConnect: false, reason: 'not_registered' }),\n }),\n ];\n}\n\n/**\n * Assemble the custom `/web/*` API routes as Mastra `server.apiRoutes`:\n * - fs browser routes (project picker), confined to `fsRoot`\n * - config routes (provider/API-key/default-model/OM management)\n * - every registered integration's `routes()` surface (full set when ready,\n * disabled-status stub otherwise), plus stubs for absent known ids\n */\nexport function assembleFactoryApiRoutes(deps: FactoryApiRoutesDeps): ApiRoute[] {\n const emitAudit: AuditEmitter['emit'] = args => deps.audit.emit(args);\n const registrations = deps.integrations ?? [];\n const githubRegistration = registrations.find(({ integration }) => integration.id === 'github');\n const githubStorage = githubRegistration ? deps.sourceControlStorage.forIntegration('github') : undefined;\n const githubIntegration = githubRegistration?.integration as GithubIntegration | undefined;\n const sourceControlRegistrations = registrations.filter(({ integration }) => integration.versionControl);\n const sourceControlIntegrationIds = [\n ...new Set(['github', ...sourceControlRegistrations.map(({ integration }) => integration.id)]),\n ];\n const sourceControls = sourceControlIntegrationIds.map(id => deps.sourceControlStorage.forIntegration(id));\n const sourceControlSessions = createSourceControlSessionLookup(sourceControls);\n\n const integrationRoutes = registrations.flatMap(registration => {\n const { integration } = registration;\n if (!deps.stateSigner) return disabledIntegrationStatusRoutes(deps, integration.id, true);\n const context = buildIntegrationContext(\n {\n ...deps,\n stateSigner: deps.stateSigner,\n emitAudit,\n ...(githubRegistration ? { sourceControlOwnerId: 'github' } : {}),\n },\n integration.id,\n );\n return guardIntegrationRoutes({ ...registration, routes: integration.routes(context) });\n });\n // Session persistence belongs to the linked source-control provider, not to\n // GitHub. Replace legacy GitHub-owned handlers with one resolver spanning\n // the legacy GitHub partition and every registered source-control partition.\n const sharedSessionRouteKeys = new Set([\n 'GET /web/github/projects/:id/sessions',\n 'POST /web/github/projects/:id/sessions',\n 'GET /web/user-sessions/:sessionId',\n 'DELETE /web/user-sessions/:sessionId',\n 'POST /web/user-sessions/:sessionId/owner',\n 'POST /web/user-sessions/:sessionId/title',\n ]);\n const providerRoutes = integrationRoutes.filter(\n route => !sharedSessionRouteKeys.has(route.method + ' ' + route.path),\n );\n const sourceControlSessionRoutes =\n sourceControls.length === 0\n ? []\n : buildSourceControlSessionRoutes({\n auth: deps.auth,\n sourceControls,\n projects: deps.domains.projects,\n ...(deps.users ? { users: deps.users } : {}),\n controller: deps.controller,\n audit: deps.audit,\n memorySettings: deps.domains.memorySettings,\n sessionRetirement: deps.sessionRetirement,\n // A local sandbox's destroy keeps the session directory, so recreate\n // couldn't deliver the fresh checkout it promises there.\n sandboxRecreateSupported: deps.sandbox !== undefined && deps.sandbox.provider !== 'local',\n filesystem: deps.domains.filesystem,\n ...(deps.factoryReady ? { workItems: deps.domains.workItems } : {}),\n ...(deps.moveThread ? { moveThread: deps.moveThread } : {}),\n ...(githubStorage ? { githubRepositories: githubStorage.repositories } : {}),\n });\n // Absent known integrations still get their disabled-status stub.\n const absentStubs = ['github', 'linear', 'jira']\n .filter(id => !registrations.some(({ integration }) => integration.id === id))\n .flatMap(id => disabledIntegrationStatusRoutes(deps, id));\n // Absent slack gets the channel-accounts not-registered stub (registered\n // slack owns the path via its own connect routes).\n const slackAbsentStubs = registrations.some(({ integration }) => integration.id === 'slack')\n ? []\n : absentSlackChannelAccountsRoutes();\n\n const transitionService = deps.factoryReady\n ? (deps.factoryTransitionService ??\n new FactoryTransitionService({\n configVersion: deps.configVersion,\n boards: deps.boardRegistry,\n storage: deps.domains.workItems,\n audit: deps.audit,\n }))\n : undefined;\n const startCoordinator = transitionService\n ? new FactoryStartCoordinator(\n deps.controller,\n deps.domains.workItems,\n transitionService,\n request =>\n resolveFactorySourceControl({\n sourceControls,\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n sessionId: request.sessionId,\n }),\n deps.domains.intake,\n { projects: deps.domains.projects, modelDefaults: deps.domains.modelDefaults },\n )\n : undefined;\n if (transitionService && startCoordinator) {\n deps.onFactoryRuntime?.({\n transitionService,\n startCoordinator,\n ...(sourceControlRegistrations.length > 0\n ? {\n prepareBinding: async (input: FactoryBindingPreparationInput) => {\n const sourceControl = await resolveFactorySourceControl({\n sourceControls,\n orgId: input.record.orgId,\n factoryProjectId: input.record.factoryProjectId,\n });\n if (!sourceControl) {\n throw new FactoryDispatchError(\n 'source_control_missing',\n 'Factory project has no linked source-control repository.',\n );\n }\n return prepareFactoryRuleBinding(\n sourceControl,\n startCoordinator,\n deps.domains.projects,\n deps.boardRegistry,\n input,\n deps.domains.intake,\n );\n },\n }\n : {}),\n });\n }\n\n return [\n ...buildFsRoutes({\n root: deps.fsRoot,\n sessionFs: {\n auth: deps.auth,\n sessions: sourceControlSessions,\n },\n }),\n ...new ConfigRoutes({\n auth: deps.auth,\n controller: deps.controller,\n authStorage: deps.authStorage,\n modelCredentials: deps.domains.modelCredentials,\n modelDefaults: deps.domains.modelDefaults,\n sourceControlSessions,\n sourceControls,\n workItems: deps.domains.workItems,\n memorySettings: deps.domains.memorySettings,\n factoryProjects: deps.domains.projects,\n customProviders: deps.domains.customProviders,\n features: { knowledge: deps.knowledgeEnabled, mcp: deps.mcpEnabled },\n deploymentProviders: deps.deploymentModelProviders,\n onCredentialsChanged: invalidateTenantCredentialSnapshots,\n onCustomProvidersChanged: invalidateCustomProvidersSnapshots,\n }).routes(),\n ...new OAuthRoutes({\n auth: deps.auth,\n authStorage: deps.authStorage,\n modelCredentials: deps.domains.modelCredentials,\n onCredentialsChanged: invalidateTenantCredentialSnapshots,\n }).routes(),\n ...new SkillRoutes({\n auth: deps.auth,\n controllerId: deps.controllerId,\n controller: deps.controller,\n sourceControlStorage: githubStorage,\n ensureSourceControlReady: githubRegistration?.ensureReady,\n ...(deps.skills ? { skills: deps.skills } : {}),\n }).routes(),\n ...sourceControlSessionRoutes,\n ...(sourceControls.length === 0 ? [] : buildSourceControlSettingsRoutes({ auth: deps.auth, sourceControls })),\n ...providerRoutes,\n ...absentStubs,\n ...slackAbsentStubs,\n ...(deps.intakeReady\n ? new IntakeRoutes({\n auth: deps.auth,\n audit: deps.audit,\n intake: deps.domains.intake,\n projects: deps.domains.projects,\n boardRegistry: deps.boardRegistry,\n // Relocation reads and writes work items, which init fail-soft separately from intake.\n ...(deps.factoryReady ? { workItems: deps.domains.workItems } : {}),\n integrations: (deps.integrations ?? []).flatMap(({ integration }) =>\n integration.intake ? [{ id: integration.id, intake: integration.intake }] : [],\n ),\n }).routes()\n : []),\n ...(deps.factoryReady && deps.knowledgeEnabled\n ? new KnowledgeRoutes({\n auth: deps.auth,\n projects: deps.domains.projects,\n knowledge: async key => deps.controller.getMastra()?.getKnowledge(key),\n defaultKnowledgeKey: deps.knowledgeKey,\n threadTitle: async threadId => {\n const memory = await deps.controller.getMastra()?.getStorage()?.getStore('memory');\n return (await memory?.getThreadById({ threadId }))?.title;\n },\n }).routes()\n : []),\n ...(deps.factoryReady\n ? new WorkItemRoutes({\n auth: deps.auth,\n audit: deps.audit,\n projects: deps.domains.projects,\n workItems: deps.domains.workItems,\n boardRegistry: deps.boardRegistry,\n comments: deps.domains.comments,\n queueHealth: deps.domains.queueHealth,\n transitionService,\n startCoordinator,\n liveSessions: deps.liveSessions,\n }).routes()\n : []),\n ...(deps.factoryReady\n ? buildAutomationRunRoutes({\n auth: deps.auth,\n audit: deps.audit,\n projects: deps.domains.projects,\n workItems: deps.domains.workItems,\n configVersion: deps.configVersion,\n })\n : []),\n ];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"surface.js","names":[],"sources":["../../src/routes/surface.ts"],"sourcesContent":["import type { AuthStorage } from '@mastra/code-sdk/auth/storage';\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\nimport type { ApiRoute, IUserProvider } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { InlineSkill } from '@mastra/core/skills';\nimport type { FactoryStorage } from '@mastra/core/storage';\nimport type { FactorySandbox } from '@mastra/core/workspace';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { legacyPlanRoleFor, workLaneForRole } from '../boards/legacy-plan-role.js';\nimport { toBoardSource } from '../boards/project-boards.js';\nimport type { ProjectBoardCache, ProjectBoardSource } from '../boards/project-boards.js';\nimport type { FactoryIntegration, IntegrationContext, IntegrationHooks } from '../integrations/base.js';\nimport { getGithubFeatureDiagnostics } from '../integrations/github/config.js';\nimport type { GithubIntegration } from '../integrations/github/integration.js';\nimport { MaterializeError } from '../integrations/github/sandbox.js';\nimport { FactoryDispatchError } from '../rules/dispatch-errors.js';\nimport type { FactoryBindingPreparationInput } from '../rules/dispatcher.js';\nimport { FactoryStartCoordinator } from '../rules/start-coordinator.js';\nimport { FactoryTransitionService } from '../rules/transition-service.js';\nimport {\n createSourceControlSessionLookup,\n ensureFactorySourceSession,\n FactorySourceSessionResolutionError,\n resolveFactorySourceControl,\n} from '../session/factory-session.js';\nimport type { EnsuredFactorySourceSession } from '../session/factory-session.js';\nimport type { LiveSessions } from '../session/live-sessions.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { StateSigner } from '../state-signing.js';\nimport type { AuditEmitter, AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { StoredBoardsStorage } from '../storage/domains/boards/base.js';\nimport type { ChannelIdentityStorage } from '../storage/domains/channel-identity/base.js';\nimport type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';\nimport type { CommentsDomain } from '../storage/domains/comments/domain.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { CustomProvidersStorage } from '../storage/domains/custom-providers/base.js';\nimport type { FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { IntegrationStorage } from '../storage/domains/integrations/base.js';\nimport type { MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { QueueHealthStorage } from '../storage/domains/queue-health/base.js';\nimport {\n SessionBranchHeldError,\n SessionOwnershipMismatchError,\n SourceControlConnectionNotFoundError,\n} from '../storage/domains/source-control/base.js';\nimport type { SourceControlStorage, SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { isAgentActor } from '../storage/domains/work-items/base.js';\nimport type { FactoryDispatchFailureCode, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport {\n legacyGithubWorkItemBranch,\n workItemBranch,\n workItemBranchSource,\n workItemThreadTitle,\n} from '../work-item-branch.js';\nimport { buildAutomationRunRoutes } from './automation-runs.js';\nimport { BoardRoutes } from './boards.js';\nimport { ConfigRoutes } from './config.js';\nimport { invalidateCustomProvidersSnapshots } from './custom-provider-source.js';\nimport { buildFsRoutes } from './fs.js';\nimport { IntakeRoutes } from './intake.js';\nimport { KnowledgeRoutes } from './knowledge.js';\nimport { OAuthRoutes } from './oauth.js';\nimport type { RouteAuth } from './route.js';\nimport { SkillRoutes } from './skills.js';\nimport { buildSourceControlSessionRoutes } from './source-control-sessions.js';\nimport { buildSourceControlSettingsRoutes } from './source-control-settings.js';\nimport { invalidateTenantCredentialSnapshots } from './tenant-credentials.js';\nimport { WorkItemRoutes } from './work-items.js';\n\nconst MATERIALIZE_FAILURE_CODE = {\n 'git-missing': 'repository_git_missing',\n 'sandbox-unresponsive': 'repository_sandbox_unresponsive',\n 'egress-blocked': 'repository_egress_blocked',\n 'clone-failed': 'repository_clone_failed',\n 'pull-failed': 'repository_pull_failed',\n 'push-failed': 'repository_push_failed',\n 'commit-failed': 'repository_commit_failed',\n 'gh-missing': 'repository_cli_missing',\n 'pr-failed': 'repository_pr_failed',\n} satisfies Record<MaterializeError['code'], FactoryDispatchFailureCode>;\nexport interface IntegrationRegistration {\n integration: FactoryIntegration;\n ready: boolean;\n ensureReady: () => Promise<void>;\n}\n\nexport interface FactoryApiRoutesDeps {\n controllerId: string;\n controller: AgentController<MastraCodeState>;\n /** Registry of the sessions this process holds, owned by the host so it can watch them too. */\n liveSessions: LiveSessions;\n /** Request-auth seam threaded from the host (no service locator). */\n auth: RouteAuth;\n /** Optional user directory for resolving persisted owners to display profiles. */\n users?: Pick<IUserProvider, 'getUser' | 'getUsers'>;\n authStorage: AuthStorage;\n audit: AuditEmitter & AuditRecorder;\n fsRoot?: string;\n publicOrigin: string;\n stateSigner?: StateSigner;\n /** Sandbox surface (enablement, provider label, create callback). */\n sandbox?: FactorySandbox;\n /** Push hand-off for environment builds, present when the sandbox can build templates. */\n onRepositoryPush?: IntegrationHooks['onRepositoryPush'];\n /** Root factory storage backend (distributed locks, app-db diagnostics). */\n factoryStorage?: FactoryStorage;\n integrationStorage: IntegrationStorage;\n sourceControlStorage: SourceControlStorage;\n /** App-table domain handles, registered and owned by `MastraFactory.prepare()`. */\n domains: {\n intake: IntakeStorage;\n modelCredentials: ModelCredentialsStorage;\n memorySettings: MemorySettingsStorage;\n customProviders: CustomProvidersStorage;\n filesystem: FilesystemStorage;\n modelDefaults: ModelDefaultsStorage;\n projects: FactoryProjectsStorage;\n queueHealth: QueueHealthStorage;\n workItems: WorkItemsStorage;\n channelIdentity: ChannelIdentityStorage;\n comments: WorkItemCommentsStorage;\n };\n integrations?: IntegrationRegistration[];\n intakeReady: boolean;\n factoryReady: boolean;\n knowledgeEnabled: boolean;\n /** The Factory MCP endpoint is mounted; surfaced so the UI can show its URL. */\n mcpEnabled: boolean;\n /** Providers the operator opted in to run on the server process's own credentials. */\n deploymentModelProviders?: ReadonlySet<string>;\n /** App skills from `MastraFactory({ skills })`. */\n skills?: readonly InlineSkill[];\n knowledgeKey?: string;\n /** Resolved Factory rule set, threaded from the host (no service locator). */\n configVersion: string;\n /** Each project's boards; integrations use the code boards (`codeRegistry`). */\n boardRegistry: BoardRegistry | ProjectBoardSource;\n /** Stored boards and their cache; mounts the board management routes when present. */\n storedBoards?: { storage: StoredBoardsStorage; cache: ProjectBoardCache };\n /** Work-item feed service, handed to integrations that ingest platform messages. */\n feed: CommentsDomain;\n factoryTransitionService?: FactoryTransitionService;\n sessionRetirement?: import('../sandbox/session-retirement.js').SessionRetirementCoordinator;\n /** Moves a thread with its observation vectors onto another memory resource. */\n moveThread?: (input: { threadId: string; resourceId: string }) => Promise<unknown>;\n onFactoryRuntime?: (runtime: {\n transitionService: FactoryTransitionService;\n startCoordinator: FactoryStartCoordinator;\n prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n }) => void;\n}\n\nfunction guardIntegrationRoutes({\n integration,\n ready,\n ensureReady,\n routes,\n}: IntegrationRegistration & { routes: ApiRoute[] }): ApiRoute[] {\n if (ready) return routes;\n return routes.map(route => {\n if ('handler' in route) {\n const handler = route.handler;\n return {\n ...route,\n handler: async (context: Parameters<typeof handler>[0]) => {\n try {\n await ensureReady();\n } catch {\n return context.json(\n { error: 'integration_unavailable', message: `${integration.id} integration is unavailable.` },\n 503,\n );\n }\n return handler(context, async () => {});\n },\n };\n }\n\n const createHandler = route.createHandler;\n return {\n ...route,\n createHandler: async (args: Parameters<typeof createHandler>[0]) => {\n const handler = await createHandler(args);\n return async (context: Parameters<typeof handler>[0]) => {\n try {\n await ensureReady();\n } catch {\n return context.json(\n { error: 'integration_unavailable', message: `${integration.id} integration is unavailable.` },\n 503,\n );\n }\n return handler(context);\n };\n },\n };\n });\n}\n\n/**\n * Resolve the source-control session the work item already holds for this role,\n * when it still resolves to the item's org and factory project. Returns\n * `undefined` when there is no ref or the ref no longer resolves, so the caller\n * falls back to minting one.\n */\nasync function reuseBoundSession(\n sourceControl: SourceControlStorageHandle,\n input: FactoryBindingPreparationInput,\n role: string = input.role,\n): Promise<EnsuredFactorySourceSession | undefined> {\n const ref = input.item.sessions[role];\n if (!ref) return undefined;\n // At least as strict as the coordinator's resolveSourceSession: a ref it\n // would reject must fall through to minting, not hard-fail the run.\n const session = await sourceControl.sessions.getBySessionId(ref.sessionId);\n if (\n !session?.projectRepositoryId ||\n session.orgId !== input.record.orgId ||\n session.factoryProjectId !== input.record.factoryProjectId\n ) {\n return undefined;\n }\n return {\n sessionId: session.sessionId,\n userId: session.userId,\n projectRepositoryId: session.projectRepositoryId,\n branch: session.branch,\n baseBranch: session.baseBranch,\n };\n}\n\n/**\n * Work cards planned before Planning moved into the `work` context hold the\n * plan's session under `plan`. Building continues in it, on the same branch and\n * repository a fresh session would use, so those cards keep the plan's context.\n */\nasync function legacyPlanSession(\n sourceControl: SourceControlStorageHandle,\n input: FactoryBindingPreparationInput,\n boardId: string,\n branch: string,\n projectRepositoryId: string,\n): Promise<EnsuredFactorySourceSession | undefined> {\n const legacyRole = legacyPlanRoleFor(boardId, input.role);\n if (!legacyRole) return undefined;\n const session = await reuseBoundSession(sourceControl, input, legacyRole);\n if (\n session &&\n (session.branch === branch || session.branch === legacyGithubWorkItemBranch(branch)) &&\n session.projectRepositoryId === projectRepositoryId\n ) {\n return session;\n }\n return undefined;\n}\n\n/**\n * The lane a run started on a resting card moves it to. Work's phases share\n * contexts, so its roles keep their own lanes: `work` builds and the old `plan`\n * role plans. Other boards use the first phase that runs in the role's context.\n */\nfunction restingExitLane(\n board: { phaseForRole(role: string): string | undefined } | undefined,\n role: string,\n): string | undefined {\n return workLaneForRole(board, role) ?? board?.phaseForRole(role);\n}\n\n/**\n * Start a factory run for a rule binding: ensure the source-control session the\n * coordinator requires, then hand it to `prepare` along with the factory's\n * default model. Exported for tests — this is the autonomous entry point with no\n * browser and no interactive user, so nothing else would catch a regression in\n * what it forwards.\n */\nexport async function prepareFactoryRuleBinding(\n sourceControlProvider: SourceControlStorageHandle | Pick<GithubIntegration, 'sourceControlStorage'>,\n coordinator: Pick<FactoryStartCoordinator, 'prepare'>,\n projects: FactoryProjectsStorage,\n boards: BoardRegistry | ProjectBoardSource,\n input: FactoryBindingPreparationInput,\n intake?: Pick<IntakeStorage, 'getConfig'>,\n): Promise<void> {\n try {\n const sourceControl =\n 'sourceControlStorage' in sourceControlProvider\n ? sourceControlProvider.sourceControlStorage\n : sourceControlProvider;\n const source = workItemBranchSource(input.item.externalSource);\n const branch = workItemBranch({ id: input.item.id, source, metadata: input.item.metadata });\n // Only leaving a resting phase derives a lane from the role: roles don't own lanes,\n // and the Done close-out running in the triage seat must not drag the card back.\n // Both the current phase and the role's lane come from the installed board; an\n // unknown board or phase yields no destination rather than a guessed one.\n const boardId = boardForWorkItem(input.item);\n const board = (await toBoardSource(boards).load(input.record)).get(boardId);\n const currentStage = input.item.stages.length === 1 ? input.item.stages[0] : undefined;\n const currentKind = currentStage === undefined ? undefined : board?.phaseKind(currentStage);\n const destinationStage =\n currentKind === undefined\n ? undefined\n : currentKind === 'resting'\n ? restingExitLane(board, input.role)\n : currentStage;\n if (!destinationStage) {\n throw new FactoryDispatchError(\n 'unsupported_provider_item',\n `Factory skill invocation has no destination lane (role \"${input.role}\", stages [${input.item.stages.join(', ')}]).`,\n );\n }\n const connections = await sourceControl.connections.list({\n orgId: input.record.orgId,\n factoryProjectId: input.record.factoryProjectId,\n });\n if (!connections.some(connection => connection.integrationId === sourceControl.integrationId)) {\n throw new FactoryDispatchError('source_control_missing', 'Factory source-control connection not found.');\n }\n const project = await projects.getById({ id: input.record.factoryProjectId });\n if (!project || project.orgId !== input.record.orgId) {\n throw new FactoryDispatchError(\n 'session_unavailable',\n `Factory model policy is unavailable for project ${input.record.factoryProjectId}.`,\n );\n }\n // A retry may already own a role session. Check it before treating a\n // repository-less card as ambiguous, but never override an explicit signal.\n const boundSession = await reuseBoundSession(sourceControl, input);\n const intakeConfig = await intake?.getConfig({ orgId: input.record.orgId, integrationIds: ['linear'] });\n let repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: input.record.orgId,\n factoryProjectId: input.record.factoryProjectId,\n item: input.item,\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous' && boundSession) {\n const link = await sourceControl.projectRepositories.get({\n orgId: input.record.orgId,\n id: boundSession.projectRepositoryId,\n });\n const linked =\n link && (await sourceControl.repositories.get({ orgId: input.record.orgId, id: link.repositoryId }));\n if (linked && repository.candidates.includes(linked.slug)) {\n repository = { status: 'resolved', slug: linked.slug, projectRepositoryId: link.id };\n }\n }\n if (repository.status !== 'resolved') {\n const detail =\n repository.status === 'ambiguous'\n ? `Choose one of the linked repositories: ${repository.candidates.join(', ')}.`\n : repository.hint;\n throw new FactoryDispatchError('source_repository_ambiguous', detail);\n }\n // Re-preparing a binding (server restart, retired controller session) must\n // land in the role's existing session: minting a replacement would repoint\n // the work item, flip the session's owner to the approver, and orphan the\n // previous sandbox.\n const approver = input.record.approvedBy ?? undefined;\n if (boundSession && boundSession.projectRepositoryId !== repository.projectRepositoryId) {\n throw new FactoryDispatchError(\n 'source_repository_ambiguous',\n `The existing session is bound to a different repository than ${repository.slug}. Choose the correct repository and retry.`,\n );\n }\n // The session is filed under the card's repository: the sandbox holds\n // every environment repository, but the coordinator, the PR tools and the\n // subscriptions follow the session's link, and the card names which one.\n const preparedSession =\n boundSession ??\n (await legacyPlanSession(sourceControl, input, boardId, branch, repository.projectRepositoryId)) ??\n (await ensureFactorySourceSession({\n sourceControl,\n orgId: input.record.orgId,\n factoryProjectId: input.record.factoryProjectId,\n projectRepositoryId: repository.projectRepositoryId,\n branch,\n // A person who approved the run is its interactive user: attribute it to\n // them, not the repo connector. An agent's pre-approval names no person.\n attributeToUserId: isAgentActor(approver) ? undefined : approver,\n }).catch(error => {\n if (error instanceof FactorySourceSessionResolutionError && error.reason === 'repository') {\n // The card resolved to a linked repository the sandbox will not check out.\n throw new FactoryDispatchError(\n 'source_repository_missing',\n `Repository ${repository.slug} is linked but not pre-cloned for this Factory. Turn on Pre-clone under Settings → Repositories, then retry.`,\n { cause: error },\n );\n }\n if (error instanceof SessionBranchHeldError) {\n throw new FactoryDispatchError(\n 'session_unavailable',\n `${error.message} The run cannot start until branch ${branch} is free for this repository.`,\n { cause: error },\n );\n }\n if (!(error instanceof SessionOwnershipMismatchError)) throw error;\n throw new FactoryDispatchError(\n 'session_unavailable',\n `Session ${error.sessionId} on branch ${branch} predates factory ownership and cannot be reused. Delete that session, then retry.`,\n { cause: error },\n );\n }));\n\n const modelScope = effectiveModelScope(project.modelScope, 'run');\n await coordinator.prepare({\n orgId: input.record.orgId,\n userId: preparedSession.userId,\n factoryProjectId: input.record.factoryProjectId,\n sessionId: preparedSession.sessionId,\n modelScope,\n defaultModelId: modelScope === 'factory' ? (project.defaultModelId ?? undefined) : undefined,\n threadTitle: workItemThreadTitle({ source, title: input.item.title, metadata: input.item.metadata }),\n kickoffKey: input.record.id,\n destinationStage,\n workItem: {\n id: input.item.id,\n role: input.role,\n input: {\n externalSource: input.item.externalSource,\n parentWorkItemId: input.item.parentWorkItemId,\n title: input.item.title,\n stages: ['intake'],\n sessions: input.item.sessions,\n metadata: input.item.metadata,\n },\n },\n });\n } catch (error) {\n if (error instanceof FactoryDispatchError) throw error;\n if (error instanceof FactorySourceSessionResolutionError) {\n const code = error.reason === 'connection' ? 'source_control_missing' : 'source_repository_missing';\n throw new FactoryDispatchError(code, error.message, { cause: error });\n }\n if (error instanceof SourceControlConnectionNotFoundError) {\n throw new FactoryDispatchError('source_control_missing', error.message, { cause: error });\n }\n if (error instanceof MaterializeError) {\n throw new FactoryDispatchError(MATERIALIZE_FAILURE_CODE[error.code], error.message, { cause: error });\n }\n throw error;\n }\n}\n\n/**\n * Build the {@link IntegrationContext} handed to an integration when the\n * factory collects its capabilities (routes, workers). One shape everywhere:\n * `assembleFactoryApiRoutes` uses it per registration, and `MastraFactory` uses it\n * when collecting integration workers at finalize.\n */\nexport function buildIntegrationContext(\n deps: Pick<\n FactoryApiRoutesDeps,\n | 'controller'\n | 'publicOrigin'\n | 'auth'\n | 'sandbox'\n | 'users'\n | 'factoryStorage'\n | 'integrationStorage'\n | 'sourceControlStorage'\n | 'integrations'\n > & {\n stateSigner: StateSigner;\n emitAudit?: AuditEmitter['emit'];\n onRepositoryPush?: IntegrationHooks['onRepositoryPush'];\n configVersion: string;\n boardRegistry: BoardRegistry;\n factoryReady: boolean;\n /** Work-item feed service, so a channel integration can ingest platform messages. */\n feed: CommentsDomain;\n domains: Pick<\n FactoryApiRoutesDeps['domains'],\n 'projects' | 'intake' | 'workItems' | 'channelIdentity' | 'memorySettings' | 'modelDefaults'\n >;\n /**\n * Stable id of the registered source-control-owning integration (today:\n * `'github'` when registered). Every call site must derive and pass it so\n * `routes()`, `channels()`, and `workers()` all see the same context shape.\n */\n sourceControlOwnerId?: string;\n },\n integrationId: string,\n): IntegrationContext {\n return {\n auth: deps.auth,\n sandbox: deps.sandbox,\n ...(deps.users ? { users: deps.users } : {}),\n factoryStorage: deps.factoryStorage,\n baseUrl: deps.publicOrigin,\n controller: deps.controller,\n stateSigner: deps.stateSigner,\n storage: {\n generic: deps.integrationStorage.forIntegration(integrationId),\n sourceControl: deps.sourceControlStorage.forIntegration(integrationId),\n ...(deps.sourceControlOwnerId\n ? { sourceControlOwner: deps.sourceControlStorage.forIntegration(deps.sourceControlOwnerId) }\n : {}),\n sourceControls: (deps.integrations ?? [])\n .filter(({ integration }) => integration.versionControl)\n .map(({ integration }) => deps.sourceControlStorage.forIntegration(integration.id)),\n projects: deps.domains.projects,\n intake: deps.domains.intake,\n channelIdentity: deps.domains.channelIdentity,\n memorySettings: deps.domains.memorySettings,\n modelDefaults: deps.domains.modelDefaults,\n },\n ...(deps.factoryReady ? { workItems: deps.domains.workItems, feed: deps.feed } : {}),\n ...(deps.factoryReady\n ? {\n runtime: { configVersion: deps.configVersion, workItems: deps.domains.workItems, boards: deps.boardRegistry },\n }\n : {}),\n ...(deps.emitAudit || deps.onRepositoryPush\n ? {\n hooks: {\n ...(deps.emitAudit ? { emitAudit: deps.emitAudit } : {}),\n ...(deps.onRepositoryPush ? { onRepositoryPush: deps.onRepositoryPush } : {}),\n },\n }\n : {}),\n };\n}\n\n/**\n * Disabled-status stub for the well-known integration ids. The SPA polls\n * `/web/github/status`, `/web/linear/status`, and `/web/jira/status`\n * unconditionally, so when an integration is absent (or not ready) the status\n * contract must still hold. Unknown custom ids get no stub — the SPA doesn't\n * poll them.\n */\nfunction disabledIntegrationStatusRoutes(deps: FactoryApiRoutesDeps, id: string, configured = false): ApiRoute[] {\n if (id === 'github') {\n return [\n registerApiRoute('/web/github/status', {\n method: 'GET',\n requiresAuth: false,\n handler: c =>\n c.json({\n enabled: false,\n connected: false,\n installations: [],\n reason: 'missing_config',\n diagnostics: getGithubFeatureDiagnostics({\n github: undefined,\n auth: deps.auth,\n appDbConfigured: deps.factoryStorage !== undefined,\n stateSigner: deps.stateSigner,\n sandbox: deps.sandbox,\n }),\n }),\n }),\n ];\n }\n if (id === 'linear') {\n return [\n registerApiRoute('/web/linear/status', {\n method: 'GET',\n requiresAuth: false,\n handler: c =>\n c.json({\n enabled: false,\n connected: false,\n workspace: null,\n reason: 'missing_config',\n diagnostics: {\n linearAppConfigured: configured,\n factoryAuthEnabled: deps.auth.enabled(),\n appDbConfigured: true,\n },\n }),\n }),\n ];\n }\n if (id === 'jira') {\n return [\n registerApiRoute('/web/jira/status', {\n method: 'GET',\n requiresAuth: false,\n handler: c =>\n c.json({\n enabled: false,\n configured,\n site: null,\n reason: 'missing_config',\n diagnostics: {\n jiraConfigured: configured,\n factoryAuthEnabled: deps.auth.enabled(),\n appDbConfigured: deps.factoryStorage !== undefined,\n },\n }),\n }),\n ];\n }\n return [];\n}\n\n/**\n * Stub for `GET /web/channel-accounts` when NO Slack integration is\n * registered. The SPA's Connections section polls the path unconditionally;\n * without a stub the SPA fallback serves HTML, which the UI can only read as\n * \"old server / unknown\". The machine-readable reason lets it say the truth:\n * the integration isn't registered.\n *\n * Mounted only for ABSENT slack — a registered integration owns the path via\n * its connect routes (or, when the state signer is unstable, gets no routes\n * at all and the UI falls back to the generic copy). Static payload, leaks\n * nothing → no auth needed, same posture as the github/linear stubs.\n */\nfunction absentSlackChannelAccountsRoutes(): ApiRoute[] {\n return [\n registerApiRoute('/web/channel-accounts', {\n method: 'GET',\n requiresAuth: false,\n handler: c => c.json({ accounts: [], canConnect: false, reason: 'not_registered' }),\n }),\n ];\n}\n\n/**\n * Assemble the custom `/web/*` API routes as Mastra `server.apiRoutes`:\n * - fs browser routes (project picker), confined to `fsRoot`\n * - config routes (provider/API-key/default-model/OM management)\n * - every registered integration's `routes()` surface (full set when ready,\n * disabled-status stub otherwise), plus stubs for absent known ids\n */\nexport function assembleFactoryApiRoutes(deps: FactoryApiRoutesDeps): ApiRoute[] {\n const emitAudit: AuditEmitter['emit'] = args => deps.audit.emit(args);\n const registrations = deps.integrations ?? [];\n const githubRegistration = registrations.find(({ integration }) => integration.id === 'github');\n const githubStorage = githubRegistration ? deps.sourceControlStorage.forIntegration('github') : undefined;\n const githubIntegration = githubRegistration?.integration as GithubIntegration | undefined;\n const sourceControlRegistrations = registrations.filter(({ integration }) => integration.versionControl);\n const sourceControlIntegrationIds = [\n ...new Set(['github', ...sourceControlRegistrations.map(({ integration }) => integration.id)]),\n ];\n const sourceControls = sourceControlIntegrationIds.map(id => deps.sourceControlStorage.forIntegration(id));\n const sourceControlSessions = createSourceControlSessionLookup(sourceControls);\n\n const integrationRoutes = registrations.flatMap(registration => {\n const { integration } = registration;\n if (!deps.stateSigner) return disabledIntegrationStatusRoutes(deps, integration.id, true);\n const context = buildIntegrationContext(\n {\n ...deps,\n // Integrations evaluate code behaviors and place new cards with the code boards.\n boardRegistry: toBoardSource(deps.boardRegistry).codeRegistry,\n stateSigner: deps.stateSigner,\n emitAudit,\n ...(githubRegistration ? { sourceControlOwnerId: 'github' } : {}),\n },\n integration.id,\n );\n return guardIntegrationRoutes({ ...registration, routes: integration.routes(context) });\n });\n // Session persistence belongs to the linked source-control provider, not to\n // GitHub. Replace legacy GitHub-owned handlers with one resolver spanning\n // the legacy GitHub partition and every registered source-control partition.\n const sharedSessionRouteKeys = new Set([\n 'GET /web/github/projects/:id/sessions',\n 'POST /web/github/projects/:id/sessions',\n 'GET /web/user-sessions/:sessionId',\n 'DELETE /web/user-sessions/:sessionId',\n 'POST /web/user-sessions/:sessionId/owner',\n 'POST /web/user-sessions/:sessionId/title',\n ]);\n const providerRoutes = integrationRoutes.filter(\n route => !sharedSessionRouteKeys.has(route.method + ' ' + route.path),\n );\n const sourceControlSessionRoutes =\n sourceControls.length === 0\n ? []\n : buildSourceControlSessionRoutes({\n auth: deps.auth,\n sourceControls,\n projects: deps.domains.projects,\n ...(deps.users ? { users: deps.users } : {}),\n controller: deps.controller,\n audit: deps.audit,\n memorySettings: deps.domains.memorySettings,\n sessionRetirement: deps.sessionRetirement,\n // A local sandbox's destroy keeps the session directory, so recreate\n // couldn't deliver the fresh checkout it promises there.\n sandboxRecreateSupported: deps.sandbox !== undefined && deps.sandbox.provider !== 'local',\n filesystem: deps.domains.filesystem,\n ...(deps.factoryReady ? { workItems: deps.domains.workItems } : {}),\n ...(deps.moveThread ? { moveThread: deps.moveThread } : {}),\n ...(githubStorage ? { githubRepositories: githubStorage.repositories } : {}),\n });\n // Absent known integrations still get their disabled-status stub.\n const absentStubs = ['github', 'linear', 'jira']\n .filter(id => !registrations.some(({ integration }) => integration.id === id))\n .flatMap(id => disabledIntegrationStatusRoutes(deps, id));\n // Absent slack gets the channel-accounts not-registered stub (registered\n // slack owns the path via its own connect routes).\n const slackAbsentStubs = registrations.some(({ integration }) => integration.id === 'slack')\n ? []\n : absentSlackChannelAccountsRoutes();\n\n const transitionService = deps.factoryReady\n ? (deps.factoryTransitionService ??\n new FactoryTransitionService({\n configVersion: deps.configVersion,\n boards: deps.boardRegistry,\n storage: deps.domains.workItems,\n audit: deps.audit,\n }))\n : undefined;\n const startCoordinator = transitionService\n ? new FactoryStartCoordinator(\n deps.controller,\n deps.domains.workItems,\n transitionService,\n request =>\n resolveFactorySourceControl({\n sourceControls,\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n sessionId: request.sessionId,\n }),\n deps.domains.intake,\n { projects: deps.domains.projects, modelDefaults: deps.domains.modelDefaults },\n )\n : undefined;\n if (transitionService && startCoordinator) {\n deps.onFactoryRuntime?.({\n transitionService,\n startCoordinator,\n ...(sourceControlRegistrations.length > 0\n ? {\n prepareBinding: async (input: FactoryBindingPreparationInput) => {\n const sourceControl = await resolveFactorySourceControl({\n sourceControls,\n orgId: input.record.orgId,\n factoryProjectId: input.record.factoryProjectId,\n });\n if (!sourceControl) {\n throw new FactoryDispatchError(\n 'source_control_missing',\n 'Factory project has no linked source-control repository.',\n );\n }\n return prepareFactoryRuleBinding(\n sourceControl,\n startCoordinator,\n deps.domains.projects,\n deps.boardRegistry,\n input,\n deps.domains.intake,\n );\n },\n }\n : {}),\n });\n }\n\n return [\n ...buildFsRoutes({\n root: deps.fsRoot,\n sessionFs: {\n auth: deps.auth,\n sessions: sourceControlSessions,\n },\n }),\n ...new ConfigRoutes({\n auth: deps.auth,\n controller: deps.controller,\n authStorage: deps.authStorage,\n modelCredentials: deps.domains.modelCredentials,\n modelDefaults: deps.domains.modelDefaults,\n sourceControlSessions,\n sourceControls,\n workItems: deps.domains.workItems,\n memorySettings: deps.domains.memorySettings,\n factoryProjects: deps.domains.projects,\n customProviders: deps.domains.customProviders,\n features: { knowledge: deps.knowledgeEnabled, mcp: deps.mcpEnabled },\n deploymentProviders: deps.deploymentModelProviders,\n onCredentialsChanged: invalidateTenantCredentialSnapshots,\n onCustomProvidersChanged: invalidateCustomProvidersSnapshots,\n }).routes(),\n ...new OAuthRoutes({\n auth: deps.auth,\n authStorage: deps.authStorage,\n modelCredentials: deps.domains.modelCredentials,\n onCredentialsChanged: invalidateTenantCredentialSnapshots,\n }).routes(),\n ...new SkillRoutes({\n auth: deps.auth,\n controllerId: deps.controllerId,\n controller: deps.controller,\n sourceControlStorage: githubStorage,\n ensureSourceControlReady: githubRegistration?.ensureReady,\n ...(deps.skills ? { skills: deps.skills } : {}),\n }).routes(),\n ...sourceControlSessionRoutes,\n ...(sourceControls.length === 0 ? [] : buildSourceControlSettingsRoutes({ auth: deps.auth, sourceControls })),\n ...providerRoutes,\n ...absentStubs,\n ...slackAbsentStubs,\n ...(deps.intakeReady\n ? new IntakeRoutes({\n auth: deps.auth,\n audit: deps.audit,\n intake: deps.domains.intake,\n projects: deps.domains.projects,\n boardRegistry: deps.boardRegistry,\n // Relocation reads and writes work items, which init fail-soft separately from intake.\n ...(deps.factoryReady ? { workItems: deps.domains.workItems } : {}),\n integrations: (deps.integrations ?? []).flatMap(({ integration }) =>\n integration.intake ? [{ id: integration.id, intake: integration.intake }] : [],\n ),\n }).routes()\n : []),\n ...(deps.factoryReady && deps.knowledgeEnabled\n ? new KnowledgeRoutes({\n auth: deps.auth,\n projects: deps.domains.projects,\n knowledge: async key => deps.controller.getMastra()?.getKnowledge(key),\n defaultKnowledgeKey: deps.knowledgeKey,\n threadTitle: async threadId => {\n const memory = await deps.controller.getMastra()?.getStorage()?.getStore('memory');\n return (await memory?.getThreadById({ threadId }))?.title;\n },\n }).routes()\n : []),\n ...(deps.factoryReady\n ? new WorkItemRoutes({\n auth: deps.auth,\n audit: deps.audit,\n projects: deps.domains.projects,\n workItems: deps.domains.workItems,\n boardRegistry: deps.boardRegistry,\n comments: deps.domains.comments,\n queueHealth: deps.domains.queueHealth,\n transitionService,\n startCoordinator,\n liveSessions: deps.liveSessions,\n }).routes()\n : []),\n ...(deps.factoryReady && deps.storedBoards\n ? new BoardRoutes({\n auth: deps.auth,\n audit: deps.audit,\n projects: deps.domains.projects,\n workItems: deps.domains.workItems,\n storage: deps.storedBoards.storage,\n boards: deps.storedBoards.cache,\n }).routes()\n : []),\n ...(deps.factoryReady\n ? buildAutomationRunRoutes({\n auth: deps.auth,\n audit: deps.audit,\n projects: deps.domains.projects,\n workItems: deps.domains.workItems,\n configVersion: deps.configVersion,\n })\n : []),\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,MAAM,2BAA2B;CAC/B,eAAe;CACf,wBAAwB;CACxB,kBAAkB;CAClB,gBAAgB;CAChB,eAAe;CACf,eAAe;CACf,iBAAiB;CACjB,cAAc;CACd,aAAa;AACf;AAyEA,SAAS,uBAAuB,EAC9B,aACA,OACA,aACA,UAC+D;CAC/D,IAAI,OAAO,OAAO;CAClB,OAAO,OAAO,KAAI,UAAS;EACzB,IAAI,aAAa,OAAO;GACtB,MAAM,UAAU,MAAM;GACtB,OAAO;IACL,GAAG;IACH,SAAS,OAAO,YAA2C;KACzD,IAAI;MACF,MAAM,YAAY;KACpB,QAAQ;MACN,OAAO,QAAQ,KACb;OAAE,OAAO;OAA2B,SAAS,GAAG,YAAY,GAAG;MAA8B,GAC7F,GACF;KACF;KACA,OAAO,QAAQ,SAAS,YAAY,CAAC,CAAC;IACxC;GACF;EACF;EAEA,MAAM,gBAAgB,MAAM;EAC5B,OAAO;GACL,GAAG;GACH,eAAe,OAAO,SAA8C;IAClE,MAAM,UAAU,MAAM,cAAc,IAAI;IACxC,OAAO,OAAO,YAA2C;KACvD,IAAI;MACF,MAAM,YAAY;KACpB,QAAQ;MACN,OAAO,QAAQ,KACb;OAAE,OAAO;OAA2B,SAAS,GAAG,YAAY,GAAG;MAA8B,GAC7F,GACF;KACF;KACA,OAAO,QAAQ,OAAO;IACxB;GACF;EACF;CACF,CAAC;AACH;;;;;;;AAQA,eAAe,kBACb,eACA,OACA,OAAe,MAAM,MAC6B;CAClD,MAAM,MAAM,MAAM,KAAK,SAAS;CAChC,IAAI,CAAC,KAAK,OAAO,KAAA;CAGjB,MAAM,UAAU,MAAM,cAAc,SAAS,eAAe,IAAI,SAAS;CACzE,IACE,CAAC,SAAS,uBACV,QAAQ,UAAU,MAAM,OAAO,SAC/B,QAAQ,qBAAqB,MAAM,OAAO,kBAE1C;CAEF,OAAO;EACL,WAAW,QAAQ;EACnB,QAAQ,QAAQ;EAChB,qBAAqB,QAAQ;EAC7B,QAAQ,QAAQ;EAChB,YAAY,QAAQ;CACtB;AACF;;;;;;AAOA,eAAe,kBACb,eACA,OACA,SACA,QACA,qBACkD;CAClD,MAAM,aAAa,kBAAkB,SAAS,MAAM,IAAI;CACxD,IAAI,CAAC,YAAY,OAAO,KAAA;CACxB,MAAM,UAAU,MAAM,kBAAkB,eAAe,OAAO,UAAU;CACxE,IACE,YACC,QAAQ,WAAW,UAAU,QAAQ,WAAW,2BAA2B,MAAM,MAClF,QAAQ,wBAAwB,qBAEhC,OAAO;AAGX;;;;;;AAOA,SAAS,gBACP,OACA,MACoB;CACpB,OAAO,gBAAgB,OAAO,IAAI,KAAK,OAAO,aAAa,IAAI;AACjE;;;;;;;;AASA,eAAsB,0BACpB,uBACA,aACA,UACA,QACA,OACA,QACe;CACf,IAAI;EACF,MAAM,gBACJ,0BAA0B,wBACtB,sBAAsB,uBACtB;EACN,MAAM,SAAS,qBAAqB,MAAM,KAAK,cAAc;EAC7D,MAAM,SAAS,eAAe;GAAE,IAAI,MAAM,KAAK;GAAI;GAAQ,UAAU,MAAM,KAAK;EAAS,CAAC;EAK1F,MAAM,UAAU,iBAAiB,MAAM,IAAI;EAC3C,MAAM,SAAS,MAAM,cAAc,MAAM,CAAC,CAAC,KAAK,MAAM,MAAM,EAAA,CAAG,IAAI,OAAO;EAC1E,MAAM,eAAe,MAAM,KAAK,OAAO,WAAW,IAAI,MAAM,KAAK,OAAO,KAAK,KAAA;EAC7E,MAAM,cAAc,iBAAiB,KAAA,IAAY,KAAA,IAAY,OAAO,UAAU,YAAY;EAC1F,MAAM,mBACJ,gBAAgB,KAAA,IACZ,KAAA,IACA,gBAAgB,YACd,gBAAgB,OAAO,MAAM,IAAI,IACjC;EACR,IAAI,CAAC,kBACH,MAAM,IAAI,qBACR,6BACA,2DAA2D,MAAM,KAAK,aAAa,MAAM,KAAK,OAAO,KAAK,IAAI,EAAE,IAClH;EAMF,IAAI,EAAC,MAJqB,cAAc,YAAY,KAAK;GACvD,OAAO,MAAM,OAAO;GACpB,kBAAkB,MAAM,OAAO;EACjC,CAAC,EAAA,CACgB,MAAK,eAAc,WAAW,kBAAkB,cAAc,aAAa,GAC1F,MAAM,IAAI,qBAAqB,0BAA0B,8CAA8C;EAEzG,MAAM,UAAU,MAAM,SAAS,QAAQ,EAAE,IAAI,MAAM,OAAO,iBAAiB,CAAC;EAC5E,IAAI,CAAC,WAAW,QAAQ,UAAU,MAAM,OAAO,OAC7C,MAAM,IAAI,qBACR,uBACA,mDAAmD,MAAM,OAAO,iBAAiB,EACnF;EAIF,MAAM,eAAe,MAAM,kBAAkB,eAAe,KAAK;EACjE,MAAM,eAAe,MAAM,QAAQ,UAAU;GAAE,OAAO,MAAM,OAAO;GAAO,gBAAgB,CAAC,QAAQ;EAAE,CAAC;EACtG,IAAI,aAAa,MAAM,0BAA0B;GAC/C;GACA,OAAO,MAAM,OAAO;GACpB,kBAAkB,MAAM,OAAO;GAC/B,MAAM,MAAM;GACZ,qBAAqB,cAAc,QAAQ;EAC7C,CAAC;EACD,IAAI,WAAW,WAAW,eAAe,cAAc;GACrD,MAAM,OAAO,MAAM,cAAc,oBAAoB,IAAI;IACvD,OAAO,MAAM,OAAO;IACpB,IAAI,aAAa;GACnB,CAAC;GACD,MAAM,SACJ,QAAS,MAAM,cAAc,aAAa,IAAI;IAAE,OAAO,MAAM,OAAO;IAAO,IAAI,KAAK;GAAa,CAAC;GACpG,IAAI,UAAU,WAAW,WAAW,SAAS,OAAO,IAAI,GACtD,aAAa;IAAE,QAAQ;IAAY,MAAM,OAAO;IAAM,qBAAqB,KAAK;GAAG;EAEvF;EACA,IAAI,WAAW,WAAW,YAAY;GACpC,MAAM,SACJ,WAAW,WAAW,cAClB,0CAA0C,WAAW,WAAW,KAAK,IAAI,EAAE,KAC3E,WAAW;GACjB,MAAM,IAAI,qBAAqB,+BAA+B,MAAM;EACtE;EAKA,MAAM,WAAW,MAAM,OAAO,cAAc,KAAA;EAC5C,IAAI,gBAAgB,aAAa,wBAAwB,WAAW,qBAClE,MAAM,IAAI,qBACR,+BACA,gEAAgE,WAAW,KAAK,2CAClF;EAKF,MAAM,kBACJ,gBACC,MAAM,kBAAkB,eAAe,OAAO,SAAS,QAAQ,WAAW,mBAAmB,KAC7F,MAAM,2BAA2B;GAChC;GACA,OAAO,MAAM,OAAO;GACpB,kBAAkB,MAAM,OAAO;GAC/B,qBAAqB,WAAW;GAChC;GAGA,mBAAmB,aAAa,QAAQ,IAAI,KAAA,IAAY;EAC1D,CAAC,CAAC,CAAC,OAAM,UAAS;GAChB,IAAI,iBAAiB,uCAAuC,MAAM,WAAW,cAE3E,MAAM,IAAI,qBACR,6BACA,cAAc,WAAW,KAAK,+GAC9B,EAAE,OAAO,MAAM,CACjB;GAEF,IAAI,iBAAiB,wBACnB,MAAM,IAAI,qBACR,uBACA,GAAG,MAAM,QAAQ,qCAAqC,OAAO,gCAC7D,EAAE,OAAO,MAAM,CACjB;GAEF,IAAI,EAAE,iBAAiB,gCAAgC,MAAM;GAC7D,MAAM,IAAI,qBACR,uBACA,WAAW,MAAM,UAAU,aAAa,OAAO,qFAC/C,EAAE,OAAO,MAAM,CACjB;EACF,CAAC;EAEH,MAAM,aAAa,oBAAoB,QAAQ,YAAY,KAAK;EAChE,MAAM,YAAY,QAAQ;GACxB,OAAO,MAAM,OAAO;GACpB,QAAQ,gBAAgB;GACxB,kBAAkB,MAAM,OAAO;GAC/B,WAAW,gBAAgB;GAC3B;GACA,gBAAgB,eAAe,YAAa,QAAQ,kBAAkB,KAAA,IAAa,KAAA;GACnF,aAAa,oBAAoB;IAAE;IAAQ,OAAO,MAAM,KAAK;IAAO,UAAU,MAAM,KAAK;GAAS,CAAC;GACnG,YAAY,MAAM,OAAO;GACzB;GACA,UAAU;IACR,IAAI,MAAM,KAAK;IACf,MAAM,MAAM;IACZ,OAAO;KACL,gBAAgB,MAAM,KAAK;KAC3B,kBAAkB,MAAM,KAAK;KAC7B,OAAO,MAAM,KAAK;KAClB,QAAQ,CAAC,QAAQ;KACjB,UAAU,MAAM,KAAK;KACrB,UAAU,MAAM,KAAK;IACvB;GACF;EACF,CAAC;CACH,SAAS,OAAO;EACd,IAAI,iBAAiB,sBAAsB,MAAM;EACjD,IAAI,iBAAiB,qCAAqC;GACxD,MAAM,OAAO,MAAM,WAAW,eAAe,2BAA2B;GACxE,MAAM,IAAI,qBAAqB,MAAM,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;EACtE;EACA,IAAI,iBAAiB,sCACnB,MAAM,IAAI,qBAAqB,0BAA0B,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;EAE1F,IAAI,iBAAiB,kBACnB,MAAM,IAAI,qBAAqB,yBAAyB,MAAM,OAAO,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;EAEtG,MAAM;CACR;AACF;;;;;;;AAQA,SAAgB,wBACd,MA+BA,eACoB;CACpB,OAAO;EACL,MAAM,KAAK;EACX,SAAS,KAAK;EACd,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;EAC1C,gBAAgB,KAAK;EACrB,SAAS,KAAK;EACd,YAAY,KAAK;EACjB,aAAa,KAAK;EAClB,SAAS;GACP,SAAS,KAAK,mBAAmB,eAAe,aAAa;GAC7D,eAAe,KAAK,qBAAqB,eAAe,aAAa;GACrE,GAAI,KAAK,uBACL,EAAE,oBAAoB,KAAK,qBAAqB,eAAe,KAAK,oBAAoB,EAAE,IAC1F,CAAC;GACL,iBAAiB,KAAK,gBAAgB,CAAC,EAAA,CACpC,QAAQ,EAAE,kBAAkB,YAAY,cAAc,CAAC,CACvD,KAAK,EAAE,kBAAkB,KAAK,qBAAqB,eAAe,YAAY,EAAE,CAAC;GACpF,UAAU,KAAK,QAAQ;GACvB,QAAQ,KAAK,QAAQ;GACrB,iBAAiB,KAAK,QAAQ;GAC9B,gBAAgB,KAAK,QAAQ;GAC7B,eAAe,KAAK,QAAQ;EAC9B;EACA,GAAI,KAAK,eAAe;GAAE,WAAW,KAAK,QAAQ;GAAW,MAAM,KAAK;EAAK,IAAI,CAAC;EAClF,GAAI,KAAK,eACL,EACE,SAAS;GAAE,eAAe,KAAK;GAAe,WAAW,KAAK,QAAQ;GAAW,QAAQ,KAAK;EAAc,EAC9G,IACA,CAAC;EACL,GAAI,KAAK,aAAa,KAAK,mBACvB,EACE,OAAO;GACL,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;GACtD,GAAI,KAAK,mBAAmB,EAAE,kBAAkB,KAAK,iBAAiB,IAAI,CAAC;EAC7E,EACF,IACA,CAAC;CACP;AACF;;;;;;;;AASA,SAAS,gCAAgC,MAA4B,IAAY,aAAa,OAAmB;CAC/G,IAAI,OAAO,UACT,OAAO,CACL,iBAAiB,sBAAsB;EACrC,QAAQ;EACR,cAAc;EACd,UAAS,MACP,EAAE,KAAK;GACL,SAAS;GACT,WAAW;GACX,eAAe,CAAC;GAChB,QAAQ;GACR,aAAa,4BAA4B;IACvC,QAAQ,KAAA;IACR,MAAM,KAAK;IACX,iBAAiB,KAAK,mBAAmB,KAAA;IACzC,aAAa,KAAK;IAClB,SAAS,KAAK;GAChB,CAAC;EACH,CAAC;CACL,CAAC,CACH;CAEF,IAAI,OAAO,UACT,OAAO,CACL,iBAAiB,sBAAsB;EACrC,QAAQ;EACR,cAAc;EACd,UAAS,MACP,EAAE,KAAK;GACL,SAAS;GACT,WAAW;GACX,WAAW;GACX,QAAQ;GACR,aAAa;IACX,qBAAqB;IACrB,oBAAoB,KAAK,KAAK,QAAQ;IACtC,iBAAiB;GACnB;EACF,CAAC;CACL,CAAC,CACH;CAEF,IAAI,OAAO,QACT,OAAO,CACL,iBAAiB,oBAAoB;EACnC,QAAQ;EACR,cAAc;EACd,UAAS,MACP,EAAE,KAAK;GACL,SAAS;GACT;GACA,MAAM;GACN,QAAQ;GACR,aAAa;IACX,gBAAgB;IAChB,oBAAoB,KAAK,KAAK,QAAQ;IACtC,iBAAiB,KAAK,mBAAmB,KAAA;GAC3C;EACF,CAAC;CACL,CAAC,CACH;CAEF,OAAO,CAAC;AACV;;;;;;;;;;;;;AAcA,SAAS,mCAA+C;CACtD,OAAO,CACL,iBAAiB,yBAAyB;EACxC,QAAQ;EACR,cAAc;EACd,UAAS,MAAK,EAAE,KAAK;GAAE,UAAU,CAAC;GAAG,YAAY;GAAO,QAAQ;EAAiB,CAAC;CACpF,CAAC,CACH;AACF;;;;;;;;AASA,SAAgB,yBAAyB,MAAwC;CAC/E,MAAM,aAAkC,SAAQ,KAAK,MAAM,KAAK,IAAI;CACpE,MAAM,gBAAgB,KAAK,gBAAgB,CAAC;CAC5C,MAAM,qBAAqB,cAAc,MAAM,EAAE,kBAAkB,YAAY,OAAO,QAAQ;CAC9F,MAAM,gBAAgB,qBAAqB,KAAK,qBAAqB,eAAe,QAAQ,IAAI,KAAA;CACtE,oBAAoB;CAC9C,MAAM,6BAA6B,cAAc,QAAQ,EAAE,kBAAkB,YAAY,cAAc;CAIvG,MAAM,iBAAiB,CAFrB,mBAAG,IAAI,IAAI,CAAC,UAAU,GAAG,2BAA2B,KAAK,EAAE,kBAAkB,YAAY,EAAE,CAAC,CAAC,CAE9C,CAAC,CAAC,KAAI,OAAM,KAAK,qBAAqB,eAAe,EAAE,CAAC;CACzG,MAAM,wBAAwB,iCAAiC,cAAc;CAE7E,MAAM,oBAAoB,cAAc,SAAQ,iBAAgB;EAC9D,MAAM,EAAE,gBAAgB;EACxB,IAAI,CAAC,KAAK,aAAa,OAAO,gCAAgC,MAAM,YAAY,IAAI,IAAI;EACxF,MAAM,UAAU,wBACd;GACE,GAAG;GAEH,eAAe,cAAc,KAAK,aAAa,CAAC,CAAC;GACjD,aAAa,KAAK;GAClB;GACA,GAAI,qBAAqB,EAAE,sBAAsB,SAAS,IAAI,CAAC;EACjE,GACA,YAAY,EACd;EACA,OAAO,uBAAuB;GAAE,GAAG;GAAc,QAAQ,YAAY,OAAO,OAAO;EAAE,CAAC;CACxF,CAAC;CAID,MAAM,yCAAyB,IAAI,IAAI;EACrC;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CACD,MAAM,iBAAiB,kBAAkB,QACvC,UAAS,CAAC,uBAAuB,IAAI,MAAM,SAAS,MAAM,MAAM,IAAI,CACtE;CACA,MAAM,6BACJ,eAAe,WAAW,IACtB,CAAC,IACD,gCAAgC;EAC9B,MAAM,KAAK;EACX;EACA,UAAU,KAAK,QAAQ;EACvB,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;EAC1C,YAAY,KAAK;EACjB,OAAO,KAAK;EACZ,gBAAgB,KAAK,QAAQ;EAC7B,mBAAmB,KAAK;EAGxB,0BAA0B,KAAK,YAAY,KAAA,KAAa,KAAK,QAAQ,aAAa;EAClF,YAAY,KAAK,QAAQ;EACzB,GAAI,KAAK,eAAe,EAAE,WAAW,KAAK,QAAQ,UAAU,IAAI,CAAC;EACjE,GAAI,KAAK,aAAa,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;EACzD,GAAI,gBAAgB,EAAE,oBAAoB,cAAc,aAAa,IAAI,CAAC;CAC5E,CAAC;CAEP,MAAM,cAAc;EAAC;EAAU;EAAU;CAAM,CAAC,CAC7C,QAAO,OAAM,CAAC,cAAc,MAAM,EAAE,kBAAkB,YAAY,OAAO,EAAE,CAAC,CAAC,CAC7E,SAAQ,OAAM,gCAAgC,MAAM,EAAE,CAAC;CAG1D,MAAM,mBAAmB,cAAc,MAAM,EAAE,kBAAkB,YAAY,OAAO,OAAO,IACvF,CAAC,IACD,iCAAiC;CAErC,MAAM,oBAAoB,KAAK,eAC1B,KAAK,4BACN,IAAI,yBAAyB;EAC3B,eAAe,KAAK;EACpB,QAAQ,KAAK;EACb,SAAS,KAAK,QAAQ;EACtB,OAAO,KAAK;CACd,CAAC,IACD,KAAA;CACJ,MAAM,mBAAmB,oBACrB,IAAI,wBACF,KAAK,YACL,KAAK,QAAQ,WACb,oBACA,YACE,4BAA4B;EAC1B;EACA,OAAO,QAAQ;EACf,kBAAkB,QAAQ;EAC1B,WAAW,QAAQ;CACrB,CAAC,GACH,KAAK,QAAQ,QACb;EAAE,UAAU,KAAK,QAAQ;EAAU,eAAe,KAAK,QAAQ;CAAc,CAC/E,IACA,KAAA;CACJ,IAAI,qBAAqB,kBACvB,KAAK,mBAAmB;EACtB;EACA;EACA,GAAI,2BAA2B,SAAS,IACpC,EACE,gBAAgB,OAAO,UAA0C;GAC/D,MAAM,gBAAgB,MAAM,4BAA4B;IACtD;IACA,OAAO,MAAM,OAAO;IACpB,kBAAkB,MAAM,OAAO;GACjC,CAAC;GACD,IAAI,CAAC,eACH,MAAM,IAAI,qBACR,0BACA,0DACF;GAEF,OAAO,0BACL,eACA,kBACA,KAAK,QAAQ,UACb,KAAK,eACL,OACA,KAAK,QAAQ,MACf;EACF,EACF,IACA,CAAC;CACP,CAAC;CAGH,OAAO;EACL,GAAG,cAAc;GACf,MAAM,KAAK;GACX,WAAW;IACT,MAAM,KAAK;IACX,UAAU;GACZ;EACF,CAAC;EACD,GAAG,IAAI,aAAa;GAClB,MAAM,KAAK;GACX,YAAY,KAAK;GACjB,aAAa,KAAK;GAClB,kBAAkB,KAAK,QAAQ;GAC/B,eAAe,KAAK,QAAQ;GAC5B;GACA;GACA,WAAW,KAAK,QAAQ;GACxB,gBAAgB,KAAK,QAAQ;GAC7B,iBAAiB,KAAK,QAAQ;GAC9B,iBAAiB,KAAK,QAAQ;GAC9B,UAAU;IAAE,WAAW,KAAK;IAAkB,KAAK,KAAK;GAAW;GACnE,qBAAqB,KAAK;GAC1B,sBAAsB;GACtB,0BAA0B;EAC5B,CAAC,CAAC,CAAC,OAAO;EACV,GAAG,IAAI,YAAY;GACjB,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,kBAAkB,KAAK,QAAQ;GAC/B,sBAAsB;EACxB,CAAC,CAAC,CAAC,OAAO;EACV,GAAG,IAAI,YAAY;GACjB,MAAM,KAAK;GACX,cAAc,KAAK;GACnB,YAAY,KAAK;GACjB,sBAAsB;GACtB,0BAA0B,oBAAoB;GAC9C,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;EAC/C,CAAC,CAAC,CAAC,OAAO;EACV,GAAG;EACH,GAAI,eAAe,WAAW,IAAI,CAAC,IAAI,iCAAiC;GAAE,MAAM,KAAK;GAAM;EAAe,CAAC;EAC3G,GAAG;EACH,GAAG;EACH,GAAG;EACH,GAAI,KAAK,cACL,IAAI,aAAa;GACf,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,QAAQ,KAAK,QAAQ;GACrB,UAAU,KAAK,QAAQ;GACvB,eAAe,KAAK;GAEpB,GAAI,KAAK,eAAe,EAAE,WAAW,KAAK,QAAQ,UAAU,IAAI,CAAC;GACjE,eAAe,KAAK,gBAAgB,CAAC,EAAA,CAAG,SAAS,EAAE,kBACjD,YAAY,SAAS,CAAC;IAAE,IAAI,YAAY;IAAI,QAAQ,YAAY;GAAO,CAAC,IAAI,CAAC,CAC/E;EACF,CAAC,CAAC,CAAC,OAAO,IACV,CAAC;EACL,GAAI,KAAK,gBAAgB,KAAK,mBAC1B,IAAI,gBAAgB;GAClB,MAAM,KAAK;GACX,UAAU,KAAK,QAAQ;GACvB,WAAW,OAAM,QAAO,KAAK,WAAW,UAAU,CAAC,EAAE,aAAa,GAAG;GACrE,qBAAqB,KAAK;GAC1B,aAAa,OAAM,aAAY;IAE7B,QAAQ,OAAM,MADO,KAAK,WAAW,UAAU,CAAC,EAAE,WAAW,CAAC,EAAE,SAAS,QAAQ,EAAA,EAC3D,cAAc,EAAE,SAAS,CAAC,EAAA,EAAI;GACtD;EACF,CAAC,CAAC,CAAC,OAAO,IACV,CAAC;EACL,GAAI,KAAK,eACL,IAAI,eAAe;GACjB,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK,QAAQ;GACvB,WAAW,KAAK,QAAQ;GACxB,eAAe,KAAK;GACpB,UAAU,KAAK,QAAQ;GACvB,aAAa,KAAK,QAAQ;GAC1B;GACA;GACA,cAAc,KAAK;EACrB,CAAC,CAAC,CAAC,OAAO,IACV,CAAC;EACL,GAAI,KAAK,gBAAgB,KAAK,eAC1B,IAAI,YAAY;GACd,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK,QAAQ;GACvB,WAAW,KAAK,QAAQ;GACxB,SAAS,KAAK,aAAa;GAC3B,QAAQ,KAAK,aAAa;EAC5B,CAAC,CAAC,CAAC,OAAO,IACV,CAAC;EACL,GAAI,KAAK,eACL,yBAAyB;GACvB,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK,QAAQ;GACvB,WAAW,KAAK,QAAQ;GACxB,eAAe,KAAK;EACtB,CAAC,IACD,CAAC;CACP;AACF"}
@@ -40,6 +40,15 @@ export declare class TenantCredentialStore implements CredentialStore {
40
40
  * failed refresh (caller surfaces a re-login error).
41
41
  */
42
42
  getApiKey(provider: string): Promise<string | undefined>;
43
+ /**
44
+ * Refresh the OAuth credential even though it hasn't expired. The
45
+ * account-rotation processor calls this once when the provider rejects the
46
+ * token (401/403): providers can revoke an access token before the expiry
47
+ * recorded at sign-in. Only the token this store last served is replaced, so
48
+ * when another session or replica already refreshed it the newer token is
49
+ * reused instead of rotating the refresh token again.
50
+ */
51
+ forceRefreshActiveAccount(provider: string): Promise<string | undefined>;
43
52
  }
44
53
  /**
45
54
  * Register the web tenant credential store provider with the SDK. Called by
@@ -1 +1 @@
1
- {"version":3,"file":"tenant-credentials.d.ts","sourceRoot":"","sources":["../../src/routes/tenant-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACV,gBAAgB,IAAI,mBAAmB,EACvC,wBAAwB,EACzB,MAAM,6CAA6C,CAAC;AAOrD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAG9C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AAEtF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAQ5C,qBAAa,qBAAsB,YAAW,eAAe;;IAC3D,QAAQ,CAAC,wBAAwB,SAAS;IAU1C,YACE,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,uBAAuB,GAAG,SAAS,EAChD,KAAK,EAAE,mBAAmB,CAAC,iBAAiB,CAAC,EAC7C,mBAAmB,GAAE,WAAW,CAAC,MAAM,CAAa,EAOrD;IAED,sFAAsF;IACtF,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAErD;IAED,qEAAqE;IAC/D,WAAW,CAAC,GAAG,SAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAMjD;IAeD,kFAAkF;IAClF,MAAM,IAAI,IAAI,CAIb;IAED,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEhD;IAED,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGpD;IAED;;;;;OAKG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmD7D;CACF;AA2BD;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,uBAAuB,EACpC,mBAAmB,GAAE,WAAW,CAAC,MAAM,CAAa,EACpD,aAAa,CAAC,EAAE,wBAAwB,GACvC,IAAI,CAcN;AAED;;;;;GAKG;AACH,wBAAsB,uCAAuC,CAAC,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3G;AAED,uFAAuF;AACvF,wBAAsB,+BAA+B,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhG;AAED;;;;;GAKG;AACH,wBAAsB,iCAAiC,CACrD,MAAM,EAAE,mBAAmB,EAC3B,cAAc,EAAE,MAAM,GAAG,SAAS,GACjC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU7B;AAED,iEAAiE;AACjE,wBAAgB,qCAAqC,IAAI,IAAI,CAK5D;AAED;;;;;GAKG;AACH,wBAAgB,mCAAmC,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASpG;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,EAC3C,MAAM,EACN,WAAW,GACZ,EAAE;IACD,MAAM,EAAE,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,mBAAmB,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC9G,WAAW,EAAE,uBAAuB,CAAC;CACtC,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhB;AAED,wBAAgB,4BAA4B,CAAC,EAC3C,IAAI,EACJ,WAAW,GACZ,EAAE;IACD,IAAI,EAAE,SAAS,CAAC;IAChB,WAAW,EAAE,uBAAuB,CAAC;CACtC,GAAG,iBAAiB,CAgBpB"}
1
+ {"version":3,"file":"tenant-credentials.d.ts","sourceRoot":"","sources":["../../src/routes/tenant-credentials.ts"],"names":[],"mappings":"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"}
@@ -81,7 +81,29 @@ var TenantCredentialStore = class {
81
81
  const oauthProvider = getOAuthProvider(provider);
82
82
  if (!oauthProvider) return void 0;
83
83
  if (!isOAuthCredentialExpired(resolved.credential)) return oauthProvider.getApiKey(resolved.credential);
84
- const rowTenant = resolved.scope === "user" ? {
84
+ return this.#refreshOAuth(storage, resolved.scope, provider, isOAuthCredentialExpired);
85
+ }
86
+ /**
87
+ * Refresh the OAuth credential even though it hasn't expired. The
88
+ * account-rotation processor calls this once when the provider rejects the
89
+ * token (401/403): providers can revoke an access token before the expiry
90
+ * recorded at sign-in. Only the token this store last served is replaced, so
91
+ * when another session or replica already refreshed it the newer token is
92
+ * reused instead of rotating the refresh token again.
93
+ */
94
+ async forceRefreshActiveAccount(provider) {
95
+ const storage = await getTenantCredentialsStorage(this.#credentials);
96
+ if (!storage) return void 0;
97
+ const served = this.#snapshot.get(provider);
98
+ const resolved = await storage.resolveCredential(this.#orgId, this.#userId, provider, this.#scope === "factory" ? "org" : "user");
99
+ if (resolved?.credential.type !== "oauth") return void 0;
100
+ const rejectedAccess = served?.type === "oauth" ? served.access : resolved.credential.access;
101
+ return this.#refreshOAuth(storage, resolved.scope, provider, (current) => isOAuthCredentialExpired(current) || current.access === rejectedAccess);
102
+ }
103
+ async #refreshOAuth(storage, scope, provider, needsRefresh) {
104
+ const oauthProvider = getOAuthProvider(provider);
105
+ if (!oauthProvider) return void 0;
106
+ const rowTenant = scope === "user" ? {
85
107
  orgId: this.#orgId,
86
108
  userId: this.#userId
87
109
  } : { orgId: this.#orgId };
@@ -89,7 +111,7 @@ var TenantCredentialStore = class {
89
111
  const refreshed = await storage.refreshOAuth(rowTenant, provider, async (current) => ({
90
112
  type: "oauth",
91
113
  ...await oauthProvider.refreshToken(current)
92
- }));
114
+ }), needsRefresh);
93
115
  if (!refreshed) return void 0;
94
116
  this.#snapshot.set(provider, refreshed);
95
117
  return oauthProvider.getApiKey(refreshed);
@@ -1 +1 @@
1
- {"version":3,"file":"tenant-credentials.js","names":[],"sources":["../../src/routes/tenant-credentials.ts"],"sourcesContent":["/**\n * Per-tenant credential store for model resolution (deployed mode).\n *\n * The SDK's `resolveModel` asks the registered `CredentialStoreProvider` for a\n * store synchronously, so this module keeps a small per-tenant **snapshot** of\n * credentials from one exclusive scope hydrated from the `model-credentials`\n * domain. The snapshot serves the gateway's synchronous\n * path-selection reads (`get` / `getStoredApiKey`); the fetch-time\n * `getApiKey` is authoritative — it re-resolves against the domain and\n * refreshes expired OAuth tokens under the domain's row lock, so a slightly\n * stale snapshot can never send an expired token upstream.\n *\n * Snapshots are primed per request by `createTenantCredentialPrimer` (mounted\n * after the web auth gate) so the first model call of a request already sees\n * the caller's credentials. This store explicitly disables the SDK's\n * environment fallback so server-shell credentials never leak into tenants.\n */\n\nimport type {\n CredentialTenant as SdkCredentialTenant,\n CredentialTenantResolver,\n} from '@mastra/code-sdk/agents/credential-resolver';\nimport {\n prepareCredentialTenant,\n resolveTenantFromRequestContext,\n setCredentialStoreProvider,\n} from '@mastra/code-sdk/agents/credential-resolver';\nimport { getOAuthProvider, PROVIDER_DEFAULT_MODELS } from '@mastra/code-sdk/auth/storage';\nimport type { AuthCredential, CredentialStore } from '@mastra/code-sdk/auth/types';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { MiddlewareHandler } from 'hono';\n\nimport { isOAuthCredentialExpired } from '../storage/domains/credentials/base.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport { getAuthProviderId, getTenantCredentialsStorage, tenantOrgId } from './provider-credentials.js';\nimport type { RouteAuth } from './route.js';\n\n/** How long a hydrated snapshot is considered fresh. */\nconst SNAPSHOT_TTL_MS = 15_000;\n\n/** Cap on cached tenant stores; oldest-inserted evicted beyond this. */\nconst MAX_CACHED_TENANTS = 1000;\n\nexport class TenantCredentialStore implements CredentialStore {\n readonly allowEnvironmentFallback = false;\n readonly #orgId: string;\n readonly #userId: string;\n readonly #scope: SdkCredentialTenant['credentialScope'];\n readonly #credentials: ModelCredentialsStorage | undefined;\n readonly #deploymentProviders: ReadonlySet<string>;\n #snapshot = new Map<string, AuthCredential>();\n #fetchedAt = 0;\n #hydrating: Promise<void> | undefined;\n\n constructor(\n orgId: string,\n userId: string,\n credentials: ModelCredentialsStorage | undefined,\n scope: SdkCredentialTenant['credentialScope'],\n deploymentProviders: ReadonlySet<string> = new Set(),\n ) {\n this.#orgId = orgId;\n this.#userId = userId;\n this.#credentials = credentials;\n this.#scope = scope;\n this.#deploymentProviders = deploymentProviders;\n }\n\n /** Providers the operator opted in to run on the server process's own credentials. */\n allowsDeploymentCredentials(provider: string): boolean {\n return this.#deploymentProviders.has(provider);\n }\n\n /** Hydrate the snapshot when stale; coalesces concurrent callers. */\n async ensureFresh(now = Date.now()): Promise<void> {\n if (now - this.#fetchedAt < SNAPSHOT_TTL_MS) return;\n this.#hydrating ??= this.#hydrate().finally(() => {\n this.#hydrating = undefined;\n });\n await this.#hydrating;\n }\n\n async #hydrate(): Promise<void> {\n const storage = await getTenantCredentialsStorage(this.#credentials);\n if (!storage) return; // Keep the last tenant-scoped snapshot.\n const records = await storage.listCredentials(this.#orgId, this.#userId);\n const next = new Map<string, AuthCredential>();\n const storageScope = this.#scope === 'factory' ? 'org' : 'user';\n for (const record of records.filter(r => r.scope === storageScope)) {\n next.set(record.provider, record.credential);\n }\n this.#snapshot = next;\n this.#fetchedAt = Date.now();\n }\n\n /** Sync by contract; kicks a background re-hydrate when the snapshot is stale. */\n reload(): void {\n if (Date.now() - this.#fetchedAt >= SNAPSHOT_TTL_MS) {\n void this.ensureFresh().catch(() => {});\n }\n }\n\n get(provider: string): AuthCredential | undefined {\n return this.#snapshot.get(provider);\n }\n\n getStoredApiKey(provider: string): string | undefined {\n const cred = this.#snapshot.get(provider);\n return cred?.type === 'api_key' ? cred.key : undefined;\n }\n\n /**\n * Authoritative fetch-time resolution: re-reads only the selected scope and\n * refreshes expired OAuth tokens under the domain's row lock. Mirrors\n * `AuthStorage.getApiKey` semantics: `undefined` on missing credential or\n * failed refresh (caller surfaces a re-login error).\n */\n async getApiKey(provider: string): Promise<string | undefined> {\n const storage = await getTenantCredentialsStorage(this.#credentials);\n if (!storage) {\n // Domain unavailable: best effort from the snapshot; expired OAuth\n // tokens cannot be refreshed without the domain's lock.\n const cred = this.#snapshot.get(provider);\n if (cred?.type === 'api_key') return cred.key;\n if (cred?.type === 'oauth' && !isOAuthCredentialExpired(cred)) {\n return getOAuthProvider(provider)?.getApiKey(cred);\n }\n return undefined;\n }\n\n const resolved = await storage.resolveCredential(\n this.#orgId,\n this.#userId,\n provider,\n this.#scope === 'factory' ? 'org' : 'user',\n );\n if (!resolved) {\n this.#snapshot.delete(provider);\n return undefined;\n }\n this.#snapshot.set(provider, resolved.credential);\n\n if (resolved.credential.type === 'api_key') {\n return resolved.credential.key;\n }\n\n const oauthProvider = getOAuthProvider(provider);\n if (!oauthProvider) return undefined;\n\n if (!isOAuthCredentialExpired(resolved.credential)) {\n return oauthProvider.getApiKey(resolved.credential);\n }\n\n // Refresh at the scope the credential actually lives at (OAuth rows are\n // user-scoped by policy, but resolve defensively from the record).\n const rowTenant = resolved.scope === 'user' ? { orgId: this.#orgId, userId: this.#userId } : { orgId: this.#orgId };\n try {\n const refreshed = await storage.refreshOAuth(rowTenant, provider, async current => ({\n type: 'oauth' as const,\n ...(await oauthProvider.refreshToken(current)),\n }));\n if (!refreshed) return undefined;\n this.#snapshot.set(provider, refreshed);\n return oauthProvider.getApiKey(refreshed);\n } catch {\n // Refresh failed — user needs to re-login (same posture as AuthStorage).\n return undefined;\n }\n }\n}\n\nconst tenantStores = new Map<string, TenantCredentialStore>();\nlet registeredCredentials: ModelCredentialsStorage | undefined;\nlet registeredDeploymentProviders: ReadonlySet<string> = new Set();\n\nfunction storeFor(tenant: SdkCredentialTenant, credentials: ModelCredentialsStorage): TenantCredentialStore {\n const orgId = tenantOrgId(tenant);\n const key = `${orgId}\\u0000${tenant.userId}\\u0000${tenant.credentialScope}`;\n let store = tenantStores.get(key);\n if (!store) {\n if (tenantStores.size >= MAX_CACHED_TENANTS) {\n const oldest = tenantStores.keys().next().value;\n if (oldest !== undefined) tenantStores.delete(oldest);\n }\n store = new TenantCredentialStore(\n orgId,\n tenant.userId,\n credentials,\n tenant.credentialScope,\n registeredDeploymentProviders,\n );\n tenantStores.set(key, store);\n }\n return store;\n}\n\n/**\n * Register the web tenant credential store provider with the SDK. Called by\n * the factory after storage init with the `model-credentials` domain handle;\n * from then on `resolveModel` uses per-tenant credentials and the SDK skips\n * the `loadStoredApiKeysIntoEnv` env side-channel. `deploymentProviders` are\n * the providers the operator opted in to authenticate with the server\n * process's own credentials (see `MastraFactoryConfig.deploymentModelProviders`).\n */\nexport function registerTenantCredentialResolver(\n credentials: ModelCredentialsStorage,\n deploymentProviders: ReadonlySet<string> = new Set(),\n resolveTenant?: CredentialTenantResolver,\n): void {\n registeredCredentials = credentials;\n registeredDeploymentProviders = deploymentProviders;\n tenantStores.clear();\n setCredentialStoreProvider(\n tenant => storeFor(tenant, credentials),\n resolveTenant\n ? async requestContext => {\n const tenant = await resolveTenant(requestContext);\n if (tenant) await storeFor(tenant, credentials).ensureFresh();\n return tenant;\n }\n : undefined,\n );\n}\n\n/**\n * Prime the tenant stores for a request context built outside the web layer,\n * such as a webhook-triggered session run. The auth middleware does this for\n * HTTP requests; a run that starts without one would otherwise resolve its\n * model against an empty snapshot and fail closed. No-op before registration.\n */\nexport async function primeTenantCredentialsForRequestContext(requestContext: RequestContext): Promise<void> {\n if (!registeredCredentials) return;\n await prepareCredentialTenant(requestContext);\n const tenant = resolveTenantFromRequestContext(requestContext);\n if (!tenant) return;\n await storeFor(tenant, registeredCredentials).ensureFresh();\n}\n\n/** Prime one already-resolved Factory session tenant before entering the agent run. */\nexport async function primeTenantCredentialsForTenant(tenant: SdkCredentialTenant): Promise<void> {\n if (!registeredCredentials) return;\n try {\n await storeFor(tenant, registeredCredentials).ensureFresh();\n } catch (error) {\n console.warn('[factory] Failed to prime scoped tenant model credentials', {\n orgId: tenant.orgId,\n userId: tenant.userId,\n credentialScope: tenant.credentialScope,\n error,\n });\n }\n}\n\n/**\n * The model a session should fall back to when its scope has no chosen default:\n * the provider default (`PROVIDER_DEFAULT_MODELS`) of the first provider the\n * tenant is connected to. Returns `undefined` when `currentModelId` is already\n * usable, when no connected provider has a default, or before registration.\n */\nexport async function resolveTenantProviderDefaultModel(\n tenant: SdkCredentialTenant,\n currentModelId: string | undefined,\n): Promise<string | undefined> {\n if (!registeredCredentials) return undefined;\n const store = storeFor(tenant, registeredCredentials);\n await store.ensureFresh();\n const usable = (modelId: string) => {\n const provider = modelId.split('/')[0] ?? '';\n return store.get(getAuthProviderId(provider)) !== undefined || store.allowsDeploymentCredentials(provider);\n };\n if (currentModelId && usable(currentModelId)) return undefined;\n return Object.entries(PROVIDER_DEFAULT_MODELS).find(([provider]) => store.get(provider) !== undefined)?.[1];\n}\n\n/** Test hook: clear registration and cached tenant snapshots. */\nexport function resetTenantCredentialResolverForTests(): void {\n setCredentialStoreProvider(undefined);\n registeredCredentials = undefined;\n registeredDeploymentProviders = new Set();\n tenantStores.clear();\n}\n\n/**\n * Drop cached snapshots after a credential write so the change is visible to\n * the next model call immediately instead of after the snapshot TTL. An org\n * write affects every member's resolved view, so all stores under the org are\n * invalidated; a user write only drops that user's store.\n */\nexport function invalidateTenantCredentialSnapshots(tenant: { orgId: string; userId?: string }): void {\n if (tenant.userId) {\n tenantStores.delete(`${tenant.orgId}\\u0000${tenant.userId}\\u0000user`);\n tenantStores.delete(`${tenant.orgId}\\u0000${tenant.userId}\\u0000factory`);\n return;\n }\n for (const key of tenantStores.keys()) {\n if (key.startsWith(`${tenant.orgId}\\u0000`)) tenantStores.delete(key);\n }\n}\n\n/**\n * Middleware mounted after the web auth gate: primes both exclusive credential\n * scopes because the HTTP principal is known before the controller session is.\n * Cheap when fresh because each store has a TTL.\n */\nexport async function primeTenantCredentials({\n tenant,\n credentials,\n}: {\n tenant: Pick<SdkCredentialTenant, 'orgId' | 'userId'> & Partial<Pick<SdkCredentialTenant, 'credentialScope'>>;\n credentials: ModelCredentialsStorage;\n}): Promise<void> {\n await Promise.all([\n storeFor({ ...tenant, credentialScope: 'user' }, credentials).ensureFresh(),\n storeFor({ ...tenant, credentialScope: 'factory' }, credentials).ensureFresh(),\n ]);\n}\n\nexport function createTenantCredentialPrimer({\n auth,\n credentials,\n}: {\n auth: RouteAuth;\n credentials: ModelCredentialsStorage;\n}): MiddlewareHandler {\n return async (c, next) => {\n const tenant = auth.tenant(c);\n if (tenant) {\n try {\n await primeTenantCredentials({ tenant, credentials });\n } catch (error) {\n console.warn('[factory] Failed to prime tenant model credentials', {\n orgId: tenant.orgId,\n userId: tenant.userId,\n error,\n });\n }\n }\n await next();\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tenant-credentials.js","names":[],"sources":["../../src/routes/tenant-credentials.ts"],"sourcesContent":["/**\n * Per-tenant credential store for model resolution (deployed mode).\n *\n * The SDK's `resolveModel` asks the registered `CredentialStoreProvider` for a\n * store synchronously, so this module keeps a small per-tenant **snapshot** of\n * credentials from one exclusive scope hydrated from the `model-credentials`\n * domain. The snapshot serves the gateway's synchronous\n * path-selection reads (`get` / `getStoredApiKey`); the fetch-time\n * `getApiKey` is authoritative — it re-resolves against the domain and\n * refreshes expired OAuth tokens under the domain's row lock, so a slightly\n * stale snapshot can never send an expired token upstream.\n *\n * Snapshots are primed per request by `createTenantCredentialPrimer` (mounted\n * after the web auth gate) so the first model call of a request already sees\n * the caller's credentials. This store explicitly disables the SDK's\n * environment fallback so server-shell credentials never leak into tenants.\n */\n\nimport type {\n CredentialTenant as SdkCredentialTenant,\n CredentialTenantResolver,\n} from '@mastra/code-sdk/agents/credential-resolver';\nimport {\n prepareCredentialTenant,\n resolveTenantFromRequestContext,\n setCredentialStoreProvider,\n} from '@mastra/code-sdk/agents/credential-resolver';\nimport { getOAuthProvider, PROVIDER_DEFAULT_MODELS } from '@mastra/code-sdk/auth/storage';\nimport type { AuthCredential, CredentialStore, OAuthCredential } from '@mastra/code-sdk/auth/types';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport type { MiddlewareHandler } from 'hono';\n\nimport { isOAuthCredentialExpired } from '../storage/domains/credentials/base.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport { getAuthProviderId, getTenantCredentialsStorage, tenantOrgId } from './provider-credentials.js';\nimport type { RouteAuth } from './route.js';\n\n/** How long a hydrated snapshot is considered fresh. */\nconst SNAPSHOT_TTL_MS = 15_000;\n\n/** Cap on cached tenant stores; oldest-inserted evicted beyond this. */\nconst MAX_CACHED_TENANTS = 1000;\n\nexport class TenantCredentialStore implements CredentialStore {\n readonly allowEnvironmentFallback = false;\n readonly #orgId: string;\n readonly #userId: string;\n readonly #scope: SdkCredentialTenant['credentialScope'];\n readonly #credentials: ModelCredentialsStorage | undefined;\n readonly #deploymentProviders: ReadonlySet<string>;\n #snapshot = new Map<string, AuthCredential>();\n #fetchedAt = 0;\n #hydrating: Promise<void> | undefined;\n\n constructor(\n orgId: string,\n userId: string,\n credentials: ModelCredentialsStorage | undefined,\n scope: SdkCredentialTenant['credentialScope'],\n deploymentProviders: ReadonlySet<string> = new Set(),\n ) {\n this.#orgId = orgId;\n this.#userId = userId;\n this.#credentials = credentials;\n this.#scope = scope;\n this.#deploymentProviders = deploymentProviders;\n }\n\n /** Providers the operator opted in to run on the server process's own credentials. */\n allowsDeploymentCredentials(provider: string): boolean {\n return this.#deploymentProviders.has(provider);\n }\n\n /** Hydrate the snapshot when stale; coalesces concurrent callers. */\n async ensureFresh(now = Date.now()): Promise<void> {\n if (now - this.#fetchedAt < SNAPSHOT_TTL_MS) return;\n this.#hydrating ??= this.#hydrate().finally(() => {\n this.#hydrating = undefined;\n });\n await this.#hydrating;\n }\n\n async #hydrate(): Promise<void> {\n const storage = await getTenantCredentialsStorage(this.#credentials);\n if (!storage) return; // Keep the last tenant-scoped snapshot.\n const records = await storage.listCredentials(this.#orgId, this.#userId);\n const next = new Map<string, AuthCredential>();\n const storageScope = this.#scope === 'factory' ? 'org' : 'user';\n for (const record of records.filter(r => r.scope === storageScope)) {\n next.set(record.provider, record.credential);\n }\n this.#snapshot = next;\n this.#fetchedAt = Date.now();\n }\n\n /** Sync by contract; kicks a background re-hydrate when the snapshot is stale. */\n reload(): void {\n if (Date.now() - this.#fetchedAt >= SNAPSHOT_TTL_MS) {\n void this.ensureFresh().catch(() => {});\n }\n }\n\n get(provider: string): AuthCredential | undefined {\n return this.#snapshot.get(provider);\n }\n\n getStoredApiKey(provider: string): string | undefined {\n const cred = this.#snapshot.get(provider);\n return cred?.type === 'api_key' ? cred.key : undefined;\n }\n\n /**\n * Authoritative fetch-time resolution: re-reads only the selected scope and\n * refreshes expired OAuth tokens under the domain's row lock. Mirrors\n * `AuthStorage.getApiKey` semantics: `undefined` on missing credential or\n * failed refresh (caller surfaces a re-login error).\n */\n async getApiKey(provider: string): Promise<string | undefined> {\n const storage = await getTenantCredentialsStorage(this.#credentials);\n if (!storage) {\n // Domain unavailable: best effort from the snapshot; expired OAuth\n // tokens cannot be refreshed without the domain's lock.\n const cred = this.#snapshot.get(provider);\n if (cred?.type === 'api_key') return cred.key;\n if (cred?.type === 'oauth' && !isOAuthCredentialExpired(cred)) {\n return getOAuthProvider(provider)?.getApiKey(cred);\n }\n return undefined;\n }\n\n const resolved = await storage.resolveCredential(\n this.#orgId,\n this.#userId,\n provider,\n this.#scope === 'factory' ? 'org' : 'user',\n );\n if (!resolved) {\n this.#snapshot.delete(provider);\n return undefined;\n }\n this.#snapshot.set(provider, resolved.credential);\n\n if (resolved.credential.type === 'api_key') {\n return resolved.credential.key;\n }\n\n const oauthProvider = getOAuthProvider(provider);\n if (!oauthProvider) return undefined;\n\n if (!isOAuthCredentialExpired(resolved.credential)) {\n return oauthProvider.getApiKey(resolved.credential);\n }\n\n return this.#refreshOAuth(storage, resolved.scope, provider, isOAuthCredentialExpired);\n }\n\n /**\n * Refresh the OAuth credential even though it hasn't expired. The\n * account-rotation processor calls this once when the provider rejects the\n * token (401/403): providers can revoke an access token before the expiry\n * recorded at sign-in. Only the token this store last served is replaced, so\n * when another session or replica already refreshed it the newer token is\n * reused instead of rotating the refresh token again.\n */\n async forceRefreshActiveAccount(provider: string): Promise<string | undefined> {\n const storage = await getTenantCredentialsStorage(this.#credentials);\n if (!storage) return undefined;\n const served = this.#snapshot.get(provider);\n const resolved = await storage.resolveCredential(\n this.#orgId,\n this.#userId,\n provider,\n this.#scope === 'factory' ? 'org' : 'user',\n );\n if (resolved?.credential.type !== 'oauth') return undefined;\n const rejectedAccess = served?.type === 'oauth' ? served.access : resolved.credential.access;\n return this.#refreshOAuth(\n storage,\n resolved.scope,\n provider,\n current => isOAuthCredentialExpired(current) || current.access === rejectedAccess,\n );\n }\n\n async #refreshOAuth(\n storage: ModelCredentialsStorage,\n scope: 'user' | 'org',\n provider: string,\n needsRefresh: (current: OAuthCredential) => boolean,\n ): Promise<string | undefined> {\n const oauthProvider = getOAuthProvider(provider);\n if (!oauthProvider) return undefined;\n // Refresh at the scope the credential actually lives at (OAuth rows are\n // user-scoped by policy, but resolve defensively from the record).\n const rowTenant = scope === 'user' ? { orgId: this.#orgId, userId: this.#userId } : { orgId: this.#orgId };\n try {\n const refreshed = await storage.refreshOAuth(\n rowTenant,\n provider,\n async current => ({\n type: 'oauth' as const,\n ...(await oauthProvider.refreshToken(current)),\n }),\n needsRefresh,\n );\n if (!refreshed) return undefined;\n this.#snapshot.set(provider, refreshed);\n return oauthProvider.getApiKey(refreshed);\n } catch {\n // Refresh failed — user needs to re-login (same posture as AuthStorage).\n return undefined;\n }\n }\n}\n\nconst tenantStores = new Map<string, TenantCredentialStore>();\nlet registeredCredentials: ModelCredentialsStorage | undefined;\nlet registeredDeploymentProviders: ReadonlySet<string> = new Set();\n\nfunction storeFor(tenant: SdkCredentialTenant, credentials: ModelCredentialsStorage): TenantCredentialStore {\n const orgId = tenantOrgId(tenant);\n const key = `${orgId}\\u0000${tenant.userId}\\u0000${tenant.credentialScope}`;\n let store = tenantStores.get(key);\n if (!store) {\n if (tenantStores.size >= MAX_CACHED_TENANTS) {\n const oldest = tenantStores.keys().next().value;\n if (oldest !== undefined) tenantStores.delete(oldest);\n }\n store = new TenantCredentialStore(\n orgId,\n tenant.userId,\n credentials,\n tenant.credentialScope,\n registeredDeploymentProviders,\n );\n tenantStores.set(key, store);\n }\n return store;\n}\n\n/**\n * Register the web tenant credential store provider with the SDK. Called by\n * the factory after storage init with the `model-credentials` domain handle;\n * from then on `resolveModel` uses per-tenant credentials and the SDK skips\n * the `loadStoredApiKeysIntoEnv` env side-channel. `deploymentProviders` are\n * the providers the operator opted in to authenticate with the server\n * process's own credentials (see `MastraFactoryConfig.deploymentModelProviders`).\n */\nexport function registerTenantCredentialResolver(\n credentials: ModelCredentialsStorage,\n deploymentProviders: ReadonlySet<string> = new Set(),\n resolveTenant?: CredentialTenantResolver,\n): void {\n registeredCredentials = credentials;\n registeredDeploymentProviders = deploymentProviders;\n tenantStores.clear();\n setCredentialStoreProvider(\n tenant => storeFor(tenant, credentials),\n resolveTenant\n ? async requestContext => {\n const tenant = await resolveTenant(requestContext);\n if (tenant) await storeFor(tenant, credentials).ensureFresh();\n return tenant;\n }\n : undefined,\n );\n}\n\n/**\n * Prime the tenant stores for a request context built outside the web layer,\n * such as a webhook-triggered session run. The auth middleware does this for\n * HTTP requests; a run that starts without one would otherwise resolve its\n * model against an empty snapshot and fail closed. No-op before registration.\n */\nexport async function primeTenantCredentialsForRequestContext(requestContext: RequestContext): Promise<void> {\n if (!registeredCredentials) return;\n await prepareCredentialTenant(requestContext);\n const tenant = resolveTenantFromRequestContext(requestContext);\n if (!tenant) return;\n await storeFor(tenant, registeredCredentials).ensureFresh();\n}\n\n/** Prime one already-resolved Factory session tenant before entering the agent run. */\nexport async function primeTenantCredentialsForTenant(tenant: SdkCredentialTenant): Promise<void> {\n if (!registeredCredentials) return;\n try {\n await storeFor(tenant, registeredCredentials).ensureFresh();\n } catch (error) {\n console.warn('[factory] Failed to prime scoped tenant model credentials', {\n orgId: tenant.orgId,\n userId: tenant.userId,\n credentialScope: tenant.credentialScope,\n error,\n });\n }\n}\n\n/**\n * The model a session should fall back to when its scope has no chosen default:\n * the provider default (`PROVIDER_DEFAULT_MODELS`) of the first provider the\n * tenant is connected to. Returns `undefined` when `currentModelId` is already\n * usable, when no connected provider has a default, or before registration.\n */\nexport async function resolveTenantProviderDefaultModel(\n tenant: SdkCredentialTenant,\n currentModelId: string | undefined,\n): Promise<string | undefined> {\n if (!registeredCredentials) return undefined;\n const store = storeFor(tenant, registeredCredentials);\n await store.ensureFresh();\n const usable = (modelId: string) => {\n const provider = modelId.split('/')[0] ?? '';\n return store.get(getAuthProviderId(provider)) !== undefined || store.allowsDeploymentCredentials(provider);\n };\n if (currentModelId && usable(currentModelId)) return undefined;\n return Object.entries(PROVIDER_DEFAULT_MODELS).find(([provider]) => store.get(provider) !== undefined)?.[1];\n}\n\n/** Test hook: clear registration and cached tenant snapshots. */\nexport function resetTenantCredentialResolverForTests(): void {\n setCredentialStoreProvider(undefined);\n registeredCredentials = undefined;\n registeredDeploymentProviders = new Set();\n tenantStores.clear();\n}\n\n/**\n * Drop cached snapshots after a credential write so the change is visible to\n * the next model call immediately instead of after the snapshot TTL. An org\n * write affects every member's resolved view, so all stores under the org are\n * invalidated; a user write only drops that user's store.\n */\nexport function invalidateTenantCredentialSnapshots(tenant: { orgId: string; userId?: string }): void {\n if (tenant.userId) {\n tenantStores.delete(`${tenant.orgId}\\u0000${tenant.userId}\\u0000user`);\n tenantStores.delete(`${tenant.orgId}\\u0000${tenant.userId}\\u0000factory`);\n return;\n }\n for (const key of tenantStores.keys()) {\n if (key.startsWith(`${tenant.orgId}\\u0000`)) tenantStores.delete(key);\n }\n}\n\n/**\n * Middleware mounted after the web auth gate: primes both exclusive credential\n * scopes because the HTTP principal is known before the controller session is.\n * Cheap when fresh because each store has a TTL.\n */\nexport async function primeTenantCredentials({\n tenant,\n credentials,\n}: {\n tenant: Pick<SdkCredentialTenant, 'orgId' | 'userId'> & Partial<Pick<SdkCredentialTenant, 'credentialScope'>>;\n credentials: ModelCredentialsStorage;\n}): Promise<void> {\n await Promise.all([\n storeFor({ ...tenant, credentialScope: 'user' }, credentials).ensureFresh(),\n storeFor({ ...tenant, credentialScope: 'factory' }, credentials).ensureFresh(),\n ]);\n}\n\nexport function createTenantCredentialPrimer({\n auth,\n credentials,\n}: {\n auth: RouteAuth;\n credentials: ModelCredentialsStorage;\n}): MiddlewareHandler {\n return async (c, next) => {\n const tenant = auth.tenant(c);\n if (tenant) {\n try {\n await primeTenantCredentials({ tenant, credentials });\n } catch (error) {\n console.warn('[factory] Failed to prime tenant model credentials', {\n orgId: tenant.orgId,\n userId: tenant.userId,\n error,\n });\n }\n }\n await next();\n };\n}\n"],"mappings":"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"}
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { ApiRoute } from '@mastra/core/server';
10
10
  import type { BoardRegistry } from '../boards/index.js';
11
+ import type { ProjectBoardSource } from '../boards/project-boards.js';
11
12
  import type { FactoryStartCoordinator } from '../rules/start-coordinator.js';
12
13
  import type { FactoryTransitionService } from '../rules/transition-service.js';
13
14
  import type { LiveSessions } from '../session/live-sessions.js';
@@ -25,7 +26,8 @@ export interface WorkItemRoutesDeps extends RouteDependencies {
25
26
  /** Work-items domain backing the kanban board. */
26
27
  workItems: WorkItemsStorage;
27
28
  /** Boards installed for this Factory instance. */
28
- boardRegistry?: BoardRegistry;
29
+ /** A fixed registry, or the per-project boards. Defaults to the built-in boards. */
30
+ boardRegistry?: BoardRegistry | ProjectBoardSource;
29
31
  /** Comments domain — backs the mention attention provider. */
30
32
  comments: WorkItemCommentsStorage;
31
33
  /** Per-project queue-health threshold config. */
@@ -1 +1 @@
1
- {"version":3,"file":"work-items.d.ts","sourceRoot":"","sources":["../../src/routes/work-items.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAKpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD,OAAO,KAAK,EACV,uBAAuB,EAGxB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAA4B,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAEzG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AAEnF,OAAO,KAAK,EAAuB,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AACvG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAElF,OAAO,KAAK,EACV,mBAAmB,EAEnB,mBAAmB,EAEnB,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAY/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,KAAK,EAAE,YAAY,CAAC;IACpB,yFAAyF;IACzF,QAAQ,EAAE,sBAAsB,CAAC;IACjC,kDAAkD;IAClD,SAAS,EAAE,gBAAgB,CAAC;IAC5B,kDAAkD;IAClD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,8DAA8D;IAC9D,QAAQ,EAAE,uBAAuB,CAAC;IAClC,iDAAiD;IACjD,WAAW,EAAE,kBAAkB,CAAC;IAChC,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,GAAG,eAAe,CAAC,CAAC;IACnF,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC;IAC5D,2GAA2G;IAC3G,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC,CAAC;CACvE;AA2CD,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAG7E;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAG7E;AA+ED,qBAAa,cAAe,SAAQ,KAAK,CAAC,kBAAkB,CAAC;;IAoH3D,gEAAgE;IAChE,MAAM,IAAI,QAAQ,EAAE,CA+ZnB;CACF"}
1
+ {"version":3,"file":"work-items.d.ts","sourceRoot":"","sources":["../../src/routes/work-items.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,EAAE,kBAAkB,EAAsB,MAAM,6BAA6B,CAAC;AAG1F,OAAO,KAAK,EACV,uBAAuB,EAGxB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EAA4B,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAEzG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAEhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AAEnF,OAAO,KAAK,EAAuB,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AACvG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAElF,OAAO,KAAK,EACV,mBAAmB,EAEnB,mBAAmB,EAEnB,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAY/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;IAC3D,KAAK,EAAE,YAAY,CAAC;IACpB,yFAAyF;IACzF,QAAQ,EAAE,sBAAsB,CAAC;IACjC,kDAAkD;IAClD,SAAS,EAAE,gBAAgB,CAAC;IAC5B,kDAAkD;IAClD,oFAAoF;IACpF,aAAa,CAAC,EAAE,aAAa,GAAG,kBAAkB,CAAC;IACnD,8DAA8D;IAC9D,QAAQ,EAAE,uBAAuB,CAAC;IAClC,iDAAiD;IACjD,WAAW,EAAE,kBAAkB,CAAC;IAChC,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,GAAG,eAAe,CAAC,CAAC;IACnF,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,CAAC;IAC5D,2GAA2G;IAC3G,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC,CAAC;CACvE;AA2CD,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAG7E;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAG7E;AAyFD,qBAAa,cAAe,SAAQ,KAAK,CAAC,kBAAkB,CAAC;;IAoH3D,gEAAgE;IAChE,MAAM,IAAI,QAAQ,EAAE,CAwanB;CACF"}
@@ -1,5 +1,4 @@
1
- import { createBoardRegistry } from "../boards/registry.js";
2
- import "../boards/index.js";
1
+ import { toBoardSource } from "../boards/project-boards.js";
3
2
  import { FACTORY_PULL_REQUEST_RECONCILIATION_KEY, FACTORY_RULE_MATERIALIZATION_KEY, WorkItemRelationError, WorkItemUpdateConflictError } from "../storage/domains/work-items/base.js";
4
3
  import { effectiveModelScope } from "../storage/domains/projects/base.js";
5
4
  import { decisionRetryable, overtakenDecisionIds } from "../rules/decision-applicability.js";
@@ -108,10 +107,19 @@ function decisionSummary(boards, decision, overtaken = false) {
108
107
  completedAt: decision.completedAt?.toISOString() ?? null
109
108
  };
110
109
  }
110
+ /** Where a catalog board came from, whether it can be edited, and what is wrong with it. */
111
+ function catalogDetails(details) {
112
+ if (!details) return {};
113
+ return {
114
+ source: details.source,
115
+ editable: details.editable,
116
+ ...details.problems.length > 0 ? { problems: details.problems } : {}
117
+ };
118
+ }
111
119
  var WorkItemRoutes = class extends Route {
112
- #defaultBoards;
120
+ #source;
113
121
  get #boards() {
114
- return this.deps.boardRegistry ?? (this.#defaultBoards ??= createBoardRegistry());
122
+ return this.#source ??= toBoardSource(this.deps.boardRegistry);
115
123
  }
116
124
  /** Resolve the `(orgId, userId)` tenant or a ready-to-return error response. */
117
125
  async #resolveTenant(c) {
@@ -199,7 +207,7 @@ var WorkItemRoutes = class extends Route {
199
207
  });
200
208
  if (!settled.ok) return c.json({ error: settled.code }, 409);
201
209
  const { decision } = settled;
202
- return c.json({ decision: decisionSummary(this.#boards, decision) });
210
+ return c.json({ decision: decisionSummary(await this.#boards.load(resolved), decision) });
203
211
  }
204
212
  });
205
213
  }
@@ -213,37 +221,46 @@ var WorkItemRoutes = class extends Route {
213
221
  handler: async (c) => {
214
222
  const resolved = await this.#resolveProject(loose(c));
215
223
  if ("response" in resolved) return resolved.response;
216
- return c.json({ boards: [...this.#boards].map(([id, board]) => ({
217
- id,
218
- name: board.name,
219
- title: board.name,
220
- ...board.description !== void 0 ? { description: board.description } : {},
221
- initialPhase: board.initialPhase,
222
- phases: Object.entries(board.phases).map(([phaseId, phase]) => ({
223
- id: phaseId,
224
- name: phase.name,
225
- title: phase.name,
226
- ...phase.description !== void 0 ? { description: phase.description } : {},
227
- kind: phase.kind,
228
- nextPhases: [...phase.nextPhases],
229
- ...phase.kind === "working" ? {
230
- role: phase.runner.context,
231
- context: phase.runner.context,
232
- ...phase.runner.skill ? { skill: phase.runner.skill } : {},
233
- autoStart: phase.autoStart === true,
234
- ...phase.autoStartWhen ? { autoStartWhen: phase.autoStartWhen } : {},
235
- ...phase.exitTool ? { exitTool: {
236
- id: phase.exitTool.id,
237
- autoApprove: phase.exitTool.autoApprove,
238
- ...phase.exitTool.autoApproveWhen ? { autoApproveWhen: phase.exitTool.autoApproveWhen } : {}
239
- } } : {}
240
- } : {},
241
- transitions: (board.transitions[phaseId] ?? []).map(({ outcome, to }) => ({
242
- outcome,
243
- to
224
+ const project = this.#boards.describe ? await this.#boards.describe(resolved) : {
225
+ registry: await this.#boards.load(resolved),
226
+ details: void 0,
227
+ problems: []
228
+ };
229
+ return c.json({
230
+ boards: [...project.registry].map(([id, board]) => ({
231
+ id,
232
+ ...catalogDetails(project.details?.get(id)),
233
+ name: board.name,
234
+ title: board.name,
235
+ ...board.description !== void 0 ? { description: board.description } : {},
236
+ initialPhase: board.initialPhase,
237
+ phases: Object.entries(board.phases).map(([phaseId, phase]) => ({
238
+ id: phaseId,
239
+ name: phase.name,
240
+ title: phase.name,
241
+ ...phase.description !== void 0 ? { description: phase.description } : {},
242
+ kind: phase.kind,
243
+ nextPhases: [...phase.nextPhases],
244
+ ...phase.kind === "working" ? {
245
+ role: phase.runner.context,
246
+ context: phase.runner.context,
247
+ ...phase.runner.skill ? { skill: phase.runner.skill } : {},
248
+ autoStart: phase.autoStart === true,
249
+ ...phase.autoStartWhen ? { autoStartWhen: phase.autoStartWhen } : {},
250
+ ...phase.exitTool ? { exitTool: {
251
+ id: phase.exitTool.id,
252
+ autoApprove: phase.exitTool.autoApprove,
253
+ ...phase.exitTool.autoApproveWhen ? { autoApproveWhen: phase.exitTool.autoApproveWhen } : {}
254
+ } } : {}
255
+ } : {},
256
+ transitions: (board.transitions[phaseId] ?? []).map(({ outcome, to }) => ({
257
+ outcome,
258
+ to
259
+ }))
244
260
  }))
245
- }))
246
- })) });
261
+ })),
262
+ ...project.problems.length > 0 ? { problems: [...project.problems] } : {}
263
+ });
247
264
  }
248
265
  }),
249
266
  registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemList.path, {
@@ -253,7 +270,7 @@ var WorkItemRoutes = class extends Route {
253
270
  const resolved = await this.#resolveProject(loose(c));
254
271
  if ("response" in resolved) return resolved.response;
255
272
  await workItems.ensureReady();
256
- const items = await workItems.list({
273
+ const items = await workItems.listShared({
257
274
  orgId: resolved.orgId,
258
275
  factoryProjectId: resolved.factoryProjectId
259
276
  });
@@ -324,9 +341,10 @@ var WorkItemRoutes = class extends Route {
324
341
  limit: query.data.limit
325
342
  });
326
343
  const last = page.decisions.at(-1);
327
- const overtaken = await overtakenDecisionIds(workItems, this.#boards, resolved, page.decisions.filter((decision) => decision.status === "failed"));
344
+ const boards = await this.#boards.load(resolved);
345
+ const overtaken = await overtakenDecisionIds(workItems, boards, resolved, page.decisions.filter((decision) => decision.status === "failed"));
328
346
  return c.json({
329
- decisions: page.decisions.map((decision) => decisionSummary(this.#boards, decision, overtaken.has(decision.id))),
347
+ decisions: page.decisions.map((decision) => decisionSummary(boards, decision, overtaken.has(decision.id))),
330
348
  ...page.hasMore && last ? { nextCursor: encodeDecisionCursor(last) } : {}
331
349
  });
332
350
  }
@@ -353,16 +371,17 @@ var WorkItemRoutes = class extends Route {
353
371
  });
354
372
  if (!parsedPath.success) return c.json({ error: "invalid_decision_id" }, 422);
355
373
  const { decisionId } = parsedPath.data;
374
+ const boards = await this.#boards.load(resolved);
356
375
  const retried = await retryFactoryDecision({
357
376
  workItems,
358
- boards: this.#boards,
377
+ boards,
359
378
  orgId: resolved.orgId,
360
379
  factoryProjectId: resolved.factoryProjectId,
361
380
  decisionId
362
381
  });
363
382
  if (!retried.ok) return c.json({ error: retried.code }, 409);
364
383
  const { decision } = retried;
365
- return c.json({ decision: decisionSummary(this.#boards, decision) });
384
+ return c.json({ decision: decisionSummary(boards, decision) });
366
385
  }
367
386
  }),
368
387
  registerApiRoute(FACTORY_ROUTE_CONTRACTS.workItemCreate.path, {
@@ -378,7 +397,7 @@ var WorkItemRoutes = class extends Route {
378
397
  try {
379
398
  const created = await createFactoryWorkItem({
380
399
  workItems,
381
- boards: this.#boards,
400
+ boards: await this.#boards.load(resolved),
382
401
  transitionService,
383
402
  audit: (auditInput) => audit.emit({
384
403
  context: loose(c),
@@ -506,7 +525,10 @@ var WorkItemRoutes = class extends Route {
506
525
  });
507
526
  if (!existing) return c.json({ error: "Work item not found" }, 404);
508
527
  if (patch.board !== void 0) {
509
- const board = this.#boards.get(patch.board);
528
+ const board = (await this.#boards.load({
529
+ orgId: tenant.orgId,
530
+ factoryProjectId: existing.factoryProjectId
531
+ })).get(patch.board);
510
532
  const stage = patch.stages?.length === 1 ? patch.stages[0] : void 0;
511
533
  if (existing.board !== null) return c.json({ error: "board_already_assigned" }, 409);
512
534
  if (!board) return c.json({ error: "board_not_installed" }, 400);