(ctx: SubscriptionContext)
| 110 | }, |
| 111 | |
| 112 | async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> { |
| 113 | const config = getProviderConfig(ctx.webhook) |
| 114 | const accessToken = config.accessToken as string | undefined |
| 115 | const projectId = config.projectId as string | undefined |
| 116 | const triggerId = config.triggerId as string | undefined |
| 117 | const host = config.host as string | undefined |
| 118 | |
| 119 | if (!accessToken) |
| 120 | throw new Error('GitLab Personal Access Token is required to create the webhook.') |
| 121 | if (!projectId) throw new Error('GitLab Project ID is required to create the webhook.') |
| 122 | |
| 123 | // Validate the optional self-managed host up front so a structurally unsafe |
| 124 | // value surfaces as a clear error instead of an unhandled UnsafeGitLabHostError. |
| 125 | try { |
| 126 | getGitLabApiBase(host) |
| 127 | } catch (error) { |
| 128 | if (error instanceof UnsafeGitLabHostError) { |
| 129 | throw new Error( |
| 130 | 'GitLab host is invalid. Provide a domain like gitlab.example.com (no protocol, path, or credentials).' |
| 131 | ) |
| 132 | } |
| 133 | throw error |
| 134 | } |
| 135 | |
| 136 | const { getGitLabEventFlags } = await import('@/triggers/gitlab/utils') |
| 137 | const secretToken = generateId() |
| 138 | const res = await secureFetchWithValidation(gitlabProjectHooksUrl(projectId, host), { |
| 139 | method: 'POST', |
| 140 | headers: { 'PRIVATE-TOKEN': accessToken, 'Content-Type': 'application/json' }, |
| 141 | body: JSON.stringify({ |
| 142 | url: getNotificationUrl(ctx.webhook), |
| 143 | token: secretToken, |
| 144 | enable_ssl_verification: true, |
| 145 | ...getGitLabEventFlags(triggerId ?? 'gitlab_webhook'), |
| 146 | }), |
| 147 | }) |
| 148 | |
| 149 | if (!res.ok) { |
| 150 | const detail = await res.text().catch(() => '') |
| 151 | logger.error(`[${ctx.requestId}] Failed to create GitLab webhook (${res.status})`, { detail }) |
| 152 | if (res.status === 401) |
| 153 | throw new Error( |
| 154 | 'GitLab authentication failed. Verify your Personal Access Token has the api scope.' |
| 155 | ) |
| 156 | if (res.status === 403) |
| 157 | throw new Error( |
| 158 | 'GitLab access denied. You need the Maintainer or Owner role on the project.' |
| 159 | ) |
| 160 | if (res.status === 404) throw new Error('GitLab project not found. Verify the Project ID.') |
| 161 | throw new Error(`Failed to create GitLab webhook: ${res.status}`) |
| 162 | } |
| 163 | |
| 164 | const created = (await res.json().catch(() => ({}))) as { id?: number | string } |
| 165 | if (created.id === undefined || created.id === null) { |
| 166 | // The hook was created but we can't read its id — delete it by URL so it |
| 167 | // is not orphaned in GitLab. |
| 168 | await cleanupGitLabHookByUrl(projectId, accessToken, getNotificationUrl(ctx.webhook), host) |
| 169 | throw new Error('GitLab webhook created but no hook ID was returned.') |
nothing calls this directly
no test coverage detected