Skip to content

Feat/runtime pinning and auth hardening - #328

Merged
AbelOsaretin merged 4 commits into
Heliobond:mainfrom
Joycejay17:feat/runtime-pinning-and-auth-hardening
Jul 30, 2026
Merged

Feat/runtime pinning and auth hardening#328
AbelOsaretin merged 4 commits into
Heliobond:mainfrom
Joycejay17:feat/runtime-pinning-and-auth-hardening

Conversation

@Joycejay17

Copy link
Copy Markdown
Contributor

Startup error handling, runtime pinning, and auth/header hardening

Description

Four scoped fixes, one commit each.

#206app.listen() had no error handling. A port conflict crashed with a raw
Error: listen EADDRINUSE: address already in use :::3001 stack trace. A server.on("error", …)
handler now maps the bind failure to a readable message and exits 1, so Docker/systemd/k8s see a
failed start rather than an opaque crash:

{"level":"error", … ,"message":"[startup] Port 3001 is already in use. Stop the process using it or set PORT to a free port.","error_code":"EADDRINUSE","port":3001}

EACCES (privileged port) and EADDRNOTAVAIL get their own messages; anything else falls back to
Failed to bind to port <port>: <message>. The logic lives in src/lib/listen-errors.ts with an
injectable exit so it is unit-testable without killing the test runner.

#207 — the runtime version was unenforced. README and the Dockerfile say Node.js 20, but there was
no engines field, no .nvmrc, and oven-sh/setup-bun@v2 ran unpinned in CI. Added
engines.node: ">=20.0.0" + engines.bun: ">=1.0.0", an .nvmrc pinning 20, and
bun-version: "1.x" on every setup-bun step (ci.yml, release.yml, security-audit.yml) so all
workflows resolve the same runtime.

#208X-Frame-Options was SAMEORIGIN. helmet was already wired up in
src/middleware/securityHeaders.ts and covers the rest of the required header set, so the only gap
against the issue was frameguard. Switched to DENY: this is a JSON API that is never framed, and the
CSP already declares frame-ancestors 'none', so DENY matches the policy already advertised.

#209 — the admin bearer token used !==. String comparison short-circuits on the first differing
byte, leaking how many leading characters matched and allowing byte-by-byte recovery of the key. The
middleware now uses timingSafeCompare (src/lib/timing-safe.ts), which SHA-256s both values to a
fixed 32-byte digest before crypto.timingSafeEqual — so neither the content nor the length of the
supplied token affects the comparison time. The auth flow and both error responses (500 server_misconfigured, 401 unauthorized) are unchanged.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Testing checklist

  • Tests pass locally
  • Added new tests (if applicable)
  • Code follows project conventions

New suites — src/__tests__/listen-errors.test.ts, timing-safe.test.ts,
runtime-version-pinning.test.ts — plus additions to admin.test.ts (near-miss tokens: differing
final character, prefix of the key, extension of the key) and securityHeaders.test.ts updated to
assert DENY. 34 tests across the four touched suites, all green and stable across repeated runs.

Verified the EADDRINUSE path end to end against a real occupied port: the message above is logged
and the process exits 1.

npx eslint reports 0 errors on every touched file and prettier --check is clean.

Pre-existing failures, untouched by this PR

bun run build and parts of the suite were already red on main before these changes, and this PR
neither fixes nor worsens them — the set of failing suites is byte-identical with the changes stashed:

  • src/lib/registry.ts:84,86 — two TS2339 errors against SimulateTransactionResponse (Stellar SDK
    typing drift). These fail tsc on main.
  • 7 pre-existing failures in admin.test.ts under POST /update-scores, plus 19 other suites
    (typescript-strict, error-response-consistency, routes, prometheus-metrics, …).

Happy to fold a fix for the registry.ts typing into this branch or a separate PR if you'd prefer —
left alone here to keep each commit scoped to its issue.

Related issues

Closes #206
Closes #207
Closes #208
Closes #209

Screenshots (if applicable)

n/a

app.listen() had no error handling, so a port conflict surfaced as an
uncaught EADDRINUSE with a raw stack trace. Attach an error handler that
maps EADDRINUSE, EACCES and EADDRNOTAVAIL to a readable explanation, logs
it through the structured logger, and exits 1 so process supervisors treat
the start-up as failed.

Closes Heliobond#206
The project targets Node.js 20 (README, Dockerfile) but nothing enforced
it: no engines field, no .nvmrc, and setup-bun ran unpinned in CI. Add
engines.node >=20.0.0 and engines.bun >=1.0.0, an .nvmrc for nvm users,
and pin bun-version: "1.x" on every setup-bun step so CI, release and the
security audit all resolve the same runtime.

Closes Heliobond#207
helmet already covered the header set, but frameguard was configured as
SAMEORIGIN. This service is a JSON API and is never framed, and the CSP
already declares frame-ancestors 'none', so DENY is both stricter and
consistent with the policy already advertised.

Closes Heliobond#208
The admin auth middleware compared the Authorization header to the API key
with !==, which short-circuits on the first differing byte and leaks how
many leading characters matched. Add timingSafeCompare, which hashes both
values to a fixed 32-byte digest before crypto.timingSafeEqual so neither
the contents nor the length of the token affect the comparison time. The
auth flow and both error responses are unchanged.

Closes Heliobond#209
@drips-wave

drips-wave Bot commented Jul 30, 2026

Copy link
Copy Markdown

@Joycejay17 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@AbelOsaretin
AbelOsaretin merged commit 82c3435 into Heliobond:main Jul 30, 2026
0 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants