Files
ZachariahSharma 74e276af92 fix: bind published ports to loopback and fix runHermes hang
- Ports now bind to HERMES_PUBLISHED_BIND_IP (default 127.0.0.1) so
  NPM on the same host proxies to 127.0.0.1:7843/8645/8646 and direct
  LAN/internet access is blocked without firewall rules
- runHermes: settle promise immediately on timeout (SIGKILL) instead of
  waiting for close event — prevents hanging when hermes spawns children
  that keep stdout/stderr open after the parent is killed
- Add HERMES_ADMIN_COOKIE_SECURE env var to set Secure flag on admin
  session cookie when the admin UI is served over HTTPS
- Document NPM deployment shapes in README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 21:01:10 -06:00

73 lines
2.4 KiB
JavaScript

"use strict"
const test = require("node:test")
const assert = require("node:assert/strict")
const {
adminCredentialsMatch,
createSessionToken,
createApiKey,
hashSecret,
parseCookies,
serializeAdminCookie,
clearAdminCookie,
} = require("../lib/security.cjs")
test("adminCredentialsMatch returns true for correct credentials", (t) => {
assert.equal(adminCredentialsMatch("admin", "secret-value-123", {
username: "admin", password: "secret-value-123"
}), true)
})
test("adminCredentialsMatch returns false for wrong password", (t) => {
assert.equal(adminCredentialsMatch("admin", "wrong-value-123", {
username: "admin", password: "secret-value-123"
}), false)
})
test("adminCredentialsMatch returns false for wrong username", (t) => {
assert.equal(adminCredentialsMatch("notadmin", "secret-value-123", {
username: "admin", password: "secret-value-123"
}), false)
})
test("createApiKey returns plaintext matching hms_ prefix", (t) => {
const key = createApiKey()
assert.match(key.plaintext, /^hms_[A-Za-z0-9_-]{40,}$/)
assert.equal(key.suffix.length, 4)
assert.equal(key.suffix, key.plaintext.slice(-4))
})
test("createSessionToken returns 64-char hex hash", (t) => {
const token = createSessionToken()
assert.equal(token.hash.length, 64)
assert.match(token.plaintext, /^[A-Za-z0-9_-]+$/)
})
test("hashSecret produces consistent output", (t) => {
assert.equal(hashSecret("hello"), hashSecret("hello"))
assert.notEqual(hashSecret("hello"), hashSecret("world"))
})
test("parseCookies parses a multi-value cookie header", (t) => {
const result = parseCookies("a=1; hermes_admin=abc")
assert.deepEqual(result, { a: "1", hermes_admin: "abc" })
})
test("parseCookies returns empty object for null/undefined", (t) => {
assert.deepEqual(parseCookies(null), {})
assert.deepEqual(parseCookies(undefined), {})
assert.deepEqual(parseCookies(""), {})
})
test("serializeAdminCookie can mark admin session cookies Secure for HTTPS proxy deployments", (t) => {
const cookie = serializeAdminCookie("session-value", 3600, { secure: true })
assert.match(cookie, /; HttpOnly\b/)
assert.match(cookie, /; SameSite=Lax\b/)
assert.match(cookie, /; Secure\b/)
})
test("clearAdminCookie can mark cleared admin cookies Secure for HTTPS proxy deployments", (t) => {
const cookie = clearAdminCookie({ secure: true })
assert.match(cookie, /; Secure\b/)
})