javascript-solid-server 0.0.170 → 0.0.171

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/handlers/git.js +44 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.170",
3
+ "version": "0.0.171",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -24,9 +24,11 @@ export function isGitWriteOperation(urlPath) {
24
24
  }
25
25
 
26
26
  /**
27
- * Extract the repository path from the URL with path traversal protection
27
+ * Extract the repository path from the URL with path traversal protection.
28
+ * Always returns a non-empty string: '.' for root/empty URL paths, the
29
+ * cleaned relative path otherwise.
28
30
  * @param {string} urlPath - The URL path
29
- * @returns {string|null} The repository relative path or null
31
+ * @returns {string} The repository relative path ('.' for root)
30
32
  */
31
33
  function extractRepoPath(urlPath) {
32
34
  // Remove git service suffixes to get the repo path
@@ -87,6 +89,23 @@ function findGitDir(repoPath) {
87
89
  return null;
88
90
  }
89
91
 
92
+ // CORS headers for git responses. Single source of truth — used by the
93
+ // success path (Fastify reply on the OPTIONS preflight, raw stream on
94
+ // http-backend output) and by every 4xx early-return. Without these,
95
+ // browser-based git clients (e.g. jss.live/git/) see a generic
96
+ // CORS/network error instead of the actual status, undermining #371.
97
+ const GIT_CORS_HEADERS = {
98
+ 'Access-Control-Allow-Origin': '*',
99
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
100
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, Git-Protocol',
101
+ };
102
+
103
+ function setGitCorsHeaders(reply) {
104
+ for (const [k, v] of Object.entries(GIT_CORS_HEADERS)) {
105
+ reply.header(k, v);
106
+ }
107
+ }
108
+
90
109
  /**
91
110
  * Handle Git HTTP requests using git http-backend
92
111
  * @param {FastifyRequest} request
@@ -95,20 +114,28 @@ function findGitDir(repoPath) {
95
114
  export async function handleGit(request, reply) {
96
115
  // Handle CORS preflight
97
116
  if (request.method === 'OPTIONS') {
98
- reply.header('Access-Control-Allow-Origin', '*');
99
- reply.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
100
- reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Git-Protocol');
117
+ setGitCorsHeaders(reply);
101
118
  return reply.code(200).send();
102
119
  }
103
120
 
104
- const urlPath = decodeURIComponent(request.url.split('?')[0]);
121
+ // Collapse multi-slash sequences before they reach extractRepoPath or
122
+ // get forwarded as PATH_INFO. git http-backend rejects paths like
123
+ // `/foo/test//info/refs` as "aliased" and JSS would otherwise 500.
124
+ // Frontends and bots both produce these (frontend appends `/info/refs`
125
+ // to a URL the user ended with `/`, bots probe `///wp-admin/...`).
126
+ // Same shape as the LDP fix in src/utils/url.js (#131).
127
+ //
128
+ // Note: Fastify's URL parser rejects malformed percent-encoding
129
+ // (`%g1`, truncated `%E0%`, invalid UTF-8 like `%C3%28`) with a 400
130
+ // FST_ERR_BAD_URL before this handler runs — verified empirically — so
131
+ // decodeURIComponent here is safe in practice on current Fastify.
132
+ let urlPath = decodeURIComponent(request.url.split('?')[0]);
133
+ urlPath = urlPath.replace(/\/{2,}/g, '/');
105
134
  const queryString = request.url.split('?')[1] || '';
106
135
 
107
- // Extract repository path
136
+ // extractRepoPath always returns a non-empty string ('.' for root) —
137
+ // no null check needed.
108
138
  const repoRelative = extractRepoPath(urlPath);
109
- if (!repoRelative) {
110
- return reply.code(400).send({ error: 'Invalid git request' });
111
- }
112
139
 
113
140
  // Handle subdomain mode
114
141
  let dataRoot = getDataRoot();
@@ -120,12 +147,14 @@ export async function handleGit(request, reply) {
120
147
 
121
148
  // Security: verify resolved path is within data root (path traversal protection)
122
149
  if (!isPathWithinDataRoot(repoAbs, getDataRoot())) {
150
+ setGitCorsHeaders(reply);
123
151
  return reply.code(403).send({ error: 'Path traversal detected' });
124
152
  }
125
153
 
126
154
  // Find git directory
127
155
  const gitInfo = findGitDir(repoAbs);
128
156
  if (!gitInfo) {
157
+ setGitCorsHeaders(reply);
129
158
  return reply.code(404).send({ error: 'Not a git repository' });
130
159
  }
131
160
 
@@ -214,10 +243,11 @@ export async function handleGit(request, reply) {
214
243
  }
215
244
  }
216
245
 
217
- // Add CORS headers for browser git clients
218
- reply.raw.setHeader('Access-Control-Allow-Origin', '*');
219
- reply.raw.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
220
- reply.raw.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Git-Protocol');
246
+ // Add CORS headers for browser git clients (same set as the
247
+ // 4xx return paths, kept in sync via GIT_CORS_HEADERS).
248
+ for (const [k, v] of Object.entries(GIT_CORS_HEADERS)) {
249
+ reply.raw.setHeader(k, v);
250
+ }
221
251
 
222
252
  reply.raw.writeHead(statusCode);
223
253
  headersSent = true;