Files
peardata/shared/schema.js
T
Raven Scott 4b5b78892f
CI / test (push) Successful in 2m45s
Release rolling / release (push) Successful in 11m36s
FIX: QVAC Updates
2026-07-30 15:23:43 -04:00

378 lines
12 KiB
JavaScript

/**
* Lightweight arg validation for PearData RPCs.
*/
export const SCHEMA_VERSION = 1
/**
* @typedef {{ ok: true, args: object } | { ok: false, error: string }} ValidateResult
*/
/**
* @param {string} method
* @param {object} args
* @returns {ValidateResult}
*/
export function validateMethodArgs(method, args = {}) {
if (args == null || typeof args !== 'object' || Array.isArray(args)) {
return { ok: false, error: 'Arguments must be a plain object' }
}
switch (method) {
case 'handshake':
case 'ping':
case 'getServerInfo':
case 'getAuthStatus':
case 'getNodeInfo':
case 'getHealth':
case 'listContexts':
case 'listCharts':
case 'listAnomalies':
case 'listAlerts':
case 'listJobs':
case 'listPeers':
case 'getWeights':
case 'getHostSnapshot':
case 'investigateHost':
case 'getDbInfo':
case 'listPeerLinks':
case 'listChildPeers':
case 'getFleetHealth':
case 'getStorageInfo':
case 'getRetentionConfig':
return { ok: true, args }
case 'hotMetrics': {
const limit = args.limit == null ? 15 : Number(args.limit)
if (!Number.isFinite(limit) || limit < 1 || limit > 50) {
return { ok: false, error: 'limit must be 1..50' }
}
const window = args.window == null ? 120 : Number(args.window)
if (!Number.isFinite(window) || window < 20 || window > 600) {
return { ok: false, error: 'window must be 20..600' }
}
return {
ok: true,
args: {
...args,
limit,
window,
family: args.family != null ? String(args.family) : '',
},
}
}
case 'relatedCharts': {
const chart = String(args.chart || args.id || '').trim()
if (!chart) return { ok: false, error: 'chart or id is required' }
const limit = args.limit == null ? 12 : Number(args.limit)
if (!Number.isFinite(limit) || limit < 1 || limit > 40) {
return { ok: false, error: 'limit must be 1..40' }
}
return { ok: true, args: { ...args, chart, id: chart, limit } }
}
case 'compareChartWindows': {
const chart = String(args.chart || args.id || '').trim()
if (!chart) return { ok: false, error: 'chart or id is required' }
const points = args.points == null ? 60 : Number(args.points)
if (!Number.isFinite(points) || points < 1 || points > 300) {
return { ok: false, error: 'points must be 1..300' }
}
return { ok: true, args: { ...args, chart, id: chart, points } }
}
case 'summarizeCharts': {
let charts = args.charts
if (typeof charts === 'string') {
charts = charts.split(/[\s,]+/).filter(Boolean)
}
if (!Array.isArray(charts) || !charts.length) {
return { ok: false, error: 'charts array required' }
}
charts = charts.map(String).slice(0, 12)
const points = args.points == null ? 60 : Number(args.points)
if (!Number.isFinite(points) || points < 1 || points > 120) {
return { ok: false, error: 'points must be 1..120' }
}
return {
ok: true,
args: {
...args,
charts,
points,
after: args.after != null ? Number(args.after) : -120,
},
}
}
case 'searchCharts': {
const limit = args.limit == null ? 30 : Number(args.limit)
if (!Number.isFinite(limit) || limit < 1 || limit > 100) {
return { ok: false, error: 'limit must be 1..100' }
}
return {
ok: true,
args: {
...args,
q: args.q != null ? String(args.q) : '',
limit,
},
}
}
case 'summarizeChart': {
const chart = String(args.chart || args.id || '').trim()
if (!chart) return { ok: false, error: 'chart or id is required' }
const points = args.points == null ? 90 : Number(args.points)
if (!Number.isFinite(points) || points < 1 || points > 600) {
return { ok: false, error: 'points must be 1..600' }
}
return {
ok: true,
args: {
...args,
chart,
id: chart,
points,
after: args.after != null ? Number(args.after) : -Math.min(points, 300),
group: String(args.group || 'average'),
},
}
}
case 'queryLogs': {
const source = String(args.source || 'journal').toLowerCase()
if (!['anomaly', 'audit', 'journal'].includes(source)) {
return { ok: false, error: 'source must be anomaly|audit|journal' }
}
const limit = args.limit == null ? 200 : Number(args.limit)
if (!Number.isFinite(limit) || limit < 1 || limit > 2000) {
return { ok: false, error: 'limit must be 1..2000' }
}
return {
ok: true,
args: {
...args,
source,
limit,
q: args.q != null ? String(args.q) : '',
priority: args.priority != null ? String(args.priority) : '',
unit: args.unit != null ? String(args.unit) : '',
cursor: args.cursor != null ? Number(args.cursor) : 0,
},
}
}
case 'listProcesses': {
const sort = String(args.sort || 'cpu').toLowerCase()
const allowedSort = [
'cpu',
'rss',
'pid',
'name',
'io',
'threads',
'age',
'fds',
'state',
]
if (!allowedSort.includes(sort)) {
return { ok: false, error: `sort must be ${allowedSort.join('|')}` }
}
const order = String(args.order || 'desc').toLowerCase() === 'asc' ? 'asc' : 'desc'
const filter = String(args.filter || 'all').toLowerCase()
const allowedFilter = [
'all',
'running',
'sleeping',
'zombie',
'stopped',
'highcpu',
'highmem',
'hasio',
'kernel',
'user',
]
if (!allowedFilter.includes(filter)) {
return { ok: false, error: `filter must be ${allowedFilter.join('|')}` }
}
const limit = args.limit == null ? 250 : Number(args.limit)
if (!Number.isFinite(limit) || limit < 1 || limit > 2000) {
return { ok: false, error: 'limit must be 1..2000' }
}
const offset = args.offset == null ? 0 : Number(args.offset)
if (!Number.isFinite(offset) || offset < 0) {
return { ok: false, error: 'offset must be ≥ 0' }
}
const pid =
args.pid == null || args.pid === ''
? null
: Number(args.pid)
if (pid != null && (!Number.isFinite(pid) || pid < 1)) {
return { ok: false, error: 'pid must be a positive number' }
}
return {
ok: true,
args: {
...args,
sort,
order,
filter,
limit: Math.floor(limit),
offset: Math.floor(offset),
q: args.q != null ? String(args.q) : '',
pid,
},
}
}
case 'setDisplayName': {
const name = String(args.name ?? '').trim()
if (!name) return { ok: false, error: 'name is required' }
if (name.length > 40) return { ok: false, error: 'name must be ≤ 40 characters' }
return { ok: true, args: { ...args, name } }
}
case 'getContext':
case 'getChart':
case 'getAlert': {
const id = String(args.id || args.chart || args.context || '').trim()
if (!id) return { ok: false, error: 'id is required' }
return { ok: true, args: { ...args, id } }
}
case 'queryData': {
const chart = String(args.chart || args.context || '').trim()
if (!chart) return { ok: false, error: 'chart or context is required' }
const points = args.points == null ? 60 : Number(args.points)
if (!Number.isFinite(points) || points < 1 || points > 10_000) {
return { ok: false, error: 'points must be 1..10000' }
}
return {
ok: true,
args: {
...args,
chart,
points,
after: args.after != null ? Number(args.after) : -points,
before: args.before != null ? Number(args.before) : 0,
group: String(args.group || 'average'),
format: String(args.format || 'json'),
},
}
}
case 'subscribeMetrics': {
const charts = Array.isArray(args.charts)
? args.charts.map(String)
: args.chart
? [String(args.chart)]
: ['*']
const intervalMs = Math.max(500, Number(args.intervalMs) || 1000)
return { ok: true, args: { ...args, charts, intervalMs } }
}
case 'unsubscribeMetrics':
case 'unsubscribeAnomalies':
return { ok: true, args }
case 'subscribeAnomalies':
return { ok: true, args }
case 'setAlertConfig': {
const id = String(args.id || '').trim()
if (!id) return { ok: false, error: 'id is required' }
return { ok: true, args: { ...args, id } }
}
case 'ackAlert':
case 'silenceAlert': {
const id = String(args.id || '').trim()
if (!id) return { ok: false, error: 'id is required' }
return { ok: true, args: { ...args, id } }
}
case 'runJob': {
const name = String(args.name || args.job || '').trim()
if (!name) return { ok: false, error: 'name is required' }
return { ok: true, args: { ...args, name } }
}
case 'cancelJob': {
const id = String(args.id || '').trim()
if (!id) return { ok: false, error: 'id is required' }
return { ok: true, args: { ...args, id } }
}
case 'mintInvite': {
const role = String(args.role || 'operator').toLowerCase()
if (!['viewer', 'operator', 'admin'].includes(role)) {
return { ok: false, error: 'role must be viewer|operator|admin' }
}
return { ok: true, args: { ...args, role } }
}
case 'revokePeer': {
const peerId = String(args.peerId || '').toLowerCase()
if (!/^[0-9a-f]{64}$/.test(peerId)) {
return { ok: false, error: 'peerId must be 64 hex characters' }
}
return { ok: true, args: { ...args, peerId } }
}
case 'getAllMetrics': {
const format = String(args.format || 'json').toLowerCase()
if (!['json', 'prometheus', 'shell'].includes(format)) {
return { ok: false, error: 'format must be json|prometheus|shell' }
}
return { ok: true, args: { ...args, format } }
}
case 'exportSnapshot':
return { ok: true, args }
case 'setRetentionConfig': {
const patch = args && typeof args === 'object' ? { ...args } : {}
delete patch.success
return { ok: true, args: patch }
}
case 'pruneHistory': {
return {
ok: true,
args: {
dryRun: Boolean(args?.dryRun),
beforeMs: args?.beforeMs != null ? Number(args.beforeMs) : undefined,
force: Boolean(args?.force),
// undefined = default on; false disables clearUnlinked+compact
gc: args?.gc === false ? false : args?.gc === true ? true : undefined,
},
}
}
case 'linkPeer': {
const remotePublicKey = String(args.remotePublicKey || args.peerId || '').toLowerCase()
if (!/^[0-9a-f]{64}$/.test(remotePublicKey)) {
return { ok: false, error: 'remotePublicKey must be 64 hex characters' }
}
const syncMode = String(args.syncMode || 'both')
if (!['push', 'pull', 'both'].includes(syncMode)) {
return { ok: false, error: 'syncMode must be push|pull|both' }
}
return { ok: true, args: { ...args, remotePublicKey, syncMode } }
}
case 'unlinkPeer': {
const remotePublicKey = String(args.remotePublicKey || args.peerId || '').toLowerCase()
if (!/^[0-9a-f]{64}$/.test(remotePublicKey)) {
return { ok: false, error: 'remotePublicKey must be 64 hex characters' }
}
return { ok: true, args: { ...args, remotePublicKey } }
}
default:
return { ok: true, args }
}
}