Compare commits

...

8 Commits

Author SHA1 Message Date
lilleman 2a0ae084ac Expect WebKit's plain-http cross-origin-isolation warning in the console guard
CI / full-gate (push) Waiting to run
2026-11-08 23:26:25 +01:00
renovate-bot f3ebea3c80 Update Playwright to v1.65.0 2026-11-08 23:26:25 +01:00
lilleman 1fc6033dd5 Give the JsonWebKey alias the kid a JWKS selects a key by
CI / full-gate (push) Waiting to run
2026-11-08 23:26:03 +01:00
lilleman 1304cdae56 Read JsonWebKey from webcrypto, where @types/node 26 exports it 2026-11-08 23:26:03 +01:00
renovate-bot 3f07c3e649 Update Node.js to v26
Release-Bump: major
2026-11-08 23:26:03 +01:00
lilleman 0a9c79d08f Refuse a malformed admin DSN before postgres 4 connects to the host it misreads
CI / full-gate (push) Waiting to run
2026-11-08 23:26:03 +01:00
renovate-bot 44f94c7ab0 Update dependency postgres to v4
Release-Bump: major
2026-11-08 23:26:03 +01:00
renovate-bot 2f75995b3d Update renovate/renovate Docker tag to v44.222.3
CI / full-gate (push) Has started running
2026-11-08 23:25:58 +01:00
20 changed files with 75 additions and 62 deletions
+1 -1
View File
@@ -19,4 +19,4 @@ jobs:
run: | run: |
docker run --rm -v "$PWD:/repo" -w /repo \ docker run --rm -v "$PWD:/repo" -w /repo \
-e REGISTRY_TOKEN -e REGISTRY_USER -e REPO_TOKEN -e REPOSITORY -e SERVER_URL \ -e REGISTRY_TOKEN -e REGISTRY_USER -e REPO_TOKEN -e REPOSITORY -e SERVER_URL \
node:24.22.2-alpine3.24 node registry-cleanup/cleanup.ts node:26.13.0-alpine3.24 node registry-cleanup/cleanup.ts
+3 -3
View File
@@ -22,7 +22,7 @@ jobs:
GIT_TAG: ${{ github.ref_name }} GIT_TAG: ${{ github.ref_name }}
run: | run: |
set -euo pipefail set -euo pipefail
docker run --rm -v "$PWD:/repo" -w /repo node:24.22.2-alpine3.24 \ docker run --rm -v "$PWD:/repo" -w /repo node:26.13.0-alpine3.24 \
node release-tooling/contract-version.ts "$GIT_TAG" src/plugin-host/plugin.ts node release-tooling/contract-version.ts "$GIT_TAG" src/plugin-host/plugin.ts
- name: Promote the commit-hash image to semver + latest - name: Promote the commit-hash image to semver + latest
env: env:
@@ -100,9 +100,9 @@ jobs:
VERSION=${INPUT_VERSION:-${GIT_TAG#v}} VERSION=${INPUT_VERSION:-${GIT_TAG#v}}
VERSION=${VERSION#v} VERSION=${VERSION#v}
# An empty dispatch input falls back to the branch name, so gate this like a tag. # An empty dispatch input falls back to the branch name, so gate this like a tag.
docker run --rm -v "$PWD:/repo" -w /repo node:24.22.2-alpine3.24 \ docker run --rm -v "$PWD:/repo" -w /repo node:26.13.0-alpine3.24 \
node release-tooling/contract-version.ts "$VERSION" src/plugin-host/plugin.ts node release-tooling/contract-version.ts "$VERSION" src/plugin-host/plugin.ts
docker run --rm -v "$PWD:/repo" -w /repo \ docker run --rm -v "$PWD:/repo" -w /repo \
-e DOCKERHUB_REPO -e DOCKERHUB_TOKEN -e DOCKERHUB_USER \ -e DOCKERHUB_REPO -e DOCKERHUB_TOKEN -e DOCKERHUB_USER \
node:24.22.2-alpine3.24 \ node:26.13.0-alpine3.24 \
node release-tooling/dockerhub-overview.ts "$VERSION" node release-tooling/dockerhub-overview.ts "$VERSION"
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
-e RENOVATE_PLATFORM=gitea \ -e RENOVATE_PLATFORM=gitea \
-e RENOVATE_REPOSITORIES=${{ github.repository }} \ -e RENOVATE_REPOSITORIES=${{ github.repository }} \
-e RENOVATE_TOKEN \ -e RENOVATE_TOKEN \
renovate/renovate:44.199.2 renovate/renovate:44.222.3
# After the renovate job, cut ONE tag covering the renovate-bot commits merged to main since the # After the renovate job, cut ONE tag covering the renovate-bot commits merged to main since the
# last tag (batch per run). Targets origin/main — the real post-merge tip; the checkout SHA is the # last tag (batch per run). Targets origin/main — the real post-merge tip; the checkout SHA is the
@@ -60,11 +60,11 @@ jobs:
if [ -z "$BUMPS" ]; then if [ -z "$BUMPS" ]; then
echo "Renovate commits since ${LATEST}, but none carry Release-Bump — nothing reached a running Plainpages; skipping"; exit 0 echo "Renovate commits since ${LATEST}, but none carry Release-Bump — nothing reached a running Plainpages; skipping"; exit 0
fi fi
NEXT=$(docker run --rm -v "$PWD:/repo" -w /repo node:24.22.2-alpine3.24 \ NEXT=$(docker run --rm -v "$PWD:/repo" -w /repo node:26.13.0-alpine3.24 \
node release-tooling/next-version.ts "$LATEST" $BUMPS) node release-tooling/next-version.ts "$LATEST" $BUMPS)
# Read the constant off origin/main, not the checkout, which lags the merges this run made. # Read the constant off origin/main, not the checkout, which lags the merges this run made.
git show origin/main:src/plugin-host/plugin.ts \ git show origin/main:src/plugin-host/plugin.ts \
| docker run -i --rm -v "$PWD:/repo" -w /repo node:24.22.2-alpine3.24 \ | docker run -i --rm -v "$PWD:/repo" -w /repo node:26.13.0-alpine3.24 \
node release-tooling/contract-version.ts "$NEXT" - node release-tooling/contract-version.ts "$NEXT" -
echo "Releasing $LATEST -> $NEXT" echo "Releasing $LATEST -> $NEXT"
git tag "$NEXT" origin/main git tag "$NEXT" origin/main
+1 -1
View File
@@ -1,5 +1,5 @@
# Node 24 runs TypeScript directly (type stripping) — no build step. Pinned exact tag. # Node 24 runs TypeScript directly (type stripping) — no build step. Pinned exact tag.
FROM node:24.22.2-alpine3.24 FROM node:26.13.0-alpine3.24
# Above WORKDIR so dev's `.:/app` bind mount can't shadow them; a volume at /app/node_modules # Above WORKDIR so dev's `.:/app` bind mount can't shadow them; a volume at /app/node_modules
# instead leaves a root-owned dir in the checkout (the daemon creates mount destinations as root). # instead leaves a root-owned dir in the checkout (the daemon creates mount destinations as root).
+1 -1
View File
@@ -49,7 +49,7 @@ services:
# backs it (PLUGIN_SETTING_SCHEDULING_UPSTREAM above points here). Stand-in for the customer's real service — # backs it (PLUGIN_SETTING_SCHEDULING_UPSTREAM above points here). Stand-in for the customer's real service —
# stdlib-only, in-memory, no auth. Prod points PLUGIN_SETTING_SCHEDULING_UPSTREAM at the real backend instead. # stdlib-only, in-memory, no auth. Prod points PLUGIN_SETTING_SCHEDULING_UPSTREAM at the real backend instead.
shifts-upstream: shifts-upstream:
image: node:24.22.2-alpine3.24 image: node:26.13.0-alpine3.24
command: node /srv/server.ts command: node /srv/server.ts
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+1 -1
View File
@@ -1,6 +1,6 @@
# Playwright runner — browsers preinstalled, pinned to match @playwright/test in e2e-tests/. # Playwright runner — browsers preinstalled, pinned to match @playwright/test in e2e-tests/.
# Built/run via e2e-tests/compose.visual.yml; targets the `web` service over the network. # Built/run via e2e-tests/compose.visual.yml; targets the `web` service over the network.
FROM mcr.microsoft.com/playwright:v1.64.1-noble FROM mcr.microsoft.com/playwright:v1.65.0-noble
WORKDIR /e2e-tests WORKDIR /e2e-tests
+3 -3
View File
@@ -53,7 +53,7 @@ services:
# The reference plugin's upstream (examples/shifts-upstream) so /scheduling/shifts shows real rows. # The reference plugin's upstream (examples/shifts-upstream) so /scheduling/shifts shows real rows.
shifts-upstream: shifts-upstream:
image: node:24.22.2-alpine3.24 image: node:26.13.0-alpine3.24
command: ["node", "/server.ts"] command: ["node", "/server.ts"]
volumes: volumes:
- ./examples/shifts-upstream/server.ts:/server.ts:ro - ./examples/shifts-upstream/server.ts:/server.ts:ro
@@ -66,7 +66,7 @@ services:
# Mock OIDC provider for the SSO login test — stdlib Node, auto-approves, signs an id_token Kratos # Mock OIDC provider for the SSO login test — stdlib Node, auto-approves, signs an id_token Kratos
# verifies via its jwks. Reachable as the same host (mock-oidc:9000) by both the browser and Kratos. # verifies via its jwks. Reachable as the same host (mock-oidc:9000) by both the browser and Kratos.
mock-oidc: mock-oidc:
image: node:24.22.2-alpine3.24 image: node:26.13.0-alpine3.24
command: ["node", "/mock-oidc.ts"] command: ["node", "/mock-oidc.ts"]
environment: environment:
ISSUER: http://mock-oidc:9000 ISSUER: http://mock-oidc:9000
@@ -92,7 +92,7 @@ services:
# Same-origin gateway: Kratos-owned paths → kratos, everything else → web (e2e-tests/proxy.ts). # Same-origin gateway: Kratos-owned paths → kratos, everything else → web (e2e-tests/proxy.ts).
proxy: proxy:
image: node:24.22.2-alpine3.24 image: node:26.13.0-alpine3.24
command: ["node", "/proxy.ts"] command: ["node", "/proxy.ts"]
depends_on: depends_on:
web: web:
+7 -3
View File
@@ -12,10 +12,14 @@ import { expect, test as base, type BrowserContext, type Page } from "@playwrigh
const problems: string[] = []; const problems: string[] = [];
const allowed: RegExp[] = []; const allowed: RegExp[] = [];
// The one message the stack itself provokes: the runner reaches `web`/`proxy` by container name over // The messages the stack itself provokes: the runner reaches `web`/`proxy` by container name over
// plain http, and only a `localhost` origin is trustworthy without TLS — so Chromium drops the COOP // plain http, and only a `localhost` origin is trustworthy without TLS — so Chromium drops the COOP
// header the app sends and says so on every page. Over https, where a deployment serves, it applies. // header the app sends, and WebKit reports the page not cross-origin isolated, on every page. Over
const EXPECTED = [/^console\.error: The Cross-Origin-Opener-Policy header has been ignored/]; // https, where a deployment serves, the header applies.
const EXPECTED = [
/^console\.error: The Cross-Origin-Opener-Policy header has been ignored/,
/^console\.warning: This document is not cross-origin isolated because the document is not a secure context/,
];
// Allow a message for the current test only, when the page under test provokes it on purpose. // Allow a message for the current test only, when the page under test provokes it on purpose.
export function allowConsole(...patterns: RegExp[]): void { export function allowConsole(...patterns: RegExp[]): void {
+20 -20
View File
@@ -6,19 +6,19 @@
"": { "": {
"name": "plainpages-e2e", "name": "plainpages-e2e",
"devDependencies": { "devDependencies": {
"@playwright/test": "1.64.1", "@playwright/test": "1.65.0",
"@types/node": "24.19.2", "@types/node": "26.6.6",
"typescript": "7.0.2" "typescript": "7.0.2"
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.64.1", "version": "1.65.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.65.0.tgz",
"integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==", "integrity": "sha512-iGURLGuKDLakSEl7Pgfv8h7f+XLT+V861Wfhz0zlEl5nyFuKUopdkFBPKqTllTUp06aOwPuH8iuUEb3yp3MpbQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.64.1" "playwright": "1.65.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -28,13 +28,13 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.19.2", "version": "26.6.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
"integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": ">=7.24.0 <7.24.7" "undici-types": "~8.9.0"
} }
}, },
"node_modules/@typescript/typescript-aix-ppc64": { "node_modules/@typescript/typescript-aix-ppc64": {
@@ -378,13 +378,13 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.64.1", "version": "1.65.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.65.0.tgz",
"integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==", "integrity": "sha512-F3AMWcR44qMBt4kq+6Kb6mX7bjGXshktKFOPULd7reED6ZJWevjbNRqysP7q+AATE9kdPEZBibex1BYsvpVoeA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.64.1" "playwright-core": "1.65.0"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -394,9 +394,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.64.1", "version": "1.65.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.65.0.tgz",
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", "integrity": "sha512-vNSKgH6YqtN7cdT/L334zo0a+YwkrpicyrHJlWWPIm/Q+Nn2SWInil01oT0mbu96U4GgoRtCYQD9CzJQgju+lA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -442,9 +442,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.24.6", "version": "8.9.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
} }
+2 -2
View File
@@ -8,8 +8,8 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.64.1", "@playwright/test": "1.65.0",
"@types/node": "24.19.2", "@types/node": "26.6.6",
"typescript": "7.0.2" "typescript": "7.0.2"
} }
} }
+13 -13
View File
@@ -9,12 +9,12 @@
"@larvit/log": "2.4.0", "@larvit/log": "2.4.0",
"ejs": "7.0.2", "ejs": "7.0.2",
"lucide-static": "1.70.0", "lucide-static": "1.70.0",
"postgres": "3.4.9" "postgres": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@mdn/browser-compat-data": "8.1.8", "@mdn/browser-compat-data": "8.1.8",
"@types/ejs": "3.1.5", "@types/ejs": "3.1.5",
"@types/node": "24.19.2", "@types/node": "26.6.6",
"acorn": "8.19.0", "acorn": "8.19.0",
"typescript": "7.0.2" "typescript": "7.0.2"
}, },
@@ -46,13 +46,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.19.2", "version": "26.6.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
"integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", "integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": ">=7.24.0 <7.24.7" "undici-types": "~8.9.0"
} }
}, },
"node_modules/@typescript/typescript-aix-ppc64": { "node_modules/@typescript/typescript-aix-ppc64": {
@@ -427,12 +427,12 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/postgres": { "node_modules/postgres": {
"version": "3.4.9", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", "resolved": "https://registry.npmjs.org/postgres/-/postgres-4.1.0.tgz",
"integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", "integrity": "sha512-s+0WH67YAyLdBQUb6XczbziBhY+C9uiQsg9S8R5RHdZhk6sdyK6202E+T2tXpiInSEEjDLRSJ3OpYBe3PN6MsQ==",
"license": "Unlicense", "license": "Unlicense",
"engines": { "engines": {
"node": ">=12" "node": ">=18"
}, },
"funding": { "funding": {
"type": "individual", "type": "individual",
@@ -475,9 +475,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.24.6", "version": "8.9.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
} }
+2 -2
View File
@@ -19,12 +19,12 @@
"@larvit/log": "2.4.0", "@larvit/log": "2.4.0",
"ejs": "7.0.2", "ejs": "7.0.2",
"lucide-static": "1.70.0", "lucide-static": "1.70.0",
"postgres": "3.4.9" "postgres": "4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@mdn/browser-compat-data": "8.1.8", "@mdn/browser-compat-data": "8.1.8",
"@types/ejs": "3.1.5", "@types/ejs": "3.1.5",
"@types/node": "24.19.2", "@types/node": "26.6.6",
"acorn": "8.19.0", "acorn": "8.19.0",
"typescript": "7.0.2" "typescript": "7.0.2"
} }
+2 -1
View File
@@ -4,8 +4,9 @@
import { test } from "node:test"; import { test } from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { createPrivateKey, sign, type JsonWebKey } from "node:crypto"; import { createPrivateKey, sign } from "node:crypto";
import { generateJwks, rotateJwks } from "./gen-jwks.ts"; import { generateJwks, rotateJwks } from "./gen-jwks.ts";
import type { JsonWebKey } from "./jwks.ts";
import { verifyJws } from "./jwt.ts"; import { verifyJws } from "./jwt.ts";
const b64url = (s: string) => Buffer.from(s).toString("base64url"); const b64url = (s: string) => Buffer.from(s).toString("base64url");
+2 -2
View File
@@ -1,9 +1,9 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { generateKeyPairSync, type JsonWebKey } from "node:crypto"; import { generateKeyPairSync } from "node:crypto";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { test } from "node:test"; import { test } from "node:test";
import { fileURLToPath, pathToFileURL } from "node:url"; import { fileURLToPath, pathToFileURL } from "node:url";
import { cachingJwks, createJwksProvider, loadJwks, staticJwks } from "./jwks.ts"; import { cachingJwks, createJwksProvider, loadJwks, staticJwks, type JsonWebKey } from "./jwks.ts";
const jwk = (kid: string): JsonWebKey => ({ ...(generateKeyPairSync("ec", { namedCurve: "P-256" }).publicKey.export({ format: "jwk" }) as JsonWebKey), alg: "ES256", kid }); const jwk = (kid: string): JsonWebKey => ({ ...(generateKeyPairSync("ec", { namedCurve: "P-256" }).publicKey.export({ format: "jwk" }) as JsonWebKey), alg: "ES256", kid });
const committed = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "ory/kratos/tokenizer/jwks.json"); const committed = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "ory/kratos/tokenizer/jwks.json");
+5 -1
View File
@@ -1,4 +1,4 @@
import type { JsonWebKey } from "node:crypto"; import type { webcrypto } from "node:crypto";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { currentLog } from "../logger.ts"; import { currentLog } from "../logger.ts";
@@ -8,6 +8,10 @@ import { currentLog } from "../logger.ts";
// (or re-reads a mounted file), caches for a TTL, and reloads once on a `kid` miss so a rotated- // (or re-reads a mounted file), caches for a TTL, and reloads once on a `kid` miss so a rotated-
// in key is picked up without a restart (README: zero-downtime rotation). `createJwksProvider` // in key is picked up without a restart (README: zero-downtime rotation). `createJwksProvider`
// picks the right one from the configured URL scheme and primes it at boot (fail loud). // picks the right one from the configured URL scheme and primes it at boot (fail loud).
// node:crypto exports the JWK type only under webcrypto since @types/node 26, and that type lacks
// the `kid` a JWKS selects a key by.
export type JsonWebKey = webcrypto.JsonWebKey & { kid?: string };
export interface JwksProvider { export interface JwksProvider {
getKey(kid: string | undefined): Promise<JsonWebKey | null>; getKey(kid: string | undefined): Promise<JsonWebKey | null>;
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { generateKeyPairSync, sign, type JsonWebKey, type KeyObject } from "node:crypto"; import { generateKeyPairSync, sign, type KeyObject } from "node:crypto";
import { test } from "node:test"; import { test } from "node:test";
import { staticJwks } from "./jwks.ts"; import { staticJwks, type JsonWebKey } from "./jwks.ts";
import { authenticate, claimsToUser, resolveSession, verifyToken } from "./jwt-middleware.ts"; import { authenticate, claimsToUser, resolveSession, verifyToken } from "./jwt-middleware.ts";
import { SESSION_COOKIE } from "./login.ts"; import { SESSION_COOKIE } from "./login.ts";
+2 -1
View File
@@ -1,6 +1,7 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { generateKeyPairSync, sign } from "node:crypto"; import { generateKeyPairSync, sign } from "node:crypto";
import type { JsonWebKey, KeyObject } from "node:crypto"; import type { KeyObject } from "node:crypto";
import type { JsonWebKey } from "./jwks.ts";
import { test } from "node:test"; import { test } from "node:test";
import { decodeJws, verifyJws } from "./jwt.ts"; import { decodeJws, verifyJws } from "./jwt.ts";
+2 -1
View File
@@ -1,5 +1,6 @@
import { createPublicKey, verify } from "node:crypto"; import { createPublicKey, verify } from "node:crypto";
import type { JsonWebKey, KeyObject } from "node:crypto"; import type { KeyObject } from "node:crypto";
import type { JsonWebKey } from "./jwks.ts";
// JWS signature verification with the Node stdlib — no `jose`/JWT dep: // JWS signature verification with the Node stdlib — no `jose`/JWT dep:
// `createPublicKey({format:"jwk"})` imports a JWK and verifies the RS*/ES* signatures // `createPublicKey({format:"jwk"})` imports a JWK and verifies the RS*/ES* signatures
+1
View File
@@ -20,6 +20,7 @@ export interface ProvisionResult {
// so it is swallowed here. The driver, not `new URL`, decides what parses: it takes a multi-host DSN. // so it is swallowed here. The driver, not `new URL`, decides what parses: it takes a multi-host DSN.
function connect(adminUrl: string): ReturnType<typeof postgres> { function connect(adminUrl: string): ReturnType<typeof postgres> {
try { try {
new URL(adminUrl); // postgres 4 connects to whatever a malformed DSN parses to, and its error names that host
return postgres(adminUrl, { connect_timeout: 10, max: 1 }); return postgres(adminUrl, { connect_timeout: 10, max: 1 });
} catch { } catch {
throw new Error("PLUGIN_DB_ADMIN_URL must be a Postgres URL (its value is not shown; percent-encode special characters in a password)"); throw new Error("PLUGIN_DB_ADMIN_URL must be a Postgres URL (its value is not shown; percent-encode special characters in a password)");
+2 -1
View File
@@ -1,4 +1,5 @@
import { generateKeyPairSync, sign, type JsonWebKey } from "node:crypto"; import { generateKeyPairSync, sign } from "node:crypto";
import type { JsonWebKey } from "../auth/jwks.ts";
import type { AddressInfo } from "node:net"; import type { AddressInfo } from "node:net";
import type { TestContext } from "node:test"; import type { TestContext } from "node:test";
import type { HydraAdmin } from "../auth/hydra-admin.ts"; import type { HydraAdmin } from "../auth/hydra-admin.ts";