@ric_/forgejo-mcp 0.1.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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +302 -0
  3. package/dist/client.d.ts +35 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +107 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/config.d.ts +10 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +86 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/http.d.ts +3 -0
  12. package/dist/http.d.ts.map +1 -0
  13. package/dist/http.js +108 -0
  14. package/dist/http.js.map +1 -0
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +16 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/server.d.ts +4 -0
  20. package/dist/server.d.ts.map +1 -0
  21. package/dist/server.js +27 -0
  22. package/dist/server.js.map +1 -0
  23. package/dist/tools/admin.d.ts +4 -0
  24. package/dist/tools/admin.d.ts.map +1 -0
  25. package/dist/tools/admin.js +261 -0
  26. package/dist/tools/admin.js.map +1 -0
  27. package/dist/tools/helpers.d.ts +16 -0
  28. package/dist/tools/helpers.d.ts.map +1 -0
  29. package/dist/tools/helpers.js +30 -0
  30. package/dist/tools/helpers.js.map +1 -0
  31. package/dist/tools/issues.d.ts +4 -0
  32. package/dist/tools/issues.d.ts.map +1 -0
  33. package/dist/tools/issues.js +337 -0
  34. package/dist/tools/issues.js.map +1 -0
  35. package/dist/tools/organizations.d.ts +4 -0
  36. package/dist/tools/organizations.d.ts.map +1 -0
  37. package/dist/tools/organizations.js +232 -0
  38. package/dist/tools/organizations.js.map +1 -0
  39. package/dist/tools/pullrequests.d.ts +4 -0
  40. package/dist/tools/pullrequests.d.ts.map +1 -0
  41. package/dist/tools/pullrequests.js +251 -0
  42. package/dist/tools/pullrequests.js.map +1 -0
  43. package/dist/tools/repository.d.ts +4 -0
  44. package/dist/tools/repository.d.ts.map +1 -0
  45. package/dist/tools/repository.js +437 -0
  46. package/dist/tools/repository.js.map +1 -0
  47. package/dist/tools/users.d.ts +4 -0
  48. package/dist/tools/users.d.ts.map +1 -0
  49. package/dist/tools/users.js +204 -0
  50. package/dist/tools/users.js.map +1 -0
  51. package/dist/validation.d.ts +35 -0
  52. package/dist/validation.d.ts.map +1 -0
  53. package/dist/validation.js +68 -0
  54. package/dist/validation.js.map +1 -0
  55. package/package.json +49 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 forgejo-mcp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,302 @@
1
+ # forgejo-mcp
2
+
3
+ A Model Context Protocol (MCP) server for [Forgejo](https://forgejo.org/) and [Gitea](https://gitea.com/) instances. Enables AI assistants like Claude, Cursor, and other MCP-compatible tools to interact with your Forgejo/Gitea repositories, issues, pull requests, and more.
4
+
5
+ ## Features
6
+ - Comprehensive API coverage (102 tools across 6 categories)
7
+ - Configurable base URL - works with any Forgejo or Gitea instance
8
+ - Both stdio and HTTP transport modes
9
+ - Token-based authentication with optional HTTP Bearer auth
10
+ - Input validation and security hardening (path traversal protection, SSRF prevention, rate limiting)
11
+ - Docker support with security-hardened container
12
+
13
+ ## Quick Start
14
+
15
+ ### Prerequisites
16
+ - Node.js 18+
17
+ - A Forgejo or Gitea instance
18
+ - API token (generate at `{your-instance}/user/settings/applications`)
19
+
20
+ ### Installation
21
+
22
+ ```bash
23
+ npm install -g forgejo-mcp
24
+ ```
25
+
26
+ Or run directly:
27
+ ```bash
28
+ npx forgejo-mcp
29
+ ```
30
+
31
+ ### Configuration
32
+
33
+ Set environment variables:
34
+ ```bash
35
+ export FORGEJO_URL=https://your-forgejo-instance.com
36
+ export FORGEJO_TOKEN=your-api-token
37
+ ```
38
+
39
+ Or pass as CLI args:
40
+ ```bash
41
+ forgejo-mcp --url https://your-instance.com --token your-token
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ### With Claude Code
47
+ Add to your Claude Code MCP config:
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "forgejo": {
52
+ "command": "npx",
53
+ "args": ["forgejo-mcp"],
54
+ "env": {
55
+ "FORGEJO_URL": "https://your-instance.com",
56
+ "FORGEJO_TOKEN": "your-token"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### With Claude Desktop
64
+ Add to claude_desktop_config.json:
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "forgejo": {
69
+ "command": "npx",
70
+ "args": ["forgejo-mcp"],
71
+ "env": {
72
+ "FORGEJO_URL": "https://your-instance.com",
73
+ "FORGEJO_TOKEN": "your-token"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### HTTP Mode
81
+ For remote/shared access:
82
+ ```bash
83
+ FORGEJO_URL=https://your-instance.com \
84
+ FORGEJO_TOKEN=your-token \
85
+ FORGEJO_MCP_API_KEY=your-secret-api-key \
86
+ npx forgejo-mcp-http --port 3000
87
+ ```
88
+ Endpoint: `http://localhost:3000/mcp`
89
+
90
+ **Authentication:** Set `FORGEJO_MCP_API_KEY` to require Bearer token authentication on the HTTP endpoint. Clients must include `Authorization: Bearer your-secret-api-key` in requests. If not set, the endpoint is unauthenticated (only suitable for localhost or behind a reverse proxy).
91
+
92
+ **Rate Limiting:** Enabled by default at 100 requests/minute per IP. Configure via:
93
+ - `RATE_LIMIT_MAX` - max requests per window (default: 100)
94
+ - `RATE_LIMIT_WINDOW_MS` - window size in milliseconds (default: 60000)
95
+
96
+ ### Docker
97
+ ```bash
98
+ # Build and run with docker-compose
99
+ cp .env.example .env
100
+ # Edit .env with your values, then:
101
+ docker compose up -d
102
+ ```
103
+
104
+ Or build and run directly:
105
+ ```bash
106
+ docker build -t forgejo-mcp .
107
+ docker run -p 3000:3000 \
108
+ -e FORGEJO_URL=https://your-instance.com \
109
+ -e FORGEJO_TOKEN=your-token \
110
+ -e FORGEJO_MCP_API_KEY=your-secret-key \
111
+ forgejo-mcp
112
+ ```
113
+
114
+ The Docker image:
115
+ - Uses multi-stage build for minimal image size
116
+ - Runs as non-root user
117
+ - Read-only filesystem
118
+ - No new privileges security option
119
+
120
+ ## Available Tools
121
+
122
+ ### Repository Management (24 tools)
123
+ | Tool | Description |
124
+ |------|-------------|
125
+ | `search_repos` | Search repositories |
126
+ | `get_repo` | Get repository details |
127
+ | `create_repo` | Create a new repository |
128
+ | `create_org_repo` | Create repo in an organization |
129
+ | `delete_repo` | Delete a repository |
130
+ | `fork_repo` | Fork a repository |
131
+ | `list_branches` | List branches |
132
+ | `get_branch` | Get branch details |
133
+ | `create_branch` | Create a branch |
134
+ | `delete_branch` | Delete a branch |
135
+ | `list_repo_commits` | List commits |
136
+ | `get_file_contents` | Get file contents |
137
+ | `create_file` | Create a file |
138
+ | `update_file` | Update a file |
139
+ | `delete_file` | Delete a file |
140
+ | `list_releases` | List releases |
141
+ | `create_release` | Create a release |
142
+ | `list_tags` | List tags |
143
+ | `list_repo_topics` | List topics |
144
+ | `update_repo_topics` | Update topics |
145
+ | `list_forks` | List forks |
146
+ | `list_collaborators` | List collaborators |
147
+ | `add_collaborator` | Add a collaborator |
148
+ | `transfer_repo` | Transfer repository |
149
+
150
+ ### Issue Management (20 tools)
151
+ | Tool | Description |
152
+ |------|-------------|
153
+ | `list_issues` | List repository issues |
154
+ | `get_issue` | Get issue details |
155
+ | `create_issue` | Create an issue |
156
+ | `edit_issue` | Edit an issue |
157
+ | `list_issue_comments` | List issue comments |
158
+ | `create_issue_comment` | Add a comment |
159
+ | `edit_issue_comment` | Edit a comment |
160
+ | `delete_issue_comment` | Delete a comment |
161
+ | `list_labels` | List repository labels |
162
+ | `get_label` | Get label details |
163
+ | `create_label` | Create a label |
164
+ | `edit_label` | Edit a label |
165
+ | `delete_label` | Delete a label |
166
+ | `add_issue_labels` | Add labels to issue |
167
+ | `remove_issue_label` | Remove label from issue |
168
+ | `list_milestones` | List milestones |
169
+ | `get_milestone` | Get milestone details |
170
+ | `create_milestone` | Create a milestone |
171
+ | `edit_milestone` | Edit a milestone |
172
+ | `delete_milestone` | Delete a milestone |
173
+
174
+ ### Pull Request Management (12 tools)
175
+ | Tool | Description |
176
+ |------|-------------|
177
+ | `list_pull_requests` | List pull requests |
178
+ | `get_pull_request` | Get PR details |
179
+ | `create_pull_request` | Create a pull request |
180
+ | `edit_pull_request` | Edit a pull request |
181
+ | `merge_pull_request` | Merge a pull request |
182
+ | `list_pr_commits` | List PR commits |
183
+ | `list_pr_files` | List changed files |
184
+ | `get_pr_diff` | Get PR diff |
185
+ | `list_pr_reviews` | List PR reviews |
186
+ | `create_pr_review` | Create a review |
187
+ | `request_pr_review` | Request reviewers |
188
+ | `update_pr_branch` | Update PR branch |
189
+
190
+ ### Organization Management (14 tools)
191
+ | Tool | Description |
192
+ |------|-------------|
193
+ | `list_orgs` | List organizations |
194
+ | `get_org` | Get org details |
195
+ | `create_org` | Create organization |
196
+ | `edit_org` | Edit organization |
197
+ | `delete_org` | Delete organization |
198
+ | `list_org_repos` | List org repositories |
199
+ | `list_org_members` | List org members |
200
+ | `list_org_teams` | List org teams |
201
+ | `get_team` | Get team details |
202
+ | `create_team` | Create a team |
203
+ | `add_team_member` | Add team member |
204
+ | `remove_team_member` | Remove team member |
205
+ | `list_org_labels` | List org labels |
206
+ | `list_org_hooks` | List org webhooks |
207
+
208
+ ### User Management (13 tools)
209
+ | Tool | Description |
210
+ |------|-------------|
211
+ | `get_authenticated_user` | Get current user |
212
+ | `get_user` | Get user profile |
213
+ | `list_user_repos` | List user repositories |
214
+ | `list_user_orgs` | List user organizations |
215
+ | `search_users` | Search users |
216
+ | `list_followers` | List followers |
217
+ | `list_following` | List following |
218
+ | `list_user_starred` | List starred repos |
219
+ | `list_my_starred` | List my starred repos |
220
+ | `star_repo` | Star a repository |
221
+ | `unstar_repo` | Unstar a repository |
222
+ | `list_my_notifications` | List notifications |
223
+ | `mark_notifications_read` | Mark all as read |
224
+
225
+ ### Admin & System (19 tools)
226
+ | Tool | Description |
227
+ |------|-------------|
228
+ | `admin_list_users` | List all users (admin) |
229
+ | `admin_create_user` | Create user (admin) |
230
+ | `admin_delete_user` | Delete user (admin) |
231
+ | `admin_edit_user` | Edit user (admin) |
232
+ | `admin_list_cron_jobs` | List cron jobs |
233
+ | `admin_run_cron_job` | Run cron task |
234
+ | `admin_list_hooks` | List system webhooks |
235
+ | `get_server_version` | Get server version |
236
+ | `render_markdown` | Render markdown |
237
+ | `render_markup` | Render markup |
238
+ | `list_gitignore_templates` | List gitignore templates |
239
+ | `get_gitignore_template` | Get gitignore template |
240
+ | `list_license_templates` | List license templates |
241
+ | `get_license_template` | Get license template |
242
+ | `list_label_templates` | List label templates |
243
+ | `get_label_template` | Get label template |
244
+ | `get_nodeinfo` | Get instance info |
245
+ | `list_action_runners_jobs` | List action jobs |
246
+ | `get_runner_registration_token` | Get runner token |
247
+
248
+ ## Security
249
+
250
+ ### Environment Variables
251
+
252
+ | Variable | Required | Description |
253
+ |----------|----------|-------------|
254
+ | `FORGEJO_URL` | Yes | Base URL of your Forgejo/Gitea instance |
255
+ | `FORGEJO_TOKEN` | Yes | API token ([generate here]({your-instance}/user/settings/applications)) |
256
+ | `FORGEJO_MCP_API_KEY` | No | Bearer token for HTTP endpoint authentication |
257
+ | `RATE_LIMIT_MAX` | No | Max requests per rate limit window (default: 100) |
258
+ | `RATE_LIMIT_WINDOW_MS` | No | Rate limit window in ms (default: 60000) |
259
+ | `PORT` | No | HTTP server port (default: 3000) |
260
+
261
+ ### Security Features
262
+ - **Input validation** - All parameters validated with Zod schemas (path traversal prevention, regex-validated usernames, bounded pagination, enum enforcement)
263
+ - **SSRF protection** - Base URL validated against cloud metadata endpoints and private IP ranges
264
+ - **HTTP authentication** - Optional Bearer token auth for the HTTP transport
265
+ - **Rate limiting** - Per-IP rate limiting on HTTP endpoints
266
+ - **Token safety** - API tokens never leaked in error messages; URLs sanitized in errors
267
+ - **Security headers** - `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`
268
+ - **Non-root Docker** - Container runs as unprivileged user with read-only filesystem
269
+
270
+ ### Best Practices
271
+ - Always use HTTPS for your Forgejo instance URL
272
+ - Use short-lived API tokens with minimal required permissions
273
+ - Set `FORGEJO_MCP_API_KEY` when running HTTP mode on a network
274
+ - Admin tools require an admin-level Forgejo token - use a non-admin token if you don't need them
275
+
276
+ ## Development
277
+
278
+ ```bash
279
+ git clone https://github.com/your-username/forgejo-mcp.git
280
+ cd forgejo-mcp
281
+ npm install
282
+ npm run dev # stdio mode
283
+ npm run dev:http # HTTP mode
284
+ npm test # run tests
285
+ npm run build # compile TypeScript
286
+ ```
287
+
288
+ ## Compatible Instances
289
+
290
+ This MCP server works with:
291
+ - [Forgejo](https://forgejo.org/) (v7.0+)
292
+ - [Gitea](https://gitea.com/) (v1.20+)
293
+ - [Codeberg](https://codeberg.org/)
294
+ - Any Gitea-compatible forge
295
+
296
+ ## Contributing
297
+
298
+ Contributions welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
299
+
300
+ ## License
301
+
302
+ MIT - see [LICENSE](LICENSE)
@@ -0,0 +1,35 @@
1
+ import type { ForgejoConfig } from "./config.js";
2
+ export interface PaginationParams {
3
+ page?: number;
4
+ limit?: number;
5
+ }
6
+ export interface ApiError {
7
+ message: string;
8
+ url: string;
9
+ status: number;
10
+ }
11
+ export declare class ForgejoClient {
12
+ private baseUrl;
13
+ private token;
14
+ constructor(config: ForgejoConfig);
15
+ private get apiBase();
16
+ private headers;
17
+ /** Strip sensitive information from URLs for error messages */
18
+ private sanitizeUrl;
19
+ request<T>(method: string, path: string, options?: {
20
+ body?: unknown;
21
+ params?: Record<string, string | number | boolean | undefined>;
22
+ }): Promise<T>;
23
+ get<T>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
24
+ post<T>(path: string, body?: unknown, params?: Record<string, string | number | boolean | undefined>): Promise<T>;
25
+ patch<T>(path: string, body?: unknown): Promise<T>;
26
+ put<T>(path: string, body?: unknown): Promise<T>;
27
+ delete<T = void>(path: string): Promise<T>;
28
+ getRaw(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<string>;
29
+ }
30
+ export declare class ForgejoApiError extends Error {
31
+ readonly url: string;
32
+ readonly status: number;
33
+ constructor(message: string, url: string, status: number);
34
+ }
35
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAAS;gBAEV,MAAM,EAAE,aAAa;IAKjC,OAAO,KAAK,OAAO,GAElB;IAED,OAAO,CAAC,OAAO;IASf,+DAA+D;IAC/D,OAAO,CAAC,WAAW;IAUb,OAAO,CAAC,CAAC,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QACR,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;KAChE,GACA,OAAO,CAAC,CAAC,CAAC;IAmCP,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhG,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAIjH,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIlD,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhD,MAAM,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAI1C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;CAwB5G;AAED,qBAAa,eAAgB,SAAQ,KAAK;aAGtB,GAAG,EAAE,MAAM;aACX,MAAM,EAAE,MAAM;gBAF9B,OAAO,EAAE,MAAM,EACC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM;CAKjC"}
package/dist/client.js ADDED
@@ -0,0 +1,107 @@
1
+ export class ForgejoClient {
2
+ baseUrl;
3
+ token;
4
+ constructor(config) {
5
+ this.baseUrl = config.baseUrl;
6
+ this.token = config.token;
7
+ }
8
+ get apiBase() {
9
+ return `${this.baseUrl}/api/v1`;
10
+ }
11
+ headers(extra) {
12
+ return {
13
+ Authorization: `token ${this.token}`,
14
+ "Content-Type": "application/json",
15
+ Accept: "application/json",
16
+ ...extra,
17
+ };
18
+ }
19
+ /** Strip sensitive information from URLs for error messages */
20
+ sanitizeUrl(url) {
21
+ try {
22
+ const parsed = new URL(url);
23
+ // Remove any auth info from the URL; keep path + search for debugging
24
+ return `${parsed.origin}${parsed.pathname}${parsed.search ? "?[params]" : ""}`;
25
+ }
26
+ catch {
27
+ return "[invalid URL]";
28
+ }
29
+ }
30
+ async request(method, path, options) {
31
+ const url = new URL(`${this.apiBase}${path}`);
32
+ if (options?.params) {
33
+ for (const [key, value] of Object.entries(options.params)) {
34
+ if (value !== undefined) {
35
+ url.searchParams.set(key, String(value));
36
+ }
37
+ }
38
+ }
39
+ const response = await fetch(url.toString(), {
40
+ method,
41
+ headers: this.headers(),
42
+ body: options?.body ? JSON.stringify(options.body) : undefined,
43
+ });
44
+ if (!response.ok) {
45
+ let message;
46
+ try {
47
+ const err = (await response.json());
48
+ message = err.message || response.statusText;
49
+ }
50
+ catch {
51
+ message = response.statusText;
52
+ }
53
+ throw new ForgejoApiError(message, this.sanitizeUrl(url.toString()), response.status);
54
+ }
55
+ if (response.status === 204) {
56
+ return undefined;
57
+ }
58
+ return (await response.json());
59
+ }
60
+ async get(path, params) {
61
+ return this.request("GET", path, { params });
62
+ }
63
+ async post(path, body, params) {
64
+ return this.request("POST", path, { body, params });
65
+ }
66
+ async patch(path, body) {
67
+ return this.request("PATCH", path, { body });
68
+ }
69
+ async put(path, body) {
70
+ return this.request("PUT", path, { body });
71
+ }
72
+ async delete(path) {
73
+ return this.request("DELETE", path);
74
+ }
75
+ async getRaw(path, params) {
76
+ const url = new URL(`${this.apiBase}${path}`);
77
+ if (params) {
78
+ for (const [key, value] of Object.entries(params)) {
79
+ if (value !== undefined) {
80
+ url.searchParams.set(key, String(value));
81
+ }
82
+ }
83
+ }
84
+ const response = await fetch(url.toString(), {
85
+ method: "GET",
86
+ headers: {
87
+ Authorization: `token ${this.token}`,
88
+ Accept: "text/plain",
89
+ },
90
+ });
91
+ if (!response.ok) {
92
+ throw new ForgejoApiError(response.statusText, this.sanitizeUrl(url.toString()), response.status);
93
+ }
94
+ return response.text();
95
+ }
96
+ }
97
+ export class ForgejoApiError extends Error {
98
+ url;
99
+ status;
100
+ constructor(message, url, status) {
101
+ super(`Forgejo API error (${status}): ${message}`);
102
+ this.url = url;
103
+ this.status = status;
104
+ this.name = "ForgejoApiError";
105
+ }
106
+ }
107
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export interface ForgejoConfig {
2
+ baseUrl: string;
3
+ token: string;
4
+ }
5
+ export declare function loadConfig(): ForgejoConfig;
6
+ export declare function parseCliArgs(args: string[]): Partial<ForgejoConfig> & {
7
+ port?: number;
8
+ };
9
+ export declare function resolveConfig(cliArgs?: Partial<ForgejoConfig>): ForgejoConfig;
10
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAiDD,wBAAgB,UAAU,IAAI,aAAa,CAuB1C;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAkBvF;AAED,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CAuB7E"}
package/dist/config.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Validate the base URL for safety.
3
+ * Blocks private IPs, metadata endpoints, and non-HTTP protocols.
4
+ */
5
+ function validateBaseUrl(url) {
6
+ let parsed;
7
+ try {
8
+ parsed = new URL(url);
9
+ }
10
+ catch {
11
+ throw new Error(`Invalid FORGEJO_URL: "${url}" is not a valid URL`);
12
+ }
13
+ if (!["http:", "https:"].includes(parsed.protocol)) {
14
+ throw new Error("FORGEJO_URL must use http or https protocol");
15
+ }
16
+ const hostname = parsed.hostname.toLowerCase();
17
+ // Warn (not block) for HTTP - some dev instances use it
18
+ if (parsed.protocol === "http:" && hostname !== "localhost" && hostname !== "127.0.0.1") {
19
+ console.error("WARNING: FORGEJO_URL uses HTTP. Tokens will be sent unencrypted. Use HTTPS in production.");
20
+ }
21
+ // Block cloud metadata endpoints (SSRF)
22
+ if (hostname === "169.254.169.254" || hostname === "metadata.google.internal") {
23
+ throw new Error("FORGEJO_URL must not point to cloud metadata services");
24
+ }
25
+ // Block common internal ranges when hostname is an IP
26
+ const ipMatch = hostname.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
27
+ if (ipMatch) {
28
+ const [, a, b] = ipMatch.map(Number);
29
+ if (a === 10 ||
30
+ (a === 172 && b !== undefined && b >= 16 && b <= 31) ||
31
+ (a === 192 && b === 168) ||
32
+ a === 0) {
33
+ console.error("WARNING: FORGEJO_URL points to a private IP address. Ensure this is intentional.");
34
+ }
35
+ }
36
+ }
37
+ export function loadConfig() {
38
+ const baseUrl = process.env.FORGEJO_URL;
39
+ const token = process.env.FORGEJO_TOKEN;
40
+ if (!baseUrl) {
41
+ throw new Error("FORGEJO_URL environment variable is required. Set it to your Forgejo/Gitea instance URL (e.g., https://codeberg.org)");
42
+ }
43
+ if (!token) {
44
+ throw new Error("FORGEJO_TOKEN environment variable is required. Generate one at {your-instance}/user/settings/applications");
45
+ }
46
+ const cleanUrl = baseUrl.replace(/\/+$/, "");
47
+ validateBaseUrl(cleanUrl);
48
+ return {
49
+ baseUrl: cleanUrl,
50
+ token,
51
+ };
52
+ }
53
+ export function parseCliArgs(args) {
54
+ const result = {};
55
+ for (let i = 0; i < args.length; i++) {
56
+ switch (args[i]) {
57
+ case "--url":
58
+ result.baseUrl = args[++i]?.replace(/\/+$/, "");
59
+ break;
60
+ case "--token":
61
+ result.token = args[++i];
62
+ break;
63
+ case "--port":
64
+ result.port = parseInt(args[++i], 10);
65
+ break;
66
+ }
67
+ }
68
+ return result;
69
+ }
70
+ export function resolveConfig(cliArgs) {
71
+ const baseUrl = cliArgs?.baseUrl || process.env.FORGEJO_URL;
72
+ const token = cliArgs?.token || process.env.FORGEJO_TOKEN;
73
+ if (!baseUrl) {
74
+ throw new Error("Forgejo URL is required. Set FORGEJO_URL env var or pass --url <url>");
75
+ }
76
+ if (!token) {
77
+ throw new Error("Forgejo token is required. Set FORGEJO_TOKEN env var or pass --token <token>");
78
+ }
79
+ const cleanUrl = baseUrl.replace(/\/+$/, "");
80
+ validateBaseUrl(cleanUrl);
81
+ return {
82
+ baseUrl: cleanUrl,
83
+ token,
84
+ };
85
+ }
86
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":""}