Conversation
Creates per-thread UDS mirrors for secure HTTP/TCP servers, writing YAML metadata files containing certificate information. Includes cleanup helpers for socket lifecycle management and comprehensive unit tests for metadata serialization and file operations.
When symphony connects to a per-thread UDS mirror and sends a PROXY v1 header, strip it before the HTTP parser sees it and set socket.remoteAddress / socket.remotePort to the real client values. Uses prependListener so our one-time data handler fires before Node's HTTP parser. socket.unshift() returns any non-header bytes back to the read buffer. Connections without a PROXY header pass through unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace node-unix-socket native addon with native reusePort (Node v22+)
- Add BunRequest adapter wrapping Web Fetch API Request
- Branch http.ts to use Bun.serve() with fetch handler when running on Bun
- Bridge Operations API (Fastify) to Bun via inject() in bunDelegateToNodeServer
- Add listenOnPortsBun() using Bun.serve({ reusePort: true }) per worker
- Support TLS/secure ports on Bun via Bun.serve({ tls: { cert, key } })
- Create UDS mirror sockets for secure ports via Bun.serve({ unix })
- Guard Node-specific APIs: v8, inspector, worker.performance, TLS monkey-patches
- Fix performance.eventLoopUtilization() NotImplementedError on Bun
- Skip Node version check when running on Bun
- Parameterize integration tests via HARPER_RUNTIME env var (node|bun)
- Add CI jobs to run integration tests and API tests on Bun
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Required by repo policy that all actions must be pinned to a full-length commit SHA. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 4.x-upgrade test returns early from before() when HARPER_LEGACY_VERSION_PATH is not set, leaving ctx.harper undefined. killHarper/teardownHarper now no-op gracefully in that case instead of crashing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
listenOnPortsBun() was using +port directly on keys like "127.0.0.14:9926", which produces NaN and hits the isNaN guard, skipping every port. Parse host:port strings the same way listenOnPorts() does for Node — split on the last colon and pass hostname + numeric port to Bun.serve(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Node, Harper's httpChain auth middleware sets request._nodeRequest.user for loopback connections in dev mode (AUTHORIZE_LOCAL), which Fastify picks up via req.raw.user and bypasses its own auth check (fastifyAuth.js:35). On Bun, bunDelegateToNodeServer calls fastify.inject() with a fresh synthetic request — no req.raw.user, so Fastify re-runs auth and returns 401. Fix: strip any incoming x-harper-internal-pre-auth-user header (preventing forgery), then set it in the inject() call when Harper's auth middleware has already authenticated the request. fastifyAuth.js trusts this header as an equivalent to req.raw.user. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'send' module (used for static files) calls setHeader/writeHead on the pipe destination. In the Bun handler, the destination must be a Writable with a ServerResponse shim so those calls capture headers into the web Response. The critical bug: 'on-finished' (a send dependency) calls isFinished() which checks msg.finished. In Bun, Writable.finished is undefined (not a boolean), so isFinished() returns undefined. Since undefined !== false, on-finished immediately schedules cleanup() via setImmediate, destroying the ReadStream before any data flows — causing the response to hang forever. Fix: add finished: false to the shim object so isFinished() sees a boolean false and correctly waits for the 'finish' event before calling cleanup(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
👀 |
…CE_EXIT` self-termination mechanism
… headers in Node.js v24+
…rove `process.exit` handling for Bun by force-closing server connections.
… headers in Node.js v24+
…to single HTTP worker - Remove socket file descriptor passing and proxying logic from http.ts and threadServer.js - Remove startSocketServer and session affinity implementation from socketRouter.ts - Remove proxySocket, proxyRequest, and deliverSocket functions - Simplify lite.js to only start HTTP threads without socket servers - Add Windows-specific CI workflow for build and integration tests - Limit Windows to single HTTP worker (no SO_REUSEPORT support) - Fix package.json test scripts to use double quotes for Windows compatibility - Disable segfault handler on Bun runtime
…support - Refactor static.ts to properly handle index.html paths, simplify path joining, and log received entries. - Fix deriveURLPath to standardize Windows-style paths and eliminate incorrect path joining for URLs. - Enhance checkAllowedModulePath for precise error reporting and improved URL-to-path conversion. - Add MQTT and MQTTS port constants to harperLifecycle and update lifecycle variables to prevent port conflicts in integration tests.
| if (typeof process.setSourceMapsEnabled === 'function') { | ||
| process.setSourceMapsEnabled(true); // this is necessary for source maps to work, at least on the main thread. | ||
| } |
There was a problem hiding this comment.
Can make this a one-liner if you'd like:
| if (typeof process.setSourceMapsEnabled === 'function') { | |
| process.setSourceMapsEnabled(true); // this is necessary for source maps to work, at least on the main thread. | |
| } | |
| process.setSourceMapsEnabled?.(true); // this is necessary for source maps to work, at least on the main } |
| } | ||
| } | ||
| export let createReuseportFd: any; | ||
| if (platform() != 'win32') createReuseportFd = require('node-unix-socket').createReuseportFd; |
There was a problem hiding this comment.
We can remove node-unix-socket from the package.json.
|
I nuke my Note that if I start Harper with Node.js, it works just fine. |
|
After nuking I'm guessing that's not a surprise. :) |
# Conflicts: # integrationTests/upgrade/4.x-upgrade.test.ts # integrationTests/utils/harperLifecycle.ts # package.json # server/http.ts # server/threads/threadServer.js
Installs Bun alongside Node.js and adds an entrypoint script that selects the runtime via HARPER_RUNTIME=bun at container start. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bun's net.Server compat layer does not support the reusePort option on macOS, returning ENOTSUP. Raw socket servers (e.g. MQTT on 1883) don't need it — only the HTTP Bun.serve() workers do. Also handle EADDRINUSE gracefully so workers 1+ silently skip ports already held by worker 0. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| // On Bun, Harper's auth middleware passes pre-authenticated users via this internal header. | ||
| // It is stripped from real network requests in bunDelegateToNodeServer, so it is safe to trust here. | ||
| const preAuthUser = req.headers?.[INTERNAL_USER_HEADER]; | ||
| if (preAuthUser) return next(null, JSON.parse(preAuthUser)); |
There was a problem hiding this comment.
Auth bypass on Node.js — blocker
The comment says "It is stripped from real network requests in bunDelegateToNodeServer" — that's true on Bun, but this code also runs on Node.js, where no such stripping occurs.
On Node.js, deliverSocket routes raw sockets directly to Fastify with all original headers intact. A client that sends x-harper-internal-pre-auth-user: {"role":{"permission":{"super_user":true}}} will have that header appear in req.headers here, req.raw?.user will be undefined (unauthenticated), and next(null, parsedUser) will be called — full auth bypass.
The check needs to be gated on Bun:
| if (preAuthUser) return next(null, JSON.parse(preAuthUser)); | |
| if (typeof globalThis.Bun !== 'undefined') { | |
| const preAuthUser = req.headers?.[INTERNAL_USER_HEADER]; | |
| if (preAuthUser) return next(null, JSON.parse(preAuthUser)); | |
| } |
|
|
||
| // Handle entry events for the default entry handler based on the `files` and `urlPath` options | ||
| scope.handleEntry((entry) => { | ||
| logger.error('static received entry', entry); |
There was a problem hiding this comment.
ReferenceError at runtime — blocker
logger is not defined in this file (imports are realpathSync, existsSync, join, Scope, send). The scoped logger is scope.logger, used correctly on line 32. This will throw ReferenceError: logger is not defined the first time any static file is scanned, completely breaking static file serving.
This looks like a debugging line that was accidentally left in.
| logger.error('static received entry', entry); |
|
3 blockers remain unresolved — none were addressed in this push (commits up to 1. Auth bypass on Node.js (
|
I noticed that on node.js I have been seeing new cert errors after running tests:
I wonder if this is Bun's manifestation of the same thing?
Well, a little surprised. Apparently there is just a gap in Bun's support for |
…elink step Inline env var syntax (VAR=val cmd) is not supported on Windows CMD, breaking the Windows integration test job. Moving the variable into each job's env: block fixes Windows and keeps things consistent. Also adds the missing `npm install --ignore-scripts` relink step to the Bun integration test job; artifact download does not preserve bin symlinks so harper-integration-test-run was not found. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| // On Bun, Harper's auth middleware passes pre-authenticated users via this internal header. | ||
| // It is stripped from real network requests in bunDelegateToNodeServer, so it is safe to trust here. | ||
| const preAuthUser = req.headers?.[INTERNAL_USER_HEADER]; | ||
| if (preAuthUser) return next(null, JSON.parse(preAuthUser)); |
There was a problem hiding this comment.
Auth bypass on Node.js (same concern as prior review, still unaddressed)
fastifyAuth.js trusts x-harper-internal-pre-auth-user without a Bun guard. On Bun this is safe — bunDelegateToNodeServer strips the header before fastify.inject(). On Node.js, Fastify receives the raw IncomingMessage via the unhandled-event cascade, with all client headers intact. Any client can forge this header and bypass authentication.
Suggested fix: gate the header check on the Bun runtime:
| if (preAuthUser) return next(null, JSON.parse(preAuthUser)); | |
| const preAuthUser = typeof globalThis.Bun !== 'undefined' && req.headers?.[INTERNAL_USER_HEADER]; |
|
|
||
| // Handle entry events for the default entry handler based on the `files` and `urlPath` options | ||
| scope.handleEntry((entry) => { | ||
| logger.error('static received entry', entry); |
There was a problem hiding this comment.
ReferenceError on every static file scan (same concern as prior review, still unaddressed)
logger is not defined in this scope — the scoped logger is scope.logger (used correctly on line 32). This will throw on the first entry event fired by the static plugin, breaking static file serving entirely. Looks like a debug line accidentally left in.
| logger.error('static received entry', entry); | |
| scope.logger.debug('static received entry', entry); |
Or just remove the line.
|
|
||
| - name: Build | ||
| run: npm run build | ||
| continue-on-error: true # we currently have type errors so just ignore that |
There was a problem hiding this comment.
Build failures silently ignored on Windows
continue-on-error: true suppresses all build failures (not just type warnings). Per repo policy, Harper core's build should pass cleanly — the tsc || true pattern is explicitly called out as not applicable here. A CI job that passes even when compilation fails provides false confidence.
The comment acknowledges existing type errors; those should be fixed before enabling the Windows build job, not masked. If this job is meant to be informational-only in the interim, consider making the whole job continue-on-error: true at the job level and documenting the known failures in a tracking issue.
Windows job was missing `npm install --ignore-scripts` to recreate node_modules/.bin wrappers after artifact download, causing harper-integration-test-run to be unrecognized. Both Windows and Bun jobs were calling test:integration which no longer includes a glob pattern; switch to test:integration:all. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts: # server/serverHelpers/Request.ts
| // On Bun, Harper's auth middleware passes pre-authenticated users via this internal header. | ||
| // It is stripped from real network requests in bunDelegateToNodeServer, so it is safe to trust here. | ||
| const preAuthUser = req.headers?.[INTERNAL_USER_HEADER]; | ||
| if (preAuthUser) return next(null, JSON.parse(preAuthUser)); |
There was a problem hiding this comment.
Blocker (re-raised from prior review — still unresolved): The comment says the header is stripped in bunDelegateToNodeServer, but that only covers the Bun code path. On Node.js, Fastify receives requests via the unhandled event at http.ts:279 with the raw nodeRequest — no header stripping occurs before this point. Any client can send x-harper-internal-pre-auth-user on Node.js to bypass auth.
| if (preAuthUser) return next(null, JSON.parse(preAuthUser)); | |
| if (typeof globalThis.Bun !== 'undefined' && preAuthUser) return next(null, JSON.parse(preAuthUser)); |
|
|
||
| // Handle entry events for the default entry handler based on the `files` and `urlPath` options | ||
| scope.handleEntry((entry) => { | ||
| logger.error('static received entry', entry); |
There was a problem hiding this comment.
Blocker (re-raised from prior review — still unresolved): logger is not defined in this module. This throws ReferenceError: logger is not defined on the first entry event, breaking static file serving entirely. The available logger is scope.logger, but this line looks like a debug leftover and should simply be removed.
| logger.error('static received entry', entry); |
This also includes work for Windows fixes and runs the core integration tests on Windows.
Neither Windows nor Bun is running the full integration:api-tests. There are failures, and there will need to be follow-up work to provide more comprehensive support of these platforms/runtimes. But this should ensure basis support.