@mastra/factory 0.16.0-alpha.10 → 0.16.0-alpha.12
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 +37 -0
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +19 -8
- package/dist/boards/review.js.map +1 -1
- package/dist/boards/work.d.ts.map +1 -1
- package/dist/boards/work.js +31 -3
- package/dist/boards/work.js.map +1 -1
- package/dist/capabilities/intake.d.ts +4 -0
- package/dist/capabilities/intake.d.ts.map +1 -1
- package/dist/capabilities/version-control.d.ts +15 -0
- package/dist/capabilities/version-control.d.ts.map +1 -1
- package/dist/factory.d.ts +7 -7
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +100 -40
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +3 -0
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +21 -0
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/routes.js +2 -1
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +41 -42
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +416 -227
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/webhook.d.ts +2 -5
- package/dist/integrations/github/webhook.d.ts.map +1 -1
- package/dist/integrations/github/webhook.js +8 -48
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/gitlab/agent-tools.d.ts +14 -0
- package/dist/integrations/gitlab/agent-tools.d.ts.map +1 -0
- package/dist/integrations/gitlab/agent-tools.js +42 -0
- package/dist/integrations/gitlab/agent-tools.js.map +1 -0
- package/dist/integrations/gitlab/api.d.ts +222 -0
- package/dist/integrations/gitlab/api.d.ts.map +1 -0
- package/dist/integrations/gitlab/api.js +274 -0
- package/dist/integrations/gitlab/api.js.map +1 -0
- package/dist/integrations/gitlab/default-rules.d.ts +125 -0
- package/dist/integrations/gitlab/default-rules.d.ts.map +1 -0
- package/dist/integrations/gitlab/default-rules.js +172 -0
- package/dist/integrations/gitlab/default-rules.js.map +1 -0
- package/dist/integrations/gitlab/integration.d.ts +138 -0
- package/dist/integrations/gitlab/integration.d.ts.map +1 -0
- package/dist/integrations/gitlab/integration.js +705 -0
- package/dist/integrations/gitlab/integration.js.map +1 -0
- package/dist/integrations/gitlab/issue-reconciler.d.ts +6 -0
- package/dist/integrations/gitlab/issue-reconciler.d.ts.map +1 -0
- package/dist/integrations/gitlab/issue-reconciler.js +123 -0
- package/dist/integrations/gitlab/issue-reconciler.js.map +1 -0
- package/dist/integrations/gitlab/merge-request-reconciler.d.ts +6 -0
- package/dist/integrations/gitlab/merge-request-reconciler.d.ts.map +1 -0
- package/dist/integrations/gitlab/merge-request-reconciler.js +267 -0
- package/dist/integrations/gitlab/merge-request-reconciler.js.map +1 -0
- package/dist/integrations/gitlab/reconciler.d.ts +5 -0
- package/dist/integrations/gitlab/reconciler.d.ts.map +1 -0
- package/dist/integrations/gitlab/reconciler.js +31 -0
- package/dist/integrations/gitlab/reconciler.js.map +1 -0
- package/dist/integrations/gitlab/reconciliation-config.d.ts +3 -0
- package/dist/integrations/gitlab/reconciliation-config.d.ts.map +1 -0
- package/dist/integrations/gitlab/reconciliation-config.js +17 -0
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -0
- package/dist/integrations/gitlab/routes.d.ts +20 -0
- package/dist/integrations/gitlab/routes.d.ts.map +1 -0
- package/dist/integrations/gitlab/routes.js +473 -0
- package/dist/integrations/gitlab/routes.js.map +1 -0
- package/dist/integrations/gitlab/rules.d.ts +35 -0
- package/dist/integrations/gitlab/rules.d.ts.map +1 -0
- package/dist/integrations/gitlab/rules.js +391 -0
- package/dist/integrations/gitlab/rules.js.map +1 -0
- package/dist/integrations/gitlab/session-subscriptions.d.ts +32 -0
- package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -0
- package/dist/integrations/gitlab/session-subscriptions.js +192 -0
- package/dist/integrations/gitlab/session-subscriptions.js.map +1 -0
- package/dist/integrations/gitlab/subscriptions.d.ts +70 -0
- package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -0
- package/dist/integrations/gitlab/subscriptions.js +76 -0
- package/dist/integrations/gitlab/subscriptions.js.map +1 -0
- package/dist/integrations/gitlab/version-control.d.ts +16 -0
- package/dist/integrations/gitlab/version-control.d.ts.map +1 -0
- package/dist/integrations/gitlab/version-control.js +622 -0
- package/dist/integrations/gitlab/version-control.js.map +1 -0
- package/dist/integrations/gitlab/webhook-dispatch.d.ts +75 -0
- package/dist/integrations/gitlab/webhook-dispatch.d.ts.map +1 -0
- package/dist/integrations/gitlab/webhook-dispatch.js +235 -0
- package/dist/integrations/gitlab/webhook-dispatch.js.map +1 -0
- package/dist/integrations/gitlab/webhook.d.ts +72 -0
- package/dist/integrations/gitlab/webhook.d.ts.map +1 -0
- package/dist/integrations/gitlab/webhook.js +201 -0
- package/dist/integrations/gitlab/webhook.js.map +1 -0
- package/dist/integrations/incidentio/integration.js +1 -1
- package/dist/integrations/issue-reconciler.d.ts +3 -3
- package/dist/integrations/issue-reconciler.d.ts.map +1 -1
- package/dist/integrations/issue-reconciler.js +22 -2
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +21 -0
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.d.ts +64 -0
- package/dist/integrations/platform/gitlab/event-worker.d.ts.map +1 -0
- package/dist/integrations/platform/gitlab/event-worker.js +306 -0
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -0
- package/dist/integrations/platform/gitlab/integration.d.ts +57 -0
- package/dist/integrations/platform/gitlab/integration.d.ts.map +1 -0
- package/dist/integrations/platform/gitlab/integration.js +125 -0
- package/dist/integrations/platform/gitlab/integration.js.map +1 -0
- package/dist/integrations/platform/incidentio/integration.js +1 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +3 -3
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/slack.d.ts +2 -0
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +21 -7
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +37 -0
- package/dist/integrations/subscription-session.d.ts.map +1 -0
- package/dist/integrations/subscription-session.js +83 -0
- package/dist/integrations/subscription-session.js.map +1 -0
- package/dist/packages/_internals/workspace/dist/index.js +1 -11
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/routes/intake.d.ts.map +1 -1
- package/dist/routes/intake.js +4 -2
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +1 -1
- package/dist/routes/projects.d.ts +1 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +1 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +20 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -0
- package/dist/routes/source-control-sessions.js +319 -0
- package/dist/routes/source-control-sessions.js.map +1 -0
- package/dist/routes/source-control-settings.d.ts +10 -0
- package/dist/routes/source-control-settings.d.ts.map +1 -0
- package/dist/routes/source-control-settings.js +87 -0
- package/dist/routes/source-control-settings.js.map +1 -0
- package/dist/routes/surface.d.ts +3 -3
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +55 -13
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.d.ts +8 -0
- package/dist/routes/tenant-credentials.d.ts.map +1 -1
- package/dist/routes/tenant-credentials.js +20 -3
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +7 -1
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +1 -1
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +3 -2
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +6 -3
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +67 -3
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +23 -2
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.d.ts.map +1 -1
- package/dist/rules/validation.js +2 -0
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/git-ref.d.ts +7 -0
- package/dist/sandbox/git-ref.d.ts.map +1 -0
- package/dist/sandbox/git-ref.js +15 -0
- package/dist/sandbox/git-ref.js.map +1 -0
- package/dist/sandbox/session-retirement.js +1 -1
- package/dist/sandbox/workdir.d.ts.map +1 -1
- package/dist/sandbox/workdir.js +6 -6
- package/dist/sandbox/workdir.js.map +1 -1
- package/dist/session/factory-session.d.ts +31 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +60 -1
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/source-control-tools.d.ts +16 -0
- package/dist/session/source-control-tools.d.ts.map +1 -0
- package/dist/session/source-control-tools.js +464 -0
- package/dist/session/source-control-tools.js.map +1 -0
- package/dist/storage/domains/intake/base.d.ts +18 -0
- package/dist/storage/domains/intake/base.d.ts.map +1 -1
- package/dist/storage/domains/intake/base.js +77 -0
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/work-item-branch.d.ts +3 -1
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +22 -4
- package/dist/work-item-branch.js.map +1 -1
- package/dist/workspace.d.ts +15 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +57 -23
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-gitlab-rereview/SKILL.md +19 -0
- package/factory-skills/factory-gitlab-review/SKILL.md +30 -0
- package/factory-skills/factory-rereview/SKILL.md +13 -6
- package/factory-skills/factory-review/SKILL.md +13 -6
- package/factory-skills/factory-review/references/archaeology.md +225 -0
- package/factory-skills/factory-review/references/categories/README.md +34 -0
- package/factory-skills/factory-review/references/categories/behavior-change.md +36 -0
- package/factory-skills/factory-review/references/categories/bug-fix.md +37 -0
- package/factory-skills/factory-review/references/categories/docs.md +32 -0
- package/factory-skills/factory-review/references/categories/experimental-flagged.md +34 -0
- package/factory-skills/factory-review/references/categories/infra-tooling.md +37 -0
- package/factory-skills/factory-review/references/categories/internal-capability.md +35 -0
- package/factory-skills/factory-review/references/categories/large-cross-cutting.md +35 -0
- package/factory-skills/factory-review/references/categories/mechanical.md +36 -0
- package/factory-skills/factory-review/references/categories/performance.md +33 -0
- package/factory-skills/factory-review/references/categories/public-api.md +38 -0
- package/factory-skills/factory-review/references/categories/refactor.md +35 -0
- package/factory-skills/factory-review/references/categories/revert.md +31 -0
- package/factory-skills/factory-review/references/categories/schema-storage.md +39 -0
- package/factory-skills/factory-review/references/categories/security.md +35 -0
- package/factory-skills/factory-review/references/categories/tests-only.md +38 -0
- package/package.json +9 -9
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { GitLabApiClient, GitLabApiError } from "./api.js";
|
|
2
|
+
import { IssueReconcileWorker } from "../issue-reconcile-worker.js";
|
|
3
|
+
import { buildGitLabAgentTools } from "./agent-tools.js";
|
|
4
|
+
import { resolveGitLabRules } from "./default-rules.js";
|
|
5
|
+
import { attachGitLabRules } from "./rules.js";
|
|
6
|
+
import { attachGitLabReconciler } from "./reconciler.js";
|
|
7
|
+
import { gitlabReconciliationEnabled, gitlabReconciliationInterval } from "./reconciliation-config.js";
|
|
8
|
+
import { buildGitLabRoutes } from "./routes.js";
|
|
9
|
+
import { createGitLabSubscriptionTools, parseCreatedMergeRequest, subscribeCurrentSessionToMergeRequest } from "./session-subscriptions.js";
|
|
10
|
+
import { buildGitLabVersionControl } from "./version-control.js";
|
|
11
|
+
//#region src/integrations/gitlab/integration.ts
|
|
12
|
+
const DIRECT_CONNECTION_ID = "direct";
|
|
13
|
+
const GITLAB_CONNECTION_TOKEN_PREFIX = "gitlab-connection:";
|
|
14
|
+
const GITLAB_SOURCE_PREFIX = "gitlab-project:";
|
|
15
|
+
const GITLAB_ISSUE_PREFIX = "gitlab-issue:";
|
|
16
|
+
const GITLAB_MERGE_REQUEST_PREFIX = "gitlab-pr:";
|
|
17
|
+
const GITLAB_TRUSTED_ACCESS_LEVEL = 30;
|
|
18
|
+
const MAX_NOTES_PAGES = 20;
|
|
19
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
|
+
var GitLabIntegrationBase = class {
|
|
21
|
+
id = "gitlab";
|
|
22
|
+
#projects;
|
|
23
|
+
#sourceControl;
|
|
24
|
+
#integrationStorage;
|
|
25
|
+
#auth;
|
|
26
|
+
#orgIdByResourceId = /* @__PURE__ */ new Map();
|
|
27
|
+
rules;
|
|
28
|
+
constructor(rules) {
|
|
29
|
+
this.rules = resolveGitLabRules(rules);
|
|
30
|
+
}
|
|
31
|
+
intake = {
|
|
32
|
+
resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
|
|
33
|
+
listSources: () => this.#listSources(),
|
|
34
|
+
listItems: (input) => this.#listItems(input),
|
|
35
|
+
listIssues: (input) => this.#listIssues(input),
|
|
36
|
+
getIssue: (input) => this.#getIssue(input),
|
|
37
|
+
createComment: (input) => this.#createComment(input),
|
|
38
|
+
updateIssue: (input) => this.#updateIssue(input)
|
|
39
|
+
};
|
|
40
|
+
versionControl = buildGitLabVersionControl({
|
|
41
|
+
contextForConnection: (connection) => this.#versionControlContext(connection),
|
|
42
|
+
contextForStoredInstallation: (connection, host) => this.#versionControlContextForInstallation(connection, host)
|
|
43
|
+
});
|
|
44
|
+
initialize({ storage, projects, auth, sourceControl }) {
|
|
45
|
+
this.#projects = projects;
|
|
46
|
+
this.#auth = auth;
|
|
47
|
+
this.#sourceControl = sourceControl;
|
|
48
|
+
this.#integrationStorage = storage;
|
|
49
|
+
}
|
|
50
|
+
/** Generic integration persistence (merge-request subscriptions). */
|
|
51
|
+
get integrationStorage() {
|
|
52
|
+
if (!this.#integrationStorage) throw new Error(`${this.constructor.name} is not initialized — the factory binds storage during prepare().`);
|
|
53
|
+
return this.#integrationStorage;
|
|
54
|
+
}
|
|
55
|
+
/** Source-control rows for this integration; absent when the host runs without them. */
|
|
56
|
+
get sourceControlStorage() {
|
|
57
|
+
return this.#sourceControl;
|
|
58
|
+
}
|
|
59
|
+
get authEnabled() {
|
|
60
|
+
return this.#auth?.enabled() ?? false;
|
|
61
|
+
}
|
|
62
|
+
get projects() {
|
|
63
|
+
if (!this.#projects) throw new Error(`${this.constructor.name} is not initialized — the factory binds storage during prepare().`);
|
|
64
|
+
return this.#projects;
|
|
65
|
+
}
|
|
66
|
+
async resolveOrgId(resourceId) {
|
|
67
|
+
const cached = this.#orgIdByResourceId.get(resourceId);
|
|
68
|
+
if (cached !== void 0) return cached;
|
|
69
|
+
if (!UUID_PATTERN.test(resourceId)) {
|
|
70
|
+
this.#orgIdByResourceId.set(resourceId, null);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await this.projects.ensureReady();
|
|
75
|
+
const orgId = (await this.projects.getById({ id: resourceId }))?.orgId ?? null;
|
|
76
|
+
this.#orgIdByResourceId.set(resourceId, orgId);
|
|
77
|
+
return orgId;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
clearCaches() {
|
|
83
|
+
this.#orgIdByResourceId.clear();
|
|
84
|
+
}
|
|
85
|
+
/** Resolve only a GitLab repository linked to this Factory in this organization. */
|
|
86
|
+
async getLinkedRepository(input) {
|
|
87
|
+
if (!this.#sourceControl) return null;
|
|
88
|
+
const connections = await this.#sourceControl.connections.list({
|
|
89
|
+
orgId: input.orgId,
|
|
90
|
+
factoryProjectId: input.factoryProjectId
|
|
91
|
+
});
|
|
92
|
+
for (const connection of connections) {
|
|
93
|
+
if (connection.integrationId !== "gitlab") continue;
|
|
94
|
+
const link = (await this.#sourceControl.projectRepositories.list({
|
|
95
|
+
orgId: input.orgId,
|
|
96
|
+
connectionId: connection.id
|
|
97
|
+
})).find((candidate) => candidate.id === input.projectRepositoryId);
|
|
98
|
+
if (!link) continue;
|
|
99
|
+
const repository = await this.#sourceControl.repositories.get({
|
|
100
|
+
orgId: input.orgId,
|
|
101
|
+
id: link.repositoryId
|
|
102
|
+
});
|
|
103
|
+
const installation = await this.#sourceControl.installations.get({
|
|
104
|
+
orgId: input.orgId,
|
|
105
|
+
id: connection.installationId
|
|
106
|
+
});
|
|
107
|
+
if (!repository || !installation) return null;
|
|
108
|
+
const host = installation.providerMetadata.host;
|
|
109
|
+
if (typeof host !== "string" || !host) return null;
|
|
110
|
+
return {
|
|
111
|
+
repository,
|
|
112
|
+
host: normalizeGitLabHost(host)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
async getProjectMemberAccessLevel(connectionId, projectId, username) {
|
|
118
|
+
const normalized = username.trim().toLowerCase();
|
|
119
|
+
if (!normalized) return void 0;
|
|
120
|
+
return (await (await this.contextById(connectionId)).api.listProjectMembers(projectId, { query: username })).find((candidate) => candidate.username.toLowerCase() === normalized && candidate.state !== "blocked")?.access_level;
|
|
121
|
+
}
|
|
122
|
+
async getWorkItemAuthorUsername(connectionId, projectId, kind, iid) {
|
|
123
|
+
const api = (await this.contextById(connectionId)).api;
|
|
124
|
+
return (kind === "issue" ? await api.getIssue(projectId, iid) : await api.getMergeRequest(projectId, iid)).author?.username;
|
|
125
|
+
}
|
|
126
|
+
async isProjectMemberTrustedForSource(sourceId, username) {
|
|
127
|
+
const reference = decodeSourceId(sourceId);
|
|
128
|
+
if (!reference) throw new GitLabApiError("GitLab source identity is invalid.", 400);
|
|
129
|
+
const { context, source } = await this.#resolveSourceReference(reference);
|
|
130
|
+
return (await this.getProjectMemberAccessLevel(context.id, source.projectId, username) ?? 0) >= 30;
|
|
131
|
+
}
|
|
132
|
+
async getIssueForFactoryProject(input) {
|
|
133
|
+
if (!this.#sourceControl) throw new GitLabApiError("GitLab source control is unavailable.", 503);
|
|
134
|
+
const reference = decodeIssueReference(input.issueId);
|
|
135
|
+
const locator = parseIssueLocator(input.issueId);
|
|
136
|
+
if (!reference && !locator) throw new GitLabApiError("GitLab issue must include a project and issue IID.", 400);
|
|
137
|
+
const normalizePath = (value) => value.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
138
|
+
const matches = [];
|
|
139
|
+
const connections = await this.#sourceControl.connections.list({
|
|
140
|
+
orgId: input.orgId,
|
|
141
|
+
factoryProjectId: input.factoryProjectId
|
|
142
|
+
});
|
|
143
|
+
for (const connection of connections) {
|
|
144
|
+
const installation = await this.#sourceControl.installations.get({
|
|
145
|
+
orgId: input.orgId,
|
|
146
|
+
id: connection.installationId
|
|
147
|
+
});
|
|
148
|
+
if (!installation) continue;
|
|
149
|
+
for (const projectRepository of await this.#sourceControl.projectRepositories.list({
|
|
150
|
+
orgId: input.orgId,
|
|
151
|
+
connectionId: connection.id
|
|
152
|
+
})) {
|
|
153
|
+
const repository = await this.#sourceControl.repositories.get({
|
|
154
|
+
orgId: input.orgId,
|
|
155
|
+
id: projectRepository.repositoryId
|
|
156
|
+
});
|
|
157
|
+
if (!repository) continue;
|
|
158
|
+
if (reference?.projectId && repository.externalId !== reference.projectId) continue;
|
|
159
|
+
if (reference?.projectPath && normalizePath(repository.slug) !== normalizePath(reference.projectPath)) continue;
|
|
160
|
+
if (locator?.projectPath && normalizePath(repository.slug) !== normalizePath(locator.projectPath)) continue;
|
|
161
|
+
if (reference?.connectionId && installation.externalId !== reference.connectionId) continue;
|
|
162
|
+
const installationHost = typeof installation.providerMetadata.host === "string" ? normalizeGitLabHost(installation.providerMetadata.host) : null;
|
|
163
|
+
if (reference?.host && installationHost !== normalizeGitLabHost(reference.host)) continue;
|
|
164
|
+
if (locator?.host && installationHost !== normalizeGitLabHost(locator.host)) continue;
|
|
165
|
+
matches.push({
|
|
166
|
+
connectionId: installation.externalId,
|
|
167
|
+
projectId: repository.externalId,
|
|
168
|
+
projectPath: repository.slug
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const match = matches.sort((left, right) => left.connectionId.localeCompare(right.connectionId))[0];
|
|
173
|
+
if (!match) throw new GitLabApiError("GitLab issue is outside the active Factory project.", 404);
|
|
174
|
+
return this.intake.getIssue({
|
|
175
|
+
connection: gitlabConnection(match.connectionId),
|
|
176
|
+
sourceId: encodeSourceId(match),
|
|
177
|
+
issueId: input.issueId
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/** Platform mode exposes org connections; direct mode returns undefined. */
|
|
181
|
+
async statusConnections() {}
|
|
182
|
+
/** Direct mode verifies its configured credential; Platform mode uses connection status. */
|
|
183
|
+
async verifyStatus() {}
|
|
184
|
+
get webhookSecret() {}
|
|
185
|
+
workers(ctx) {
|
|
186
|
+
if (!gitlabReconciliationEnabled()) return [];
|
|
187
|
+
const reconcile = attachGitLabReconciler(this, ctx);
|
|
188
|
+
if (!reconcile) return [];
|
|
189
|
+
const intervalMs = gitlabReconciliationInterval();
|
|
190
|
+
return [new IssueReconcileWorker({
|
|
191
|
+
integrationId: this.id,
|
|
192
|
+
reconcile,
|
|
193
|
+
...intervalMs ? { intervalMs } : {}
|
|
194
|
+
})];
|
|
195
|
+
}
|
|
196
|
+
routes(ctx) {
|
|
197
|
+
const ingestFactoryEvent = attachGitLabRules(this, ctx);
|
|
198
|
+
return buildGitLabRoutes({
|
|
199
|
+
gitlab: this,
|
|
200
|
+
auth: ctx.auth,
|
|
201
|
+
intake: ctx.storage?.intake,
|
|
202
|
+
emitAudit: ctx.hooks?.emitAudit,
|
|
203
|
+
sandbox: ctx.sandbox,
|
|
204
|
+
webhookSecret: this.webhookSecret,
|
|
205
|
+
ingestFactoryEvent,
|
|
206
|
+
...ctx.controller ? { controller: ctx.controller } : {}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async agentTools(args) {
|
|
210
|
+
return buildGitLabAgentTools({
|
|
211
|
+
requestContext: args.requestContext,
|
|
212
|
+
gitlab: this
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
sessionTools({ requestContext }) {
|
|
216
|
+
return createGitLabSubscriptionTools(requestContext, this);
|
|
217
|
+
}
|
|
218
|
+
async postToolObserver({ toolContext, requestContext }) {
|
|
219
|
+
const mergeRequestUrl = parseCreatedMergeRequest(toolContext);
|
|
220
|
+
if (!mergeRequestUrl || !requestContext) return;
|
|
221
|
+
await subscribeCurrentSessionToMergeRequest(requestContext, mergeRequestUrl, "auto-create-change-request", this);
|
|
222
|
+
}
|
|
223
|
+
/** Prefer the configured direct token over an older Platform installation on the same host. */
|
|
224
|
+
async resolveActiveConnectionForHost(storedConnectionId, host) {
|
|
225
|
+
if (this.diagnostics().mode !== "direct") return storedConnectionId;
|
|
226
|
+
const [direct] = await this.activeContexts();
|
|
227
|
+
return direct && normalizeGitLabHost(direct.host) === normalizeGitLabHost(host) ? direct.id : storedConnectionId;
|
|
228
|
+
}
|
|
229
|
+
async #versionControlContext(connection) {
|
|
230
|
+
const connectionId = connectionIdFromConnection(connection);
|
|
231
|
+
if (connectionId) return this.contextById(connectionId);
|
|
232
|
+
const contexts = await this.activeContexts();
|
|
233
|
+
if (contexts.length !== 1) throw new GitLabApiError("GitLab version-control requests must identify a connection when multiple GitLab accounts are connected.", 400);
|
|
234
|
+
return contexts[0];
|
|
235
|
+
}
|
|
236
|
+
async #versionControlContextForInstallation(connection, host) {
|
|
237
|
+
if (this.diagnostics().mode === "direct" && host) {
|
|
238
|
+
const [direct] = await this.activeContexts();
|
|
239
|
+
if (direct && normalizeGitLabHost(host) === normalizeGitLabHost(direct.host)) return direct;
|
|
240
|
+
}
|
|
241
|
+
return this.#versionControlContext(connection);
|
|
242
|
+
}
|
|
243
|
+
async #contextForReference(reference) {
|
|
244
|
+
if (reference.connectionId) return this.contextById(reference.connectionId);
|
|
245
|
+
const contexts = (await this.activeContexts()).filter((context) => !reference.host || normalizeGitLabHost(context.host) === normalizeGitLabHost(reference.host));
|
|
246
|
+
if (contexts.length === 0) throw new GitLabApiError("No active GitLab connection can access this source.", 401);
|
|
247
|
+
if (contexts.length === 1) return contexts[0];
|
|
248
|
+
if (this.#sourceControl) {
|
|
249
|
+
const linkedConnectionIds = new Set((await this.#sourceControl.projectRepositories.listConfiguredExternalKeys()).filter((key) => key.repositoryExternalId === reference.projectId).map((key) => key.installationExternalId));
|
|
250
|
+
const linked = contexts.filter((context) => linkedConnectionIds.has(context.id));
|
|
251
|
+
if (linked.length > 0) return linked.sort((left, right) => left.id.localeCompare(right.id))[0];
|
|
252
|
+
}
|
|
253
|
+
for (const context of contexts.sort((left, right) => left.id.localeCompare(right.id))) try {
|
|
254
|
+
await context.api.getProject(reference.projectId);
|
|
255
|
+
return context;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (!(error instanceof GitLabApiError) || ![
|
|
258
|
+
401,
|
|
259
|
+
403,
|
|
260
|
+
404
|
|
261
|
+
].includes(error.status ?? 0)) throw error;
|
|
262
|
+
}
|
|
263
|
+
throw new GitLabApiError("No active GitLab connection can access this source.", 404);
|
|
264
|
+
}
|
|
265
|
+
async #resolveSourceReference(reference) {
|
|
266
|
+
const context = await this.#contextForReference(reference);
|
|
267
|
+
const projectPath = reference.projectPath ?? (await context.api.getProject(reference.projectId)).path_with_namespace;
|
|
268
|
+
return {
|
|
269
|
+
context,
|
|
270
|
+
source: {
|
|
271
|
+
connectionId: context.id,
|
|
272
|
+
host: normalizeGitLabHost(reference.host ?? context.host),
|
|
273
|
+
projectId: reference.projectId,
|
|
274
|
+
projectPath
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
async #resolveIntakeDispatch({ externalSource }) {
|
|
279
|
+
if (externalSource.type !== "issue") return null;
|
|
280
|
+
const reference = decodeIssueReference(externalSource.externalId);
|
|
281
|
+
if (!reference) return null;
|
|
282
|
+
const { context, source } = await this.#resolveSourceReference(reference);
|
|
283
|
+
return {
|
|
284
|
+
connection: context.connection,
|
|
285
|
+
sourceId: encodeSourceId(source),
|
|
286
|
+
issueId: String(reference.issueIid)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async #listSources() {
|
|
290
|
+
const sources = /* @__PURE__ */ new Map();
|
|
291
|
+
for (const context of await this.activeContexts()) for (let page = 1;; page++) {
|
|
292
|
+
const projects = await context.api.listProjects({ page });
|
|
293
|
+
for (const project of projects) {
|
|
294
|
+
const source = this.#toIntakeSource(context, project);
|
|
295
|
+
if (!sources.has(source.id)) sources.set(source.id, source);
|
|
296
|
+
}
|
|
297
|
+
if (projects.length < 100) break;
|
|
298
|
+
}
|
|
299
|
+
return [...sources.values()];
|
|
300
|
+
}
|
|
301
|
+
async #listItems(input) {
|
|
302
|
+
const result = await this.#listIssuePage(input.sourceIds, input.cursor);
|
|
303
|
+
return {
|
|
304
|
+
items: result.issues.map(({ issue, source, context }) => ({
|
|
305
|
+
source: {
|
|
306
|
+
type: "issue",
|
|
307
|
+
externalId: encodeIssueReference({
|
|
308
|
+
...source,
|
|
309
|
+
issueIid: issue.iid
|
|
310
|
+
}),
|
|
311
|
+
url: issue.web_url
|
|
312
|
+
},
|
|
313
|
+
sourceId: encodeSourceId(source),
|
|
314
|
+
title: `${source.projectPath}#${issue.iid}: ${issue.title}`,
|
|
315
|
+
status: issue.state,
|
|
316
|
+
labels: issue.labels ?? [],
|
|
317
|
+
assignee: displayName(issue.assignee),
|
|
318
|
+
createdAt: issue.created_at,
|
|
319
|
+
updatedAt: issue.updated_at,
|
|
320
|
+
metadata: {
|
|
321
|
+
identifier: `${source.projectPath}#${issue.iid}`,
|
|
322
|
+
projectId: source.projectId,
|
|
323
|
+
projectPath: source.projectPath,
|
|
324
|
+
connectionId: context.id,
|
|
325
|
+
accountLabel: context.label,
|
|
326
|
+
labelColors: Object.fromEntries((issue.labelDetails ?? []).map((label) => [label.name, label.color]))
|
|
327
|
+
}
|
|
328
|
+
})),
|
|
329
|
+
nextCursor: result.nextCursor
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async #listIssues(input) {
|
|
333
|
+
const result = await this.#listIssuePage(input.sourceIds, input.cursor, input.labels);
|
|
334
|
+
return {
|
|
335
|
+
issues: result.issues.map(({ issue, source }) => ({
|
|
336
|
+
...this.#toIntakeIssue(issue, source.projectPath),
|
|
337
|
+
sourceId: encodeSourceId(source)
|
|
338
|
+
})),
|
|
339
|
+
nextCursor: result.nextCursor
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async #listIssuePage(sourceIds, cursor, labels) {
|
|
343
|
+
const sources = sourceIds.map(decodeSourceId).filter((source) => source !== null);
|
|
344
|
+
if (sources.length === 0) return {
|
|
345
|
+
issues: [],
|
|
346
|
+
nextCursor: null
|
|
347
|
+
};
|
|
348
|
+
const current = decodePageCursor(cursor);
|
|
349
|
+
if (!current || current.source >= sources.length) return {
|
|
350
|
+
issues: [],
|
|
351
|
+
nextCursor: null
|
|
352
|
+
};
|
|
353
|
+
for (let sourceIndex = current.source; sourceIndex < sources.length; sourceIndex++) {
|
|
354
|
+
const { source, context } = await this.#resolveSourceReference(sources[sourceIndex]);
|
|
355
|
+
const page = sourceIndex === current.source ? current.page : 1;
|
|
356
|
+
const issues = await context.api.listIssues(source.projectId, {
|
|
357
|
+
page,
|
|
358
|
+
labels
|
|
359
|
+
});
|
|
360
|
+
const nextCursor = issues.length === 30 ? encodePageCursor({
|
|
361
|
+
source: sourceIndex,
|
|
362
|
+
page: page + 1
|
|
363
|
+
}) : sourceIndex + 1 < sources.length ? encodePageCursor({
|
|
364
|
+
source: sourceIndex + 1,
|
|
365
|
+
page: 1
|
|
366
|
+
}) : null;
|
|
367
|
+
return {
|
|
368
|
+
issues: issues.map((issue) => ({
|
|
369
|
+
issue,
|
|
370
|
+
source,
|
|
371
|
+
context
|
|
372
|
+
})),
|
|
373
|
+
nextCursor
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
issues: [],
|
|
378
|
+
nextCursor: null
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async #getIssue(input) {
|
|
382
|
+
const resolved = await this.#resolveRequest(input);
|
|
383
|
+
let issue;
|
|
384
|
+
try {
|
|
385
|
+
issue = await resolved.context.api.getIssue(resolved.projectId, resolved.issueIid);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error instanceof GitLabApiError && error.status === 404) return null;
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
const notes = await this.#listAllNotes(resolved.context.api, resolved.projectId, resolved.issueIid);
|
|
391
|
+
return {
|
|
392
|
+
...this.#toIntakeIssue(issue, resolved.projectPath),
|
|
393
|
+
description: issue.description?.trim() || null,
|
|
394
|
+
comments: notes.filter((note) => !note.system).map((note) => ({
|
|
395
|
+
author: displayName(note.author),
|
|
396
|
+
body: note.body,
|
|
397
|
+
createdAt: note.created_at
|
|
398
|
+
}))
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async #listAllNotes(api, projectId, issueIid) {
|
|
402
|
+
const notes = [];
|
|
403
|
+
for (let page = 1; page <= MAX_NOTES_PAGES; page++) {
|
|
404
|
+
const result = await api.listNotes(projectId, issueIid, { page });
|
|
405
|
+
notes.push(...result);
|
|
406
|
+
if (result.length < 100) break;
|
|
407
|
+
}
|
|
408
|
+
return notes;
|
|
409
|
+
}
|
|
410
|
+
async #createComment(input) {
|
|
411
|
+
const resolved = await this.#resolveRequest(input);
|
|
412
|
+
try {
|
|
413
|
+
const issue = await resolved.context.api.getIssue(resolved.projectId, resolved.issueIid);
|
|
414
|
+
const note = await resolved.context.api.createNote(resolved.projectId, resolved.issueIid, input.body);
|
|
415
|
+
return {
|
|
416
|
+
id: String(note.id),
|
|
417
|
+
url: `${issue.web_url}#note_${note.id}`
|
|
418
|
+
};
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error instanceof GitLabApiError && error.status === 404) return null;
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async #updateIssue(input) {
|
|
425
|
+
const resolved = await this.#resolveRequest(input);
|
|
426
|
+
let issue;
|
|
427
|
+
try {
|
|
428
|
+
issue = await resolved.context.api.getIssue(resolved.projectId, resolved.issueIid);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
if (error instanceof GitLabApiError && error.status === 404) return null;
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
const stateEvent = targetStateEvent(input);
|
|
434
|
+
if (!stateEvent) return null;
|
|
435
|
+
if (stateEvent === "close" && issue.state === "closed" || stateEvent === "reopen" && issue.state === "opened") return this.#toIntakeIssue(issue, resolved.projectPath);
|
|
436
|
+
const updated = await resolved.context.api.updateIssueState(resolved.projectId, resolved.issueIid, stateEvent);
|
|
437
|
+
return this.#toIntakeIssue(updated, resolved.projectPath);
|
|
438
|
+
}
|
|
439
|
+
async #resolveRequest(input) {
|
|
440
|
+
const issueReference = decodeIssueReference(input.issueId);
|
|
441
|
+
const source = input.sourceId ? decodeSourceId(input.sourceId) : null;
|
|
442
|
+
const locator = parseIssueLocator(input.issueId);
|
|
443
|
+
const reference = issueReference ?? source;
|
|
444
|
+
let context;
|
|
445
|
+
if (reference) context = await this.#contextForReference(reference);
|
|
446
|
+
else {
|
|
447
|
+
const connectionId = connectionIdFromConnection(input.connection);
|
|
448
|
+
const contexts = connectionId ? [await this.contextById(connectionId)] : await this.activeContexts();
|
|
449
|
+
if (contexts.length !== 1) throw new GitLabApiError("GitLab issue reference must resolve to exactly one active connection.", 400);
|
|
450
|
+
context = contexts[0];
|
|
451
|
+
}
|
|
452
|
+
const projectId = issueReference?.projectId ?? source?.projectId ?? locator?.projectPath;
|
|
453
|
+
const issueIid = issueReference?.issueIid ?? locator?.issueIid ?? parsePositiveInteger(input.issueId);
|
|
454
|
+
if (!projectId || !issueIid) throw new GitLabApiError("GitLab issue must include a project and issue IID (for example, group/project#42).", 400);
|
|
455
|
+
const projectPath = issueReference?.projectPath ?? source?.projectPath ?? locator?.projectPath ?? (await context.api.getProject(projectId)).path_with_namespace;
|
|
456
|
+
return {
|
|
457
|
+
context,
|
|
458
|
+
projectId,
|
|
459
|
+
projectPath,
|
|
460
|
+
issueIid
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
#toIntakeSource(context, project) {
|
|
464
|
+
return {
|
|
465
|
+
id: encodeSourceId({
|
|
466
|
+
host: normalizeGitLabHost(context.host),
|
|
467
|
+
projectId: String(project.id),
|
|
468
|
+
projectPath: project.path_with_namespace
|
|
469
|
+
}),
|
|
470
|
+
name: project.path_with_namespace,
|
|
471
|
+
type: "project",
|
|
472
|
+
metadata: {
|
|
473
|
+
projectId: String(project.id),
|
|
474
|
+
projectPath: project.path_with_namespace,
|
|
475
|
+
defaultBranch: project.default_branch ?? null,
|
|
476
|
+
connectionId: context.id,
|
|
477
|
+
accountLabel: context.label,
|
|
478
|
+
url: project.web_url
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
#toIntakeIssue(issue, projectPath) {
|
|
483
|
+
const assignees = issue.assignees?.map(displayName).filter((name) => name !== null) ?? [];
|
|
484
|
+
return {
|
|
485
|
+
id: String(issue.iid),
|
|
486
|
+
identifier: `${projectPath}#${issue.iid}`,
|
|
487
|
+
title: issue.title,
|
|
488
|
+
url: issue.web_url,
|
|
489
|
+
author: displayName(issue.author),
|
|
490
|
+
authorUsername: issue.author?.username ?? null,
|
|
491
|
+
state: issue.state,
|
|
492
|
+
stateType: issue.state === "closed" ? "completed" : "unstarted",
|
|
493
|
+
priority: typeof issue.weight === "number" ? String(issue.weight) : null,
|
|
494
|
+
assignee: displayName(issue.assignee) ?? assignees[0] ?? null,
|
|
495
|
+
assignees,
|
|
496
|
+
source: projectPath,
|
|
497
|
+
labels: issue.labels ?? [],
|
|
498
|
+
labelColors: Object.fromEntries((issue.labelDetails ?? []).map((label) => [label.name, label.color])),
|
|
499
|
+
commentCount: issue.user_notes_count ?? null,
|
|
500
|
+
createdAt: issue.created_at,
|
|
501
|
+
updatedAt: issue.updated_at
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
const DIRECT_CONNECTION_TOKEN = "gitlab-direct-access-token";
|
|
506
|
+
var GitLabIntegration = class extends GitLabIntegrationBase {
|
|
507
|
+
#accessToken;
|
|
508
|
+
#accessTokenType;
|
|
509
|
+
#baseUrl;
|
|
510
|
+
#webhookSecret;
|
|
511
|
+
#context;
|
|
512
|
+
constructor(config = {}) {
|
|
513
|
+
super(config.rules);
|
|
514
|
+
const accessToken = config.accessToken?.trim() || process.env.GITLAB_ACCESS_TOKEN?.trim();
|
|
515
|
+
if (!accessToken) throw new Error("GitLabIntegration: missing required GITLAB_ACCESS_TOKEN.");
|
|
516
|
+
this.#accessToken = accessToken;
|
|
517
|
+
this.#accessTokenType = parseAccessTokenType(config.accessTokenType ?? process.env.GITLAB_ACCESS_TOKEN_TYPE?.trim());
|
|
518
|
+
this.#baseUrl = (config.baseUrl ?? process.env.GITLAB_BASE_URL?.trim() ?? "https://gitlab.com").replace(/\/+$/, "");
|
|
519
|
+
this.#webhookSecret = config.webhookSecret?.trim() || process.env.GITLAB_WEBHOOK_SECRET?.trim() || void 0;
|
|
520
|
+
const api = new GitLabApiClient({
|
|
521
|
+
baseUrl: this.#baseUrl,
|
|
522
|
+
accessToken: this.#accessToken,
|
|
523
|
+
...config.fetchImpl ? { fetchImpl: config.fetchImpl } : {}
|
|
524
|
+
});
|
|
525
|
+
this.#context = {
|
|
526
|
+
id: DIRECT_CONNECTION_ID,
|
|
527
|
+
label: new URL(this.#baseUrl).host,
|
|
528
|
+
api,
|
|
529
|
+
connection: {
|
|
530
|
+
type: "oauth",
|
|
531
|
+
accessToken: DIRECT_CONNECTION_TOKEN
|
|
532
|
+
},
|
|
533
|
+
host: new URL(this.#baseUrl).host,
|
|
534
|
+
webBaseUrl: this.#baseUrl,
|
|
535
|
+
repositoryAccessToken: async () => this.#accessToken
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
get webhookSecret() {
|
|
539
|
+
return this.#webhookSecret;
|
|
540
|
+
}
|
|
541
|
+
async hasActiveConnections() {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
async verifyStatus() {
|
|
545
|
+
await this.#context.api.getCurrentUser();
|
|
546
|
+
}
|
|
547
|
+
authFailureMessage() {
|
|
548
|
+
return `GitLab rejected the configured ${this.#accessTokenType} access token. Check the token and its scopes.`;
|
|
549
|
+
}
|
|
550
|
+
async activeContexts() {
|
|
551
|
+
return [this.#context];
|
|
552
|
+
}
|
|
553
|
+
async contextById(connectionId) {
|
|
554
|
+
if (connectionId !== DIRECT_CONNECTION_ID) throw new GitLabApiError("GitLab connection is unavailable.", 401);
|
|
555
|
+
return this.#context;
|
|
556
|
+
}
|
|
557
|
+
diagnostics() {
|
|
558
|
+
return {
|
|
559
|
+
configured: true,
|
|
560
|
+
mode: "direct",
|
|
561
|
+
accessTokenType: this.#accessTokenType,
|
|
562
|
+
endpointHost: new URL(this.#baseUrl).host,
|
|
563
|
+
webhookConfigured: Boolean(this.#webhookSecret)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
function parseAccessTokenType(value) {
|
|
568
|
+
const normalized = value?.trim().toLowerCase() || "personal";
|
|
569
|
+
if (normalized === "personal" || normalized === "group") return normalized;
|
|
570
|
+
throw new Error("GitLabIntegration: GITLAB_ACCESS_TOKEN_TYPE must be 'personal' or 'group'.");
|
|
571
|
+
}
|
|
572
|
+
function encodeSourceId(reference) {
|
|
573
|
+
if (reference.host) return `${GITLAB_SOURCE_PREFIX}${encodeOpaque({
|
|
574
|
+
version: 2,
|
|
575
|
+
host: normalizeGitLabHost(reference.host),
|
|
576
|
+
projectId: reference.projectId
|
|
577
|
+
})}`;
|
|
578
|
+
if (!reference.connectionId || !reference.projectPath) throw new Error("GitLab source reference is missing its instance host.");
|
|
579
|
+
return `${GITLAB_SOURCE_PREFIX}${encodeOpaque(reference)}`;
|
|
580
|
+
}
|
|
581
|
+
function decodeSourceId(value) {
|
|
582
|
+
return decodeOpaque(value, GITLAB_SOURCE_PREFIX, isSourceReference);
|
|
583
|
+
}
|
|
584
|
+
function encodeIssueReference(reference) {
|
|
585
|
+
if (reference.host) return `${GITLAB_ISSUE_PREFIX}${encodeOpaque({
|
|
586
|
+
version: 2,
|
|
587
|
+
host: normalizeGitLabHost(reference.host),
|
|
588
|
+
projectId: reference.projectId,
|
|
589
|
+
issueIid: reference.issueIid
|
|
590
|
+
})}`;
|
|
591
|
+
if (!reference.connectionId || !reference.projectPath) throw new Error("GitLab issue reference is missing its instance host.");
|
|
592
|
+
return `${GITLAB_ISSUE_PREFIX}${encodeOpaque(reference)}`;
|
|
593
|
+
}
|
|
594
|
+
function decodeIssueReference(value) {
|
|
595
|
+
return decodeOpaque(value, GITLAB_ISSUE_PREFIX, isIssueReference);
|
|
596
|
+
}
|
|
597
|
+
function decodeMergeRequestReference(value) {
|
|
598
|
+
return decodeOpaque(value, GITLAB_MERGE_REQUEST_PREFIX, isMergeRequestReference);
|
|
599
|
+
}
|
|
600
|
+
function gitlabConnection(connectionId) {
|
|
601
|
+
return {
|
|
602
|
+
type: "oauth",
|
|
603
|
+
accessToken: `${GITLAB_CONNECTION_TOKEN_PREFIX}${connectionId}`
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
function connectionIdFromConnection(connection) {
|
|
607
|
+
if (connection.type !== "oauth" || !connection.accessToken.startsWith(GITLAB_CONNECTION_TOKEN_PREFIX)) return null;
|
|
608
|
+
return connection.accessToken.slice(18) || null;
|
|
609
|
+
}
|
|
610
|
+
function encodeOpaque(value) {
|
|
611
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
612
|
+
}
|
|
613
|
+
function decodeOpaque(value, prefix, guard) {
|
|
614
|
+
if (!value.startsWith(prefix)) return null;
|
|
615
|
+
try {
|
|
616
|
+
const decoded = JSON.parse(Buffer.from(value.slice(prefix.length), "base64url").toString("utf8"));
|
|
617
|
+
return guard(decoded) ? decoded : null;
|
|
618
|
+
} catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function normalizeGitLabHost(host) {
|
|
623
|
+
return host.trim().toLowerCase().replace(/\.$/, "");
|
|
624
|
+
}
|
|
625
|
+
function isSourceReference(value) {
|
|
626
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
627
|
+
const ref = value;
|
|
628
|
+
if (typeof ref.projectId !== "string" || ref.projectId.length === 0) return false;
|
|
629
|
+
const canonical = ref.version === 2 && typeof ref.host === "string" && normalizeGitLabHost(ref.host).length > 0;
|
|
630
|
+
const legacy = typeof ref.connectionId === "string" && ref.connectionId.length > 0 && typeof ref.projectPath === "string" && ref.projectPath.length > 0;
|
|
631
|
+
return canonical || legacy;
|
|
632
|
+
}
|
|
633
|
+
function isIssueReference(value) {
|
|
634
|
+
return isSourceReference(value) && Number.isSafeInteger(value.issueIid) && value.issueIid > 0;
|
|
635
|
+
}
|
|
636
|
+
function isMergeRequestReference(value) {
|
|
637
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
638
|
+
const ref = value;
|
|
639
|
+
return ref.version === 1 && typeof ref.host === "string" && normalizeGitLabHost(ref.host).length > 0 && Number.isSafeInteger(ref.projectId) && ref.projectId > 0 && Number.isSafeInteger(ref.mergeRequestIid) && ref.mergeRequestIid > 0;
|
|
640
|
+
}
|
|
641
|
+
function encodePageCursor(cursor) {
|
|
642
|
+
return encodeOpaque(cursor);
|
|
643
|
+
}
|
|
644
|
+
function decodePageCursor(value) {
|
|
645
|
+
if (!value) return {
|
|
646
|
+
source: 0,
|
|
647
|
+
page: 1
|
|
648
|
+
};
|
|
649
|
+
try {
|
|
650
|
+
const decoded = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
651
|
+
if (!Number.isSafeInteger(decoded.source) || Number(decoded.source) < 0) return null;
|
|
652
|
+
if (!Number.isSafeInteger(decoded.page) || Number(decoded.page) < 1) return null;
|
|
653
|
+
return {
|
|
654
|
+
source: Number(decoded.source),
|
|
655
|
+
page: Number(decoded.page)
|
|
656
|
+
};
|
|
657
|
+
} catch {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function displayName(user) {
|
|
662
|
+
return user?.name?.trim() || user?.username || null;
|
|
663
|
+
}
|
|
664
|
+
function parsePositiveInteger(value) {
|
|
665
|
+
if (!/^\d+$/.test(value)) return null;
|
|
666
|
+
const parsed = Number(value);
|
|
667
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
668
|
+
}
|
|
669
|
+
function parseIssueLocator(value) {
|
|
670
|
+
const shorthand = value.match(/^(.+?)#(\d+)$/);
|
|
671
|
+
if (shorthand) {
|
|
672
|
+
const issueIid = parsePositiveInteger(shorthand[2]);
|
|
673
|
+
const projectPath = shorthand[1].replace(/^\/+|\/+$/g, "");
|
|
674
|
+
return issueIid && projectPath ? {
|
|
675
|
+
projectPath,
|
|
676
|
+
issueIid
|
|
677
|
+
} : null;
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const url = new URL(value);
|
|
681
|
+
const match = url.pathname.match(/^\/(.+)\/-\/issues\/(\d+)\/?$/);
|
|
682
|
+
if (!match) return null;
|
|
683
|
+
const issueIid = parsePositiveInteger(match[2]);
|
|
684
|
+
if (!issueIid) return null;
|
|
685
|
+
const projectPath = decodeURIComponent(match[1]);
|
|
686
|
+
return {
|
|
687
|
+
host: url.host,
|
|
688
|
+
projectPath,
|
|
689
|
+
issueIid
|
|
690
|
+
};
|
|
691
|
+
} catch {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
function targetStateEvent(input) {
|
|
696
|
+
if (input.state.kind === "byType") return input.state.stateType === "completed" || input.state.stateType === "canceled" ? "close" : "reopen";
|
|
697
|
+
const name = input.state.name.trim().toLowerCase();
|
|
698
|
+
if (name === "closed" || name === "close") return "close";
|
|
699
|
+
if (name === "opened" || name === "open" || name === "reopen") return "reopen";
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
703
|
+
export { GITLAB_TRUSTED_ACCESS_LEVEL, GitLabIntegration, GitLabIntegrationBase, decodeIssueReference, decodeMergeRequestReference, decodeSourceId, encodeIssueReference, encodeSourceId, gitlabConnection, normalizeGitLabHost };
|
|
704
|
+
|
|
705
|
+
//# sourceMappingURL=integration.js.map
|