378 lines
12 KiB
JavaScript
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 }
|
|
}
|
|
}
|