javascript-solid-server 0.0.36 → 0.0.37

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.
@@ -114,7 +114,9 @@
114
114
  "Bash(if [ ! -d \"jose\" ])",
115
115
  "Bash(then git clone --depth 1 --branch v0.7.0 https://github.com/solid/jose.git)",
116
116
  "Bash(fi)",
117
- "Bash(timeout 45 node:*)"
117
+ "Bash(timeout 45 node:*)",
118
+ "Bash(gh issue list:*)",
119
+ "Bash(DATA_ROOT=/tmp/jss-git-test JSS_PORT=4444 timeout 3 node:*)"
118
120
  ]
119
121
  }
120
122
  }
package/bin/jss.js CHANGED
@@ -54,6 +54,8 @@ program
54
54
  .option('--mashlib-cdn', 'Enable Mashlib data browser (CDN mode, no local files needed)')
55
55
  .option('--no-mashlib', 'Disable Mashlib data browser')
56
56
  .option('--mashlib-version <version>', 'Mashlib version for CDN mode (default: 2.0.0)')
57
+ .option('--git', 'Enable Git HTTP backend (clone/push support)')
58
+ .option('--no-git', 'Disable Git HTTP backend')
57
59
  .option('-q, --quiet', 'Suppress log output')
58
60
  .option('--print-config', 'Print configuration and exit')
59
61
  .action(async (options) => {
@@ -95,6 +97,7 @@ program
95
97
  mashlib: config.mashlib || config.mashlibCdn,
96
98
  mashlibCdn: config.mashlibCdn,
97
99
  mashlibVersion: config.mashlibVersion,
100
+ git: config.git,
98
101
  });
99
102
 
100
103
  await server.listen({ port: config.port, host: config.host });
@@ -113,6 +116,7 @@ program
113
116
  } else if (config.mashlib) {
114
117
  console.log(` Mashlib: local (data browser enabled)`);
115
118
  }
119
+ if (config.git) console.log(' Git: enabled (clone/push support)');
116
120
  console.log('\n Press Ctrl+C to stop\n');
117
121
  }
118
122
 
@@ -0,0 +1,283 @@
1
+ # Adding Git Support to a Solid Server
2
+
3
+ This guide explains how to add Git HTTP backend support to a Solid server, enabling `git clone` and `git push` operations on pod containers.
4
+
5
+ ## Overview
6
+
7
+ The Git HTTP protocol allows clients to clone and push to repositories over HTTP. This is implemented using Git's built-in `git http-backend` CGI program - the same one used by Apache and Nginx.
8
+
9
+ ### How It Works
10
+
11
+ ```
12
+ ┌─────────────┐ HTTP ┌──────────────┐ CGI ┌─────────────────┐
13
+ │ Git Client │ ─────────────▶│ Solid Server │ ────────────▶│ git http-backend│
14
+ │ │◀───────────── │ │◀──────────── │ │
15
+ └─────────────┘ └──────────────┘ └─────────────────┘
16
+ ```
17
+
18
+ **Clone flow:**
19
+ 1. `GET /repo/info/refs?service=git-upload-pack` - Discovery
20
+ 2. `POST /repo/git-upload-pack` - Fetch objects
21
+
22
+ **Push flow:**
23
+ 1. `GET /repo/info/refs?service=git-receive-pack` - Discovery
24
+ 2. `POST /repo/git-receive-pack` - Send objects
25
+
26
+ ## Implementation
27
+
28
+ ### 1. Detect Git Requests
29
+
30
+ Git protocol requests are identified by URL patterns:
31
+
32
+ ```javascript
33
+ function isGitRequest(urlPath) {
34
+ return urlPath.includes('/info/refs') ||
35
+ urlPath.includes('/git-upload-pack') ||
36
+ urlPath.includes('/git-receive-pack');
37
+ }
38
+
39
+ function isGitWriteOperation(urlPath) {
40
+ return urlPath.includes('/git-receive-pack');
41
+ }
42
+ ```
43
+
44
+ ### 2. Security: Block Direct .git Access
45
+
46
+ **Important:** Git protocol requests should be allowed, but direct file access to `.git/` contents must be blocked:
47
+
48
+ ```javascript
49
+ // BLOCK: Direct access to .git contents (security risk)
50
+ GET /.git/config → 403 Forbidden
51
+ GET /.git/objects/abc123 → 403 Forbidden
52
+
53
+ // ALLOW: Git protocol (handled by git http-backend)
54
+ GET /repo/info/refs?service=git-upload-pack → 200 OK
55
+ POST /repo/git-upload-pack → 200 OK
56
+ ```
57
+
58
+ ### 3. Authorization with WAC
59
+
60
+ Check permissions before allowing git operations:
61
+
62
+ ```javascript
63
+ // Clone/fetch requires Read access
64
+ // Push requires Write access
65
+
66
+ const needsWrite = isGitWriteOperation(request.url);
67
+ const requiredMode = needsWrite ? 'write' : 'read';
68
+
69
+ const { allowed } = await checkAccess({
70
+ resourceUrl,
71
+ resourcePath,
72
+ agentWebId: request.webId,
73
+ requiredMode
74
+ });
75
+
76
+ if (!allowed) {
77
+ return reply.code(needsWrite ? 403 : 401).send({
78
+ error: needsWrite ? 'Write access required' : 'Read access required'
79
+ });
80
+ }
81
+ ```
82
+
83
+ ### 4. Git HTTP Backend Handler
84
+
85
+ The core handler spawns `git http-backend` with CGI environment variables:
86
+
87
+ ```javascript
88
+ import { spawn } from 'child_process';
89
+
90
+ async function handleGit(request, reply) {
91
+ const urlPath = decodeURIComponent(request.url.split('?')[0]);
92
+ const queryString = request.url.split('?')[1] || '';
93
+
94
+ // Build CGI environment
95
+ const env = {
96
+ ...process.env,
97
+ GIT_PROJECT_ROOT: dataRoot, // Where repos are stored
98
+ GIT_HTTP_EXPORT_ALL: '', // Allow read access
99
+ GIT_HTTP_RECEIVE_PACK: 'true', // Enable push
100
+ PATH_INFO: urlPath,
101
+ REQUEST_METHOD: request.method,
102
+ CONTENT_TYPE: request.headers['content-type'] || '',
103
+ QUERY_STRING: queryString,
104
+ CONTENT_LENGTH: request.headers['content-length'] || '0',
105
+ };
106
+
107
+ // For non-bare repos, set GIT_DIR to .git subdirectory
108
+ if (isRegularRepo) {
109
+ env.GIT_DIR = path.join(repoPath, '.git');
110
+ }
111
+
112
+ // Spawn git http-backend
113
+ const child = spawn('git', ['http-backend'], { env });
114
+
115
+ // Send request body (for POST requests)
116
+ if (request.body && request.body.length > 0) {
117
+ child.stdin.write(request.body);
118
+ }
119
+ child.stdin.end();
120
+
121
+ // Parse CGI response and send to client
122
+ // ... (see full implementation below)
123
+ }
124
+ ```
125
+
126
+ ### 5. CGI Response Parsing
127
+
128
+ Git http-backend outputs CGI format (headers + body). Parse and forward:
129
+
130
+ ```javascript
131
+ let buffer = Buffer.alloc(0);
132
+ let headersSent = false;
133
+
134
+ child.stdout.on('data', (data) => {
135
+ buffer = Buffer.concat([buffer, data]);
136
+
137
+ if (!headersSent) {
138
+ // Find header/body separator (try both \r\n\r\n and \n\n)
139
+ let headerEnd = buffer.indexOf('\r\n\r\n');
140
+ let sep = '\r\n';
141
+ let sepLen = 4;
142
+
143
+ if (headerEnd === -1) {
144
+ headerEnd = buffer.indexOf('\n\n');
145
+ sep = '\n';
146
+ sepLen = 2;
147
+ }
148
+
149
+ if (headerEnd !== -1) {
150
+ const headerSection = buffer.subarray(0, headerEnd).toString();
151
+ const bodySection = buffer.subarray(headerEnd + sepLen);
152
+
153
+ // Parse CGI headers
154
+ for (const line of headerSection.split(sep)) {
155
+ const colonIdx = line.indexOf(':');
156
+ if (colonIdx > 0) {
157
+ const key = line.substring(0, colonIdx).trim();
158
+ const value = line.substring(colonIdx + 1).trim();
159
+
160
+ if (key.toLowerCase() === 'status') {
161
+ statusCode = parseInt(value.split(' ')[0], 10);
162
+ } else {
163
+ reply.raw.setHeader(key, value);
164
+ }
165
+ }
166
+ }
167
+
168
+ reply.raw.writeHead(statusCode);
169
+ reply.raw.write(bodySection);
170
+ headersSent = true;
171
+ }
172
+ } else {
173
+ reply.raw.write(buffer);
174
+ }
175
+ buffer = Buffer.alloc(0);
176
+ });
177
+
178
+ child.stdout.on('end', () => {
179
+ reply.raw.end();
180
+ });
181
+ ```
182
+
183
+ ## Repository Setup
184
+
185
+ ### Regular Repository (with working directory)
186
+
187
+ ```bash
188
+ cd /path/to/pod/myrepo
189
+ git init
190
+ echo "# My Project" > README.md
191
+ git add .
192
+ git commit -m "Initial commit"
193
+ ```
194
+
195
+ ### Bare Repository (server-only, more efficient)
196
+
197
+ ```bash
198
+ cd /path/to/pod
199
+ git init --bare myrepo.git
200
+ ```
201
+
202
+ ### ACL for Public Clone
203
+
204
+ Create `/path/to/pod/myrepo/.acl`:
205
+
206
+ ```turtle
207
+ @prefix acl: <http://www.w3.org/ns/auth/acl#>.
208
+ @prefix foaf: <http://xmlns.com/foaf/0.1/>.
209
+
210
+ <#public>
211
+ a acl:Authorization;
212
+ acl:agentClass foaf:Agent;
213
+ acl:accessTo <./>;
214
+ acl:default <./>;
215
+ acl:mode acl:Read.
216
+ ```
217
+
218
+ ### ACL for Authenticated Push
219
+
220
+ ```turtle
221
+ @prefix acl: <http://www.w3.org/ns/auth/acl#>.
222
+ @prefix foaf: <http://xmlns.com/foaf/0.1/>.
223
+
224
+ <#owner>
225
+ a acl:Authorization;
226
+ acl:agent <https://alice.example.com/#me>;
227
+ acl:accessTo <./>;
228
+ acl:default <./>;
229
+ acl:mode acl:Read, acl:Write, acl:Control.
230
+
231
+ <#public>
232
+ a acl:Authorization;
233
+ acl:agentClass foaf:Agent;
234
+ acl:accessTo <./>;
235
+ acl:default <./>;
236
+ acl:mode acl:Read.
237
+ ```
238
+
239
+ ## Usage
240
+
241
+ ### Server
242
+
243
+ ```bash
244
+ # Start server with git support enabled
245
+ jss start --git
246
+
247
+ # Or via environment variable
248
+ JSS_GIT=true jss start
249
+ ```
250
+
251
+ ### Client
252
+
253
+ ```bash
254
+ # Clone
255
+ git clone http://localhost:3000/myrepo
256
+
257
+ # Clone with authentication (if required)
258
+ git clone http://localhost:3000/myrepo
259
+ # Git will prompt for credentials
260
+
261
+ # Push (requires write access)
262
+ cd myrepo
263
+ echo "New content" >> README.md
264
+ git add .
265
+ git commit -m "Update readme"
266
+ git push
267
+ ```
268
+
269
+ ## Complete Handler Code
270
+
271
+ See `src/handlers/git.js` in the JSS repository for the full implementation.
272
+
273
+ ## References
274
+
275
+ - [Git HTTP Protocol](https://git-scm.com/book/en/v2/Git-on-the-Server-Smart-HTTP)
276
+ - [git-http-backend documentation](https://git-scm.com/docs/git-http-backend)
277
+ - [CGI Specification](https://www.rfc-editor.org/rfc/rfc3875)
278
+ - [Web Access Control (WAC)](https://solidproject.org/TR/wac)
279
+
280
+ ## Prior Art
281
+
282
+ - [nosdav/server](https://github.com/nosdav/server) - Git support implementation
283
+ - [QuitStore](https://github.com/AKSW/QuitStore) - Git + RDF versioning
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.36",
3
+ "version": "0.0.37",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -0,0 +1,207 @@
1
+ import { spawn } from 'child_process';
2
+ import { existsSync, statSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ /**
6
+ * Check if a URL path is a Git protocol request
7
+ * @param {string} urlPath - The URL path
8
+ * @returns {boolean}
9
+ */
10
+ export function isGitRequest(urlPath) {
11
+ return urlPath.includes('/info/refs') ||
12
+ urlPath.includes('/git-upload-pack') ||
13
+ urlPath.includes('/git-receive-pack');
14
+ }
15
+
16
+ /**
17
+ * Determine if this is a write operation (push)
18
+ * @param {string} urlPath - The URL path
19
+ * @returns {boolean}
20
+ */
21
+ export function isGitWriteOperation(urlPath) {
22
+ return urlPath.includes('/git-receive-pack');
23
+ }
24
+
25
+ /**
26
+ * Extract the repository path from the URL
27
+ * @param {string} urlPath - The URL path
28
+ * @returns {string|null} The repository relative path or null
29
+ */
30
+ function extractRepoPath(urlPath) {
31
+ // Remove git service suffixes to get the repo path
32
+ const cleanPath = urlPath
33
+ .replace(/\/info\/refs.*$/, '')
34
+ .replace(/\/git-upload-pack$/, '')
35
+ .replace(/\/git-receive-pack$/, '');
36
+
37
+ // Remove leading slash
38
+ return cleanPath.replace(/^\//, '') || null;
39
+ }
40
+
41
+ /**
42
+ * Find the git directory for a path
43
+ * @param {string} repoPath - Absolute path to check
44
+ * @returns {{gitDir: string, isRegular: boolean}|null}
45
+ */
46
+ function findGitDir(repoPath) {
47
+ if (!existsSync(repoPath) || !statSync(repoPath).isDirectory()) {
48
+ return null;
49
+ }
50
+
51
+ // Check for regular repo with .git subdirectory
52
+ const dotGitPath = join(repoPath, '.git');
53
+ if (existsSync(dotGitPath) && statSync(dotGitPath).isDirectory()) {
54
+ return { gitDir: dotGitPath, isRegular: true };
55
+ }
56
+
57
+ // Check for bare repository
58
+ const objectsPath = join(repoPath, 'objects');
59
+ const refsPath = join(repoPath, 'refs');
60
+ if (existsSync(objectsPath) && existsSync(refsPath)) {
61
+ return { gitDir: repoPath, isRegular: false };
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ /**
68
+ * Handle Git HTTP requests using git http-backend
69
+ * @param {FastifyRequest} request
70
+ * @param {FastifyReply} reply
71
+ */
72
+ export async function handleGit(request, reply) {
73
+ const urlPath = decodeURIComponent(request.url.split('?')[0]);
74
+ const queryString = request.url.split('?')[1] || '';
75
+
76
+ // Extract repository path
77
+ const repoRelative = extractRepoPath(urlPath);
78
+ if (!repoRelative) {
79
+ return reply.code(400).send({ error: 'Invalid git request' });
80
+ }
81
+
82
+ // Handle subdomain mode
83
+ let dataRoot = process.env.DATA_ROOT || './data';
84
+ if (request.podName) {
85
+ dataRoot = join(dataRoot, request.podName);
86
+ }
87
+
88
+ const repoAbs = join(dataRoot, repoRelative);
89
+
90
+ // Find git directory
91
+ const gitInfo = findGitDir(repoAbs);
92
+ if (!gitInfo) {
93
+ return reply.code(404).send({ error: 'Not a git repository' });
94
+ }
95
+
96
+ // Build CGI environment
97
+ const env = {
98
+ ...process.env,
99
+ GIT_PROJECT_ROOT: dataRoot,
100
+ GIT_HTTP_EXPORT_ALL: '', // Allow read access
101
+ GIT_HTTP_RECEIVE_PACK: 'true', // Enable push
102
+ GIT_CONFIG_PARAMETERS: "'uploadpack.allowTipSHA1InWant=true'",
103
+ PATH_INFO: urlPath,
104
+ REQUEST_METHOD: request.method,
105
+ CONTENT_TYPE: request.headers['content-type'] || '',
106
+ QUERY_STRING: queryString,
107
+ REMOTE_USER: request.webId || '', // Pass authenticated user
108
+ CONTENT_LENGTH: request.headers['content-length'] || '0',
109
+ };
110
+
111
+ // For regular repositories, set GIT_DIR
112
+ if (gitInfo.isRegular) {
113
+ env.GIT_DIR = gitInfo.gitDir;
114
+ }
115
+
116
+ // Spawn git http-backend
117
+ return new Promise((resolve, reject) => {
118
+ const child = spawn('git', ['http-backend'], { env });
119
+
120
+ let buffer = Buffer.alloc(0);
121
+ let headersSent = false;
122
+
123
+ child.stdout.on('data', (data) => {
124
+ buffer = Buffer.concat([buffer, data]);
125
+
126
+ if (!headersSent) {
127
+ // Look for end of CGI headers (try both \r\n\r\n and \n\n)
128
+ let headerEnd = buffer.indexOf('\r\n\r\n');
129
+ let headerSep = '\r\n';
130
+ let headerEndLen = 4;
131
+
132
+ if (headerEnd === -1) {
133
+ headerEnd = buffer.indexOf('\n\n');
134
+ headerSep = '\n';
135
+ headerEndLen = 2;
136
+ }
137
+
138
+ if (headerEnd !== -1) {
139
+ const headerSection = buffer.subarray(0, headerEnd).toString();
140
+ const bodySection = buffer.subarray(headerEnd + headerEndLen);
141
+
142
+ // Parse CGI headers and set on raw response
143
+ const lines = headerSection.split(headerSep);
144
+ let statusCode = 200;
145
+
146
+ for (const line of lines) {
147
+ const colonIndex = line.indexOf(':');
148
+ if (colonIndex > 0) {
149
+ const key = line.substring(0, colonIndex).trim();
150
+ const value = line.substring(colonIndex + 1).trim();
151
+
152
+ // Handle Status header specially
153
+ if (key.toLowerCase() === 'status') {
154
+ statusCode = parseInt(value.split(' ')[0], 10);
155
+ } else {
156
+ reply.raw.setHeader(key, value);
157
+ }
158
+ }
159
+ }
160
+
161
+ reply.raw.writeHead(statusCode);
162
+ headersSent = true;
163
+ reply.raw.write(bodySection);
164
+ buffer = Buffer.alloc(0);
165
+ }
166
+ } else {
167
+ reply.raw.write(buffer);
168
+ buffer = Buffer.alloc(0);
169
+ }
170
+ });
171
+
172
+ child.stdout.on('end', () => {
173
+ reply.raw.end();
174
+ resolve();
175
+ });
176
+
177
+ // Send request body to git
178
+ // For POST requests, Fastify has already parsed the body into request.body
179
+ if (request.body && request.body.length > 0) {
180
+ child.stdin.write(request.body);
181
+ child.stdin.end();
182
+ } else {
183
+ // For GET requests or empty bodies, just close stdin
184
+ child.stdin.end();
185
+ }
186
+
187
+ // Log errors
188
+ child.stderr.on('data', (data) => {
189
+ console.error('git http-backend stderr:', data.toString());
190
+ });
191
+
192
+ child.on('error', (err) => {
193
+ console.error('Failed to spawn git http-backend:', err);
194
+ if (!headersSent) {
195
+ reply.code(500).send({ error: 'Git backend error' });
196
+ }
197
+ resolve();
198
+ });
199
+
200
+ child.on('close', (code) => {
201
+ if (code !== 0 && !headersSent) {
202
+ reply.code(500).send({ error: 'Git operation failed' });
203
+ }
204
+ resolve();
205
+ });
206
+ });
207
+ }
package/src/server.js CHANGED
@@ -8,6 +8,7 @@ import { getCorsHeaders } from './ldp/headers.js';
8
8
  import { authorize, handleUnauthorized } from './auth/middleware.js';
9
9
  import { notificationsPlugin } from './notifications/index.js';
10
10
  import { idpPlugin } from './idp/index.js';
11
+ import { isGitRequest, isGitWriteOperation, handleGit } from './handlers/git.js';
11
12
 
12
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
14
 
@@ -23,6 +24,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
23
24
  * @param {string} options.root - Data directory path (default from env or ./data)
24
25
  * @param {boolean} options.subdomains - Enable subdomain-based pods for XSS protection (default false)
25
26
  * @param {string} options.baseDomain - Base domain for subdomain pods (e.g., "example.com")
27
+ * @param {boolean} options.git - Enable Git HTTP backend for clone/push (default false)
26
28
  */
27
29
  export function createServer(options = {}) {
28
30
  // Content negotiation is OFF by default - we're a JSON-LD native server
@@ -40,6 +42,8 @@ export function createServer(options = {}) {
40
42
  const mashlibEnabled = options.mashlib ?? false;
41
43
  const mashlibCdn = options.mashlibCdn ?? false;
42
44
  const mashlibVersion = options.mashlibVersion ?? '2.0.0';
45
+ // Git HTTP backend is OFF by default - enables clone/push via git protocol
46
+ const gitEnabled = options.git ?? false;
43
47
 
44
48
  // Set data root via environment variable if provided
45
49
  if (options.root) {
@@ -128,16 +132,64 @@ export function createServer(options = {}) {
128
132
  // Note: OPTIONS requests are handled by handleOptions to include Accept-* headers
129
133
  });
130
134
 
135
+ // Security: Block access to dotfiles except allowed Solid-specific ones
136
+ // This prevents exposure of .git/, .env, .htpasswd, etc.
137
+ // Git protocol requests bypass this check when git is enabled
138
+ const ALLOWED_DOTFILES = ['.well-known', '.acl', '.meta'];
139
+ fastify.addHook('onRequest', async (request, reply) => {
140
+ // Allow git protocol requests through when git is enabled
141
+ if (gitEnabled && isGitRequest(request.url)) {
142
+ return;
143
+ }
144
+
145
+ const segments = request.url.split('/').map(s => s.split('?')[0]); // Remove query strings
146
+ const hasForbiddenDotfile = segments.some(seg =>
147
+ seg.startsWith('.') &&
148
+ seg.length > 1 &&
149
+ !ALLOWED_DOTFILES.includes(seg)
150
+ );
151
+
152
+ if (hasForbiddenDotfile) {
153
+ return reply.code(403).send({ error: 'Forbidden', message: 'Dotfile access is not allowed' });
154
+ }
155
+ });
156
+
157
+ // Git HTTP backend handler - uses git http-backend CGI
158
+ // Authorization: Read for clone/fetch, Write for push
159
+ if (gitEnabled) {
160
+ fastify.addHook('preHandler', async (request, reply) => {
161
+ if (!isGitRequest(request.url)) {
162
+ return;
163
+ }
164
+
165
+ // Run WAC authorization - checkAccess already verifies the required mode
166
+ const { authorized, webId, wacAllow, authError } = await authorize(request, reply);
167
+ request.webId = webId;
168
+ request.wacAllow = wacAllow;
169
+
170
+ if (!authorized) {
171
+ const needsWrite = isGitWriteOperation(request.url);
172
+ const message = needsWrite ? 'Write access required for push' : 'Read access required for clone';
173
+ reply.header('WAC-Allow', wacAllow);
174
+ return reply.code(webId ? 403 : 401).send({ error: message });
175
+ }
176
+
177
+ // Handle the git request directly
178
+ return handleGit(request, reply);
179
+ });
180
+ }
181
+
131
182
  // Authorization hook - check WAC permissions
132
183
  // Skip for pod creation endpoint (needs special handling)
133
184
  fastify.addHook('preHandler', async (request, reply) => {
134
- // Skip auth for pod creation, OPTIONS, IdP routes, mashlib, well-known, and notifications
185
+ // Skip auth for pod creation, OPTIONS, IdP routes, mashlib, well-known, notifications, and git
135
186
  const mashlibPaths = ['/mashlib.min.js', '/mash.css', '/841.mashlib.min.js'];
136
187
  if (request.url === '/.pods' ||
137
188
  request.url === '/.notifications' ||
138
189
  request.method === 'OPTIONS' ||
139
190
  request.url.startsWith('/idp/') ||
140
191
  request.url.startsWith('/.well-known/') ||
192
+ (gitEnabled && isGitRequest(request.url)) ||
141
193
  mashlibPaths.some(p => request.url === p || request.url.startsWith(p + '.'))) {
142
194
  return;
143
195
  }