@inkeep/agents-work-apps 0.0.0-dev-20260203033642

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.
@@ -0,0 +1,217 @@
1
+ import { env } from "../../env.js";
2
+ import { getLogger } from "../../logger.js";
3
+ import runDbClient_default from "../../db/runDbClient.js";
4
+ import { getStateSigningSecret, isStateSigningConfigured } from "../config.js";
5
+ import { createAppJwt, determineStatus, fetchInstallationDetails, fetchInstallationRepositories } from "../installation.js";
6
+ import { createInstallation, generateId, getInstallationByGitHubId, listProjectsMetadata, setProjectAccessMode, syncRepositories, updateInstallationStatusByGitHubId } from "@inkeep/agents-core";
7
+ import { Hono } from "hono";
8
+ import { jwtVerify } from "jose";
9
+ import { z } from "zod";
10
+
11
+ //#region src/github/routes/setup.ts
12
+ const logger = getLogger("github-setup");
13
+ const STATE_JWT_ISSUER = "inkeep-agents-api";
14
+ const STATE_JWT_AUDIENCE = "github-app-install";
15
+ const CallbackQuerySchema = z.object({
16
+ installation_id: z.string(),
17
+ setup_action: z.enum([
18
+ "install",
19
+ "update",
20
+ "request"
21
+ ]),
22
+ state: z.string()
23
+ });
24
+ function getManageUiUrl() {
25
+ return env.INKEEP_AGENTS_MANAGE_UI_URL || "http://localhost:3000";
26
+ }
27
+ function buildErrorRedirectUrl(message) {
28
+ const baseUrl = getManageUiUrl();
29
+ const url = new URL("/github/setup-error", baseUrl);
30
+ url.searchParams.set("message", message);
31
+ return url.toString();
32
+ }
33
+ function buildRedirectUrl(params) {
34
+ const baseUrl = getManageUiUrl();
35
+ const url = new URL(`/${params.tenantId}/work-apps/github`, baseUrl);
36
+ url.searchParams.set("status", params.status);
37
+ if (params.message) url.searchParams.set("message", params.message);
38
+ if (params.installationId) url.searchParams.set("installation_id", params.installationId);
39
+ return url.toString();
40
+ }
41
+ async function verifyStateToken(state) {
42
+ if (!isStateSigningConfigured()) return {
43
+ success: false,
44
+ error: "GitHub App installation is not configured"
45
+ };
46
+ const secret = getStateSigningSecret();
47
+ const secretKey = new TextEncoder().encode(secret);
48
+ try {
49
+ const { payload } = await jwtVerify(state, secretKey, {
50
+ issuer: STATE_JWT_ISSUER,
51
+ audience: STATE_JWT_AUDIENCE
52
+ });
53
+ const tenantId = payload.tenantId;
54
+ if (!tenantId) return {
55
+ success: false,
56
+ error: "Invalid state: missing tenantId"
57
+ };
58
+ return {
59
+ success: true,
60
+ tenantId
61
+ };
62
+ } catch (error) {
63
+ if (error instanceof Error) {
64
+ const errorMessage = error.message.toLowerCase();
65
+ const errorName = error.name.toLowerCase();
66
+ if (errorMessage.includes("expired") || errorName.includes("expired")) return {
67
+ success: false,
68
+ error: "State token has expired. Please try installing again."
69
+ };
70
+ if (errorMessage.includes("signature")) return {
71
+ success: false,
72
+ error: "Invalid state signature"
73
+ };
74
+ }
75
+ logger.error({ error }, "Failed to verify state token");
76
+ return {
77
+ success: false,
78
+ error: "Invalid state token"
79
+ };
80
+ }
81
+ }
82
+ const app = new Hono();
83
+ app.get("/", async (c) => {
84
+ const queryParams = {
85
+ installation_id: c.req.query("installation_id"),
86
+ setup_action: c.req.query("setup_action"),
87
+ state: c.req.query("state")
88
+ };
89
+ const parseResult = CallbackQuerySchema.safeParse(queryParams);
90
+ if (!parseResult.success) {
91
+ logger.warn({ errors: parseResult.error.issues }, "Invalid callback parameters");
92
+ return c.redirect(buildErrorRedirectUrl("Invalid callback parameters"));
93
+ }
94
+ const { installation_id, setup_action, state } = parseResult.data;
95
+ logger.info({
96
+ installation_id,
97
+ setup_action
98
+ }, "Processing GitHub callback");
99
+ const stateResult = await verifyStateToken(state);
100
+ if (!stateResult.success) {
101
+ logger.warn({ error: stateResult.error }, "State verification failed");
102
+ return c.redirect(buildErrorRedirectUrl(stateResult.error));
103
+ }
104
+ const { tenantId } = stateResult;
105
+ logger.info({
106
+ tenantId,
107
+ installation_id
108
+ }, "State verified successfully");
109
+ let appJwt;
110
+ try {
111
+ appJwt = await createAppJwt();
112
+ } catch (error) {
113
+ logger.error({ error }, "Failed to create GitHub App JWT");
114
+ return c.redirect(buildErrorRedirectUrl("GitHub App not configured properly"));
115
+ }
116
+ const installationResult = await fetchInstallationDetails(installation_id, appJwt);
117
+ if (!installationResult.success) return c.redirect(buildRedirectUrl({
118
+ tenantId,
119
+ status: "error",
120
+ message: "Failed to verify installation with GitHub"
121
+ }));
122
+ const { installation } = installationResult;
123
+ logger.info({
124
+ accountLogin: installation.account.login,
125
+ accountType: installation.account.type,
126
+ installationId: installation.id
127
+ }, "Fetched installation details from GitHub");
128
+ const reposResult = await fetchInstallationRepositories(installation_id, appJwt);
129
+ if (!reposResult.success) logger.warn({ error: reposResult.error }, "Failed to fetch repositories, continuing with empty list");
130
+ const repositories = reposResult.success ? reposResult.repositories : [];
131
+ logger.info({ repositoryCount: repositories.length }, "Fetched repositories from GitHub");
132
+ const status = determineStatus(setup_action);
133
+ try {
134
+ const existingInstallation = await getInstallationByGitHubId(runDbClient_default)(installation_id);
135
+ let internalInstallationId;
136
+ if (existingInstallation) {
137
+ logger.info({
138
+ existingId: existingInstallation.id,
139
+ setup_action
140
+ }, "Updating existing installation");
141
+ const updated = await updateInstallationStatusByGitHubId(runDbClient_default)({
142
+ gitHubInstallationId: installation_id,
143
+ status
144
+ });
145
+ if (!updated) throw new Error("Failed to update installation status");
146
+ internalInstallationId = updated.id;
147
+ } else {
148
+ logger.info({
149
+ tenantId,
150
+ setup_action
151
+ }, "Creating new installation record");
152
+ internalInstallationId = (await createInstallation(runDbClient_default)({
153
+ id: generateId(),
154
+ tenantId,
155
+ installationId: installation_id,
156
+ accountLogin: installation.account.login,
157
+ accountId: String(installation.account.id),
158
+ accountType: installation.account.type,
159
+ status
160
+ })).id;
161
+ const projectsInTenant = await listProjectsMetadata(runDbClient_default)({ tenantId });
162
+ if (projectsInTenant.length > 0) {
163
+ logger.info({
164
+ tenantId,
165
+ projectCount: projectsInTenant.length
166
+ }, "Setting GitHub access mode to \"all\" for all existing projects");
167
+ await Promise.all(projectsInTenant.map((project) => setProjectAccessMode(runDbClient_default)({
168
+ tenantId,
169
+ projectId: project.id,
170
+ mode: "all"
171
+ })));
172
+ }
173
+ }
174
+ if (repositories.length > 0) {
175
+ const syncResult = await syncRepositories(runDbClient_default)({
176
+ installationId: internalInstallationId,
177
+ repositories: repositories.map((repo) => ({
178
+ repositoryId: String(repo.id),
179
+ repositoryName: repo.name,
180
+ repositoryFullName: repo.full_name,
181
+ private: repo.private
182
+ }))
183
+ });
184
+ logger.info({
185
+ added: syncResult.added,
186
+ removed: syncResult.removed,
187
+ updated: syncResult.updated
188
+ }, "Synced repositories");
189
+ }
190
+ logger.info({
191
+ tenantId,
192
+ installationId: installation_id,
193
+ accountLogin: installation.account.login,
194
+ status
195
+ }, "GitHub App installation processed successfully");
196
+ return c.redirect(buildRedirectUrl({
197
+ tenantId,
198
+ status: "success",
199
+ installationId: internalInstallationId
200
+ }));
201
+ } catch (error) {
202
+ logger.error({
203
+ error,
204
+ tenantId,
205
+ installation_id
206
+ }, "Failed to store installation in database");
207
+ return c.redirect(buildRedirectUrl({
208
+ tenantId,
209
+ status: "error",
210
+ message: "Failed to complete installation setup"
211
+ }));
212
+ }
213
+ });
214
+ var setup_default = app;
215
+
216
+ //#endregion
217
+ export { setup_default as default };
@@ -0,0 +1,7 @@
1
+ import { Hono } from "hono";
2
+ import * as hono_types0 from "hono/types";
3
+
4
+ //#region src/github/routes/tokenExchange.d.ts
5
+ declare const app: Hono<hono_types0.BlankEnv, hono_types0.BlankSchema, "/">;
6
+ //#endregion
7
+ export { app as default };
@@ -0,0 +1,233 @@
1
+ import { getLogger } from "../../logger.js";
2
+ import runDbClient_default from "../../db/runDbClient.js";
3
+ import { isGitHubAppConfigured } from "../config.js";
4
+ import { generateInstallationAccessToken, lookupInstallationForRepo } from "../installation.js";
5
+ import { validateOidcToken } from "../oidcToken.js";
6
+ import { checkProjectRepositoryAccess, getInstallationByGitHubId } from "@inkeep/agents-core";
7
+ import { Hono } from "hono";
8
+ import { z } from "zod";
9
+
10
+ //#region src/github/routes/tokenExchange.ts
11
+ const logger = getLogger("github-token-exchange");
12
+ const TokenExchangeRequestSchema = z.object({
13
+ oidc_token: z.string(),
14
+ project_id: z.string().optional()
15
+ });
16
+ const app = new Hono();
17
+ /**
18
+ * Exchange GitHub OIDC token for installation token.
19
+ *
20
+ * This is an internal infrastructure endpoint called by the CLI from GitHub Actions.
21
+ * It exchanges a GitHub Actions OIDC token for a GitHub App installation access token.
22
+ * Not included in the public OpenAPI spec.
23
+ */
24
+ app.post("/", async (c) => {
25
+ const rawBody = await c.req.json().catch(() => null);
26
+ const parseResult = TokenExchangeRequestSchema.safeParse(rawBody);
27
+ if (!parseResult.success) {
28
+ const errorMessage = parseResult.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
29
+ c.header("Content-Type", "application/problem+json");
30
+ return c.json({
31
+ title: "Bad Request",
32
+ status: 400,
33
+ detail: errorMessage,
34
+ error: errorMessage
35
+ }, 400);
36
+ }
37
+ const body = parseResult.data;
38
+ logger.info({}, "Processing token exchange request");
39
+ if (!isGitHubAppConfigured()) {
40
+ logger.error({}, "GitHub App credentials not configured");
41
+ const errorMessage = "GitHub App credentials are not configured. Please contact the administrator to set up GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY.";
42
+ c.header("Content-Type", "application/problem+json");
43
+ return c.json({
44
+ title: "GitHub App Not Configured",
45
+ status: 500,
46
+ detail: errorMessage,
47
+ error: errorMessage
48
+ }, 500);
49
+ }
50
+ const validationResult = await validateOidcToken(body.oidc_token);
51
+ if (!validationResult.success) {
52
+ const errorType = validationResult.errorType;
53
+ logger.warn({
54
+ errorType,
55
+ message: validationResult.message
56
+ }, "OIDC token validation failed");
57
+ c.header("Content-Type", "application/problem+json");
58
+ if (errorType === "malformed") return c.json({
59
+ title: "Bad Request",
60
+ status: 400,
61
+ detail: validationResult.message,
62
+ error: validationResult.message
63
+ }, 400);
64
+ return c.json({
65
+ title: "Token Validation Failed",
66
+ status: 401,
67
+ detail: validationResult.message,
68
+ error: validationResult.message
69
+ }, 401);
70
+ }
71
+ const { claims } = validationResult;
72
+ const installationResult = await lookupInstallationForRepo(claims.repository_owner, claims.repository.split("/")[1]);
73
+ if (!installationResult.success) {
74
+ const { errorType, message } = installationResult;
75
+ if (errorType === "not_installed") {
76
+ c.header("Content-Type", "application/problem+json");
77
+ return c.json({
78
+ title: "GitHub App Not Installed",
79
+ status: 403,
80
+ detail: message,
81
+ error: message
82
+ }, 403);
83
+ }
84
+ logger.error({
85
+ errorType,
86
+ message,
87
+ repository: claims.repository
88
+ }, "Failed to look up GitHub App installation");
89
+ c.header("Content-Type", "application/problem+json");
90
+ return c.json({
91
+ title: "Installation Lookup Failed",
92
+ status: 500,
93
+ detail: message,
94
+ error: message
95
+ }, 500);
96
+ }
97
+ const { installation } = installationResult;
98
+ logger.info({
99
+ installationId: installation.installationId,
100
+ repository: claims.repository
101
+ }, "Found GitHub App installation");
102
+ const dbInstallation = await getInstallationByGitHubId(runDbClient_default)(installation.installationId.toString());
103
+ if (!dbInstallation) {
104
+ const errorMessage = "GitHub App installation not registered. Please connect your GitHub organization in the Inkeep dashboard.";
105
+ logger.warn({
106
+ installationId: installation.installationId,
107
+ repository: claims.repository
108
+ }, "Installation not found in database");
109
+ c.header("Content-Type", "application/problem+json");
110
+ return c.json({
111
+ title: "Installation Not Registered",
112
+ status: 403,
113
+ detail: errorMessage,
114
+ error: errorMessage
115
+ }, 403);
116
+ }
117
+ if (dbInstallation.status === "pending") {
118
+ const errorMessage = "GitHub App installation is pending organization admin approval";
119
+ logger.warn({
120
+ installationId: installation.installationId,
121
+ repository: claims.repository,
122
+ status: dbInstallation.status
123
+ }, "Installation is pending approval");
124
+ c.header("Content-Type", "application/problem+json");
125
+ return c.json({
126
+ title: "Installation Pending",
127
+ status: 403,
128
+ detail: errorMessage,
129
+ error: errorMessage
130
+ }, 403);
131
+ }
132
+ if (dbInstallation.status === "suspended") {
133
+ const errorMessage = "GitHub App installation is suspended";
134
+ logger.warn({
135
+ installationId: installation.installationId,
136
+ repository: claims.repository,
137
+ status: dbInstallation.status
138
+ }, "Installation is suspended");
139
+ c.header("Content-Type", "application/problem+json");
140
+ return c.json({
141
+ title: "Installation Suspended",
142
+ status: 403,
143
+ detail: errorMessage,
144
+ error: errorMessage
145
+ }, 403);
146
+ }
147
+ if (dbInstallation.status === "deleted") {
148
+ const errorMessage = "GitHub App installation has been disconnected";
149
+ logger.warn({
150
+ installationId: installation.installationId,
151
+ repository: claims.repository,
152
+ status: dbInstallation.status
153
+ }, "Installation has been deleted");
154
+ c.header("Content-Type", "application/problem+json");
155
+ return c.json({
156
+ title: "Installation Disconnected",
157
+ status: 403,
158
+ detail: errorMessage,
159
+ error: errorMessage
160
+ }, 403);
161
+ }
162
+ logger.info({
163
+ installationId: installation.installationId,
164
+ tenantId: dbInstallation.tenantId,
165
+ repository: claims.repository
166
+ }, "Installation validated against database");
167
+ if (body.project_id) {
168
+ const accessCheck = await checkProjectRepositoryAccess(runDbClient_default)({
169
+ projectId: body.project_id,
170
+ repositoryFullName: claims.repository,
171
+ tenantId: dbInstallation.tenantId
172
+ });
173
+ if (!accessCheck.hasAccess) {
174
+ const errorMessage = `Project does not have access to repository ${claims.repository}. ${accessCheck.reason}`;
175
+ logger.warn({
176
+ installationId: installation.installationId,
177
+ tenantId: dbInstallation.tenantId,
178
+ projectId: body.project_id,
179
+ repository: claims.repository,
180
+ reason: accessCheck.reason
181
+ }, "Project does not have access to repository");
182
+ c.header("Content-Type", "application/problem+json");
183
+ return c.json({
184
+ title: "Repository Access Denied",
185
+ status: 403,
186
+ detail: errorMessage,
187
+ error: errorMessage
188
+ }, 403);
189
+ }
190
+ logger.info({
191
+ installationId: installation.installationId,
192
+ tenantId: dbInstallation.tenantId,
193
+ projectId: body.project_id,
194
+ repository: claims.repository,
195
+ reason: accessCheck.reason
196
+ }, "Project has access to repository");
197
+ }
198
+ const tokenResult = await generateInstallationAccessToken(installation.installationId);
199
+ if (!tokenResult.success) {
200
+ const { errorType, message } = tokenResult;
201
+ logger.error({
202
+ errorType,
203
+ message,
204
+ installationId: installation.installationId,
205
+ repository: claims.repository
206
+ }, "Failed to generate installation access token");
207
+ c.header("Content-Type", "application/problem+json");
208
+ return c.json({
209
+ title: "Token Generation Failed",
210
+ status: 500,
211
+ detail: message,
212
+ error: message
213
+ }, 500);
214
+ }
215
+ const { accessToken } = tokenResult;
216
+ logger.info({
217
+ installationId: installation.installationId,
218
+ tenantId: dbInstallation.tenantId,
219
+ repository: claims.repository,
220
+ expiresAt: accessToken.expiresAt
221
+ }, "Token exchange completed successfully");
222
+ return c.json({
223
+ token: accessToken.token,
224
+ expires_at: accessToken.expiresAt,
225
+ repository: claims.repository,
226
+ installation_id: installation.installationId,
227
+ tenant_id: dbInstallation.tenantId
228
+ }, 200);
229
+ });
230
+ var tokenExchange_default = app;
231
+
232
+ //#endregion
233
+ export { tokenExchange_default as default };
@@ -0,0 +1,12 @@
1
+ import { Hono } from "hono";
2
+ import * as hono_types4 from "hono/types";
3
+
4
+ //#region src/github/routes/webhooks.d.ts
5
+ interface WebhookVerificationResult {
6
+ success: boolean;
7
+ error?: string;
8
+ }
9
+ declare function verifyWebhookSignature(payload: string, signature: string | undefined, secret: string): WebhookVerificationResult;
10
+ declare const app: Hono<hono_types4.BlankEnv, hono_types4.BlankSchema, "/">;
11
+ //#endregion
12
+ export { WebhookVerificationResult, app as default, verifyWebhookSignature };