From 3616da77b7ee6f2bcbccf7353cc09abf80643fc4 Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Wed, 19 Aug 2026 20:16:50 -0500 Subject: [PATCH 1/3] Allow local overrides of the dev server config. Testing the mediator from a phone means serving it under a tunnel hostname, which requires changing `server.host` and `server.baseUri` -- values in a tracked config that differ per machine. `configs/dev.js` now imports `configs/local.js` last, when present, so those overrides win. The file is gitignored; absent it, behavior is unchanged. --- .gitignore | 1 + configs/dev.js | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.gitignore b/.gitignore index 983fb76..6983a3c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ TAGS bower_components configs/*-secrets.js +configs/local.js coverage node_modules npm-debug.log diff --git a/configs/dev.js b/configs/dev.js index c191b45..622db5a 100644 --- a/configs/dev.js +++ b/configs/dev.js @@ -7,6 +7,7 @@ * All rights reserved. */ import {config} from '@bedrock/core'; +import {existsSync} from 'node:fs'; import {fileURLToPath} from 'node:url'; import os from 'node:os'; import path from 'node:path'; @@ -22,3 +23,12 @@ config.paths.log = path.join(os.tmpdir(), 'authn.localhost'); // note: this also exposes the mediator on all LAN interfaces, not just // Docker's bridge — acceptable for local dev, avoid on shared networks. config.server.bindAddr = ['0.0.0.0']; + +/* Optional local overrides, loaded last so they win. `configs/local.js` is +gitignored, so machine-specific settings -- e.g. pointing `server.domain` at +a tunnel hostname so a phone can reach the mediator -- stay out of the +tracked config. */ +const localConfigPath = path.join(__dirname, 'local.js'); +if(existsSync(localConfigPath)) { + await import(localConfigPath); +} From 224df6744617c1613316ab096078c6007694b832 Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Thu, 20 Aug 2026 08:09:22 -0500 Subject: [PATCH 2/3] Add an example local config and document the override. The override is only discoverable by reading `configs/dev.js`, and anyone using it has to derive the file's shape and the bedrock config keys involved from scratch. `configs/local.js.example` is a copyable starting point covering the case the override exists for: serving the mediator under a tunnel hostname so a phone can reach it. It carries the reasoning that is easy to get wrong -- why `server.host` and `server.baseUri` are set rather than `server.domain`, and why the tunnel must point at the HTTPS port rather than the HTTP one, which would redirect to itself forever. The hostname is a placeholder. Also corrects the Configuration section, which pointed at `./configs/authn.localhost.js`. That file is `authn.localhost.js` at the repository root; the tunable config is `configs/dev.js`. --- README.md | 17 +++++++++++++++-- configs/local.js.example | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 configs/local.js.example diff --git a/README.md b/README.md index 06f244d..cb30870 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,21 @@ software on a local development machine. ### Configuration -The options in the `./configs/authn.localhost.js` file can be tuned to your -environment as needed. +The options in the `./configs/dev.js` file can be tuned to your environment as +needed. + +Prefer a local override to editing that file, since it is checked in. Copy the +example and edit the copy: + + cp configs/local.js.example configs/local.js + +`configs/local.js` is imported last when it exists, so its settings win, and it +is gitignored. Delete it to go back to the defaults. The server reads it at +startup, so restart after changing it. + +The example covers the case it exists for: serving the mediator under a tunnel +hostname so a phone can reach it, which needs `server.host` and +`server.baseUri` changed and cannot be done from a checked-in file. ### Setup diff --git a/configs/local.js.example b/configs/local.js.example new file mode 100644 index 0000000..e7f9ddd --- /dev/null +++ b/configs/local.js.example @@ -0,0 +1,36 @@ +/*! + * Example local development overrides. + * + * Copy to `configs/local.js` and edit. That path is gitignored, so machine + * specific settings stay out of the tracked config. + * + * Copyright (c) 2026, Digital Bazaar, Inc. + */ +import {config} from '@bedrock/core'; + +/* Serve the mediator under a tunnel hostname, so a phone can reach it. + +`server.host` and `server.baseUri` are what the HTTP->HTTPS redirect and the +mediator's own origin checks use. Left at their defaults, a request arriving +through a tunnel is redirected to `https://authn.localhost:33443`, which +resolves only on the machine running the server -- so the mediator iframe +fails to load on a phone. + +Set these two derived values rather than `server.domain`: that also drives +`server.bindAddr`, and `server.host` appends `server.port` unless the port is +443. Overriding the port to 443 would stop the server listening on 33443 +locally. Setting `host` and `baseUri` leaves the local ports (33443 HTTPS, +33080 HTTP) alone, so `https://authn.localhost:33443` keeps working alongside +the tunnel. + +Point the tunnel at the HTTPS port (33443), not the HTTP one. The HTTP port +redirects unconditionally to `https://`, which with a tunnel +hostname redirects to itself forever. For example, with ngrok: + + ngrok http --url=YOUR-HOSTNAME.ngrok.app \ + --upstream-tls-verify=false https://localhost:33443 + +Coordinator sites under test must point their CHAPI mediator origin at the +same hostname. */ +config.server.host = 'YOUR-HOSTNAME.ngrok.app'; +config.server.baseUri = 'https://YOUR-HOSTNAME.ngrok.app'; From 0b7e2cda125feb67acf76d9bf95593e2c11c0176 Mon Sep 17 00:00:00 2001 From: Derek Scruggs Date: Thu, 20 Aug 2026 09:59:48 -0500 Subject: [PATCH 3/3] Address review of the local dev config override. Import by file URL, not filesystem path. `import()` takes a URL, so a bare absolute path truncates at a `#` in any parent directory name -- reporting a module nobody asked for -- and on Windows parses the drive letter as a URL scheme. Reproduced both with a `#` in the path; `pathToFileURL` fixes it. Drop `server.baseUri` from the example. It is the lazily-evaluated template `https://${server.host}`, so it already follows `host`; setting both ships two values that must agree, which is the mismatch the surrounding prose warns about. Verified by removing it: both the tunnel and local origins still serve. Correct the `bindAddr` justification. `configs/dev.js` hard-assigns `config.server.bindAddr`, so setting `server.domain` cannot affect it -- the reason to prefer `server.host` is that `host` appends the port. Point `configs/dev.js` at `server.host` rather than `server.domain`. That comment is the first thing a reader opens and it recommended the knob the example warns against. Note that the file cannot register bedrock event handlers, since it is imported after `@bedrock/config-yaml` and would trip that package's last-import assertion with a message that does not explain itself. Note that overriding `server.host` stops `@bedrock/express` sending `Access-Control-Allow-Private-Network`, which it gates on `host` containing `localhost`. Traffic through the tunnel is unaffected, but a public-origin coordinator reaching the local mediator directly will start failing Chrome's private-network preflight while the file is in place. Name `local.js` as the development override channel, since `@bedrock/config-yaml` is a third nominal channel with no stated precedence. Show restricting `server.bindAddr` to loopback in the example. The all- interfaces default exists for Docker and is documented as such, but nothing offers the alternative at the point of use. Correct the README's Node and npm versions, which read v16+/v8+ against an `engines.node` of `>=24`. --- README.md | 9 ++++---- configs/dev.js | 23 +++++++++++++++----- configs/local.js.example | 46 ++++++++++++++++++++++++++++------------ 3 files changed, 56 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index cb30870..e8f180b 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ implements the [Credential Handler API][] would function. ## Requirements -* Node.js v16+ -* npm v8+ +* Node.js v24+ +* npm v10+ ## Development @@ -68,8 +68,9 @@ is gitignored. Delete it to go back to the defaults. The server reads it at startup, so restart after changing it. The example covers the case it exists for: serving the mediator under a tunnel -hostname so a phone can reach it, which needs `server.host` and -`server.baseUri` changed and cannot be done from a checked-in file. +hostname so a phone can reach it, which needs `server.host` changed and cannot +be done from a checked-in file. It also shows how to restrict `server.bindAddr` +to loopback, since `configs/dev.js` binds to all interfaces for Docker. ### Setup diff --git a/configs/dev.js b/configs/dev.js index 622db5a..590e553 100644 --- a/configs/dev.js +++ b/configs/dev.js @@ -6,9 +6,9 @@ * Copyright (c) 2015-2016, Accreditrust Technologies, LLC * All rights reserved. */ +import {fileURLToPath, pathToFileURL} from 'node:url'; import {config} from '@bedrock/core'; import {existsSync} from 'node:fs'; -import {fileURLToPath} from 'node:url'; import os from 'node:os'; import path from 'node:path'; @@ -25,10 +25,23 @@ config.paths.log = path.join(os.tmpdir(), 'authn.localhost'); config.server.bindAddr = ['0.0.0.0']; /* Optional local overrides, loaded last so they win. `configs/local.js` is -gitignored, so machine-specific settings -- e.g. pointing `server.domain` at -a tunnel hostname so a phone can reach the mediator -- stay out of the -tracked config. */ +gitignored, so machine-specific settings -- e.g. pointing `server.host` at a +tunnel hostname so a phone can reach the mediator -- stay out of the tracked +config. See `configs/local.js.example`. + +This is the development override channel. `@bedrock/config-yaml` (imported +from `lib/index.js`) is another, but it reads `/etc/bedrock-config/app.yaml` +or a base64 blob in `BEDROCK_CONFIG`, which suits deployment rather than a +working copy. + +Assign config values only. The file is imported after `lib/index.js` has +imported `@bedrock/config-yaml`, so registering a `bedrock.events` handler +here fails with `"bedrock-config-yaml" must be the last import`. + +`pathToFileURL` because `import()` takes a URL, not a filesystem path: a +checkout under a directory containing `#` truncates at the fragment, and a +Windows path parses its drive letter as a URL scheme. */ const localConfigPath = path.join(__dirname, 'local.js'); if(existsSync(localConfigPath)) { - await import(localConfigPath); + await import(pathToFileURL(localConfigPath).href); } diff --git a/configs/local.js.example b/configs/local.js.example index e7f9ddd..7656e7b 100644 --- a/configs/local.js.example +++ b/configs/local.js.example @@ -4,24 +4,30 @@ * Copy to `configs/local.js` and edit. That path is gitignored, so machine * specific settings stay out of the tracked config. * + * Assign config values only. This file is imported after `lib/index.js` has + * imported `@bedrock/config-yaml`, so registering a `bedrock.events` handler + * here fails with `"bedrock-config-yaml" must be the last import`. + * * Copyright (c) 2026, Digital Bazaar, Inc. */ import {config} from '@bedrock/core'; /* Serve the mediator under a tunnel hostname, so a phone can reach it. -`server.host` and `server.baseUri` are what the HTTP->HTTPS redirect and the -mediator's own origin checks use. Left at their defaults, a request arriving -through a tunnel is redirected to `https://authn.localhost:33443`, which -resolves only on the machine running the server -- so the mediator iframe -fails to load on a phone. +`server.host` is what the HTTP->HTTPS redirect uses. Left at its default, a +request arriving through a tunnel is redirected to +`https://authn.localhost:33443`, which resolves only on the machine running +the server -- so the mediator iframe fails to load on a phone. + +Set `server.host` rather than `server.domain`: `host` is derived as +`domain + ':' + port` unless the port is 443, so overriding `domain` yields a +tunnel hostname with the local port glued on. Overriding the port to 443 to +avoid that would stop the server listening on 33443. Setting `host` leaves +both local ports (33443 HTTPS, 33080 HTTP) alone. -Set these two derived values rather than `server.domain`: that also drives -`server.bindAddr`, and `server.host` appends `server.port` unless the port is -443. Overriding the port to 443 would stop the server listening on 33443 -locally. Setting `host` and `baseUri` leaves the local ports (33443 HTTPS, -33080 HTTP) alone, so `https://authn.localhost:33443` keeps working alongside -the tunnel. +`server.baseUri` needs no override: it is the lazily-evaluated template +`https://${server.host}`, so it follows `host` automatically. Setting both +means two values that have to agree. Point the tunnel at the HTTPS port (33443), not the HTTP one. The HTTP port redirects unconditionally to `https://`, which with a tunnel @@ -31,6 +37,20 @@ hostname redirects to itself forever. For example, with ngrok: --upstream-tls-verify=false https://localhost:33443 Coordinator sites under test must point their CHAPI mediator origin at the -same hostname. */ +same hostname. + +One side effect while this is set: `@bedrock/express` sends +`Access-Control-Allow-Private-Network` only when `server.host` contains +`localhost`, so a coordinator site on a public origin that reaches the local +mediator *directly* -- rather than through the tunnel -- will start failing +Chrome's private-network preflight. Traffic through the tunnel is unaffected. +Delete this file to restore the header. */ config.server.host = 'YOUR-HOSTNAME.ngrok.app'; -config.server.baseUri = 'https://YOUR-HOSTNAME.ngrok.app'; + +/* `configs/dev.js` binds to all interfaces so Docker containers can reach the +host via `host.docker.internal`. That also exposes the dev mediator on every +LAN interface, which is worth avoiding on a shared or public network. Restrict +it here when you do not need Docker access: + +config.server.bindAddr = ['127.0.0.1']; +*/