Files
werkator/docs/prs/2026-10-03-PR#96-artifact-cors.md
miandmhoennig fd44422a9f
werkator build succeeded after 01:50
werkator/deploy build succeeded after 00:06
Artifacts readable from allowed websites (v1.30.0) (#96)
## The Problem

werkcrappit's code city page links a CodeCharta viewer with the URL of the `cc.json` beside it.
Served by Werkator, the viewer at javagil.de could not load it: the browser blocks a script from reading another site's response unless that site allows it with `Access-Control-Allow-Origin`.
CodeCharta reported only `Http failure response … 0 Unknown Error` and loaded its sample files instead.

## Non-Goals

- Credentials: no `Access-Control-Allow-Credentials`, so a private repository's artifacts stay unreadable from other sites.
- Preflight handling: a plain `GET` without own headers needs none.
- Other routes than artifact files: pages and the API keep answering without CORS.

## The Scenarios

#### Scenario#96.01: an artifact file answers an allowed origin

… so that a viewer on another host, such as CodeCharta at javagil.de, loads a code city straight from Werkator.

- **Given** `server.corsAllowedOrigins` in the instance file lists `https://javagil.de`
- **When** the browser fetches an artifact file of a public repository with `Origin: https://javagil.de`
- **Then** the response carries `Access-Control-Allow-Origin: https://javagil.de` and `Vary: Origin`
- **And** no `Access-Control-Allow-Credentials`, so a token or cookie never reaches the other origin

##### Verified by

- [ArtifactFileControllerRestTest](../../src/test/kotlin/de/hoennig/werkator/server/ArtifactFileControllerRestTest.kt) `an artifact file answers an allowed origin, without credentials`

#### Scenario#96.02: no header for other origins or without the setting

… so that Werkator shares nothing with a website the instance did not name.

- **Given** `server.corsAllowedOrigins` lists `https://javagil.de`
- **When** the browser fetches an artifact file with `Origin: https://evil.example`, or without `Origin`
- **Then** the response carries no `Access-Control-Allow-Origin`
- **And** without the setting, the default, no response carries one, as before

##### Verified by

- [ArtifactFileControllerRestTest](../../src/test/kotlin/de/hoennig/werkator/server/ArtifactFileControllerRestTest.kt) `another origin, or none, gets no CORS header`
- [ConfigLoaderUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderUnitTest.kt) `without a setting no origin may read artifacts`

#### Scenario#96.03: a repository cannot widen the setting

… so that only the operator of an instance decides which websites may read its artifacts.

- **Given** a repository's `.werkator.yml`, committed or on a branch, sets `server.corsAllowedOrigins`
- **Then** that value is ignored, with the same warning as other `server` keys outside the instance file
- **And** a private repository still answers 404 without the control token, `Origin` or not

##### Verified by

- [ConfigLoaderUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderUnitTest.kt) `the origins allowed to read artifacts are the instance's`
- [ArtifactFileControllerRestTest](../../src/test/kotlin/de/hoennig/werkator/server/ArtifactFileControllerRestTest.kt) `a private repository stays hidden to an allowed origin`

## The Solution

- `ServerConfig.corsAllowedOrigins`, a list of exact origins, empty by default.
  The `server` section is instance-level: with an instance file, a repository's copy is ignored with a warning, and a branch layer never carries it.
  Without an instance file, the repository's committed file is the instance's, as for every `server` key.
- `ArtifactFileController` answers both artifact routes, by key and the permanent branch URL, with the request's `Origin` when it is listed, and always with `Vary: Origin`, so a cache never hands one site's answer to another.
- The commented `init` template and `docs/configuration.md` name the key.

## Open Questions

- mih09 needs `server.corsAllowedOrigins: ["https://javagil.de", "https://codecharta.com"]` in `~/.werkator.yml`; read per request, no restart needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: mhoennig <michael@hoennig.de>
Reviewed-on: #96
2026-10-03 15:28:20 +02:00

4.0 KiB

PR#96: Artifacts Readable From Allowed Websites

This document describes the change of this pull request at the time it was made. Later pull requests may change the behaviour without updating it.

The Problem

werkcrappit's code city page links a CodeCharta viewer with the URL of the cc.json beside it. Served by Werkator, the viewer at javagil.de could not load it: the browser blocks a script from reading another site's response unless that site allows it with Access-Control-Allow-Origin. CodeCharta reported only Http failure response … 0 Unknown Error and loaded its sample files instead.

Non-Goals

  • Credentials: no Access-Control-Allow-Credentials, so a private repository's artifacts stay unreadable from other sites.
  • Preflight handling: a plain GET without own headers needs none.
  • Other routes than artifact files: pages and the API keep answering without CORS.

The Scenarios

Scenario#96.01: an artifact file answers an allowed origin

… so that a viewer on another host, such as CodeCharta at javagil.de, loads a code city straight from Werkator.

  • Given server.corsAllowedOrigins in the instance file lists https://javagil.de
  • When the browser fetches an artifact file of a public repository with Origin: https://javagil.de
  • Then the response carries Access-Control-Allow-Origin: https://javagil.de and Vary: Origin
  • And no Access-Control-Allow-Credentials, so a token or cookie never reaches the other origin
Verified by

Scenario#96.02: no header for other origins or without the setting

… so that Werkator shares nothing with a website the instance did not name.

  • Given server.corsAllowedOrigins lists https://javagil.de
  • When the browser fetches an artifact file with Origin: https://evil.example, or without Origin
  • Then the response carries no Access-Control-Allow-Origin
  • And without the setting, the default, no response carries one, as before
Verified by

Scenario#96.03: a repository cannot widen the setting

… so that only the operator of an instance decides which websites may read its artifacts.

  • Given a repository's .werkator.yml, committed or on a branch, sets server.corsAllowedOrigins
  • Then that value is ignored, with the same warning as other server keys outside the instance file
  • And a private repository still answers 404 without the control token, Origin or not
Verified by

The Solution

  • ServerConfig.corsAllowedOrigins, a list of exact origins, empty by default. The server section is instance-level: with an instance file, a repository's copy is ignored with a warning, and a branch layer never carries it. Without an instance file, the repository's committed file is the instance's, as for every server key.
  • ArtifactFileController answers both artifact routes, by key and the permanent branch URL, with the request's Origin when it is listed, and always with Vary: Origin, so a cache never hands one site's answer to another.
  • The commented init template and docs/configuration.md name the key.

Open Questions

  • mih09 needs server.corsAllowedOrigins: ["https://javagil.de", "https://codecharta.com"] in ~/.werkator.yml; read per request, no restart needed.