Skip to content

realm-server: HTTPS+HTTP/2 in local dev#4797

Draft
habdelra wants to merge 22 commits into
mainfrom
worktree-cs-11114-http2-v2
Draft

realm-server: HTTPS+HTTP/2 in local dev#4797
habdelra wants to merge 22 commits into
mainfrom
worktree-cs-11114-http2-v2

Conversation

@habdelra
Copy link
Copy Markdown
Contributor

@habdelra habdelra commented May 12, 2026

⚠️ Required dev setup before pulling this branch

Local realm-server now speaks HTTPS+HTTP/2 only — there is no HTTP fallback. Every dev needs mkcert on their machine and a provisioned cert before mise run dev / pnpm start:all will work. CI is handled (init action installs mkcert and runs ensure-dev-cert automatically), but local boxes are on you.

Do this once, before your first mise run dev:

  1. Install mkcert (one-time, system package):

    • Debian/Ubuntu: sudo apt install -y mkcert libnss3-tools
    • Fedora/RHEL: sudo dnf install -y mkcert nss-tools
    • macOS (Homebrew): brew install mkcert nss
  2. Provision the cert:

    mise run infra:ensure-dev-cert

    That generates ~/.local/share/boxel/dev-certs/{localhost.pem, localhost-key.pem} and (best-effort) runs mkcert -install so your browser silently trusts it. The task is idempotent — re-runs are no-ops until the cert is within 7 days of expiry.

If mkcert is missing, mise run dev will exit with a clear install hint instead of starting. There is no skip flag and no fallback path — provisioning the cert is a hard prereq.

After pulling:

The first mise run dev will run a Postgres migration (1779100257124_canonical-url-http-to-https.js) that rewrites every text/varchar/jsonb column on every public table from http://localhost:42XX/… to https://localhost:42XX/… in place — index rows, realm registry, permissions, JSONB documents inside pristine_doc/search_doc/etc. The migration is idempotent and gated on a cheap realm_registry pre-check, so re-runs and production environments are no-ops.

If you have stale http://localhost:42XX/… URLs in personal-realm card .json files (in realms/localhost_4201/**), the wire-level HTTP→HTTPS dispatcher 301-redirects them at runtime so cards still resolve — no on-disk rewrite is required. If you want clean data:

find realms/localhost_4201 -name '*.json' -exec sed -i 's|http://localhost:4201|https://localhost:4201|g' {} +

Navigation:

Visit http://localhost:4200/ (vite host) as the manual-browser entry point. The host bundle there fetches realm data from the realm-server's https origin on :4201, which is where the HTTP/2 multiplexing win lands. Visiting https://localhost:4201/ directly works but surfaces mixed-content warnings, because vite (:4200), icons (:4206), and synapse (:8008) still serve plain HTTP.


Summary

Local dev's realm-server now speaks HTTPS+HTTP/2 on a single canonical origin (https://localhost:4201, plus https://localhost:4202 for test-realms). This unblocks the heavy aggregator-card prerender bottleneck described in CS-11114 — cohort and dashboard renders today fan out 80+ federated-search requests inside one Chromium tab, get throttled by Chrome's HTTP/1.1 6-per-origin connection ceiling, and take minutes; HTTP/2 multiplexes them over one connection and the same render finishes in seconds.

Following @lukemelia's suggestion in #4787, this PR ships the single-origin design rather than the dual-listen alternative that #4787 had been carrying. There is no separate h2 alias port, no per-page __realmH2OriginMappings__ injection, no alias-host rewrite middleware — the wire protocol and the canonical realm URL agree.

Design

  • RealmServer.listen(port): when REALM_SERVER_TLS_CERT_FILE/_KEY_FILE are set, binds a single net.Server that peeks the first byte of every connection. 0x16 (TLS ClientHello) routes to the HTTP/2 secure server; anything else is treated as plain HTTP and 301-redirected to https://<host><path>. So http://localhost:4201/foo in a browser bar or a curl invocation gets a clean redirect instead of a TLS handshake failure. Same listener, no extra port. When the cert is absent (tests/CI without the init action's cert-provision step), falls back to plain http.createServer — unchanged behavior.
  • The dispatcher tracks every accepted socket and exposes closeAllConnections() so shutdown can force-close in-flight TLS / HTTP/2 / keep-alive sessions rather than waiting for peers to release them. main.ts's existing typeof feature-detect picks it up unchanged.
  • middleware/index.ts#fullRequestURL detects ctx.req.socket.encrypted so URL-keyed realm lookup matches the HTTPS canonical. It also falls back to the HTTP/2 :authority pseudo-header when headers.host is absent.
  • middleware/index.ts#fetchRequestFromContext strips :-prefixed pseudo-headers before constructing new Request(...) — WHATWG Headers rejects them, and Node's http2 compat layer surfaces them on req.headers alongside the regular headers.
  • Defensive cert load: readFileSync and createSecureServer are wrapped in try/catch so a malformed cert downgrades to plain HTTP with a warning rather than killing boot.
  • mise run infra:ensure-dev-cert provisions the cert via mkcert and attempts mkcert -install once (non-fatal if declined). Idempotent.
  • env-vars.sh defaults REALM_BASE_URL/REALM_TEST_URL to https unconditionally and exports NODE_EXTRA_CA_CERTS pointing at mkcert's root CA so Node-side fetches trust the cert without requiring -install.
  • dev-common.sh + test-services/{host,realm-server,matrix} switch wait-on readiness probes to https-get:// when the realm URL is https.
  • Host config/environment.js defaults flip to https://localhost:4201 for realmServerURL/baseRealmURL/catalogRealmURL/legacyCatalogRealmURL/skillsRealmURL/openRouterRealmURL.
  • Prerender Chromium gets --ignore-certificate-errors when REALM_BASE_URL is https, so puppeteer's bundled NSS DB (which mkcert may or may not touch depending on platform) doesn't reject the cert.

CI

.github/actions/init/action.yml installs mkcert via apt and runs mise run infra:ensure-dev-cert as part of every job's init, so realm-servers in CI come up HTTPS+HTTP/2 the same as local. The realm-server test bootstrap (tests/index.ts) deletes the TLS env vars before any in-process fixture realm-server is spun up — supertest connects plain HTTP to those fixtures on random 127.0.0.1:444X ports, and the dispatcher's plain-HTTP→301 path would otherwise break every assertion.

Data migration

1779100257124_canonical-url-http-to-https.js:

  • Walks information_schema.columns to find every text/varchar/jsonb column on every public table.
  • Skips modules (truncated on every realm-server boot), pgmigrations/migrations (the migration trackers), and generated columns (is_generated = 'NEVER').
  • For each column, runs an in-place REPLACE(...)-based UPDATE for both http://localhost:4201https://localhost:4201 and http://localhost:4202https://localhost:4202.
  • WHERE filter restricts the touch set to rows that still contain the old URL — idempotent.
  • Gated on a cheap realm_registry pre-check so production/staging databases short-circuit before any full-column scan (canonical URLs are real hostnames there, never localhost).
  • Runs automatically: mise run dev passes --migrateDB to the realm-server, so the migration fires on the first post-pull boot.

Tests

  • packages/realm-server/tests/listener-dispatcher-test.ts covers the dispatcher: TLS h2, ALPN HTTP/1.1 fallback, plain-HTTP 301, no-Host-header raw-socket path (socket.localAddress:localPort fallback), malformed-cert downgrade, and no-cert-env-vars plain HTTP.
  • The rest of the realm-server qunit/mocha suite continues to run plain HTTP via the test-bootstrap env-var delete (no fixture changes required).

Test plan

  • mise run infra:ensure-dev-cert succeeds with mkcert installed; emits clean install hints + exits 1 when missing.
  • curl -kI --http2 https://localhost:4201/_alive returns HTTP/2 200.
  • curl -kI --http1.1 https://localhost:4201/_alive returns HTTP/1.1 200 (ALPN fallback works for h1 clients).
  • curl -sI http://localhost:4201/_alive returns HTTP/1.1 301 with Location: https://localhost:4201/_alive.
  • curl -skiL http://localhost:4201/_alive (follow redirect) lands on HTTP/2 200.
  • Pull this branch with existing local realm data → first mise run dev runs the URL-rewrite migration → realm-server boots clean on https.
  • Trigger a base-realm reindex via /_grafana-reindex?authHeader=…&realm=base/ — completes without errors.
  • After mkcert -install, open http://localhost:4200/, log in, load a card — realm fetches show h2 protocol in DevTools.
  • mise run dev shutdown closes the listener cleanly.
  • pnpm lint passes on packages/realm-server and packages/host (lint:js + prettier; pre-existing lint:types errors in ../base/*.gts are unrelated).

Closes #4787 (dual-listen approach abandoned in favor of this single-origin design).

🤖 Generated with Claude Code

Heavy aggregator-card renders (cohort, dashboards) fan out 80+
federated-search requests per render inside one Chromium tab. Chrome's
HTTP/1.1 6-per-origin connection ceiling serializes them and turns a
single render into multiple minutes; HTTP/2 multiplexes them over one
connection and the same render finishes in seconds. Browsers only do
HTTP/2 over TLS, so the local realm-server now terminates a cert.

Single-origin design: the realm-server listens on
`https://localhost:4201` (and `https://localhost:4202` for test-realms)
when the dev cert is provisioned. There is no parallel HTTP listener
and no h2 alias port; the wire protocol and the canonical realm URL
agree. In-process tests and any environment without a cert keep getting
plain HTTP/1.1 via the same `listen(port)` entry point — `RealmServer`
picks the protocol from `REALM_SERVER_TLS_CERT_FILE`/`_KEY_FILE` rather
than two separate methods.

Cert provisioning is opt-in via `mise run infra:ensure-dev-cert`:

  - Requires `mkcert` (single-origin HTTPS has no HTTP fallback in
    dev, so a missing prereq is a hard error with install hints).
  - Attempts `mkcert -install` once for system trust; declining the
    sudo prompt is non-fatal — the cert still gets generated and
    indexing keeps working via puppeteer's `--ignore-certificate-errors`
    flag and `NODE_EXTRA_CA_CERTS` for Node clients.
  - Idempotent: re-runs are a no-op until the cert is within 7 days of
    expiry.

`env-vars.sh` flips `REALM_BASE_URL`/`REALM_TEST_URL` defaults to
`https://localhost:4201`/`4202`, exports the cert paths when files
exist, and points `NODE_EXTRA_CA_CERTS` at mkcert's root CA so Node-
side fetches (worker, scripts, prerender Node) trust the cert without
requiring `mkcert -install` to have run. `dev-common.sh` switches
wait-on's readiness probes to `https-get://` when the realm URL is
HTTPS. The host's `config/environment.js` defaults flip to
`https://localhost:4201` for `realmServerURL`, `baseRealmURL`,
`catalogRealmURL`, `legacyCatalogRealmURL`, `skillsRealmURL`, and
`openRouterRealmURL`. `middleware/index.ts#fullRequestURL` now detects
`ctx.req.socket.encrypted` so URL-keyed realm lookup matches the wire
protocol — combined with the canonical-URL flip, both halves agree.

CI / hermetic test harness path stays HTTP-only: if no cert is
provisioned, `env-vars.sh` leaves the TLS env vars unset and the
realm-server boots `http.createServer`, exactly as before.

Migration after pulling: any local card data created under the old
`http://localhost:4201/...` canonical references is stale and needs to
be re-indexed. README documents the one-time `mise run
infra:full-reset` step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1655a6f2df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread mise-tasks/lib/env-vars.sh
Comment thread packages/host/config/environment.js
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

Preview deployments

Realm Server Test Results

    1 files  ±    0      1 suites  +1   49m 6s ⏱️ + 49m 6s
1 345 tests +1 345  1 255 ✅ +1 255  0 💤 ±0  90 ❌ +90 
1 424 runs  +1 424  1 326 ✅ +1 326  0 💤 ±0  98 ❌ +98 

Results for commit 03b5a55. ± Comparison against earlier commit ec75fea.

For more details on these errors, see this check.

Adds the two missing pieces from the initial HTTPS+HTTP/2 flip:

1. Same-port HTTP→HTTPS dispatcher in `server.ts`. When the realm-server
   speaks TLS, `listen(port)` now binds a net.Server that peeks the
   first byte off every connection: 0x16 (TLS ClientHello) routes to
   the http2 secure server; anything else is treated as plain HTTP and
   handed to a tiny 301-redirect handler that rewrites the URL to
   `https://<inbound-host><path>`. So `http://localhost:4201/…` in a
   browser bar or a `curl` invocation gets a clean 301 instead of a
   TLS handshake failure. Same listener, no extra port.

2. A node-pg-migrate that rewrites every URL-bearing text/varchar/jsonb
   column on every public table (except `modules`, which the
   realm-server truncates on startup) from `http://localhost:42XX` to
   `https://localhost:42XX`. Auto-discovered via
   `information_schema.columns` — covers `boxel_index`,
   `boxel_index_working`, `realm_registry`, `realm_meta`,
   `realm_metadata`, `realm_user_permissions`, `realm_versions`,
   `realm_file_meta`, `module_transpile_cache`, plus any future
   URL-bearing column that's added later (the discovery picks it up).
   WHERE-filtered so it only touches rows still containing the old URL
   — idempotent, no-op in production.

`mise run dev` already passes `--migrateDB` to the realm-server, so the
migration runs automatically on the first post-pull boot. README's
"Local HTTPS dev access" section is rewritten to describe the new
auto-migration flow (no more `mise run infra:full-reset` callout).

Schema file renamed from `1779100257123_schema.sql` to
`1779200000000_schema.sql` so host/config/environment.js's
migration-vs-schema-name sentinel matches the new latest migration.
Content is unchanged (the new migration is data-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI was failing across host/realm-server/matrix test suites because
ensure-dev-cert exited non-zero when mkcert was missing, killing the
mise dep chain before any service started, and because env-vars.sh
flipped REALM_BASE_URL to https unconditionally — so even when the
realm-server fell back to plain HTTP, every consumer was still asked
to fetch against https. The host config defaults had the same
problem: hardcoded https meant the in-browser realmServerURL didn't
match the wire scheme.

Three fixes, gated on cert presence:

1. `ensure-dev-cert` now exits 0 with a soft warning when mkcert is
   missing. The realm-server's `listen()` already falls back to plain
   `http.createServer` when the TLS env vars are unset, so this is
   the honest behavior for CI / hermetic-test environments.
2. `env-vars.sh` defaults `REALM_BASE_URL`/`REALM_TEST_URL` to http
   and only upgrades them to https inside the cert-detected block
   alongside the existing TLS env var exports.
3. `packages/host/config/environment.js` derives its scheme from
   `process.env.REALM_BASE_URL`, so the host config follows the same
   cert-presence-driven flip rather than baking https into the JS
   defaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables local-dev realm-server to serve a single canonical HTTPS origin with HTTP/2 (plus same-port HTTP→HTTPS redirect) to remove Chrome’s HTTP/1.1 per-origin connection bottleneck during heavy prerender/search fan-outs, and migrates local indexed data from http://localhost:42xx to https://localhost:42xx.

Changes:

  • Add TLS-capable listener that multiplexes HTTPS/HTTP2 and HTTP redirect on the same port; update URL construction to recognize TLS sockets.
  • Default local dev URLs/config/docs to https://localhost:4201 (+ :4202 for test realms) and add mkcert-based cert provisioning.
  • Add a Postgres migration to rewrite persisted localhost canonical URLs from http→https.

Reviewed changes

Copilot reviewed 45 out of 46 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
README.md Document local HTTPS/HTTP2 setup, migration, and updated local URLs.
QUICKSTART.md Update quickstart URLs to https://localhost:4201.
packages/realm-server/tests/types-endpoint-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/server-endpoints/search-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/server-endpoints/search-prerendered-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/server-endpoints/info-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/server-endpoints/index-responses-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/server-endpoints/helpers.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/server-endpoints/federated-types-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/server-endpoints/authentication-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/request-forward-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/realm-endpoints/user-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/realm-endpoints/reindex-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/realm-endpoints/markdown-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/realm-endpoints/info-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/realm-endpoints/dependencies-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/realm-endpoints-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/publish-unpublish-realm-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/prerender-manager-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/openrouter-passthrough-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/module-cache-race-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/helpers/index.ts Update close helpers/types to tolerate non-http.Server server handles.
packages/realm-server/tests/get-boxel-claimed-domain-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/file-watcher-events-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/delete-boxel-claimed-domain-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/claim-boxel-domain-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/card-source-endpoints-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/card-endpoints-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/card-dependencies-endpoint-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/boxel-domain-availability-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/tests/atomic-endpoints-test.ts Use RealmHttpServer type alias for server handle.
packages/realm-server/server.ts Add TLS/http2+redirect dispatcher and export RealmHttpServer type; update listen logging.
packages/realm-server/prerender/browser-manager.ts Add --ignore-certificate-errors for prerender Chromium when using https.
packages/realm-server/middleware/index.ts Treat TLS sockets as https for fullRequestURL() computation.
packages/realm-server/main.ts Make shutdown tolerant of non-http.Server handles lacking closeAllConnections().
packages/realm-server/lib/dev-service-registry.ts Broaden registry typing to net.Server.
packages/postgres/migrations/1779200000000_canonical-url-http-to-https.js Add migration to rewrite localhost canonical URLs from http→https.
packages/host/config/schema/1779200000000_schema.sql Add regenerated host sqlite schema snapshot.
packages/host/config/environment.js Flip local default realm URLs to https.
mise-tasks/services/test-realms Ensure dev cert task runs before test realms.
mise-tasks/services/realm-server-base Ensure dev cert task runs before base realm server.
mise-tasks/services/realm-server Ensure dev cert task runs before realm server.
mise-tasks/lib/env-vars.sh Flip default realm URLs to https and export TLS cert/CA env vars.
mise-tasks/lib/dev-common.sh Use https readiness probes when realm URLs are https.
mise-tasks/infra/ensure-dev-cert New task to provision mkcert leaf cert for local HTTPS/HTTP2.
.claude/skills/indexing-diagnostics/SKILL.md Update localhost URLs and markdown formatting in diagnostics skill doc.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/realm-server/server.ts Outdated
Comment thread mise-tasks/lib/env-vars.sh Outdated
Comment thread mise-tasks/lib/env-vars.sh Outdated
Comment thread README.md
Comment thread README.md Outdated
Comment thread QUICKSTART.md
habdelra and others added 2 commits May 12, 2026 19:13
Local realm-server speaks HTTPS+HTTP/2 in every environment — there is
no HTTP fallback or opt-in. The dev cert is a hard prereq:

- `ensure-dev-cert` exits non-zero when mkcert is missing.
- `env-vars.sh` defaults `REALM_BASE_URL`/`REALM_TEST_URL` to https
  unconditionally and no longer flips schemes based on cert presence.
- `host/config/environment.js` defaults to `https://localhost:4201`
  unconditionally; the previous scheme-from-env-var branch is gone.
- The new `.github/actions/init` step installs mkcert via apt and runs
  `mise run infra:ensure-dev-cert` before any downstream job, so CI
  realm-servers boot HTTPS+HTTP/2 too. Test harnesses that launch
  Chromium already pass `--ignore-certificate-errors`; Node clients
  pick up the cert via `NODE_EXTRA_CA_CERTS`.
- README's CI/harness paragraph is rewritten to describe the cert
  provisioning in the init action (no more "boots HTTP/1.1 in CI" line).

Carries over the Copilot-flagged fixes:

- Migration renamed to `1779100257124_canonical-url-http-to-https.js`
  (one greater than the existing latest, no 6+ consecutive zeros so it
  passes `lint:migrations`) and the matching schema dump renamed.
- Migration body adds a `realm_registry` LIKE pre-check that short-
  circuits the full-column scans on production/staging databases where
  the canonical URLs never reference localhost.
- Drops the unused `/* eslint-disable camelcase */` line that
  `lint:js` flagged.
- `redirectToHttps()` parses the inbound `Host` via `new URL()` so
  bracketed IPv6 authorities (`[::1]:4201`) round-trip cleanly instead
  of the regex producing an invalid `https://::1:4201/...`.
- `env-vars.sh` no longer concatenates `NODE_EXTRA_CA_CERTS` with `:`
  separators — Node accepts a single PEM path, not a list. If the dev
  already has it set, leave it alone; otherwise point at mkcert's CA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Copilot 3230386975 — the previous QUICKSTART pointed users at
https://localhost:4201 without telling them how to provision the cert
that makes that origin work. Adds mkcert to the system dependencies
list at step 1 with platform-specific install hints and the
`mise run infra:ensure-dev-cert` one-liner, linking back to the
README's "Local HTTPS dev access" section for the full story.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three task scripts under `mise-tasks/test-services/` were stuck on the
old `http-get://${REALM_BASE_URL#http://}/base/...` readiness probe
shape that strips a hardcoded `http://`. After env-vars.sh flipped
REALM_BASE_URL to https, that strip becomes a no-op and the probe URL
turns into the malformed `http-get://https://localhost:4201/...`,
which wait-on can't reach — every CI suite that drives `mise run
test-services:*` would hang on phase-1 readiness instead of starting
the next phase.

Same fix as `mise-tasks/lib/dev-common.sh`: detect the scheme from
`$REALM_BASE_URL` / `$REALM_TEST_URL` and pick `http-get://` or
`https-get://` accordingly; strip `*://` to leave just the authority.
Also wires `infra:ensure-dev-cert` into each script's depends list so
local invocations of `mise run test-services:*` (outside CI's init
action) provision the cert before the realm-server starts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 49 out of 50 changed files in this pull request and generated 11 comments.

Comment thread mise-tasks/lib/env-vars.sh
Comment thread mise-tasks/infra/ensure-dev-cert Outdated
Comment thread packages/realm-server/tests/helpers/index.ts
Comment thread QUICKSTART.md Outdated
Comment thread packages/realm-server/middleware/index.ts Outdated
Comment thread .claude/skills/indexing-diagnostics/SKILL.md Outdated
Comment thread .claude/skills/indexing-diagnostics/SKILL.md Outdated
Comment thread README.md
Comment thread packages/realm-server/server.ts
Comment thread packages/realm-server/main.ts
habdelra and others added 9 commits May 12, 2026 19:37
Blockers (B1–B3):
- tests/index.ts deletes REALM_SERVER_TLS_CERT_FILE/_KEY_FILE before any
  fixture realm-server is spun up; without this CI's globally-provisioned
  cert leaks into supertest-driven in-process servers, the dispatcher
  binds TLS on 127.0.0.1:444X, and the plain-HTTP-from-supertest path is
  301-redirected, breaking every assertion that expects 200/4xx.
- realm-server/package.json `test:wait-for-servers` now uses
  `https-get://` to match the new wire scheme; the previous `http-get://`
  hit the dispatcher's 301 path and never reported ready.
- server.ts attaches a per-socket `error` handler before the readable
  callback so an RST mid-handshake (or any peer-side socket error)
  doesn't escalate to an uncaught exception — dispatcher is the only
  inbound listener for the realm-server, can't be allowed to crash.
- `null` reads on the dispatcher socket now `destroy()` instead of just
  resuming so half-open accumulators (port scanners, eager load
  balancers) don't tie up file descriptors.

Major (M1, M3–M5):
- README's auto-migration callout pointed at the wrong migration filename
  (1779200000000_… → 1779100257124_…).
- pg-adapter.ts env-mode regex now matches `^https?://localhost:42XX/`
  so the post-flip https canonicals get rewritten to Traefik hostnames
  when a dev switches the same DB into BOXEL_ENVIRONMENT mode.
- server.ts's serveIndex / serveFromRealm URL constructions now go
  through `fullRequestURL(ctxt)` instead of `${ctxt.protocol}//${ctxt.host}`;
  `ctxt.protocol` only honors x-forwarded-proto when `app.proxy = true`,
  while `fullRequestURL` also reads the TLS socket flag. Pre-existing
  inconsistency that the https flip would have made load-bearing.
- migration's information_schema walk excludes `is_generated = 'NEVER'`
  so a future generated column on any public table doesn't abort the DO
  block with "column can only be updated to DEFAULT".

Copilot's second pass:
- ensure-dev-cert checks for mkcert BEFORE the idempotent-skip — env-vars.sh
  needs `mkcert -CAROOT` to populate NODE_EXTRA_CA_CERTS even when an
  old cert already exists, and the previous ordering let a stale cert
  slip past with the trust path half-wired.
- middleware/index.ts `fullRequestURL` falls back to `:authority` when
  `headers.host` is absent — HTTP/2's compat layer normally populates
  host from :authority but the pseudo-header is the canonical source.
- middleware/index.ts `fetchRequestFromContext` strips `:`-prefixed
  pseudo-headers (`:method`, `:scheme`, `:path`, `:authority`) before
  feeding them into `new Request(headers)`, which WHATWG Headers rejects.
- QUICKSTART mkcert bullet's continuation line is properly indented now
  so markdown renders it inside the bullet instead of as a new paragraph.
- indexing-diagnostics SKILL.md two table rows now have the missing third
  cell so the table renders correctly.

Minor (m2, m6, n3) + Option A:
- redirectToHttps falls back to `socket.localAddress:localPort` when the
  Host header is absent (HTTP/1.0 client), instead of bare `localhost`
  that would route to port 443.
- scripts/full-reindex.sh and register-bot.sh flip to `https://` with
  `-k` (curl doesn't pick up NODE_EXTRA_CA_CERTS, and the local mkcert
  CA isn't necessarily in the system trust store).
- prerender/browser-manager.ts comment references only REALM_BASE_URL
  (REALM_SERVER_DOMAIN was stale — never exported by env-vars.sh).
- QUICKSTART step 10/11 and README's "view a realm's app" paragraph
  redirect manual-browser navigation to `http://localhost:4200/` (the
  vite host), with a note that visiting `https://localhost:4201` directly
  surfaces mixed-content warnings because vite + icons + synapse still
  speak http. Realm-server's https origin is reached only via fetches
  inside the vite-served page, which is where the federated-search h2
  win lands. README's "view example" output also flipped the realm log
  line to `https://localhost:4202/test/` to match the new canonical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README list item 3's wrapped continuation line is now indented under
  the bullet so markdown doesn't break it into a separate paragraph.
- server.ts dispatcher tracks every accepted socket in a Set and mirrors
  http.Server's `closeAllConnections()` API. main.ts's existing typeof
  feature-detect picks this up; shutdown no longer hangs on long-lived
  h2 sessions or keep-alive sockets.
- tests/listener-dispatcher-test.ts is new coverage for the dispatcher:
  generates a self-signed cert via openssl into a tmp dir, then exercises
  TLS+h2, ALPN HTTP/1.1 fallback, plain-HTTP→https 301 redirect, the
  no-Host-header path that uses `socket.localAddress`, malformed-cert
  downgrade to plain HTTP, and the no-cert-env-vars path. `createListener`
  is now exported from server.ts so the test can drive it without
  spinning up a full realm-server fixture (and the test bootstrap's
  global TLS-env-var delete doesn't interfere — each test restores its
  own env around `startListener`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`qunit/no-assert-logical-expression` was failing on three assertions
that combined multiple conditions via `&&` / `||`. Splitting them into
discrete `assert.true(...)` calls makes the failure point obvious when
a test breaks and clears the lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both `packages/workspace-sync-cli/tests/helpers/start-test-realm.ts`
and `packages/realm-test-harness/src/isolated-realm-stack.ts` spawn a
realm-server subprocess that inherits `process.env`. After CI's init
action provisions the dev cert and `env-vars.sh` exports
`REALM_SERVER_TLS_CERT_FILE/_KEY_FILE`, those env vars leak into the
spawned realm-server, which binds the HTTPS+HTTP/2 dispatcher on the
harness's chosen port. The integration tests and the realm-perf bench
both drive plain `http://localhost:<port>/...` URLs against that
server, hit the dispatcher's 301 path, and break: workspace-sync's
CLI fails its session handshake with "expected 'Authorization'
header" (it doesn't follow the redirect through the auth flow), and
the bench fails its first GET with `404` because the realm route is
behind https now.

Same shape of fix as `realm-server/tests/index.ts` for the in-process
qunit suite: destructure the two TLS env-var keys out of the spawn
env so the child inherits everything except those. Plain
`http.createServer` path, no redirect, harness HTTP URLs work as
written. Production realm-servers and local dev are unaffected
because they don't go through these harnesses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`packages/host/testem-live.js` was hardcoding `http://localhost:4201/catalog/`
as the realm URL and launching Chrome with the default trust policy. After
the HTTPS flip, the live-test runner's `discoverTestModules` fetched against
`https://localhost:4201/catalog/...` (via the host's `realmServerURL`
default) but the browser navigated to `http://localhost:4201/...`, getting
a 301 to https and then failing the cert check — `mkcert -install` in CI's
init action is best-effort and the headless Chrome in CI doesn't always
pick up the system trust store anyway.

Two fixes paired:
- Default realm URL flips to `https://localhost:4201/catalog/` so the
  navigation target matches the wire.
- Chrome's CI launch args get `--ignore-certificate-errors` so the live
  test runner accepts the mkcert leaf without depending on system trust.
  Safe — the URL is fixed by REALM_URL and the connection is loopback.

Dev (`launch_in_dev`) doesn't add the flag because local devs typically
have run `mkcert -install` successfully and the cert is trusted normally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…registry

The pre-check needs to fire on a fresh install too. `realm_registry` is
populated by the realm-server's runtime bootstrap (registry backfill +
reconciler), not by migrations, so it's empty when this migration runs
against a freshly-created DB — the migration short-circuited and the
`http://localhost:42XX` permission rows seeded by the earlier
`1726671342065_backfill-realm-owners.js` migration stayed un-rewritten.

The realm-server then matches incoming requests against the new
`https://localhost:42XX/…` canonical and the permission rows fail to
join → world-readable catalog returns 401 → Live Tests fail with
"Cannot access realm https://localhost:4201/catalog/ (HTTP 401)".

Switch the pre-check to `realm_user_permissions.realm_url`, which is
reliably populated with the localhost canonicals by the earlier
seed-style migrations. The rest of the migration body is unchanged —
the per-column WHERE clauses still restrict the touch set to rows that
actually contain the old URL, so production/staging DBs (real
hostnames, never localhost) still no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Test mode runs against the host-internal `http://test-realm/...`
virtual origin via VirtualNetwork; there is no real realm-server on
the wire. Many host test fixtures hardcode the
`http://localhost:4201/...` canonicals in mock setups, VirtualNetwork
mappings, and JSON test data, so flipping the default URLs to https
caused every fetch in the test suite to fail with
`TypeError: Failed to fetch` — the host's VirtualNetwork was wired
with https URL mappings the test mocks didn't recognize.

`environmentDefaults(environment)` now reads the ember env and picks
http for `environment === 'test'`, https otherwise. Dev gets the
HTTPS+HTTP/2 flip exactly as designed; test stays where it always was.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous test-mode-on-http revert was wrong: in Host Tests the
realm-server actually IS running (via mise run test-services:host),
and that realm-server speaks HTTPS+HTTP/2. The host bundle's defaults
need to match the wire so module/data fetches over the wire (like
GET /base/card-api during warmup) reach the live realm-server. The
http defaults were producing failed http→https mismatches.

So:
- environment.js test mode reverts to https defaults (same as dev).
- test-wait-for-servers.sh + live-test-wait-for-servers.sh default
  their readiness probe URLs to `https-get://` to match.
  live-test-wait-for-servers.sh also gets the same scheme-detection
  helper (`to_wait_scheme`) the other scripts use so an explicit
  REALM_URL with either scheme works.

`http://test-realm/...` URLs in tests (used by the in-memory test
realm registry) are still intercepted by `getRealmInfoForURL` before
any wire fetch — that path is unrelated to the wire defaults and any
remaining failures there are a separate concern from the HTTPS flip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep of every place `http://localhost:4201`/`4202` appears with
runtime impact:

Runtime / wire-touching:
- `package.json` `openrouter:sync` default REALM_URL → https
- `mise-tasks/lib/test-dev-common.sh` stub env defaults → https
- `packages/host/app/services/host-mode-service.ts`
  `originIsNotMatrixTests` accepts both http and https origins on the
  matrix-tests realm ports (https is the new default; http stays
  recognized so older snapshots still detect the test mode).
- `packages/observability/scripts/apply.sh` / `diff.sh` default
  `REALM_SERVER_URL` → https.

Cache import:
- `scripts/import-cached-index.sh` env-mode sed remap now matches both
  `http://localhost:4201` and `https://localhost:4201` — older cache
  snapshots have http canonicals, post-flip dumps have https. Either
  prefix gets rewritten to the env-mode Traefik hostname.

In-tree realm fixture data (cards served by dev realm-server):
- `packages/experiments-realm/**/*.json` and
  `packages/catalog-realm/**/*.json` `id` / `relationships` URLs
  flipped from http to https. Without this every cross-card fetch
  inside a render paid a wire-level 301 redirect from the dispatcher.

Docs:
- `README.md`, `QUICKSTART.md`, `packages/host/docs/live-tests.md`,
  `packages/software-factory/README.md`, `packages/bot-runner/README.md`,
  `docs/commands-in-headless-chrome.md` — example URLs updated.

Not flipped (intentional):
- Test fixture JSONs under `packages/host/tests/cards/`,
  `packages/realm-server/tests/cards/`, ai-bot resource chats, and
  bench-realm snapshot fixtures. Those URLs match test-side mount
  points (`http://test-realm/...`, `http://127.0.0.1:4444/test/`,
  bench-stack http://localhost:4201) where the test infrastructure
  spawns the realm-server with TLS env vars cleared and listens
  plain HTTP. Flipping them would diverge from what the test code
  registers and break the in-process fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 13, 2026

Observability diff (vs staging)

No dashboard / folder changes detected against the staging Grafana.

(Run: https://github.com/cardstack/boxel/actions/runs/25784659673)

habdelra and others added 4 commits May 12, 2026 22:33
Host Tests load the host bundle in a headless Chrome on testem (port
7357). The bundle's `realmServerURL` / `resolvedBaseRealmURL` defaults
now point at `https://localhost:4201` to match the wire, but
`mkcert -install` in CI's init action is best-effort and doesn't
reliably land mkcert's root CA in headless Chrome's NSS trust store.
Without `--ignore-certificate-errors`, every realm fetch made during
shard warmup fails with `TypeError: Failed to fetch` against the
self-signed cert and the rest of the shard never starts.

Same fix already shipped in `testem-live.js`. Loopback only, fixed
origin via host config — safe to relax cert trust.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Boxel-cli's vitest suite (and any other non-qunit caller of these
helpers) doesn't share `packages/realm-server/tests/index.ts`'s
bootstrap, so the global TLS env var delete that protects in-process
qunit fixtures didn't apply to it. The CI init action provisions the
cert, env-vars.sh exports the paths, and the test process inherits
them — the spawned realm-server then binds HTTPS+HTTP/2 on its
fixture port (`127.0.0.1:4446` for boxel-cli) and the CLI's plain-HTTP
session calls fail with `404 Not Found` from the dispatcher's 301
path.

Moving the env-var strip into the two `runTestRealmServer*` helpers
themselves makes it defense-in-depth: every caller (qunit, vitest,
software-factory harness) now goes through the same kill switch when
spinning a fixture realm-server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p2-v2

# Conflicts:
#	.claude/skills/indexing-diagnostics/SKILL.md
#	packages/realm-server/scripts/full-reindex.sh
#	packages/realm-server/tests/realm-endpoints/info-test.ts
#	packages/realm-server/tests/realm-endpoints/user-test.ts
Matrix client tests timed out waiting for
`http-get://localhost:4201/base/_readiness-check` because the realm-server
now speaks HTTPS+HTTP/2 only. Wait-on's plain http-get probe never
resolves against the https listener. Same fix for
start-without-matrix.sh (dev convenience script used to bring up the
stack without Synapse).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
habdelra and others added 3 commits May 13, 2026 00:23
Card fixture data hardcoded http://localhost:4202 in adoptsFrom.module.
With the realm-server now on HTTPS, the page is served over https and
Chrome blocks mixed-content fetches of the http module URL. Flipping
to https keeps the canonical realm URL consistent with the actual
listener scheme.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_FORCE_HTTP1

Adds an opt-in env-var toggle in createListener that swaps the ALPN h2
dispatcher for a plain HTTPS+HTTP/1.1 listener. CI workflows
(ci.yaml, ci-host.yaml, ci-software-factory.yaml) set
BOXEL_REALM_FORCE_HTTP1=1 at the workflow level so every job
exercises the h1 path.

Goal: isolate whether Chromium's --ignore-certificate-errors not fully
covering h2 streams with the mkcert leaf cert is the cause of the Host
Tests warmup hangs (and downstream test flakes). If the h1 cycle clears
those failures, we know HTTP/2 is the surface that needs work; if it
doesn't, the env-var toggle is a single revert.

createListener now returns `proto: 'http' | 'https/h1' | 'https/h2'`
instead of `isHttp2: boolean` so the listening log says exactly what
mode we're in. listener-dispatcher-test clears the env var so its
h2-mode assertions still exercise the h2 path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants