@opengeni/core 0.28.0 → 1.0.0
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/README.md +1 -1
- package/dist/access/external-actor-authority.d.ts +47 -0
- package/dist/access/index.d.ts +182 -3
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +24 -0
- package/dist/application/connect-authority.d.ts +13 -0
- package/dist/application/connect-operation.d.ts +31 -0
- package/dist/application/external-continuation.d.ts +15 -0
- package/dist/application/external-identity-lifecycle.d.ts +20 -0
- package/dist/application/external-link-work-admission.d.ts +12 -0
- package/dist/application/external-workspace-members.d.ts +34 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +39 -2
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +26 -13
- package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
- package/dist/application/session-tenancy.d.ts +19 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/billing/limits.d.ts +11 -0
- package/dist/canonical-human-identities.d.ts +7 -2
- package/dist/canonical-human-identities.js +17 -4
- package/dist/canonical-human-identities.js.map +1 -1
- package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
- package/dist/chunk-2MKMRGSY.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
- package/dist/chunk-GVO3N37G.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
- package/dist/chunk-LCQXNR62.js.map +1 -0
- package/dist/chunk-MM6N7CTO.js +387 -0
- package/dist/chunk-MM6N7CTO.js.map +1 -0
- package/dist/chunk-QO5GVFFO.js +235 -0
- package/dist/chunk-QO5GVFFO.js.map +1 -0
- package/dist/codex-model-availability.d.ts +14 -0
- package/dist/default-session-model.d.ts +139 -0
- package/dist/dependencies.d.ts +87 -4
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +58 -0
- package/dist/domain/capabilities.d.ts +22 -4
- package/dist/domain/company-profile-agent-admin.d.ts +25 -0
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +36 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +4 -3
- package/dist/domain/environments.d.ts +5 -9
- package/dist/domain/external-creation-attribution.d.ts +6 -0
- package/dist/domain/file-owner.d.ts +8 -0
- package/dist/domain/github-action-policies.d.ts +36 -0
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/github-skill-source.d.ts +5 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +28 -1
- package/dist/domain/knowledge-files.d.ts +20 -0
- package/dist/domain/knowledge-messages.d.ts +39 -0
- package/dist/domain/knowledge-messages.test.d.ts +1 -0
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -0
- package/dist/domain/knowledge-search.test.d.ts +1 -0
- package/dist/domain/knowledge.d.ts +9 -0
- package/dist/domain/mcp-account-bindings.d.ts +19 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/personal-connection-delegations.d.ts +87 -4
- package/dist/domain/pr-review.d.ts +249 -0
- package/dist/domain/resources.d.ts +24 -3
- package/dist/domain/scheduled-task-access.d.ts +247 -0
- package/dist/domain/scheduled-tasks.d.ts +103 -6
- package/dist/domain/session-connection-accounts.d.ts +16 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +93 -4
- package/dist/domain/sessions.d.ts +275 -28
- package/dist/domain/skill-imports.d.ts +1 -0
- package/dist/domain/skill-search.d.ts +30 -0
- package/dist/domain/skills.d.ts +117 -0
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/editable-artifact-live/application.d.ts +12 -3
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +5 -2
- package/dist/editable-artifact-live/ticket.d.ts +2 -2
- package/dist/editable-artifact-live/types.d.ts +14 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -4
- package/dist/index.d.ts +49 -3
- package/dist/index.js +18826 -6847
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +131 -0
- package/dist/managed-auth-session-sets.js +51 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/managed-session.d.ts +98 -11
- package/dist/model-catalog.d.ts +117 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +22 -5
- package/dist/rigs/provider-images.d.ts +1 -1
- package/dist/sandbox/fleet.d.ts +59 -10
- package/dist/sandbox/routing.d.ts +22 -2
- package/dist/sandbox/runtime-settings.d.ts +56 -0
- package/dist/session-authorization.d.ts +38 -1
- package/dist/site-session-origin.d.ts +9 -0
- package/dist/transcription.d.ts +12 -1
- package/dist/turn-surface.d.ts +18 -0
- package/dist/user-presence.d.ts +32 -0
- package/package.json +18 -11
- package/src/access/external-actor-authority.ts +94 -0
- package/src/access/index.ts +1386 -44
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +77 -0
- package/src/application/connect-authority.ts +77 -0
- package/src/application/connect-operation.ts +80 -0
- package/src/application/external-continuation.ts +129 -0
- package/src/application/external-identity-lifecycle.ts +48 -0
- package/src/application/external-link-work-admission.ts +90 -0
- package/src/application/external-workspace-members.ts +232 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +222 -29
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +430 -73
- package/src/application/session-mcp-credential-rotation.ts +217 -0
- package/src/application/session-tenancy.ts +445 -0
- package/src/application/user-resource-grants.ts +154 -0
- package/src/billing/limits.ts +103 -24
- package/src/canonical-human-identities.ts +22 -3
- package/src/codex-model-availability.ts +136 -0
- package/src/default-session-model.ts +656 -0
- package/src/dependencies.ts +97 -5
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +270 -0
- package/src/domain/capabilities.ts +244 -151
- package/src/domain/company-profile-agent-admin.ts +56 -0
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +371 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +61 -15
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +32 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +25 -45
- package/src/domain/external-creation-attribution.ts +22 -0
- package/src/domain/file-owner.ts +67 -0
- package/src/domain/github-action-policies.ts +169 -0
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/github-skill-source.ts +233 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +270 -169
- package/src/domain/knowledge-files.ts +46 -0
- package/src/domain/knowledge-messages.test.ts +91 -0
- package/src/domain/knowledge-messages.ts +106 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +561 -0
- package/src/domain/knowledge-search.ts +256 -0
- package/src/domain/knowledge.ts +133 -0
- package/src/domain/mcp-account-bindings.ts +171 -0
- package/src/domain/mcp-account-routes.ts +121 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/organization-membership-lifecycle.ts +10 -0
- package/src/domain/personal-connection-delegations.ts +758 -115
- package/src/domain/pr-review.ts +687 -0
- package/src/domain/resources.ts +182 -25
- package/src/domain/scheduled-task-access.ts +1052 -0
- package/src/domain/scheduled-tasks.ts +1952 -215
- package/src/domain/session-connection-accounts.ts +67 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +55 -0
- package/src/domain/session-tool-policy.ts +443 -19
- package/src/domain/sessions.ts +3417 -619
- package/src/domain/skill-imports.ts +137 -17
- package/src/domain/skill-search.ts +221 -0
- package/src/domain/skills.ts +104 -0
- package/src/domain/slack-bot.ts +128 -3
- package/src/domain/workspace-members.ts +34 -2
- package/src/editable-artifact-live/application.ts +29 -4
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +167 -13
- package/src/editable-artifact-live/ticket.ts +87 -6
- package/src/editable-artifact-live/types.ts +16 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +69 -4
- package/src/managed-auth-session-sets.ts +371 -0
- package/src/managed-session.ts +520 -2
- package/src/model-catalog.ts +873 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +179 -63
- package/src/rigs/provider-images.ts +2 -3
- package/src/sandbox/fleet.ts +471 -59
- package/src/sandbox/routing.ts +160 -33
- package/src/sandbox/runtime-settings.ts +281 -0
- package/src/session-authorization.ts +262 -78
- package/src/site-session-origin.ts +22 -0
- package/src/transcription.ts +13 -1
- package/src/turn-surface.ts +45 -0
- package/src/user-presence.ts +134 -0
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-IBOEYG6N.js +0 -34
- package/dist/chunk-IBOEYG6N.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/dist/domain/packs.d.ts +0 -55
- package/src/domain/durable-learning-slack-publication.ts +0 -202
- package/src/domain/packs.ts +0 -556
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GitHubActionPolicyActor,
|
|
3
|
+
GitHubActionPolicyActorState,
|
|
4
|
+
GitHubActionPolicyDecision,
|
|
5
|
+
GitHubActionPolicyEffectiveDecision,
|
|
6
|
+
GitHubActionPolicyGroup,
|
|
7
|
+
} from "@opengeni/contracts";
|
|
8
|
+
import {
|
|
9
|
+
listConnectorActionPolicies,
|
|
10
|
+
resolveConnectorActionPolicy,
|
|
11
|
+
upsertConnectorActionPolicies,
|
|
12
|
+
type ConnectorActionPolicySnapshotEntry,
|
|
13
|
+
type Database,
|
|
14
|
+
} from "@opengeni/db";
|
|
15
|
+
import {
|
|
16
|
+
GITHUB_REST_MCP_APP_SERVER_ID,
|
|
17
|
+
GITHUB_REST_MCP_PERSONAL_SERVER_ID,
|
|
18
|
+
GITHUB_REST_WRITE_TOOL_NAMES,
|
|
19
|
+
} from "@opengeni/runtime/github-rest-mcp";
|
|
20
|
+
|
|
21
|
+
export const GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES = {
|
|
22
|
+
routine: GITHUB_REST_WRITE_TOOL_NAMES.filter(
|
|
23
|
+
(toolName) => toolName !== "pull_request_review_submit" && toolName !== "pull_request_merge",
|
|
24
|
+
),
|
|
25
|
+
review: ["pull_request_review_submit"],
|
|
26
|
+
merge: ["pull_request_merge"],
|
|
27
|
+
} as const satisfies Record<GitHubActionPolicyGroup, readonly string[]>;
|
|
28
|
+
|
|
29
|
+
const groupedToolNames = Object.values(GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES).flat();
|
|
30
|
+
if (
|
|
31
|
+
groupedToolNames.length !== new Set(groupedToolNames).size ||
|
|
32
|
+
GITHUB_REST_WRITE_TOOL_NAMES.some((toolName) => !groupedToolNames.includes(toolName))
|
|
33
|
+
) {
|
|
34
|
+
throw new Error("GitHub action policy groups must cover every write tool exactly once");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type GitHubActionPolicyActorBinding = {
|
|
38
|
+
actor: GitHubActionPolicyActor;
|
|
39
|
+
label: string;
|
|
40
|
+
connectionId: string;
|
|
41
|
+
serverId: typeof GITHUB_REST_MCP_APP_SERVER_ID | typeof GITHUB_REST_MCP_PERSONAL_SERVER_ID;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function effectiveToolDecision(
|
|
45
|
+
policies: readonly ConnectorActionPolicySnapshotEntry[],
|
|
46
|
+
actor: GitHubActionPolicyActorBinding,
|
|
47
|
+
toolName: string,
|
|
48
|
+
): GitHubActionPolicyDecision {
|
|
49
|
+
const resolved = resolveConnectorActionPolicy(policies, {
|
|
50
|
+
connectionId: actor.connectionId,
|
|
51
|
+
serverId: actor.serverId,
|
|
52
|
+
toolName,
|
|
53
|
+
actionName: toolName,
|
|
54
|
+
});
|
|
55
|
+
// Match connector_write admission: absent policy inherits repository capability.
|
|
56
|
+
// This is a policy projection, not a grant of repository or provider access.
|
|
57
|
+
if (!resolved.managed) return "allow";
|
|
58
|
+
if (resolved.entry) return resolved.entry.policy;
|
|
59
|
+
return resolved.decision;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function effectiveGroupDecision(
|
|
63
|
+
policies: readonly ConnectorActionPolicySnapshotEntry[],
|
|
64
|
+
actor: GitHubActionPolicyActorBinding,
|
|
65
|
+
group: GitHubActionPolicyGroup,
|
|
66
|
+
): GitHubActionPolicyEffectiveDecision {
|
|
67
|
+
const decisions = new Set(
|
|
68
|
+
GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES[group].map((toolName) =>
|
|
69
|
+
effectiveToolDecision(policies, actor, toolName),
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
return decisions.size === 1 ? decisions.values().next().value! : "mixed";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function projectGitHubActionPolicyActor(
|
|
76
|
+
policies: readonly ConnectorActionPolicySnapshotEntry[],
|
|
77
|
+
binding: GitHubActionPolicyActorBinding,
|
|
78
|
+
): GitHubActionPolicyActorState {
|
|
79
|
+
const groups = {
|
|
80
|
+
routine: effectiveGroupDecision(policies, binding, "routine"),
|
|
81
|
+
review: effectiveGroupDecision(policies, binding, "review"),
|
|
82
|
+
merge: effectiveGroupDecision(policies, binding, "merge"),
|
|
83
|
+
};
|
|
84
|
+
return binding.actor.kind === "workspace_app"
|
|
85
|
+
? {
|
|
86
|
+
kind: "workspace_app",
|
|
87
|
+
installationId: binding.actor.installationId,
|
|
88
|
+
label: binding.label,
|
|
89
|
+
groups,
|
|
90
|
+
}
|
|
91
|
+
: {
|
|
92
|
+
kind: "personal",
|
|
93
|
+
connectionId: binding.actor.connectionId,
|
|
94
|
+
label: binding.label,
|
|
95
|
+
groups,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function listGitHubActionPolicyActors(
|
|
100
|
+
db: Database,
|
|
101
|
+
input: {
|
|
102
|
+
accountId: string;
|
|
103
|
+
workspaceId: string;
|
|
104
|
+
actors: readonly GitHubActionPolicyActorBinding[];
|
|
105
|
+
},
|
|
106
|
+
): Promise<GitHubActionPolicyActorState[]> {
|
|
107
|
+
const policies = await listConnectorActionPolicies(db, {
|
|
108
|
+
accountId: input.accountId,
|
|
109
|
+
workspaceId: input.workspaceId,
|
|
110
|
+
connectionIds: input.actors.map((actor) => actor.connectionId),
|
|
111
|
+
});
|
|
112
|
+
return input.actors.map((actor) => projectGitHubActionPolicyActor(policies, actor));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function updateGitHubActionPolicyGroup(
|
|
116
|
+
db: Database,
|
|
117
|
+
input: {
|
|
118
|
+
accountId: string;
|
|
119
|
+
workspaceId: string;
|
|
120
|
+
subjectId: string;
|
|
121
|
+
actor: GitHubActionPolicyActorBinding;
|
|
122
|
+
group: GitHubActionPolicyGroup;
|
|
123
|
+
decision: GitHubActionPolicyDecision;
|
|
124
|
+
},
|
|
125
|
+
): Promise<GitHubActionPolicyActorState> {
|
|
126
|
+
await upsertConnectorActionPolicies(
|
|
127
|
+
db,
|
|
128
|
+
GITHUB_ACTION_POLICY_GROUP_TOOL_NAMES[input.group].map((toolName) => ({
|
|
129
|
+
accountId: input.accountId,
|
|
130
|
+
workspaceId: input.workspaceId,
|
|
131
|
+
subjectId: input.subjectId,
|
|
132
|
+
connectionId: input.actor.connectionId,
|
|
133
|
+
serverId: input.actor.serverId,
|
|
134
|
+
toolName,
|
|
135
|
+
actionName: toolName,
|
|
136
|
+
policy: input.decision,
|
|
137
|
+
})),
|
|
138
|
+
);
|
|
139
|
+
const policies = await listConnectorActionPolicies(db, {
|
|
140
|
+
accountId: input.accountId,
|
|
141
|
+
workspaceId: input.workspaceId,
|
|
142
|
+
connectionIds: [input.actor.connectionId],
|
|
143
|
+
});
|
|
144
|
+
return projectGitHubActionPolicyActor(policies, input.actor);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function githubAppActionPolicyActor(input: {
|
|
148
|
+
installationId: number;
|
|
149
|
+
accountLogin: string | null;
|
|
150
|
+
}): GitHubActionPolicyActorBinding {
|
|
151
|
+
return {
|
|
152
|
+
actor: { kind: "workspace_app", installationId: input.installationId },
|
|
153
|
+
label: input.accountLogin ? `OpenGeni bot on ${input.accountLogin}` : "OpenGeni bot",
|
|
154
|
+
connectionId: `github-app:${input.installationId}`,
|
|
155
|
+
serverId: GITHUB_REST_MCP_APP_SERVER_ID,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function personalGitHubActionPolicyActor(input: {
|
|
160
|
+
connectionId: string;
|
|
161
|
+
githubLogin: string;
|
|
162
|
+
}): GitHubActionPolicyActorBinding {
|
|
163
|
+
return {
|
|
164
|
+
actor: { kind: "personal", connectionId: input.connectionId },
|
|
165
|
+
label: `@${input.githubLogin}`,
|
|
166
|
+
connectionId: input.connectionId,
|
|
167
|
+
serverId: GITHUB_REST_MCP_PERSONAL_SERVER_ID,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { RepositoryResourceRef, ResourceRef } from "@opengeni/contracts";
|
|
2
|
+
import {
|
|
3
|
+
hasAuditableGitHubInstallationAuthority,
|
|
4
|
+
listGitHubInstallationAccessForWorkspace,
|
|
5
|
+
type Database,
|
|
6
|
+
} from "@opengeni/db";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GitHub App repository-binding resolution for bare repository resources.
|
|
10
|
+
*
|
|
11
|
+
* A repository resource that names a GitHub repository by URI alone (no
|
|
12
|
+
* `githubInstallationId`/`githubRepositoryId`) never mints an installation
|
|
13
|
+
* token, so the sandbox clones it anonymously and cannot push or use `gh`.
|
|
14
|
+
* The workspace allowlist (`github_installation_repositories`) stores only
|
|
15
|
+
* numeric repository ids, so resolving a URI needs the installation's owner
|
|
16
|
+
* login from the durable binding plus one provider read for the repository id.
|
|
17
|
+
* This module owns the pure parts: URI parsing, candidate selection under the
|
|
18
|
+
* workspace's auditable bindings, and the exactly-one-match stamping rule. The
|
|
19
|
+
* provider read is injected so the worker can supply the live GitHub lookup
|
|
20
|
+
* while tests stay deterministic.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type GitHubRepositoryCoordinates = { owner: string; name: string };
|
|
24
|
+
|
|
25
|
+
const GITHUB_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/u;
|
|
26
|
+
const GITHUB_NAME_PATTERN = /^[A-Za-z0-9._-]+$/u;
|
|
27
|
+
const GITHUB_HOSTS = new Set(["github.com", "www.github.com"]);
|
|
28
|
+
const GITHUB_SCP_LIKE_PATTERN = /^git@(?:www\.)?github\.com:(.+)$/iu;
|
|
29
|
+
|
|
30
|
+
function coordinatesFromPath(path: string): GitHubRepositoryCoordinates | null {
|
|
31
|
+
const segments = path.split("/").filter(Boolean);
|
|
32
|
+
if (segments.length !== 2) return null;
|
|
33
|
+
const owner = segments[0]!;
|
|
34
|
+
const name = segments[1]!.replace(/\.git$/u, "");
|
|
35
|
+
if (!GITHUB_OWNER_PATTERN.test(owner) || !name || !GITHUB_NAME_PATTERN.test(name)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return { owner, name };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a GitHub repository reference into owner/name, else null. Accepts
|
|
43
|
+
* `https://github.com/<owner>/<repo>[.git][/]`, `www.github.com`,
|
|
44
|
+
* `ssh://git@github.com/<owner>/<repo>[.git]`, and the scp-like
|
|
45
|
+
* `git@github.com:<owner>/<repo>[.git]`.
|
|
46
|
+
*/
|
|
47
|
+
export function parseGitHubRepositoryCoordinates(uri: string): GitHubRepositoryCoordinates | null {
|
|
48
|
+
const trimmed = uri.trim();
|
|
49
|
+
const scpLike = GITHUB_SCP_LIKE_PATTERN.exec(trimmed);
|
|
50
|
+
if (scpLike) {
|
|
51
|
+
return coordinatesFromPath(scpLike[1]!);
|
|
52
|
+
}
|
|
53
|
+
let url: URL;
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(trimmed);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (!GITHUB_HOSTS.has(url.hostname.toLowerCase()) || url.search || url.hash) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (url.protocol === "https:" || url.protocol === "http:") {
|
|
63
|
+
if (url.username || url.password) return null;
|
|
64
|
+
} else if (url.protocol === "ssh:") {
|
|
65
|
+
if (url.password || (url.username && url.username !== "git")) return null;
|
|
66
|
+
} else {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return coordinatesFromPath(url.pathname);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type UnboundGitHubRepositoryResource = {
|
|
73
|
+
resource: RepositoryResourceRef;
|
|
74
|
+
owner: string;
|
|
75
|
+
name: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Repository resources that name a github.com repository without any
|
|
80
|
+
* platform identity: no GitHub App ids, no credential binding, no personal
|
|
81
|
+
* connection, and either the explicit `github` provider or no provider at all.
|
|
82
|
+
* Resources that already carry ids, legacy provider ids, a binding, or a
|
|
83
|
+
* connection are someone else's authority and are never rewritten here.
|
|
84
|
+
*/
|
|
85
|
+
export function unboundGitHubRepositoryResources(
|
|
86
|
+
resources: readonly ResourceRef[],
|
|
87
|
+
): UnboundGitHubRepositoryResource[] {
|
|
88
|
+
const out: UnboundGitHubRepositoryResource[] = [];
|
|
89
|
+
for (const resource of resources) {
|
|
90
|
+
if (resource.kind !== "repository") continue;
|
|
91
|
+
if (resource.provider !== undefined && resource.provider !== "github") continue;
|
|
92
|
+
if (
|
|
93
|
+
resource.githubInstallationId !== undefined ||
|
|
94
|
+
resource.githubRepositoryId !== undefined ||
|
|
95
|
+
resource.installationId !== undefined ||
|
|
96
|
+
resource.repositoryId !== undefined ||
|
|
97
|
+
resource.credentialBindingId !== undefined ||
|
|
98
|
+
resource.connectionId !== undefined ||
|
|
99
|
+
resource.connectionType !== undefined
|
|
100
|
+
) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const coordinates = parseGitHubRepositoryCoordinates(resource.uri);
|
|
104
|
+
if (!coordinates) continue;
|
|
105
|
+
out.push({ resource, ...coordinates });
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type GitHubRepositoryBindingCandidate = {
|
|
111
|
+
installationId: number;
|
|
112
|
+
accountLogin: string;
|
|
113
|
+
repositoryIds: number[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The workspace's auditable GitHub App bindings, under workspace RLS. Only
|
|
118
|
+
* bindings that could authorize a token mint are candidates; legacy
|
|
119
|
+
* `unverified` rows never resolve a URI.
|
|
120
|
+
*/
|
|
121
|
+
export async function listGitHubRepositoryBindingCandidates(
|
|
122
|
+
db: Database,
|
|
123
|
+
workspaceId: string,
|
|
124
|
+
): Promise<GitHubRepositoryBindingCandidate[]> {
|
|
125
|
+
const installations = await listGitHubInstallationAccessForWorkspace(db, workspaceId);
|
|
126
|
+
return installations
|
|
127
|
+
.filter((installation) => hasAuditableGitHubInstallationAuthority(installation))
|
|
128
|
+
.map((installation) => ({
|
|
129
|
+
installationId: installation.installationId,
|
|
130
|
+
accountLogin: installation.accountLogin ?? "",
|
|
131
|
+
repositoryIds: [...installation.repositoryIds],
|
|
132
|
+
}))
|
|
133
|
+
.filter((candidate) => candidate.accountLogin.length > 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type GitHubRepositoryBindingLookup = (input: {
|
|
137
|
+
installationId: number;
|
|
138
|
+
owner: string;
|
|
139
|
+
name: string;
|
|
140
|
+
}) => Promise<{ id: number } | null>;
|
|
141
|
+
|
|
142
|
+
export type GitHubRepositoryBinding = { installationId: number; repositoryId: number };
|
|
143
|
+
|
|
144
|
+
export type GitHubRepositoryBindingOutcome =
|
|
145
|
+
| { status: "resolved"; binding: GitHubRepositoryBinding }
|
|
146
|
+
/** No workspace binding owns this repository's account: anonymous, as before. */
|
|
147
|
+
| { status: "unbound" }
|
|
148
|
+
/** The account is bound, but no installation's allowlist holds this repository. */
|
|
149
|
+
| { status: "not_allowlisted"; installationIds: number[] }
|
|
150
|
+
/** More than one bound installation's allowlist holds this repository. */
|
|
151
|
+
| { status: "ambiguous"; installationIds: number[] }
|
|
152
|
+
/** The provider lookup failed (suspended/deleted installation, outage). */
|
|
153
|
+
| { status: "unavailable"; installationIds: number[]; message: string };
|
|
154
|
+
|
|
155
|
+
export type GitHubRepositoryBindingResolution = {
|
|
156
|
+
uri: string;
|
|
157
|
+
owner: string;
|
|
158
|
+
name: string;
|
|
159
|
+
outcome: GitHubRepositoryBindingOutcome;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export type GitHubRepositoryBindingResolutionResult = {
|
|
163
|
+
resources: ResourceRef[];
|
|
164
|
+
bindings: Map<string, GitHubRepositoryBinding>;
|
|
165
|
+
resolutions: GitHubRepositoryBindingResolution[];
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve every bare github.com repository resource against the workspace's
|
|
170
|
+
* auditable bindings. A resource is stamped only when exactly one bound
|
|
171
|
+
* installation's allowlist contains the repository id GitHub reports for that
|
|
172
|
+
* URI through that installation. Every other case leaves the resource bare
|
|
173
|
+
* and reports why, so the caller can surface a warning without ever failing
|
|
174
|
+
* the turn or the session. Distinct URIs resolve concurrently, as do the
|
|
175
|
+
* candidate installations of one owner.
|
|
176
|
+
*/
|
|
177
|
+
export async function resolveGitHubRepositoryBindings(input: {
|
|
178
|
+
resources: readonly ResourceRef[];
|
|
179
|
+
candidates: readonly GitHubRepositoryBindingCandidate[];
|
|
180
|
+
lookup: GitHubRepositoryBindingLookup;
|
|
181
|
+
}): Promise<GitHubRepositoryBindingResolutionResult> {
|
|
182
|
+
const unbound = unboundGitHubRepositoryResources(input.resources);
|
|
183
|
+
const bindings = new Map<string, GitHubRepositoryBinding>();
|
|
184
|
+
if (unbound.length === 0) {
|
|
185
|
+
return { resources: [...input.resources], bindings, resolutions: [] };
|
|
186
|
+
}
|
|
187
|
+
const candidatesByLogin = new Map<string, GitHubRepositoryBindingCandidate[]>();
|
|
188
|
+
for (const candidate of input.candidates) {
|
|
189
|
+
const key = candidate.accountLogin.toLowerCase();
|
|
190
|
+
const list = candidatesByLogin.get(key) ?? [];
|
|
191
|
+
list.push(candidate);
|
|
192
|
+
candidatesByLogin.set(key, list);
|
|
193
|
+
}
|
|
194
|
+
const distinct = new Map<string, UnboundGitHubRepositoryResource>();
|
|
195
|
+
for (const entry of unbound) {
|
|
196
|
+
if (!distinct.has(entry.resource.uri)) distinct.set(entry.resource.uri, entry);
|
|
197
|
+
}
|
|
198
|
+
const resolutions = await Promise.all(
|
|
199
|
+
[...distinct.values()].map(async (entry) => ({
|
|
200
|
+
uri: entry.resource.uri,
|
|
201
|
+
owner: entry.owner,
|
|
202
|
+
name: entry.name,
|
|
203
|
+
outcome: await resolveOne(
|
|
204
|
+
entry,
|
|
205
|
+
candidatesByLogin.get(entry.owner.toLowerCase()) ?? [],
|
|
206
|
+
input.lookup,
|
|
207
|
+
),
|
|
208
|
+
})),
|
|
209
|
+
);
|
|
210
|
+
for (const resolution of resolutions) {
|
|
211
|
+
if (resolution.outcome.status === "resolved") {
|
|
212
|
+
bindings.set(resolution.uri, resolution.outcome.binding);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
resources: applyGitHubRepositoryBindings(input.resources, bindings),
|
|
217
|
+
bindings,
|
|
218
|
+
resolutions,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function resolveOne(
|
|
223
|
+
entry: UnboundGitHubRepositoryResource,
|
|
224
|
+
installations: readonly GitHubRepositoryBindingCandidate[],
|
|
225
|
+
lookup: GitHubRepositoryBindingLookup,
|
|
226
|
+
): Promise<GitHubRepositoryBindingOutcome> {
|
|
227
|
+
if (installations.length === 0) {
|
|
228
|
+
return { status: "unbound" };
|
|
229
|
+
}
|
|
230
|
+
const installationIds = installations.map((installation) => installation.installationId);
|
|
231
|
+
const results = await Promise.allSettled(
|
|
232
|
+
installations.map(async (installation) => {
|
|
233
|
+
const found = await lookup({
|
|
234
|
+
installationId: installation.installationId,
|
|
235
|
+
owner: entry.owner,
|
|
236
|
+
name: entry.name,
|
|
237
|
+
});
|
|
238
|
+
return found &&
|
|
239
|
+
Number.isSafeInteger(found.id) &&
|
|
240
|
+
found.id > 0 &&
|
|
241
|
+
installation.repositoryIds.includes(found.id)
|
|
242
|
+
? { installationId: installation.installationId, repositoryId: found.id }
|
|
243
|
+
: null;
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
const failure = results.find(
|
|
247
|
+
(result): result is PromiseRejectedResult => result.status === "rejected",
|
|
248
|
+
);
|
|
249
|
+
if (failure) {
|
|
250
|
+
return {
|
|
251
|
+
status: "unavailable",
|
|
252
|
+
installationIds,
|
|
253
|
+
message: failure.reason instanceof Error ? failure.reason.message : String(failure.reason),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const matches = results.flatMap((result) =>
|
|
257
|
+
result.status === "fulfilled" && result.value ? [result.value] : [],
|
|
258
|
+
);
|
|
259
|
+
if (matches.length === 1) {
|
|
260
|
+
return { status: "resolved", binding: matches[0]! };
|
|
261
|
+
}
|
|
262
|
+
if (matches.length === 0) {
|
|
263
|
+
return { status: "not_allowlisted", installationIds };
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
status: "ambiguous",
|
|
267
|
+
installationIds: matches.map((match) => match.installationId),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Stamp resolved GitHub App ids onto the matching bare resources of any
|
|
273
|
+
* resource list (turn resources and runtime resources must agree, because the
|
|
274
|
+
* credential binding id and token filename derive from the installation id).
|
|
275
|
+
*/
|
|
276
|
+
export function applyGitHubRepositoryBindings(
|
|
277
|
+
resources: readonly ResourceRef[],
|
|
278
|
+
bindings: ReadonlyMap<string, GitHubRepositoryBinding>,
|
|
279
|
+
): ResourceRef[] {
|
|
280
|
+
if (bindings.size === 0) return [...resources];
|
|
281
|
+
const stampable = new Set(
|
|
282
|
+
unboundGitHubRepositoryResources(resources).map((entry) => entry.resource),
|
|
283
|
+
);
|
|
284
|
+
return resources.map((resource) => {
|
|
285
|
+
if (resource.kind !== "repository" || !stampable.has(resource)) return resource;
|
|
286
|
+
const binding = bindings.get(resource.uri);
|
|
287
|
+
if (!binding) return resource;
|
|
288
|
+
return {
|
|
289
|
+
...resource,
|
|
290
|
+
provider: "github",
|
|
291
|
+
githubInstallationId: binding.installationId,
|
|
292
|
+
githubRepositoryId: binding.repositoryId,
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import { HTTPException } from "hono/http-exception";
|
|
3
|
+
import type { GitHubSkillSourceClient, GitHubSkillTreeEntry } from "./skill-imports";
|
|
4
|
+
import { pinnedFetch, readResponseJsonBounded, readResponseTextBounded } from "@opengeni/network";
|
|
5
|
+
import {
|
|
6
|
+
PORTABLE_SKILL_MAX_FILES,
|
|
7
|
+
PORTABLE_SKILL_MAX_FILE_BYTES,
|
|
8
|
+
PORTABLE_SKILL_MAX_TOTAL_BYTES,
|
|
9
|
+
type SkillLibraryFile,
|
|
10
|
+
} from "@opengeni/runtime/skill-library";
|
|
11
|
+
|
|
12
|
+
const githubApiBase = "https://api.github.com";
|
|
13
|
+
const githubRequestTimeoutMs = 15_000;
|
|
14
|
+
const githubMetadataMaxBytes = 4 * 1024 * 1024;
|
|
15
|
+
// GitHub blobs are base64-encoded JSON; allow framing and line wrapping.
|
|
16
|
+
const githubBlobResponseMaxBytes = PORTABLE_SKILL_MAX_FILE_BYTES * 2;
|
|
17
|
+
|
|
18
|
+
type GitHubJsonRequest = (path: string, maxBytes: number, label: string) => Promise<unknown>;
|
|
19
|
+
|
|
20
|
+
export function createGitHubSkillSourceClient(
|
|
21
|
+
settings: Settings,
|
|
22
|
+
requestJson: GitHubJsonRequest = (path, maxBytes, label) =>
|
|
23
|
+
githubJson(settings, path, maxBytes, label),
|
|
24
|
+
): GitHubSkillSourceClient {
|
|
25
|
+
const readJson = requestJson;
|
|
26
|
+
const cache = new Map<string, { expires: number; payload: unknown }>();
|
|
27
|
+
const pending = new Map<string, Promise<unknown>>();
|
|
28
|
+
requestJson = async (path, maxBytes, label) => {
|
|
29
|
+
const cached = cache.get(path);
|
|
30
|
+
if (cached && cached.expires > Date.now()) return cached.payload;
|
|
31
|
+
const existing = pending.get(path);
|
|
32
|
+
if (existing) return existing;
|
|
33
|
+
const task = readJson(path, maxBytes, label).then((payload) => {
|
|
34
|
+
if (cache.size >= 128) cache.delete(cache.keys().next().value!);
|
|
35
|
+
cache.set(path, {
|
|
36
|
+
payload,
|
|
37
|
+
expires: Date.now() + (path.includes("/commits/") ? 60_000 : 3_600_000),
|
|
38
|
+
});
|
|
39
|
+
return payload;
|
|
40
|
+
});
|
|
41
|
+
pending.set(path, task);
|
|
42
|
+
try {
|
|
43
|
+
return await task;
|
|
44
|
+
} finally {
|
|
45
|
+
pending.delete(path);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
downloadSnapshot: (owner, repository, slug) =>
|
|
50
|
+
downloadSkillSnapshot(settings, owner, repository, slug),
|
|
51
|
+
resolveCommit: async (owner, repository, ref) => {
|
|
52
|
+
const payload = recordValue(
|
|
53
|
+
await requestJson(
|
|
54
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/commits/${encodeURIComponent(ref)}`,
|
|
55
|
+
githubMetadataMaxBytes,
|
|
56
|
+
"GitHub Skill commit",
|
|
57
|
+
),
|
|
58
|
+
"GitHub commit",
|
|
59
|
+
);
|
|
60
|
+
const sha = stringValue(payload.sha);
|
|
61
|
+
if (!sha) throw new Error("GitHub commit response omitted sha");
|
|
62
|
+
return sha.toLowerCase();
|
|
63
|
+
},
|
|
64
|
+
listTree: async (owner, repository, commit) => {
|
|
65
|
+
const payload = recordValue(
|
|
66
|
+
await requestJson(
|
|
67
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/git/trees/${encodeURIComponent(commit)}?recursive=1`,
|
|
68
|
+
githubMetadataMaxBytes,
|
|
69
|
+
"GitHub Skill tree",
|
|
70
|
+
),
|
|
71
|
+
"GitHub tree",
|
|
72
|
+
);
|
|
73
|
+
if (payload.truncated === true) {
|
|
74
|
+
throw new Error("GitHub repository tree is too large to import safely");
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(payload.tree)) throw new Error("GitHub tree response omitted entries");
|
|
77
|
+
return payload.tree.map((entry, index): GitHubSkillTreeEntry => {
|
|
78
|
+
const record = recordValue(entry, `GitHub tree entry ${index}`);
|
|
79
|
+
const path = stringValue(record.path);
|
|
80
|
+
const type = stringValue(record.type);
|
|
81
|
+
const mode = stringValue(record.mode);
|
|
82
|
+
const sha = stringValue(record.sha);
|
|
83
|
+
const size = record.size;
|
|
84
|
+
if (
|
|
85
|
+
!path ||
|
|
86
|
+
(type !== "blob" && type !== "tree" && type !== "commit") ||
|
|
87
|
+
!mode ||
|
|
88
|
+
!sha ||
|
|
89
|
+
(size !== undefined &&
|
|
90
|
+
size !== null &&
|
|
91
|
+
(typeof size !== "number" || !Number.isSafeInteger(size) || size < 0))
|
|
92
|
+
) {
|
|
93
|
+
throw new Error(`GitHub tree entry ${index} is invalid`);
|
|
94
|
+
}
|
|
95
|
+
return { path, type, mode, sha, size: typeof size === "number" ? size : null };
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
readBlob: async (owner, repository, sha) => {
|
|
99
|
+
const payload = recordValue(
|
|
100
|
+
await requestJson(
|
|
101
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/git/blobs/${encodeURIComponent(sha)}`,
|
|
102
|
+
githubBlobResponseMaxBytes,
|
|
103
|
+
"GitHub Skill file",
|
|
104
|
+
),
|
|
105
|
+
"GitHub blob",
|
|
106
|
+
);
|
|
107
|
+
if (payload.encoding !== "base64" || typeof payload.content !== "string") {
|
|
108
|
+
throw new Error("GitHub Skill file did not use base64 encoding");
|
|
109
|
+
}
|
|
110
|
+
const normalized = payload.content.replace(/\s+/gu, "");
|
|
111
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(normalized)) {
|
|
112
|
+
throw new Error("GitHub Skill file contained invalid base64");
|
|
113
|
+
}
|
|
114
|
+
const bytes = Uint8Array.from(Buffer.from(normalized, "base64"));
|
|
115
|
+
if (
|
|
116
|
+
typeof payload.size === "number" &&
|
|
117
|
+
Number.isSafeInteger(payload.size) &&
|
|
118
|
+
payload.size !== bytes.byteLength
|
|
119
|
+
) {
|
|
120
|
+
throw new Error("GitHub Skill file size did not match its payload");
|
|
121
|
+
}
|
|
122
|
+
return bytes;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function downloadSkillSnapshot(
|
|
128
|
+
settings: Settings,
|
|
129
|
+
owner: string,
|
|
130
|
+
repository: string,
|
|
131
|
+
slug: string,
|
|
132
|
+
): Promise<readonly SkillLibraryFile[]> {
|
|
133
|
+
const signal = AbortSignal.timeout(githubRequestTimeoutMs);
|
|
134
|
+
const response = await pinnedFetch(
|
|
135
|
+
`https://skills.sh/api/download/${[owner, repository, slug].map(encodeURIComponent).join("/")}`,
|
|
136
|
+
{ headers: { accept: "application/json" }, credentials: "omit", redirect: "manual", signal },
|
|
137
|
+
settings,
|
|
138
|
+
{ label: "Skill download", requireHttpsOutsideLocalTest: true },
|
|
139
|
+
);
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
await response.body?.cancel();
|
|
142
|
+
throw new HTTPException(response.status === 429 ? 429 : 422, {
|
|
143
|
+
message:
|
|
144
|
+
response.status === 429
|
|
145
|
+
? "skills.sh is busy. Try again shortly."
|
|
146
|
+
: response.status === 404
|
|
147
|
+
? "This skill has no downloadable snapshot. Import its GitHub folder URL instead."
|
|
148
|
+
: "Could not download this skill from skills.sh. Try again shortly.",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// JSON escaping may expand the text payload. Artifact validation below applies
|
|
152
|
+
// the stricter limits to decoded files, paths, and total content size.
|
|
153
|
+
const payload = recordValue(
|
|
154
|
+
await readResponseJsonBounded(
|
|
155
|
+
response,
|
|
156
|
+
PORTABLE_SKILL_MAX_TOTAL_BYTES * 6 + 128_000,
|
|
157
|
+
"Skill download",
|
|
158
|
+
{ signal },
|
|
159
|
+
),
|
|
160
|
+
"Skill download",
|
|
161
|
+
);
|
|
162
|
+
if (
|
|
163
|
+
!Array.isArray(payload.files) ||
|
|
164
|
+
payload.files.length === 0 ||
|
|
165
|
+
payload.files.length > PORTABLE_SKILL_MAX_FILES
|
|
166
|
+
) {
|
|
167
|
+
throw new HTTPException(422, { message: "The downloaded skill has an invalid file list" });
|
|
168
|
+
}
|
|
169
|
+
return payload.files.map((file) => {
|
|
170
|
+
if (
|
|
171
|
+
!file ||
|
|
172
|
+
typeof file !== "object" ||
|
|
173
|
+
typeof file.path !== "string" ||
|
|
174
|
+
typeof file.contents !== "string"
|
|
175
|
+
) {
|
|
176
|
+
throw new HTTPException(422, { message: "The downloaded skill contains an invalid file" });
|
|
177
|
+
}
|
|
178
|
+
return { path: file.path, content: file.contents };
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function githubJson(
|
|
183
|
+
settings: Settings,
|
|
184
|
+
path: string,
|
|
185
|
+
maxBytes: number,
|
|
186
|
+
label: string,
|
|
187
|
+
): Promise<unknown> {
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
const timeout = setTimeout(() => controller.abort(), githubRequestTimeoutMs);
|
|
190
|
+
try {
|
|
191
|
+
const response = await pinnedFetch(
|
|
192
|
+
`${githubApiBase}${path}`,
|
|
193
|
+
{
|
|
194
|
+
method: "GET",
|
|
195
|
+
headers: {
|
|
196
|
+
accept: "application/vnd.github+json",
|
|
197
|
+
"user-agent": "OpenGeni-Capabilities",
|
|
198
|
+
"x-github-api-version": "2022-11-28",
|
|
199
|
+
},
|
|
200
|
+
signal: controller.signal,
|
|
201
|
+
},
|
|
202
|
+
settings,
|
|
203
|
+
{ label, requireHttpsOutsideLocalTest: true },
|
|
204
|
+
);
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
await readResponseTextBounded(response, 8_192, `${label} error`).catch(() => undefined);
|
|
207
|
+
if (response.status === 404) throw new Error(`${label} was not found or is not public`);
|
|
208
|
+
if (response.status === 403 || response.status === 429) {
|
|
209
|
+
throw new HTTPException(429, {
|
|
210
|
+
message:
|
|
211
|
+
"GitHub's public request limit has been reached. Skill search is still available; try previewing again after the limit resets.",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`${label} failed with HTTP ${response.status}`);
|
|
215
|
+
}
|
|
216
|
+
return await readResponseJsonBounded(response, maxBytes, label, {
|
|
217
|
+
signal: controller.signal,
|
|
218
|
+
});
|
|
219
|
+
} finally {
|
|
220
|
+
clearTimeout(timeout);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function recordValue(value: unknown, label: string): Record<string, unknown> {
|
|
225
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
226
|
+
throw new Error(`${label} response is invalid`);
|
|
227
|
+
}
|
|
228
|
+
return value as Record<string, unknown>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function stringValue(value: unknown): string | null {
|
|
232
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
233
|
+
}
|