@opengeni/api-router 0.11.1 → 0.11.8
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/dist/app.d.ts +3 -2
- package/dist/app.js +3 -1
- package/dist/{chunk-7PQKPKW5.js → chunk-DQWFAIPE.js} +2311 -1661
- package/dist/chunk-DQWFAIPE.js.map +1 -0
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/app.ts +108 -2
- package/src/github-access.ts +117 -9
- package/src/github-browser-flow.ts +4 -4
- package/src/http/auth.ts +2 -3
- package/src/index.ts +20 -1
- package/src/mcp/documents.ts +19 -5
- package/src/mcp/server.ts +38 -12
- package/src/routes/documents.ts +191 -9
- package/src/routes/files.ts +1 -1
- package/src/routes/github.ts +331 -23
- package/src/routes/install.ts +8 -9
- package/src/routes/sessions.ts +27 -0
- package/dist/chunk-7PQKPKW5.js.map +0 -1
package/src/github-access.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
GitHubBindingStatus,
|
|
3
|
+
GitHubInstallationBinding,
|
|
4
|
+
GitHubRepository,
|
|
5
|
+
} from "@opengeni/contracts";
|
|
6
|
+
import {
|
|
7
|
+
hasAuditableGitHubInstallationAuthority,
|
|
8
|
+
listGitHubInstallationAccessForWorkspace,
|
|
9
|
+
} from "@opengeni/db";
|
|
10
|
+
import { listGitHubAppInstallationSummaries, listGitHubAppRepositories } from "@opengeni/github";
|
|
4
11
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
5
12
|
|
|
6
13
|
export async function listWorkspaceGitHubInstallationBindings(
|
|
@@ -8,10 +15,53 @@ export async function listWorkspaceGitHubInstallationBindings(
|
|
|
8
15
|
workspaceId: string,
|
|
9
16
|
): Promise<GitHubInstallationBinding[]> {
|
|
10
17
|
const installations = await listGitHubInstallationAccessForWorkspace(deps.db, workspaceId);
|
|
18
|
+
if (installations.length === 0) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
let liveById = new Map<number, LiveGitHubInstallation | null>();
|
|
22
|
+
let lifecycleVerified = false;
|
|
23
|
+
try {
|
|
24
|
+
if (deps.githubAppApi?.getInstallation) {
|
|
25
|
+
liveById = new Map(
|
|
26
|
+
await Promise.all(
|
|
27
|
+
installations.map(
|
|
28
|
+
async (installation) =>
|
|
29
|
+
[
|
|
30
|
+
installation.installationId,
|
|
31
|
+
await deps.githubAppApi!.getInstallation!({
|
|
32
|
+
installationId: installation.installationId,
|
|
33
|
+
}),
|
|
34
|
+
] as const,
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
lifecycleVerified = true;
|
|
39
|
+
} else if (!deps.githubAppApi) {
|
|
40
|
+
liveById = new Map(
|
|
41
|
+
(await listGitHubAppInstallationSummaries(deps.settings)).map((installation) => [
|
|
42
|
+
installation.installationId,
|
|
43
|
+
installation,
|
|
44
|
+
]),
|
|
45
|
+
);
|
|
46
|
+
lifecycleVerified = true;
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// A provider outage cannot make a stored row healthy. Preserve the row for
|
|
50
|
+
// audit/unlink but project it as unverified and keep workspace status
|
|
51
|
+
// unbound until GitHub can be checked again.
|
|
52
|
+
liveById = new Map();
|
|
53
|
+
}
|
|
11
54
|
return installations.map((installation) => ({
|
|
12
55
|
installationId: installation.installationId,
|
|
56
|
+
githubAccountId: installation.githubAccountId,
|
|
13
57
|
accountLogin: installation.accountLogin,
|
|
14
58
|
accountType: installation.accountType,
|
|
59
|
+
lifecycle: githubInstallationBindingLifecycle(
|
|
60
|
+
installation,
|
|
61
|
+
liveById,
|
|
62
|
+
installation.installationId,
|
|
63
|
+
lifecycleVerified,
|
|
64
|
+
),
|
|
15
65
|
repositoryScope: installation.repositoryScope,
|
|
16
66
|
repositoryCount: installation.repositoryIds.length,
|
|
17
67
|
createdAt: installation.createdAt,
|
|
@@ -19,28 +69,86 @@ export async function listWorkspaceGitHubInstallationBindings(
|
|
|
19
69
|
}));
|
|
20
70
|
}
|
|
21
71
|
|
|
72
|
+
export function githubBindingStatus(
|
|
73
|
+
configured: boolean,
|
|
74
|
+
installations: GitHubInstallationBinding[],
|
|
75
|
+
): GitHubBindingStatus {
|
|
76
|
+
if (!configured) {
|
|
77
|
+
return "disabled";
|
|
78
|
+
}
|
|
79
|
+
return installations.some((installation) => installation.lifecycle === "active")
|
|
80
|
+
? "bound"
|
|
81
|
+
: "unbound";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type LiveGitHubInstallation = {
|
|
85
|
+
installationId: number;
|
|
86
|
+
accountId: number;
|
|
87
|
+
suspended: boolean;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function githubInstallationBindingLifecycle(
|
|
91
|
+
stored: Awaited<ReturnType<typeof listGitHubInstallationAccessForWorkspace>>[number],
|
|
92
|
+
liveById: Map<number, LiveGitHubInstallation | null>,
|
|
93
|
+
installationId: number,
|
|
94
|
+
lifecycleVerified: boolean,
|
|
95
|
+
): GitHubInstallationBinding["lifecycle"] {
|
|
96
|
+
if (!hasAuditableGitHubInstallationAuthority(stored)) {
|
|
97
|
+
return "unverified";
|
|
98
|
+
}
|
|
99
|
+
if (!lifecycleVerified) {
|
|
100
|
+
return "unverified";
|
|
101
|
+
}
|
|
102
|
+
if (!liveById.has(installationId)) {
|
|
103
|
+
return "deleted";
|
|
104
|
+
}
|
|
105
|
+
const live = liveById.get(installationId);
|
|
106
|
+
if (!live) {
|
|
107
|
+
return "deleted";
|
|
108
|
+
}
|
|
109
|
+
if (live.suspended) {
|
|
110
|
+
return "suspended";
|
|
111
|
+
}
|
|
112
|
+
if (live.installationId !== installationId || stored.githubAccountId !== live.accountId) {
|
|
113
|
+
return "unverified";
|
|
114
|
+
}
|
|
115
|
+
return "active";
|
|
116
|
+
}
|
|
117
|
+
|
|
22
118
|
export async function listWorkspaceGitHubRepositories(
|
|
23
119
|
deps: ApiRouteDeps,
|
|
24
120
|
workspaceId: string,
|
|
25
121
|
): Promise<GitHubRepository[]> {
|
|
122
|
+
const bindings = await listWorkspaceGitHubInstallationBindings(deps, workspaceId);
|
|
123
|
+
const activeInstallationIds = new Set(
|
|
124
|
+
bindings
|
|
125
|
+
.filter((installation) => installation.lifecycle === "active")
|
|
126
|
+
.map((installation) => installation.installationId),
|
|
127
|
+
);
|
|
128
|
+
if (activeInstallationIds.size === 0) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
26
131
|
const access = await listGitHubInstallationAccessForWorkspace(deps.db, workspaceId);
|
|
27
|
-
|
|
132
|
+
const authorizedAccess = access.filter(
|
|
133
|
+
(installation) =>
|
|
134
|
+
activeInstallationIds.has(installation.installationId) &&
|
|
135
|
+
hasAuditableGitHubInstallationAuthority(installation),
|
|
136
|
+
);
|
|
137
|
+
if (authorizedAccess.length === 0) {
|
|
28
138
|
return [];
|
|
29
139
|
}
|
|
30
|
-
const installationIds =
|
|
140
|
+
const installationIds = authorizedAccess.map((installation) => installation.installationId);
|
|
31
141
|
const repositories = deps.githubAppApi?.listRepositories
|
|
32
142
|
? await deps.githubAppApi.listRepositories({ installationIds })
|
|
33
143
|
: await listGitHubAppRepositories(deps.settings, { installationIds });
|
|
34
144
|
const accessByInstallation = new Map(
|
|
35
|
-
|
|
145
|
+
authorizedAccess.map((installation) => [installation.installationId, installation]),
|
|
36
146
|
);
|
|
37
147
|
return repositories.filter((repository) => {
|
|
38
148
|
const installation = accessByInstallation.get(repository.installationId);
|
|
39
149
|
if (!installation) {
|
|
40
150
|
return false;
|
|
41
151
|
}
|
|
42
|
-
return (
|
|
43
|
-
installation.repositoryScope === "all" || installation.repositoryIds.includes(repository.id)
|
|
44
|
-
);
|
|
152
|
+
return installation.repositoryIds.includes(repository.id);
|
|
45
153
|
});
|
|
46
154
|
}
|
|
@@ -4,10 +4,10 @@ import { hasPermission } from "@opengeni/core";
|
|
|
4
4
|
import type { GitHubSignedStatePayload } from "@opengeni/github";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Bounded configured-token browser handoff. These signed claims preserve only
|
|
8
|
+
* the exact OpenGeni github:manage grant across GitHub redirects; the callback
|
|
9
|
+
* independently proves current GitHub personal/organization ownership. This
|
|
10
|
+
* state must never be interpreted as GitHub installation authority.
|
|
11
11
|
*/
|
|
12
12
|
export const githubBrowserGrantMaxAgeSeconds = 10 * 60;
|
|
13
13
|
|
package/src/http/auth.ts
CHANGED
|
@@ -59,9 +59,8 @@ function isAuthExempt(c: Context, settings: Settings): boolean {
|
|
|
59
59
|
if (path.startsWith("/v1/catalog-assets/")) {
|
|
60
60
|
return true;
|
|
61
61
|
}
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// and then terminates with 410 while new installation binding is disabled.
|
|
62
|
+
// The GitHub owner-consent entry remains public like the callbacks above; it
|
|
63
|
+
// verifies fresh signed workspace-bound state before redirecting to GitHub.
|
|
65
64
|
if (githubConnectPathPattern.test(path)) {
|
|
66
65
|
return true;
|
|
67
66
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,13 @@ import type {
|
|
|
12
12
|
ScheduledTaskOverlapPolicy,
|
|
13
13
|
ScheduledTaskScheduleSpec,
|
|
14
14
|
} from "@opengeni/contracts";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
assertRuntimeDatabasePosture,
|
|
17
|
+
createDb,
|
|
18
|
+
markSessionWorkflowWakeDelivered,
|
|
19
|
+
runtimeDatabaseReadyCheck,
|
|
20
|
+
type Database,
|
|
21
|
+
} from "@opengeni/db";
|
|
16
22
|
import { createNatsEventBus, type ResponderConnection } from "@opengeni/events";
|
|
17
23
|
import { createObservability, logStartupDependencyRetry } from "@opengeni/observability";
|
|
18
24
|
import { SESSION_WORKFLOW_WAKE_DISPATCHER_SCHEDULE_ID } from "@opengeni/core";
|
|
@@ -253,11 +259,21 @@ export async function startApi() {
|
|
|
253
259
|
const retryOptions = startupRetryOptions(settings);
|
|
254
260
|
const onRetry = (event: Parameters<typeof logStartupDependencyRetry>[1]) =>
|
|
255
261
|
logStartupDependencyRetry(observability, event);
|
|
262
|
+
const databasePosture = {
|
|
263
|
+
rlsStrategy: settings.rlsStrategy,
|
|
264
|
+
expectedRole: settings.runtimeDatabaseRole,
|
|
265
|
+
targetSchema: settings.dbSchema.trim() || "public",
|
|
266
|
+
} as const;
|
|
256
267
|
// The PRIVILEGED control-plane NATS login (M-AUTH): when the server runs with
|
|
257
268
|
// auth_callout, api/worker authenticate as a static account user permitted to
|
|
258
269
|
// request `agent.*.rpc`. Null in local dev (anonymous connect — the bus default).
|
|
259
270
|
const controlPlaneAuth = resolveNatsControlPlaneAuth(settings);
|
|
260
271
|
try {
|
|
272
|
+
await retryStartupDependency(
|
|
273
|
+
"PostgreSQL runtime posture",
|
|
274
|
+
() => assertRuntimeDatabasePosture(dbClient.db, databasePosture),
|
|
275
|
+
{ ...retryOptions, onRetry },
|
|
276
|
+
);
|
|
261
277
|
bus = await retryStartupDependency(
|
|
262
278
|
"NATS",
|
|
263
279
|
() =>
|
|
@@ -296,6 +312,9 @@ export async function startApi() {
|
|
|
296
312
|
workflowClient: workflowClient.client,
|
|
297
313
|
documentIndexer: workflowClient.documentIndexer,
|
|
298
314
|
observability,
|
|
315
|
+
readinessChecks: {
|
|
316
|
+
db: runtimeDatabaseReadyCheck(dbClient.db, databasePosture),
|
|
317
|
+
},
|
|
299
318
|
});
|
|
300
319
|
const server = Bun.serve({
|
|
301
320
|
hostname: settings.apiHost,
|
package/src/mcp/documents.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
getDocumentChunk,
|
|
3
3
|
listDocumentBases,
|
|
4
4
|
searchDocuments,
|
|
5
|
+
type DocumentAccessFilter,
|
|
5
6
|
type DocumentServices,
|
|
6
7
|
} from "@opengeni/documents";
|
|
7
8
|
import { createKnowledgeMemory, listKnowledgeMemories, type Database } from "@opengeni/db";
|
|
@@ -44,12 +45,23 @@ export function buildDocumentsMcpServer(
|
|
|
44
45
|
accountId: string,
|
|
45
46
|
workspaceId: string,
|
|
46
47
|
documentServices: DocumentServices,
|
|
47
|
-
options: {
|
|
48
|
+
options: {
|
|
49
|
+
createdBySessionId?: string | undefined;
|
|
50
|
+
/** The human subject whose agent is making this retrieval request. */
|
|
51
|
+
viewerSubjectId?: string | undefined;
|
|
52
|
+
} = {},
|
|
48
53
|
): McpServer {
|
|
49
54
|
const server = new McpServer({
|
|
50
55
|
name: "opengeni-documents",
|
|
51
56
|
version: "1.0.0",
|
|
52
57
|
});
|
|
58
|
+
// This server is the agent retrieval surface. Agent-disabled documents are
|
|
59
|
+
// never reachable. Workspace-visible documents are shared; private
|
|
60
|
+
// documents are available only to the creating subject's agent.
|
|
61
|
+
const agentAccess: DocumentAccessFilter = {
|
|
62
|
+
agentOnly: true,
|
|
63
|
+
...(options.viewerSubjectId ? { viewerSubjectId: options.viewerSubjectId } : {}),
|
|
64
|
+
};
|
|
53
65
|
|
|
54
66
|
server.registerTool(
|
|
55
67
|
"list_document_bases",
|
|
@@ -68,7 +80,7 @@ export function buildDocumentsMcpServer(
|
|
|
68
80
|
description: "Search indexed documents with hybrid, vector, or keyword retrieval.",
|
|
69
81
|
inputSchema: SearchInputSchema,
|
|
70
82
|
},
|
|
71
|
-
async (input) => searchContent(db, workspaceId, documentServices, input),
|
|
83
|
+
async (input) => searchContent(db, workspaceId, documentServices, input, agentAccess),
|
|
72
84
|
);
|
|
73
85
|
|
|
74
86
|
server.registerTool(
|
|
@@ -78,7 +90,7 @@ export function buildDocumentsMcpServer(
|
|
|
78
90
|
"Search company knowledge sources with optional base, source-kind, ACL, and retrieval-mode filters.",
|
|
79
91
|
inputSchema: SearchInputSchema,
|
|
80
92
|
},
|
|
81
|
-
async (input) => searchContent(db, workspaceId, documentServices, input),
|
|
93
|
+
async (input) => searchContent(db, workspaceId, documentServices, input, agentAccess),
|
|
82
94
|
);
|
|
83
95
|
|
|
84
96
|
server.registerTool(
|
|
@@ -90,7 +102,7 @@ export function buildDocumentsMcpServer(
|
|
|
90
102
|
},
|
|
91
103
|
},
|
|
92
104
|
async ({ chunkId }) => {
|
|
93
|
-
const found = await getDocumentChunk(db, workspaceId, chunkId);
|
|
105
|
+
const found = await getDocumentChunk(db, workspaceId, chunkId, agentAccess);
|
|
94
106
|
return {
|
|
95
107
|
content: [
|
|
96
108
|
{ type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
|
|
@@ -109,7 +121,7 @@ export function buildDocumentsMcpServer(
|
|
|
109
121
|
},
|
|
110
122
|
},
|
|
111
123
|
async ({ chunkId }) => {
|
|
112
|
-
const found = await getDocumentChunk(db, workspaceId, chunkId);
|
|
124
|
+
const found = await getDocumentChunk(db, workspaceId, chunkId, agentAccess);
|
|
113
125
|
return {
|
|
114
126
|
content: [
|
|
115
127
|
{ type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` },
|
|
@@ -214,6 +226,7 @@ async function searchContent(
|
|
|
214
226
|
| undefined;
|
|
215
227
|
aclTags?: string[] | undefined;
|
|
216
228
|
},
|
|
229
|
+
access: DocumentAccessFilter,
|
|
217
230
|
) {
|
|
218
231
|
return {
|
|
219
232
|
content: [
|
|
@@ -230,6 +243,7 @@ async function searchContent(
|
|
|
230
243
|
...(input.mode ? { mode: input.mode } : {}),
|
|
231
244
|
...(input.sourceKinds ? { sourceKinds: input.sourceKinds } : {}),
|
|
232
245
|
...(input.aclTags ? { aclTags: input.aclTags } : {}),
|
|
246
|
+
access,
|
|
233
247
|
},
|
|
234
248
|
documentServices,
|
|
235
249
|
),
|
package/src/mcp/server.ts
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
} from "@opengeni/db";
|
|
69
69
|
import { appendAndPublishEvents, publishDurableSessionEvents } from "@opengeni/events";
|
|
70
70
|
import {
|
|
71
|
+
createSignedState,
|
|
71
72
|
createGitHubAppInstallationToken,
|
|
72
73
|
GitHubAppConfigurationError,
|
|
73
74
|
githubAppMissingSettings,
|
|
@@ -82,7 +83,12 @@ import {
|
|
|
82
83
|
} from "@opengeni/core";
|
|
83
84
|
import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
|
|
84
85
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
85
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
githubBindingStatus,
|
|
88
|
+
listWorkspaceGitHubInstallationBindings,
|
|
89
|
+
listWorkspaceGitHubRepositories,
|
|
90
|
+
} from "../github-access";
|
|
91
|
+
import { githubBrowserBaseUrl, githubBrowserGrantClaims } from "../github-browser-flow";
|
|
86
92
|
import {
|
|
87
93
|
promoteVerifiedDefinitionEditChangeForApi,
|
|
88
94
|
proposeRigChangeForApi,
|
|
@@ -140,9 +146,8 @@ import type { ToolspaceMcpSurface } from "./toolspace";
|
|
|
140
146
|
import { ensureSessionGroupReady as ensureViewerSessionGroupReady } from "../sandbox/viewer";
|
|
141
147
|
|
|
142
148
|
export type McpServerOptions = {
|
|
143
|
-
// Origin of the HTTP request that reached the MCP route.
|
|
144
|
-
//
|
|
145
|
-
// it or mint state while new installation binding is disabled.
|
|
149
|
+
// Origin of the HTTP request that reached the MCP route. Browser-oriented
|
|
150
|
+
// tools use it only when no configured public base URL is available.
|
|
146
151
|
requestOrigin?: string | null;
|
|
147
152
|
toolspace?: ToolspaceMcpSurface | null;
|
|
148
153
|
workspaceMemoryEnabled?: boolean | undefined;
|
|
@@ -229,7 +234,7 @@ export function buildOpenGeniMcpServer(
|
|
|
229
234
|
registerWorkspaceOrchestrationTools(server, deps, grant, can, sessionId, toolspaceMode, json);
|
|
230
235
|
registerVariableSetTools(server, deps, grant, can, json);
|
|
231
236
|
if (can("github:use")) {
|
|
232
|
-
registerGitHubConnectTool(server, deps, json);
|
|
237
|
+
registerGitHubConnectTool(server, deps, grant, options, json);
|
|
233
238
|
// TOKEN-BROKER (B1): the agent-refreshable git token. Session-scoped (keys off the
|
|
234
239
|
// worker-signed sessionId claim so it mints for THIS session's repos), gated on
|
|
235
240
|
// the same github:use capability as github_connect_link.
|
|
@@ -2031,15 +2036,18 @@ function registerVariableSetTools(
|
|
|
2031
2036
|
}
|
|
2032
2037
|
}
|
|
2033
2038
|
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2039
|
+
function registerGitHubConnectTool(
|
|
2040
|
+
server: McpServer,
|
|
2041
|
+
deps: ApiRouteDeps,
|
|
2042
|
+
grant: AccessGrant,
|
|
2043
|
+
options: McpServerOptions,
|
|
2044
|
+
json: JsonResult,
|
|
2045
|
+
): void {
|
|
2038
2046
|
server.registerTool(
|
|
2039
2047
|
"github_connect_link",
|
|
2040
2048
|
{
|
|
2041
2049
|
description:
|
|
2042
|
-
"Report GitHub App
|
|
2050
|
+
"Report truthful GitHub App workspace binding status and, for a human grant with github:manage, return the fresh GitHub owner-consent link. Server App configuration alone is never reported as a usable binding.",
|
|
2043
2051
|
inputSchema: {},
|
|
2044
2052
|
},
|
|
2045
2053
|
async () => {
|
|
@@ -2049,17 +2057,35 @@ function registerGitHubConnectTool(server: McpServer, deps: ApiRouteDeps, json:
|
|
|
2049
2057
|
if (missing.length > 0 || !slug) {
|
|
2050
2058
|
return json({
|
|
2051
2059
|
configured: false,
|
|
2060
|
+
status: "disabled",
|
|
2052
2061
|
appSlug: slug,
|
|
2053
2062
|
installUrl: null,
|
|
2054
2063
|
linkUrl: null,
|
|
2055
2064
|
missing,
|
|
2056
2065
|
});
|
|
2057
2066
|
}
|
|
2067
|
+
const installations = await listWorkspaceGitHubInstallationBindings(deps, grant.workspaceId);
|
|
2068
|
+
const status = githubBindingStatus(true, installations);
|
|
2069
|
+
const baseUrl = githubBrowserBaseUrl(settings, options.requestOrigin);
|
|
2070
|
+
const state =
|
|
2071
|
+
baseUrl && hasPermission(grant.permissions, "github:manage")
|
|
2072
|
+
? createSignedState(deps.githubStateSecret, {
|
|
2073
|
+
accountId: grant.accountId,
|
|
2074
|
+
workspaceId: grant.workspaceId,
|
|
2075
|
+
intent: "installation_authority",
|
|
2076
|
+
...githubBrowserGrantClaims(settings, grant),
|
|
2077
|
+
})
|
|
2078
|
+
: null;
|
|
2079
|
+
const connectUrl = state
|
|
2080
|
+
? `${baseUrl}/v1/workspaces/${grant.workspaceId}/github/connect?state=${encodeURIComponent(state)}`
|
|
2081
|
+
: null;
|
|
2058
2082
|
return json({
|
|
2059
2083
|
configured: true,
|
|
2084
|
+
status,
|
|
2060
2085
|
appSlug: slug,
|
|
2061
|
-
installUrl:
|
|
2062
|
-
linkUrl:
|
|
2086
|
+
installUrl: connectUrl,
|
|
2087
|
+
linkUrl: connectUrl,
|
|
2088
|
+
installations,
|
|
2063
2089
|
missing: [],
|
|
2064
2090
|
});
|
|
2065
2091
|
},
|