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]>
76 lines
2.0 KiB
JSON
76 lines
2.0 KiB
JSON
{
|
|
"name": "@ekklesia/core",
|
|
"version": "0.1.0",
|
|
"private": true,
|
|
"type": "module",
|
|
"files": [
|
|
"dist"
|
|
],
|
|
"exports": {
|
|
".": {
|
|
"types": "./dist/index.d.ts",
|
|
"import": "./dist/index.js"
|
|
},
|
|
"./modules": {
|
|
"types": "./dist/modules/index.d.ts",
|
|
"import": "./dist/modules/index.js"
|
|
},
|
|
"./storage": {
|
|
"types": "./dist/storage/index.d.ts",
|
|
"import": "./dist/storage/index.js"
|
|
},
|
|
"./dashboard": {
|
|
"types": "./dist/dashboard/index.d.ts",
|
|
"import": "./dist/dashboard/index.js"
|
|
},
|
|
"./migrations": {
|
|
"types": "./dist/migrations/index.d.ts",
|
|
"import": "./dist/migrations/index.js"
|
|
},
|
|
"./diagnostics": {
|
|
"types": "./dist/diagnostics/index.d.ts",
|
|
"import": "./dist/diagnostics/index.js"
|
|
},
|
|
"./identity": {
|
|
"types": "./dist/identity/index.d.ts",
|
|
"import": "./dist/identity/index.js"
|
|
},
|
|
"./authorization": {
|
|
"types": "./dist/authorization/index.d.ts",
|
|
"import": "./dist/authorization/index.js"
|
|
},
|
|
"./database": {
|
|
"types": "./dist/database/index.d.ts",
|
|
"import": "./dist/database/index.js"
|
|
}
|
|
},
|
|
"scripts": {
|
|
"build": "tsc --project tsconfig.build.json",
|
|
"identity:migration:generate": "drizzle-kit generate --config=drizzle.config.ts",
|
|
"identity:schema:generate": "auth generate --config=identity-schema-generator.config.ts --output=.identity-schema-reference.ts -y",
|
|
"lint": "biome lint .",
|
|
"test": "vitest run",
|
|
"typecheck": "tsc --noEmit --project tsconfig.json"
|
|
},
|
|
"dependencies": {
|
|
"@aws-sdk/client-s3": "3.1116.0",
|
|
"@aws-sdk/s3-request-presigner": "3.1116.0",
|
|
"@ekklesia/contracts": "workspace:*",
|
|
"better-auth": "1.6.23",
|
|
"drizzle-orm": "0.45.2",
|
|
"pg": "8.22.0"
|
|
},
|
|
"devDependencies": {
|
|
"@biomejs/biome": "2.5.3",
|
|
"@types/node": "24.13.3",
|
|
"@types/pg": "8.20.0",
|
|
"auth": "1.6.23",
|
|
"drizzle-kit": "0.31.10",
|
|
"typescript": "7.0.2",
|
|
"vitest": "4.1.10"
|
|
},
|
|
"ekklesia": {
|
|
"layer": "core"
|
|
}
|
|
}
|