@kietdev/taiga-mcp 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +285 -2
- package/dist/config.js +59 -0
- package/dist/config.js.map +1 -0
- package/dist/format.js +104 -0
- package/dist/format.js.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/remote/app.js +238 -0
- package/dist/remote/app.js.map +1 -0
- package/dist/remote/config.js +52 -0
- package/dist/remote/config.js.map +1 -0
- package/dist/remote/guard.js +120 -0
- package/dist/remote/guard.js.map +1 -0
- package/dist/remote/pages.js +472 -0
- package/dist/remote/pages.js.map +1 -0
- package/dist/remote/provider.js +211 -0
- package/dist/remote/provider.js.map +1 -0
- package/dist/remote/seal.js +50 -0
- package/dist/remote/seal.js.map +1 -0
- package/dist/remote/taiga-auth.js +70 -0
- package/dist/remote/taiga-auth.js.map +1 -0
- package/dist/remote.js +23 -0
- package/dist/remote.js.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/taiga/client.js +1274 -0
- package/dist/taiga/client.js.map +1 -0
- package/dist/taiga/files.js +331 -0
- package/dist/taiga/files.js.map +1 -0
- package/dist/taiga/http.js +237 -0
- package/dist/taiga/http.js.map +1 -0
- package/dist/taiga/types.js +6 -0
- package/dist/taiga/types.js.map +1 -0
- package/dist/tools/add-comment.js +24 -0
- package/dist/tools/add-comment.js.map +1 -0
- package/dist/tools/create-item.js +63 -0
- package/dist/tools/create-item.js.map +1 -0
- package/dist/tools/file-view.js +300 -0
- package/dist/tools/file-view.js.map +1 -0
- package/dist/tools/get-attachment.js +76 -0
- package/dist/tools/get-attachment.js.map +1 -0
- package/dist/tools/get-item.js +120 -0
- package/dist/tools/get-item.js.map +1 -0
- package/dist/tools/get-project.js +47 -0
- package/dist/tools/get-project.js.map +1 -0
- package/dist/tools/get-sprint.js +56 -0
- package/dist/tools/get-sprint.js.map +1 -0
- package/dist/tools/list-activity.js +102 -0
- package/dist/tools/list-activity.js.map +1 -0
- package/dist/tools/list-epics.js +58 -0
- package/dist/tools/list-epics.js.map +1 -0
- package/dist/tools/list-issues.js +78 -0
- package/dist/tools/list-issues.js.map +1 -0
- package/dist/tools/list-projects.js +22 -0
- package/dist/tools/list-projects.js.map +1 -0
- package/dist/tools/list-sprints.js +46 -0
- package/dist/tools/list-sprints.js.map +1 -0
- package/dist/tools/list-tasks.js +84 -0
- package/dist/tools/list-tasks.js.map +1 -0
- package/dist/tools/list-user-stories.js +85 -0
- package/dist/tools/list-user-stories.js.map +1 -0
- package/dist/tools/search.js +83 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/shared.js +90 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/update-item.js +74 -0
- package/dist/tools/update-item.js.map +1 -0
- package/dist/tools/wiki.js +62 -0
- package/dist/tools/wiki.js.map +1 -0
- package/dist/tools/written.js +18 -0
- package/dist/tools/written.js.map +1 -0
- package/package.json +78 -4
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
|
|
2
|
+
import { getOAuthProtectedResourceMetadataUrl, mcpAuthRouter, } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import express, {} from 'express';
|
|
5
|
+
import { rateLimit } from 'express-rate-limit';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { createServer } from '../server.js';
|
|
8
|
+
import { TaigaClient } from '../taiga/client.js';
|
|
9
|
+
import { TaigaError } from '../taiga/http.js';
|
|
10
|
+
import { guardedFetch } from './guard.js';
|
|
11
|
+
import { expiredPage, homePage, loginPage, SCRIPT_HASH } from './pages.js';
|
|
12
|
+
import { READ, TaigaOAuthProvider, WRITE } from './provider.js';
|
|
13
|
+
import { Sealer } from './seal.js';
|
|
14
|
+
import { TaigaAuth } from './taiga-auth.js';
|
|
15
|
+
const EXPIRED_HINT = 'The Taiga sign-in behind this connection has expired. Disconnect the Taiga connector and connect it again to sign in.';
|
|
16
|
+
/** The icons, the preview image and robots.txt. From src/remote and from dist/remote it is two folders up. */
|
|
17
|
+
const PUBLIC_DIR = fileURLToPath(new URL('../../public', import.meta.url));
|
|
18
|
+
/** The language a link asked for with `?lang=`, if it is one the pages are written in. */
|
|
19
|
+
function chosenLang(req) {
|
|
20
|
+
const { lang } = req.query;
|
|
21
|
+
return lang === 'vi' || lang === 'en' ? lang : undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The language of a link that names one; otherwise Vietnamese for a browser that prefers it
|
|
25
|
+
* over English, and English for everyone else.
|
|
26
|
+
*/
|
|
27
|
+
function langOf(req) {
|
|
28
|
+
const chosen = chosenLang(req);
|
|
29
|
+
if (chosen !== undefined)
|
|
30
|
+
return chosen;
|
|
31
|
+
for (const tag of req.acceptsLanguages()) {
|
|
32
|
+
const language = tag.toLowerCase().split('-')[0];
|
|
33
|
+
if (language === 'vi' || language === 'en')
|
|
34
|
+
return language;
|
|
35
|
+
}
|
|
36
|
+
return 'en';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pages with a password field: never cached, never framed, nothing loaded from elsewhere
|
|
40
|
+
* (the only thing loaded at all is this server's own icon). The only script allowed is the copy
|
|
41
|
+
* buttons' on the front page, by its hash; the sign-in page carries none.
|
|
42
|
+
*/
|
|
43
|
+
function sendPage(res, status, html) {
|
|
44
|
+
res
|
|
45
|
+
.status(status)
|
|
46
|
+
.set({
|
|
47
|
+
'Cache-Control': 'no-store',
|
|
48
|
+
'Content-Security-Policy': `default-src 'none'; script-src ${SCRIPT_HASH}; style-src 'unsafe-inline'; img-src 'self'; base-uri 'none'; frame-ancestors 'none'`,
|
|
49
|
+
// The same address answers in English or Vietnamese.
|
|
50
|
+
Vary: 'Accept-Language',
|
|
51
|
+
'Referrer-Policy': 'no-referrer',
|
|
52
|
+
'X-Content-Type-Options': 'nosniff',
|
|
53
|
+
'X-Frame-Options': 'DENY',
|
|
54
|
+
})
|
|
55
|
+
.type('html')
|
|
56
|
+
.send(html);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The hosted server: the same tools over HTTP, for many people at once.
|
|
60
|
+
*
|
|
61
|
+
* Each person signs in with their own Taiga account through the OAuth pages, and every request
|
|
62
|
+
* to `/mcp` is answered with that person's Taiga token, so they see what Taiga lets them see.
|
|
63
|
+
* The server is set up either for one Taiga, or (`anyTaiga`) for whichever Taiga each person names.
|
|
64
|
+
* Nothing is kept between requests: no sessions, no stored tokens, no cache shared by two people.
|
|
65
|
+
*/
|
|
66
|
+
export function createApp(options) {
|
|
67
|
+
const anyTaiga = options.anyTaiga === true;
|
|
68
|
+
const allowWrites = options.allowWrites === true;
|
|
69
|
+
const fixedTaiga = anyTaiga ? undefined : options.taigaUrl;
|
|
70
|
+
if (!anyTaiga && fixedTaiga === undefined)
|
|
71
|
+
throw new Error('taigaUrl is required unless anyTaiga is set');
|
|
72
|
+
const fixedHost = fixedTaiga === undefined ? undefined : new URL(fixedTaiga).host;
|
|
73
|
+
// Requests to an address a stranger typed never leave the public internet.
|
|
74
|
+
const fetchImpl = options.fetch ?? (anyTaiga ? guardedFetch : fetch);
|
|
75
|
+
const now = options.now ?? (() => new Date());
|
|
76
|
+
const seconds = () => Math.floor(now().getTime() / 1000);
|
|
77
|
+
const mcpUrl = new URL('/mcp', options.publicUrl);
|
|
78
|
+
const provider = new TaigaOAuthProvider({
|
|
79
|
+
sealer: new Sealer(options.secret, seconds),
|
|
80
|
+
taigaAt: (baseUrl) => new TaigaAuth(baseUrl, fetchImpl),
|
|
81
|
+
fixedTaiga,
|
|
82
|
+
resource: mcpUrl,
|
|
83
|
+
allowWrites,
|
|
84
|
+
now: seconds,
|
|
85
|
+
showLogin: (res, view) => sendPage(res, 200, loginPage({ lang: langOf(res.req), fixedHost, address: options.taigaUrl, ...view })),
|
|
86
|
+
});
|
|
87
|
+
const app = express();
|
|
88
|
+
app.disable('x-powered-by');
|
|
89
|
+
app.set('trust proxy', options.trustProxy ?? 0);
|
|
90
|
+
/* ------------------------------ signing in ------------------------------ */
|
|
91
|
+
// /authorize, /token, /register and the two discovery documents.
|
|
92
|
+
app.use(mcpAuthRouter({
|
|
93
|
+
provider,
|
|
94
|
+
issuerUrl: options.publicUrl,
|
|
95
|
+
resourceServerUrl: mcpUrl,
|
|
96
|
+
scopesSupported: allowWrites ? [READ, WRITE] : [READ],
|
|
97
|
+
resourceName: `Taiga${fixedHost === undefined ? '' : ` at ${fixedHost}`}${allowWrites ? '' : ' (read-only)'}`,
|
|
98
|
+
}));
|
|
99
|
+
const showLoginAgain = (req, res, status, error) => {
|
|
100
|
+
const { request, username, address, write } = req.body;
|
|
101
|
+
const asker = provider.describeLogin(request);
|
|
102
|
+
if (asker === undefined) {
|
|
103
|
+
sendPage(res, 400, expiredPage(langOf(req)));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
sendPage(res, status, loginPage({
|
|
107
|
+
lang: langOf(req),
|
|
108
|
+
fixedHost,
|
|
109
|
+
address,
|
|
110
|
+
...asker,
|
|
111
|
+
request,
|
|
112
|
+
error,
|
|
113
|
+
username,
|
|
114
|
+
write: write === 'on',
|
|
115
|
+
}));
|
|
116
|
+
};
|
|
117
|
+
app.post('/login', express.urlencoded({ extended: false, limit: '16kb' }), (req, res, next) => {
|
|
118
|
+
const body = (req.body ?? {});
|
|
119
|
+
if (typeof body['request'] !== 'string' || body['request'] === '') {
|
|
120
|
+
sendPage(res, 400, expiredPage(langOf(req)));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
next();
|
|
124
|
+
},
|
|
125
|
+
// Taiga throttles sign-ins too; this keeps a guesser from using the server as a relay.
|
|
126
|
+
rateLimit({
|
|
127
|
+
windowMs: 15 * 60 * 1000,
|
|
128
|
+
limit: 10,
|
|
129
|
+
standardHeaders: true,
|
|
130
|
+
legacyHeaders: false,
|
|
131
|
+
handler: (req, res) => showLoginAgain(req, res, 429, 'tooMany'),
|
|
132
|
+
}), async (req, res) => {
|
|
133
|
+
const { request, username, password, address, write } = req.body;
|
|
134
|
+
if (typeof username !== 'string' || typeof password !== 'string' || !username || !password) {
|
|
135
|
+
showLoginAgain(req, res, 400, 'rejected');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const outcome = await provider.completeLogin(request, username.trim(), password, typeof address === 'string' ? address : undefined,
|
|
140
|
+
// A ticked checkbox sends "on"; an unticked one sends nothing.
|
|
141
|
+
write === 'on');
|
|
142
|
+
if (outcome.kind === 'redirect') {
|
|
143
|
+
res.set('Cache-Control', 'no-store').redirect(302, outcome.url);
|
|
144
|
+
}
|
|
145
|
+
else if (outcome.kind === 'rejected') {
|
|
146
|
+
showLoginAgain(req, res, 401, 'rejected');
|
|
147
|
+
}
|
|
148
|
+
else if (outcome.kind === 'badAddress') {
|
|
149
|
+
showLoginAgain(req, res, 400, 'badAddress');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
sendPage(res, 400, expiredPage(langOf(req)));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// The reason may name the Taiga host; it never contains the username or the password.
|
|
157
|
+
console.error(`taiga-mcp: sign-in could not be checked: ${String(error)}`);
|
|
158
|
+
// At a typed address, anything but "busy" or "broken" most likely means it is not a Taiga.
|
|
159
|
+
const busy = error instanceof TaigaError && (error.status === 429 || (error.status ?? 0) >= 500);
|
|
160
|
+
if (anyTaiga && !busy)
|
|
161
|
+
showLoginAgain(req, res, 400, 'badAddress');
|
|
162
|
+
else
|
|
163
|
+
showLoginAgain(req, res, 502, 'unavailable');
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
/* ------------------------------- the tools ------------------------------- */
|
|
167
|
+
const bearer = requireBearerAuth({
|
|
168
|
+
verifier: provider,
|
|
169
|
+
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpUrl),
|
|
170
|
+
expectedResource: mcpUrl,
|
|
171
|
+
});
|
|
172
|
+
// Tokens travel in a header, never in a cookie, so any web page may call the endpoint.
|
|
173
|
+
app.use('/mcp', (req, res, next) => {
|
|
174
|
+
res.set({
|
|
175
|
+
'Access-Control-Allow-Origin': '*',
|
|
176
|
+
'Access-Control-Allow-Headers': 'Authorization, Content-Type, Accept, Mcp-Protocol-Version, Mcp-Session-Id',
|
|
177
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
178
|
+
'Access-Control-Expose-Headers': 'WWW-Authenticate, Mcp-Session-Id',
|
|
179
|
+
});
|
|
180
|
+
if (req.method === 'OPTIONS') {
|
|
181
|
+
res.status(204).end();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
next();
|
|
185
|
+
});
|
|
186
|
+
app.post('/mcp', bearer, express.json({ limit: '1mb' }), async (req, res) => {
|
|
187
|
+
const { taigaUrl: baseUrl, taigaToken: token, write } = req.auth?.extra ?? {};
|
|
188
|
+
if (typeof baseUrl !== 'string' || typeof token !== 'string') {
|
|
189
|
+
res.status(401).json({ error: 'invalid_token' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// A server and a Taiga client per request: nothing one person reads is kept for the next.
|
|
193
|
+
const server = createServer(new TaigaClient({ baseUrl, token, expiredHint: EXPIRED_HINT, write: write === true }, fetchImpl, now), allowWrites && write !== true
|
|
194
|
+
? {
|
|
195
|
+
howToAllowWrites: 'disconnect the Taiga connector, connect it again, and tick the box on the sign-in page that allows changes.',
|
|
196
|
+
}
|
|
197
|
+
: {});
|
|
198
|
+
const transport = new StreamableHTTPServerTransport({ enableJsonResponse: true });
|
|
199
|
+
res.on('close', () => {
|
|
200
|
+
void transport.close();
|
|
201
|
+
void server.close();
|
|
202
|
+
});
|
|
203
|
+
await server.connect(transport);
|
|
204
|
+
await transport.handleRequest(req, res, req.body);
|
|
205
|
+
});
|
|
206
|
+
// No sessions and no server-sent stream: the endpoint only answers POST.
|
|
207
|
+
app.all('/mcp', (_req, res) => {
|
|
208
|
+
res
|
|
209
|
+
.status(405)
|
|
210
|
+
.set('Allow', 'POST')
|
|
211
|
+
.json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed. Use POST.' }, id: null });
|
|
212
|
+
});
|
|
213
|
+
/* -------------------------------- the rest -------------------------------- */
|
|
214
|
+
app.get('/', (req, res) => {
|
|
215
|
+
sendPage(res, 200, homePage(langOf(req), fixedHost, options.publicUrl, chosenLang(req), allowWrites));
|
|
216
|
+
});
|
|
217
|
+
// Hosts that serve `public/` themselves (Vercel) answer these before the app is asked.
|
|
218
|
+
app.use(express.static(PUBLIC_DIR, { index: false, maxAge: '1d' }));
|
|
219
|
+
app.get('/healthz', (_req, res) => {
|
|
220
|
+
res.json({ ok: true });
|
|
221
|
+
});
|
|
222
|
+
app.use((_req, res) => {
|
|
223
|
+
res.status(404).json({ error: 'not_found' });
|
|
224
|
+
});
|
|
225
|
+
// Express tells an error handler from a route by its four parameters.
|
|
226
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
227
|
+
app.use((error, _req, res, _next) => {
|
|
228
|
+
const status = typeof error === 'object' && error !== null && 'status' in error && typeof error.status === 'number'
|
|
229
|
+
? error.status
|
|
230
|
+
: 500;
|
|
231
|
+
if (status >= 500)
|
|
232
|
+
console.error(`taiga-mcp: request failed: ${String(error)}`);
|
|
233
|
+
if (!res.headersSent)
|
|
234
|
+
res.status(status).json({ error: status >= 500 ? 'server_error' : 'bad_request' });
|
|
235
|
+
});
|
|
236
|
+
return app;
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/remote/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ConfigError, flag, normalizeBaseUrl, read } from '../config.js';
|
|
2
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1']);
|
|
3
|
+
function wholeNumber(env, key, fallback) {
|
|
4
|
+
const raw = read(env, key);
|
|
5
|
+
if (raw === undefined)
|
|
6
|
+
return fallback;
|
|
7
|
+
const value = Number(raw);
|
|
8
|
+
if (!Number.isInteger(value) || value < 0)
|
|
9
|
+
throw new ConfigError(`${key} must be a whole number.`);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
export function loadRemoteConfig(env = process.env) {
|
|
13
|
+
const anyTaiga = flag(env, 'ANY_TAIGA');
|
|
14
|
+
const allowWrites = flag(env, 'ALLOW_WRITES');
|
|
15
|
+
const rawTaiga = read(env, 'TAIGA_URL');
|
|
16
|
+
if (!rawTaiga && !anyTaiga) {
|
|
17
|
+
throw new ConfigError('TAIGA_URL is not set. Set it to your Taiga API host, e.g. https://api.taiga.io, or set ANY_TAIGA=true to let each person name their own Taiga.');
|
|
18
|
+
}
|
|
19
|
+
const rawPublic = read(env, 'PUBLIC_URL');
|
|
20
|
+
if (!rawPublic) {
|
|
21
|
+
throw new ConfigError('PUBLIC_URL is not set. Set it to the address people reach this server at, e.g. https://taiga-mcp.example.com');
|
|
22
|
+
}
|
|
23
|
+
let publicUrl;
|
|
24
|
+
try {
|
|
25
|
+
publicUrl = new URL(rawPublic);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new ConfigError(`PUBLIC_URL is not a valid address: "${rawPublic}".`);
|
|
29
|
+
}
|
|
30
|
+
const local = LOCAL_HOSTS.has(publicUrl.hostname);
|
|
31
|
+
if (publicUrl.protocol !== 'https:' && !(local && publicUrl.protocol === 'http:')) {
|
|
32
|
+
throw new ConfigError('PUBLIC_URL must start with https:// (http:// is accepted for localhost only).');
|
|
33
|
+
}
|
|
34
|
+
if (publicUrl.pathname !== '/' || publicUrl.search || publicUrl.hash) {
|
|
35
|
+
throw new ConfigError('PUBLIC_URL must be the bare address, without a path: https://taiga-mcp.example.com');
|
|
36
|
+
}
|
|
37
|
+
const secret = read(env, 'MCP_SECRET');
|
|
38
|
+
if (secret === undefined || secret.length < 32) {
|
|
39
|
+
throw new ConfigError('MCP_SECRET must be set to a random value of at least 32 characters, e.g. the output of `openssl rand -base64 48`.');
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
taigaUrl: rawTaiga ? normalizeBaseUrl(rawTaiga) : undefined,
|
|
43
|
+
anyTaiga,
|
|
44
|
+
allowWrites,
|
|
45
|
+
publicUrl,
|
|
46
|
+
secret,
|
|
47
|
+
port: wholeNumber(env, 'PORT', 3000),
|
|
48
|
+
// A hosted server sits behind the host's proxy; a local one is reached directly.
|
|
49
|
+
trustProxy: wholeNumber(env, 'TRUST_PROXY', local ? 0 : 1),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/remote/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAY,MAAM,cAAc,CAAC;AAyBnF,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;AAExD,SAAS,WAAW,CAAC,GAAQ,EAAE,GAAW,EAAE,QAAgB;IAC1D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC3B,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,QAAQ,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;QAAE,MAAM,IAAI,WAAW,CAAC,GAAG,GAAG,0BAA0B,CAAC,CAAC;IACnG,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAW,OAAO,CAAC,GAAG;IACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IACxC,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAE9C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IACxC,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;QAC3B,MAAM,IAAI,WAAW,CACnB,gJAAgJ,CACjJ,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC1C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,WAAW,CACnB,8GAA8G,CAC/G,CAAC;IACJ,CAAC;IACD,IAAI,SAAc,CAAC;IACnB,IAAI,CAAC;QACH,SAAS,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,WAAW,CAAC,uCAAuC,SAAS,IAAI,CAAC,CAAC;IAC9E,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAClD,IAAI,SAAS,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,KAAK,IAAI,SAAS,CAAC,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;QAClF,MAAM,IAAI,WAAW,CAAC,+EAA+E,CAAC,CAAC;IACzG,CAAC;IACD,IAAI,SAAS,CAAC,QAAQ,KAAK,GAAG,IAAI,SAAS,CAAC,MAAM,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC;QACrE,MAAM,IAAI,WAAW,CACnB,oFAAoF,CACrF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACvC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,WAAW,CACnB,mHAAmH,CACpH,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS;QAC3D,QAAQ;QACR,WAAW;QACX,SAAS;QACT,MAAM;QACN,IAAI,EAAE,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC;QACpC,iFAAiF;QACjF,UAAU,EAAE,WAAW,CAAC,GAAG,EAAE,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAC3D,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { lookup as dnsLookup } from 'node:dns';
|
|
2
|
+
import { BlockList, isIP } from 'node:net';
|
|
3
|
+
import { Agent, buildConnector, fetch as undiciFetch } from 'undici';
|
|
4
|
+
/**
|
|
5
|
+
* When people may type the address of their own Taiga, the server makes requests to hosts
|
|
6
|
+
* chosen by strangers. This file keeps those requests on the public internet: a typed address
|
|
7
|
+
* must not be a way to reach the machine the server runs on, or the network around it.
|
|
8
|
+
*/
|
|
9
|
+
const NOT_PUBLIC = new BlockList();
|
|
10
|
+
for (const [network, prefix] of [
|
|
11
|
+
['0.0.0.0', 8], // "this network"
|
|
12
|
+
['10.0.0.0', 8], // private
|
|
13
|
+
['100.64.0.0', 10], // carrier-grade NAT
|
|
14
|
+
['127.0.0.0', 8], // loopback
|
|
15
|
+
['169.254.0.0', 16], // link-local, where cloud hosts keep their metadata service
|
|
16
|
+
['172.16.0.0', 12], // private
|
|
17
|
+
['192.0.0.0', 24], // protocol assignments
|
|
18
|
+
['192.0.2.0', 24], // documentation
|
|
19
|
+
['192.168.0.0', 16], // private
|
|
20
|
+
['198.18.0.0', 15], // benchmarking
|
|
21
|
+
['198.51.100.0', 24], // documentation
|
|
22
|
+
['203.0.113.0', 24], // documentation
|
|
23
|
+
['224.0.0.0', 4], // multicast
|
|
24
|
+
['240.0.0.0', 4], // reserved, and broadcast
|
|
25
|
+
]) {
|
|
26
|
+
NOT_PUBLIC.addSubnet(network, prefix, 'ipv4');
|
|
27
|
+
}
|
|
28
|
+
for (const [network, prefix] of [
|
|
29
|
+
['::', 128], // unspecified
|
|
30
|
+
['::1', 128], // loopback
|
|
31
|
+
['64:ff9b::', 96], // NAT64: an IPv4 address in disguise
|
|
32
|
+
['2001:db8::', 32], // documentation
|
|
33
|
+
['fc00::', 7], // unique local
|
|
34
|
+
['fe80::', 10], // link-local
|
|
35
|
+
['ff00::', 8], // multicast
|
|
36
|
+
]) {
|
|
37
|
+
NOT_PUBLIC.addSubnet(network, prefix, 'ipv6');
|
|
38
|
+
}
|
|
39
|
+
/** Whether an IP address (not a name) is one a stranger may send the server to. */
|
|
40
|
+
export function isPublicAddress(address) {
|
|
41
|
+
const family = isIP(address);
|
|
42
|
+
if (family === 0)
|
|
43
|
+
return false;
|
|
44
|
+
// An IPv4 address written as IPv6 (::ffff:10.0.0.1) is checked against the IPv4 list too.
|
|
45
|
+
return !NOT_PUBLIC.check(address, family === 4 ? 'ipv4' : 'ipv6');
|
|
46
|
+
}
|
|
47
|
+
/** Taiga cloud is opened at one address and answers its API at another. */
|
|
48
|
+
const TAIGA_CLOUD = new Set(['taiga.io', 'www.taiga.io', 'tree.taiga.io', 'api.taiga.io']);
|
|
49
|
+
/**
|
|
50
|
+
* The API host for what a person typed as "the address of my Taiga", or `undefined` when it
|
|
51
|
+
* cannot be one. People paste whatever is in their address bar, so the scheme may be missing
|
|
52
|
+
* and a path may follow; only a host name reached over HTTPS is kept.
|
|
53
|
+
*/
|
|
54
|
+
export function taigaAddress(typed) {
|
|
55
|
+
const text = typed.trim();
|
|
56
|
+
if (text === '' || text.length > 300)
|
|
57
|
+
return undefined;
|
|
58
|
+
let url;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(text) ? text : `https://${text}`);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
// A password is never sent over plain HTTP: http:// is read as https://.
|
|
66
|
+
if (url.protocol === 'http:')
|
|
67
|
+
url.protocol = 'https:';
|
|
68
|
+
if (url.protocol !== 'https:' || url.username !== '' || url.password !== '')
|
|
69
|
+
return undefined;
|
|
70
|
+
const host = url.hostname.toLowerCase();
|
|
71
|
+
// Names only: no bare IP addresses, no single-label names such as "localhost".
|
|
72
|
+
if (isIP(host.replace(/^\[|\]$/g, '')) !== 0 || !/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(host))
|
|
73
|
+
return undefined;
|
|
74
|
+
if (TAIGA_CLOUD.has(host))
|
|
75
|
+
return 'https://api.taiga.io';
|
|
76
|
+
return url.origin;
|
|
77
|
+
}
|
|
78
|
+
class NotPublicError extends Error {
|
|
79
|
+
name = 'NotPublicError';
|
|
80
|
+
constructor(host) {
|
|
81
|
+
super(`${host} is not an address on the public internet`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Name resolution for the guarded connections: a name is refused if any address it resolves
|
|
86
|
+
* to is not public. Checking here, at the moment of connecting, also covers redirects and a
|
|
87
|
+
* name that changes its answer between two look-ups.
|
|
88
|
+
*/
|
|
89
|
+
function guardedLookup(hostname, options, callback) {
|
|
90
|
+
dnsLookup(hostname, { ...options, all: true }, (error, addresses) => {
|
|
91
|
+
if (error) {
|
|
92
|
+
callback(error, []);
|
|
93
|
+
}
|
|
94
|
+
else if (addresses.length === 0 || addresses.some(({ address }) => !isPublicAddress(address))) {
|
|
95
|
+
callback(new NotPublicError(hostname), []);
|
|
96
|
+
}
|
|
97
|
+
else if (options.all) {
|
|
98
|
+
callback(null, addresses);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const [first] = addresses;
|
|
102
|
+
callback(null, first.address, first.family);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const connect = buildConnector({ lookup: guardedLookup });
|
|
107
|
+
const agent = new Agent({
|
|
108
|
+
connect: (options, callback) => {
|
|
109
|
+
// A literal IP address is connected to without a look-up, so it is checked here.
|
|
110
|
+
const host = options.hostname.replace(/^\[|\]$/g, '');
|
|
111
|
+
if (isIP(host) !== 0 && !isPublicAddress(host)) {
|
|
112
|
+
callback(new NotPublicError(host), null);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
connect(options, callback);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
/** `fetch` that only ever connects to public addresses. */
|
|
119
|
+
export const guardedFetch = ((input, init) => undiciFetch(input, { ...init, dispatcher: agent }));
|
|
120
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/remote/guard.ts"],"names":[],"mappings":"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"}
|