@mastra/factory 0.1.0-alpha.5 → 0.1.0-alpha.7
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 +13 -0
- package/dist/factory.d.ts +2 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +141 -9
- package/dist/factory.js.map +1 -1
- package/dist/index.js +141 -9
- package/dist/index.js.map +1 -1
- package/dist/integrations/github/integration.js +108 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/pat.d.ts +38 -0
- package/dist/integrations/github/pat.d.ts.map +1 -0
- package/dist/integrations/github/pat.js +48 -0
- package/dist/integrations/github/pat.js.map +1 -0
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +94 -1
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +30 -0
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.d.ts +6 -0
- package/dist/integrations/github/token-refresh.d.ts.map +1 -1
- package/dist/integrations/github/token-refresh.js +10 -0
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +110 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/routes/surface.js +7 -1
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +7 -1
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/workspace.d.ts +5 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +64 -5
- package/dist/workspace.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org-scoped GitHub Personal Access Token settings.
|
|
3
|
+
*
|
|
4
|
+
* GitHub App installation tokens are the wrong credential for the `gh` CLI —
|
|
5
|
+
* they hit "Resource not accessible by integration" on endpoints the CLI
|
|
6
|
+
* needs regardless of the minted permission set. When an org pastes a PAT in
|
|
7
|
+
* Settings, the sandbox `GH_TOKEN` injection sites and the
|
|
8
|
+
* `github_refresh_token` tool use it instead of a minted installation token.
|
|
9
|
+
* Tokens must be classic PATs whose account has access to the linked repos.
|
|
10
|
+
*
|
|
11
|
+
* Two kinds:
|
|
12
|
+
* - `default` — the worker token every sandbox gets.
|
|
13
|
+
* - `reviewer` — optional second token for review-board sessions, so PR
|
|
14
|
+
* reviews come from a different account than the author. When it isn't
|
|
15
|
+
* configured, review sessions fall back to the worker token.
|
|
16
|
+
*
|
|
17
|
+
* Both live in the generic `integration_settings` collection for the
|
|
18
|
+
* `github` integration under a sentinel user id (the settings are org-wide,
|
|
19
|
+
* but the schema keys settings per `(org, user)`). Tokens are never returned
|
|
20
|
+
* to the browser — the routes only report whether each is configured.
|
|
21
|
+
*/
|
|
22
|
+
import type { GithubSubscriptionStorage } from './subscriptions.js';
|
|
23
|
+
export type GithubPatKind = 'default' | 'reviewer';
|
|
24
|
+
/** The PAT to install for `kind`, or null. `reviewer` falls back to the
|
|
25
|
+
* worker token so review sessions still authenticate when no dedicated
|
|
26
|
+
* reviewer token is configured. Fail-soft: storage errors (e.g. integration
|
|
27
|
+
* storage not initialized in a partial test harness) read as "no PAT
|
|
28
|
+
* configured" so token minting still works. */
|
|
29
|
+
export declare function getGithubPat(getStorage: () => GithubSubscriptionStorage, orgId: string, kind?: GithubPatKind): Promise<string | null>;
|
|
30
|
+
/** Which tokens are configured, without fallback semantics — feeds the
|
|
31
|
+
* settings UI status badges. */
|
|
32
|
+
export declare function getGithubPatStatus(getStorage: () => GithubSubscriptionStorage, orgId: string): Promise<{
|
|
33
|
+
configured: boolean;
|
|
34
|
+
reviewerConfigured: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
export declare function setGithubPat(storage: GithubSubscriptionStorage, orgId: string, pat: string, kind?: GithubPatKind): Promise<void>;
|
|
37
|
+
export declare function clearGithubPat(storage: GithubSubscriptionStorage, orgId: string, kind?: GithubPatKind): Promise<void>;
|
|
38
|
+
//# sourceMappingURL=pat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pat.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/pat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAKpE,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC;AAanD;;;;+CAI+C;AAC/C,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,yBAAyB,EAC3C,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,aAAyB,GAC9B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxB;AAED;gCACgC;AAChC,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,yBAAyB,EAC3C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,kBAAkB,EAAE,OAAO,CAAA;CAAE,CAAC,CAU/D;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,yBAAyB,EAClC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,aAAyB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,yBAAyB,EAClC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,aAAyB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/integrations/github/pat.ts
|
|
2
|
+
var PAT_SETTINGS_USER_ID = "__github_org_settings__";
|
|
3
|
+
var FIELD_FOR_KIND = {
|
|
4
|
+
default: "pat",
|
|
5
|
+
reviewer: "reviewerPat"
|
|
6
|
+
};
|
|
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
|
+
async function getGithubPatStatus(getStorage, orgId) {
|
|
21
|
+
try {
|
|
22
|
+
const settings = await getStorage().settings.get(orgId, PAT_SETTINGS_USER_ID);
|
|
23
|
+
return {
|
|
24
|
+
configured: asToken(settings?.pat) !== null,
|
|
25
|
+
reviewerConfigured: asToken(settings?.reviewerPat) !== null
|
|
26
|
+
};
|
|
27
|
+
} catch {
|
|
28
|
+
return { configured: false, reviewerConfigured: false };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function setGithubPat(storage, orgId, pat, kind = "default") {
|
|
32
|
+
const existing = await storage.settings.get(orgId, PAT_SETTINGS_USER_ID) ?? {};
|
|
33
|
+
await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, { ...existing, [FIELD_FOR_KIND[kind]]: pat });
|
|
34
|
+
}
|
|
35
|
+
async function clearGithubPat(storage, orgId, kind = "default") {
|
|
36
|
+
const existing = await storage.settings.get(orgId, PAT_SETTINGS_USER_ID);
|
|
37
|
+
const field = FIELD_FOR_KIND[kind];
|
|
38
|
+
if (!existing?.[field]) return;
|
|
39
|
+
const { [field]: _removed, ...rest } = existing;
|
|
40
|
+
await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, rest);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
clearGithubPat,
|
|
44
|
+
getGithubPat,
|
|
45
|
+
getGithubPatStatus,
|
|
46
|
+
setGithubPat
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=pat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/integrations/github/pat.ts"],"sourcesContent":["/**\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"],"mappings":";AAyBA,IAAM,uBAAuB;AAM7B,IAAM,iBAAiE;AAAA,EACrE,SAAS;AAAA,EACT,UAAU;AACZ;AAEA,SAAS,QAAQ,OAA+B;AAC9C,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAOA,eAAsB,aACpB,YACA,OACA,OAAsB,WACE;AACxB,MAAI;AACF,UAAM,WAAY,MAAM,WAAW,EAAE,SAAS,IAAI,OAAO,oBAAoB;AAC7E,QAAI,CAAC,SAAU,QAAO;AACtB,QAAI,SAAS,WAAY,QAAO,QAAQ,SAAS,WAAW,KAAK,QAAQ,SAAS,GAAG;AACrF,WAAO,QAAQ,SAAS,GAAG;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIA,eAAsB,mBACpB,YACA,OAC+D;AAC/D,MAAI;AACF,UAAM,WAAY,MAAM,WAAW,EAAE,SAAS,IAAI,OAAO,oBAAoB;AAC7E,WAAO;AAAA,MACL,YAAY,QAAQ,UAAU,GAAG,MAAM;AAAA,MACvC,oBAAoB,QAAQ,UAAU,WAAW,MAAM;AAAA,IACzD;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,YAAY,OAAO,oBAAoB,MAAM;AAAA,EACxD;AACF;AAEA,eAAsB,aACpB,SACA,OACA,KACA,OAAsB,WACP;AACf,QAAM,WAAa,MAAM,QAAQ,SAAS,IAAI,OAAO,oBAAoB,KAAM,CAAC;AAChF,QAAM,QAAQ,SAAS,KAAK,OAAO,sBAAsB,EAAE,GAAG,UAAU,CAAC,eAAe,IAAI,CAAC,GAAG,IAAI,CAAC;AACvG;AAEA,eAAsB,eACpB,SACA,OACA,OAAsB,WACP;AACf,QAAM,WAAY,MAAM,QAAQ,SAAS,IAAI,OAAO,oBAAoB;AACxE,QAAM,QAAQ,eAAe,IAAI;AACjC,MAAI,CAAC,WAAW,KAAK,EAAG;AACxB,QAAM,EAAE,CAAC,KAAK,GAAG,UAAU,GAAG,KAAK,IAAI;AACvC,QAAM,QAAQ,SAAS,KAAK,OAAO,sBAAsB,IAAI;AAC/D;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/integrations/github/session-subscriptions.ts","../../../src/integrations/github/provenance.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 { subscribeToPullRequest, unsubscribeFromPullRequest } from './subscriptions.js';\nimport { 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 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","import type { FactoryRuleJsonValue } from '../../rules/types.js';\nimport type { IntegrationStorageHandle } from '../../storage/domains/integrations/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { FactoryRunBindingRecord, WorkItemRow } from '../../storage/domains/work-items/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { parseCreatedPullRequest } from './session-subscriptions.js';\n\nexport interface RecordFactoryPullRequestProvenanceInput {\n binding: FactoryRunBindingRecord;\n item: WorkItemRow;\n assistantMessageId: string;\n toolCallId: string;\n toolName: string;\n toolInput: FactoryRuleJsonValue;\n toolResult: FactoryRuleJsonValue;\n status: 'success' | 'error';\n}\n\nexport interface FactoryPullRequestProvenanceData {\n kind: 'factory-pr-provenance';\n bindingId: string;\n workItemId: string;\n repositoryId: number;\n pullRequestNumber: number;\n pullRequestUrl: string;\n assistantMessageId: string;\n toolCallId: string;\n}\n\nexport async function recordFactoryPullRequestProvenance(\n github: GithubIntegration,\n sourceControl: SourceControlStorageHandle,\n integrationStorage: IntegrationStorageHandle<\n Record<string, unknown>,\n Record<string, unknown>,\n FactoryPullRequestProvenanceData\n >,\n input: RecordFactoryPullRequestProvenanceInput,\n): Promise<void> {\n if (input.status !== 'success' || input.item.externalSource?.type === 'pull-request') return;\n const url = parseCreatedPullRequest({\n toolName: input.toolName,\n input: input.toolInput,\n output: input.toolResult,\n });\n if (!url) return;\n\n try {\n const match = url.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match) return;\n const repositorySlug = match[1]!;\n let repositoryId: number | undefined;\n let installationId: number | undefined;\n for (const connection of await sourceControl.connections.list({\n orgId: input.binding.orgId,\n factoryProjectId: input.binding.factoryProjectId,\n })) {\n const installation = await sourceControl.installations.get({\n orgId: input.binding.orgId,\n id: connection.installationId,\n });\n if (!installation) continue;\n for (const link of await sourceControl.projectRepositories.list({\n orgId: input.binding.orgId,\n connectionId: connection.id,\n })) {\n const repository = await sourceControl.repositories.get({ orgId: input.binding.orgId, id: link.repositoryId });\n if (!repository || repository.slug.toLowerCase() !== repositorySlug.toLowerCase()) continue;\n repositoryId = Number(repository.externalId);\n installationId = Number(installation.externalId);\n break;\n }\n if (repositoryId !== undefined) break;\n }\n const pullRequestNumber = Number(match[2]);\n const [owner, repo] = repositorySlug.split('/');\n if (\n !owner ||\n !repo ||\n repositoryId === undefined ||\n installationId === undefined ||\n !Number.isInteger(repositoryId) ||\n !Number.isInteger(installationId) ||\n !Number.isInteger(pullRequestNumber) ||\n pullRequestNumber < 1\n )\n return;\n const targetKey = `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;\n if (\n (await integrationStorage.subscriptions.listByTarget(targetKey)).some(row => row.orgId === input.binding.orgId)\n ) {\n return;\n }\n\n const { data } = await github\n .getInstallationOctokit(installationId)\n .pulls.get({ owner, repo, pull_number: pullRequestNumber });\n if (data.base.repo.id !== repositoryId || data.number !== pullRequestNumber || data.html_url !== url) return;\n\n await integrationStorage.subscriptions.create({\n orgId: input.binding.orgId,\n targetKey,\n threadId: input.binding.threadId,\n status: 'active',\n data: {\n kind: 'factory-pr-provenance',\n bindingId: input.binding.id,\n workItemId: input.item.id,\n repositoryId,\n pullRequestNumber,\n pullRequestUrl: url,\n assistantMessageId: input.assistantMessageId,\n toolCallId: input.toolCallId,\n },\n });\n } catch {\n return;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/integrations/github/session-subscriptions.ts","../../../src/integrations/github/provenance.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","import type { FactoryRuleJsonValue } from '../../rules/types.js';\nimport type { IntegrationStorageHandle } from '../../storage/domains/integrations/base.js';\nimport type { SourceControlStorageHandle } from '../../storage/domains/source-control/base.js';\nimport type { FactoryRunBindingRecord, WorkItemRow } from '../../storage/domains/work-items/base.js';\nimport type { GithubIntegration } from './integration.js';\nimport { parseCreatedPullRequest } from './session-subscriptions.js';\n\nexport interface RecordFactoryPullRequestProvenanceInput {\n binding: FactoryRunBindingRecord;\n item: WorkItemRow;\n assistantMessageId: string;\n toolCallId: string;\n toolName: string;\n toolInput: FactoryRuleJsonValue;\n toolResult: FactoryRuleJsonValue;\n status: 'success' | 'error';\n}\n\nexport interface FactoryPullRequestProvenanceData {\n kind: 'factory-pr-provenance';\n bindingId: string;\n workItemId: string;\n repositoryId: number;\n pullRequestNumber: number;\n pullRequestUrl: string;\n assistantMessageId: string;\n toolCallId: string;\n}\n\nexport async function recordFactoryPullRequestProvenance(\n github: GithubIntegration,\n sourceControl: SourceControlStorageHandle,\n integrationStorage: IntegrationStorageHandle<\n Record<string, unknown>,\n Record<string, unknown>,\n FactoryPullRequestProvenanceData\n >,\n input: RecordFactoryPullRequestProvenanceInput,\n): Promise<void> {\n if (input.status !== 'success' || input.item.externalSource?.type === 'pull-request') return;\n const url = parseCreatedPullRequest({\n toolName: input.toolName,\n input: input.toolInput,\n output: input.toolResult,\n });\n if (!url) return;\n\n try {\n const match = url.match(/^https:\\/\\/github\\.com\\/([^/]+\\/[^/]+)\\/pull\\/(\\d+)\\/?$/i);\n if (!match) return;\n const repositorySlug = match[1]!;\n let repositoryId: number | undefined;\n let installationId: number | undefined;\n for (const connection of await sourceControl.connections.list({\n orgId: input.binding.orgId,\n factoryProjectId: input.binding.factoryProjectId,\n })) {\n const installation = await sourceControl.installations.get({\n orgId: input.binding.orgId,\n id: connection.installationId,\n });\n if (!installation) continue;\n for (const link of await sourceControl.projectRepositories.list({\n orgId: input.binding.orgId,\n connectionId: connection.id,\n })) {\n const repository = await sourceControl.repositories.get({ orgId: input.binding.orgId, id: link.repositoryId });\n if (!repository || repository.slug.toLowerCase() !== repositorySlug.toLowerCase()) continue;\n repositoryId = Number(repository.externalId);\n installationId = Number(installation.externalId);\n break;\n }\n if (repositoryId !== undefined) break;\n }\n const pullRequestNumber = Number(match[2]);\n const [owner, repo] = repositorySlug.split('/');\n if (\n !owner ||\n !repo ||\n repositoryId === undefined ||\n installationId === undefined ||\n !Number.isInteger(repositoryId) ||\n !Number.isInteger(installationId) ||\n !Number.isInteger(pullRequestNumber) ||\n pullRequestNumber < 1\n )\n return;\n const targetKey = `factory-pr-provenance:${repositoryId}:${pullRequestNumber}`;\n if (\n (await integrationStorage.subscriptions.listByTarget(targetKey)).some(row => row.orgId === input.binding.orgId)\n ) {\n return;\n }\n\n const { data } = await github\n .getInstallationOctokit(installationId)\n .pulls.get({ owner, repo, pull_number: pullRequestNumber });\n if (data.base.repo.id !== repositoryId || data.number !== pullRequestNumber || data.html_url !== url) return;\n\n await integrationStorage.subscriptions.create({\n orgId: input.binding.orgId,\n targetKey,\n threadId: input.binding.threadId,\n status: 'active',\n data: {\n kind: 'factory-pr-provenance',\n bindingId: input.binding.id,\n workItemId: input.item.id,\n repositoryId,\n pullRequestNumber,\n pullRequestUrl: url,\n assistantMessageId: input.assistantMessageId,\n toolCallId: input.toolCallId,\n },\n });\n } catch {\n return;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,KAAK,EAAuD,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAChH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AASrF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,KAAK,EAAuD,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAChH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AASrF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAmB1D,OAAO,KAAK,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAgB/G,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC7C,iEAAiE;IACjE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC3F,6EAA6E;IAC7E,SAAS,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,+FAA+F;IAC/F,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAuPD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,QAAQ,EAAE,CA4oB/E"}
|
|
@@ -48,6 +48,48 @@ function getGithubFeatureDiagnostics(options) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// src/integrations/github/pat.ts
|
|
52
|
+
var PAT_SETTINGS_USER_ID = "__github_org_settings__";
|
|
53
|
+
var FIELD_FOR_KIND = {
|
|
54
|
+
default: "pat",
|
|
55
|
+
reviewer: "reviewerPat"
|
|
56
|
+
};
|
|
57
|
+
function asToken(value) {
|
|
58
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
59
|
+
}
|
|
60
|
+
async function getGithubPat(getStorage, orgId, kind = "default") {
|
|
61
|
+
try {
|
|
62
|
+
const settings = await getStorage().settings.get(orgId, PAT_SETTINGS_USER_ID);
|
|
63
|
+
if (!settings) return null;
|
|
64
|
+
if (kind === "reviewer") return asToken(settings.reviewerPat) ?? asToken(settings.pat);
|
|
65
|
+
return asToken(settings.pat);
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function getGithubPatStatus(getStorage, orgId) {
|
|
71
|
+
try {
|
|
72
|
+
const settings = await getStorage().settings.get(orgId, PAT_SETTINGS_USER_ID);
|
|
73
|
+
return {
|
|
74
|
+
configured: asToken(settings?.pat) !== null,
|
|
75
|
+
reviewerConfigured: asToken(settings?.reviewerPat) !== null
|
|
76
|
+
};
|
|
77
|
+
} catch {
|
|
78
|
+
return { configured: false, reviewerConfigured: false };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function setGithubPat(storage, orgId, pat, kind = "default") {
|
|
82
|
+
const existing = await storage.settings.get(orgId, PAT_SETTINGS_USER_ID) ?? {};
|
|
83
|
+
await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, { ...existing, [FIELD_FOR_KIND[kind]]: pat });
|
|
84
|
+
}
|
|
85
|
+
async function clearGithubPat(storage, orgId, kind = "default") {
|
|
86
|
+
const existing = await storage.settings.get(orgId, PAT_SETTINGS_USER_ID);
|
|
87
|
+
const field = FIELD_FOR_KIND[kind];
|
|
88
|
+
if (!existing?.[field]) return;
|
|
89
|
+
const { [field]: _removed, ...rest } = existing;
|
|
90
|
+
await storage.settings.save(orgId, PAT_SETTINGS_USER_ID, rest);
|
|
91
|
+
}
|
|
92
|
+
|
|
51
93
|
// src/integrations/github/project-lock.ts
|
|
52
94
|
import { createHash } from "crypto";
|
|
53
95
|
var inProcessLocks = /* @__PURE__ */ new Map();
|
|
@@ -1353,6 +1395,56 @@ function buildGithubRoutes(options) {
|
|
|
1353
1395
|
}
|
|
1354
1396
|
})
|
|
1355
1397
|
);
|
|
1398
|
+
const parsePatKind = (value) => {
|
|
1399
|
+
if (value === void 0 || value === null || value === "default") return "default";
|
|
1400
|
+
if (value === "reviewer") return "reviewer";
|
|
1401
|
+
return null;
|
|
1402
|
+
};
|
|
1403
|
+
routes.push(
|
|
1404
|
+
registerApiRoute("/web/github/pat", {
|
|
1405
|
+
method: "GET",
|
|
1406
|
+
requiresAuth: false,
|
|
1407
|
+
handler: async (c) => {
|
|
1408
|
+
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1409
|
+
if ("response" in resolved) return resolved.response;
|
|
1410
|
+
return c.json(await getGithubPatStatus(() => github.integrationStorage, resolved.tenant.orgId));
|
|
1411
|
+
}
|
|
1412
|
+
}),
|
|
1413
|
+
registerApiRoute("/web/github/pat", {
|
|
1414
|
+
method: "POST",
|
|
1415
|
+
requiresAuth: false,
|
|
1416
|
+
handler: async (c) => {
|
|
1417
|
+
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1418
|
+
if ("response" in resolved) return resolved.response;
|
|
1419
|
+
let body;
|
|
1420
|
+
try {
|
|
1421
|
+
body = await c.req.json();
|
|
1422
|
+
} catch {
|
|
1423
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
1424
|
+
}
|
|
1425
|
+
const kind = parsePatKind(body.kind);
|
|
1426
|
+
if (!kind) return c.json({ error: "kind must be 'default' or 'reviewer'" }, 400);
|
|
1427
|
+
const token = typeof body.token === "string" ? body.token.trim() : "";
|
|
1428
|
+
if (!token) return c.json({ error: "A token is required" }, 400);
|
|
1429
|
+
if (token.length > 500) return c.json({ error: "Token too long (max 500 characters)" }, 400);
|
|
1430
|
+
if (/\s/.test(token)) return c.json({ error: "Token must not contain whitespace" }, 400);
|
|
1431
|
+
await setGithubPat(github.integrationStorage, resolved.tenant.orgId, token, kind);
|
|
1432
|
+
return c.json(await getGithubPatStatus(() => github.integrationStorage, resolved.tenant.orgId));
|
|
1433
|
+
}
|
|
1434
|
+
}),
|
|
1435
|
+
registerApiRoute("/web/github/pat", {
|
|
1436
|
+
method: "DELETE",
|
|
1437
|
+
requiresAuth: false,
|
|
1438
|
+
handler: async (c) => {
|
|
1439
|
+
const resolved = await resolveOrgTenant(loose(c), auth);
|
|
1440
|
+
if ("response" in resolved) return resolved.response;
|
|
1441
|
+
const kind = parsePatKind(c.req.query("kind"));
|
|
1442
|
+
if (!kind) return c.json({ error: "kind must be 'default' or 'reviewer'" }, 400);
|
|
1443
|
+
await clearGithubPat(github.integrationStorage, resolved.tenant.orgId, kind);
|
|
1444
|
+
return c.json(await getGithubPatStatus(() => github.integrationStorage, resolved.tenant.orgId));
|
|
1445
|
+
}
|
|
1446
|
+
})
|
|
1447
|
+
);
|
|
1356
1448
|
routes.push(...buildProjectGitRoutes({ github, auth, fleet, storage, emitAudit }));
|
|
1357
1449
|
return routes;
|
|
1358
1450
|
}
|
|
@@ -1395,11 +1487,12 @@ async function prepareProject(options) {
|
|
|
1395
1487
|
if (!access.authorization) {
|
|
1396
1488
|
throw new MaterializeError("Repository access did not include a bearer token.", "clone-failed");
|
|
1397
1489
|
}
|
|
1490
|
+
const ghCliToken = await getGithubPat(() => github.integrationStorage, project.installation.orgId) ?? access.authorization.token;
|
|
1398
1491
|
const sandbox = await ensureProjectSandbox({
|
|
1399
1492
|
fleet,
|
|
1400
1493
|
row: sandboxRow,
|
|
1401
1494
|
storage: github.sourceControlStorage.sandboxes,
|
|
1402
|
-
token:
|
|
1495
|
+
token: ghCliToken,
|
|
1403
1496
|
onProgress
|
|
1404
1497
|
});
|
|
1405
1498
|
const fresh = await github.sourceControlStorage.sandboxes.getById({ id: sandboxRow.id });
|