gitlab-mcp 0.1.5 → 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/.dockerignore +7 -0
- package/.editorconfig +9 -0
- package/.env.example +75 -0
- package/.github/workflows/nodejs.yml +31 -0
- package/.github/workflows/npm-publish.yml +31 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierrc.json +6 -0
- package/Dockerfile +20 -0
- package/README.md +416 -251
- package/docker-compose.yml +10 -0
- package/docs/architecture.md +310 -0
- package/docs/authentication.md +299 -0
- package/docs/configuration.md +149 -0
- package/docs/deployment.md +336 -0
- package/docs/tools.md +294 -0
- package/eslint.config.js +23 -0
- package/package.json +70 -32
- package/scripts/get-oauth-token.example.sh +15 -0
- package/src/config/env.ts +171 -0
- package/src/http.ts +605 -0
- package/src/index.ts +77 -0
- package/src/lib/auth-context.ts +19 -0
- package/src/lib/gitlab-client.ts +1810 -0
- package/src/lib/logger.ts +17 -0
- package/src/lib/network.ts +45 -0
- package/src/lib/oauth.ts +287 -0
- package/src/lib/output.ts +51 -0
- package/src/lib/policy.ts +78 -0
- package/src/lib/request-runtime.ts +376 -0
- package/src/lib/sanitize.ts +25 -0
- package/src/server/build-server.ts +17 -0
- package/src/tools/gitlab.ts +3128 -0
- package/src/tools/health.ts +27 -0
- package/src/tools/mr-code-context.ts +473 -0
- package/src/types/context.ts +13 -0
- package/tests/auth-context.test.ts +102 -0
- package/tests/gitlab-client.test.ts +674 -0
- package/tests/graphql-guard.test.ts +121 -0
- package/tests/integration/agent-loop.integration.test.ts +552 -0
- package/tests/integration/server.integration.test.ts +543 -0
- package/tests/mr-code-context.test.ts +600 -0
- package/tests/oauth.test.ts +43 -0
- package/tests/output.test.ts +186 -0
- package/tests/policy.test.ts +324 -0
- package/tests/request-runtime.test.ts +252 -0
- package/tests/sanitize.test.ts +123 -0
- package/tests/upload-reference.test.ts +84 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +12 -0
- package/LICENSE +0 -21
- package/build/index.js +0 -1642
- package/build/schemas.js +0 -684
- package/build/test-note.js +0 -54
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for helper functions exported or used internally by request-runtime.ts.
|
|
3
|
+
* Since some functions are private, we test them indirectly or test the module-level
|
|
4
|
+
* exported utilities that are accessible.
|
|
5
|
+
*
|
|
6
|
+
* For deeper testing we extract testable logic patterns.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Replicate the parseTokenOutput logic for testing.
|
|
14
|
+
* This matches the private function in request-runtime.ts.
|
|
15
|
+
*/
|
|
16
|
+
function parseTokenOutput(rawOutput: string): string | undefined {
|
|
17
|
+
const output = rawOutput.trim();
|
|
18
|
+
if (!output) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(output) as Record<string, unknown>;
|
|
24
|
+
const token =
|
|
25
|
+
getStringField(parsed, "token") ||
|
|
26
|
+
getStringField(parsed, "access_token") ||
|
|
27
|
+
getStringField(parsed, "private_token");
|
|
28
|
+
if (token) {
|
|
29
|
+
return token;
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Plain string output is valid.
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return output.split(/\r?\n/, 1)[0]?.trim() || undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getStringField(record: Record<string, unknown>, key: string): string | undefined {
|
|
39
|
+
const value = record[key];
|
|
40
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replicate resolveHomePath for testing.
|
|
45
|
+
*/
|
|
46
|
+
function resolveHomePath(input?: string): string | undefined {
|
|
47
|
+
if (!input) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (input.startsWith("~/")) {
|
|
52
|
+
return path.join(os.homedir(), input.slice(2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return input;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Replicate normalizeWarmupPath for testing.
|
|
60
|
+
*/
|
|
61
|
+
function normalizeWarmupPath(value: string): string {
|
|
62
|
+
const trimmed = value.trim();
|
|
63
|
+
if (!trimmed) {
|
|
64
|
+
return "/user";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Replicate resolveApiRoot for testing.
|
|
72
|
+
*/
|
|
73
|
+
function resolveApiRoot(url: URL): string | undefined {
|
|
74
|
+
const match = url.pathname.match(/^(.*\/api\/v4)(?:\/|$)/);
|
|
75
|
+
return match?.[1];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Replicate parseOauthScopes for testing.
|
|
80
|
+
*/
|
|
81
|
+
function parseOauthScopes(rawScopes: string): string[] {
|
|
82
|
+
return rawScopes
|
|
83
|
+
.split(/[,\s]+/)
|
|
84
|
+
.map((scope) => scope.trim())
|
|
85
|
+
.filter((scope) => scope.length > 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe("parseTokenOutput", () => {
|
|
89
|
+
it("returns undefined for empty string", () => {
|
|
90
|
+
expect(parseTokenOutput("")).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns undefined for whitespace-only string", () => {
|
|
94
|
+
expect(parseTokenOutput(" \n ")).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("parses plain text token", () => {
|
|
98
|
+
expect(parseTokenOutput("glpat-abc123")).toBe("glpat-abc123");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("parses plain text with trailing newline", () => {
|
|
102
|
+
expect(parseTokenOutput("glpat-abc123\n")).toBe("glpat-abc123");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("takes first line of multi-line output", () => {
|
|
106
|
+
expect(parseTokenOutput("glpat-abc123\nsome debug info\nmore stuff")).toBe("glpat-abc123");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("parses JSON with token field", () => {
|
|
110
|
+
expect(parseTokenOutput('{"token": "glpat-from-json"}')).toBe("glpat-from-json");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("parses JSON with access_token field", () => {
|
|
114
|
+
expect(parseTokenOutput('{"access_token": "oauth-token-123"}')).toBe("oauth-token-123");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("parses JSON with private_token field", () => {
|
|
118
|
+
expect(parseTokenOutput('{"private_token": "private-123"}')).toBe("private-123");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("prefers token field over access_token", () => {
|
|
122
|
+
expect(parseTokenOutput('{"token": "primary", "access_token": "secondary"}')).toBe("primary");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("ignores JSON with empty token fields", () => {
|
|
126
|
+
expect(parseTokenOutput('{"token": "", "other": "value"}')).toBe(
|
|
127
|
+
'{"token": "", "other": "value"}'
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("ignores JSON with whitespace-only token fields", () => {
|
|
132
|
+
expect(parseTokenOutput('{"token": " "}')).toBe('{"token": " "}');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("trims whitespace from parsed token values", () => {
|
|
136
|
+
expect(parseTokenOutput('{"token": " trimmed "}')).toBe("trimmed");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("handles JSON with no recognized token fields", () => {
|
|
140
|
+
const input = '{"unknown_field": "some-value"}';
|
|
141
|
+
// Falls back to first line
|
|
142
|
+
expect(parseTokenOutput(input)).toBe(input);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("resolveHomePath", () => {
|
|
147
|
+
it("returns undefined for empty input", () => {
|
|
148
|
+
expect(resolveHomePath("")).toBeUndefined();
|
|
149
|
+
expect(resolveHomePath(undefined)).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("expands ~ to home directory", () => {
|
|
153
|
+
const result = resolveHomePath("~/some/path");
|
|
154
|
+
expect(result).toBeDefined();
|
|
155
|
+
expect(result).not.toContain("~/");
|
|
156
|
+
expect(result).toContain("some/path");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("returns absolute paths unchanged", () => {
|
|
160
|
+
expect(resolveHomePath("/absolute/path")).toBe("/absolute/path");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("returns relative paths unchanged", () => {
|
|
164
|
+
expect(resolveHomePath("relative/path")).toBe("relative/path");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("normalizeWarmupPath", () => {
|
|
169
|
+
it("returns /user for empty string", () => {
|
|
170
|
+
expect(normalizeWarmupPath("")).toBe("/user");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("returns /user for whitespace-only string", () => {
|
|
174
|
+
expect(normalizeWarmupPath(" ")).toBe("/user");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("preserves leading slash", () => {
|
|
178
|
+
expect(normalizeWarmupPath("/custom")).toBe("/custom");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("adds leading slash when missing", () => {
|
|
182
|
+
expect(normalizeWarmupPath("custom")).toBe("/custom");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("trims whitespace", () => {
|
|
186
|
+
expect(normalizeWarmupPath(" /user ")).toBe("/user");
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("resolveApiRoot", () => {
|
|
191
|
+
it("extracts /api/v4 from standard URL", () => {
|
|
192
|
+
const url = new URL("https://gitlab.example.com/api/v4/projects/1");
|
|
193
|
+
expect(resolveApiRoot(url)).toBe("/api/v4");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("extracts subpath /api/v4", () => {
|
|
197
|
+
const url = new URL("https://example.com/gitlab/api/v4/projects");
|
|
198
|
+
expect(resolveApiRoot(url)).toBe("/gitlab/api/v4");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("returns undefined for non-API URLs", () => {
|
|
202
|
+
const url = new URL("https://gitlab.example.com/group/project");
|
|
203
|
+
expect(resolveApiRoot(url)).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("matches when path ends with /api/v4", () => {
|
|
207
|
+
const url = new URL("https://gitlab.example.com/api/v4/");
|
|
208
|
+
expect(resolveApiRoot(url)).toBe("/api/v4");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("parseOauthScopes", () => {
|
|
213
|
+
it("parses space-separated scopes", () => {
|
|
214
|
+
expect(parseOauthScopes("api read_user")).toEqual(["api", "read_user"]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("parses comma-separated scopes", () => {
|
|
218
|
+
expect(parseOauthScopes("api,read_user,write_repository")).toEqual([
|
|
219
|
+
"api",
|
|
220
|
+
"read_user",
|
|
221
|
+
"write_repository"
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("handles mixed separators", () => {
|
|
226
|
+
expect(parseOauthScopes("api, read_user write_repository")).toEqual([
|
|
227
|
+
"api",
|
|
228
|
+
"read_user",
|
|
229
|
+
"write_repository"
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("filters empty entries", () => {
|
|
234
|
+
expect(parseOauthScopes("api,,read_user, ,write_repository")).toEqual([
|
|
235
|
+
"api",
|
|
236
|
+
"read_user",
|
|
237
|
+
"write_repository"
|
|
238
|
+
]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("handles single scope", () => {
|
|
242
|
+
expect(parseOauthScopes("api")).toEqual(["api"]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("handles empty string", () => {
|
|
246
|
+
expect(parseOauthScopes("")).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("trims whitespace from scopes", () => {
|
|
250
|
+
expect(parseOauthScopes(" api , read_user ")).toEqual(["api", "read_user"]);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { stripNullsDeep } from "../src/lib/sanitize.js";
|
|
4
|
+
|
|
5
|
+
describe("stripNullsDeep", () => {
|
|
6
|
+
it("removes null values recursively from objects", () => {
|
|
7
|
+
const input = {
|
|
8
|
+
title: "demo",
|
|
9
|
+
description: null,
|
|
10
|
+
nested: {
|
|
11
|
+
keep: 1,
|
|
12
|
+
drop: null,
|
|
13
|
+
arr: [1, null, 2, null, 3]
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
expect(stripNullsDeep(input)).toEqual({
|
|
18
|
+
title: "demo",
|
|
19
|
+
nested: {
|
|
20
|
+
keep: 1,
|
|
21
|
+
arr: [1, 2, 3]
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns undefined for top-level null", () => {
|
|
27
|
+
expect(stripNullsDeep(null)).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("preserves primitive string values", () => {
|
|
31
|
+
expect(stripNullsDeep("hello")).toBe("hello");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("preserves primitive number values", () => {
|
|
35
|
+
expect(stripNullsDeep(42)).toBe(42);
|
|
36
|
+
expect(stripNullsDeep(0)).toBe(0);
|
|
37
|
+
expect(stripNullsDeep(-1)).toBe(-1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("preserves boolean values", () => {
|
|
41
|
+
expect(stripNullsDeep(true)).toBe(true);
|
|
42
|
+
expect(stripNullsDeep(false)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("preserves undefined as-is", () => {
|
|
46
|
+
expect(stripNullsDeep(undefined)).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("handles empty object", () => {
|
|
50
|
+
expect(stripNullsDeep({})).toEqual({});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("handles empty array", () => {
|
|
54
|
+
expect(stripNullsDeep([])).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("handles object with all null values", () => {
|
|
58
|
+
expect(stripNullsDeep({ a: null, b: null, c: null })).toEqual({});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("handles array with all null values", () => {
|
|
62
|
+
expect(stripNullsDeep([null, null, null])).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("handles deeply nested null removal", () => {
|
|
66
|
+
const input = {
|
|
67
|
+
level1: {
|
|
68
|
+
level2: {
|
|
69
|
+
level3: {
|
|
70
|
+
keep: "value",
|
|
71
|
+
drop: null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
expect(stripNullsDeep(input)).toEqual({
|
|
78
|
+
level1: {
|
|
79
|
+
level2: {
|
|
80
|
+
level3: {
|
|
81
|
+
keep: "value"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("handles mixed array contents", () => {
|
|
89
|
+
const input = [1, "hello", null, true, null, { key: null, keep: "yes" }];
|
|
90
|
+
|
|
91
|
+
expect(stripNullsDeep(input)).toEqual([1, "hello", true, { keep: "yes" }]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("handles nested arrays within objects", () => {
|
|
95
|
+
const input = {
|
|
96
|
+
items: [{ name: "a", value: null }, null, { name: "b", value: 1 }]
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
expect(stripNullsDeep(input)).toEqual({
|
|
100
|
+
items: [{ name: "a" }, { name: "b", value: 1 }]
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("preserves zero, empty string, and false values", () => {
|
|
105
|
+
const input = {
|
|
106
|
+
zero: 0,
|
|
107
|
+
emptyStr: "",
|
|
108
|
+
falsy: false,
|
|
109
|
+
nullValue: null
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
expect(stripNullsDeep(input)).toEqual({
|
|
113
|
+
zero: 0,
|
|
114
|
+
emptyStr: "",
|
|
115
|
+
falsy: false
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("handles arrays nested within arrays", () => {
|
|
120
|
+
const input = [[null, 1], [2, null], null];
|
|
121
|
+
expect(stripNullsDeep(input)).toEqual([[1], [2]]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { parseProjectUploadReference } from "../src/tools/gitlab.js";
|
|
4
|
+
|
|
5
|
+
describe("parseProjectUploadReference", () => {
|
|
6
|
+
it("parses absolute upload URL", () => {
|
|
7
|
+
expect(
|
|
8
|
+
parseProjectUploadReference("https://gitlab.example.com/group/repo/uploads/abc123/file.txt")
|
|
9
|
+
).toEqual({
|
|
10
|
+
secret: "abc123",
|
|
11
|
+
filename: "file.txt"
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("parses relative upload path", () => {
|
|
16
|
+
expect(parseProjectUploadReference("/uploads/abc123/file.txt")).toEqual({
|
|
17
|
+
secret: "abc123",
|
|
18
|
+
filename: "file.txt"
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("parses relative upload path with encoded filename", () => {
|
|
23
|
+
expect(parseProjectUploadReference("/uploads/abc123/%E6%B5%8B%E8%AF%95.txt")).toEqual({
|
|
24
|
+
secret: "abc123",
|
|
25
|
+
filename: "测试.txt"
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns undefined for non-upload paths", () => {
|
|
30
|
+
expect(parseProjectUploadReference("/api/v4/projects/1/issues")).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns undefined for empty string", () => {
|
|
34
|
+
expect(parseProjectUploadReference("")).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns undefined for whitespace-only string", () => {
|
|
38
|
+
expect(parseProjectUploadReference(" ")).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("handles URL with query parameters", () => {
|
|
42
|
+
const result = parseProjectUploadReference(
|
|
43
|
+
"https://gitlab.example.com/group/repo/uploads/abc123/file.txt?inline=true"
|
|
44
|
+
);
|
|
45
|
+
expect(result).toEqual({
|
|
46
|
+
secret: "abc123",
|
|
47
|
+
filename: "file.txt"
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("handles URL with fragment", () => {
|
|
52
|
+
const result = parseProjectUploadReference(
|
|
53
|
+
"https://gitlab.example.com/group/repo/uploads/abc123/file.txt#section"
|
|
54
|
+
);
|
|
55
|
+
expect(result).toEqual({
|
|
56
|
+
secret: "abc123",
|
|
57
|
+
filename: "file.txt"
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("handles URL with nested group paths", () => {
|
|
62
|
+
const result = parseProjectUploadReference(
|
|
63
|
+
"https://gitlab.example.com/group/subgroup/repo/uploads/secret123/document.pdf"
|
|
64
|
+
);
|
|
65
|
+
expect(result).toEqual({
|
|
66
|
+
secret: "secret123",
|
|
67
|
+
filename: "document.pdf"
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns undefined for invalid URL", () => {
|
|
72
|
+
expect(parseProjectUploadReference("not a url at all")).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("handles upload path without leading slash", () => {
|
|
76
|
+
// This depends on implementation, but should handle gracefully
|
|
77
|
+
const result = parseProjectUploadReference("uploads/abc123/file.txt");
|
|
78
|
+
// May or may not match depending on implementation
|
|
79
|
+
if (result) {
|
|
80
|
+
expect(result.secret).toBe("abc123");
|
|
81
|
+
expect(result.filename).toBe("file.txt");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"noImplicitOverride": true,
|
|
10
|
+
"useUnknownInCatchVariables": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"esModuleInterop": false,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"types": ["node", "vitest/globals"]
|
|
18
|
+
},
|
|
19
|
+
"include": ["src", "tests", "eslint.config.js", "vitest.config.ts"],
|
|
20
|
+
"exclude": ["dist", "node_modules"]
|
|
21
|
+
}
|
package/vitest.config.ts
ADDED
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Michael Lin
|
|
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.
|