Skip to content

Commit 666f7db

Browse files
fix(git): WAC preHandler 401/402/403 responses carry the git CORS headers (JavaScriptSolidServer#548) (JavaScriptSolidServer#550)
The 401/403 for an unauthorized git operation is emitted by the WAC preHandler in src/server.js — before handleGit runs — and that path set WAC-Allow / WWW-Authenticate but not the git CORS headers. A browser-based git client (e.g. jss.live/git/) hitting an auth-gated repo therefore saw a generic CORS/network error instead of the 401 auth challenge — the exact failure mode JavaScriptSolidServer#371 fixed for handleGit's own paths. Fix: export setGitCorsHeaders from src/handlers/git.js (it was module-private) and apply it on the preHandler's early returns — the 402 paymentRequired branch and the 401/403 unauthorized branch. The GIT_CORS_HEADERS doc comment is updated to name the new caller so it stays the single source of truth. The 402 branch gets the same one-line helper call as the tested 401/403 path; a dedicated 402 test would need a pay-gated git fixture, which is disproportionate to an identical mechanical call — the 401 test pins the helper's output. Test: the deliberately-scoped 401 test from JavaScriptSolidServer#549 (which asserted WWW-Authenticate but NOT CORS, with a pointer comment at this issue) now asserts the full contract via assertGitCors. Test title, inline comment, and the file docstring all updated to match the new behaviour. Full suite: 934/934 passing. Closes JavaScriptSolidServer#548.
1 parent 6e92f35 commit 666f7db

3 files changed

Lines changed: 25 additions & 13 deletions

File tree

src/handlers/git.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,16 +167,18 @@ function tryAutoInitRepo(repoAbs, log) {
167167

168168
// CORS headers for git responses. Single source of truth — used by the
169169
// success path (Fastify reply on the OPTIONS preflight, raw stream on
170-
// http-backend output) and by every 4xx early-return. Without these,
171-
// browser-based git clients (e.g. jss.live/git/) see a generic
172-
// CORS/network error instead of the actual status, undermining #371.
170+
// http-backend output), by every 4xx early-return in this module, and
171+
// by the WAC preHandler's 401/402/403 returns in src/server.js (#548).
172+
// Without these, browser-based git clients (e.g. jss.live/git/) see a
173+
// generic CORS/network error instead of the actual status,
174+
// undermining #371.
173175
const GIT_CORS_HEADERS = {
174176
'Access-Control-Allow-Origin': '*',
175177
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
176178
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Git-Protocol',
177179
};
178180

179-
function setGitCorsHeaders(reply) {
181+
export function setGitCorsHeaders(reply) {
180182
for (const [k, v] of Object.entries(GIT_CORS_HEADERS)) {
181183
reply.header(k, v);
182184
}

src/server.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { idpPlugin } from './idp/index.js';
1717
// below so non-IdP deployments don't pull in the IdP accounts module
1818
// (bcryptjs etc.) just to register Fastify routes. The same lazy-load
1919
// pattern is used in src/auth/nostr.js for the NIP-98 verifier.
20-
import { isGitRequest, isGitWriteOperation, handleGit } from './handlers/git.js';
20+
import { isGitRequest, isGitWriteOperation, handleGit, setGitCorsHeaders } from './handlers/git.js';
2121
import { handleCorsProxy, isCorsProxyRequest, setProxyCorsHeaders } from './handlers/cors-proxy.js';
2222
import { AccessMode } from './wac/parser.js';
2323
import { registerNostrRelay } from './nostr/relay.js';
@@ -533,11 +533,20 @@ export function createServer(options = {}) {
533533
request.wacAllow = wacAllow;
534534

535535
if (paymentRequired) {
536+
// Git CORS headers on the early return — same reasoning as the
537+
// 401/403 below: a browser git client must see the 402, not a
538+
// generic CORS/network error. See #548 / #371.
539+
setGitCorsHeaders(reply);
536540
return reply.code(402).send({ type: 'PaymentRequired', ...paymentRequired });
537541
}
538542

539543
if (!authorized) {
540544
const message = needsWrite ? 'Write access required for push' : 'Read access required for clone';
545+
// Without the git CORS headers, browser-based git clients (e.g.
546+
// jss.live/git/) hitting an auth-gated repo saw a generic CORS
547+
// error instead of this 401/403 — the same failure mode #371
548+
// fixed inside handleGit. See #548.
549+
setGitCorsHeaders(reply);
541550
reply.header('WAC-Allow', wacAllow);
542551
if (!webId) {
543552
// No authentication - request Basic auth for git clients

test/git-handler.test.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
* them a browser git client sees a CORS error, not the 404)
1414
* - path traversal attempts are blocked (never 200, never 500)
1515
* with CORS headers
16-
* - unauthenticated push advertise → 401 + WWW-Authenticate
17-
* (NOTE: this 401 is emitted by the server.js WAC preHandler,
18-
* which does NOT currently set the git CORS headers — that gap is
19-
* tracked separately; the test asserts what is true today)
16+
* - unauthenticated push advertise → 401 + WWW-Authenticate + the
17+
* git CORS headers (the preHandler's 401/403 path lacked them
18+
* until #548; this 401 is emitted by the server.js WAC
19+
* preHandler, not by handleGit)
2020
* - a real end-to-end `git push` succeeds and the pushed file is
2121
* served as a static resource (receive-pack POST through
2222
* http-backend + updateInstead extraction)
@@ -179,7 +179,7 @@ describe('git handler HTTP contract (#375)', () => {
179179
assertGitCors(res, 'traversal block');
180180
});
181181

182-
it('unauthenticated push advertise returns 401 with WWW-Authenticate (non-public server)', async () => {
182+
it('unauthenticated push advertise returns 401 with WWW-Authenticate and git CORS headers (non-public server, #548)', async () => {
183183
// Second server WITHOUT public:true so the WAC preHandler gates
184184
// the push. Snapshot/restore DATA_ROOT — createServer({ root })
185185
// mutates it process-wide and the main suite's server reads it
@@ -198,9 +198,10 @@ describe('git handler HTTP contract (#375)', () => {
198198
`unauthenticated push advertise must be 401, got ${res.status}`);
199199
assert.ok(res.headers.get('www-authenticate'),
200200
'WWW-Authenticate must be present so git CLI clients prompt for credentials');
201-
// Deliberately NOT asserting CORS here: the WAC preHandler's
202-
// 401 path does not currently set the git CORS headers (tracked
203-
// as a follow-up). Assert truth, not aspiration.
201+
// #548: the WAC preHandler's 401/403 path must carry the git
202+
// CORS headers, or browser git clients see a generic CORS error
203+
// instead of the auth challenge.
204+
assertGitCors(res, 'unauthenticated push 401');
204205
} finally {
205206
await authServer.close();
206207
if (originalDataRoot === undefined) delete process.env.DATA_ROOT;

0 commit comments

Comments
 (0)