236 lines
8.1 KiB
JavaScript
236 lines
8.1 KiB
JavaScript
/**
|
|
* Stock `ctx.bareOsHrpcRequest` factory (strict validation + built-in routes).
|
|
*/
|
|
import b4a from 'b4a'
|
|
import {
|
|
parseBareOsHrpcAllowlistJson,
|
|
bareOsHrpcAllowlistDeniesRoute
|
|
} from './bare-os-hrpc-allowlist.js'
|
|
import {
|
|
BARE_OS_HRPC_ROUTE_TABLE_SCHEMA_VERSION,
|
|
BARE_OS_HRPC_STOCK_ROUTES
|
|
} from './bare-os-hrpc-route-table.js'
|
|
|
|
/**
|
|
* @typedef {object} StockHrpcDeps
|
|
* @property {Record<string, string | undefined>} env
|
|
* @property {string} sessionId
|
|
* @property {{ readFile?: Function } | null | undefined} vfs
|
|
* @property {{ os?: { execRpc?: Function, searchLocal?: Function } } | null | undefined} disk
|
|
* @property {unknown} capabilityWords
|
|
* @property {string} protocolVersion
|
|
* @property {string} booterVersion
|
|
* @property {(row: Record<string, unknown>) => unknown} [auditAppend]
|
|
*/
|
|
|
|
/**
|
|
* @param {StockHrpcDeps} deps
|
|
*/
|
|
export function createStockBareOsHrpcRequest(deps) {
|
|
const {
|
|
env,
|
|
sessionId,
|
|
vfs,
|
|
disk,
|
|
capabilityWords,
|
|
protocolVersion,
|
|
booterVersion,
|
|
auditAppend
|
|
} = deps
|
|
|
|
/**
|
|
* Built-in hrpc bridge with strict validation and optional host event emission.
|
|
* Hosts can still replace `ctx.bareOsHrpcRequest`, but the stock handler is functional.
|
|
* @param {string} service
|
|
* @param {string} method
|
|
* @param {Record<string, unknown>} [payload]
|
|
*/
|
|
async function stockBareOsHrpcRequest(service, method, payload = {}) {
|
|
const svc = String(service || '').trim()
|
|
const m = String(method || '').trim()
|
|
if (!/^[a-zA-Z0-9._-]{1,64}$/.test(svc)) {
|
|
throw new Error('bareOsHrpcRequest: invalid service')
|
|
}
|
|
if (!/^[a-zA-Z0-9._-]{1,96}$/.test(m)) {
|
|
throw new Error('bareOsHrpcRequest: invalid method')
|
|
}
|
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
throw new Error('bareOsHrpcRequest: payload must be an object')
|
|
}
|
|
|
|
const hrpcAuditOn =
|
|
env.BARE_OS_HRPC_AUDIT === '1' || env.BARE_OS_HRPC_AUDIT === 'true'
|
|
/** @param {Record<string, unknown>} row */
|
|
function hrpcAuditAppend(row) {
|
|
if (!hrpcAuditOn || typeof auditAppend !== 'function') return
|
|
void auditAppend({
|
|
type: 'hrpc.request',
|
|
service: svc,
|
|
method: m,
|
|
sessionId,
|
|
ts: Date.now(),
|
|
...row
|
|
})
|
|
}
|
|
|
|
const allowParsed = parseBareOsHrpcAllowlistJson(
|
|
String(env.BARE_OS_HRPC_ALLOWLIST_JSON || '')
|
|
)
|
|
if (allowParsed.parseError) {
|
|
throw new Error('bareOsHrpcRequest: invalid BARE_OS_HRPC_ALLOWLIST_JSON')
|
|
}
|
|
if (bareOsHrpcAllowlistDeniesRoute(allowParsed.allow, svc, m)) {
|
|
hrpcAuditAppend({ ok: false, reason: 'allowlist_denied' })
|
|
throw new Error('bareOsHrpcRequest: method denied by allowlist')
|
|
}
|
|
|
|
if (svc === 'kernel' && m === 'ping') {
|
|
hrpcAuditAppend({ ok: true, route: 'kernel.ping' })
|
|
return { ok: true, sessionId, ts: Date.now() }
|
|
}
|
|
if (svc === 'kernel' && m === 'capabilities') {
|
|
hrpcAuditAppend({ ok: true, route: 'kernel.capabilities' })
|
|
return {
|
|
ok: true,
|
|
sessionId,
|
|
capabilityWords,
|
|
protocolVersion,
|
|
booterVersion,
|
|
hrpcRouteTableSchemaVersion: BARE_OS_HRPC_ROUTE_TABLE_SCHEMA_VERSION,
|
|
hrpcStockRoutes: [...BARE_OS_HRPC_STOCK_ROUTES]
|
|
}
|
|
}
|
|
if (svc === 'vfs' && m === 'readText') {
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
hrpcAuditAppend({ ok: false, reason: 'vfs_unavailable' })
|
|
throw new Error('bareOsHrpcRequest: vfs.readFile unavailable')
|
|
}
|
|
const reqPath = String(payload.path || '').trim()
|
|
if (!reqPath.startsWith('/')) {
|
|
hrpcAuditAppend({ ok: false, reason: 'invalid_path' })
|
|
throw new Error('bareOsHrpcRequest: payload.path must be absolute')
|
|
}
|
|
const buf = await vfs.readFile(reqPath)
|
|
hrpcAuditAppend({ ok: true, route: 'vfs.readText' })
|
|
return {
|
|
ok: true,
|
|
path: reqPath,
|
|
text: b4a.toString(buf, 'utf8')
|
|
}
|
|
}
|
|
if (svc === 'bare_os' && m === 'echo') {
|
|
hrpcAuditAppend({ ok: true, route: 'bare_os.echo' })
|
|
return { ok: true, payload }
|
|
}
|
|
if (svc === 'bare_os' && m === 'disk_os_hints') {
|
|
if (!disk?.os || typeof disk.os.execRpc !== 'function') {
|
|
hrpcAuditAppend({ ok: false, reason: 'disk_os_unavailable' })
|
|
throw new Error('bareOsHrpcRequest: disk.os bridge unavailable')
|
|
}
|
|
const txt = await disk.os.execRpc('bare_os', 'disk_os_hints', [])
|
|
let parsed = null
|
|
try {
|
|
parsed = JSON.parse(txt)
|
|
} catch {
|
|
parsed = { error: 'invalid_json' }
|
|
}
|
|
hrpcAuditAppend({ ok: true, route: 'bare_os.disk_os_hints' })
|
|
return { ok: true, text: txt, json: parsed }
|
|
}
|
|
if (svc === 'bare_os' && m === 'search_local') {
|
|
if (!disk?.os || typeof disk.os.searchLocal !== 'function') {
|
|
hrpcAuditAppend({ ok: false, reason: 'disk_os_unavailable' })
|
|
throw new Error('bareOsHrpcRequest: disk.os searchLocal unavailable')
|
|
}
|
|
const query = String(payload.query ?? payload.q ?? '').trim()
|
|
if (!query) {
|
|
hrpcAuditAppend({ ok: false, reason: 'invalid_query' })
|
|
throw new Error('bareOsHrpcRequest: payload.query (or q) is required')
|
|
}
|
|
if (query.length > 512) {
|
|
hrpcAuditAppend({ ok: false, reason: 'query_too_long' })
|
|
throw new Error('bareOsHrpcRequest: payload.query exceeds 512 chars')
|
|
}
|
|
const paths = await disk.os.searchLocal(query)
|
|
hrpcAuditAppend({ ok: true, route: 'bare_os.search_local' })
|
|
return { ok: true, query, paths }
|
|
}
|
|
if (svc === 'bare_os' && m === 'replication_operator_sketch') {
|
|
if (!disk?.os || typeof disk.os.execRpc !== 'function') {
|
|
hrpcAuditAppend({ ok: false, reason: 'disk_os_unavailable' })
|
|
throw new Error('bareOsHrpcRequest: disk.os bridge unavailable')
|
|
}
|
|
const txt = await disk.os.execRpc(
|
|
'bare_os',
|
|
'replication_operator_sketch',
|
|
[]
|
|
)
|
|
let parsed = null
|
|
try {
|
|
parsed = JSON.parse(txt)
|
|
} catch {
|
|
parsed = { error: 'invalid_json' }
|
|
}
|
|
hrpcAuditAppend({
|
|
ok: true,
|
|
route: 'bare_os.replication_operator_sketch'
|
|
})
|
|
return { ok: true, text: txt, json: parsed }
|
|
}
|
|
if (svc === 'bare_os' && m === 'replication_snapshot') {
|
|
if (!disk?.os || typeof disk.os.execRpc !== 'function') {
|
|
hrpcAuditAppend({ ok: false, reason: 'disk_os_unavailable' })
|
|
throw new Error('bareOsHrpcRequest: disk.os bridge unavailable')
|
|
}
|
|
const txt = await disk.os.execRpc('bare_os', 'replication_snapshot', [])
|
|
let parsed = null
|
|
try {
|
|
parsed = JSON.parse(txt)
|
|
} catch {
|
|
parsed = { error: 'invalid_json' }
|
|
}
|
|
hrpcAuditAppend({ ok: true, route: 'bare_os.replication_snapshot' })
|
|
return { ok: true, text: txt, json: parsed }
|
|
}
|
|
if (svc === 'bare_os' && m === 'pkg_index_get') {
|
|
if (!disk?.os || typeof disk.os.execRpc !== 'function') {
|
|
hrpcAuditAppend({ ok: false, reason: 'disk_os_unavailable' })
|
|
throw new Error('bareOsHrpcRequest: disk.os bridge unavailable')
|
|
}
|
|
const key = String(payload.key ?? payload.name ?? '').trim()
|
|
const txt = await disk.os.execRpc('bare_os', 'pkg_index_get', [key])
|
|
let parsed = null
|
|
try {
|
|
parsed = JSON.parse(txt)
|
|
} catch {
|
|
parsed = { error: 'invalid_json' }
|
|
}
|
|
hrpcAuditAppend({ ok: true, route: 'bare_os.pkg_index_get' })
|
|
return { ok: true, text: txt, json: parsed }
|
|
}
|
|
|
|
const emitUnlisted =
|
|
env.BARE_OS_HRPC_EMIT_UNLISTED === '1' ||
|
|
env.BARE_OS_HRPC_EMIT_UNLISTED === 'true'
|
|
if (emitUnlisted && typeof globalThis.process?.emit === 'function') {
|
|
try {
|
|
globalThis.process.emit('bare-os:hrpc-request', {
|
|
service: svc,
|
|
method: m,
|
|
payload,
|
|
ts: Date.now(),
|
|
sessionId
|
|
})
|
|
} catch {
|
|
/* ignore event bus failures */
|
|
}
|
|
}
|
|
hrpcAuditAppend({ ok: false, reason: 'unsupported_route' })
|
|
throw new Error(
|
|
`bareOsHrpcRequest: unsupported route "${svc}.${m}" (stock handler)`
|
|
)
|
|
}
|
|
stockBareOsHrpcRequest.__bareOsStockHrpcRequest = true
|
|
return stockBareOsHrpcRequest
|
|
}
|