Files
bare-operating-system/packages/bare-os-booter/lib/p2p/bare-os-peer-admission.js
T
2026-08-18 18:11:28 -04:00

236 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Optional peer admission gate from **`BARE_OS_PEER_ALLOWLIST_HEX`**
* (comma-separated hex public keys; empty = deny unless explicit allow-all).
*
* **`BARE_OS_PEER_DENYLIST_HEX`** — comma-separated hex keys; if the peer matches,
* verdict is **`deny`** before allowlist evaluation (denylist wins).
*
* **`BARE_OS_PEER_REQUIRE_CAPS_JSON`** — JSON array of capability strings; when set,
* **`meta.caps`** must include every required token (hosts pass peer caps when known).
*
* When **`BARE_OS_DHT_ADDRESS_CLASS_ALLOWLIST`** is set and callers pass
* **`dhtAddressClass`** (hyperdht-addressstyle hint), the class must appear in
* the allow list after the key gate passes.
*
* Admission results use **`schema: 2`** (v1 was **`schema: 1`** only).
*
* @param {Record<string, unknown> | null | undefined} env
* @param {string} peerKeyHex
* @param {{ dhtAddressClass?: string, caps?: string[] } | null | undefined} [meta]
*/
const ADMISSION_SCHEMA = 2
/**
* @param {Record<string, unknown> | null | undefined} env
*/
function zeroTrustStrictProfile(env) {
const p = String(env?.BARE_OS_ZERO_TRUST_PROFILE || '')
.trim()
.toLowerCase()
return p === 'strict' || p === 'security'
}
/** @param {string} s */
function normKeyHex(s) {
return String(s || '')
.trim()
.toLowerCase()
.replace(/^0x/, '')
}
/** @param {string} raw */
function keySetFromCommaHex(raw) {
return new Set(
raw
.split(/[\s,]+/)
.map((s) => normKeyHex(s))
.filter(Boolean)
)
}
/**
* @param {Record<string, unknown> | null | undefined} env
* @returns {string[] | null}
*/
function parseRequireCaps(env) {
const raw = String(env?.BARE_OS_PEER_REQUIRE_CAPS_JSON || '').trim()
if (!raw) return null
try {
const j = JSON.parse(raw)
if (!Array.isArray(j)) return null
const caps = j
.map((x) => String(x).trim())
.filter(Boolean)
.slice(0, 32)
return caps.length ? caps : null
} catch {
return null
}
}
export function evaluateBareOsPeerAdmission(env, peerKeyHex, meta = undefined) {
const want = normKeyHex(peerKeyHex)
const atMs = () => Date.now()
const denyRaw = String(env?.BARE_OS_PEER_DENYLIST_HEX || '').trim()
if (denyRaw) {
const denySet = keySetFromCommaHex(denyRaw)
if (want && denySet.has(want)) {
return {
schema: ADMISSION_SCHEMA,
verdict: 'deny',
reason: 'peer_denylist',
denylistSize: denySet.size,
atMs: atMs()
}
}
}
const requireCaps = parseRequireCaps(env)
if (requireCaps && requireCaps.length > 0) {
const peerCapsRaw = meta && Array.isArray(meta.caps) ? meta.caps : []
const peerSet = new Set(
peerCapsRaw.map((c) => String(c).trim()).filter(Boolean)
)
for (const c of requireCaps) {
if (!peerSet.has(c)) {
return {
schema: ADMISSION_SCHEMA,
verdict: 'deny',
reason: 'peer_missing_cap',
requiredCaps: [...requireCaps],
atMs: atMs()
}
}
}
}
const raw = String(env?.BARE_OS_PEER_ALLOWLIST_HEX || '').trim()
const strict =
env?.BARE_OS_PEER_ALLOWLIST_STRICT === '1' ||
env?.BARE_OS_PEER_ALLOWLIST_STRICT === 'true' ||
zeroTrustStrictProfile(env)
const allowAll =
env?.BARE_OS_PEER_ALLOW_ALL === '1' || env?.BARE_OS_PEER_ALLOW_ALL === 'true'
/** @type {{ schema: number, verdict: string, note?: string, allowlistSize?: number, strictProfile?: boolean, atMs?: number, dhtAddressClassGate?: Record<string, unknown> }} */
let base
if (!raw) {
if (strict || !allowAll) {
base = {
schema: ADMISSION_SCHEMA,
verdict: 'deny',
reason: 'peer_allowlist_empty',
note:
'Peer allowlist is empty; admission denied unless BARE_OS_PEER_ALLOW_ALL=1.'
}
} else {
base = {
schema: ADMISSION_SCHEMA,
verdict: 'allow',
note: 'No allowlist and BARE_OS_PEER_ALLOW_ALL=1; peers admitted.'
}
}
} else {
const set = keySetFromCommaHex(raw)
const ok = want && set.has(want)
base = {
schema: ADMISSION_SCHEMA,
verdict: ok ? 'allow' : 'deny',
allowlistSize: set.size,
strictProfile: strict || undefined,
atMs: atMs()
}
}
const classRaw = String(env?.BARE_OS_DHT_ADDRESS_CLASS_ALLOWLIST || '').trim()
if (!classRaw) {
if (base.atMs == null) base.atMs = atMs()
return base
}
const allowClasses = new Set(
classRaw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
)
if (allowClasses.size === 0) {
if (base.atMs == null) base.atMs = atMs()
return base
}
const cls = String(meta?.dhtAddressClass || '')
.trim()
.toLowerCase()
if (!cls) {
return {
...base,
atMs: base.atMs ?? atMs(),
dhtAddressClassGate: {
schema: 1,
mode: 'no_peer_class_hint',
note: 'BARE_OS_DHT_ADDRESS_CLASS_ALLOWLIST is set but caller did not supply dhtAddressClass — stock Hyperswarm path does not classify peers; allow key verdict only.'
}
}
}
if (!allowClasses.has(cls)) {
return {
schema: ADMISSION_SCHEMA,
verdict: 'deny',
reason: 'dht_address_class',
dhtAddressClass: cls,
allowedClasses: [...allowClasses],
note: 'Peer DHT address class not listed in BARE_OS_DHT_ADDRESS_CLASS_ALLOWLIST.',
atMs: atMs()
}
}
if (base.verdict === 'deny') return base
return {
...base,
atMs: base.atMs ?? atMs(),
dhtAddressClassGate: { schema: 1, mode: 'allow', class: cls }
}
}
/**
* Build an event-bus row for **`peer_admission`** audit (**never** includes full peer keys).
*
* @param {string} peerKeyHex
* @param {{ verdict: string, reason?: string, schema: number }} result
* @param {{ dhtAddressClass?: string } | null | undefined} [meta]
*/
export function bareOsFormatPeerAdmissionAuditEvent(peerKeyHex, result, meta) {
const norm = String(peerKeyHex || '')
.trim()
.toLowerCase()
.replace(/^0x/, '')
const keyShort = norm ? norm.slice(0, 16) : ''
return {
type: 'peer_admission',
verdict: result.verdict,
reason: result.reason,
peerKeyHexPrefix: keyShort || undefined,
admissionSchema: result.schema,
dhtAddressClass:
meta && typeof meta === 'object'
? String(meta.dhtAddressClass || '').trim() || undefined
: undefined
}
}
/**
* Per-bucket rate gate for admission audit (**mutates** **`lastMap`** on allow).
*
* @param {string} bucket
* @param {number} now
* @param {number} rateMs
* @param {Map<string, number>} lastMap
*/
export function bareOsPeerAdmissionAuditRateAllow(bucket, now, rateMs, lastMap) {
if (rateMs <= 0) return true
const last = lastMap.get(bucket) ?? 0
if (now - last < rateMs) return false
lastMap.set(bucket, now)
return true
}