@mastra/factory 0.0.0 → 0.1.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/LICENSE.md +30 -0
- package/dist/auth.d.ts +181 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +370 -0
- package/dist/auth.js.map +1 -0
- package/dist/capabilities/connection.d.ts +9 -0
- package/dist/capabilities/connection.d.ts.map +1 -0
- package/dist/capabilities/connection.js +1 -0
- package/dist/capabilities/connection.js.map +1 -0
- package/dist/capabilities/intake.d.ts +93 -0
- package/dist/capabilities/intake.d.ts.map +1 -0
- package/dist/capabilities/intake.js +1 -0
- package/dist/capabilities/intake.js.map +1 -0
- package/dist/capabilities/version-control.d.ts +239 -0
- package/dist/capabilities/version-control.d.ts.map +1 -0
- package/dist/capabilities/version-control.js +1 -0
- package/dist/capabilities/version-control.js.map +1 -0
- package/dist/factory.d.ts +145 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +38245 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38245 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/base.d.ts +197 -0
- package/dist/integrations/base.d.ts.map +1 -0
- package/dist/integrations/base.js +1 -0
- package/dist/integrations/base.js.map +1 -0
- package/dist/integrations/github/config.d.ts +60 -0
- package/dist/integrations/github/config.d.ts.map +1 -0
- package/dist/integrations/github/config.js +28 -0
- package/dist/integrations/github/config.js.map +1 -0
- package/dist/integrations/github/factory-session.d.ts +12 -0
- package/dist/integrations/github/factory-session.d.ts.map +1 -0
- package/dist/integrations/github/factory-session.js +41 -0
- package/dist/integrations/github/factory-session.js.map +1 -0
- package/dist/integrations/github/integration.d.ts +191 -0
- package/dist/integrations/github/integration.d.ts.map +1 -0
- package/dist/integrations/github/integration.js +3001 -0
- package/dist/integrations/github/integration.js.map +1 -0
- package/dist/integrations/github/issue-triage.d.ts +27 -0
- package/dist/integrations/github/issue-triage.d.ts.map +1 -0
- package/dist/integrations/github/issue-triage.js +82 -0
- package/dist/integrations/github/issue-triage.js.map +1 -0
- package/dist/integrations/github/project-lock.d.ts +78 -0
- package/dist/integrations/github/project-lock.d.ts.map +1 -0
- package/dist/integrations/github/project-lock.js +70 -0
- package/dist/integrations/github/project-lock.js.map +1 -0
- package/dist/integrations/github/provenance.d.ts +27 -0
- package/dist/integrations/github/provenance.d.ts.map +1 -0
- package/dist/integrations/github/provenance.js +104 -0
- package/dist/integrations/github/provenance.js.map +1 -0
- package/dist/integrations/github/routes.d.ts +72 -0
- package/dist/integrations/github/routes.d.ts.map +1 -0
- package/dist/integrations/github/routes.js +1834 -0
- package/dist/integrations/github/routes.js.map +1 -0
- package/dist/integrations/github/sandbox.d.ts +268 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -0
- package/dist/integrations/github/sandbox.js +381 -0
- package/dist/integrations/github/sandbox.js.map +1 -0
- package/dist/integrations/github/session-subscriptions.d.ts +23 -0
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/session-subscriptions.js +190 -0
- package/dist/integrations/github/session-subscriptions.js.map +1 -0
- package/dist/integrations/github/subscriptions.d.ts +54 -0
- package/dist/integrations/github/subscriptions.d.ts.map +1 -0
- package/dist/integrations/github/subscriptions.js +70 -0
- package/dist/integrations/github/subscriptions.js.map +1 -0
- package/dist/integrations/github/webhook.d.ts +87 -0
- package/dist/integrations/github/webhook.d.ts.map +1 -0
- package/dist/integrations/github/webhook.js +381 -0
- package/dist/integrations/github/webhook.js.map +1 -0
- package/dist/integrations/linear/agent-tools.d.ts +34 -0
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -0
- package/dist/integrations/linear/agent-tools.js +108 -0
- package/dist/integrations/linear/agent-tools.js.map +1 -0
- package/dist/integrations/linear/integration.d.ts +233 -0
- package/dist/integrations/linear/integration.d.ts.map +1 -0
- package/dist/integrations/linear/integration.js +962 -0
- package/dist/integrations/linear/integration.js.map +1 -0
- package/dist/integrations/linear/routes.d.ts +61 -0
- package/dist/integrations/linear/routes.d.ts.map +1 -0
- package/dist/integrations/linear/routes.js +242 -0
- package/dist/integrations/linear/routes.js.map +1 -0
- package/dist/integrations/linear/storage.d.ts +65 -0
- package/dist/integrations/linear/storage.d.ts.map +1 -0
- package/dist/integrations/linear/storage.js +1 -0
- package/dist/integrations/linear/storage.js.map +1 -0
- package/dist/integrations/platform/api-client.d.ts +24 -0
- package/dist/integrations/platform/api-client.d.ts.map +1 -0
- package/dist/integrations/platform/api-client.js +153 -0
- package/dist/integrations/platform/api-client.js.map +1 -0
- package/dist/integrations/platform/github/event-worker.d.ts +44 -0
- package/dist/integrations/platform/github/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/github/event-worker.js +585 -0
- package/dist/integrations/platform/github/event-worker.js.map +1 -0
- package/dist/integrations/platform/github/integration.d.ts +36 -0
- package/dist/integrations/platform/github/integration.d.ts.map +1 -0
- package/dist/integrations/platform/github/integration.js +3253 -0
- package/dist/integrations/platform/github/integration.js.map +1 -0
- package/dist/integrations/platform/linear/integration.d.ts +46 -0
- package/dist/integrations/platform/linear/integration.d.ts.map +1 -0
- package/dist/integrations/platform/linear/integration.js +891 -0
- package/dist/integrations/platform/linear/integration.js.map +1 -0
- package/dist/integrations/workos/integration.d.ts +39 -0
- package/dist/integrations/workos/integration.d.ts.map +1 -0
- package/dist/integrations/workos/integration.js +96 -0
- package/dist/integrations/workos/integration.js.map +1 -0
- package/dist/routes/config.d.ts +220 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +770 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/fs.d.ts +105 -0
- package/dist/routes/fs.d.ts.map +1 -0
- package/dist/routes/fs.js +294 -0
- package/dist/routes/fs.js.map +1 -0
- package/dist/routes/intake.d.ts +23 -0
- package/dist/routes/intake.d.ts.map +1 -0
- package/dist/routes/intake.js +179 -0
- package/dist/routes/intake.js.map +1 -0
- package/dist/routes/oauth.d.ts +43 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +25058 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/projects.d.ts +19 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +395 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/provider-credentials.d.ts +81 -0
- package/dist/routes/provider-credentials.d.ts.map +1 -0
- package/dist/routes/provider-credentials.js +68 -0
- package/dist/routes/provider-credentials.js.map +1 -0
- package/dist/routes/route.d.ts +48 -0
- package/dist/routes/route.d.ts.map +1 -0
- package/dist/routes/route.js +11 -0
- package/dist/routes/route.js.map +1 -0
- package/dist/routes/skills.d.ts +30 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +181 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/surface.d.ts +83 -0
- package/dist/routes/surface.d.ts.map +1 -0
- package/dist/routes/surface.js +28519 -0
- package/dist/routes/surface.js.map +1 -0
- package/dist/routes/tenant-credentials.d.ts +74 -0
- package/dist/routes/tenant-credentials.d.ts.map +1 -0
- package/dist/routes/tenant-credentials.js +181 -0
- package/dist/routes/tenant-credentials.js.map +1 -0
- package/dist/routes/work-items.d.ts +40 -0
- package/dist/routes/work-items.d.ts.map +1 -0
- package/dist/routes/work-items.js +902 -0
- package/dist/routes/work-items.js.map +1 -0
- package/dist/rules/binding-context.d.ts +5 -0
- package/dist/rules/binding-context.d.ts.map +1 -0
- package/dist/rules/binding-context.js +40 -0
- package/dist/rules/binding-context.js.map +1 -0
- package/dist/rules/defaults.d.ts +9 -0
- package/dist/rules/defaults.d.ts.map +1 -0
- package/dist/rules/defaults.js +334 -0
- package/dist/rules/defaults.js.map +1 -0
- package/dist/rules/dispatcher.d.ts +40 -0
- package/dist/rules/dispatcher.d.ts.map +1 -0
- package/dist/rules/dispatcher.js +695 -0
- package/dist/rules/dispatcher.js.map +1 -0
- package/dist/rules/github-service.d.ts +29 -0
- package/dist/rules/github-service.d.ts.map +1 -0
- package/dist/rules/github-service.js +474 -0
- package/dist/rules/github-service.js.map +1 -0
- package/dist/rules/index.d.ts +7 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +583 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/linear-service.d.ts +27 -0
- package/dist/rules/linear-service.d.ts.map +1 -0
- package/dist/rules/linear-service.js +343 -0
- package/dist/rules/linear-service.js.map +1 -0
- package/dist/rules/processor.d.ts +54 -0
- package/dist/rules/processor.d.ts.map +1 -0
- package/dist/rules/processor.js +629 -0
- package/dist/rules/processor.js.map +1 -0
- package/dist/rules/resolve.d.ts +17 -0
- package/dist/rules/resolve.d.ts.map +1 -0
- package/dist/rules/resolve.js +27 -0
- package/dist/rules/resolve.js.map +1 -0
- package/dist/rules/start-coordinator.d.ts +58 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -0
- package/dist/rules/start-coordinator.js +136 -0
- package/dist/rules/start-coordinator.js.map +1 -0
- package/dist/rules/tools.d.ts +10 -0
- package/dist/rules/tools.d.ts.map +1 -0
- package/dist/rules/tools.js +93 -0
- package/dist/rules/tools.js.map +1 -0
- package/dist/rules/transition-service.d.ts +32 -0
- package/dist/rules/transition-service.d.ts.map +1 -0
- package/dist/rules/transition-service.js +474 -0
- package/dist/rules/transition-service.js.map +1 -0
- package/dist/rules/types.d.ts +225 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +33 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/validation.d.ts +11 -0
- package/dist/rules/validation.d.ts.map +1 -0
- package/dist/rules/validation.js +310 -0
- package/dist/rules/validation.js.map +1 -0
- package/dist/sandbox/fleet.d.ts +208 -0
- package/dist/sandbox/fleet.d.ts.map +1 -0
- package/dist/sandbox/fleet.js +259 -0
- package/dist/sandbox/fleet.js.map +1 -0
- package/dist/sandbox/reattach.d.ts +3 -0
- package/dist/sandbox/reattach.d.ts.map +1 -0
- package/dist/sandbox/reattach.js +9 -0
- package/dist/sandbox/reattach.js.map +1 -0
- package/dist/server-error.d.ts +12 -0
- package/dist/server-error.d.ts.map +1 -0
- package/dist/server-error.js +20 -0
- package/dist/server-error.js.map +1 -0
- package/dist/skills/service.d.ts +48 -0
- package/dist/skills/service.d.ts.map +1 -0
- package/dist/skills/service.js +45 -0
- package/dist/skills/service.js.map +1 -0
- package/dist/spa-static.d.ts +34 -0
- package/dist/spa-static.d.ts.map +1 -0
- package/dist/spa-static.js +73 -0
- package/dist/spa-static.js.map +1 -0
- package/dist/state-signing.d.ts +48 -0
- package/dist/state-signing.d.ts.map +1 -0
- package/dist/state-signing.js +48 -0
- package/dist/state-signing.js.map +1 -0
- package/dist/storage/domains/audit/agent-audit.d.ts +27 -0
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -0
- package/dist/storage/domains/audit/agent-audit.js +65 -0
- package/dist/storage/domains/audit/agent-audit.js.map +1 -0
- package/dist/storage/domains/audit/base.d.ts +140 -0
- package/dist/storage/domains/audit/base.d.ts.map +1 -0
- package/dist/storage/domains/audit/base.js +127 -0
- package/dist/storage/domains/audit/base.js.map +1 -0
- package/dist/storage/domains/audit/domain.d.ts +69 -0
- package/dist/storage/domains/audit/domain.d.ts.map +1 -0
- package/dist/storage/domains/audit/domain.js +165 -0
- package/dist/storage/domains/audit/domain.js.map +1 -0
- package/dist/storage/domains/credentials/base.d.ts +134 -0
- package/dist/storage/domains/credentials/base.d.ts.map +1 -0
- package/dist/storage/domains/credentials/base.js +257 -0
- package/dist/storage/domains/credentials/base.js.map +1 -0
- package/dist/storage/domains/intake/base.d.ts +38 -0
- package/dist/storage/domains/intake/base.d.ts.map +1 -0
- package/dist/storage/domains/intake/base.js +62 -0
- package/dist/storage/domains/intake/base.js.map +1 -0
- package/dist/storage/domains/integrations/base.d.ts +126 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -0
- package/dist/storage/domains/integrations/base.js +252 -0
- package/dist/storage/domains/integrations/base.js.map +1 -0
- package/dist/storage/domains/model-packs/base.d.ts +49 -0
- package/dist/storage/domains/model-packs/base.d.ts.map +1 -0
- package/dist/storage/domains/model-packs/base.js +95 -0
- package/dist/storage/domains/model-packs/base.js.map +1 -0
- package/dist/storage/domains/projects/base.d.ts +55 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -0
- package/dist/storage/domains/projects/base.js +99 -0
- package/dist/storage/domains/projects/base.js.map +1 -0
- package/dist/storage/domains/queue-health/base.d.ts +52 -0
- package/dist/storage/domains/queue-health/base.d.ts.map +1 -0
- package/dist/storage/domains/queue-health/base.js +87 -0
- package/dist/storage/domains/queue-health/base.js.map +1 -0
- package/dist/storage/domains/source-control/base.d.ts +288 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -0
- package/dist/storage/domains/source-control/base.js +722 -0
- package/dist/storage/domains/source-control/base.js.map +1 -0
- package/dist/storage/domains/source-control/inmemory.d.ts +158 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -0
- package/dist/storage/domains/source-control/inmemory.js +348 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -0
- package/dist/storage/domains/work-items/base.d.ts +412 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -0
- package/dist/storage/domains/work-items/base.js +1174 -0
- package/dist/storage/domains/work-items/base.js.map +1 -0
- package/dist/storage/domains/work-items/metrics.d.ts +101 -0
- package/dist/storage/domains/work-items/metrics.d.ts.map +1 -0
- package/dist/storage/domains/work-items/metrics.js +203 -0
- package/dist/storage/domains/work-items/metrics.js.map +1 -0
- package/dist/workspace.d.ts +20 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +364 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +19 -19
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
// src/integrations/platform/linear/integration.ts
|
|
2
|
+
import { registerApiRoute as registerApiRoute2 } from "@mastra/core/server";
|
|
3
|
+
|
|
4
|
+
// src/integrations/linear/agent-tools.ts
|
|
5
|
+
import { createTool } from "@mastra/core/tools";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// src/integrations/linear/routes.ts
|
|
9
|
+
import { registerApiRoute } from "@mastra/core/server";
|
|
10
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
+
function loose(c) {
|
|
12
|
+
return c;
|
|
13
|
+
}
|
|
14
|
+
async function resolveOrgTenant(c, auth) {
|
|
15
|
+
await auth.ensureUser(c);
|
|
16
|
+
const tenant = auth.tenant(c);
|
|
17
|
+
if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
|
|
18
|
+
if (!tenant.orgId) {
|
|
19
|
+
return {
|
|
20
|
+
response: c.json(
|
|
21
|
+
{
|
|
22
|
+
error: "organization_required",
|
|
23
|
+
message: "Linear intake requires an organization. Personal accounts cannot connect Linear."
|
|
24
|
+
},
|
|
25
|
+
403
|
|
26
|
+
)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { tenant: { orgId: tenant.orgId, userId: tenant.userId } };
|
|
30
|
+
}
|
|
31
|
+
function parseAfterCursor(raw) {
|
|
32
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
33
|
+
if (raw.length > 512 || !/^[\w+/=.:-]+$/.test(raw)) return null;
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
|
36
|
+
function linearFetchError(c, err) {
|
|
37
|
+
if (err instanceof LinearReauthRequiredError || err.status === 401) {
|
|
38
|
+
return c.json({ error: "linear_reauth_required", message: new LinearReauthRequiredError().message }, 409);
|
|
39
|
+
}
|
|
40
|
+
return c.json({ error: "linear_fetch_failed", message: err instanceof Error ? err.message : String(err) }, 502);
|
|
41
|
+
}
|
|
42
|
+
function buildLinearRoutes(options) {
|
|
43
|
+
const routes = [];
|
|
44
|
+
const { linear, auth, stateSigner, intake } = options;
|
|
45
|
+
const enabled = Boolean(linear) && auth.enabled();
|
|
46
|
+
const diagnostics = () => ({
|
|
47
|
+
linearAppConfigured: Boolean(linear),
|
|
48
|
+
factoryAuthEnabled: auth.enabled(),
|
|
49
|
+
appDbConfigured: true
|
|
50
|
+
});
|
|
51
|
+
routes.push(
|
|
52
|
+
registerApiRoute("/web/linear/status", {
|
|
53
|
+
method: "GET",
|
|
54
|
+
requiresAuth: false,
|
|
55
|
+
handler: async (c) => {
|
|
56
|
+
if (!enabled || !linear || !stateSigner) {
|
|
57
|
+
return c.json({
|
|
58
|
+
enabled: false,
|
|
59
|
+
connected: false,
|
|
60
|
+
workspace: null,
|
|
61
|
+
reason: "missing_config",
|
|
62
|
+
diagnostics: diagnostics()
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
await auth.ensureUser(loose(c));
|
|
66
|
+
const tenant = auth.tenant(loose(c));
|
|
67
|
+
if (!tenant) return c.json({ error: "unauthorized", reason: "auth_required" }, 401);
|
|
68
|
+
if (!tenant.orgId) {
|
|
69
|
+
return c.json({
|
|
70
|
+
enabled: true,
|
|
71
|
+
organizationRequired: true,
|
|
72
|
+
connected: false,
|
|
73
|
+
workspace: null,
|
|
74
|
+
reason: "organization_required",
|
|
75
|
+
diagnostics: diagnostics()
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const connection = await linear.loadConnection(tenant.orgId);
|
|
79
|
+
return c.json({
|
|
80
|
+
enabled: true,
|
|
81
|
+
connected: Boolean(connection),
|
|
82
|
+
workspace: connection ? { name: connection.workspaceName, urlKey: connection.workspaceUrlKey } : null,
|
|
83
|
+
reason: connection ? "ready" : "not_connected",
|
|
84
|
+
diagnostics: diagnostics()
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
if (!enabled || !linear || !stateSigner || !intake) {
|
|
90
|
+
return routes;
|
|
91
|
+
}
|
|
92
|
+
const redirectUri = options.redirectUri ?? `${(options.baseUrl ?? "").replace(/\/$/, "")}/auth/linear/callback`;
|
|
93
|
+
routes.push(
|
|
94
|
+
registerApiRoute("/auth/linear/connect", {
|
|
95
|
+
method: "GET",
|
|
96
|
+
requiresAuth: false,
|
|
97
|
+
handler: async (c) => {
|
|
98
|
+
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
99
|
+
if ("response" in resolved) return resolved.response;
|
|
100
|
+
const state = stateSigner.sign(resolved.tenant.orgId, resolved.tenant.userId);
|
|
101
|
+
return c.redirect(linear.buildAuthorizeUrl(state, redirectUri));
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
routes.push(
|
|
106
|
+
registerApiRoute("/auth/linear/callback", {
|
|
107
|
+
method: "GET",
|
|
108
|
+
requiresAuth: false,
|
|
109
|
+
handler: async (c) => {
|
|
110
|
+
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
111
|
+
if ("response" in resolved) return resolved.response;
|
|
112
|
+
const { orgId, userId } = resolved.tenant;
|
|
113
|
+
const stateTenant = stateSigner.verify(c.req.query("state"));
|
|
114
|
+
if (!stateTenant || stateTenant.userId !== userId || stateTenant.orgId !== orgId) {
|
|
115
|
+
console.warn("[Linear] OAuth callback rejected: state/tenant mismatch.");
|
|
116
|
+
return c.redirect("/?linear=error");
|
|
117
|
+
}
|
|
118
|
+
const code = c.req.query("code");
|
|
119
|
+
if (!code) {
|
|
120
|
+
return c.redirect("/?linear=error");
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const tokens = await linear.exchangeOAuthCode(code, redirectUri);
|
|
124
|
+
const workspace = await linear.fetchWorkspace(tokens.accessToken);
|
|
125
|
+
await linear.upsertConnection({
|
|
126
|
+
orgId,
|
|
127
|
+
userId,
|
|
128
|
+
accessToken: tokens.accessToken,
|
|
129
|
+
refreshToken: tokens.refreshToken,
|
|
130
|
+
expiresAt: tokens.expiresAt,
|
|
131
|
+
scope: tokens.scope,
|
|
132
|
+
workspaceName: workspace.name,
|
|
133
|
+
workspaceUrlKey: workspace.urlKey
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.warn(`[Linear] OAuth callback failed to persist connection for org ${orgId}.`, error);
|
|
137
|
+
return c.redirect("/?linear=error");
|
|
138
|
+
}
|
|
139
|
+
return c.redirect("/?linear=connected");
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
routes.push(
|
|
144
|
+
registerApiRoute("/web/linear/projects", {
|
|
145
|
+
method: "GET",
|
|
146
|
+
requiresAuth: false,
|
|
147
|
+
handler: async (c) => {
|
|
148
|
+
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
149
|
+
if ("response" in resolved) return resolved.response;
|
|
150
|
+
const connection = await linear.loadConnection(resolved.tenant.orgId);
|
|
151
|
+
if (!connection) {
|
|
152
|
+
return c.json({ error: "linear_not_connected", message: "Connect Linear to list Linear projects." }, 409);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const accessToken = await linear.getFreshAccessToken(connection);
|
|
156
|
+
const projects = await linear.listProjects(accessToken);
|
|
157
|
+
return c.json({ projects });
|
|
158
|
+
} catch (err) {
|
|
159
|
+
return linearFetchError(loose(c), err);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
routes.push(
|
|
165
|
+
registerApiRoute("/web/linear/issues", {
|
|
166
|
+
method: "GET",
|
|
167
|
+
requiresAuth: false,
|
|
168
|
+
handler: async (c) => {
|
|
169
|
+
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
170
|
+
if ("response" in resolved) return resolved.response;
|
|
171
|
+
const after = parseAfterCursor(c.req.query("after"));
|
|
172
|
+
if (after === null) return c.json({ error: "invalid_cursor" }, 400);
|
|
173
|
+
const factoryProjectId = c.req.query("factoryProjectId");
|
|
174
|
+
if (factoryProjectId && !UUID_RE.test(factoryProjectId)) {
|
|
175
|
+
return c.json({ error: "invalid_factory_project_id" }, 400);
|
|
176
|
+
}
|
|
177
|
+
const connection = await linear.loadConnection(resolved.tenant.orgId);
|
|
178
|
+
if (!connection) {
|
|
179
|
+
return c.json({ error: "linear_not_connected", message: "Connect Linear to see intake issues." }, 409);
|
|
180
|
+
}
|
|
181
|
+
await intake.ensureReady();
|
|
182
|
+
const config = await intake.getConfig({
|
|
183
|
+
orgId: resolved.tenant.orgId,
|
|
184
|
+
userId: resolved.tenant.userId,
|
|
185
|
+
integrationIds: ["linear"]
|
|
186
|
+
});
|
|
187
|
+
const selection = config.linear;
|
|
188
|
+
if (!selection.enabled) {
|
|
189
|
+
return c.json({ error: "linear_intake_disabled", message: "Linear intake is turned off in Settings." }, 404);
|
|
190
|
+
}
|
|
191
|
+
const projectIds = selection.sourceIds ?? [];
|
|
192
|
+
if (projectIds.length === 0) {
|
|
193
|
+
return c.json({ issues: [], nextCursor: null });
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const accessToken = await linear.getFreshAccessToken(connection);
|
|
197
|
+
const { issues, nextCursor } = await linear.intake.listIssues({
|
|
198
|
+
connection: { type: "oauth", accessToken },
|
|
199
|
+
sourceIds: projectIds,
|
|
200
|
+
cursor: after
|
|
201
|
+
});
|
|
202
|
+
const issuePayload = issues.map((issue) => ({
|
|
203
|
+
id: issue.id,
|
|
204
|
+
identifier: issue.identifier,
|
|
205
|
+
title: issue.title,
|
|
206
|
+
url: issue.url,
|
|
207
|
+
state: issue.state ?? "",
|
|
208
|
+
stateType: issue.stateType ?? "",
|
|
209
|
+
priorityLabel: issue.priority ?? "",
|
|
210
|
+
assignee: issue.assignee,
|
|
211
|
+
team: issue.source,
|
|
212
|
+
labels: issue.labels,
|
|
213
|
+
createdAt: issue.createdAt,
|
|
214
|
+
updatedAt: issue.updatedAt
|
|
215
|
+
}));
|
|
216
|
+
if (factoryProjectId && options.hooks?.ingestLinearIssues) {
|
|
217
|
+
await options.hooks.ingestLinearIssues({
|
|
218
|
+
orgId: resolved.tenant.orgId,
|
|
219
|
+
userId: resolved.tenant.userId,
|
|
220
|
+
factoryProjectId,
|
|
221
|
+
issues: issuePayload
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return c.json({ issues: issuePayload, nextCursor });
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return linearFetchError(loose(c), err);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
return routes;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/integrations/linear/integration.ts
|
|
235
|
+
var LinearReauthRequiredError = class extends Error {
|
|
236
|
+
constructor() {
|
|
237
|
+
super("Linear authorization expired. Reconnect Linear to keep syncing intake issues.");
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/integrations/linear/agent-tools.ts
|
|
242
|
+
function createLinearGetIssueTool(linear, orgId) {
|
|
243
|
+
return createTool({
|
|
244
|
+
id: "linear_get_issue",
|
|
245
|
+
description: "Fetch a Linear issue's full details \u2014 title, description, state, assignee, labels, priority, and discussion comments. Use this whenever you're working on a Linear issue (e.g. ENG-123) to get its complete context.",
|
|
246
|
+
inputSchema: z.object({
|
|
247
|
+
issue: z.string().min(1).describe('The Linear issue identifier (e.g. "ENG-123") or issue UUID.')
|
|
248
|
+
}),
|
|
249
|
+
execute: async ({ issue }) => {
|
|
250
|
+
const connection = await linear.loadConnection(orgId);
|
|
251
|
+
if (!connection) {
|
|
252
|
+
return { error: "Linear is not connected for this repository. Connect Linear in Settings to fetch issues." };
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const accessToken = await linear.getFreshAccessToken(connection);
|
|
256
|
+
const detail = await linear.intake.getIssue({
|
|
257
|
+
connection: { type: "oauth", accessToken },
|
|
258
|
+
issueId: issue.trim()
|
|
259
|
+
});
|
|
260
|
+
if (!detail) {
|
|
261
|
+
return { error: `Linear issue "${issue}" was not found in this workspace.` };
|
|
262
|
+
}
|
|
263
|
+
return detail;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err instanceof LinearReauthRequiredError) {
|
|
266
|
+
return { error: err.message };
|
|
267
|
+
}
|
|
268
|
+
return { error: `Failed to fetch Linear issue: ${err instanceof Error ? err.message : String(err)}` };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function createLinearCommentTool(linear, orgId) {
|
|
274
|
+
return createTool({
|
|
275
|
+
id: "linear_create_comment",
|
|
276
|
+
description: "Post a comment on a Linear issue (e.g. to report investigation findings, link a PR, or ask a clarifying question). The comment is posted as the connected Linear integration, so make clear it comes from the agent.",
|
|
277
|
+
inputSchema: z.object({
|
|
278
|
+
issue: z.string().min(1).describe('The Linear issue identifier (e.g. "ENG-123") or issue UUID.'),
|
|
279
|
+
body: z.string().min(1).describe("The comment body, as Linear-flavored markdown.")
|
|
280
|
+
}),
|
|
281
|
+
execute: async ({ issue, body }) => {
|
|
282
|
+
const connection = await linear.loadConnection(orgId);
|
|
283
|
+
if (!connection) {
|
|
284
|
+
return { error: "Linear is not connected for this repository. Connect Linear in Settings to post comments." };
|
|
285
|
+
}
|
|
286
|
+
if (!linear.canPostComments(connection)) {
|
|
287
|
+
return {
|
|
288
|
+
error: "The Linear connection does not have comment permissions. Reconnect Linear in Settings to grant them."
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const accessToken = await linear.getFreshAccessToken(connection);
|
|
293
|
+
const comment = await linear.intake.createComment({
|
|
294
|
+
connection: { type: "oauth", accessToken },
|
|
295
|
+
issueId: issue.trim(),
|
|
296
|
+
body
|
|
297
|
+
});
|
|
298
|
+
if (!comment) {
|
|
299
|
+
return { error: `Linear issue "${issue}" was not found in this workspace.` };
|
|
300
|
+
}
|
|
301
|
+
return { posted: true, url: comment.url };
|
|
302
|
+
} catch (err) {
|
|
303
|
+
if (err instanceof LinearReauthRequiredError) {
|
|
304
|
+
return { error: err.message };
|
|
305
|
+
}
|
|
306
|
+
return { error: `Failed to post Linear comment: ${err instanceof Error ? err.message : String(err)}` };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
async function buildLinearAgentTools({
|
|
312
|
+
requestContext,
|
|
313
|
+
linear
|
|
314
|
+
}) {
|
|
315
|
+
if (!linear.authEnabled) return {};
|
|
316
|
+
const ctx = requestContext.get("controller");
|
|
317
|
+
const resourceId = ctx?.resourceId;
|
|
318
|
+
if (!resourceId) return {};
|
|
319
|
+
const orgId = await linear.resolveOrgId(resourceId);
|
|
320
|
+
if (!orgId) return {};
|
|
321
|
+
const check = await linear.checkConnection(orgId);
|
|
322
|
+
if (!check.connected) return {};
|
|
323
|
+
return {
|
|
324
|
+
linear_get_issue: createLinearGetIssueTool(linear, orgId),
|
|
325
|
+
// Only offered when the granted OAuth scope allows posting comments —
|
|
326
|
+
// connections made before `comments:create` was requested are read-only
|
|
327
|
+
// until the org reconnects Linear.
|
|
328
|
+
...check.canComment ? { linear_create_comment: createLinearCommentTool(linear, orgId) } : {}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/integrations/platform/api-client.ts
|
|
333
|
+
function platformApiClientConfigFromEnv() {
|
|
334
|
+
const sharedApiUrl = process.env.MASTRA_SHARED_API_URL?.trim() || "https://platform.mastra.ai/v1";
|
|
335
|
+
const accessToken = process.env.MASTRA_PLATFORM_SECRET_KEY?.trim();
|
|
336
|
+
if (!accessToken) {
|
|
337
|
+
throw new Error("Platform integration: missing required environment variable MASTRA_PLATFORM_SECRET_KEY.");
|
|
338
|
+
}
|
|
339
|
+
return { baseUrl: normalizeSharedApiUrl(sharedApiUrl), accessToken };
|
|
340
|
+
}
|
|
341
|
+
function normalizeSharedApiUrl(sharedApiUrl) {
|
|
342
|
+
return sharedApiUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
343
|
+
}
|
|
344
|
+
var PlatformApiError = class extends Error {
|
|
345
|
+
status;
|
|
346
|
+
retryAfterSeconds;
|
|
347
|
+
constructor(message, status, retryAfterSeconds = null) {
|
|
348
|
+
super(message);
|
|
349
|
+
this.name = "PlatformApiError";
|
|
350
|
+
this.status = status;
|
|
351
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var PlatformApiClient = class {
|
|
355
|
+
#baseUrl;
|
|
356
|
+
#accessToken;
|
|
357
|
+
#fetch;
|
|
358
|
+
constructor(config) {
|
|
359
|
+
const missing = ["baseUrl", "accessToken"].filter((field) => !config[field]);
|
|
360
|
+
if (missing.length > 0) {
|
|
361
|
+
throw new Error(`Platform integration: missing required config field(s): ${missing.join(", ")}.`);
|
|
362
|
+
}
|
|
363
|
+
this.#baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
364
|
+
this.#accessToken = config.accessToken;
|
|
365
|
+
this.#fetch = config.fetchImpl ?? globalThis.fetch;
|
|
366
|
+
}
|
|
367
|
+
async request(method, path, body, options) {
|
|
368
|
+
const response = await this.#send(method, path, body, options);
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
const message = redact(await extractError(response), this.#accessToken);
|
|
371
|
+
const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after"));
|
|
372
|
+
logPlatformError("Platform API request failed", {
|
|
373
|
+
method,
|
|
374
|
+
path,
|
|
375
|
+
status: response.status,
|
|
376
|
+
retryAfterSeconds,
|
|
377
|
+
message
|
|
378
|
+
});
|
|
379
|
+
throw new PlatformApiError(message, response.status, retryAfterSeconds);
|
|
380
|
+
}
|
|
381
|
+
if (response.status === 204) return void 0;
|
|
382
|
+
return await response.json();
|
|
383
|
+
}
|
|
384
|
+
async requestRedirect(method, path, options) {
|
|
385
|
+
const response = await this.#send(method, path, void 0, options, "manual");
|
|
386
|
+
if (response.status >= 300 && response.status < 400) {
|
|
387
|
+
const location = response.headers.get("location");
|
|
388
|
+
if (location) return location;
|
|
389
|
+
}
|
|
390
|
+
if (!response.ok) {
|
|
391
|
+
const message = redact(await extractError(response), this.#accessToken);
|
|
392
|
+
const retryAfterSeconds = parseRetryAfter(response.headers.get("retry-after"));
|
|
393
|
+
logPlatformError("Platform API redirect request failed", {
|
|
394
|
+
method,
|
|
395
|
+
path,
|
|
396
|
+
status: response.status,
|
|
397
|
+
retryAfterSeconds,
|
|
398
|
+
message
|
|
399
|
+
});
|
|
400
|
+
throw new PlatformApiError(message, response.status, retryAfterSeconds);
|
|
401
|
+
}
|
|
402
|
+
logPlatformError("Platform API request did not return a redirect", { method, path, status: response.status });
|
|
403
|
+
throw new PlatformApiError("Platform API request did not return a redirect.", response.status);
|
|
404
|
+
}
|
|
405
|
+
async #send(method, path, body, options, redirect) {
|
|
406
|
+
const headers = {
|
|
407
|
+
accept: "application/json",
|
|
408
|
+
authorization: `Bearer ${this.#accessToken}`
|
|
409
|
+
};
|
|
410
|
+
const timeoutSignal = AbortSignal.timeout(15e3);
|
|
411
|
+
const init = {
|
|
412
|
+
method,
|
|
413
|
+
headers,
|
|
414
|
+
redirect,
|
|
415
|
+
signal: options?.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal
|
|
416
|
+
};
|
|
417
|
+
if (body !== void 0) {
|
|
418
|
+
headers["content-type"] = "application/json";
|
|
419
|
+
init.body = JSON.stringify(body);
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
return await this.#fetch(`${this.#baseUrl}${path}`, init);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (error instanceof Error && error.message.includes(this.#accessToken)) {
|
|
425
|
+
const redacted = new Error(redact(error.message, this.#accessToken));
|
|
426
|
+
redacted.name = error.name;
|
|
427
|
+
logPlatformError("Platform API transport error", {
|
|
428
|
+
method,
|
|
429
|
+
path,
|
|
430
|
+
name: redacted.name,
|
|
431
|
+
message: redacted.message
|
|
432
|
+
});
|
|
433
|
+
throw redacted;
|
|
434
|
+
}
|
|
435
|
+
logPlatformError("Platform API transport error", {
|
|
436
|
+
method,
|
|
437
|
+
path,
|
|
438
|
+
name: error instanceof Error ? error.name : void 0,
|
|
439
|
+
message: error instanceof Error ? error.message : String(error)
|
|
440
|
+
});
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
async function extractError(response) {
|
|
446
|
+
try {
|
|
447
|
+
const data = await response.clone().json();
|
|
448
|
+
for (const field of ["detail", "error", "title"]) {
|
|
449
|
+
if (typeof data[field] === "string" && data[field]) return data[field];
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
return `Platform API request failed (${response.status})`;
|
|
454
|
+
}
|
|
455
|
+
function redact(message, accessToken) {
|
|
456
|
+
return message.split(accessToken).join("[REDACTED]");
|
|
457
|
+
}
|
|
458
|
+
function logPlatformInfo(message, fields) {
|
|
459
|
+
writePlatformLog("info", message, fields);
|
|
460
|
+
}
|
|
461
|
+
function logPlatformError(message, fields) {
|
|
462
|
+
writePlatformLog("error", message, fields);
|
|
463
|
+
}
|
|
464
|
+
function writePlatformLog(level, message, fields) {
|
|
465
|
+
const metadata = fields ? ` ${JSON.stringify(stripUndefined(fields))}` : "";
|
|
466
|
+
process.stderr.write(`[MastraCode Web] ${level.toUpperCase()} ${message}${metadata}
|
|
467
|
+
`);
|
|
468
|
+
}
|
|
469
|
+
function stripUndefined(fields) {
|
|
470
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== void 0));
|
|
471
|
+
}
|
|
472
|
+
function parseRetryAfter(value) {
|
|
473
|
+
if (!value) return null;
|
|
474
|
+
const seconds = Number.parseInt(value, 10);
|
|
475
|
+
return Number.isSafeInteger(seconds) && seconds >= 0 ? seconds : null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/integrations/platform/linear/integration.ts
|
|
479
|
+
var API_PREFIX = "/v1/server/linear";
|
|
480
|
+
var PAGE_SIZE = 30;
|
|
481
|
+
var MAX_REFERENCE_PAGES = 20;
|
|
482
|
+
var MAX_COMMENT_PAGES = 20;
|
|
483
|
+
var PLATFORM_MANAGED_CONNECTION_TOKEN = "platform-managed";
|
|
484
|
+
function loose2(c) {
|
|
485
|
+
return c;
|
|
486
|
+
}
|
|
487
|
+
function routeBaseUrl(ctx, requestUrl) {
|
|
488
|
+
return (ctx.baseUrl || new URL(requestUrl).origin).replace(/\/+$/, "");
|
|
489
|
+
}
|
|
490
|
+
var PlatformLinearIntegration = class {
|
|
491
|
+
id = "linear";
|
|
492
|
+
#client;
|
|
493
|
+
#endpointHost;
|
|
494
|
+
#projects;
|
|
495
|
+
#auth;
|
|
496
|
+
intake = {
|
|
497
|
+
listSources: async () => {
|
|
498
|
+
const sources = await this.#listProjectSources();
|
|
499
|
+
return sources.map(({ workspace, project }) => ({
|
|
500
|
+
id: encodeSourceId(workspace.linearWorkspaceId, project.id),
|
|
501
|
+
name: project.name,
|
|
502
|
+
type: "project",
|
|
503
|
+
metadata: {
|
|
504
|
+
workspaceId: workspace.linearWorkspaceId,
|
|
505
|
+
workspaceName: workspace.linearWorkspaceName,
|
|
506
|
+
workspaceUrlKey: workspace.urlKey,
|
|
507
|
+
state: project.state,
|
|
508
|
+
teams: project.teams
|
|
509
|
+
}
|
|
510
|
+
}));
|
|
511
|
+
},
|
|
512
|
+
listItems: async ({ sourceIds, cursor }) => {
|
|
513
|
+
const result = await this.#listIssues(sourceIds, cursor);
|
|
514
|
+
return {
|
|
515
|
+
items: result.issues.map(({ issue, source }) => ({
|
|
516
|
+
source: { type: "issue", externalId: issue.id, url: issue.url },
|
|
517
|
+
sourceId: encodeSourceId(source.workspace.linearWorkspaceId, source.project.id),
|
|
518
|
+
title: issue.title,
|
|
519
|
+
status: issue.state.name,
|
|
520
|
+
labels: issue.labels.map((label) => label.name),
|
|
521
|
+
assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,
|
|
522
|
+
createdAt: issue.createdAt,
|
|
523
|
+
updatedAt: issue.updatedAt,
|
|
524
|
+
metadata: {
|
|
525
|
+
identifier: issue.identifier,
|
|
526
|
+
workspaceId: source.workspace.linearWorkspaceId,
|
|
527
|
+
workspaceName: source.workspace.linearWorkspaceName,
|
|
528
|
+
projectId: source.project.id,
|
|
529
|
+
projectName: source.project.name,
|
|
530
|
+
team: issue.team.key,
|
|
531
|
+
priority: issue.priorityLabel
|
|
532
|
+
}
|
|
533
|
+
})),
|
|
534
|
+
nextCursor: result.nextCursor
|
|
535
|
+
};
|
|
536
|
+
},
|
|
537
|
+
listIssues: async ({ connection, sourceIds, labels, cursor }) => {
|
|
538
|
+
requireLinearConnection(connection);
|
|
539
|
+
const result = await this.#listIssues(sourceIds, cursor, labels);
|
|
540
|
+
return {
|
|
541
|
+
issues: result.issues.map(({ issue }) => parseIssue(issue)),
|
|
542
|
+
nextCursor: result.nextCursor
|
|
543
|
+
};
|
|
544
|
+
},
|
|
545
|
+
getIssue: async ({ connection, sourceId, issueId }) => {
|
|
546
|
+
requireLinearConnection(connection);
|
|
547
|
+
const located = await this.#findIssue(sourceId, issueId);
|
|
548
|
+
if (!located) return null;
|
|
549
|
+
const comments = await this.#loadComments(located.workspaceId, issueId, located.issue.comments);
|
|
550
|
+
return parseIssueDetail(located.issue, comments);
|
|
551
|
+
},
|
|
552
|
+
createComment: async ({ connection, sourceId, issueId, body }) => {
|
|
553
|
+
requireLinearConnection(connection);
|
|
554
|
+
const workspaceId = await this.#resolveWorkspaceForIssue(sourceId, issueId);
|
|
555
|
+
if (!workspaceId) return null;
|
|
556
|
+
try {
|
|
557
|
+
const comment = await this.#client.request(
|
|
558
|
+
"POST",
|
|
559
|
+
`${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments`,
|
|
560
|
+
{ body }
|
|
561
|
+
);
|
|
562
|
+
return { id: comment.id, url: comment.url };
|
|
563
|
+
} catch (error) {
|
|
564
|
+
if (isNotFound(error)) return null;
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
constructor() {
|
|
570
|
+
const config = platformApiClientConfigFromEnv();
|
|
571
|
+
this.#client = new PlatformApiClient(config);
|
|
572
|
+
this.#endpointHost = new URL(config.baseUrl).host;
|
|
573
|
+
}
|
|
574
|
+
get storage() {
|
|
575
|
+
const now = /* @__PURE__ */ new Date();
|
|
576
|
+
return {
|
|
577
|
+
integrationId: this.id,
|
|
578
|
+
connections: {
|
|
579
|
+
get: async (orgId) => ({
|
|
580
|
+
id: `platform-linear:${orgId}`,
|
|
581
|
+
orgId,
|
|
582
|
+
userId: null,
|
|
583
|
+
data: {
|
|
584
|
+
accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,
|
|
585
|
+
refreshToken: null,
|
|
586
|
+
expiresAtMs: null,
|
|
587
|
+
scope: "read,comments:create",
|
|
588
|
+
workspaceName: null,
|
|
589
|
+
workspaceUrlKey: null
|
|
590
|
+
},
|
|
591
|
+
metadata: {},
|
|
592
|
+
createdAt: now,
|
|
593
|
+
updatedAt: now
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
get projects() {
|
|
599
|
+
if (!this.#projects) throw new Error("PlatformLinearIntegration projects storage has not been initialized.");
|
|
600
|
+
return this.#projects;
|
|
601
|
+
}
|
|
602
|
+
initialize({ projects, auth }) {
|
|
603
|
+
this.#projects = projects;
|
|
604
|
+
this.#auth = auth;
|
|
605
|
+
logPlatformInfo("Platform Linear integration initialized", { endpointHost: this.#endpointHost });
|
|
606
|
+
}
|
|
607
|
+
get authEnabled() {
|
|
608
|
+
return this.#auth?.enabled() ?? false;
|
|
609
|
+
}
|
|
610
|
+
async resolveOrgId(resourceId) {
|
|
611
|
+
try {
|
|
612
|
+
const project = await this.projects.getById({ id: resourceId });
|
|
613
|
+
return project?.orgId ?? null;
|
|
614
|
+
} catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async loadConnection(orgId) {
|
|
619
|
+
const workspace = (await this.#listWorkspaces())[0];
|
|
620
|
+
if (!workspace) return null;
|
|
621
|
+
const now = /* @__PURE__ */ new Date();
|
|
622
|
+
return {
|
|
623
|
+
id: `platform-linear:${orgId}`,
|
|
624
|
+
orgId,
|
|
625
|
+
userId: null,
|
|
626
|
+
accessToken: PLATFORM_MANAGED_CONNECTION_TOKEN,
|
|
627
|
+
scope: "read,comments:create",
|
|
628
|
+
refreshToken: null,
|
|
629
|
+
expiresAt: null,
|
|
630
|
+
workspaceName: workspace.linearWorkspaceName,
|
|
631
|
+
workspaceUrlKey: workspace.urlKey,
|
|
632
|
+
createdAt: now,
|
|
633
|
+
updatedAt: now
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
async getFreshAccessToken(_connection) {
|
|
637
|
+
return PLATFORM_MANAGED_CONNECTION_TOKEN;
|
|
638
|
+
}
|
|
639
|
+
canPostComments(connection) {
|
|
640
|
+
const scopes = (connection.scope ?? "").split(/[\s,]+/).filter(Boolean);
|
|
641
|
+
return scopes.some((scope) => scope === "comments:create" || scope === "write" || scope === "admin");
|
|
642
|
+
}
|
|
643
|
+
async checkConnection(orgId) {
|
|
644
|
+
const connection = await this.loadConnection(orgId);
|
|
645
|
+
return {
|
|
646
|
+
connected: connection !== null,
|
|
647
|
+
canComment: connection !== null && this.canPostComments(connection),
|
|
648
|
+
checkedAt: Date.now()
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
routes(ctx) {
|
|
652
|
+
return [
|
|
653
|
+
this.#connectRoute(ctx),
|
|
654
|
+
...buildLinearRoutes({
|
|
655
|
+
auth: ctx.auth,
|
|
656
|
+
linear: this,
|
|
657
|
+
stateSigner: ctx.stateSigner,
|
|
658
|
+
baseUrl: ctx.baseUrl,
|
|
659
|
+
intake: ctx.storage.intake
|
|
660
|
+
}).filter((route) => !route.path.startsWith("/auth/linear/"))
|
|
661
|
+
];
|
|
662
|
+
}
|
|
663
|
+
#connectRoute(ctx) {
|
|
664
|
+
return registerApiRoute2("/auth/linear/connect", {
|
|
665
|
+
method: "GET",
|
|
666
|
+
requiresAuth: false,
|
|
667
|
+
handler: async (c) => {
|
|
668
|
+
await ctx.auth.ensureUser(loose2(c));
|
|
669
|
+
const tenant = ctx.auth.tenant(loose2(c));
|
|
670
|
+
if (!tenant?.orgId) return c.json({ error: "unauthorized" }, 401);
|
|
671
|
+
const returnTo = c.req.query("return_to") || "/";
|
|
672
|
+
const originator = routeBaseUrl(ctx, c.req.url);
|
|
673
|
+
logPlatformInfo("Starting Platform Linear connect flow", {
|
|
674
|
+
orgId: tenant.orgId,
|
|
675
|
+
returnTo,
|
|
676
|
+
originator
|
|
677
|
+
});
|
|
678
|
+
const query = new URLSearchParams({ return_to: returnTo, originator });
|
|
679
|
+
const location = await this.#client.requestRedirect("GET", `${API_PREFIX}/authorize?${query}`);
|
|
680
|
+
return c.redirect(location);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
async agentTools({ requestContext }) {
|
|
685
|
+
return buildLinearAgentTools({ requestContext, linear: this });
|
|
686
|
+
}
|
|
687
|
+
diagnostics() {
|
|
688
|
+
return { mode: "platform", endpointHost: this.#endpointHost };
|
|
689
|
+
}
|
|
690
|
+
async listProjects() {
|
|
691
|
+
return (await this.#listProjectSources()).map(({ workspace, project }) => ({
|
|
692
|
+
...project,
|
|
693
|
+
id: encodeSourceId(workspace.linearWorkspaceId, project.id),
|
|
694
|
+
workspaceId: workspace.linearWorkspaceId
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
async #listProjectSources() {
|
|
698
|
+
const workspaces = await this.#listWorkspaces();
|
|
699
|
+
const projectGroups = await Promise.all(
|
|
700
|
+
workspaces.map(async (workspace) => {
|
|
701
|
+
const projects = [];
|
|
702
|
+
let after;
|
|
703
|
+
for (let page = 0; page < MAX_REFERENCE_PAGES; page += 1) {
|
|
704
|
+
const query = new URLSearchParams({ first: "200" });
|
|
705
|
+
if (after) query.set("after", after);
|
|
706
|
+
const result = await this.#client.request(
|
|
707
|
+
"GET",
|
|
708
|
+
`${API_PREFIX}/workspaces/${encodeURIComponent(workspace.linearWorkspaceId)}/projects?${query}`
|
|
709
|
+
);
|
|
710
|
+
projects.push(...result.projects);
|
|
711
|
+
if (!result.pageInfo.hasNextPage || !result.pageInfo.endCursor) break;
|
|
712
|
+
after = result.pageInfo.endCursor;
|
|
713
|
+
}
|
|
714
|
+
return projects.map((project) => ({ workspace, project }));
|
|
715
|
+
})
|
|
716
|
+
);
|
|
717
|
+
return projectGroups.flat();
|
|
718
|
+
}
|
|
719
|
+
async #listWorkspaces() {
|
|
720
|
+
const result = await this.#client.request("GET", `${API_PREFIX}/workspaces`);
|
|
721
|
+
return result.workspaces.filter((workspace) => workspace.connected);
|
|
722
|
+
}
|
|
723
|
+
async #listIssues(sourceIds, cursor, labels) {
|
|
724
|
+
if (sourceIds.length === 0)
|
|
725
|
+
return { issues: [], nextCursor: null };
|
|
726
|
+
const sources = await this.#listProjectSources();
|
|
727
|
+
const sourceMap = new Map(
|
|
728
|
+
sources.map((source) => [encodeSourceId(source.workspace.linearWorkspaceId, source.project.id), source])
|
|
729
|
+
);
|
|
730
|
+
const selected = sourceIds.map((sourceId) => sourceMap.get(sourceId)).filter((source) => !!source);
|
|
731
|
+
const cursors = decodeCursor(cursor, sourceIds);
|
|
732
|
+
const normalizedLabels = normalizeLabels(labels);
|
|
733
|
+
const nextState = {};
|
|
734
|
+
let hasNextPage = false;
|
|
735
|
+
const pages = await Promise.all(
|
|
736
|
+
selected.map(async (source) => {
|
|
737
|
+
const sourceId = encodeSourceId(source.workspace.linearWorkspaceId, source.project.id);
|
|
738
|
+
if (cursors[sourceId] === null) {
|
|
739
|
+
nextState[sourceId] = null;
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
const query = new URLSearchParams({
|
|
743
|
+
first: String(PAGE_SIZE),
|
|
744
|
+
projectIds: source.project.id,
|
|
745
|
+
stateType: "triage,backlog,unstarted,started",
|
|
746
|
+
orderBy: "updatedAt"
|
|
747
|
+
});
|
|
748
|
+
const after = cursors[sourceId];
|
|
749
|
+
if (after) query.set("after", after);
|
|
750
|
+
const result = await this.#client.request(
|
|
751
|
+
"GET",
|
|
752
|
+
`${API_PREFIX}/workspaces/${encodeURIComponent(source.workspace.linearWorkspaceId)}/issues?${query}`
|
|
753
|
+
);
|
|
754
|
+
const next = result.pageInfo.hasNextPage ? result.pageInfo.endCursor : null;
|
|
755
|
+
nextState[sourceId] = next;
|
|
756
|
+
hasNextPage ||= next !== null;
|
|
757
|
+
return result.issues.filter(
|
|
758
|
+
(issue) => normalizedLabels.length === 0 || issue.labels.some((label) => normalizedLabels.includes(label.name))
|
|
759
|
+
).map((issue) => ({ issue, source }));
|
|
760
|
+
})
|
|
761
|
+
);
|
|
762
|
+
return {
|
|
763
|
+
issues: pages.flat(),
|
|
764
|
+
nextCursor: hasNextPage ? encodeCursor(nextState, sourceIds) : null
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
async #findIssue(sourceId, issueId) {
|
|
768
|
+
const workspaceIds = await this.#candidateWorkspaceIds(sourceId);
|
|
769
|
+
for (const workspaceId of workspaceIds) {
|
|
770
|
+
try {
|
|
771
|
+
const issue = await this.#client.request(
|
|
772
|
+
"GET",
|
|
773
|
+
`${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}?include=comments`
|
|
774
|
+
);
|
|
775
|
+
return { workspaceId, issue };
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (!isNotFound(error)) throw error;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
async #resolveWorkspaceForIssue(sourceId, issueId) {
|
|
783
|
+
const workspaceIds = await this.#candidateWorkspaceIds(sourceId);
|
|
784
|
+
if (workspaceIds.length === 1) return workspaceIds[0];
|
|
785
|
+
for (const workspaceId of workspaceIds) {
|
|
786
|
+
try {
|
|
787
|
+
await this.#client.request(
|
|
788
|
+
"GET",
|
|
789
|
+
`${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}`
|
|
790
|
+
);
|
|
791
|
+
return workspaceId;
|
|
792
|
+
} catch (error) {
|
|
793
|
+
if (!isNotFound(error)) throw error;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
async #candidateWorkspaceIds(sourceId) {
|
|
799
|
+
if (sourceId) return [decodeSourceId(sourceId).workspaceId];
|
|
800
|
+
return (await this.#listWorkspaces()).map((workspace) => workspace.linearWorkspaceId);
|
|
801
|
+
}
|
|
802
|
+
async #loadComments(workspaceId, issueId, embedded) {
|
|
803
|
+
const comments = [...embedded?.nodes ?? []];
|
|
804
|
+
let pageInfo = embedded?.pageInfo;
|
|
805
|
+
let page = 0;
|
|
806
|
+
while (pageInfo?.hasNextPage && pageInfo.endCursor && page < MAX_COMMENT_PAGES) {
|
|
807
|
+
const result = await this.#client.request(
|
|
808
|
+
"GET",
|
|
809
|
+
`${API_PREFIX}/workspaces/${encodeURIComponent(workspaceId)}/issues/${encodeURIComponent(issueId)}/comments?first=200&after=${encodeURIComponent(pageInfo.endCursor)}`
|
|
810
|
+
);
|
|
811
|
+
comments.push(...result.comments);
|
|
812
|
+
pageInfo = result.pageInfo;
|
|
813
|
+
page += 1;
|
|
814
|
+
}
|
|
815
|
+
return comments;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
function parseIssue(issue) {
|
|
819
|
+
return {
|
|
820
|
+
id: issue.id,
|
|
821
|
+
identifier: issue.identifier,
|
|
822
|
+
title: issue.title,
|
|
823
|
+
url: issue.url,
|
|
824
|
+
author: issue.creator?.displayName ?? issue.creator?.name ?? null,
|
|
825
|
+
state: issue.state.name,
|
|
826
|
+
stateType: issue.state.type,
|
|
827
|
+
priority: issue.priorityLabel,
|
|
828
|
+
assignee: issue.assignee?.displayName ?? issue.assignee?.name ?? null,
|
|
829
|
+
source: issue.team.key,
|
|
830
|
+
labels: issue.labels.map((label) => label.name),
|
|
831
|
+
commentCount: null,
|
|
832
|
+
createdAt: issue.createdAt,
|
|
833
|
+
updatedAt: issue.updatedAt
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function parseIssueDetail(issue, comments) {
|
|
837
|
+
return {
|
|
838
|
+
...parseIssue(issue),
|
|
839
|
+
commentCount: comments.length,
|
|
840
|
+
description: issue.description?.trim() ? issue.description : null,
|
|
841
|
+
comments: comments.map((comment) => ({
|
|
842
|
+
author: comment.user?.displayName ?? comment.user?.name ?? null,
|
|
843
|
+
body: comment.body,
|
|
844
|
+
createdAt: comment.createdAt
|
|
845
|
+
}))
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function encodeSourceId(workspaceId, projectId) {
|
|
849
|
+
return `linear-project:${Buffer.from(JSON.stringify({ workspaceId, projectId })).toString("base64url")}`;
|
|
850
|
+
}
|
|
851
|
+
function decodeSourceId(sourceId) {
|
|
852
|
+
if (!sourceId.startsWith("linear-project:")) throw new Error("Linear project source id is invalid.");
|
|
853
|
+
try {
|
|
854
|
+
const parsed = JSON.parse(Buffer.from(sourceId.slice("linear-project:".length), "base64url").toString("utf8"));
|
|
855
|
+
if (typeof parsed.workspaceId !== "string" || !parsed.workspaceId) throw new Error();
|
|
856
|
+
if (typeof parsed.projectId !== "string" || !parsed.projectId) throw new Error();
|
|
857
|
+
return { workspaceId: parsed.workspaceId, projectId: parsed.projectId };
|
|
858
|
+
} catch {
|
|
859
|
+
throw new Error("Linear project source id is invalid.");
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function normalizeLabels(labels) {
|
|
863
|
+
return [...new Set((labels ?? []).map((label) => label.trim()).filter(Boolean))];
|
|
864
|
+
}
|
|
865
|
+
function decodeCursor(cursor, sourceIds) {
|
|
866
|
+
if (!cursor) return {};
|
|
867
|
+
if (sourceIds.length === 1) return { [sourceIds[0]]: cursor };
|
|
868
|
+
try {
|
|
869
|
+
const parsed = JSON.parse(cursor);
|
|
870
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
|
|
871
|
+
return parsed;
|
|
872
|
+
} catch {
|
|
873
|
+
throw new Error("Linear cursor is invalid.");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function encodeCursor(state, sourceIds) {
|
|
877
|
+
if (sourceIds.length === 1) return state[sourceIds[0]];
|
|
878
|
+
return JSON.stringify(state);
|
|
879
|
+
}
|
|
880
|
+
function requireLinearConnection(connection) {
|
|
881
|
+
if (connection.type !== "oauth") {
|
|
882
|
+
throw new Error("Linear capabilities require an OAuth connection.");
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
function isNotFound(error) {
|
|
886
|
+
return error instanceof PlatformApiError && error.status === 404;
|
|
887
|
+
}
|
|
888
|
+
export {
|
|
889
|
+
PlatformLinearIntegration
|
|
890
|
+
};
|
|
891
|
+
//# sourceMappingURL=integration.js.map
|