import assert from "node:assert/strict"; import { generateKeyPairSync, randomUUID, sign, type JsonWebKey } from "node:crypto"; import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { request as httpRequest } from "node:http"; import type { AddressInfo } from "node:net"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { after, before, test, type TestContext } from "node:test"; import { fileURLToPath } from "node:url"; import { createApp, type AppOptions } from "./app.ts"; import { readFormBody } from "./body.ts"; import { createLogger } from "../logger.ts"; import { createDenylist } from "../auth/denylist.ts"; import { CSRF_COOKIE, issueCsrfToken } from "../auth/csrf.ts"; import { can, check, GuardError, requireSession } from "../auth/guards.ts"; import { HydraError, type HydraAdmin, type OAuth2Client } from "../auth/hydra-admin.ts"; import { staticJwks } from "../auth/jwks.ts"; import type { ExpandTree, KetoClient, RelationTuple, SubjectSet } from "../auth/keto-client.ts"; import type { Identity, KratosAdmin } from "../auth/kratos-admin.ts"; import { KratosError, type Flow, type FlowType, type KratosPublic, type Session, type UiNode } from "../auth/kratos-public.ts"; import { SESSION_COOKIE } from "../auth/login.ts"; import type { Plugin } from "../plugin-host/plugin.ts"; import { contentTypeFor, resolveStaticPath, routePublic } from "./static.ts"; import adminManifest from "../../examples/plugins/admin/plugin.ts"; const viewsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "views"); // The admin screens ship as a drop-in example plugin; the HTTP-level admin tests mount it via // createApp (with stub Ory clients on ctx.system + views from examples/plugins) exactly as an // operator would after copying it into plugins/. const examplesPluginsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "examples", "plugins"); const adminPlugin: Plugin = { ...adminManifest, id: "admin" }; // A session JWT signed with a throwaway test key — the verify path. Wired into the shared // `server` (and the per-test apps) so a request can present a valid session; the dashboard and the // gated routes need one. `staticJwks([ecJwk])` is the matching verify side. const ec = generateKeyPairSync("ec", { namedCurve: "P-256" }); const ecJwk: JsonWebKey = { ...(ec.publicKey.export({ format: "jwk" }) as JsonWebKey), alg: "ES256", kid: "test-kid" }; const b64url = (i: Buffer | string): string => Buffer.from(i).toString("base64url"); function mintJwt(payload: Record): string { const input = `${b64url(JSON.stringify({ alg: "ES256", kid: "test-kid", typ: "JWT" }))}.${b64url(JSON.stringify(payload))}`; return `${input}.${b64url(sign("SHA256", Buffer.from(input), { dsaEncoding: "ieee-p1363", key: ec.privateKey }))}`; } // A session cookie carrying `roles`, valid for 10 min — the auth most tests need to reach a gated page. const session = (roles: string[] = []): string => `${SESSION_COOKIE}=${mintJwt({ email: "a@b.c", exp: Math.floor(Date.now() / 1000) + 600, roles, sub: "u1" })}`; const server = createApp({ jwks: staticJwks([ecJwk]) }); let base = ""; before(async () => { await new Promise((resolve) => server.listen(0, resolve)); base = `http://localhost:${(server.address() as AddressInfo).port}`; }); after(() => server.close()); test("the dashboard at /dashboard: the instructional starter in the unified shell, gated to a session", async () => { // The dashboard is gated to a signed-in user, so present a session. const res = await fetch(base + "/dashboard", { headers: { cookie: session() } }); assert.equal(res.status, 200); assert.match(res.headers.get("content-type") ?? "", /text\/html/); const html = await res.text(); // The unified app shell: the same sidebar/menu every page renders. assert.match(html, /Plainpages/); // sidebar brand assert.match(html, /