Unify §3 test overlaps (todo §3); fold the 5× image-pin checks into one compose.test.ts scan + same-version sidecar test, drop the duplicate committed-JWKS re-validation in config.test.ts
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
// Guards the dev/prod compose split + stack ordering (§3): long-running Ory services
|
||||
// carry readiness healthchecks so `depends_on: service_healthy` works, the web app waits
|
||||
// for the services it talks to (kratos + keto, per config.ts), prod publishes no internal
|
||||
// Ory ports while dev exposes the ones a browser must reach, and the visual E2E stays
|
||||
// Ory-free. Real boot is verified by running the stack; this catches edits.
|
||||
// Guards the dev/prod compose split + stack ordering (§3): every image is pinned to an
|
||||
// exact version (AGENTS.md), long-running Ory services carry readiness healthchecks so
|
||||
// `depends_on: service_healthy` works, the web app waits for the services it talks to
|
||||
// (kratos + keto, per config.ts), prod publishes no internal Ory ports while dev exposes
|
||||
// the ones a browser must reach, and the visual E2E stays Ory-free. Real boot is verified
|
||||
// by running the stack; this catches edits.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -15,6 +16,24 @@ const e2e = read("compose.e2e.yml");
|
||||
// compose.yml lists web first, postgres second — slice the web service block.
|
||||
const webBlock = compose.slice(compose.indexOf("\n web:"), compose.indexOf("\n postgres:"));
|
||||
|
||||
test("every image is pinned to an exact version, never a floating tag", () => {
|
||||
// One scan over all compose files (postgres, the three Ory pairs, mailpit); web/e2e build.
|
||||
const images = [...[compose, override, e2e].join("\n").matchAll(/image:\s*(\S+)/g)].map((m) => m[1]!);
|
||||
assert.ok(images.length >= 8, "scans the pinned images");
|
||||
for (const img of images) {
|
||||
assert.match(img.split(":").at(-1)!, /^v?\d+\.\d+/, `${img} pins a version-like tag`);
|
||||
assert.doesNotMatch(img, /latest|edge|[\^~*]/, `${img} is exact, not floating`);
|
||||
}
|
||||
});
|
||||
|
||||
test("each Ory service and its migrate sidecar share one pinned version", () => {
|
||||
for (const svc of ["hydra", "keto", "kratos"]) {
|
||||
const tags = [...compose.matchAll(new RegExp(`image:\\s*oryd/${svc}:(\\S+)`, "g"))].map((m) => m[1]);
|
||||
assert.equal(tags.length, 2, `${svc} + ${svc}-migrate both present`);
|
||||
assert.equal(tags[0], tags[1], `${svc} server + migrate pinned to the same version`);
|
||||
}
|
||||
});
|
||||
|
||||
test("long-running Ory services declare readiness healthchecks", () => {
|
||||
for (const [svc, port] of [["kratos", 4433], ["keto", 4466], ["hydra", 4444]] as const)
|
||||
assert.match(compose, new RegExp(`wget[^\\n]*:${port}/health/ready`),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { loadConfig } from "./config.ts";
|
||||
|
||||
// Explicit secure-secret enforcement (no environment sniffing): secrets are the only
|
||||
@@ -27,17 +25,10 @@ test("loads dev defaults when the environment is empty", () => {
|
||||
test("JWKS_URL defaults to the committed Kratos tokenizer signing key, not an http endpoint", () => {
|
||||
// The session JWT is signed by the tokenizer key (kratos.yml jwks_url); Kratos does NOT
|
||||
// republish it at /.well-known/jwks.json, so the §4 verifier reads that same file://.
|
||||
// gen-jwks.test.ts owns that the file is a valid ES256 signing key with a kid.
|
||||
const url = new URL(loadConfig({}).jwksUrl);
|
||||
assert.equal(url.protocol, "file:");
|
||||
assert.match(url.pathname, /tokenizer\/jwks\.json$/);
|
||||
|
||||
// And that file is a real ES256 signing JWKS carrying a kid (what the verifier resolves by).
|
||||
const path = fileURLToPath(new URL("../ory/kratos/tokenizer/jwks.json", import.meta.url));
|
||||
const key = (JSON.parse(readFileSync(path, "utf8")) as { keys: { alg: string; kid: string; kty: string }[] }).keys[0];
|
||||
assert.ok(key, "tokenizer JWKS must have a key");
|
||||
assert.equal(key.alg, "ES256");
|
||||
assert.equal(key.kty, "EC");
|
||||
assert.ok(key.kid, "tokenizer JWKS key must carry a kid");
|
||||
});
|
||||
|
||||
test("parses explicit boolean toggles and rejects non-boolean values", () => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Guards the Ory Hydra config (§3): image pinned to an exact version (AGENTS.md),
|
||||
// migrations run before the server (hydra-migrate → hydra), the DSN targets the hydra
|
||||
// database, the server listens on the public/admin ports, and the issuer +
|
||||
// login/consent/logout URLs point at our app. Real boot is verified by running the
|
||||
// stack; this catches edits.
|
||||
// Guards the Ory Hydra config (§3): migrations run before the server (hydra-migrate →
|
||||
// hydra), the DSN targets the hydra database, the server listens on the public/admin
|
||||
// ports, and the issuer + login/consent/logout URLs point at our app. Version pinning is
|
||||
// in compose.test.ts. Real boot is verified by running the stack; this catches edits.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -11,15 +10,6 @@ const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), "u
|
||||
const compose = read("compose.yml");
|
||||
const hydraYml = read("ory/hydra/hydra.yml");
|
||||
|
||||
test("compose pins both hydra services to one exact version", () => {
|
||||
const tags = [...compose.matchAll(/image:\s*oryd\/hydra:(\S+)/g)].map((m) => m[1]);
|
||||
assert.equal(tags.length, 2, "hydra + hydra-migrate both present");
|
||||
assert.equal(tags[0], tags[1], "both pinned to the same version");
|
||||
const tag = tags[0]!;
|
||||
assert.match(tag, /^v\d+\.\d+\.\d+$/, `${tag} is an exact vMAJOR.MINOR.PATCH`);
|
||||
assert.doesNotMatch(tag, /latest|[\^~*]/, `${tag} is exact, not floating`);
|
||||
});
|
||||
|
||||
test("hydra migrations run once before the server starts", () => {
|
||||
assert.ok((compose.match(/migrate sql -e --yes/g) ?? []).length >= 2,
|
||||
"hydra-migrate runs SQL migrations (alongside kratos)");
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Guards the Ory Keto config (§3): image pinned to an exact version (AGENTS.md),
|
||||
// migrations run before the server (keto-migrate → keto), the DSN targets the keto
|
||||
// database, read/write APIs serve on the ports config.ts points at, and the OPL
|
||||
// declares the role/group/resource namespaces. Real boot is verified by running the
|
||||
// stack; this catches edits.
|
||||
// Guards the Ory Keto config (§3): migrations run before the server (keto-migrate →
|
||||
// keto), the DSN targets the keto database, read/write APIs serve on the ports config.ts
|
||||
// points at, and the OPL declares the role/group/resource namespaces. Version pinning is
|
||||
// in compose.test.ts. Real boot is verified by running the stack; this catches edits.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -12,15 +11,6 @@ const compose = read("compose.yml");
|
||||
const ketoYml = read("ory/keto/keto.yml");
|
||||
const opl = read("ory/keto/namespaces.keto.ts");
|
||||
|
||||
test("compose pins both keto services to one exact version", () => {
|
||||
const tags = [...compose.matchAll(/image:\s*oryd\/keto:(\S+)/g)].map((m) => m[1]);
|
||||
assert.equal(tags.length, 2, "keto + keto-migrate both present");
|
||||
assert.equal(tags[0], tags[1], "both pinned to the same version");
|
||||
const tag = tags[0]!;
|
||||
assert.match(tag, /^v\d+\.\d+\.\d+$/, `${tag} is an exact vMAJOR.MINOR.PATCH`);
|
||||
assert.doesNotMatch(tag, /latest|[\^~*]/, `${tag} is exact, not floating`);
|
||||
});
|
||||
|
||||
test("keto migrations run once before the server starts", () => {
|
||||
assert.match(compose, /migrate\s+up\s+-y/, "keto-migrate runs migrations");
|
||||
assert.ok((compose.match(/condition:\s*service_completed_successfully/g) ?? []).length >= 2,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Guards the Ory Kratos config (§3): image pinned to an exact version (AGENTS.md),
|
||||
// migrations run before the server (kratos-migrate → kratos), the DSN targets the
|
||||
// kratos database, and the identity schema carries email (password identifier) +
|
||||
// name traits. Real boot is verified by running the stack; this catches edits.
|
||||
// Guards the Ory Kratos config (§3): migrations run before the server (kratos-migrate →
|
||||
// kratos), the DSN targets the kratos database, and the identity schema carries email
|
||||
// (password identifier) + name traits. Version pinning is in compose.test.ts. Real boot
|
||||
// is verified by running the stack; this catches edits.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -11,15 +11,6 @@ const compose = read("compose.yml");
|
||||
const kratosYml = read("ory/kratos/kratos.yml");
|
||||
const schema = JSON.parse(read("ory/kratos/identity.schema.json"));
|
||||
|
||||
test("compose pins both kratos services to one exact version", () => {
|
||||
const tags = [...compose.matchAll(/image:\s*oryd\/kratos:(\S+)/g)].map((m) => m[1]);
|
||||
assert.equal(tags.length, 2, "kratos + kratos-migrate both present");
|
||||
assert.equal(tags[0], tags[1], "both pinned to the same version");
|
||||
const tag = tags[0]!;
|
||||
assert.match(tag, /^v\d+\.\d+\.\d+$/, `${tag} is an exact vMAJOR.MINOR.PATCH`);
|
||||
assert.doesNotMatch(tag, /latest|[\^~*]/, `${tag} is exact, not floating`);
|
||||
});
|
||||
|
||||
test("migrations run once before the server starts", () => {
|
||||
assert.match(compose, /migrate sql -e --yes/, "kratos-migrate runs SQL migrations");
|
||||
assert.match(compose, /condition:\s*service_completed_successfully/,
|
||||
@@ -98,10 +89,3 @@ test("the committed OIDC claims mapper maps email + name", () => {
|
||||
assert.match(mapper, /given_name/, "given name → name.first");
|
||||
assert.match(mapper, /family_name/, "family name → name.last");
|
||||
});
|
||||
|
||||
test("compose pins the dev mail catcher to an exact version", () => {
|
||||
const tag = read("compose.override.yml").match(/image:\s*axllent\/mailpit:(\S+)/)?.[1];
|
||||
assert.ok(tag, "compose.override.yml pins a mailpit image");
|
||||
assert.match(tag, /^v\d+\.\d+\.\d+$/, `${tag} is an exact version`);
|
||||
assert.doesNotMatch(tag, /latest|edge|[\^~*]/, `${tag} is exact, not floating`);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Guards the Ory Postgres config (§3): image stays pinned to an exact version
|
||||
// (AGENTS.md rule) and each Ory service keeps its own database. Real container
|
||||
// behaviour is verified by booting postgres in CI/e2e; this catches edits.
|
||||
// Guards the Ory Postgres config (§3): each Ory service keeps its own database (the
|
||||
// image pin is covered by compose.test.ts's global scan). Real container behaviour is
|
||||
// verified by booting postgres in CI/e2e; this catches edits.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -8,13 +8,6 @@ import { readFileSync } from "node:fs";
|
||||
const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), "utf8");
|
||||
const ORY_DATABASES = ["hydra", "keto", "kratos"]; // one DB per Ory service
|
||||
|
||||
test("compose pins the postgres image to an exact version", () => {
|
||||
const tag = read("compose.yml").match(/image:\s*postgres:(\S+)/)?.[1];
|
||||
assert.ok(tag, "compose.yml pins a postgres image");
|
||||
assert.match(tag, /^\d+\.\d+/, `${tag} pins major.minor`);
|
||||
assert.doesNotMatch(tag, /latest|[\^~*]/, `${tag} is exact, not floating`);
|
||||
});
|
||||
|
||||
test("init SQL gives each Ory service its own database", () => {
|
||||
const sql = read("ory/postgres/init/init.sql");
|
||||
for (const db of ORY_DATABASES) {
|
||||
|
||||
2
todo.md
2
todo.md
@@ -72,7 +72,7 @@ everything via Docker.
|
||||
- [x] Document the *only* things that can't be auto-generated: third-party **SSO provider** client id/secret (optional — password login works without them) and **production secrets** (real cookie/CSRF secret + signing key, supplied via env, replacing the dev throwaways). Everything else must work from a clean clone. → New README **What you must supply (the only manual prep)** subsection (under Configuration) consolidates the previously-scattered facts into one authoritative list: a clean clone needs nothing; exactly two production-only things can't be auto-generated — (1) production secrets (`COOKIE_SECRET`/`CSRF_SECRET` + the JWT signing key, with `REQUIRE_SECURE_SECRETS=true` refusing throwaways) and (2) optional SSO provider creds (no creds ⇒ no button). States everything else (Ory migrations, dev signing key, demo admin + Keto roles, OPL model) is generated/seeded on first boot. Cross-links the existing SSO + JWT-rotation subsections (no duplication) and adds a pointer from **Production / deployment**. All four anchors verified; docs-only — typecheck + 152 units green.
|
||||
- [x] Run the architecture _and_ the stability reviewer agents on the _whole_ project, not just the latest changes, and address their issues. → Ran both on the whole project (weighted to the §3 Ory stack). Verdict: architecture sound + disciplined, no Critical; both independently flagged the *same* top issue. **Fixed now:** (1) HIGH (both agents) — `JWKS_URL` default was `http://kratos:4433/.well-known/jwks.json`, but Kratos does **not** republish the session-tokenizer key there (no OIDC discovery on Kratos — that's Hydra), so the §4 verifier would have fetched the wrong/empty set and *no one* could be authorized. Repointed the default to `file:///etc/config/kratos/tokenizer/jwks.json` — the exact key Kratos signs with (`kratos.yml` `jwks_url`) — and mounted that tokenizer dir **read-only into `web`** (`compose.yml`) so the verifier resolves the live key in dev *and* prod (same file bootstrap regenerates). `config.test.ts` now locks the default to the tokenizer file + asserts the committed key is a real ES256 JWKS carrying a `kid` (the regression the old `/jwks/` match missed). (2) MEDIUM (stability) — `bootstrap` had uncapped `restart: on-failure`; a *permanent* seed error would loop forever and silently hang `web` (gates on `service_completed_successfully`). Capped to `on-failure:5` (seed is idempotent — 409-create + idempotent PUT — so transient Ory blips still recover, permanent ones give up loud). (3) §3's new `web` `depends_on` made the documented `docker compose run --rm web …` typecheck/test/gen-jwks commands drag up the whole Ory stack — added `--no-deps` (README + AGENTS.md). **Deferred (reviewer-scoped, not §3):** extract `buildShellContext` out of `dashboard.ts` + route built-in screens through `matchRoute`/`isAuthorized` → §5 (forcing function arrives with the 2nd/3rd screen); seed the demo admin's `metadata_admin.roles` projection so first login is non-empty → §4 (the login-completion projection owns it); enforce Ory `*.yml` prod secrets + self-service return-URLs via env → §9 (ops). typecheck + 153 units green; both compose files validated.
|
||||
- [x] Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff. → Pass over the §3 Ory accretion. Killed the now-stale "the next §3 item generates/mounts" forward-refs (the JWKS shipped) in `kratos.yml` (×2) + `kratos.test.ts`. Tightened the verbose service/header blocks in `compose.yml` (web depends_on/JWKS-mount, the three Ory headers, the bootstrap block) and the `bootstrap.ts`/`gen-jwks.ts` module headers — dropping prose the README/`src/bootstrap.ts` already carry, keeping the security/stability rationale (read-only mount, bounded retry). Trimmed `config.ts`'s JWKS comment and the `kratos.yml` SSO block (kept the concrete env example), and aligned the `gen-jwks.ts` command with the README's `--no-deps`. Net −12 lines; typecheck + 153 units green. The §3 README sections (Development / What you must supply / SSO / JWT rotation) were already authored concise in §3 (todo lines 70–72) and left intact.
|
||||
- [ ] Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.
|
||||
- [x] Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us. → Pass over the §3 Ory-stack tests. The clear overlap: the "image pinned to an exact version" AGENTS.md check was re-implemented 5× (postgres/kratos/keto/hydra + mailpit). Unified into one `compose.test.ts` scan over all three compose files (strictly stronger — auto-covers any future image) + one test asserting each Ory service & its migrate sidecar share one version (subsumes the per-service "both present + same version" halves). Dropped the now-redundant pin tests from `postgres/kratos/keto/hydra.test.ts` (each keeps its config-semantics tests; comments point pinning at `compose.test.ts`). Also trimmed `config.test.ts`'s duplicate re-validation of the committed JWKS key — `gen-jwks.test.ts` already owns key validity (round-trips a signature); the config test keeps the default-path assertion. The migrate-before-server / DSN / port / URL tests stay per-service (distinct config, distinct files — merging would hurt the per-module structure). 153 → 150 tests, zero coverage lost; typecheck + tests green.
|
||||
|
||||
## 4. Auth — identity, session JWT, guards
|
||||
- [ ] Kratos public client (fetch): init/get/submit flows, `whoami`, `whoami?tokenize_as=plainpages`.
|
||||
|
||||
Reference in New Issue
Block a user