ADR 0030 left exactly one way to get an image to a browser: read the
object into the module's process and write it back out of a module HTTP
route. Every byte of every asset travelled storage -> Ekklesia -> browser.
For the slideshow this platform exists to replace that means Ekklesia in
the path of every image on every display, permanently, for bytes that
never change — and it would make Ekklesia responsible for re-implementing
byte ranges, without which video seeking cannot work at all.
A module can now ask a logical bucket for a temporary browser URL:
const { url, expiresAt } = await assets.createReadUrl("slides/welcome.png");
The contract names intent, not mechanism. There is no signing, expiry
format, endpoint, or bucket in it — a host with no S3 anywhere could
satisfy the same method, which is what keeps a future single-module
standalone host able to implement it. The URL is opaque to module code and
a bearer capability to whoever holds it, so a module authorizes the
request in its own route before asking: storage bounds reach, never
identity. Lifetimes run one minute to six hours, fifteen by default, and
out-of-range values are refused rather than clamped, so "expires forever"
is unreachable. Logical keys stay canonical state; URLs are generated on
demand and never persisted or logged.
The provider endpoint and the browser delivery origin are separated
because measurement showed they cannot be collapsed: an S3-compatible
signature covers the request Host, so a URL signed for one origin and
rewritten to another is rejected. The browser-facing origin is therefore a
signing input (S3_DELIVERY_ENDPOINT), defaulting to S3_ENDPOINT — so the
standard deployment configures nothing and delivery just works. The scheme
is not signed, so a reverse proxy may terminate TLS provided it passes the
original Host through; that is pinned by a test rather than described and
hoped for.
Read URLs are stable within a window of one third of their lifetime.
Signing is a pure function of the clock, so naive re-signing yields a new
URL per second — a guaranteed browser cache miss, and the reference client
keys its <img> by URL, so an unchanged image would be torn down and
re-downloaded continuously. Quantizing the signing timestamp makes
repeated calls byte-identical while leaving at least two-thirds of the
lifetime; Cache-Control is set at signing time to the guaranteed
remainder, because the pinned SeaweedFS image sends none of its own.
Verified against live SeaweedFS: byte ranges answered directly with 206
and a correct Content-Range, so no range proxy is needed in Core; signing
is local (200 URLs in 66ms), so no batch API is needed; unsigned requests
for the same object are refused, so delivery never depends on a public
bucket; tampered signatures, swapped keys, and swapped buckets all
rejected. Signed write and delete URLs are deliberately not built.
Co-Authored-By: Claude Opus 5 <[email protected]>
47 lines
2.8 KiB
Bash
47 lines
2.8 KiB
Bash
# Local development only. Replace every credential outside a developer workstation.
|
|
|
|
# Required for a local workstation, and only ever correct there. Anything else — including an
|
|
# unset value — is validated as production, which requires certificate-verified PostgreSQL TLS
|
|
# (EKKLESIA_DATABASE_TLS_MODE defaults to verify-full) and rejects loopback/http identity origins.
|
|
# The plaintext loopback DATABASE_URL below is therefore only usable with this set.
|
|
NODE_ENV=development
|
|
|
|
POSTGRES_DB=ekklesia
|
|
POSTGRES_USER=ekklesia
|
|
POSTGRES_PASSWORD=ekklesia-development-only
|
|
POSTGRES_PORT=5432
|
|
DATABASE_URL=postgresql://ekklesia:[email protected]:5432/ekklesia
|
|
|
|
# Ekklesia's object-storage provider (ADR 0030). Absent S3_ENDPOINT means this installation has no
|
|
# object storage, which is a complete deployment: only a module that declares a logical bucket in its
|
|
# manifest is affected, and it is reported blocked rather than breaking anything. Production requires
|
|
# https, exactly as the database transport requires verified TLS.
|
|
#
|
|
# These are host infrastructure. No module ever sees them: a module names a logical bucket and the
|
|
# host resolves the binding.
|
|
S3_ACCESS_KEY=ekklesia-development
|
|
S3_SECRET_KEY=ekklesia-development-secret
|
|
S3_ENDPOINT=http://127.0.0.1:8333
|
|
S3_PORT=8333
|
|
S3_REGION=us-east-1
|
|
S3_FORCE_PATH_STYLE=true
|
|
# The origin a *browser* reaches object storage on, used only when signing temporary read URLs for
|
|
# direct delivery (ADR 0031). Leave it unset whenever S3_ENDPOINT is itself reachable from a browser,
|
|
# which is the case for this development stack: Compose publishes the service on loopback, so the
|
|
# endpoint above serves both roles and direct delivery works with no extra configuration.
|
|
#
|
|
# Set it when Core reaches storage by a name a browser cannot resolve — a Compose service name such
|
|
# as http://object-storage:8333, for instance — and browsers reach it through a public origin:
|
|
# S3_DELIVERY_ENDPOINT=https://storage.example.org
|
|
# It must be the origin the browser actually requests. An S3-compatible signature covers the request
|
|
# Host, so a URL signed for one origin cannot be rewritten to another afterwards; a reverse proxy in
|
|
# front of storage must therefore pass the original Host header through unchanged. The scheme is not
|
|
# signed, so terminating TLS at that proxy and forwarding plaintext to the service is fine.
|
|
# S3_DELIVERY_ENDPOINT=
|
|
# Namespaces every physical bucket this installation owns, so several Ekklesia-compatible deployments
|
|
# can share one S3-compatible service. Optional; defaults to "ekklesia".
|
|
S3_BUCKET_PREFIX=ekklesia
|
|
# Consumed only by the development Compose service's own bootstrap, never by Ekklesia's provider —
|
|
# physical buckets are derived from a module's manifest declaration, not configured here.
|
|
S3_BUCKET=ekklesia-development
|