@ticatec/omniflow-gitlab 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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/README_CN.md +95 -0
  4. package/dist/artifacts/GitLabContainerPublisher.d.ts +17 -0
  5. package/dist/artifacts/GitLabContainerPublisher.d.ts.map +1 -0
  6. package/dist/artifacts/GitLabContainerPublisher.js +44 -0
  7. package/dist/artifacts/GitLabContainerPublisher.js.map +1 -0
  8. package/dist/artifacts/GitLabGenericPublisher.d.ts +27 -0
  9. package/dist/artifacts/GitLabGenericPublisher.d.ts.map +1 -0
  10. package/dist/artifacts/GitLabGenericPublisher.js +176 -0
  11. package/dist/artifacts/GitLabGenericPublisher.js.map +1 -0
  12. package/dist/artifacts/GitLabMavenPublisher.d.ts +24 -0
  13. package/dist/artifacts/GitLabMavenPublisher.d.ts.map +1 -0
  14. package/dist/artifacts/GitLabMavenPublisher.js +185 -0
  15. package/dist/artifacts/GitLabMavenPublisher.js.map +1 -0
  16. package/dist/artifacts/GitLabNpmPublisher.d.ts +20 -0
  17. package/dist/artifacts/GitLabNpmPublisher.d.ts.map +1 -0
  18. package/dist/artifacts/GitLabNpmPublisher.js +68 -0
  19. package/dist/artifacts/GitLabNpmPublisher.js.map +1 -0
  20. package/dist/client/GitLabClient.d.ts +59 -0
  21. package/dist/client/GitLabClient.d.ts.map +1 -0
  22. package/dist/client/GitLabClient.js +169 -0
  23. package/dist/client/GitLabClient.js.map +1 -0
  24. package/dist/index.d.ts +29 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +41 -0
  27. package/dist/index.js.map +1 -0
  28. package/package.json +58 -0
  29. package/src/artifacts/GitLabContainerPublisher.ts +50 -0
  30. package/src/artifacts/GitLabGenericPublisher.ts +221 -0
  31. package/src/artifacts/GitLabMavenPublisher.ts +224 -0
  32. package/src/artifacts/GitLabNpmPublisher.ts +90 -0
  33. package/src/client/GitLabClient.ts +200 -0
  34. package/src/index.ts +58 -0
@@ -0,0 +1,200 @@
1
+ import { safeFetch, maskSecrets, extractErrorMessage } from '@ticatec/omniflow-artifact'
2
+
3
+ export interface GitLabClientConfig {
4
+ baseUrl: string
5
+ token?: string
6
+ username?: string
7
+ password?: string
8
+ timeoutMs?: number
9
+ }
10
+
11
+ export interface ParsedGitLabRepo {
12
+ host: string
13
+ owner: string
14
+ repo: string
15
+ projectPath: string
16
+ }
17
+
18
+ /**
19
+ * Detect whether a hostname is official GitLab SaaS or a valid self-hosted GitLab instance,
20
+ * strictly rejecting spoofed hostnames (e.g. gitlab.com.evil.net, gitlab.com.cn).
21
+ */
22
+ export function isGitLabHost(hostname: string): boolean {
23
+ const host = hostname.toLowerCase()
24
+ if (host === 'gitlab.com' || host === 'registry.gitlab.com' || host.endsWith('.gitlab.com')) {
25
+ return true
26
+ }
27
+ const segments = host.split('.')
28
+ // Reject subdomain spoofing where "gitlab.com" appears as subdomains of a foreign host
29
+ const gitlabIdx = segments.indexOf('gitlab')
30
+ if (gitlabIdx !== -1 && segments[gitlabIdx + 1] === 'com') {
31
+ return false
32
+ }
33
+ return segments.includes('gitlab')
34
+ }
35
+
36
+ export class GitLabClient {
37
+ readonly baseUrl: string
38
+ readonly token?: string
39
+ readonly username?: string
40
+ readonly password?: string
41
+ readonly timeoutMs: number
42
+
43
+ constructor(config: GitLabClientConfig) {
44
+ let base = config.baseUrl.trim().replace(/\/+$/, '')
45
+ if (!/^https?:\/\//i.test(base)) {
46
+ base = `https://${base}`
47
+ }
48
+ this.baseUrl = base
49
+ this.token = config.token
50
+ this.username = config.username
51
+ this.password = config.password
52
+ this.timeoutMs = config.timeoutMs ?? 30000
53
+ }
54
+
55
+ get host(): string {
56
+ const parsed = new URL(this.baseUrl)
57
+ return parsed.host
58
+ }
59
+
60
+ get hostname(): string {
61
+ const parsed = new URL(this.baseUrl)
62
+ return parsed.hostname
63
+ }
64
+
65
+ /**
66
+ * Return sensitive credential strings of length >= 3 for scrubbing.
67
+ * Username is intentionally excluded to preserve debugging visibility.
68
+ */
69
+ getSecrets(): string[] {
70
+ const secrets = [this.token, this.password].filter(
71
+ (s): s is string => typeof s === 'string' && s.length >= 3
72
+ )
73
+ if (this.username && this.password) {
74
+ const basic = Buffer.from(`${this.username}:${this.password}`).toString('base64')
75
+ if (basic.length >= 3) {
76
+ secrets.push(basic)
77
+ }
78
+ }
79
+ return secrets
80
+ }
81
+
82
+ /**
83
+ * Mask secrets in text using shared utility.
84
+ */
85
+ maskSecrets(text: string): string {
86
+ return maskSecrets(text, this.getSecrets())
87
+ }
88
+
89
+ getAuthHeaders(): Record<string, string> {
90
+ const headers: Record<string, string> = {
91
+ Accept: 'application/json'
92
+ }
93
+ if (this.token) {
94
+ headers['PRIVATE-TOKEN'] = this.token
95
+ } else if (this.username && this.password) {
96
+ const basic = Buffer.from(`${this.username}:${this.password}`).toString('base64')
97
+ headers['Authorization'] = `Basic ${basic}`
98
+ } else if (this.password) {
99
+ headers['PRIVATE-TOKEN'] = this.password
100
+ }
101
+ return headers
102
+ }
103
+
104
+ /**
105
+ * Return GitLab API v4 project endpoint URL with URL-encoded project path/ID.
106
+ */
107
+ getProjectApiUrl(projectPathOrId: string, subpath = ''): string {
108
+ const encoded = encodeURIComponent(projectPathOrId.replace(/^\/+/, '').replace(/\/+$/, ''))
109
+ const cleanSubpath = subpath ? (subpath.startsWith('/') ? subpath : `/${subpath}`) : ''
110
+ return `${this.baseUrl}/api/v4/projects/${encoded}${cleanSubpath}`
111
+ }
112
+
113
+ /**
114
+ * Return package registry endpoint for specific package type.
115
+ */
116
+ getPackageApiUrl(projectPathOrId: string, packageType: 'npm' | 'maven' | 'generic', subpath = ''): string {
117
+ const projectUrl = this.getProjectApiUrl(projectPathOrId)
118
+ const cleanSubpath = subpath ? (subpath.startsWith('/') ? subpath : `/${subpath}`) : ''
119
+
120
+ switch (packageType) {
121
+ case 'npm':
122
+ // NPM registry endpoint must end with trailing slash
123
+ return `${projectUrl}/packages/npm/`
124
+ case 'maven':
125
+ return `${projectUrl}/packages/maven${cleanSubpath}`
126
+ case 'generic':
127
+ return `${projectUrl}/packages/generic${cleanSubpath}`
128
+ default:
129
+ throw new Error(`Unsupported GitLab package type: ${packageType}`)
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Standard HTTP request wrapper with timeout and credential masking.
135
+ */
136
+ async request(endpointOrUrl: string, init: RequestInit = {}): Promise<Response> {
137
+ const url = endpointOrUrl.startsWith('http://') || endpointOrUrl.startsWith('https://')
138
+ ? endpointOrUrl
139
+ : `${this.baseUrl}${endpointOrUrl.startsWith('/') ? endpointOrUrl : `/${endpointOrUrl}`}`
140
+
141
+ const headers = {
142
+ ...this.getAuthHeaders(),
143
+ ...(init.headers as Record<string, string> || {})
144
+ }
145
+
146
+ return await safeFetch(url, {
147
+ ...init,
148
+ headers,
149
+ timeoutMs: this.timeoutMs,
150
+ secrets: this.getSecrets()
151
+ })
152
+ }
153
+
154
+ /**
155
+ * Safely extract, truncate (max 500 chars), and mask error text from a response.
156
+ */
157
+ async extractErrorMessage(res: Response): Promise<string> {
158
+ return extractErrorMessage(res, this.getSecrets())
159
+ }
160
+
161
+ /**
162
+ * Parse repository URL or slug into host, owner, and repo.
163
+ */
164
+ static parseRepo(urlOrSlug: string, defaultHost = 'gitlab.com'): ParsedGitLabRepo {
165
+ let clean = urlOrSlug.trim()
166
+ let host = defaultHost
167
+
168
+ if (clean.includes('://')) {
169
+ const parsed = new URL(clean)
170
+ host = parsed.host
171
+ clean = decodeURIComponent(parsed.pathname)
172
+ } else if (clean.includes('@') && clean.includes(':')) {
173
+ const colonIdx = clean.indexOf(':')
174
+ const hostPart = clean.slice(0, colonIdx).replace(/.*@/, '')
175
+ let pathPart = clean.slice(colonIdx + 1)
176
+ const portMatch = pathPart.match(/^(\d+)\/(.*)/)
177
+ if (portMatch) {
178
+ host = `${hostPart}:${portMatch[1]}`
179
+ pathPart = portMatch[2]
180
+ } else {
181
+ host = hostPart
182
+ }
183
+ clean = decodeURIComponent(pathPart)
184
+ } else {
185
+ clean = decodeURIComponent(clean)
186
+ }
187
+
188
+ clean = clean.replace(/^\/+/, '').replace(/\.git$/, '')
189
+ const segments = clean.split('/').filter(Boolean)
190
+ if (segments.length < 2) {
191
+ throw new Error(`Invalid repository path: "${urlOrSlug}". Expected owner/repo format.`)
192
+ }
193
+
194
+ const owner = segments.slice(0, -1).join('/')
195
+ const repo = segments[segments.length - 1]
196
+ const projectPath = segments.join('/')
197
+
198
+ return { host, owner, repo, projectPath }
199
+ }
200
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { PluginContext } from '@ticatec/omniflow-core'
2
+ import type { ArtifactPublisherRegistry } from '@ticatec/omniflow-artifact'
3
+ import { GitLabContainerPublisher } from './artifacts/GitLabContainerPublisher.js'
4
+ import { GitLabNpmPublisher } from './artifacts/GitLabNpmPublisher.js'
5
+ import { GitLabMavenPublisher } from './artifacts/GitLabMavenPublisher.js'
6
+ import { GitLabGenericPublisher } from './artifacts/GitLabGenericPublisher.js'
7
+
8
+ export * from './client/GitLabClient.js'
9
+ export * from './artifacts/GitLabContainerPublisher.js'
10
+ export * from './artifacts/GitLabNpmPublisher.js'
11
+ export * from './artifacts/GitLabMavenPublisher.js'
12
+ export * from './artifacts/GitLabGenericPublisher.js'
13
+
14
+ export interface RegisterGitLabOptions {
15
+ artifactRegistry?: ArtifactPublisherRegistry
16
+ }
17
+
18
+ /**
19
+ * Standard OmniFlow Plugin SPI entry point.
20
+ */
21
+ export function register(ctx: PluginContext, options: Record<string, unknown> = {}): void {
22
+ const artifactsEnabled = options.artifacts !== false
23
+
24
+ registerGitLab({
25
+ artifactRegistry: artifactsEnabled ? ctx.artifactRegistry : undefined
26
+ })
27
+ }
28
+
29
+ export default register
30
+
31
+ /**
32
+ * Register all GitLab artifact publishers into the artifact registry.
33
+ */
34
+ export function registerGitLab(options: RegisterGitLabOptions = {}): {
35
+ containerPublisher: GitLabContainerPublisher
36
+ npmPublisher: GitLabNpmPublisher
37
+ mavenPublisher: GitLabMavenPublisher
38
+ genericPublisher: GitLabGenericPublisher
39
+ } {
40
+ const containerPublisher = new GitLabContainerPublisher()
41
+ const npmPublisher = new GitLabNpmPublisher()
42
+ const mavenPublisher = new GitLabMavenPublisher()
43
+ const genericPublisher = new GitLabGenericPublisher()
44
+
45
+ if (options.artifactRegistry) {
46
+ options.artifactRegistry.register(containerPublisher)
47
+ options.artifactRegistry.register(npmPublisher)
48
+ options.artifactRegistry.register(mavenPublisher)
49
+ options.artifactRegistry.register(genericPublisher)
50
+ }
51
+
52
+ return {
53
+ containerPublisher,
54
+ npmPublisher,
55
+ mavenPublisher,
56
+ genericPublisher
57
+ }
58
+ }