detached 85f934a

fix: expose webkit config api to trusted fetch

commit
85f934a5e0fb909aa890b7b4d7eec761c60d9441
author
brice · · 2026-06-24 04:43 UTC
parent
d823634
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +40 -1
M internal/infrastructure/webkit/scheme_handler.go +12 -1
@@ -309,15 +309,26 @@ func privateJSONErrorResponse(status int, message string) *SchemeResponse {
309309 return resp
310310 }
311311
312+func trustedPrivateAPIHeaders() map[string]string {
313+ return map[string]string{
314+ "Access-Control-Allow-Origin": "*",
315+ "Access-Control-Allow-Methods": "GET, OPTIONS",
316+ "Access-Control-Allow-Headers": "Content-Type",
317+ "Cache-Control": "no-store",
318+ }
319+}
320+
312321 func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse {
313322 if build == nil {
314323 resp := privateJSONErrorResponse(http.StatusInternalServerError, "config payload builder not configured")
324+ resp.Headers = trustedPrivateAPIHeaders()
315325 return resp
316326 }
317327
318328 data, err := build()
319329 if err != nil {
320330 resp := privateJSONErrorResponse(http.StatusInternalServerError, err.Error())
331+ resp.Headers = trustedPrivateAPIHeaders()
321332 return resp
322333 }
323334
@@ -325,7 +336,7 @@ func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse {
325336 Data: data,
326337 ContentType: "application/json",
327338 StatusCode: http.StatusOK,
328- Headers: map[string]string{"Cache-Control": "no-store"},
339+ Headers: trustedPrivateAPIHeaders(),
329340 SuppressDefaultHeaders: true,
330341 }
331342 }
@@ -309,15 +309,26 @@ func privateJSONErrorResponse(status int, message string) *SchemeResponse {
309 return resp309 return resp
310 }310 }
311 311
312+func trustedPrivateAPIHeaders() map[string]string {
313+ return map[string]string{
314+ "Access-Control-Allow-Origin": "*",
315+ "Access-Control-Allow-Methods": "GET, OPTIONS",
316+ "Access-Control-Allow-Headers": "Content-Type",
317+ "Cache-Control": "no-store",
318+ }
319+}
320+
312 func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse {321 func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse {
313 if build == nil {322 if build == nil {
314 resp := privateJSONErrorResponse(http.StatusInternalServerError, "config payload builder not configured")323 resp := privateJSONErrorResponse(http.StatusInternalServerError, "config payload builder not configured")
324+ resp.Headers = trustedPrivateAPIHeaders()
315 return resp325 return resp
316 }326 }
317 327
318 data, err := build()328 data, err := build()
319 if err != nil {329 if err != nil {
320 resp := privateJSONErrorResponse(http.StatusInternalServerError, err.Error())330 resp := privateJSONErrorResponse(http.StatusInternalServerError, err.Error())
331+ resp.Headers = trustedPrivateAPIHeaders()
321 return resp332 return resp
322 }333 }
323 334
@@ -325,7 +336,7 @@ func buildConfigResponse(build func() ([]byte, error)) *SchemeResponse {
325 Data: data,336 Data: data,
326 ContentType: "application/json",337 ContentType: "application/json",
327 StatusCode: http.StatusOK,338 StatusCode: http.StatusOK,
328- Headers: map[string]string{"Cache-Control": "no-store"},339+ Headers: trustedPrivateAPIHeaders(),
329 SuppressDefaultHeaders: true,340 SuppressDefaultHeaders: true,
330 }341 }
331 }342 }
M internal/infrastructure/webkit/scheme_handler_test.go +28 -0
@@ -268,6 +268,34 @@ func TestConfigHandlersAllowOpaqueOriginWithTrustedReferer(t *testing.T) {
268268 assert.Equal(t, []byte(`{"current":true}`), resp.Data)
269269 }
270270
271+func TestConfigHandlersExposeTrustedResponsesToWebKitFetch(t *testing.T) {
272+ h := NewDumbSchemeHandler(context.Background())
273+ h.SetConfigPayloadBuilders(
274+ func() ([]byte, error) { return []byte(`{"current":true}`), nil },
275+ func() ([]byte, error) { return []byte(`{"default":true}`), nil },
276+ )
277+
278+ h.mu.RLock()
279+ currentHandler := h.handlers["/api/config"]
280+ h.mu.RUnlock()
281+ require.NotNil(t, currentHandler)
282+
283+ resp := currentHandler.Handle(&SchemeRequest{
284+ URI: "dumb://config/api/config",
285+ Path: "/api/config",
286+ Method: http.MethodGet,
287+ Scheme: "dumb",
288+ Origin: "null",
289+ Referer: "dumb://config",
290+ })
291+
292+ require.NotNil(t, resp)
293+ assert.Equal(t, http.StatusOK, resp.StatusCode)
294+ assert.Equal(t, "*", resp.Headers["Access-Control-Allow-Origin"])
295+ assert.Equal(t, "GET, OPTIONS", resp.Headers["Access-Control-Allow-Methods"])
296+ assert.Equal(t, "Content-Type", resp.Headers["Access-Control-Allow-Headers"])
297+}
298+
271299 func TestShouldAddCORSHeaders(t *testing.T) {
272300 t.Parallel()
273301
@@ -268,6 +268,34 @@ func TestConfigHandlersAllowOpaqueOriginWithTrustedReferer(t *testing.T) {
268 assert.Equal(t, []byte(`{"current":true}`), resp.Data)268 assert.Equal(t, []byte(`{"current":true}`), resp.Data)
269 }269 }
270 270
271+func TestConfigHandlersExposeTrustedResponsesToWebKitFetch(t *testing.T) {
272+ h := NewDumbSchemeHandler(context.Background())
273+ h.SetConfigPayloadBuilders(
274+ func() ([]byte, error) { return []byte(`{"current":true}`), nil },
275+ func() ([]byte, error) { return []byte(`{"default":true}`), nil },
276+ )
277+
278+ h.mu.RLock()
279+ currentHandler := h.handlers["/api/config"]
280+ h.mu.RUnlock()
281+ require.NotNil(t, currentHandler)
282+
283+ resp := currentHandler.Handle(&SchemeRequest{
284+ URI: "dumb://config/api/config",
285+ Path: "/api/config",
286+ Method: http.MethodGet,
287+ Scheme: "dumb",
288+ Origin: "null",
289+ Referer: "dumb://config",
290+ })
291+
292+ require.NotNil(t, resp)
293+ assert.Equal(t, http.StatusOK, resp.StatusCode)
294+ assert.Equal(t, "*", resp.Headers["Access-Control-Allow-Origin"])
295+ assert.Equal(t, "GET, OPTIONS", resp.Headers["Access-Control-Allow-Methods"])
296+ assert.Equal(t, "Content-Type", resp.Headers["Access-Control-Allow-Headers"])
297+}
298+
271 func TestShouldAddCORSHeaders(t *testing.T) {299 func TestShouldAddCORSHeaders(t *testing.T) {
272 t.Parallel()300 t.Parallel()
273 301

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close