@mastra/factory 0.2.2-alpha.1 → 0.2.2-alpha.2
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 +10 -0
- package/dist/auth.js +401 -334
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/connection.js +1 -1
- package/dist/capabilities/intake.js +1 -1
- package/dist/capabilities/version-control.js +1 -1
- package/dist/factory.js +441 -39622
- package/dist/factory.js.map +1 -1
- package/dist/index.js +2 -39633
- package/dist/integrations/base.js +1 -1
- package/dist/integrations/github/config.js +36 -22
- package/dist/integrations/github/config.js.map +1 -1
- package/dist/integrations/github/factory-session.js +33 -37
- package/dist/integrations/github/factory-session.js.map +1 -1
- package/dist/integrations/github/integration.js +1037 -3672
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-triage.js +71 -71
- package/dist/integrations/github/issue-triage.js.map +1 -1
- package/dist/integrations/github/pat.js +47 -36
- package/dist/integrations/github/pat.js.map +1 -1
- package/dist/integrations/github/provenance.js +72 -101
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.js +1272 -1881
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +203 -471
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +400 -345
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +165 -223
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.js +43 -51
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.js +15 -18
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/github/webhook.js +359 -343
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/agent-tools.js +77 -100
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js +611 -1309
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.js +227 -230
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.js +126 -346
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/linear/storage.js +1 -1
- package/dist/integrations/platform/api-client.js +131 -135
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +275 -573
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +845 -4022
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js +457 -1309
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/workos/integration.js +80 -85
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +933 -912
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/custom-provider-source.js +110 -93
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.js +401 -349
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +165 -165
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +447 -25166
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +437 -376
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.js +76 -58
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/route.js +12 -8
- package/dist/routes/route.js.map +1 -1
- package/dist/routes/skills.js +160 -173
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/surface.js +250 -28615
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js +141 -166
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js +654 -862
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +23 -35
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.js +200 -309
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.js +438 -683
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.js +5 -599
- package/dist/rules/processor.js +344 -596
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.js +21 -18
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.js +168 -157
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +50 -85
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js +183 -454
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.js +33 -29
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.js +266 -281
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/fleet.js +272 -250
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/sandbox/reattach.js +13 -6
- package/dist/sandbox/reattach.js.map +1 -1
- package/dist/server-error.js +12 -16
- package/dist/server-error.js.map +1 -1
- package/dist/skills/service.js +30 -35
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.js +91 -65
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +73 -44
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +60 -59
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js +169 -112
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +155 -153
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/credentials/base.js +259 -243
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +138 -143
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js +75 -57
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js +310 -245
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js +117 -102
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.js +89 -88
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js +97 -92
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js +111 -76
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.js +777 -706
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +322 -348
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.js +1266 -1148
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js +202 -184
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/timing.js +29 -22
- package/dist/timing.js.map +1 -1
- package/dist/workspace.js +236 -477
- package/dist/workspace.js.map +1 -1
- package/package.json +8 -8
- package/dist/capabilities/connection.js.map +0 -1
- package/dist/capabilities/intake.js.map +0 -1
- package/dist/capabilities/version-control.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/base.js.map +0 -1
- package/dist/integrations/linear/storage.js.map +0 -1
- package/dist/rules/index.js.map +0 -1
|
@@ -1,250 +1,192 @@
|
|
|
1
|
-
|
|
1
|
+
import { getGithubPat } from "./pat.js";
|
|
2
|
+
import { subscribeToPullRequest, unsubscribeFromPullRequest } from "./subscriptions.js";
|
|
3
|
+
import { getRegisteredGithubPatKind, injectGithubToken } from "./token-refresh.js";
|
|
2
4
|
import { createTool } from "@mastra/core/tools";
|
|
3
5
|
import { z } from "zod";
|
|
4
|
-
|
|
5
|
-
// src/integrations/github/pat.ts
|
|
6
|
-
var PAT_SETTINGS_USER_ID = "__github_org_settings__";
|
|
7
|
-
function asToken(value) {
|
|
8
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
9
|
-
}
|
|
10
|
-
async function getGithubPat(getStorage, orgId, kind = "default") {
|
|
11
|
-
try {
|
|
12
|
-
const settings = await getStorage().settings.get(orgId, PAT_SETTINGS_USER_ID);
|
|
13
|
-
if (!settings) return null;
|
|
14
|
-
if (kind === "reviewer") return asToken(settings.reviewerPat) ?? asToken(settings.pat);
|
|
15
|
-
return asToken(settings.pat);
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// src/integrations/github/subscriptions.ts
|
|
22
|
-
function changeRequestTargetKey(input) {
|
|
23
|
-
return `change-request:${input.installationExternalId}:${input.repositoryExternalId}:${input.changeRequestId}`;
|
|
24
|
-
}
|
|
25
|
-
function sameSession(row, input) {
|
|
26
|
-
return row.orgId === input.orgId && row.sessionId === input.sessionId && row.resourceId === input.resourceId && row.threadId === input.threadId && (row.sessionScope ?? "") === (input.sessionScope ?? "");
|
|
27
|
-
}
|
|
28
|
-
async function subscribeToPullRequest(input, storage) {
|
|
29
|
-
const targetKey = changeRequestTargetKey(input);
|
|
30
|
-
const existing = (await storage.subscriptions.listByTarget(targetKey)).find((row) => sameSession(row, input));
|
|
31
|
-
if (existing) {
|
|
32
|
-
if (existing.status !== "open") await storage.subscriptions.updateStatus(existing.id, "open");
|
|
33
|
-
return { ...existing, status: "open" };
|
|
34
|
-
}
|
|
35
|
-
return storage.subscriptions.create({
|
|
36
|
-
orgId: input.orgId,
|
|
37
|
-
targetKey,
|
|
38
|
-
sessionId: input.sessionId,
|
|
39
|
-
resourceId: input.resourceId,
|
|
40
|
-
threadId: input.threadId,
|
|
41
|
-
sessionScope: input.sessionScope ?? "",
|
|
42
|
-
status: "open",
|
|
43
|
-
data: {
|
|
44
|
-
installationExternalId: input.installationExternalId,
|
|
45
|
-
projectRepositoryId: input.projectRepositoryId,
|
|
46
|
-
repositoryExternalId: input.repositoryExternalId,
|
|
47
|
-
repositorySlug: input.repositorySlug,
|
|
48
|
-
changeRequestId: input.changeRequestId,
|
|
49
|
-
ownerId: input.ownerId,
|
|
50
|
-
source: input.source,
|
|
51
|
-
subscribedByUserId: input.subscribedByUserId ?? null
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
async function unsubscribeFromPullRequest(input, storage) {
|
|
56
|
-
const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));
|
|
57
|
-
await Promise.all(rows.filter((row) => sameSession(row, input)).map((row) => storage.subscriptions.delete(row.id)));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// src/integrations/github/token-refresh.ts
|
|
61
|
-
var GITHUB_TOKEN_INJECTOR_CONTEXT_KEY = "factoryGithubTokenInjector";
|
|
62
|
-
var GITHUB_PAT_KIND_CONTEXT_KEY = "factoryGithubPatKind";
|
|
63
|
-
function getRegisteredGithubPatKind(requestContext) {
|
|
64
|
-
const kind = requestContext.get(GITHUB_PAT_KIND_CONTEXT_KEY);
|
|
65
|
-
return kind === "reviewer" ? "reviewer" : "default";
|
|
66
|
-
}
|
|
67
|
-
function injectGithubToken(requestContext, token) {
|
|
68
|
-
const injector = requestContext.get(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY);
|
|
69
|
-
if (!injector) {
|
|
70
|
-
throw new Error("GitHub token refresh requires an active Factory sandbox workspace.");
|
|
71
|
-
}
|
|
72
|
-
injector(token);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// src/integrations/github/session-subscriptions.ts
|
|
6
|
+
//#region src/integrations/github/session-subscriptions.ts
|
|
76
7
|
function sessionUserId(user) {
|
|
77
|
-
|
|
8
|
+
return user?.workosId ?? user?.id;
|
|
78
9
|
}
|
|
79
10
|
function sessionOrgId(user) {
|
|
80
|
-
|
|
11
|
+
return user?.organizationId;
|
|
81
12
|
}
|
|
82
|
-
|
|
83
|
-
pullRequest: z.union([z.number().int().positive(), z.string().min(1)])
|
|
84
|
-
});
|
|
13
|
+
const pullRequestInputSchema = z.object({ pullRequest: z.union([z.number().int().positive(), z.string().min(1)]) });
|
|
85
14
|
function parsePullRequest(value, expectedRepo) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
15
|
+
if (typeof value === "number") return value;
|
|
16
|
+
if (/^\d+$/.test(value)) return Number(value);
|
|
17
|
+
const match = value.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)\/?$/i);
|
|
18
|
+
if (!match || match[1].toLowerCase() !== expectedRepo.toLowerCase()) throw new Error(`Pull request must belong to ${expectedRepo}.`);
|
|
19
|
+
return Number(match[2]);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Whether the current request comes from a session that GitHub subscriptions
|
|
23
|
+
* can ever apply to: an authenticated org user on a GitHub-project session
|
|
24
|
+
* with an active thread. Mirrors the gate in `resolveSessionTarget` without
|
|
25
|
+
* throwing, for passive callers that should no-op instead of erroring.
|
|
26
|
+
*/
|
|
94
27
|
function isGithubProjectSession(requestContext) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
context?.threadId && context.getState().projectRepositoryId && sessionOrgId(user) && sessionUserId(user)
|
|
99
|
-
);
|
|
28
|
+
const context = requestContext.get("controller");
|
|
29
|
+
const user = requestContext.get("user");
|
|
30
|
+
return Boolean(context?.threadId && context.getState().projectRepositoryId && sessionOrgId(user) && sessionUserId(user));
|
|
100
31
|
}
|
|
101
32
|
async function resolveSessionTarget(requestContext, github) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
33
|
+
const context = requestContext.get("controller");
|
|
34
|
+
const user = requestContext.get("user");
|
|
35
|
+
const orgId = sessionOrgId(user);
|
|
36
|
+
const userId = sessionUserId(user);
|
|
37
|
+
const projectRepositoryId = context?.getState().projectRepositoryId;
|
|
38
|
+
if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) throw new Error("GitHub subscriptions require an authenticated repository session with an active thread.");
|
|
39
|
+
const projectRepository = await github.sourceControlStorage.projectRepositories.get({
|
|
40
|
+
orgId,
|
|
41
|
+
id: projectRepositoryId
|
|
42
|
+
});
|
|
43
|
+
if (!projectRepository) throw new Error("Project repository not found for this organization.");
|
|
44
|
+
const connection = await github.sourceControlStorage.connections.get({
|
|
45
|
+
orgId,
|
|
46
|
+
id: projectRepository.connectionId
|
|
47
|
+
});
|
|
48
|
+
if (!connection) throw new Error("Source-control connection not found for this organization.");
|
|
49
|
+
const repository = await github.sourceControlStorage.repositories.get({
|
|
50
|
+
orgId,
|
|
51
|
+
id: projectRepository.repositoryId
|
|
52
|
+
});
|
|
53
|
+
if (!repository) throw new Error("Repository not found for this organization.");
|
|
54
|
+
const installation = await github.sourceControlStorage.installations.get({
|
|
55
|
+
orgId,
|
|
56
|
+
id: connection.installationId
|
|
57
|
+
});
|
|
58
|
+
if (!installation) throw new Error("Source-control installation not found for this organization.");
|
|
59
|
+
return {
|
|
60
|
+
context,
|
|
61
|
+
projectRepository,
|
|
62
|
+
connection,
|
|
63
|
+
installation,
|
|
64
|
+
repository,
|
|
65
|
+
orgId,
|
|
66
|
+
userId
|
|
67
|
+
};
|
|
122
68
|
}
|
|
123
69
|
async function verifyPullRequest(target, pullRequest, github) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
70
|
+
const [owner, repo] = target.repository.slug.split("/");
|
|
71
|
+
if (!owner || !repo) throw new Error("GitHub repository is invalid.");
|
|
72
|
+
const { data } = await github.getInstallationOctokit(Number(target.installation.externalId)).pulls.get({
|
|
73
|
+
owner,
|
|
74
|
+
repo,
|
|
75
|
+
pull_number: pullRequest
|
|
76
|
+
});
|
|
77
|
+
if (String(data.base.repo.id) !== target.repository.externalId) throw new Error("Pull request repository does not match the active project repository.");
|
|
130
78
|
}
|
|
131
79
|
async function subscriptionInput(target, pullRequestNumber) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
80
|
+
return {
|
|
81
|
+
orgId: target.orgId,
|
|
82
|
+
installationExternalId: target.installation.externalId,
|
|
83
|
+
projectRepositoryId: target.projectRepository.id,
|
|
84
|
+
repositoryExternalId: target.repository.externalId,
|
|
85
|
+
repositorySlug: target.repository.slug,
|
|
86
|
+
changeRequestId: String(pullRequestNumber),
|
|
87
|
+
sessionId: target.context.session.id,
|
|
88
|
+
ownerId: target.context.session.ownerId,
|
|
89
|
+
resourceId: target.connection.factoryProjectId,
|
|
90
|
+
threadId: target.context.threadId,
|
|
91
|
+
sessionScope: target.context.scope,
|
|
92
|
+
source: "explicit-tool",
|
|
93
|
+
subscribedByUserId: target.userId
|
|
94
|
+
};
|
|
147
95
|
}
|
|
148
96
|
async function subscribeCurrentSessionToPullRequest(requestContext, pullRequest, source, github) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
97
|
+
if (source === "auto-gh-pr-create" && !isGithubProjectSession(requestContext)) return void 0;
|
|
98
|
+
const target = await resolveSessionTarget(requestContext, github);
|
|
99
|
+
const number = parsePullRequest(pullRequest, target.repository.slug);
|
|
100
|
+
await verifyPullRequest(target, number, github);
|
|
101
|
+
await subscribeToPullRequest({
|
|
102
|
+
...await subscriptionInput(target, number),
|
|
103
|
+
source
|
|
104
|
+
}, github.integrationStorage);
|
|
105
|
+
return number;
|
|
155
106
|
}
|
|
156
107
|
async function unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
108
|
+
const target = await resolveSessionTarget(requestContext, github);
|
|
109
|
+
const number = parsePullRequest(pullRequest, target.repository.slug);
|
|
110
|
+
await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);
|
|
111
|
+
return number;
|
|
161
112
|
}
|
|
162
113
|
async function refreshGithubToken(requestContext, github) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
repositoryId: target.repository.id
|
|
176
|
-
});
|
|
177
|
-
const token = access.authorization?.token;
|
|
178
|
-
if (!token) throw new Error("Repository access did not include a bearer token for the Factory session.");
|
|
179
|
-
injectGithubToken(requestContext, token);
|
|
114
|
+
const target = await resolveSessionTarget(requestContext, github);
|
|
115
|
+
const pat = await getGithubPat(() => github.integrationStorage, target.orgId, getRegisteredGithubPatKind(requestContext));
|
|
116
|
+
if (pat) {
|
|
117
|
+
injectGithubToken(requestContext, pat);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const token = (await github.versionControl.getRepositoryAccess({
|
|
121
|
+
orgId: target.orgId,
|
|
122
|
+
repositoryId: target.repository.id
|
|
123
|
+
})).authorization?.token;
|
|
124
|
+
if (!token) throw new Error("Repository access did not include a bearer token for the Factory session.");
|
|
125
|
+
injectGithubToken(requestContext, token);
|
|
180
126
|
}
|
|
181
127
|
function createGithubSubscriptionTools(requestContext, github) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
128
|
+
const context = requestContext.get("controller");
|
|
129
|
+
const user = requestContext.get("user");
|
|
130
|
+
if (!context?.getState().projectRepositoryId || !sessionOrgId(user) || !sessionUserId(user)) return {};
|
|
131
|
+
return {
|
|
132
|
+
github_refresh_token: createTool({
|
|
133
|
+
id: "github_refresh_token",
|
|
134
|
+
description: "Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.",
|
|
135
|
+
inputSchema: z.object({}),
|
|
136
|
+
execute: async () => {
|
|
137
|
+
await refreshGithubToken(requestContext, github);
|
|
138
|
+
return { refreshed: true };
|
|
139
|
+
}
|
|
140
|
+
}),
|
|
141
|
+
github_subscribe_pr: createTool({
|
|
142
|
+
id: "github_subscribe_pr",
|
|
143
|
+
description: "Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.",
|
|
144
|
+
inputSchema: pullRequestInputSchema,
|
|
145
|
+
execute: async ({ pullRequest }) => {
|
|
146
|
+
return {
|
|
147
|
+
subscribed: true,
|
|
148
|
+
pullRequestNumber: await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, "explicit-tool", github)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}),
|
|
152
|
+
github_unsubscribe_pr: createTool({
|
|
153
|
+
id: "github_unsubscribe_pr",
|
|
154
|
+
description: "Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.",
|
|
155
|
+
inputSchema: pullRequestInputSchema,
|
|
156
|
+
execute: async ({ pullRequest }) => {
|
|
157
|
+
return {
|
|
158
|
+
subscribed: false,
|
|
159
|
+
pullRequestNumber: await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
};
|
|
214
164
|
}
|
|
215
165
|
function stripHeredocBodies(command) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return executableLines.join("\n");
|
|
166
|
+
const lines = command.split("\n");
|
|
167
|
+
const executableLines = [];
|
|
168
|
+
let delimiter;
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
if (delimiter) {
|
|
171
|
+
if (line.trim() === delimiter) delimiter = void 0;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
executableLines.push(line);
|
|
175
|
+
delimiter = line.match(/<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/)?.[2];
|
|
176
|
+
}
|
|
177
|
+
return executableLines.join("\n");
|
|
229
178
|
}
|
|
230
179
|
function parseCreatedPullRequest(context) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
createGithubSubscriptionTools,
|
|
244
|
-
parseCreatedPullRequest,
|
|
245
|
-
refreshGithubToken,
|
|
246
|
-
stripHeredocBodies,
|
|
247
|
-
subscribeCurrentSessionToPullRequest,
|
|
248
|
-
unsubscribeCurrentSessionFromPullRequest
|
|
249
|
-
};
|
|
180
|
+
if (context.toolName !== "execute_command" || context.error) return void 0;
|
|
181
|
+
const command = context.input?.command;
|
|
182
|
+
if (typeof command !== "string" || !/(?:^|\n|;|&&|\|\|)\s*gh\s+pr\s+create(?:\s|$)/.test(stripHeredocBodies(command))) return;
|
|
183
|
+
const output = context.output;
|
|
184
|
+
const stdout = typeof context.output === "string" ? context.output : output?.stdout ?? output?.result;
|
|
185
|
+
if (typeof stdout !== "string") return void 0;
|
|
186
|
+
const urls = stdout.match(/https:\/\/github\.com\/[^\s/]+\/[^\s/]+\/pull\/\d+/g) ?? [];
|
|
187
|
+
return urls.length === 1 ? urls[0] : void 0;
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
export { createGithubSubscriptionTools, parseCreatedPullRequest, refreshGithubToken, stripHeredocBodies, subscribeCurrentSessionToPullRequest, unsubscribeCurrentSessionFromPullRequest };
|
|
191
|
+
|
|
250
192
|
//# sourceMappingURL=session-subscriptions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/integrations/github/session-subscriptions.ts","../../../src/integrations/github/pat.ts","../../../src/integrations/github/subscriptions.ts","../../../src/integrations/github/token-refresh.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\nimport type {\n ProjectRepository,\n ProjectSourceControlConnection,\n SourceControlInstallation,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { getGithubPat } from './pat.js';\nimport { subscribeToPullRequest, unsubscribeFromPullRequest } from './subscriptions.js';\nimport { getRegisteredGithubPatKind, injectGithubToken } from './token-refresh.js';\n\ntype RepositorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\n/**\n * Minimal shape of the host-authenticated user placed on the request context\n * under the `user` key. Mirrors the host's auth user without importing it:\n * `workosId` (stable external id) wins over the row `id`, and `organizationId`\n * scopes org tenancy.\n */\ninterface SessionAuthUser {\n workosId?: string;\n id?: string;\n organizationId?: string;\n}\n\nfunction sessionUserId(user: SessionAuthUser | undefined): string | undefined {\n return user?.workosId ?? user?.id;\n}\n\nfunction sessionOrgId(user: SessionAuthUser | undefined): string | undefined {\n return user?.organizationId;\n}\n\nconst pullRequestInputSchema = z.object({\n pullRequest: z.union([z.number().int().positive(), z.string().min(1)]),\n});\n\ninterface SessionTarget {\n context: AgentControllerRequestContext<RepositorySessionState>;\n projectRepository: ProjectRepository;\n connection: ProjectSourceControlConnection;\n installation: SourceControlInstallation;\n repository: SourceControlRepository;\n orgId: string;\n userId: string;\n}\n\nfunction parsePullRequest(value: number | string, expectedRepo: string): number {\n if (typeof value === 'number') return value;\n if (/^\\d+$/.test(value)) return Number(value);\n const match = value.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match || match[1]!.toLowerCase() !== expectedRepo.toLowerCase()) {\n throw new Error(`Pull request must belong to ${expectedRepo}.`);\n }\n return Number(match[2]);\n}\n\n/**\n * Whether the current request comes from a session that GitHub subscriptions\n * can ever apply to: an authenticated org user on a GitHub-project session\n * with an active thread. Mirrors the gate in `resolveSessionTarget` without\n * throwing, for passive callers that should no-op instead of erroring.\n */\nfunction isGithubProjectSession(requestContext: RequestContext): boolean {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n return Boolean(\n context?.threadId && context.getState().projectRepositoryId && sessionOrgId(user) && sessionUserId(user),\n );\n}\n\nasync function resolveSessionTarget(requestContext: RequestContext, github: GithubIntegration): Promise<SessionTarget> {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n const orgId = sessionOrgId(user);\n const userId = sessionUserId(user);\n const projectRepositoryId = context?.getState().projectRepositoryId;\n if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) {\n throw new Error('GitHub subscriptions require an authenticated repository session with an active thread.');\n }\n\n const projectRepository = await github.sourceControlStorage.projectRepositories.get({\n orgId,\n id: projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Project repository not found for this organization.');\n const connection = await github.sourceControlStorage.connections.get({ orgId, id: projectRepository.connectionId });\n if (!connection) throw new Error('Source-control connection not found for this organization.');\n const repository = await github.sourceControlStorage.repositories.get({ orgId, id: projectRepository.repositoryId });\n if (!repository) throw new Error('Repository not found for this organization.');\n const installation = await github.sourceControlStorage.installations.get({ orgId, id: connection.installationId });\n if (!installation) throw new Error('Source-control installation not found for this organization.');\n return { context, projectRepository, connection, installation, repository, orgId, userId };\n}\n\nasync function verifyPullRequest(target: SessionTarget, pullRequest: number, github: GithubIntegration) {\n const [owner, repo] = target.repository.slug.split('/');\n if (!owner || !repo) throw new Error('GitHub repository is invalid.');\n const octokit = github.getInstallationOctokit(Number(target.installation.externalId));\n const { data } = await octokit.pulls.get({ owner, repo, pull_number: pullRequest });\n if (String(data.base.repo.id) !== target.repository.externalId)\n throw new Error('Pull request repository does not match the active project repository.');\n}\n\nasync function subscriptionInput(target: SessionTarget, pullRequestNumber: number) {\n return {\n orgId: target.orgId,\n installationExternalId: target.installation.externalId,\n projectRepositoryId: target.projectRepository.id,\n repositoryExternalId: target.repository.externalId,\n repositorySlug: target.repository.slug,\n changeRequestId: String(pullRequestNumber),\n sessionId: target.context.session.id,\n ownerId: target.context.session.ownerId,\n resourceId: target.connection.factoryProjectId,\n threadId: target.context.threadId!,\n sessionScope: target.context.scope,\n source: 'explicit-tool' as const,\n subscribedByUserId: target.userId,\n };\n}\n\nexport async function subscribeCurrentSessionToPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n source: 'auto-gh-pr-create' | 'explicit-tool',\n github: GithubIntegration,\n) {\n // The auto path observes every successful `gh pr create` in every session,\n // including local and non-GitHub-project sessions where subscriptions can\n // never apply. Skip silently there; only the explicit tool should surface\n // \"this session cannot subscribe\" as an error.\n if (source === 'auto-gh-pr-create' && !isGithubProjectSession(requestContext)) return undefined;\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await verifyPullRequest(target, number, github);\n await subscribeToPullRequest({ ...(await subscriptionInput(target, number)), source }, github.integrationStorage);\n return number;\n}\n\nexport async function unsubscribeCurrentSessionFromPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);\n return number;\n}\n\nexport async function refreshGithubToken(requestContext: RequestContext, github: GithubIntegration): Promise<void> {\n const target = await resolveSessionTarget(requestContext, github);\n // `GH_TOKEN` feeds the `gh` CLI, so a configured org PAT wins over a minted\n // installation token (which 403s on integration-restricted endpoints). The\n // workspace records which PAT kind the sandbox was provisioned with, so a\n // review-board sandbox keeps its reviewer token on refresh.\n const pat = await getGithubPat(\n () => github.integrationStorage,\n target.orgId,\n getRegisteredGithubPatKind(requestContext),\n );\n if (pat) {\n injectGithubToken(requestContext, pat);\n return;\n }\n const access = await github.versionControl.getRepositoryAccess({\n orgId: target.orgId,\n repositoryId: target.repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session.');\n injectGithubToken(requestContext, token);\n}\n\nexport function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration) {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n if (!context?.getState().projectRepositoryId || !sessionOrgId(user) || !sessionUserId(user)) return {};\n\n return {\n github_refresh_token: createTool({\n id: 'github_refresh_token',\n description:\n 'Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.',\n inputSchema: z.object({}),\n execute: async () => {\n await refreshGithubToken(requestContext, github);\n return { refreshed: true };\n },\n }),\n github_subscribe_pr: createTool({\n id: 'github_subscribe_pr',\n description:\n 'Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, 'explicit-tool', github);\n return { subscribed: true, pullRequestNumber: number };\n },\n }),\n github_unsubscribe_pr: createTool({\n id: 'github_unsubscribe_pr',\n description:\n 'Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);\n return { subscribed: false, pullRequestNumber: number };\n },\n }),\n };\n}\n\nexport function stripHeredocBodies(command: string): string {\n const lines = command.split('\\n');\n const executableLines: string[] = [];\n let delimiter: string | undefined;\n\n for (const line of lines) {\n if (delimiter) {\n if (line.trim() === delimiter) delimiter = undefined;\n continue;\n }\n executableLines.push(line);\n const heredoc = line.match(/<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1/);\n delimiter = heredoc?.[2];\n }\n\n return executableLines.join('\\n');\n}\n\nexport function parseCreatedPullRequest(context: {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n}) {\n if (context.toolName !== 'execute_command' || context.error) return undefined;\n const command = (context.input as { command?: unknown } | undefined)?.command;\n if (\n typeof command !== 'string' ||\n !/(?:^|\\n|;|&&|\\|\\|)\\s*gh\\s+pr\\s+create(?:\\s|$)/.test(stripHeredocBodies(command))\n ) {\n return undefined;\n }\n const output = context.output as { stdout?: unknown; result?: unknown } | undefined;\n const stdout = typeof context.output === 'string' ? context.output : (output?.stdout ?? output?.result);\n if (typeof stdout !== 'string') return undefined;\n const urls = stdout.match(/https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+/g) ?? [];\n return urls.length === 1 ? urls[0] : undefined;\n}\n","/**\n * Org-scoped GitHub Personal Access Token settings.\n *\n * GitHub App installation tokens are the wrong credential for the `gh` CLI —\n * they hit \"Resource not accessible by integration\" on endpoints the CLI\n * needs regardless of the minted permission set. When an org pastes a PAT in\n * Settings, the sandbox `GH_TOKEN` injection sites and the\n * `github_refresh_token` tool use it instead of a minted installation token.\n * Tokens must be classic PATs whose account has access to the linked repos.\n *\n * Two kinds:\n * - `default` — the worker token every sandbox gets.\n * - `reviewer` — optional second token for review-board sessions, so PR\n * reviews come from a different account than the author. When it isn't\n * configured, review sessions fall back to the worker token.\n *\n * Both live in the generic `integration_settings` collection for the\n * `github` integration under a sentinel user id (the settings are org-wide,\n * but the schema keys settings per `(org, user)`). Tokens are never returned\n * to the browser — the routes only report whether each is configured.\n */\n\nimport type { GithubSubscriptionStorage } from './subscriptions.js';\n\n/** Sentinel `user_id` for the org-wide settings row. */\nconst PAT_SETTINGS_USER_ID = '__github_org_settings__';\n\nexport type GithubPatKind = 'default' | 'reviewer';\n\ntype GithubOrgSettings = { pat?: string; reviewerPat?: string };\n\nconst FIELD_FOR_KIND: Record<GithubPatKind, keyof GithubOrgSettings> = {\n default: 'pat',\n reviewer: 'reviewerPat',\n};\n\nfunction asToken(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\n/** The PAT to install for `kind`, or null. `reviewer` falls back to the\n * worker token so review sessions still authenticate when no dedicated\n * reviewer token is configured. Fail-soft: storage errors (e.g. integration\n * storage not initialized in a partial test harness) read as \"no PAT\n * configured\" so token minting still works. */\nexport async function getGithubPat(\n getStorage: () => GithubSubscriptionStorage,\n orgId: string,\n kind: GithubPatKind = 'default',\n): Promise<string | null> {\n try {\n const settings = (await getStorage().settings.get(orgId, PAT_SETTINGS_USER_ID)) as GithubOrgSettings | null;\n if (!settings) return null;\n if (kind === 'reviewer') return asToken(settings.reviewerPat) ?? asToken(settings.pat);\n return asToken(settings.pat);\n } catch {\n return null;\n }\n}\n\n/** Which tokens are configured, without fallback semantics — feeds the\n * settings UI status badges. */\nexport async function getGithubPatStatus(\n getStorage: () => GithubSubscriptionStorage,\n orgId: string,\n): Promise<{ configured: boolean; reviewerConfigured: boolean }> {\n try {\n const settings = (await getStorage().settings.get(orgId, PAT_SETTINGS_USER_ID)) as GithubOrgSettings | null;\n return {\n configured: asToken(settings?.pat) !== null,\n reviewerConfigured: asToken(settings?.reviewerPat) !== null,\n };\n } catch {\n return { configured: false, reviewerConfigured: false };\n }\n}\n\nexport async function setGithubPat(\n storage: GithubSubscriptionStorage,\n orgId: string,\n pat: string,\n kind: GithubPatKind = 'default',\n): Promise<void> {\n const existing = ((await storage.settings.get(orgId, PAT_SETTINGS_USER_ID)) ?? {}) as GithubOrgSettings;\n await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, { ...existing, [FIELD_FOR_KIND[kind]]: pat });\n}\n\nexport async function clearGithubPat(\n storage: GithubSubscriptionStorage,\n orgId: string,\n kind: GithubPatKind = 'default',\n): Promise<void> {\n const existing = (await storage.settings.get(orgId, PAT_SETTINGS_USER_ID)) as GithubOrgSettings | null;\n const field = FIELD_FOR_KIND[kind];\n if (!existing?.[field]) return;\n const { [field]: _removed, ...rest } = existing;\n await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, rest);\n}\n","import type { IntegrationStorageHandle, IntegrationSubscription } from '../../storage/domains/integrations/base.js';\n\nexport type GithubSignalSubscriptionSource = 'auto-gh-pr-create' | 'factory-pr-create' | 'explicit-tool';\nexport type GithubSignalSubscriptionStatus = 'open' | 'closed' | 'merged';\n\nexport interface GithubSignalSubscriptionData {\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n ownerId: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId: string | null;\n}\n\nexport type GithubSignalSubscriptionRow = IntegrationSubscription<GithubSignalSubscriptionData>;\nexport type GithubSubscriptionStorage = IntegrationStorageHandle<\n Record<string, unknown>,\n Record<string, unknown>,\n GithubSignalSubscriptionData\n>;\n\nexport interface SubscribeToPullRequestInput {\n orgId: string;\n installationExternalId: string;\n projectRepositoryId: string;\n repositoryExternalId: string;\n repositorySlug: string;\n changeRequestId: string;\n sessionId: string;\n ownerId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n source: GithubSignalSubscriptionSource;\n subscribedByUserId?: string;\n}\n\nexport interface ThreadSubscriptionTarget {\n orgId: string;\n resourceId: string;\n threadId: string;\n sessionScope?: string;\n}\n\nexport interface PullRequestSubscriptionTarget {\n orgId: string;\n installationExternalId: string;\n repositoryExternalId: string;\n changeRequestId: string;\n}\n\nexport type GithubWebhookPullRequestTarget = Omit<PullRequestSubscriptionTarget, 'orgId'>;\n\nexport function changeRequestTargetKey(input: GithubWebhookPullRequestTarget): string {\n return `change-request:${input.installationExternalId}:${input.repositoryExternalId}:${input.changeRequestId}`;\n}\n\nfunction sameSession(row: GithubSignalSubscriptionRow, input: SubscribeToPullRequestInput): boolean {\n return (\n row.orgId === input.orgId &&\n row.sessionId === input.sessionId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? '')\n );\n}\n\nexport async function subscribeToPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow> {\n const targetKey = changeRequestTargetKey(input);\n const existing = (await storage.subscriptions.listByTarget(targetKey)).find(row => sameSession(row, input));\n if (existing) {\n if (existing.status !== 'open') await storage.subscriptions.updateStatus(existing.id, 'open');\n return { ...existing, status: 'open' };\n }\n\n return storage.subscriptions.create({\n orgId: input.orgId,\n targetKey,\n sessionId: input.sessionId,\n resourceId: input.resourceId,\n threadId: input.threadId,\n sessionScope: input.sessionScope ?? '',\n status: 'open',\n data: {\n installationExternalId: input.installationExternalId,\n projectRepositoryId: input.projectRepositoryId,\n repositoryExternalId: input.repositoryExternalId,\n repositorySlug: input.repositorySlug,\n changeRequestId: input.changeRequestId,\n ownerId: input.ownerId,\n source: input.source,\n subscribedByUserId: input.subscribedByUserId ?? null,\n },\n });\n}\n\nexport async function unsubscribeFromPullRequest(\n input: SubscribeToPullRequestInput,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n await Promise.all(rows.filter(row => sameSession(row, input)).map(row => storage.subscriptions.delete(row.id)));\n}\n\nexport async function listPullRequestSubscriptionsForThread(\n input: ThreadSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByThread(input.resourceId, input.threadId);\n return rows.filter(\n row =>\n row.orgId === input.orgId &&\n row.resourceId === input.resourceId &&\n row.threadId === input.threadId &&\n (row.sessionScope ?? '') === (input.sessionScope ?? ''),\n );\n}\n\nexport async function listPullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return rows.filter(row => row.orgId === input.orgId && row.status === 'open');\n}\n\nexport async function listPullRequestSubscriptionsForWebhook(\n input: GithubWebhookPullRequestTarget,\n options: { includeTerminal?: boolean } | undefined,\n storage: GithubSubscriptionStorage,\n): Promise<GithubSignalSubscriptionRow[]> {\n const rows = await storage.subscriptions.listByTarget(changeRequestTargetKey(input));\n return options?.includeTerminal ? rows : rows.filter(row => row.status === 'open');\n}\n\nexport function retirePullRequestSubscription(\n id: string,\n status: GithubSignalSubscriptionStatus,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n return storage.subscriptions.updateStatus(id, status);\n}\n\nexport async function retirePullRequestSubscriptions(\n input: PullRequestSubscriptionTarget,\n storage: GithubSubscriptionStorage,\n): Promise<void> {\n const rows = await listPullRequestSubscriptions(input, storage);\n await Promise.all(rows.map(row => storage.subscriptions.updateStatus(row.id, 'closed')));\n}\n","import type { RequestContext } from '@mastra/core/request-context';\n\nimport type { GithubPatKind } from './pat.js';\n\nconst GITHUB_TOKEN_INJECTOR_CONTEXT_KEY = 'factoryGithubTokenInjector';\nconst GITHUB_PAT_KIND_CONTEXT_KEY = 'factoryGithubPatKind';\n\ntype GithubTokenInjector = (token: string) => void;\n\nexport function registerGithubTokenInjector(requestContext: RequestContext, injector: GithubTokenInjector): void {\n requestContext.set(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY, injector);\n}\n\n/** Record which PAT kind the active sandbox was provisioned with, so token\n * refresh re-injects the same credential (review-board sandboxes keep the\n * reviewer token instead of being clobbered with the worker token). */\nexport function registerGithubPatKind(requestContext: RequestContext, kind: GithubPatKind): void {\n requestContext.set(GITHUB_PAT_KIND_CONTEXT_KEY, kind);\n}\n\nexport function getRegisteredGithubPatKind(requestContext: RequestContext): GithubPatKind {\n const kind = requestContext.get(GITHUB_PAT_KIND_CONTEXT_KEY);\n return kind === 'reviewer' ? 'reviewer' : 'default';\n}\n\nexport function injectGithubToken(requestContext: RequestContext, token: string): void {\n const injector = requestContext.get(GITHUB_TOKEN_INJECTOR_CONTEXT_KEY) as GithubTokenInjector | undefined;\n if (!injector) {\n throw new Error('GitHub token refresh requires an active Factory sandbox workspace.');\n }\n injector(token);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"file":"session-subscriptions.js","names":[],"sources":["../../../src/integrations/github/session-subscriptions.ts"],"sourcesContent":["import type { AgentControllerRequestContext } from '@mastra/core/agent-controller';\nimport type { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\nimport type {\n ProjectRepository,\n ProjectSourceControlConnection,\n SourceControlInstallation,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { getGithubPat } from './pat.js';\nimport { subscribeToPullRequest, unsubscribeFromPullRequest } from './subscriptions.js';\nimport { getRegisteredGithubPatKind, injectGithubToken } from './token-refresh.js';\n\ntype RepositorySessionState = { factoryProjectId?: string; projectRepositoryId?: string };\n\n/**\n * Minimal shape of the host-authenticated user placed on the request context\n * under the `user` key. Mirrors the host's auth user without importing it:\n * `workosId` (stable external id) wins over the row `id`, and `organizationId`\n * scopes org tenancy.\n */\ninterface SessionAuthUser {\n workosId?: string;\n id?: string;\n organizationId?: string;\n}\n\nfunction sessionUserId(user: SessionAuthUser | undefined): string | undefined {\n return user?.workosId ?? user?.id;\n}\n\nfunction sessionOrgId(user: SessionAuthUser | undefined): string | undefined {\n return user?.organizationId;\n}\n\nconst pullRequestInputSchema = z.object({\n pullRequest: z.union([z.number().int().positive(), z.string().min(1)]),\n});\n\ninterface SessionTarget {\n context: AgentControllerRequestContext<RepositorySessionState>;\n projectRepository: ProjectRepository;\n connection: ProjectSourceControlConnection;\n installation: SourceControlInstallation;\n repository: SourceControlRepository;\n orgId: string;\n userId: string;\n}\n\nfunction parsePullRequest(value: number | string, expectedRepo: string): number {\n if (typeof value === 'number') return value;\n if (/^\\d+$/.test(value)) return Number(value);\n const match = value.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match || match[1]!.toLowerCase() !== expectedRepo.toLowerCase()) {\n throw new Error(`Pull request must belong to ${expectedRepo}.`);\n }\n return Number(match[2]);\n}\n\n/**\n * Whether the current request comes from a session that GitHub subscriptions\n * can ever apply to: an authenticated org user on a GitHub-project session\n * with an active thread. Mirrors the gate in `resolveSessionTarget` without\n * throwing, for passive callers that should no-op instead of erroring.\n */\nfunction isGithubProjectSession(requestContext: RequestContext): boolean {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n return Boolean(\n context?.threadId && context.getState().projectRepositoryId && sessionOrgId(user) && sessionUserId(user),\n );\n}\n\nasync function resolveSessionTarget(requestContext: RequestContext, github: GithubIntegration): Promise<SessionTarget> {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n const orgId = sessionOrgId(user);\n const userId = sessionUserId(user);\n const projectRepositoryId = context?.getState().projectRepositoryId;\n if (!context || !context.threadId || !projectRepositoryId || !orgId || !userId) {\n throw new Error('GitHub subscriptions require an authenticated repository session with an active thread.');\n }\n\n const projectRepository = await github.sourceControlStorage.projectRepositories.get({\n orgId,\n id: projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Project repository not found for this organization.');\n const connection = await github.sourceControlStorage.connections.get({ orgId, id: projectRepository.connectionId });\n if (!connection) throw new Error('Source-control connection not found for this organization.');\n const repository = await github.sourceControlStorage.repositories.get({ orgId, id: projectRepository.repositoryId });\n if (!repository) throw new Error('Repository not found for this organization.');\n const installation = await github.sourceControlStorage.installations.get({ orgId, id: connection.installationId });\n if (!installation) throw new Error('Source-control installation not found for this organization.');\n return { context, projectRepository, connection, installation, repository, orgId, userId };\n}\n\nasync function verifyPullRequest(target: SessionTarget, pullRequest: number, github: GithubIntegration) {\n const [owner, repo] = target.repository.slug.split('/');\n if (!owner || !repo) throw new Error('GitHub repository is invalid.');\n const octokit = github.getInstallationOctokit(Number(target.installation.externalId));\n const { data } = await octokit.pulls.get({ owner, repo, pull_number: pullRequest });\n if (String(data.base.repo.id) !== target.repository.externalId)\n throw new Error('Pull request repository does not match the active project repository.');\n}\n\nasync function subscriptionInput(target: SessionTarget, pullRequestNumber: number) {\n return {\n orgId: target.orgId,\n installationExternalId: target.installation.externalId,\n projectRepositoryId: target.projectRepository.id,\n repositoryExternalId: target.repository.externalId,\n repositorySlug: target.repository.slug,\n changeRequestId: String(pullRequestNumber),\n sessionId: target.context.session.id,\n ownerId: target.context.session.ownerId,\n resourceId: target.connection.factoryProjectId,\n threadId: target.context.threadId!,\n sessionScope: target.context.scope,\n source: 'explicit-tool' as const,\n subscribedByUserId: target.userId,\n };\n}\n\nexport async function subscribeCurrentSessionToPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n source: 'auto-gh-pr-create' | 'explicit-tool',\n github: GithubIntegration,\n) {\n // The auto path observes every successful `gh pr create` in every session,\n // including local and non-GitHub-project sessions where subscriptions can\n // never apply. Skip silently there; only the explicit tool should surface\n // \"this session cannot subscribe\" as an error.\n if (source === 'auto-gh-pr-create' && !isGithubProjectSession(requestContext)) return undefined;\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await verifyPullRequest(target, number, github);\n await subscribeToPullRequest({ ...(await subscriptionInput(target, number)), source }, github.integrationStorage);\n return number;\n}\n\nexport async function unsubscribeCurrentSessionFromPullRequest(\n requestContext: RequestContext,\n pullRequest: number | string,\n github: GithubIntegration,\n) {\n const target = await resolveSessionTarget(requestContext, github);\n const number = parsePullRequest(pullRequest, target.repository.slug);\n await unsubscribeFromPullRequest(await subscriptionInput(target, number), github.integrationStorage);\n return number;\n}\n\nexport async function refreshGithubToken(requestContext: RequestContext, github: GithubIntegration): Promise<void> {\n const target = await resolveSessionTarget(requestContext, github);\n // `GH_TOKEN` feeds the `gh` CLI, so a configured org PAT wins over a minted\n // installation token (which 403s on integration-restricted endpoints). The\n // workspace records which PAT kind the sandbox was provisioned with, so a\n // review-board sandbox keeps its reviewer token on refresh.\n const pat = await getGithubPat(\n () => github.integrationStorage,\n target.orgId,\n getRegisteredGithubPatKind(requestContext),\n );\n if (pat) {\n injectGithubToken(requestContext, pat);\n return;\n }\n const access = await github.versionControl.getRepositoryAccess({\n orgId: target.orgId,\n repositoryId: target.repository.id,\n });\n const token = access.authorization?.token;\n if (!token) throw new Error('Repository access did not include a bearer token for the Factory session.');\n injectGithubToken(requestContext, token);\n}\n\nexport function createGithubSubscriptionTools(requestContext: RequestContext, github: GithubIntegration) {\n const context = requestContext.get('controller') as AgentControllerRequestContext<RepositorySessionState> | undefined;\n const user = requestContext.get('user') as SessionAuthUser | undefined;\n if (!context?.getState().projectRepositoryId || !sessionOrgId(user) || !sessionUserId(user)) return {};\n\n return {\n github_refresh_token: createTool({\n id: 'github_refresh_token',\n description:\n 'Refresh GitHub CLI authentication in the active Factory sandbox. Use this after a gh command fails because authentication is expired, invalid, or missing. It installs a fresh GH_TOKEN for subsequent sandbox commands. After this tool succeeds, retry the failed gh command. Takes no arguments and never returns the token.',\n inputSchema: z.object({}),\n execute: async () => {\n await refreshGithubToken(requestContext, github);\n return { refreshed: true };\n },\n }),\n github_subscribe_pr: createTool({\n id: 'github_subscribe_pr',\n description:\n 'Subscribe this thread to GitHub pull request activity. You usually do not need this tool: successful gh pr create commands subscribe automatically. Use it for an existing PR or to recover when automatic subscription did not occur. Closed or merged PRs are unsubscribed automatically. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await subscribeCurrentSessionToPullRequest(requestContext, pullRequest, 'explicit-tool', github);\n return { subscribed: true, pullRequestNumber: number };\n },\n }),\n github_unsubscribe_pr: createTool({\n id: 'github_unsubscribe_pr',\n description:\n 'Manually unsubscribe this thread from GitHub pull request activity. You usually do not need this tool because closed or merged PRs are unsubscribed automatically. Use it to stop notifications before then. Accepts a PR number or canonical URL for the active project.',\n inputSchema: pullRequestInputSchema,\n execute: async ({ pullRequest }) => {\n const number = await unsubscribeCurrentSessionFromPullRequest(requestContext, pullRequest, github);\n return { subscribed: false, pullRequestNumber: number };\n },\n }),\n };\n}\n\nexport function stripHeredocBodies(command: string): string {\n const lines = command.split('\\n');\n const executableLines: string[] = [];\n let delimiter: string | undefined;\n\n for (const line of lines) {\n if (delimiter) {\n if (line.trim() === delimiter) delimiter = undefined;\n continue;\n }\n executableLines.push(line);\n const heredoc = line.match(/<<-?\\s*(['\"]?)([A-Za-z_][A-Za-z0-9_]*)\\1/);\n delimiter = heredoc?.[2];\n }\n\n return executableLines.join('\\n');\n}\n\nexport function parseCreatedPullRequest(context: {\n toolName: string;\n input: unknown;\n output?: unknown;\n error?: unknown;\n}) {\n if (context.toolName !== 'execute_command' || context.error) return undefined;\n const command = (context.input as { command?: unknown } | undefined)?.command;\n if (\n typeof command !== 'string' ||\n !/(?:^|\\n|;|&&|\\|\\|)\\s*gh\\s+pr\\s+create(?:\\s|$)/.test(stripHeredocBodies(command))\n ) {\n return undefined;\n }\n const output = context.output as { stdout?: unknown; result?: unknown } | undefined;\n const stdout = typeof context.output === 'string' ? context.output : (output?.stdout ?? output?.result);\n if (typeof stdout !== 'string') return undefined;\n const urls = stdout.match(/https:\\/\\/github\\.com\\/[^\\s/]+\\/[^\\s/]+\\/pull\\/\\d+/g) ?? [];\n return urls.length === 1 ? urls[0] : undefined;\n}\n"],"mappings":"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"}
|