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

71 lines
4.0 KiB
Markdown

# 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
- [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.