@mastra/factory 0.15.0 → 0.16.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/boards/index.d.ts +1 -1
- package/dist/boards/index.d.ts.map +1 -1
- package/dist/boards/index.js +2 -2
- package/dist/boards/index.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +4 -1
- package/dist/boards/review.js.map +1 -1
- package/dist/boards/semantics.d.ts +7 -0
- package/dist/boards/semantics.d.ts.map +1 -1
- package/dist/boards/semantics.js +15 -1
- package/dist/boards/semantics.js.map +1 -1
- package/dist/boards/transition-policy.d.ts +4 -0
- package/dist/boards/transition-policy.d.ts.map +1 -1
- package/dist/boards/transition-policy.js.map +1 -1
- package/dist/boards/work-transition-policy.d.ts.map +1 -1
- package/dist/boards/work-transition-policy.js +6 -1
- package/dist/boards/work-transition-policy.js.map +1 -1
- package/dist/boards/work.d.ts.map +1 -1
- package/dist/boards/work.js +37 -3
- package/dist/boards/work.js.map +1 -1
- package/dist/capabilities/intake.d.ts +4 -0
- package/dist/capabilities/intake.d.ts.map +1 -1
- package/dist/factory.d.ts +6 -3
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +66 -18
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +6 -2
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/incidentio/agent-tools.d.ts +49 -0
- package/dist/integrations/incidentio/agent-tools.d.ts.map +1 -0
- package/dist/integrations/incidentio/agent-tools.js +90 -0
- package/dist/integrations/incidentio/agent-tools.js.map +1 -0
- package/dist/integrations/incidentio/claim.d.ts +8 -0
- package/dist/integrations/incidentio/claim.d.ts.map +1 -0
- package/dist/integrations/incidentio/claim.js +14 -0
- package/dist/integrations/incidentio/claim.js.map +1 -0
- package/dist/integrations/incidentio/default-rules.d.ts +46 -0
- package/dist/integrations/incidentio/default-rules.d.ts.map +1 -0
- package/dist/integrations/incidentio/default-rules.js +66 -0
- package/dist/integrations/incidentio/default-rules.js.map +1 -0
- package/dist/integrations/incidentio/integration.d.ts +48 -2
- package/dist/integrations/incidentio/integration.d.ts.map +1 -1
- package/dist/integrations/incidentio/integration.js +88 -2
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/incidentio/issue-reconciler.d.ts +2 -0
- package/dist/integrations/incidentio/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/incidentio/issue-reconciler.js +38 -2
- package/dist/integrations/incidentio/issue-reconciler.js.map +1 -1
- package/dist/integrations/incidentio/routes.d.ts +71 -0
- package/dist/integrations/incidentio/routes.d.ts.map +1 -0
- package/dist/integrations/incidentio/routes.js +245 -0
- package/dist/integrations/incidentio/routes.js.map +1 -0
- package/dist/integrations/incidentio/rules.d.ts +55 -0
- package/dist/integrations/incidentio/rules.d.ts.map +1 -0
- package/dist/integrations/incidentio/rules.js +175 -0
- package/dist/integrations/incidentio/rules.js.map +1 -0
- package/dist/integrations/jira/adf.d.ts +26 -0
- package/dist/integrations/jira/adf.d.ts.map +1 -0
- package/dist/integrations/jira/adf.js +73 -0
- package/dist/integrations/jira/adf.js.map +1 -0
- package/dist/integrations/jira/agent-tools.d.ts +45 -0
- package/dist/integrations/jira/agent-tools.d.ts.map +1 -0
- package/dist/integrations/jira/agent-tools.js +87 -0
- package/dist/integrations/jira/agent-tools.js.map +1 -0
- package/dist/integrations/jira/api.d.ts +157 -0
- package/dist/integrations/jira/api.d.ts.map +1 -0
- package/dist/integrations/jira/api.js +141 -0
- package/dist/integrations/jira/api.js.map +1 -0
- package/dist/integrations/jira/claim.d.ts +9 -0
- package/dist/integrations/jira/claim.d.ts.map +1 -0
- package/dist/integrations/jira/claim.js +15 -0
- package/dist/integrations/jira/claim.js.map +1 -0
- package/dist/integrations/jira/default-rules.d.ts +47 -0
- package/dist/integrations/jira/default-rules.d.ts.map +1 -0
- package/dist/integrations/jira/default-rules.js +67 -0
- package/dist/integrations/jira/default-rules.js.map +1 -0
- package/dist/integrations/jira/integration.d.ts +114 -0
- package/dist/integrations/jira/integration.d.ts.map +1 -0
- package/dist/integrations/jira/integration.js +401 -0
- package/dist/integrations/jira/integration.js.map +1 -0
- package/dist/integrations/jira/issue-reconciler.d.ts +10 -0
- package/dist/integrations/jira/issue-reconciler.d.ts.map +1 -0
- package/dist/integrations/jira/issue-reconciler.js +70 -0
- package/dist/integrations/jira/issue-reconciler.js.map +1 -0
- package/dist/integrations/jira/reconciliation-config.d.ts +3 -0
- package/dist/integrations/jira/reconciliation-config.d.ts.map +1 -0
- package/dist/integrations/jira/reconciliation-config.js +17 -0
- package/dist/integrations/jira/reconciliation-config.js.map +1 -0
- package/dist/integrations/jira/routes.d.ts +54 -0
- package/dist/integrations/jira/routes.d.ts.map +1 -0
- package/dist/integrations/jira/routes.js +260 -0
- package/dist/integrations/jira/routes.js.map +1 -0
- package/dist/integrations/jira/rules.d.ts +57 -0
- package/dist/integrations/jira/rules.d.ts.map +1 -0
- package/dist/integrations/jira/rules.js +175 -0
- package/dist/integrations/jira/rules.js.map +1 -0
- package/dist/integrations/linear/claim.d.ts +8 -0
- package/dist/integrations/linear/claim.d.ts.map +1 -0
- package/dist/integrations/linear/claim.js +14 -0
- package/dist/integrations/linear/claim.js.map +1 -0
- package/dist/integrations/linear/default-rules.d.ts +1 -0
- package/dist/integrations/linear/default-rules.d.ts.map +1 -1
- package/dist/integrations/linear/default-rules.js +2 -0
- package/dist/integrations/linear/default-rules.js.map +1 -1
- package/dist/integrations/linear/integration.d.ts +24 -6
- package/dist/integrations/linear/integration.d.ts.map +1 -1
- package/dist/integrations/linear/integration.js +257 -37
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.d.ts +10 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -1
- package/dist/integrations/linear/routes.js +74 -12
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.d.ts +1 -1
- package/dist/integrations/linear/rules.d.ts.map +1 -1
- package/dist/integrations/linear/rules.js +27 -2
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/connect/routes.d.ts +39 -0
- package/dist/integrations/platform/connect/routes.d.ts.map +1 -0
- package/dist/integrations/platform/connect/routes.js +129 -0
- package/dist/integrations/platform/connect/routes.js.map +1 -0
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/integration.js +1 -1
- package/dist/integrations/platform/incidentio/integration.d.ts +61 -4
- package/dist/integrations/platform/incidentio/integration.d.ts.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +371 -22
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/agent-tools.d.ts +36 -0
- package/dist/integrations/platform/jira/agent-tools.d.ts.map +1 -0
- package/dist/integrations/platform/jira/agent-tools.js +85 -0
- package/dist/integrations/platform/jira/agent-tools.js.map +1 -0
- package/dist/integrations/platform/jira/integration.d.ts +65 -0
- package/dist/integrations/platform/jira/integration.d.ts.map +1 -0
- package/dist/integrations/platform/jira/integration.js +540 -0
- package/dist/integrations/platform/jira/integration.js.map +1 -0
- package/dist/integrations/platform/jira/routes.d.ts +55 -0
- package/dist/integrations/platform/jira/routes.d.ts.map +1 -0
- package/dist/integrations/platform/jira/routes.js +274 -0
- package/dist/integrations/platform/jira/routes.js.map +1 -0
- package/dist/integrations/platform/linear/integration.d.ts +34 -1
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
- package/dist/integrations/platform/linear/integration.js +271 -48
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +15 -5
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +2 -5
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +24 -4
- package/dist/routes/surface.js.map +1 -1
- package/dist/rules/dispatch-errors.d.ts.map +1 -1
- package/dist/rules/dispatch-errors.js +8 -0
- package/dist/rules/dispatch-errors.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +1 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +180 -106
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.d.ts +2 -2
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +2 -2
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +4 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/terminal-cleanup.d.ts +14 -1
- package/dist/rules/terminal-cleanup.d.ts.map +1 -1
- package/dist/rules/terminal-cleanup.js +20 -0
- package/dist/rules/terminal-cleanup.js.map +1 -1
- package/dist/rules/tools.d.ts +2 -0
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +30 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +13 -0
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +11 -2
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +72 -3
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +9 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.d.ts.map +1 -1
- package/dist/rules/validation.js +13 -2
- package/dist/rules/validation.js.map +1 -1
- package/dist/session/run-audit.d.ts +9 -0
- package/dist/session/run-audit.d.ts.map +1 -1
- package/dist/session/run-audit.js +37 -6
- package/dist/session/run-audit.js.map +1 -1
- package/dist/skills/service.d.ts +12 -0
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +27 -1
- package/dist/skills/service.js.map +1 -1
- package/dist/storage/domains/intake/base.d.ts +11 -8
- package/dist/storage/domains/intake/base.d.ts.map +1 -1
- package/dist/storage/domains/intake/base.js +69 -24
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +41 -2
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +159 -12
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/supervisor/session-messaging.d.ts +23 -0
- package/dist/supervisor/session-messaging.d.ts.map +1 -0
- package/dist/supervisor/session-messaging.js +15 -0
- package/dist/supervisor/session-messaging.js.map +1 -0
- package/dist/supervisor/write-tools.d.ts +2 -1
- package/dist/supervisor/write-tools.d.ts.map +1 -1
- package/dist/supervisor/write-tools.js +11 -7
- package/dist/supervisor/write-tools.js.map +1 -1
- package/dist/work-item-branch.d.ts +1 -1
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +4 -2
- package/dist/work-item-branch.js.map +1 -1
- package/package.json +11 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","names":[],"sources":["../../../src/integrations/linear/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for the Linear intake feature.\n *\n * Registered alongside the other `/web/*` routes, behind the WorkOS auth gate.\n * Mirrors the GitHub module: every route re-resolves the authenticated user\n * from the request cookie and scopes all rows by the caller's WorkOS org, so an\n * org can only ever see its own Linear connection and issues.\n *\n * When the feature is disabled (`isLinearFeatureEnabled()` false),\n * `buildLinearRoutes` returns only `GET /web/linear/status`, which reports\n * `enabled:false` so the SPA can cleanly hide all Linear UI.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { RouteAuth } from '../../routes/route.js';\nimport type { StateSigner } from '../../state-signing.js';\nimport type { IntakeStorage } from '../../storage/domains/intake/base.js';\nimport type { LinearIntegration } from './integration.js';\nimport { LinearReauthRequiredError } from './integration.js';\nimport type { LinearRulesIngress } from './rules.js';\n\ntype RouteContext = Context;\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): RouteContext {\n return c as RouteContext;\n}\n\n/**\n * Non-secret diagnostic snapshot of every Linear feature gate, mirroring the\n * GitHub diagnostics shape. Only booleans — never values.\n */\nexport interface LinearFeatureDiagnostics {\n linearAppConfigured: boolean;\n factoryAuthEnabled: boolean;\n appDbConfigured: boolean;\n}\n\nexport interface MountLinearRoutesOptions {\n /**\n * The integration instance providing OAuth + GraphQL access. Required for\n * everything beyond the disabled `status` route.\n */\n linear?: LinearIntegration;\n /** Host auth seam. Linear connections are org-owned, so the feature is inert without it. */\n auth: RouteAuth;\n /**\n * Absolute base URL of the web server (e.g. `http://localhost:4111`), used to\n * build the OAuth redirect URI when one isn't explicitly configured.\n */\n baseUrl?: string;\n /** Explicit OAuth callback URI; defaults to `<baseUrl>/auth/linear/callback`. */\n redirectUri?: string;\n /**\n * Shared OAuth `state` signer (created once per boot by the factory).\n * Required for the connect/callback flow; when absent, only the disabled\n * `status` route is served.\n */\n stateSigner?: StateSigner;\n /**\n * Cross-integration intake selection domain. Required for the issues route's\n * project filter; when absent, only the disabled `status` route is served.\n */\n intake?: IntakeStorage;\n /**\n * Factory project domain, used to keep single-project installs working\n * without any source binding. When absent, unbound sources are treated as\n * belonging to no project.\n */\n projects?: { list(input: { orgId: string }): Promise<unknown[]> };\n ingestFactoryIssues?: (input: LinearRulesIngress) => Promise<unknown>;\n}\n\n/**\n * Narrow the caller's selected Linear sources to the ones that feed this\n * Factory project.\n *\n * A Linear issue carries no Factory project of its own, so without a binding\n * every board view would ingest every selected source's issues into whichever\n * project happened to be on screen. Routing is explicit: a source feeds this\n * project only when its binding names both the project and a board. Returns\n * the bound board per source so the ingest lands cards where the user asked.\n */\nasync function scopeSourceIdsToProject({\n intake,\n orgId,\n factoryProjectId,\n selectedIds,\n}: {\n intake: IntakeStorage;\n orgId: string;\n factoryProjectId: string;\n selectedIds: string[];\n}): Promise<Record<string, string>> {\n const selected = new Set(selectedIds);\n const intakeBoards: Record<string, string> = {};\n for (const binding of await intake.listBindings({ orgId, integrationId: 'linear' })) {\n if (binding.factoryProjectId === factoryProjectId && binding.board && selected.has(binding.sourceId)) {\n intakeBoards[binding.sourceId] = binding.board;\n }\n }\n return intakeBoards;\n}\n\n/**\n * Resolve the org-scoped tenant for a Linear request. The connection is\n * org-owned, so it requires both a signed-in user and an organization — same\n * tenancy rules as the GitHub routes.\n */\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n {\n error: 'organization_required',\n message: 'Linear intake requires an organization. Personal accounts cannot connect Linear.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\n/**\n * Validate an opaque Linear pagination cursor from the query string. Cursors\n * are server-issued (`pageInfo.endCursor`), so anything outside a conservative\n * charset/length is rejected rather than forwarded to Linear.\n */\nfunction parseAfterCursor(raw: string | undefined): string | undefined | null {\n if (raw === undefined || raw === '') return undefined;\n if (raw.length > 512 || !/^[\\w+/=.:-]+$/.test(raw)) return null;\n return raw;\n}\n\n/** Human issue key as it appears on a card (`ENG-123`). */\nconst ISSUE_IDENTIFIER_RE = /^[A-Za-z][A-Za-z0-9]{0,9}-\\d{1,7}$/;\n\n/** Map a Linear read failure to the API response for the SPA. */\nfunction linearFetchError(c: RouteContext, err: unknown) {\n if (err instanceof LinearReauthRequiredError || (err as { status?: number }).status === 401) {\n return c.json({ error: 'linear_reauth_required', message: new LinearReauthRequiredError().message }, 409);\n }\n return c.json({ error: 'linear_fetch_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the Linear routes as Mastra `apiRoutes`. When the feature is disabled,\n * returns only the `status` route so the SPA can detect the disabled state.\n */\nexport function buildLinearRoutes(options: MountLinearRoutesOptions): ApiRoute[] {\n const routes: ApiRoute[] = [];\n const { linear, auth, stateSigner, intake } = options;\n const enabled = Boolean(linear) && auth.enabled();\n const diagnostics = (): LinearFeatureDiagnostics => ({\n linearAppConfigured: Boolean(linear),\n factoryAuthEnabled: auth.enabled(),\n appDbConfigured: true,\n });\n\n // The status route is always registered so the SPA can detect the disabled state.\n routes.push(\n registerApiRoute('/web/linear/status', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!enabled || !linear || !stateSigner) {\n return c.json({\n enabled: false,\n connected: false,\n workspace: null,\n reason: 'missing_config',\n diagnostics: diagnostics(),\n });\n }\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized', reason: 'auth_required' }, 401);\n\n if (!tenant.orgId) {\n return c.json({\n enabled: true,\n organizationRequired: true,\n connected: false,\n workspace: null,\n reason: 'organization_required',\n diagnostics: diagnostics(),\n });\n }\n\n const connection = await linear.loadConnection(tenant.orgId);\n return c.json({\n enabled: true,\n connected: Boolean(connection),\n workspace: connection ? { name: connection.workspaceName, urlKey: connection.workspaceUrlKey } : null,\n reason: connection ? 'ready' : 'not_connected',\n diagnostics: diagnostics(),\n });\n },\n }),\n );\n\n // Without the integration instance or a state signer the connect/callback\n // flow cannot talk to Linear or bind the OAuth round-trip to a tenant —\n // serve only the disabled `status` route (mirrors the feature gate).\n if (!enabled || !linear || !stateSigner || !intake) {\n return routes;\n }\n\n const redirectUri = options.redirectUri ?? `${(options.baseUrl ?? '').replace(/\\/$/, '')}/auth/linear/callback`;\n\n // ── Connect: send the user to Linear's OAuth consent screen ─────────────\n routes.push(\n registerApiRoute('/auth/linear/connect', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n const state = stateSigner.sign(resolved.tenant.orgId, resolved.tenant.userId);\n return c.redirect(linear.buildAuthorizeUrl(state, redirectUri));\n },\n }),\n );\n\n // ── Callback: exchange the code, persist the connection for the org ─────\n routes.push(\n registerApiRoute('/auth/linear/callback', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n const { orgId, userId } = resolved.tenant;\n\n // CSRF / cross-tenant linking protection: the signed state must belong\n // to the same logged-in user *and* their current org.\n const stateTenant = stateSigner.verify(c.req.query('state'));\n if (!stateTenant || stateTenant.userId !== userId || stateTenant.orgId !== orgId) {\n console.warn('[Linear] OAuth callback rejected: state/tenant mismatch.');\n return c.redirect('/?linear=error');\n }\n\n const code = c.req.query('code');\n if (!code) {\n // User denied consent (or Linear returned an error).\n return c.redirect('/?linear=error');\n }\n\n try {\n const tokens = await linear.exchangeOAuthCode(code, redirectUri);\n const workspace = await linear.fetchWorkspace(tokens.accessToken);\n await linear.upsertConnection({\n orgId,\n userId,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n workspaceName: workspace.name,\n workspaceUrlKey: workspace.urlKey,\n });\n } catch (error) {\n console.warn(`[Linear] OAuth callback failed to persist connection for org ${orgId}.`, error);\n return c.redirect('/?linear=error');\n }\n\n return c.redirect('/?linear=connected');\n },\n }),\n );\n\n // ── List the workspace's projects (Settings intake-source picker) ───────\n routes.push(\n registerApiRoute('/web/linear/projects', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to list Linear projects.' }, 409);\n }\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const projects = await linear.listProjects(accessToken);\n return c.json({ projects });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── List the workspace's active issues (cursor-paged) ───────────────────\n // Respects the caller's intake config: disabled Linear intake 404s the\n // source, and an explicit project selection narrows the issue filter.\n routes.push(\n registerApiRoute('/web/linear/issues', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const after = parseAfterCursor(c.req.query('after'));\n if (after === null) return c.json({ error: 'invalid_cursor' }, 400);\n const factoryProjectId = c.req.query('factoryProjectId');\n if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to see intake issues.' }, 409);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n integrationIds: ['linear'],\n });\n const selection = config.linear!;\n if (!selection.enabled) {\n return c.json({ error: 'linear_intake_disabled', message: 'Linear intake is turned off in Settings.' }, 404);\n }\n\n // No projects selected means nothing is synced — don't fan out to Linear.\n const selectedIds = selection.sourceIds ?? [];\n // A board request is also an ingest, so it only ever sees the sources\n // routed to a board of that Factory project.\n const intakeBoards = factoryProjectId\n ? await scopeSourceIdsToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds,\n })\n : null;\n const projectIds = intakeBoards ? Object.keys(intakeBoards) : selectedIds;\n if (projectIds.length === 0) {\n return c.json({ issues: [], nextCursor: null });\n }\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const { issues, nextCursor } = await linear.intake.listIssues({\n connection: { type: 'oauth', accessToken },\n sourceIds: projectIds,\n cursor: after,\n });\n const issuePayload = issues.map(issue => ({\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n state: issue.state ?? '',\n stateType: issue.stateType ?? '',\n priorityLabel: issue.priority ?? '',\n assignee: issue.assignee,\n creator: issue.author,\n team: issue.source,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n sourceId: issue.sourceId ?? null,\n }));\n if (factoryProjectId && intakeBoards && options.ingestFactoryIssues) {\n await options.ingestFactoryIssues({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n factoryProjectId,\n issues: issuePayload,\n intakeBoards,\n });\n }\n return c.json({ issues: issuePayload, nextCursor });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n routes.push(\n registerApiRoute('/web/linear/issues/:identifier', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const identifier = c.req.param('identifier');\n if (!ISSUE_IDENTIFIER_RE.test(identifier)) return c.json({ error: 'invalid_identifier' }, 400);\n const factoryProjectId = c.req.query('factoryProjectId');\n if (!factoryProjectId || !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to see intake issues.' }, 409);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n integrationIds: ['linear'],\n });\n const selection = config.linear!;\n if (!selection.enabled) {\n return c.json({ error: 'linear_intake_disabled', message: 'Linear intake is turned off in Settings.' }, 404);\n }\n const projectIds = Object.keys(\n await scopeSourceIdsToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds: selection.sourceIds ?? [],\n }),\n );\n if (projectIds.length === 0) return c.json({ error: 'issue_not_found' }, 404);\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const issue = await linear.fetchIssueDetail(accessToken, identifier);\n // Reads exactly like an issue that doesn't exist.\n if (!issue || issue.projectId === null || !projectIds.includes(issue.projectId)) {\n return c.json({ error: 'issue_not_found' }, 404);\n }\n return c.json({\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n description: issue.description,\n });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n return routes;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../../src/integrations/linear/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` for the Linear intake feature.\n *\n * Registered alongside the other `/web/*` routes, behind the WorkOS auth gate.\n * Mirrors the GitHub module: every route re-resolves the authenticated user\n * from the request cookie and scopes all rows by the caller's WorkOS org, so an\n * org can only ever see its own Linear connection and issues.\n *\n * When the feature is disabled (`isLinearFeatureEnabled()` false),\n * `buildLinearRoutes` returns only `GET /web/linear/status`, which reports\n * `enabled:false` so the SPA can cleanly hide all Linear UI.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport { isTerminalWorkItem } from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport type { RouteAuth } from '../../routes/route.js';\nimport { isTerminalFactoryRuleStage } from '../../rules/types.js';\nimport type { StateSigner } from '../../state-signing.js';\nimport type { IntakeStorage } from '../../storage/domains/intake/base.js';\nimport type { WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport { linearClaimKey } from './claim.js';\nimport type { LinearIntegration } from './integration.js';\nimport { LinearReauthRequiredError } from './integration.js';\nimport type { LinearRulesIngress } from './rules.js';\n\ntype RouteContext = Context;\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): RouteContext {\n return c as RouteContext;\n}\n\n/**\n * Non-secret diagnostic snapshot of every Linear feature gate, mirroring the\n * GitHub diagnostics shape. Only booleans — never values.\n */\nexport interface LinearFeatureDiagnostics {\n linearAppConfigured: boolean;\n factoryAuthEnabled: boolean;\n appDbConfigured: boolean;\n}\n\nexport interface MountLinearRoutesOptions {\n /**\n * The integration instance providing OAuth + GraphQL access. Required for\n * everything beyond the disabled `status` route.\n */\n linear?: LinearIntegration;\n /** Host auth seam. Linear connections are org-owned, so the feature is inert without it. */\n auth: RouteAuth;\n /**\n * Absolute base URL of the web server (e.g. `http://localhost:4111`), used to\n * build the OAuth redirect URI when one isn't explicitly configured.\n */\n baseUrl?: string;\n /** Explicit OAuth callback URI; defaults to `<baseUrl>/auth/linear/callback`. */\n redirectUri?: string;\n /**\n * Shared OAuth `state` signer (created once per boot by the factory).\n * Required for the connect/callback flow; when absent, only the disabled\n * `status` route is served.\n */\n stateSigner?: StateSigner;\n /**\n * Cross-integration intake selection domain. Required for the issues route's\n * project filter; when absent, only the disabled `status` route is served.\n */\n intake?: IntakeStorage;\n /**\n * Factory project domain, used to keep single-project installs working\n * without any source binding. When absent, unbound sources are treated as\n * belonging to no project.\n */\n projects?: { list(input: { orgId: string }): Promise<unknown[]> };\n ingestFactoryIssues?: (input: LinearRulesIngress) => Promise<unknown>;\n /**\n * Work-item lookup so the detail route can keep serving a card this Factory\n * already holds after the issue's winning source was routed elsewhere. When\n * absent, details resolve only through sources routed to the Factory.\n */\n workItems?: Pick<WorkItemsStorage, 'getByProjectSource' | 'getByClaimKey'>;\n /** Installed boards, so a held card's terminal status follows its own board. */\n boards?: BoardRegistry;\n}\n\n/**\n * Narrow the caller's selected Linear sources to the ones that feed this\n * Factory project.\n *\n * A Linear issue carries no Factory project of its own, so without a binding\n * every board view would ingest every selected source's issues into whichever\n * project happened to be on screen. Routing is explicit: a source feeds this\n * project only when its binding names both the project and a board. Returns\n * the bound board per source so the ingest lands cards where the user asked.\n */\nasync function scopeSourceIdsToProject({\n intake,\n orgId,\n factoryProjectId,\n selectedIds,\n}: {\n intake: IntakeStorage;\n orgId: string;\n factoryProjectId: string;\n selectedIds: string[];\n}): Promise<Record<string, string>> {\n const selected = new Set(selectedIds);\n const intakeBoards: Record<string, string> = {};\n for (const binding of await intake.listBindings({ orgId, integrationId: 'linear' })) {\n if (binding.factoryProjectId === factoryProjectId && binding.board && selected.has(binding.sourceId)) {\n intakeBoards[binding.sourceId] = binding.board;\n }\n }\n return intakeBoards;\n}\n\n/**\n * Resolve the org-scoped tenant for a Linear request. The connection is\n * org-owned, so it requires both a signed-in user and an organization — same\n * tenancy rules as the GitHub routes.\n */\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n {\n error: 'organization_required',\n message: 'Linear intake requires an organization. Personal accounts cannot connect Linear.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\n/**\n * Validate an opaque Linear pagination cursor from the query string. Cursors\n * are server-issued (`pageInfo.endCursor`), so anything outside a conservative\n * charset/length is rejected rather than forwarded to Linear.\n */\nfunction parseAfterCursor(raw: string | undefined): string | undefined | null {\n if (raw === undefined || raw === '') return undefined;\n if (raw.length > 8192 || !/^[\\w+/=.:-]+$/.test(raw)) return null;\n return raw;\n}\n\n/** Human issue key as it appears on a card (`ENG-123`). */\nconst ISSUE_IDENTIFIER_RE = /^[A-Za-z][A-Za-z0-9]{0,9}-\\d{1,7}$/;\n/** Project sources are more specific than their team and win every routed read. */\nfunction winningLinearSourceId(\n linear: LinearIntegration,\n sourceIds: string[],\n issue: Parameters<LinearIntegration['sourceMatchesIssue']>[1],\n): string | null {\n const matching = sourceIds.filter(sourceId => linear.sourceMatchesIssue(sourceId, issue));\n return matching.find(sourceId => !sourceId.startsWith('linear-team:')) ?? matching[0] ?? null;\n}\n\n/**\n * The card this Factory holds for an issue, if any: by the stable issue id when\n * the SPA sent one, else by the identifier the card was filed under.\n */\nasync function findHeldCard(\n options: MountLinearRoutesOptions,\n orgId: string,\n factoryProjectId: string,\n identifier: string,\n issueId: string | undefined,\n) {\n if (!options.workItems) return null;\n if (issueId) {\n const claimant = await options.workItems.getByClaimKey({ orgId, claimKey: linearClaimKey(issueId) });\n if (claimant) return claimant.factoryProjectId === factoryProjectId ? claimant : null;\n }\n return options.workItems.getByProjectSource({\n orgId,\n factoryProjectId,\n source: { integrationId: 'linear', type: 'issue', externalId: `linear:${identifier}` },\n });\n}\n\n/** Map a Linear read failure to the API response for the SPA. */\nfunction linearFetchError(c: RouteContext, err: unknown) {\n if ((err as { code?: unknown }).code === 'invalid_cursor') {\n return c.json({ error: 'invalid_cursor' }, 400);\n }\n if (err instanceof LinearReauthRequiredError || (err as { status?: number }).status === 401) {\n return c.json({ error: 'linear_reauth_required', message: new LinearReauthRequiredError().message }, 409);\n }\n return c.json({ error: 'linear_fetch_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the Linear routes as Mastra `apiRoutes`. When the feature is disabled,\n * returns only the `status` route so the SPA can detect the disabled state.\n */\nexport function buildLinearRoutes(options: MountLinearRoutesOptions): ApiRoute[] {\n const routes: ApiRoute[] = [];\n const { linear, auth, stateSigner, intake } = options;\n const enabled = Boolean(linear) && auth.enabled();\n const diagnostics = (): LinearFeatureDiagnostics => ({\n linearAppConfigured: Boolean(linear),\n factoryAuthEnabled: auth.enabled(),\n appDbConfigured: true,\n });\n\n // The status route is always registered so the SPA can detect the disabled state.\n routes.push(\n registerApiRoute('/web/linear/status', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n if (!enabled || !linear || !stateSigner) {\n return c.json({\n enabled: false,\n connected: false,\n workspace: null,\n reason: 'missing_config',\n diagnostics: diagnostics(),\n });\n }\n await auth.ensureUser(loose(c));\n const tenant = auth.tenant(loose(c));\n if (!tenant) return c.json({ error: 'unauthorized', reason: 'auth_required' }, 401);\n\n if (!tenant.orgId) {\n return c.json({\n enabled: true,\n organizationRequired: true,\n connected: false,\n workspace: null,\n reason: 'organization_required',\n diagnostics: diagnostics(),\n });\n }\n\n const connection = await linear.loadConnection(tenant.orgId);\n return c.json({\n enabled: true,\n connected: Boolean(connection),\n workspace: connection ? { name: connection.workspaceName, urlKey: connection.workspaceUrlKey } : null,\n reason: connection ? 'ready' : 'not_connected',\n diagnostics: diagnostics(),\n });\n },\n }),\n );\n\n // Without the integration instance or a state signer the connect/callback\n // flow cannot talk to Linear or bind the OAuth round-trip to a tenant —\n // serve only the disabled `status` route (mirrors the feature gate).\n if (!enabled || !linear || !stateSigner || !intake) {\n return routes;\n }\n\n const redirectUri = options.redirectUri ?? `${(options.baseUrl ?? '').replace(/\\/$/, '')}/auth/linear/callback`;\n\n // ── Connect: send the user to Linear's OAuth consent screen ─────────────\n routes.push(\n registerApiRoute('/auth/linear/connect', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n const state = stateSigner.sign(resolved.tenant.orgId, resolved.tenant.userId);\n return c.redirect(linear.buildAuthorizeUrl(state, redirectUri));\n },\n }),\n );\n\n // ── Callback: exchange the code, persist the connection for the org ─────\n routes.push(\n registerApiRoute('/auth/linear/callback', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n const { orgId, userId } = resolved.tenant;\n\n // CSRF / cross-tenant linking protection: the signed state must belong\n // to the same logged-in user *and* their current org.\n const stateTenant = stateSigner.verify(c.req.query('state'));\n if (!stateTenant || stateTenant.userId !== userId || stateTenant.orgId !== orgId) {\n console.warn('[Linear] OAuth callback rejected: state/tenant mismatch.');\n return c.redirect('/?linear=error');\n }\n\n const code = c.req.query('code');\n if (!code) {\n // User denied consent (or Linear returned an error).\n return c.redirect('/?linear=error');\n }\n\n try {\n const tokens = await linear.exchangeOAuthCode(code, redirectUri);\n const workspace = await linear.fetchWorkspace(tokens.accessToken);\n await linear.upsertConnection({\n orgId,\n userId,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n expiresAt: tokens.expiresAt,\n scope: tokens.scope,\n workspaceName: workspace.name,\n workspaceUrlKey: workspace.urlKey,\n });\n } catch (error) {\n console.warn(`[Linear] OAuth callback failed to persist connection for org ${orgId}.`, error);\n return c.redirect('/?linear=error');\n }\n\n return c.redirect('/?linear=connected');\n },\n }),\n );\n\n // ── List the workspace's projects (Settings intake-source picker) ───────\n routes.push(\n registerApiRoute('/web/linear/projects', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to list Linear projects.' }, 409);\n }\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const projects = await linear.listProjects(accessToken);\n return c.json({ projects });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── List the workspace's teams (Settings intake-source picker) ──────────\n routes.push(\n registerApiRoute('/web/linear/teams', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to list Linear teams.' }, 409);\n }\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const teams = await linear.listTeams(accessToken);\n return c.json({ teams });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n // ── List the workspace's active issues (cursor-paged) ───────────────────\n // Respects the org's intake config: disabled Linear intake 404s the\n // source, and an explicit project selection narrows the issue filter.\n routes.push(\n registerApiRoute('/web/linear/issues', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const after = parseAfterCursor(c.req.query('after'));\n if (after === null) return c.json({ error: 'invalid_cursor' }, 400);\n const factoryProjectId = c.req.query('factoryProjectId');\n if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to see intake issues.' }, 409);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['linear'] });\n const selection = config.linear!;\n if (!selection.enabled) {\n return c.json({ error: 'linear_intake_disabled', message: 'Linear intake is turned off in Settings.' }, 404);\n }\n\n // No sources selected means nothing is synced — don't fan out to Linear.\n const selectedIds = selection.sourceIds ?? [];\n // A board request is also an ingest, so it only ever sees the sources\n // routed to a board of that Factory project. Sources may be projects or\n // whole teams; both are keyed by their opaque source id.\n const intakeBoards = factoryProjectId\n ? await scopeSourceIdsToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds,\n })\n : null;\n const routedSourceIds = intakeBoards ? Object.keys(intakeBoards) : selectedIds;\n if (routedSourceIds.length === 0) {\n return c.json({ issues: [], nextCursor: null });\n }\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const { issues, nextCursor } = await linear.intake.listIssues({\n connection: { type: 'oauth', accessToken },\n sourceIds: routedSourceIds,\n attributionSourceIds: selectedIds,\n cursor: after,\n });\n // Source precedence must be resolved against the complete selection,\n // then the winning issues can be narrowed to this Factory project.\n const routedIssues = intakeBoards\n ? issues.filter(issue => issue.sourceId != null && routedSourceIds.includes(issue.sourceId))\n : issues;\n const issuePayload = routedIssues.map(issue => ({\n id: issue.id,\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n state: issue.state ?? '',\n stateType: issue.stateType ?? '',\n priorityLabel: issue.priority ?? '',\n assignee: issue.assignee,\n creator: issue.author,\n team: issue.source,\n labels: issue.labels,\n createdAt: issue.createdAt,\n updatedAt: issue.updatedAt,\n sourceId: issue.sourceId ?? null,\n }));\n if (factoryProjectId && intakeBoards && options.ingestFactoryIssues) {\n await options.ingestFactoryIssues({\n orgId: resolved.tenant.orgId,\n userId: resolved.tenant.userId,\n factoryProjectId,\n issues: issuePayload,\n intakeBoards,\n });\n }\n return c.json({ issues: issuePayload, nextCursor });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n routes.push(\n registerApiRoute('/web/linear/issues/:identifier', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const resolved = await resolveOrgTenant(loose(c), auth);\n if ('response' in resolved) return resolved.response;\n\n const identifier = c.req.param('identifier');\n if (!ISSUE_IDENTIFIER_RE.test(identifier)) return c.json({ error: 'invalid_identifier' }, 400);\n const issueId = c.req.query('issueId');\n if (issueId !== undefined && (issueId.length === 0 || issueId.length > 512)) {\n return c.json({ error: 'invalid_issue_id' }, 400);\n }\n const issueReference = issueId ?? identifier;\n const factoryProjectId = c.req.query('factoryProjectId');\n if (!factoryProjectId || !UUID_RE.test(factoryProjectId)) {\n return c.json({ error: 'invalid_factory_project_id' }, 400);\n }\n\n const connection = await linear.loadConnection(resolved.tenant.orgId);\n if (!connection) {\n return c.json({ error: 'linear_not_connected', message: 'Connect Linear to see intake issues.' }, 409);\n }\n\n await intake.ensureReady();\n const config = await intake.getConfig({ orgId: resolved.tenant.orgId, integrationIds: ['linear'] });\n const selection = config.linear!;\n if (!selection.enabled) {\n return c.json({ error: 'linear_intake_disabled', message: 'Linear intake is turned off in Settings.' }, 404);\n }\n const selectedIds = selection.sourceIds ?? [];\n const routedSourceIds = Object.keys(\n await scopeSourceIdsToProject({\n intake,\n orgId: resolved.tenant.orgId,\n factoryProjectId,\n selectedIds,\n }),\n );\n // A card this Factory already ingested stays readable after its issue's\n // winning source moved to a source routed elsewhere: the fetch widens to\n // every selected source (never beyond the selection), and the routing\n // check is waived for that card alone. Without a live card, the strict\n // routed-source rule applies.\n const held = await findHeldCard(options, resolved.tenant.orgId, factoryProjectId, identifier, issueId);\n const holdsLiveCard =\n held != null &&\n !(options.boards ? isTerminalWorkItem(options.boards, held) : isTerminalFactoryRuleStage(held.stages));\n if (routedSourceIds.length === 0 && !holdsLiveCard) return c.json({ error: 'issue_not_found' }, 404);\n const fetchSourceIds = holdsLiveCard ? selectedIds : routedSourceIds;\n\n try {\n const accessToken = await linear.getFreshAccessToken(connection);\n const issue = await linear.fetchIssueDetail(accessToken, issueReference, selectedIds, fetchSourceIds);\n const matchesReference = issueId !== undefined ? issue?.id === issueId : issue?.identifier === identifier;\n const winningSourceId = issue ? winningLinearSourceId(linear, selectedIds, issue) : null;\n const isRouted = winningSourceId != null && (holdsLiveCard || routedSourceIds.includes(winningSourceId));\n // Reads exactly like an issue that doesn't exist.\n if (!issue || !matchesReference || !isRouted) {\n return c.json({ error: 'issue_not_found' }, 404);\n }\n return c.json({\n identifier: issue.identifier,\n title: issue.title,\n url: issue.url,\n description: issue.description,\n });\n } catch (err) {\n return linearFetchError(loose(c), err);\n }\n },\n }),\n );\n\n return routes;\n}\n"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ export interface LinearIssueIngress {
|
|
|
18
18
|
labels: string[];
|
|
19
19
|
createdAt: string;
|
|
20
20
|
updatedAt: string;
|
|
21
|
-
/** Linear
|
|
21
|
+
/** Linear source the issue was read from (project or team); resolves the bound board via `intakeBoards`. */
|
|
22
22
|
sourceId?: string | null;
|
|
23
23
|
}
|
|
24
24
|
export interface LinearRulesOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../src/integrations/linear/rules.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AAC9F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAkB3D,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,4GAA4G;IAC5G,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,aAAa,CAAC;IACtB,WAAW,EAAE,gBAAgB,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACjD;AAED,KAAK,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAE1D,qBAAa,WAAW;;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAyI9F;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAA;CAAE,EAC5C,OAAO,EAAE,kBAAkB,GAC1B,CAAC,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAU/D"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from "../../rules/validation.js";
|
|
2
|
-
import { boardForWorkItem } from "../../boards/semantics.js";
|
|
2
|
+
import { boardForWorkItem, isTerminalWorkItem } from "../../boards/semantics.js";
|
|
3
3
|
import "../../boards/index.js";
|
|
4
|
+
import { linearClaimKey } from "./claim.js";
|
|
4
5
|
//#region src/integrations/linear/rules.ts
|
|
5
6
|
const RULE_TIMEOUT_MS = 5e3;
|
|
6
7
|
async function withRuleTimeout(promise) {
|
|
@@ -31,8 +32,16 @@ var LinearRules = class {
|
|
|
31
32
|
factoryProjectId: input.factoryProjectId
|
|
32
33
|
});
|
|
33
34
|
const itemsBySourceKey = new Map(items.map((item) => [item.externalSource?.externalId, item]));
|
|
35
|
+
const itemsByClaimKey = new Map(items.filter((item) => item.claimKey).map((item) => [item.claimKey, item]));
|
|
34
36
|
const statuses = [];
|
|
35
|
-
for (const issue of input.issues)
|
|
37
|
+
for (const issue of input.issues) {
|
|
38
|
+
const relatedItem = itemsByClaimKey.get(linearClaimKey(issue.id)) ?? itemsBySourceKey.get(`linear:${issue.identifier}`);
|
|
39
|
+
if (!relatedItem && await this.#heldElsewhere(input, issue)) {
|
|
40
|
+
statuses.push("missing");
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
statuses.push(await this.#ingestIssue(input, issue, relatedItem));
|
|
44
|
+
}
|
|
36
45
|
if (statuses.some((status) => status === "committed")) return {
|
|
37
46
|
status: "committed",
|
|
38
47
|
ingested: statuses.length
|
|
@@ -46,6 +55,22 @@ var LinearRules = class {
|
|
|
46
55
|
ingested: statuses.length
|
|
47
56
|
};
|
|
48
57
|
}
|
|
58
|
+
async #heldElsewhere(input, issue) {
|
|
59
|
+
const heldLive = (row) => row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row);
|
|
60
|
+
const claimant = await this.options.storage.getByClaimKey({
|
|
61
|
+
orgId: input.orgId,
|
|
62
|
+
claimKey: linearClaimKey(issue.id)
|
|
63
|
+
});
|
|
64
|
+
if (claimant && heldLive(claimant)) return true;
|
|
65
|
+
return (await this.options.storage.listBySource({
|
|
66
|
+
orgId: input.orgId,
|
|
67
|
+
source: {
|
|
68
|
+
integrationId: "linear",
|
|
69
|
+
type: "issue",
|
|
70
|
+
externalId: `linear:${issue.identifier}`
|
|
71
|
+
}
|
|
72
|
+
})).some(heldLive);
|
|
73
|
+
}
|
|
49
74
|
async #ingestIssue(input, issue, relatedItem) {
|
|
50
75
|
const ingressId = `linear:${issue.id}:${issue.updatedAt}`;
|
|
51
76
|
const actor = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","names":["#ingestIssue"],"sources":["../../../src/integrations/linear/rules.ts"],"sourcesContent":["import { boardForWorkItem } from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport type { FactoryLinearRuleContext, FactoryRuleDecision } from '../../rules/types.js';\nimport { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport type { LinearEventRules } from './default-rules.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nexport interface LinearIssueIngress {\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n /** Display name of the Linear user who created the issue, when available. */\n creator: string | null;\n team: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n /** Linear project the issue was read from; resolves the bound board via `intakeBoards`. */\n sourceId?: string | null;\n}\n\nexport interface LinearRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n configVersion: string;\n boards: BoardRegistry;\n linearRules: LinearEventRules;\n}\n\nexport interface LinearRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: LinearIssueIngress[];\n /** Board id per bound Linear source id, from the project's intake bindings. */\n intakeBoards?: Readonly<Record<string, string>>;\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class LinearRules {\n constructor(private readonly options: LinearRulesOptions) {}\n\n async ingest(input: LinearRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n statuses.push(await this.#ingestIssue(input, issue, itemsBySourceKey.get(`linear:${issue.identifier}`)));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #ingestIssue(\n input: LinearRulesIngress,\n issue: LinearIssueIngress,\n relatedItem: WorkItemRow | undefined,\n ): Promise<IngressStatus> {\n const ingressId = `linear:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed issues without existing work items should not create new ones.\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n if (isClosed && !relatedItem) {\n return 'missing';\n }\n\n const event = isClosed ? 'issueClosed' : 'issueObserved';\n const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : undefined;\n const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : undefined;\n const context: FactoryLinearRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'linear', id: ingressId },\n cause: `linear.${event}`,\n causalChain: [],\n configVersion: this.options.configVersion,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'linear-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n acceptedAt: relatedItem.acceptedAt,\n metadata: relatedItem.metadata,\n },\n board: boardForWorkItem(relatedItem),\n itemRevision: relatedItem.revision,\n }\n : {}),\n ...(boundBoard ? { intake: { board: boundBoard.id, initialPhase: boundBoard.initialPhase } } : {}),\n event,\n issue,\n };\n\n const rule = this.options.linearRules[context.event];\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => {\n assertFactoryDecisionTarget(\n entry,\n this.options.boards,\n relatedItem ? boardForWorkItem(relatedItem) : undefined,\n );\n return { ...entry };\n });\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Linear rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'linear.issueObserved' },\n configVersion: this.options.configVersion,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n return committed.status;\n }\n}\n\nexport function attachLinearRules(\n linear: { readonly rules: LinearEventRules },\n context: IntegrationContext,\n): ((input: LinearRulesIngress) => Promise<unknown>) | undefined {\n if (!context.runtime) return undefined;\n const rules = new LinearRules({\n projects: context.storage.projects,\n storage: context.runtime.workItems,\n configVersion: context.runtime.configVersion,\n boards: context.runtime.boards,\n linearRules: linear.rules,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rules.js","names":["#heldElsewhere","#ingestIssue"],"sources":["../../../src/integrations/linear/rules.ts"],"sourcesContent":["import { boardForWorkItem, isTerminalWorkItem } from '../../boards/index.js';\nimport type { BoardRegistry } from '../../boards/index.js';\nimport type { FactoryLinearRuleContext, FactoryRuleDecision } from '../../rules/types.js';\nimport { assertFactoryDecisionTarget, validateFactoryRuleDecisions } from '../../rules/validation.js';\nimport type { FactoryProjectsStorage } from '../../storage/domains/projects/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../../storage/domains/work-items/base.js';\nimport type { IntegrationContext } from '../base.js';\nimport { linearClaimKey } from './claim.js';\nimport type { LinearEventRules } from './default-rules.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\n\nasync function withRuleTimeout<T>(promise: Promise<T>): Promise<T> {\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), RULE_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nexport interface LinearIssueIngress {\n id: string;\n identifier: string;\n title: string;\n url: string;\n state: string;\n stateType: string;\n priorityLabel: string;\n assignee: string | null;\n /** Display name of the Linear user who created the issue, when available. */\n creator: string | null;\n team: string | null;\n labels: string[];\n createdAt: string;\n updatedAt: string;\n /** Linear source the issue was read from (project or team); resolves the bound board via `intakeBoards`. */\n sourceId?: string | null;\n}\n\nexport interface LinearRulesOptions {\n projects: Pick<FactoryProjectsStorage, 'get'>;\n storage: WorkItemsStorage;\n configVersion: string;\n boards: BoardRegistry;\n linearRules: LinearEventRules;\n}\n\nexport interface LinearRulesIngress {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n issues: LinearIssueIngress[];\n /** Board id per bound Linear source id, from the project's intake bindings. */\n intakeBoards?: Readonly<Record<string, string>>;\n}\n\ntype IngressStatus = 'committed' | 'replayed' | 'missing';\n\nexport class LinearRules {\n constructor(private readonly options: LinearRulesOptions) {}\n\n async ingest(input: LinearRulesIngress): Promise<{ status: IngressStatus; ingested: number }> {\n const project = await this.options.projects.get({ orgId: input.orgId, id: input.factoryProjectId });\n if (!project) return { status: 'missing', ingested: 0 };\n\n const items = await this.options.storage.list({ orgId: input.orgId, factoryProjectId: input.factoryProjectId });\n const itemsBySourceKey = new Map(items.map(item => [item.externalSource?.externalId, item]));\n const itemsByClaimKey = new Map(items.filter(item => item.claimKey).map(item => [item.claimKey, item]));\n const statuses: IngressStatus[] = [];\n for (const issue of input.issues) {\n // The stable issue id finds the card even after Linear renamed the\n // identifier; the identifier covers cards filed before claims existed.\n const relatedItem =\n itemsByClaimKey.get(linearClaimKey(issue.id)) ?? itemsBySourceKey.get(`linear:${issue.identifier}`);\n // One live card per Linear issue per org. When the winning source for an\n // issue moves to a source routed elsewhere (a project deselected under a\n // selected team, or the reverse), the card that already exists keeps the\n // issue; this Factory must not mint a second one. The store's claim index\n // is the guarantee; this check spares the dispatcher a refused upsert.\n if (!relatedItem && (await this.#heldElsewhere(input, issue))) {\n statuses.push('missing');\n continue;\n }\n statuses.push(await this.#ingestIssue(input, issue, relatedItem));\n }\n if (statuses.some(status => status === 'committed')) return { status: 'committed', ingested: statuses.length };\n if (statuses.some(status => status === 'replayed')) return { status: 'replayed', ingested: statuses.length };\n return { status: 'missing', ingested: statuses.length };\n }\n\n async #heldElsewhere(input: LinearRulesIngress, issue: LinearIssueIngress): Promise<boolean> {\n const heldLive = (row: WorkItemRow) =>\n row.factoryProjectId !== input.factoryProjectId && !isTerminalWorkItem(this.options.boards, row);\n const claimant = await this.options.storage.getByClaimKey({\n orgId: input.orgId,\n claimKey: linearClaimKey(issue.id),\n });\n if (claimant && heldLive(claimant)) return true;\n // Cards filed before claims existed carry only the identifier.\n const legacy = await this.options.storage.listBySource({\n orgId: input.orgId,\n source: { integrationId: 'linear', type: 'issue', externalId: `linear:${issue.identifier}` },\n });\n return legacy.some(heldLive);\n }\n\n async #ingestIssue(\n input: LinearRulesIngress,\n issue: LinearIssueIngress,\n relatedItem: WorkItemRow | undefined,\n ): Promise<IngressStatus> {\n const ingressId = `linear:${issue.id}:${issue.updatedAt}`;\n const actor = { type: 'human' as const, id: input.userId };\n\n // Closed issues without existing work items should not create new ones.\n const isClosed = issue.stateType === 'completed' || issue.stateType === 'canceled';\n if (isClosed && !relatedItem) {\n return 'missing';\n }\n\n const event = isClosed ? 'issueClosed' : 'issueObserved';\n const boundBoardId = issue.sourceId ? input.intakeBoards?.[issue.sourceId] : undefined;\n const boundBoard = boundBoardId ? this.options.boards.get(boundBoardId) : undefined;\n const context: FactoryLinearRuleContext = {\n tenant: { orgId: input.orgId, projectId: input.factoryProjectId },\n actor,\n ingress: { type: 'linear', id: ingressId },\n cause: `linear.${event}`,\n causalChain: [],\n configVersion: this.options.configVersion,\n ...(relatedItem\n ? {\n item: {\n id: relatedItem.id,\n source: 'linear-issue',\n sourceKey: relatedItem.externalSource?.externalId ?? null,\n parentWorkItemId: relatedItem.parentWorkItemId,\n title: relatedItem.title,\n url: relatedItem.externalSource?.url ?? null,\n stages: relatedItem.stages,\n acceptedAt: relatedItem.acceptedAt,\n metadata: relatedItem.metadata,\n },\n board: boardForWorkItem(relatedItem),\n itemRevision: relatedItem.revision,\n }\n : {}),\n ...(boundBoard ? { intake: { board: boundBoard.id, initialPhase: boundBoard.initialPhase } } : {}),\n event,\n issue,\n };\n\n const rule = this.options.linearRules[context.event];\n let decision: FactoryRuleDecision | void;\n let decisions: Record<string, unknown>[] = [];\n let outcome: { status: 'accepted' | 'rejected'; code?: string; reason?: string } = { status: 'accepted' };\n try {\n decision = rule ? await withRuleTimeout(Promise.resolve(rule(Object.freeze(context)))) : undefined;\n if (decision?.type === 'reject') {\n outcome = { status: 'rejected', code: decision.code, reason: decision.reason };\n } else if (decision) {\n decisions = validateFactoryRuleDecisions([decision]).map(entry => {\n assertFactoryDecisionTarget(\n entry,\n this.options.boards,\n relatedItem ? boardForWorkItem(relatedItem) : undefined,\n );\n return { ...entry };\n });\n }\n } catch (error) {\n const timedOut = error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT';\n outcome = {\n status: 'rejected',\n code: timedOut ? 'timeout' : 'rule_error',\n reason: timedOut\n ? 'Factory rule evaluation timed out.'\n : error instanceof Error\n ? error.message.slice(0, 2_000)\n : 'Factory Linear rule failed.',\n };\n }\n\n const committed = await this.options.storage.commitRuleEvaluation({\n orgId: input.orgId,\n factoryProjectId: input.factoryProjectId,\n workItemId: relatedItem?.id ?? null,\n ingress: { identity: ingressId, triggerType: 'linear.issueObserved' },\n configVersion: this.options.configVersion,\n expectedRevision: relatedItem?.revision ?? null,\n actor,\n outcome,\n decisions,\n causalChain: [],\n now: new Date(),\n });\n return committed.status;\n }\n}\n\nexport function attachLinearRules(\n linear: { readonly rules: LinearEventRules },\n context: IntegrationContext,\n): ((input: LinearRulesIngress) => Promise<unknown>) | undefined {\n if (!context.runtime) return undefined;\n const rules = new LinearRules({\n projects: context.storage.projects,\n storage: context.runtime.workItems,\n configVersion: context.runtime.configVersion,\n boards: context.runtime.boards,\n linearRules: linear.rules,\n });\n return input => rules.ingest(input);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra `apiRoutes` that let the Factory SPA connect and reconnect
|
|
3
|
+
* Platform-managed provider accounts without leaving Factory.
|
|
4
|
+
*
|
|
5
|
+
* The browser cannot talk to the integrations service itself — it has no
|
|
6
|
+
* Platform session — so these routes mint Nango connect/reconnect sessions
|
|
7
|
+
* server-side with the deploy's machine credentials
|
|
8
|
+
* (`MASTRA_PLATFORM_ACCESS_TOKEN`/`MASTRA_PLATFORM_SECRET_KEY`) and hand the
|
|
9
|
+
* short-lived session token back to the SPA, which drives the provider's
|
|
10
|
+
* OAuth popup headlessly via `@nangohq/frontend`. Gating mirrors the
|
|
11
|
+
* Platform's own rule for these providers: a signed-in organization member
|
|
12
|
+
* (org:write parity with Linear), not an organization admin.
|
|
13
|
+
*/
|
|
14
|
+
import type { ApiRoute } from '@mastra/core/server';
|
|
15
|
+
import type { RouteAuth } from '../../../routes/route.js';
|
|
16
|
+
import { PlatformApiClient } from '../api-client.js';
|
|
17
|
+
export interface PlatformConnectProvider {
|
|
18
|
+
/** Platform integration id used to mint connect sessions. */
|
|
19
|
+
integrationId: string;
|
|
20
|
+
/** Connection `integrationId` variants that belong to this provider. */
|
|
21
|
+
connectionIntegrationIds: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Providers the Factory SPA may connect through these routes. Keys are the
|
|
25
|
+
* SPA-facing provider slugs; `integrationId` is the Platform catalog id used
|
|
26
|
+
* for new connect sessions. Provider-specific PRs extend this registry when
|
|
27
|
+
* their Factory integrations are available.
|
|
28
|
+
*/
|
|
29
|
+
export declare const PLATFORM_CONNECT_PROVIDERS: Record<string, PlatformConnectProvider>;
|
|
30
|
+
export interface BuildPlatformConnectRoutesOptions {
|
|
31
|
+
auth: RouteAuth;
|
|
32
|
+
client: PlatformApiClient;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the `/web/integrations/platform/*` routes. Callers only mount these
|
|
36
|
+
* when Platform machine credentials are configured.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildPlatformConnectRoutes(options: BuildPlatformConnectRoutesOptions): ApiRoute[];
|
|
39
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/connect/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAoB,MAAM,kBAAkB,CAAC;AASvE,MAAM,WAAW,uBAAuB;IACtC,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,wBAAwB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAG9E,CAAC;AAmBF,MAAM,WAAW,iCAAiC;IAChD,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AA2CD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,iCAAiC,GAAG,QAAQ,EAAE,CAgFjG"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { PlatformApiError } from "../api-client.js";
|
|
2
|
+
import { registerApiRoute } from "@mastra/core/server";
|
|
3
|
+
//#region src/integrations/platform/connect/routes.ts
|
|
4
|
+
/** Erase a route handler's path-parameterized context to a plain `Context`. */
|
|
5
|
+
function loose(c) {
|
|
6
|
+
return c;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Providers the Factory SPA may connect through these routes. Keys are the
|
|
10
|
+
* SPA-facing provider slugs; `integrationId` is the Platform catalog id used
|
|
11
|
+
* for new connect sessions. Provider-specific PRs extend this registry when
|
|
12
|
+
* their Factory integrations are available.
|
|
13
|
+
*/
|
|
14
|
+
const PLATFORM_CONNECT_PROVIDERS = {
|
|
15
|
+
jira: {
|
|
16
|
+
integrationId: "jira",
|
|
17
|
+
connectionIntegrationIds: ["jira"]
|
|
18
|
+
},
|
|
19
|
+
"incident-io": {
|
|
20
|
+
integrationId: "incident-io",
|
|
21
|
+
connectionIntegrationIds: ["incident-io"]
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
async function resolveOrgTenant(c, auth) {
|
|
25
|
+
if (!auth.enabled()) return { response: c.json({
|
|
26
|
+
error: "auth_disabled",
|
|
27
|
+
message: "Factory auth is not enabled."
|
|
28
|
+
}, 403) };
|
|
29
|
+
await auth.ensureUser(c);
|
|
30
|
+
const tenant = auth.tenant(c);
|
|
31
|
+
if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
|
|
32
|
+
if (!tenant.orgId) return { response: c.json({
|
|
33
|
+
error: "organization_required",
|
|
34
|
+
message: "Provider connections require an organization. Personal accounts cannot connect providers."
|
|
35
|
+
}, 403) };
|
|
36
|
+
return { tenant: {
|
|
37
|
+
orgId: tenant.orgId,
|
|
38
|
+
userId: tenant.userId
|
|
39
|
+
} };
|
|
40
|
+
}
|
|
41
|
+
function providerFromParam(c) {
|
|
42
|
+
const provider = c.req.param("provider");
|
|
43
|
+
return provider ? PLATFORM_CONNECT_PROVIDERS[provider] ?? null : null;
|
|
44
|
+
}
|
|
45
|
+
function platformError(c, err) {
|
|
46
|
+
if (err instanceof PlatformApiError) {
|
|
47
|
+
if (err.status === 409) return c.json({
|
|
48
|
+
error: "session_pending",
|
|
49
|
+
message: err.message
|
|
50
|
+
}, 409);
|
|
51
|
+
if (err.status === 403) return c.json({
|
|
52
|
+
error: "platform_forbidden",
|
|
53
|
+
message: err.message
|
|
54
|
+
}, 403);
|
|
55
|
+
}
|
|
56
|
+
return c.json({
|
|
57
|
+
error: "platform_request_failed",
|
|
58
|
+
message: err instanceof Error ? err.message : String(err)
|
|
59
|
+
}, 502);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build the `/web/integrations/platform/*` routes. Callers only mount these
|
|
63
|
+
* when Platform machine credentials are configured.
|
|
64
|
+
*/
|
|
65
|
+
function buildPlatformConnectRoutes(options) {
|
|
66
|
+
const { auth, client } = options;
|
|
67
|
+
return [
|
|
68
|
+
registerApiRoute("/web/integrations/platform/:provider/connections", {
|
|
69
|
+
method: "GET",
|
|
70
|
+
requiresAuth: false,
|
|
71
|
+
handler: async (rawContext) => {
|
|
72
|
+
const c = loose(rawContext);
|
|
73
|
+
const provider = providerFromParam(c);
|
|
74
|
+
if (!provider) return c.json({ error: "unknown_provider" }, 404);
|
|
75
|
+
const resolved = await resolveOrgTenant(c, auth);
|
|
76
|
+
if ("response" in resolved) return resolved.response;
|
|
77
|
+
try {
|
|
78
|
+
const connections = (await client.request("GET", "/v2/connections")).connections.filter((connection) => provider.connectionIntegrationIds.includes(connection.integrationId));
|
|
79
|
+
return c.json({ connections });
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return platformError(c, err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}),
|
|
85
|
+
registerApiRoute("/web/integrations/platform/:provider/connect-session", {
|
|
86
|
+
method: "POST",
|
|
87
|
+
requiresAuth: false,
|
|
88
|
+
handler: async (rawContext) => {
|
|
89
|
+
const c = loose(rawContext);
|
|
90
|
+
const provider = providerFromParam(c);
|
|
91
|
+
if (!provider) return c.json({ error: "unknown_provider" }, 404);
|
|
92
|
+
const resolved = await resolveOrgTenant(c, auth);
|
|
93
|
+
if ("response" in resolved) return resolved.response;
|
|
94
|
+
try {
|
|
95
|
+
const session = await client.request("POST", `/v2/integrations/${encodeURIComponent(provider.integrationId)}/connect-sessions`, {});
|
|
96
|
+
c.header("Cache-Control", "no-store");
|
|
97
|
+
return c.json(session, 201);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return platformError(c, err);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
registerApiRoute("/web/integrations/platform/:provider/connections/:connectionId/reconnect-session", {
|
|
104
|
+
method: "POST",
|
|
105
|
+
requiresAuth: false,
|
|
106
|
+
handler: async (rawContext) => {
|
|
107
|
+
const c = loose(rawContext);
|
|
108
|
+
const provider = providerFromParam(c);
|
|
109
|
+
if (!provider) return c.json({ error: "unknown_provider" }, 404);
|
|
110
|
+
const resolved = await resolveOrgTenant(c, auth);
|
|
111
|
+
if ("response" in resolved) return resolved.response;
|
|
112
|
+
const connectionId = c.req.param("connectionId");
|
|
113
|
+
if (!connectionId) return c.json({ error: "connection_required" }, 400);
|
|
114
|
+
try {
|
|
115
|
+
if (!(await client.request("GET", "/v2/connections")).connections.find((candidate) => candidate.id === connectionId && provider.connectionIntegrationIds.includes(candidate.integrationId))) return c.json({ error: "connection_not_found" }, 404);
|
|
116
|
+
const session = await client.request("POST", `/v2/connections/${encodeURIComponent(connectionId)}/reconnect-session`, {});
|
|
117
|
+
c.header("Cache-Control", "no-store");
|
|
118
|
+
return c.json(session, 201);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return platformError(c, err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
export { PLATFORM_CONNECT_PROVIDERS, buildPlatformConnectRoutes };
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../../../src/integrations/platform/connect/routes.ts"],"sourcesContent":["/**\n * Mastra `apiRoutes` that let the Factory SPA connect and reconnect\n * Platform-managed provider accounts without leaving Factory.\n *\n * The browser cannot talk to the integrations service itself — it has no\n * Platform session — so these routes mint Nango connect/reconnect sessions\n * server-side with the deploy's machine credentials\n * (`MASTRA_PLATFORM_ACCESS_TOKEN`/`MASTRA_PLATFORM_SECRET_KEY`) and hand the\n * short-lived session token back to the SPA, which drives the provider's\n * OAuth popup headlessly via `@nangohq/frontend`. Gating mirrors the\n * Platform's own rule for these providers: a signed-in organization member\n * (org:write parity with Linear), not an organization admin.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { RouteAuth } from '../../../routes/route.js';\nimport { PlatformApiClient, PlatformApiError } from '../api-client.js';\n\ntype RouteContext = Context;\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): RouteContext {\n return c as RouteContext;\n}\n\nexport interface PlatformConnectProvider {\n /** Platform integration id used to mint connect sessions. */\n integrationId: string;\n /** Connection `integrationId` variants that belong to this provider. */\n connectionIntegrationIds: readonly string[];\n}\n\n/**\n * Providers the Factory SPA may connect through these routes. Keys are the\n * SPA-facing provider slugs; `integrationId` is the Platform catalog id used\n * for new connect sessions. Provider-specific PRs extend this registry when\n * their Factory integrations are available.\n */\nexport const PLATFORM_CONNECT_PROVIDERS: Record<string, PlatformConnectProvider> = {\n jira: { integrationId: 'jira', connectionIntegrationIds: ['jira'] },\n 'incident-io': { integrationId: 'incident-io', connectionIntegrationIds: ['incident-io'] },\n};\n\ninterface PlatformConnectionRow {\n id: string;\n integrationId: string;\n status: 'active' | 'needs_reauth';\n accountLabel: string | null;\n displayName?: string | null;\n connectedAt?: string;\n}\n\ninterface PlatformSessionResponse {\n connectionId: string;\n integrationId: string;\n connectUrl: string;\n sessionToken: string;\n expiresAt: string;\n}\n\nexport interface BuildPlatformConnectRoutesOptions {\n auth: RouteAuth;\n client: PlatformApiClient;\n}\n\nasync function resolveOrgTenant(\n c: RouteContext,\n auth: RouteAuth,\n): Promise<{ tenant: { orgId: string; userId: string } } | { response: Response }> {\n if (!auth.enabled()) {\n return { response: c.json({ error: 'auth_disabled', message: 'Factory auth is not enabled.' }, 403) };\n }\n await auth.ensureUser(c);\n const tenant = auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n {\n error: 'organization_required',\n message: 'Provider connections require an organization. Personal accounts cannot connect providers.',\n },\n 403,\n ),\n };\n }\n return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };\n}\n\nfunction providerFromParam(c: RouteContext): PlatformConnectProvider | null {\n const provider = c.req.param('provider');\n return provider ? (PLATFORM_CONNECT_PROVIDERS[provider] ?? null) : null;\n}\n\nfunction platformError(c: RouteContext, err: unknown) {\n if (err instanceof PlatformApiError) {\n if (err.status === 409) {\n return c.json({ error: 'session_pending', message: err.message }, 409);\n }\n if (err.status === 403) {\n return c.json({ error: 'platform_forbidden', message: err.message }, 403);\n }\n }\n return c.json({ error: 'platform_request_failed', message: err instanceof Error ? err.message : String(err) }, 502);\n}\n\n/**\n * Build the `/web/integrations/platform/*` routes. Callers only mount these\n * when Platform machine credentials are configured.\n */\nexport function buildPlatformConnectRoutes(options: BuildPlatformConnectRoutesOptions): ApiRoute[] {\n const { auth, client } = options;\n\n return [\n registerApiRoute('/web/integrations/platform/:provider/connections', {\n method: 'GET',\n requiresAuth: false,\n handler: async rawContext => {\n const c = loose(rawContext);\n const provider = providerFromParam(c);\n if (!provider) return c.json({ error: 'unknown_provider' }, 404);\n const resolved = await resolveOrgTenant(c, auth);\n if ('response' in resolved) return resolved.response;\n try {\n const result = await client.request<{ connections: PlatformConnectionRow[] }>('GET', '/v2/connections');\n const connections = result.connections.filter(connection =>\n provider.connectionIntegrationIds.includes(connection.integrationId),\n );\n return c.json({ connections });\n } catch (err) {\n return platformError(c, err);\n }\n },\n }),\n registerApiRoute('/web/integrations/platform/:provider/connect-session', {\n method: 'POST',\n requiresAuth: false,\n handler: async rawContext => {\n const c = loose(rawContext);\n const provider = providerFromParam(c);\n if (!provider) return c.json({ error: 'unknown_provider' }, 404);\n const resolved = await resolveOrgTenant(c, auth);\n if ('response' in resolved) return resolved.response;\n try {\n const session = await client.request<PlatformSessionResponse>(\n 'POST',\n `/v2/integrations/${encodeURIComponent(provider.integrationId)}/connect-sessions`,\n {},\n );\n c.header('Cache-Control', 'no-store');\n return c.json(session, 201);\n } catch (err) {\n return platformError(c, err);\n }\n },\n }),\n registerApiRoute('/web/integrations/platform/:provider/connections/:connectionId/reconnect-session', {\n method: 'POST',\n requiresAuth: false,\n handler: async rawContext => {\n const c = loose(rawContext);\n const provider = providerFromParam(c);\n if (!provider) return c.json({ error: 'unknown_provider' }, 404);\n const resolved = await resolveOrgTenant(c, auth);\n if ('response' in resolved) return resolved.response;\n const connectionId = c.req.param('connectionId');\n if (!connectionId) return c.json({ error: 'connection_required' }, 400);\n try {\n // Confirm the connection belongs to this provider before minting so\n // one provider's page cannot reconnect a different provider's\n // connection through this route family.\n const listed = await client.request<{ connections: PlatformConnectionRow[] }>('GET', '/v2/connections');\n const connection = listed.connections.find(\n candidate =>\n candidate.id === connectionId && provider.connectionIntegrationIds.includes(candidate.integrationId),\n );\n if (!connection) return c.json({ error: 'connection_not_found' }, 404);\n const session = await client.request<PlatformSessionResponse>(\n 'POST',\n `/v2/connections/${encodeURIComponent(connectionId)}/reconnect-session`,\n {},\n );\n c.header('Cache-Control', 'no-store');\n return c.json(session, 201);\n } catch (err) {\n return platformError(c, err);\n }\n },\n }),\n ];\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { listPullRequestSubscriptionsForWebhook, retirePullRequestSubscription } from "../../github/subscriptions.js";
|
|
2
|
-
import { dispatchGithubWebhook, isFactoryAppSender, resolveAuthorizedBots } from "../../github/webhook.js";
|
|
3
2
|
import { PlatformApiError } from "../api-client.js";
|
|
3
|
+
import { dispatchGithubWebhook, isFactoryAppSender, resolveAuthorizedBots } from "../../github/webhook.js";
|
|
4
4
|
import { NoopLeaseProvider, isLeaseProvider } from "@mastra/core/events";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
6
|
import { MastraWorker } from "@mastra/core/worker";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createGithubSubscriptionTools, parseCreatedPullRequest, subscribeCurrentSessionToPullRequest } from "../../github/session-subscriptions.js";
|
|
2
|
+
import { PlatformApiClient, PlatformApiError, logPlatformInfo, logPlatformWarn, platformApiClientConfigFromEnv } from "../api-client.js";
|
|
2
3
|
import { GithubAppIdentity } from "../../github/app-identity.js";
|
|
3
4
|
import { resolveGithubRules } from "../../github/default-rules.js";
|
|
4
5
|
import { attachGithubReconciler, attachGithubRules } from "../../github/rules.js";
|
|
@@ -7,7 +8,6 @@ import { reconcileInterval, reconciliationEnabled } from "../../github/reconcili
|
|
|
7
8
|
import { parseAuthorizedBotsEnv } from "../../github/webhook.js";
|
|
8
9
|
import { buildGithubRoutes } from "../../github/routes.js";
|
|
9
10
|
import { settleOrAbort } from "../../github/settle-or-abort.js";
|
|
10
|
-
import { PlatformApiClient, PlatformApiError, logPlatformInfo, logPlatformWarn, platformApiClientConfigFromEnv } from "../api-client.js";
|
|
11
11
|
import { PlatformGithubEventWorker } from "./event-worker.js";
|
|
12
12
|
import { registerApiRoute } from "@mastra/core/server";
|
|
13
13
|
//#region src/integrations/platform/github/integration.ts
|
|
@@ -1,17 +1,74 @@
|
|
|
1
|
+
import type { RequestContext } from '@mastra/core/request-context';
|
|
2
|
+
import type { ApiRoute } from '@mastra/core/server';
|
|
1
3
|
import type { MastraWorker } from '@mastra/core/worker';
|
|
2
|
-
import type {
|
|
4
|
+
import type { Intake } from '../../../capabilities/intake.js';
|
|
5
|
+
import type { RouteAuth } from '../../../routes/route.js';
|
|
6
|
+
import type { IntakeStorage } from '../../../storage/domains/intake/base.js';
|
|
7
|
+
import type { FactoryProjectsStorage } from '../../../storage/domains/projects/base.js';
|
|
8
|
+
import type { FactoryIntegration, IntegrationContext, IntegrationTools } from '../../base.js';
|
|
9
|
+
import type { IncidentioEventRules, IncidentioRuleOverrides } from '../../incidentio/default-rules.js';
|
|
3
10
|
import { type PlatformApiClientConfig } from '../api-client.js';
|
|
4
11
|
export interface PlatformIncidentioIntegrationConfig {
|
|
5
12
|
clientConfig?: PlatformApiClientConfig;
|
|
6
|
-
|
|
13
|
+
/** Per-event replacements for the default Factory incident.io rules. */
|
|
14
|
+
rules?: IncidentioRuleOverrides;
|
|
7
15
|
}
|
|
16
|
+
interface PlatformIntegrationConnection {
|
|
17
|
+
id: string;
|
|
18
|
+
integrationId: string;
|
|
19
|
+
status: 'active' | 'needs_reauth';
|
|
20
|
+
accountLabel: string | null;
|
|
21
|
+
}
|
|
22
|
+
interface ScopedSourceId {
|
|
23
|
+
connectionId: string;
|
|
24
|
+
sourceId: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function encodeScopedSourceId(connectionId: string, sourceId: string): string;
|
|
27
|
+
export declare function decodeScopedSourceId(value: string): ScopedSourceId | null;
|
|
8
28
|
export declare class PlatformIncidentioIntegration implements FactoryIntegration {
|
|
9
29
|
#private;
|
|
10
30
|
readonly id = "incidentio";
|
|
11
|
-
readonly intake: import("../../../capabilities/intake.js").Intake;
|
|
12
31
|
constructor(config?: PlatformIncidentioIntegrationConfig);
|
|
32
|
+
/** Event rules driving automatic follow-up materialization and close handling. */
|
|
33
|
+
get rules(): IncidentioEventRules;
|
|
34
|
+
initialize({ projects, auth, intake, }: {
|
|
35
|
+
projects: FactoryProjectsStorage;
|
|
36
|
+
auth: RouteAuth;
|
|
37
|
+
intake: IntakeStorage;
|
|
38
|
+
}): void;
|
|
39
|
+
/** Cross-integration intake selection/binding domain — agent-tool authorization. */
|
|
40
|
+
get intakeStorage(): IntakeStorage;
|
|
41
|
+
/** Factory projects domain — maps a session's resourceId to its owning org. */
|
|
42
|
+
get projects(): FactoryProjectsStorage;
|
|
43
|
+
/** Whether the host runs with web auth enabled; every surface is inert without it. */
|
|
44
|
+
get authEnabled(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Map a session's resourceId (the factory project id) to its owning org.
|
|
47
|
+
* Same caching semantics as the Jira integration: definitive misses are
|
|
48
|
+
* cached, transient database failures are not.
|
|
49
|
+
*/
|
|
50
|
+
resolveOrgId(resourceId: string): Promise<string | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Org-scoped agent tools: the read-only follow-up detail tool for sessions
|
|
53
|
+
* whose resource is a factory project with an active Platform-managed
|
|
54
|
+
* incident.io connection.
|
|
55
|
+
*/
|
|
56
|
+
agentTools(args: {
|
|
57
|
+
requestContext: RequestContext;
|
|
58
|
+
}): Promise<IntegrationTools>;
|
|
59
|
+
listConnections(): Promise<PlatformIntegrationConnection[]>;
|
|
60
|
+
hasActiveConnections(): Promise<boolean>;
|
|
61
|
+
clearCaches(): void;
|
|
62
|
+
readonly intake: Intake;
|
|
13
63
|
workers(ctx: IntegrationContext): MastraWorker[];
|
|
14
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The integration's HTTP surface: `/web/incidentio/*` Mastra `apiRoutes`
|
|
66
|
+
* (status + follow-up listing/detail for Intake), aggregated across every
|
|
67
|
+
* discovered Platform connection. A board-scoped listing also feeds the
|
|
68
|
+
* incident.io event rules.
|
|
69
|
+
*/
|
|
70
|
+
routes(ctx: IntegrationContext): ApiRoute[];
|
|
15
71
|
diagnostics(): Record<string, unknown>;
|
|
16
72
|
}
|
|
73
|
+
export {};
|
|
17
74
|
//# sourceMappingURL=integration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/incidentio/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../../../../src/integrations/platform/incidentio/integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD,OAAO,KAAK,EAIV,MAAM,EAUP,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2CAA2C,CAAC;AACxF,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAI9F,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAcvG,OAAO,EAAqD,KAAK,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAEnH,MAAM,WAAW,mCAAmC;IAClD,YAAY,CAAC,EAAE,uBAAuB,CAAC;IACvC,wEAAwE;IACxE,KAAK,CAAC,EAAE,uBAAuB,CAAC;CACjC;AAaD,UAAU,6BAA6B;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,QAAQ,GAAG,cAAc,CAAC;IAClC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,UAAU,cAAc;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAcD,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAYzE;AAoDD,qBAAa,6BAA8B,YAAW,kBAAkB;;IACtE,QAAQ,CAAC,EAAE,gBAAgB;gBAaf,MAAM,GAAE,mCAAwC;IAO5D,kFAAkF;IAClF,IAAI,KAAK,IAAI,oBAAoB,CAEhC;IAED,UAAU,CAAC,EACT,QAAQ,EACR,IAAI,EACJ,MAAM,GACP,EAAE;QACD,QAAQ,EAAE,sBAAsB,CAAC;QACjC,IAAI,EAAE,SAAS,CAAC;QAChB,MAAM,EAAE,aAAa,CAAC;KACvB,GAAG,IAAI;IAMR,oFAAoF;IACpF,IAAI,aAAa,IAAI,aAAa,CAKjC;IAED,+EAA+E;IAC/E,IAAI,QAAQ,IAAI,sBAAsB,CAKrC;IAED,sFAAsF;IACtF,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;;;OAIG;IACG,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAqB9D;;;;OAIG;IACG,UAAU,CAAC,IAAI,EAAE;QAAE,cAAc,EAAE,cAAc,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAK/E,eAAe,IAAI,OAAO,CAAC,6BAA6B,EAAE,CAAC;IAS3D,oBAAoB,IAAI,OAAO,CAAC,OAAO,CAAC;IAI9C,WAAW,IAAI,IAAI;IAiEnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAQrB;IA4JF,OAAO,CAAC,GAAG,EAAE,kBAAkB,GAAG,YAAY,EAAE;IAchD;;;;;OAKG;IACH,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,QAAQ,EAAE;IAS3C,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAGvC"}
|