@opengeni/core 0.4.5 → 0.4.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/dist/index.d.ts +179 -30
- package/dist/index.js +1324 -477
- package/dist/index.js.map +1 -1
- package/package.json +18 -18
- package/src/access/index.ts +132 -57
- package/src/billing/limits.ts +76 -34
- package/src/dependencies.ts +76 -14
- package/src/domain/capabilities.ts +388 -182
- package/src/domain/environments.ts +58 -38
- package/src/domain/packs.ts +49 -16
- package/src/domain/resources.ts +71 -28
- package/src/domain/scheduled-tasks.ts +107 -41
- package/src/domain/sessions.ts +514 -258
- package/src/domain/workspace-members.ts +6 -2
- package/src/index.ts +1 -0
- package/src/rigs/index.ts +540 -0
- package/src/sandbox/fleet.ts +97 -18
- package/src/sandbox/routing.ts +6 -1
- package/src/sandbox-types.ts +17 -5
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { environmentsEncryptionKeyBytes, type Settings } from "@opengeni/config";
|
|
2
|
-
import type { AccessGrant,
|
|
3
|
-
import {
|
|
4
|
-
getWorkspaceEnvironment,
|
|
5
|
-
recordAuditEvent,
|
|
6
|
-
type Database,
|
|
7
|
-
} from "@opengeni/db";
|
|
2
|
+
import type { AccessGrant, VariableSet } from "@opengeni/contracts";
|
|
3
|
+
import { getVariableSet, recordAuditEvent, type Database } from "@opengeni/db";
|
|
8
4
|
import { HTTPException } from "hono/http-exception";
|
|
9
5
|
import { requirePermission } from "../access";
|
|
10
6
|
|
|
@@ -13,8 +9,8 @@ export const MAX_VARIABLES_PER_ENVIRONMENT = 100;
|
|
|
13
9
|
|
|
14
10
|
// Names the platform itself injects into sandboxes (sandboxEnvironmentForRun,
|
|
15
11
|
// collectGitIdentityEnvironment) plus loader/startup-injection vectors. These
|
|
16
|
-
// can never be set as
|
|
17
|
-
//
|
|
12
|
+
// can never be set as variable set variables, so the run-scoped
|
|
13
|
+
// git auth block and git identity always win without silent collisions.
|
|
18
14
|
const reservedExactNames = new Set([
|
|
19
15
|
"HOME",
|
|
20
16
|
"PATH",
|
|
@@ -32,6 +28,8 @@ const reservedExactNames = new Set([
|
|
|
32
28
|
"PERL5LIB",
|
|
33
29
|
"GH_TOKEN",
|
|
34
30
|
"GITHUB_TOKEN",
|
|
31
|
+
"GITLAB_TOKEN",
|
|
32
|
+
"AZURE_DEVOPS_EXT_PAT",
|
|
35
33
|
"GIT_ASKPASS",
|
|
36
34
|
"GIT_TERMINAL_PROMPT",
|
|
37
35
|
]);
|
|
@@ -45,70 +43,92 @@ const reservedPrefixes = [
|
|
|
45
43
|
"DYLD_",
|
|
46
44
|
];
|
|
47
45
|
|
|
48
|
-
export function
|
|
46
|
+
export function assertAllowedVariableSetVariableName(name: string): void {
|
|
49
47
|
if (reservedExactNames.has(name) || reservedPrefixes.some((prefix) => name.startsWith(prefix))) {
|
|
50
|
-
throw new HTTPException(422, {
|
|
48
|
+
throw new HTTPException(422, {
|
|
49
|
+
message: `reserved variable set variable name / reserved environment variable name: ${name}`,
|
|
50
|
+
});
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
/** @deprecated use assertAllowedVariableSetVariableName */
|
|
55
|
+
export const assertAllowedEnvironmentVariableName = assertAllowedVariableSetVariableName;
|
|
56
|
+
|
|
57
|
+
export function requireVariableSetEncryption(settings: Settings): Uint8Array {
|
|
55
58
|
const key = environmentsEncryptionKeyBytes(settings);
|
|
56
59
|
if (!key) {
|
|
57
|
-
throw new HTTPException(503, {
|
|
60
|
+
throw new HTTPException(503, {
|
|
61
|
+
message: "variable sets require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY",
|
|
62
|
+
});
|
|
58
63
|
}
|
|
59
64
|
return key;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
/** @deprecated use requireVariableSetEncryption */
|
|
68
|
+
export const requireEnvironmentEncryption = requireVariableSetEncryption;
|
|
69
|
+
|
|
70
|
+
export async function requireVariableSetForApi(
|
|
71
|
+
db: Database,
|
|
72
|
+
workspaceId: string,
|
|
73
|
+
variableSetId: string,
|
|
74
|
+
): Promise<VariableSet> {
|
|
75
|
+
const variableSet = await getVariableSet(db, workspaceId, variableSetId);
|
|
76
|
+
if (!variableSet) {
|
|
77
|
+
throw new HTTPException(404, { message: "variableSet not found" });
|
|
66
78
|
}
|
|
67
|
-
return
|
|
79
|
+
return variableSet;
|
|
68
80
|
}
|
|
69
81
|
|
|
70
82
|
/**
|
|
71
|
-
* Validates an
|
|
83
|
+
* Validates an variableSet attachment supplied in a request payload (session
|
|
72
84
|
* create, scheduled task create/update, pack enable). Requires the
|
|
73
|
-
* `
|
|
85
|
+
* `variable-sets:use` permission unless the attachment was already authorized
|
|
74
86
|
* (pack-installation-inherited attachments), and maps a missing or
|
|
75
|
-
* cross-
|
|
87
|
+
* cross-variable set to 422 because the id is payload, not the route
|
|
76
88
|
* target. RLS plus the workspace_id clause make cross-workspace ids
|
|
77
89
|
* indistinguishable from missing ones.
|
|
78
90
|
*/
|
|
79
|
-
export async function
|
|
91
|
+
export async function validateVariableSetAttachment(
|
|
80
92
|
deps: { settings: Settings; db: Database },
|
|
81
93
|
grant: AccessGrant,
|
|
82
94
|
workspaceId: string,
|
|
83
|
-
|
|
95
|
+
variableSetId: string,
|
|
84
96
|
options: { preauthorized?: boolean } = {},
|
|
85
|
-
): Promise<
|
|
86
|
-
|
|
97
|
+
): Promise<VariableSet> {
|
|
98
|
+
requireVariableSetEncryption(deps.settings);
|
|
87
99
|
if (!options.preauthorized) {
|
|
88
|
-
requirePermission(grant, "
|
|
100
|
+
requirePermission(grant, "variable-sets:use");
|
|
89
101
|
}
|
|
90
|
-
const
|
|
91
|
-
if (!
|
|
92
|
-
throw new HTTPException(422, { message: "unknown
|
|
102
|
+
const variableSet = await getVariableSet(deps.db, workspaceId, variableSetId);
|
|
103
|
+
if (!variableSet) {
|
|
104
|
+
throw new HTTPException(422, { message: "unknown variableSetId" });
|
|
93
105
|
}
|
|
94
|
-
return
|
|
106
|
+
return variableSet;
|
|
95
107
|
}
|
|
96
108
|
|
|
97
|
-
export async function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
export async function recordVariableSetAuditEvent(
|
|
110
|
+
db: Database,
|
|
111
|
+
input: {
|
|
112
|
+
grant: AccessGrant;
|
|
113
|
+
action:
|
|
114
|
+
| "variable_set.created"
|
|
115
|
+
| "variable_set.updated"
|
|
116
|
+
| "variable_set.deleted"
|
|
117
|
+
| "variable_set.variable.set"
|
|
118
|
+
| "variable_set.variable.deleted";
|
|
119
|
+
variableSetId: string;
|
|
120
|
+
variableName?: string;
|
|
121
|
+
},
|
|
122
|
+
): Promise<void> {
|
|
103
123
|
await recordAuditEvent(db, {
|
|
104
124
|
accountId: input.grant.accountId,
|
|
105
125
|
workspaceId: input.grant.workspaceId,
|
|
106
126
|
subjectId: input.grant.subjectId,
|
|
107
127
|
action: input.action,
|
|
108
|
-
targetType: "
|
|
109
|
-
targetId: input.
|
|
128
|
+
targetType: "workspace_variable_set",
|
|
129
|
+
targetId: input.variableSetId,
|
|
110
130
|
metadata: {
|
|
111
|
-
|
|
131
|
+
variableSetId: input.variableSetId,
|
|
112
132
|
...(input.variableName ? { name: input.variableName } : {}),
|
|
113
133
|
},
|
|
114
134
|
});
|
package/src/domain/packs.ts
CHANGED
|
@@ -3,7 +3,12 @@ import {
|
|
|
3
3
|
type ScheduledTaskAgentConfig,
|
|
4
4
|
type SocialConnection,
|
|
5
5
|
} from "@opengeni/contracts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getWorkspacePack,
|
|
8
|
+
listPackInstallations,
|
|
9
|
+
listWorkspacePacks,
|
|
10
|
+
type Database,
|
|
11
|
+
} from "@opengeni/db";
|
|
7
12
|
import { HTTPException } from "hono/http-exception";
|
|
8
13
|
|
|
9
14
|
export const MARKETING_SOCIAL_PACK_ID = "marketing-social-daily-analysis";
|
|
@@ -11,7 +16,8 @@ export const MARKETING_SOCIAL_PACK_ID = "marketing-social-daily-analysis";
|
|
|
11
16
|
const marketingSocialPack: CapabilityPack = {
|
|
12
17
|
id: MARKETING_SOCIAL_PACK_ID,
|
|
13
18
|
name: "Marketing social daily analysis",
|
|
14
|
-
description:
|
|
19
|
+
description:
|
|
20
|
+
"Connect social accounts, attach marketing knowledge, and schedule agents to produce daily media performance analysis.",
|
|
15
21
|
role: "marketing",
|
|
16
22
|
category: "social-media",
|
|
17
23
|
version: "0.1.0",
|
|
@@ -54,7 +60,12 @@ const marketingSocialPack: CapabilityPack = {
|
|
|
54
60
|
category: "social-media",
|
|
55
61
|
authModel: "oauth2_authorization_code",
|
|
56
62
|
providers: ["instagram", "facebook"],
|
|
57
|
-
scopes: [
|
|
63
|
+
scopes: [
|
|
64
|
+
"instagram_basic",
|
|
65
|
+
"instagram_manage_insights",
|
|
66
|
+
"pages_read_engagement",
|
|
67
|
+
"pages_show_list",
|
|
68
|
+
],
|
|
58
69
|
required: false,
|
|
59
70
|
metadata: {
|
|
60
71
|
docs: "https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/",
|
|
@@ -78,7 +89,10 @@ const marketingSocialPack: CapabilityPack = {
|
|
|
78
89
|
category: "social-media",
|
|
79
90
|
authModel: "oauth2_authorization_code",
|
|
80
91
|
providers: ["youtube"],
|
|
81
|
-
scopes: [
|
|
92
|
+
scopes: [
|
|
93
|
+
"https://www.googleapis.com/auth/youtube.readonly",
|
|
94
|
+
"https://www.googleapis.com/auth/yt-analytics.readonly",
|
|
95
|
+
],
|
|
82
96
|
required: false,
|
|
83
97
|
metadata: {
|
|
84
98
|
docs: "https://developers.google.com/youtube/v3",
|
|
@@ -90,7 +104,8 @@ const marketingSocialPack: CapabilityPack = {
|
|
|
90
104
|
type: "document_base",
|
|
91
105
|
id: "marketing-playbook",
|
|
92
106
|
name: "Marketing playbook",
|
|
93
|
-
description:
|
|
107
|
+
description:
|
|
108
|
+
"Optional workspace document base with brand voice, campaign calendars, audience research, and reporting rules.",
|
|
94
109
|
required: false,
|
|
95
110
|
},
|
|
96
111
|
],
|
|
@@ -139,7 +154,10 @@ export function isBuiltInCapabilityPack(packId: string): boolean {
|
|
|
139
154
|
* (for example after a contract tightening) are skipped instead of breaking
|
|
140
155
|
* the whole catalog.
|
|
141
156
|
*/
|
|
142
|
-
export async function listWorkspaceCapabilityPacks(
|
|
157
|
+
export async function listWorkspaceCapabilityPacks(
|
|
158
|
+
db: Database,
|
|
159
|
+
workspaceId: string,
|
|
160
|
+
): Promise<CapabilityPack[]> {
|
|
143
161
|
const registered = await listWorkspacePacks(db, workspaceId);
|
|
144
162
|
const builtInIds = new Set(packs.map((pack) => pack.id));
|
|
145
163
|
const registeredPacks = registered
|
|
@@ -151,7 +169,11 @@ export async function listWorkspaceCapabilityPacks(db: Database, workspaceId: st
|
|
|
151
169
|
return [...packs, ...registeredPacks];
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
export async function resolveCapabilityPack(
|
|
172
|
+
export async function resolveCapabilityPack(
|
|
173
|
+
db: Database,
|
|
174
|
+
workspaceId: string,
|
|
175
|
+
packId: string,
|
|
176
|
+
): Promise<CapabilityPack | null> {
|
|
155
177
|
const builtIn = getCapabilityPack(packId);
|
|
156
178
|
if (builtIn) {
|
|
157
179
|
return builtIn;
|
|
@@ -171,7 +193,11 @@ export async function resolveCapabilityPack(db: Database, workspaceId: string, p
|
|
|
171
193
|
* generic capability enable path) and re-checked at session start by the
|
|
172
194
|
* worker, which also covers manifests re-registered after enablement.
|
|
173
195
|
*/
|
|
174
|
-
export async function assertPackSandboxImageCompatible(
|
|
196
|
+
export async function assertPackSandboxImageCompatible(
|
|
197
|
+
db: Database,
|
|
198
|
+
workspaceId: string,
|
|
199
|
+
pack: CapabilityPack,
|
|
200
|
+
): Promise<void> {
|
|
175
201
|
if (!pack.sandboxImage) {
|
|
176
202
|
return;
|
|
177
203
|
}
|
|
@@ -218,13 +244,18 @@ function marketingDailyAnalysisPrompt(input: {
|
|
|
218
244
|
documentBaseIds: string[];
|
|
219
245
|
promptInstructions?: string;
|
|
220
246
|
}): string {
|
|
221
|
-
const connectionLines = input.connections
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
const connectionLines = input.connections
|
|
248
|
+
.map((connection) => {
|
|
249
|
+
return `- ${connection.provider}: ${connection.accountHandle} (${connection.id})`;
|
|
250
|
+
})
|
|
251
|
+
.join("\n");
|
|
252
|
+
const knowledgeLine =
|
|
253
|
+
input.documentBaseIds.length > 0
|
|
254
|
+
? `Use these document base IDs for brand/campaign knowledge through the docs MCP: ${input.documentBaseIds.join(", ")}.`
|
|
255
|
+
: "No document base IDs were selected; rely only on social context returned by tools.";
|
|
256
|
+
const extra = input.promptInstructions
|
|
257
|
+
? `\nAdditional operator instructions:\n${input.promptInstructions.trim()}\n`
|
|
258
|
+
: "";
|
|
228
259
|
|
|
229
260
|
return [
|
|
230
261
|
"Run the daily social media analysis for the selected accounts.",
|
|
@@ -237,5 +268,7 @@ function marketingDailyAnalysisPrompt(input: {
|
|
|
237
268
|
extra,
|
|
238
269
|
"Produce a concise report with these sections: executive summary, notable account changes, winning posts, underperforming posts, audience and content signals, recommended actions for the next 24 hours, and data gaps.",
|
|
239
270
|
"Use only metrics and posts returned by tools or document search. Do not invent metrics, posts, or account capabilities.",
|
|
240
|
-
]
|
|
271
|
+
]
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.join("\n");
|
|
241
274
|
}
|
package/src/domain/resources.ts
CHANGED
|
@@ -8,11 +8,7 @@ import {
|
|
|
8
8
|
type ResourceRef,
|
|
9
9
|
type ToolRef,
|
|
10
10
|
} from "@opengeni/contracts";
|
|
11
|
-
import {
|
|
12
|
-
listGitHubInstallationIdsForWorkspace,
|
|
13
|
-
requireFile,
|
|
14
|
-
type Database,
|
|
15
|
-
} from "@opengeni/db";
|
|
11
|
+
import { listGitHubInstallationIdsForWorkspace, requireFile, type Database } from "@opengeni/db";
|
|
16
12
|
import { HTTPException } from "hono/http-exception";
|
|
17
13
|
|
|
18
14
|
export function validateToolRefs(tools: ToolRef[], settings: Settings): ToolRef[] {
|
|
@@ -20,7 +16,9 @@ export function validateToolRefs(tools: ToolRef[], settings: Settings): ToolRef[
|
|
|
20
16
|
const out: ToolRef[] = [];
|
|
21
17
|
for (const tool of tools) {
|
|
22
18
|
if (tool.kind !== "mcp") {
|
|
23
|
-
throw new HTTPException(422, {
|
|
19
|
+
throw new HTTPException(422, {
|
|
20
|
+
message: `unsupported tool kind: ${(tool as { kind?: string }).kind}`,
|
|
21
|
+
});
|
|
24
22
|
}
|
|
25
23
|
const optional = tool.optional === true;
|
|
26
24
|
if (!mcpServerIds.has(tool.id)) {
|
|
@@ -37,25 +35,36 @@ export function validateToolRefs(tools: ToolRef[], settings: Settings): ToolRef[
|
|
|
37
35
|
// - optional:true + unknown id is skipped above: the client explicitly
|
|
38
36
|
// opted into graceful degradation for MCPs (for example docs servers
|
|
39
37
|
// like context7) that only some deployments configure.
|
|
40
|
-
out.push(
|
|
38
|
+
out.push(
|
|
39
|
+
optional ? { kind: "mcp", id: tool.id, optional: true } : { kind: "mcp", id: tool.id },
|
|
40
|
+
);
|
|
41
41
|
}
|
|
42
42
|
return mergeToolRefs([], out);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
type McpSettings = Pick<Settings, "mcpServers">;
|
|
46
46
|
|
|
47
|
-
export function enabledCapabilityMcpToolRefs(
|
|
47
|
+
export function enabledCapabilityMcpToolRefs(
|
|
48
|
+
settings: McpSettings,
|
|
49
|
+
runtimeSettings: McpSettings,
|
|
50
|
+
): ToolRef[] {
|
|
48
51
|
const configuredIds = new Set(settings.mcpServers.map((server) => server.id));
|
|
49
|
-
return
|
|
50
|
-
.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
return (
|
|
53
|
+
runtimeSettings.mcpServers
|
|
54
|
+
.filter((server) => !configuredIds.has(server.id))
|
|
55
|
+
// AUTO-ATTACHED (workspace-default) capability servers are marked optional:
|
|
56
|
+
// one of them having a broken/expired credential must SKIP that server, not
|
|
57
|
+
// fail the whole turn before the model runs. The caller only reaches here
|
|
58
|
+
// when the request omitted `tools`; an explicit list is never defaulted.
|
|
59
|
+
.map((server) => ({ kind: "mcp", id: server.id, optional: true }))
|
|
60
|
+
);
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
export function withDefaultEnabledCapabilityMcpTools(
|
|
63
|
+
export function withDefaultEnabledCapabilityMcpTools(
|
|
64
|
+
tools: ToolRef[],
|
|
65
|
+
settings: McpSettings,
|
|
66
|
+
runtimeSettings: McpSettings,
|
|
67
|
+
): ToolRef[] {
|
|
59
68
|
return mergeToolRefs(tools, enabledCapabilityMcpToolRefs(settings, runtimeSettings));
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -91,14 +100,25 @@ export function normalizeResources(resources: ResourceRef[]): ResourceRef[] {
|
|
|
91
100
|
ref: resource.ref.trim(),
|
|
92
101
|
mountPath,
|
|
93
102
|
...(resource.subpath ? { subpath: normalizeMountPath(resource.subpath) } : {}),
|
|
94
|
-
...(resource.
|
|
103
|
+
...(resource.provider ? { provider: resource.provider } : {}),
|
|
104
|
+
...(resource.repositoryId !== undefined ? { repositoryId: resource.repositoryId } : {}),
|
|
105
|
+
...(resource.installationId !== undefined
|
|
106
|
+
? { installationId: resource.installationId }
|
|
107
|
+
: {}),
|
|
108
|
+
...(resource.projectId !== undefined ? { projectId: resource.projectId } : {}),
|
|
109
|
+
...(resource.connectionId ? { connectionId: resource.connectionId } : {}),
|
|
110
|
+
...(resource.githubInstallationId
|
|
111
|
+
? { githubInstallationId: resource.githubInstallationId }
|
|
112
|
+
: {}),
|
|
95
113
|
...(resource.githubRepositoryId ? { githubRepositoryId: resource.githubRepositoryId } : {}),
|
|
96
114
|
};
|
|
97
115
|
}
|
|
98
116
|
const key = stableJson(normalized);
|
|
99
117
|
const mounted = normalized.mountPath ? mountPaths.get(normalized.mountPath) : undefined;
|
|
100
118
|
if (mounted && mounted !== key) {
|
|
101
|
-
throw new HTTPException(422, {
|
|
119
|
+
throw new HTTPException(422, {
|
|
120
|
+
message: `duplicate resource mount path: ${normalized.mountPath}`,
|
|
121
|
+
});
|
|
102
122
|
}
|
|
103
123
|
if (normalized.mountPath) {
|
|
104
124
|
mountPaths.set(normalized.mountPath, key);
|
|
@@ -106,7 +126,9 @@ export function normalizeResources(resources: ResourceRef[]): ResourceRef[] {
|
|
|
106
126
|
const identity = resourceIdentityKey(normalized);
|
|
107
127
|
const seenIdentity = identities.get(identity);
|
|
108
128
|
if (seenIdentity && seenIdentity !== key) {
|
|
109
|
-
throw new HTTPException(422, {
|
|
129
|
+
throw new HTTPException(422, {
|
|
130
|
+
message: `duplicate resource with different settings: ${identity}`,
|
|
131
|
+
});
|
|
110
132
|
}
|
|
111
133
|
identities.set(identity, key);
|
|
112
134
|
if (!seenResources.has(key)) {
|
|
@@ -117,7 +139,10 @@ export function normalizeResources(resources: ResourceRef[]): ResourceRef[] {
|
|
|
117
139
|
return out;
|
|
118
140
|
}
|
|
119
141
|
|
|
120
|
-
export function mergeResourceRefs(
|
|
142
|
+
export function mergeResourceRefs(
|
|
143
|
+
existing: ResourceRef[],
|
|
144
|
+
additions: ResourceRef[],
|
|
145
|
+
): ResourceRef[] {
|
|
121
146
|
try {
|
|
122
147
|
return mergeContractResourceRefs(existing, additions, { rejectConflicts: true });
|
|
123
148
|
} catch (error) {
|
|
@@ -133,8 +158,12 @@ export function validateGitHubRepositorySelectionShape(resources: ResourceRef[])
|
|
|
133
158
|
if (resource.kind !== "repository") {
|
|
134
159
|
return [];
|
|
135
160
|
}
|
|
136
|
-
const installationRaw =
|
|
137
|
-
|
|
161
|
+
const installationRaw =
|
|
162
|
+
resource.githubInstallationId ??
|
|
163
|
+
(resource.provider === "github" ? resource.installationId : undefined);
|
|
164
|
+
const repositoryRaw =
|
|
165
|
+
resource.githubRepositoryId ??
|
|
166
|
+
(resource.provider === "github" ? resource.repositoryId : undefined);
|
|
138
167
|
if (installationRaw === null && repositoryRaw === null) {
|
|
139
168
|
return [];
|
|
140
169
|
}
|
|
@@ -145,7 +174,8 @@ export function validateGitHubRepositorySelectionShape(resources: ResourceRef[])
|
|
|
145
174
|
const repositoryId = positiveInteger(repositoryRaw);
|
|
146
175
|
if (!installationId || !repositoryId) {
|
|
147
176
|
throw new HTTPException(422, {
|
|
148
|
-
message:
|
|
177
|
+
message:
|
|
178
|
+
"GitHub App repository resources require positive github_installation_id and github_repository_id",
|
|
149
179
|
});
|
|
150
180
|
}
|
|
151
181
|
return [{ installationId, repositoryId }];
|
|
@@ -162,20 +192,31 @@ export function validateGitHubRepositorySelectionShape(resources: ResourceRef[])
|
|
|
162
192
|
return installationId;
|
|
163
193
|
}
|
|
164
194
|
|
|
165
|
-
export async function validateGitHubRepositorySelection(
|
|
195
|
+
export async function validateGitHubRepositorySelection(
|
|
196
|
+
db: Database,
|
|
197
|
+
workspaceId: string,
|
|
198
|
+
resources: ResourceRef[],
|
|
199
|
+
): Promise<void> {
|
|
166
200
|
const installationId = validateGitHubRepositorySelectionShape(resources);
|
|
167
201
|
if (installationId === null) {
|
|
168
202
|
return;
|
|
169
203
|
}
|
|
170
|
-
const linkedInstallationIds = new Set(
|
|
204
|
+
const linkedInstallationIds = new Set(
|
|
205
|
+
await listGitHubInstallationIdsForWorkspace(db, workspaceId),
|
|
206
|
+
);
|
|
171
207
|
if (!linkedInstallationIds.has(installationId)) {
|
|
172
208
|
throw new HTTPException(422, {
|
|
173
|
-
message:
|
|
209
|
+
message:
|
|
210
|
+
"GitHub App repository resources must belong to a GitHub App installation linked to this workspace",
|
|
174
211
|
});
|
|
175
212
|
}
|
|
176
213
|
}
|
|
177
214
|
|
|
178
|
-
export async function validateFileResources(
|
|
215
|
+
export async function validateFileResources(
|
|
216
|
+
db: Database,
|
|
217
|
+
workspaceId: string,
|
|
218
|
+
resources: ResourceRef[],
|
|
219
|
+
): Promise<void> {
|
|
179
220
|
const fileIds = new Set<string>();
|
|
180
221
|
for (const resource of resources) {
|
|
181
222
|
if (resource.kind !== "file") {
|
|
@@ -190,7 +231,9 @@ export async function validateFileResources(db: Database, workspaceId: string, r
|
|
|
190
231
|
throw new HTTPException(422, { message: `unknown file resource: ${resource.fileId}` });
|
|
191
232
|
}
|
|
192
233
|
if (file.status !== "ready") {
|
|
193
|
-
throw new HTTPException(422, {
|
|
234
|
+
throw new HTTPException(422, {
|
|
235
|
+
message: `file resource ${resource.fileId} is ${file.status}`,
|
|
236
|
+
});
|
|
194
237
|
}
|
|
195
238
|
}
|
|
196
239
|
}
|