chore(openspec): archive host-agnostic-object-urls and sync main specs
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-11
|
||||
@@ -0,0 +1,69 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
See `proposal.md` - Why. Relevant current state:
|
||||
|
||||
- `storage.Signer.URL` builds `baseURL + "/api/objects/<key>?e&s"` (`signer.go`), with
|
||||
`baseURL` from `cfg.Server.BaseURL` (`APP_BASE_URL`, default `http://localhost:8080`).
|
||||
- That absolute URL is the only use of `Server.BaseURL`; SSE and API traffic are already
|
||||
root-relative (`/api/...`).
|
||||
- `photos.Service.attachURLs` runs outside an HTTP request, so it cannot see the request
|
||||
host to derive a base.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Photo/preview URLs work on any host or reverse proxy with no per-deployment base URL.
|
||||
- Preserve signing, expiry, quantization and caching behavior exactly.
|
||||
|
||||
**Non-Goals:**
|
||||
- Serving the app under a URL subpath.
|
||||
- Absolute URL generation for non-browser consumers (no such consumer today).
|
||||
- Changing the object route, storage backends, or delivery semantics.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Emit origin-relative (path-only) object URLs
|
||||
`Signer.URL(key)` returns `"/api/objects/<escaped-key>?e=<exp>&s=<sig>"`. A leading slash
|
||||
makes it origin-relative: the browser resolves it against the page's scheme/host/port,
|
||||
which is exactly the backend that served the page.
|
||||
|
||||
- Alternative considered: keep absolute URLs and require operators to set
|
||||
`APP_BASE_URL`. Rejected - this is the current footgun; a wrong or default value
|
||||
silently breaks all images.
|
||||
- Alternative considered: derive the base from the request (`Host`,
|
||||
`X-Forwarded-Proto`). Rejected - URLs are minted in `attachURLs`/SSE paths that have no
|
||||
request; threading a base through every service is invasive for no benefit once URLs
|
||||
are relative.
|
||||
- Alternative considered: document-relative URLs without a leading slash. Rejected - the
|
||||
app and its API calls already use root-relative paths, so root-relative is consistent.
|
||||
|
||||
### Remove the base URL from the signer and config
|
||||
Drop the `baseURL` field and parameter from `Signer`/`NewSigner`, stop reading
|
||||
`cfg.Server.BaseURL` in `storage.Open`, and remove `Server.BaseURL` from config,
|
||||
`.env.example` and the README. Leaving a now-unused variable would invite the same
|
||||
misconfiguration.
|
||||
|
||||
### Verification unchanged
|
||||
`VerifyRequest` checks the key and query (`e`, `s`) only; it never used the host, so
|
||||
signing and validation are untouched.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Subpath deployment (`https://host/galo/`) would break with root-relative URLs] ->
|
||||
The app already assumes root (`/api/...`); subpath support would be a separate change
|
||||
adding a base-path prefix. Documented as out of scope.
|
||||
- [Non-browser clients need a base to resolve object URLs] -> None consume them today;
|
||||
if one appears, it resolves against the instance base like any HTTP client.
|
||||
- [Tests that assumed an absolute URL] -> They parse the URL and use `Path`/`RawQuery`;
|
||||
update `NewSigner` call sites to drop the base argument.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Remove `APP_BASE_URL` from the server environment and deploy the backend. No data or
|
||||
schema change. Rollback is reverting the deploy.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None blocking.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Object URLs are absolute and built from `APP_BASE_URL`, whose default is
|
||||
`http://localhost:8080`. On a server reached by any other address the browser is told to
|
||||
fetch thumbnails and originals from `localhost`, so no photo or preview loads. This was
|
||||
observed on a deployment served from `http://192.168.77.82:8080/` with the default
|
||||
config: pages, login and album lists work (relative `/api/...`), but every image URL
|
||||
points at the client's own machine.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Emit same-origin object URLs: the signer returns a path-only URL
|
||||
(`/api/objects/<key>?e=...&s=...`) instead of an absolute one.
|
||||
- Remove the base URL from the signer (`Signer`/`NewSigner`) since it is no longer
|
||||
needed; verification is unchanged (it uses the key and query only).
|
||||
- **BREAKING** (config): remove `APP_BASE_URL` / `Server.BaseURL`, which after this has
|
||||
no remaining use (SSE and API calls are already relative). Update `.env.example` and
|
||||
the README.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
<!-- none -->
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `storage-backends`: add a requirement that issued object URLs are host-independent
|
||||
(same-origin), so the app works behind any host or reverse proxy without calibrated
|
||||
base-URL configuration.
|
||||
|
||||
## Impact
|
||||
|
||||
- Backend: `backend/internal/storage/signer.go`, `storage.go`
|
||||
(`Open`/`NewSigner` call), `backend/internal/config/config.go` (`Server.BaseURL`).
|
||||
- Docs/config: `.env.example`, `README.md`.
|
||||
- Tests: signer/fs/objects/app/e2e call sites of `NewSigner` / `Server.BaseURL`.
|
||||
- Frontend: unchanged; relative URLs resolve against the page origin. The `sameOrigin`
|
||||
helper in `PanoViewer` becomes a no-op but harmless.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Spec Delta
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Хост-независимые URL объектов
|
||||
Выдаваемый клиенту подписанный URL объекта SHALL быть относительным к текущему источнику
|
||||
(same-origin) и SHALL не содержать настроенный хост. Просмотр и скачивание медиа SHALL
|
||||
работать за любым хостом или обратным прокси без настройки внешнего базового URL.
|
||||
|
||||
#### Scenario: Другой хост
|
||||
- **WHEN** приложение доступно по адресу, отличному от адреса по умолчанию, и клиент запрашивает фото
|
||||
- **THEN** URL объекта указывает на тот же источник, что и страница, и превью загружается без дополнительной настройки
|
||||
|
||||
#### Scenario: Обратный прокси
|
||||
- **WHEN** приложение доступно за обратным прокси
|
||||
- **THEN** URL объектов сохраняют тот же источник, что и страница приложения
|
||||
|
||||
#### Scenario: Подпись сохраняется
|
||||
- **WHEN** клиент получает относительный URL объекта
|
||||
- **THEN** подпись, срок действия и кэширование объекта работают как прежде
|
||||
@@ -0,0 +1,20 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Make object URLs origin-relative
|
||||
|
||||
- [x] 1.1 In `backend/internal/storage/signer.go`, return a path-only URL (`/api/objects/<key>?e&s`) and drop the `baseURL` field/parameter from `Signer`/`NewSigner`; verify with `go test ./internal/storage/...`.
|
||||
- [x] 1.2 In `backend/internal/storage/storage.go`, stop passing `cfg.Server.BaseURL` to `NewSigner`; verify with `go build ./...`.
|
||||
|
||||
## 2. Remove the base-URL configuration
|
||||
|
||||
- [x] 2.1 Remove `Server.BaseURL` from `backend/internal/config/config.go` and delete the `APP_BASE_URL` line from `.env.example` and the README table; verify `go build ./...` and that no `APP_BASE_URL`/`Server.BaseURL` references remain.
|
||||
- [x] 2.2 Update every other `storage.NewSigner(...)` call site (tests/helpers in `internal/storage`, `internal/photos`, `internal/app`, `internal/e2e`) to drop the base argument; verify with `make test`.
|
||||
|
||||
## 3. Integration verification
|
||||
|
||||
- [x] 3.1 Run `make test` (backend) and `npm run build` (frontend); verify both pass and that an issued object URL has no scheme or host.
|
||||
|
||||
## Workflow follow-up
|
||||
|
||||
- Run `/opsx-apply` to implement this change.
|
||||
- Archive the change after review and verification (sync the delta spec into `storage-backends`).
|
||||
Reference in New Issue
Block a user