@tetherto/mdk-core 0.0.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +572 -0
  3. package/index.js +519 -0
  4. package/lib/address-policy.js +124 -0
  5. package/lib/local-discovery.js +321 -0
  6. package/lib/services/actions.service.js +68 -0
  7. package/lib/services/alerts.service.js +207 -0
  8. package/lib/services/comments.service.js +118 -0
  9. package/lib/services/log-history.service.js +307 -0
  10. package/lib/services/logs.service.js +249 -0
  11. package/lib/services/pool-utils/constants.js +20 -0
  12. package/lib/services/pool-utils/time.js +49 -0
  13. package/lib/services/pool.service.js +397 -0
  14. package/lib/services/provisioning.service.js +420 -0
  15. package/lib/services/settings.service.js +26 -0
  16. package/lib/services/snaps.service.js +181 -0
  17. package/lib/services/stats.service.js +205 -0
  18. package/lib/stats/README.md +80 -0
  19. package/lib/stats/index.js +57 -0
  20. package/lib/stats/ops/alerts_aggr.js +40 -0
  21. package/lib/stats/ops/alerts_group_cnt.js +43 -0
  22. package/lib/stats/ops/arr_concat.js +27 -0
  23. package/lib/stats/ops/array_obj_calc.js +41 -0
  24. package/lib/stats/ops/avg.js +42 -0
  25. package/lib/stats/ops/cnt.js +19 -0
  26. package/lib/stats/ops/group.js +38 -0
  27. package/lib/stats/ops/group_avg.js +53 -0
  28. package/lib/stats/ops/group_cnt.js +38 -0
  29. package/lib/stats/ops/group_max.js +40 -0
  30. package/lib/stats/ops/group_multiple_stats.js +40 -0
  31. package/lib/stats/ops/group_sum.js +40 -0
  32. package/lib/stats/ops/index.js +35 -0
  33. package/lib/stats/ops/nested_obj_concat.js +54 -0
  34. package/lib/stats/ops/obj_concat.js +33 -0
  35. package/lib/stats/ops/sum.js +27 -0
  36. package/lib/stats/utils.js +99 -0
  37. package/lib/templates/alerts.js +128 -0
  38. package/lib/templates/stats.js +476 -0
  39. package/lib/things/constants.js +65 -0
  40. package/lib/things/container.js +84 -0
  41. package/lib/things/miner.js +190 -0
  42. package/lib/things/powermeter.js +30 -0
  43. package/lib/things/sensor.js +18 -0
  44. package/lib/things/thing.js +93 -0
  45. package/lib/utils.js +603 -0
  46. package/lib/worker-infra.js +221 -0
  47. package/package.json +64 -1
  48. package/services.js +246 -0
  49. package/utils/compose-yaml.js +56 -0
  50. package/utils/constants.js +30 -0
  51. package/utils/index.js +29 -0
  52. package/utils/initialize.js +148 -0
  53. package/utils/service-bootstrap.js +271 -0
  54. package/worker.js +9 -0
@@ -0,0 +1,321 @@
1
+ 'use strict'
2
+
3
+ // Same-device worker discovery by direct key handoff — an optional, faster
4
+ // alternative to DHT topic discovery (the default) for setups where every
5
+ // component runs on one machine.
6
+ //
7
+ // Each worker publishes its stable (seed-derived) RPC public key to a shared
8
+ // keys dir; the Kernel reads each key and connects BY KEY, running the normal
9
+ // identity → capability → Ready flow. This skips the DHT topic announce/lookup
10
+ // round-trip, so registration is immediate as soon as the key file appears.
11
+ //
12
+ // This is the `discovery: { mode: 'local' }` path. The dir is resolved the same
13
+ // way on both sides — getKernel and startWorker default it to keysDir(root) and
14
+ // share the same root default — so the two processes agree with no config.
15
+
16
+ const path = require('path')
17
+ const fs = require('fs')
18
+ const crypto = require('crypto')
19
+ const debug = require('debug')('mdk:local-discovery')
20
+
21
+ function keysDir (root) {
22
+ return path.join(root, '.worker-keys')
23
+ }
24
+
25
+ const WORKER_KEY_FILE = /^[A-Za-z0-9._-]+\.key$/
26
+
27
+ // Worker side: publish this worker's RPC public key to `dir`. The key is stable
28
+ // across restarts (derived from the persisted seedRpc), so re-publishing is a
29
+ // no-op for the Kernel (already-known peers are deduped by the listener).
30
+ // The Kernel dials whatever it finds here, so the dir and files get the same
31
+ // protections as the trusted-keys dir: a safe file name, a 0700 dir, a 0600
32
+ // file written atomically, and a 32-byte hex key.
33
+ function publishWorkerKey (dir, workerId, rpcKeyHex) {
34
+ const hex = _keyHex(rpcKeyHex)
35
+ const file = path.join(dir, `${_safeName(workerId)}.key`)
36
+ _ensurePrivateDir(dir)
37
+ _writeKeyFile(file, hex)
38
+ debug('published %s key %s…', workerId, hex.slice(0, 16))
39
+ }
40
+
41
+ // Kernel side: offer every published worker key to the discovery listener, then
42
+ // keep watching `dir` for new ones. The listener dedupes already-known peers and
43
+ // drops a key on connect failure (so a periodic rescan retries workers whose RPC
44
+ // server was not yet reachable, and re-discovers all workers after an Kernel
45
+ // restart). Returns stop() to tear the watcher + timer down.
46
+ // Only `<name>.key` files that pass the trusted-keys checks (regular file, not a
47
+ // symlink, owned by this user, not group/world-writable, 64-hex content) are
48
+ // offered; anything else in the dir is ignored.
49
+ function discoverWorkerKeys (kernel, dir, { rescanMs = 4000 } = {}) {
50
+ _ensurePrivateDir(dir)
51
+
52
+ const offer = (file) => {
53
+ if (typeof file !== 'string' || !WORKER_KEY_FILE.test(file)) return
54
+ const key = _readKeyFile(path.join(dir, file))
55
+ if (!key) return
56
+ Promise.resolve(kernel.dhtListener.discoverWorker(key))
57
+ .catch((e) => debug('discover %s failed: %s', file, e.message))
58
+ }
59
+
60
+ const scan = () => {
61
+ let files = []
62
+ try { files = fs.readdirSync(dir) } catch { return }
63
+ for (const f of files) offer(f)
64
+ }
65
+
66
+ scan()
67
+ let watcher = null
68
+ try {
69
+ watcher = fs.watch(dir, (_event, filename) => offer(filename))
70
+ watcher.on('error', (err) => {
71
+ debug('watch %s failed: %s', dir, err.message)
72
+ try { watcher.close() } catch {}
73
+ watcher = null
74
+ })
75
+ } catch (err) {
76
+ debug('watch %s failed: %s', dir, err.message)
77
+ }
78
+ const timer = setInterval(scan, rescanMs)
79
+ timer.unref()
80
+
81
+ return {
82
+ stop () {
83
+ if (watcher) {
84
+ try { watcher.close() } catch {}
85
+ watcher = null
86
+ }
87
+ clearInterval(timer)
88
+ }
89
+ }
90
+ }
91
+
92
+ // ─── Trusted caller keys (fail-closed RPC admission) ──────────────────────
93
+ //
94
+ // The Kernel HRPC listener and every Worker RPC server reject unknown public
95
+ // keys. Same-machine processes that cannot hand each other a key in memory
96
+ // meet in a trusted-keys dir instead: each publishes `<name>.pub` (hex of the
97
+ // DHT key it dials with) and the other side admits what it finds there.
98
+ //
99
+ // Kernel publishes kernel.pub (kernel.getClientPublicKey(), the key it dials
100
+ // workers with) and admits every other key as an HRPC caller.
101
+ // Gateway publishes gateway.pub (status.rpcClientKey); MCP/CLI clients
102
+ // publish their persistent caller key the same way.
103
+ // Worker allows every key in the dir (the Kernel, and the Gateway, whose
104
+ // client also calls workers directly).
105
+ //
106
+ // Whoever can write the dir is trusted: it is created 0700, and files that are
107
+ // group/world-writable or owned by another user are ignored.
108
+
109
+ const KEY_HEX = /^[0-9a-f]{64}$/
110
+ const KERNEL_KEY_NAME = 'kernel'
111
+
112
+ function trustedKeysDir (root) {
113
+ return path.join(root, '.trusted-keys')
114
+ }
115
+
116
+ function _keyHex (key) {
117
+ const hex = Buffer.isBuffer(key) ? key.toString('hex') : String(key || '').trim().toLowerCase()
118
+ if (!KEY_HEX.test(hex)) throw new Error('ERR_TRUSTED_KEY_INVALID: expected a 32-byte hex public key')
119
+ return hex
120
+ }
121
+
122
+ function _safeName (name) {
123
+ if (typeof name !== 'string' || !/^[A-Za-z0-9._-]+$/.test(name)) throw new Error(`ERR_TRUSTED_KEY_NAME_INVALID: ${name}`)
124
+ return name
125
+ }
126
+
127
+ function publishTrustedKey (dir, name, key) {
128
+ const hex = _keyHex(key)
129
+ const file = path.join(dir, `${_safeName(name)}.pub`)
130
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
131
+ _writeKeyFile(file, hex)
132
+ debug('published trusted key %s %s…', name, hex.slice(0, 16))
133
+ return hex
134
+ }
135
+
136
+ // Creates `dir` 0700 when missing; an existing one must be a real directory
137
+ // (not a symlink) owned by this user and not group/world-writable.
138
+ function _ensurePrivateDir (dir) {
139
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
140
+ const st = fs.lstatSync(dir)
141
+ if (!st.isDirectory()) throw new Error(`ERR_KEYS_DIR_UNSAFE: ${dir} is not a directory`)
142
+ if (process.platform !== 'win32') {
143
+ if (typeof process.getuid === 'function' && st.uid !== process.getuid()) {
144
+ throw new Error(`ERR_KEYS_DIR_UNSAFE: ${dir} is owned by another user`)
145
+ }
146
+ if (st.mode & 0o022) throw new Error(`ERR_KEYS_DIR_UNSAFE: ${dir} is group/world-writable`)
147
+ }
148
+ }
149
+
150
+ // Atomic 0600 write through an unpredictable temp name created exclusively.
151
+ function _writeKeyFile (file, hex) {
152
+ const tmp = `${file}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`
153
+ try {
154
+ fs.writeFileSync(tmp, hex, { encoding: 'utf8', mode: 0o600, flag: 'wx' })
155
+ fs.renameSync(tmp, file)
156
+ } catch (err) {
157
+ try { fs.unlinkSync(tmp) } catch {}
158
+ throw err
159
+ }
160
+ }
161
+
162
+ const O_NOFOLLOW = fs.constants.O_NOFOLLOW || 0
163
+
164
+ function _readKeyFile (file) {
165
+ let fd = null
166
+ try {
167
+ const lst = fs.lstatSync(file)
168
+ if (!lst.isFile()) return null
169
+ fd = fs.openSync(file, fs.constants.O_RDONLY | O_NOFOLLOW)
170
+ const st = fs.fstatSync(fd)
171
+ if (!st.isFile() || st.ino !== lst.ino || st.dev !== lst.dev) return null
172
+ if (process.platform !== 'win32') {
173
+ if (st.mode & 0o022) { debug('ignoring group/world-writable key file %s', file); return null }
174
+ if (typeof process.getuid === 'function' && st.uid !== process.getuid()) { debug('ignoring key file owned by another user %s', file); return null }
175
+ }
176
+ const buf = Buffer.alloc(128)
177
+ const n = fs.readSync(fd, buf, 0, buf.length, 0)
178
+ const hex = buf.subarray(0, n).toString('utf8').trim().toLowerCase()
179
+ return KEY_HEX.test(hex) ? hex : null
180
+ } catch {
181
+ return null
182
+ } finally {
183
+ if (fd !== null) { try { fs.closeSync(fd) } catch {} }
184
+ }
185
+ }
186
+
187
+ // Returns [{ name, key }] for every valid `<name>.pub` in dir.
188
+ function readTrustedKeys (dir) {
189
+ let files = []
190
+ try { files = fs.readdirSync(dir) } catch { return [] }
191
+ const out = []
192
+ for (const f of files) {
193
+ if (!f.endsWith('.pub')) continue
194
+ const key = _readKeyFile(path.join(dir, f))
195
+ if (key) out.push({ name: f.slice(0, -4), key })
196
+ }
197
+ return out
198
+ }
199
+
200
+ // Calls onKey(hex, name) once per new key now and whenever one appears.
201
+ function watchTrustedKeys (dir, onKey, { rescanMs = 2000 } = {}) {
202
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
203
+ const seen = new Set()
204
+ const scan = () => {
205
+ for (const { name, key } of readTrustedKeys(dir)) {
206
+ if (seen.has(key)) continue
207
+ seen.add(key)
208
+ try { onKey(key, name) } catch (err) { debug('trusted key %s rejected: %s', name, err.message) }
209
+ }
210
+ }
211
+ scan()
212
+ let watcher = null
213
+ try {
214
+ watcher = fs.watch(dir, () => scan())
215
+ watcher.on('error', () => { try { watcher.close() } catch {} watcher = null })
216
+ } catch {}
217
+ const timer = setInterval(scan, rescanMs)
218
+ timer.unref()
219
+ return {
220
+ stop () {
221
+ if (watcher) { try { watcher.close() } catch {} watcher = null }
222
+ clearInterval(timer)
223
+ }
224
+ }
225
+ }
226
+
227
+ // Kernel side: admit every published caller key (full access) except its own.
228
+ function admitTrustedKeys (kernel, dir, opts) {
229
+ const own = kernel.getClientPublicKey && kernel.getClientPublicKey()
230
+ const ownHex = own ? own.toString('hex') : null
231
+ return watchTrustedKeys(dir, (key, name) => {
232
+ if (key === ownHex) return
233
+ kernel.allowKey(key)
234
+ debug('kernel admitted trusted caller %s', name)
235
+ }, opts)
236
+ }
237
+
238
+ // Worker side (separate process): allow every published key. Call before
239
+ // runtime.start() — it throws when nothing is trusted yet.
240
+ function trustKeysFromDir (runtime, dir, opts) {
241
+ return watchTrustedKeys(dir, (key, name) => {
242
+ runtime.allowKey(key)
243
+ debug('worker %s trusts %s', runtime.workerId, name)
244
+ }, opts)
245
+ }
246
+
247
+ // Wait until the Kernel has published its client key to dir.
248
+ async function waitForKernelKey (dir, { timeoutMs = 30000, intervalMs = 250 } = {}) {
249
+ const deadline = Date.now() + timeoutMs
250
+ for (;;) {
251
+ const hit = readTrustedKeys(dir).find((k) => k.name === KERNEL_KEY_NAME)
252
+ if (hit) return hit.key
253
+ if (Date.now() >= deadline) {
254
+ throw new Error(`ERR_KERNEL_CLIENT_KEY_NOT_FOUND: no ${KERNEL_KEY_NAME}.pub in ${dir} — start the Kernel first (it publishes the key its workers must allow), or pass allowedKeys explicitly`)
255
+ }
256
+ await new Promise((resolve) => setTimeout(resolve, intervalMs))
257
+ }
258
+ }
259
+
260
+ // Worker side (same process as the Kernel): allow the Kernel's client key and
261
+ // every write-capable caller it admits, now and later. Call before start().
262
+ function trustKernel (runtime, kernel) {
263
+ const client = kernel.getClientPublicKey()
264
+ if (!client) throw new Error('ERR_KERNEL_CLIENT_KEY_UNAVAILABLE: start the Kernel (with HRPC) before its workers')
265
+ runtime.allowKey(client)
266
+ for (const key of kernel.listAllowedKeys ? kernel.listAllowedKeys('write') : []) runtime.allowKey(key)
267
+ const onAllowed = ({ key, roles }) => {
268
+ if ((roles || []).some((r) => r === 'write' || r === 'admin')) runtime.allowKey(key)
269
+ }
270
+ const onRevoked = ({ key }) => { try { runtime.revokeKey(key) } catch {} }
271
+ kernel.on('caller-allowed', onAllowed)
272
+ kernel.on('caller-revoked', onRevoked)
273
+ return {
274
+ stop () {
275
+ kernel.removeListener('caller-allowed', onAllowed)
276
+ kernel.removeListener('caller-revoked', onRevoked)
277
+ }
278
+ }
279
+ }
280
+
281
+ // A persistent caller identity (MCP server, CLI client): an ed25519 keypair
282
+ // in hyperdht's format derived from a 32-byte seed kept at <dir>/seed (0600).
283
+ function callerKeyPair (dir) {
284
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
285
+ const seedPath = path.join(dir, 'seed')
286
+ let seed = null
287
+ try {
288
+ const buf = fs.readFileSync(seedPath)
289
+ if (buf.length === 32) seed = buf
290
+ else throw new Error(`ERR_CALLER_SEED_MALFORMED: ${seedPath}`)
291
+ } catch (err) {
292
+ if (err.code !== 'ENOENT') throw err
293
+ }
294
+ if (!seed) {
295
+ seed = crypto.randomBytes(32)
296
+ fs.writeFileSync(seedPath, seed, { mode: 0o600, flag: 'wx' })
297
+ }
298
+ const priv = crypto.createPrivateKey({
299
+ key: Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), seed]),
300
+ format: 'der',
301
+ type: 'pkcs8'
302
+ })
303
+ const publicKey = crypto.createPublicKey(priv).export({ format: 'der', type: 'spki' }).subarray(-32)
304
+ return { publicKey, secretKey: Buffer.concat([seed, publicKey]) }
305
+ }
306
+
307
+ module.exports = {
308
+ keysDir,
309
+ publishWorkerKey,
310
+ discoverWorkerKeys,
311
+ KERNEL_KEY_NAME,
312
+ trustedKeysDir,
313
+ publishTrustedKey,
314
+ readTrustedKeys,
315
+ watchTrustedKeys,
316
+ admitTrustedKeys,
317
+ trustKeysFromDir,
318
+ waitForKernelKey,
319
+ trustKernel,
320
+ callerKeyPair
321
+ }
@@ -0,0 +1,68 @@
1
+ 'use strict'
2
+
3
+ const async = require('async')
4
+ const { applyFilters } = require('../utils')
5
+
6
+ /**
7
+ * Write-call approval surface for the Kernel action-approver flow
8
+ * (`write.calls.request`). Actions must be whitelisted with their required
9
+ * vote count; per-device validation is the injected
10
+ * validateWriteAction(device, action, params) — wire it to the vendor
11
+ * client's validateWriteAction. Non-whitelisted actions answer zero calls.
12
+ */
13
+ class ActionsService {
14
+ constructor ({ listDevices, validateWriteAction, maxParallelWriteValidations }) {
15
+ this.listDevices = listDevices || (() => [])
16
+ this.validateWriteAction = validateWriteAction || (async () => 1)
17
+ this.maxParallelWriteValidations = maxParallelWriteValidations || 5
18
+ this.allowedActions = new Map()
19
+ }
20
+
21
+ whitelistActions (actions) {
22
+ for (const [action, reqVotes] of actions) {
23
+ this.allowedActions.set(action, reqVotes)
24
+ }
25
+ }
26
+
27
+ delistActions (actions) {
28
+ for (const action of actions) {
29
+ this.allowedActions.delete(action)
30
+ }
31
+ }
32
+
33
+ async getWriteCalls (data) {
34
+ const { query, action, params = [], rackActionId = null } = data
35
+
36
+ const res = {
37
+ calls: [],
38
+ reqVotes: 1
39
+ }
40
+
41
+ if (!this.allowedActions.has(action)) {
42
+ return res
43
+ }
44
+ res.reqVotes = this.allowedActions.get(action)
45
+
46
+ if (rackActionId) {
47
+ res.calls.push({
48
+ id: rackActionId,
49
+ tags: []
50
+ })
51
+ return res
52
+ }
53
+
54
+ const devices = applyFilters(this.listDevices(), { query, limit: 100000 }, true)
55
+
56
+ await async.eachLimit(devices, this.maxParallelWriteValidations, async (dev) => {
57
+ await this.validateWriteAction(dev, action, params)
58
+ res.calls.push({
59
+ id: dev.id,
60
+ tags: dev.tags || []
61
+ })
62
+ })
63
+
64
+ return res
65
+ }
66
+ }
67
+
68
+ module.exports = ActionsService
@@ -0,0 +1,207 @@
1
+ 'use strict'
2
+
3
+ const debug = require('debug')('mdk:alerts')
4
+ const { v4: uuidv4 } = require('uuid')
5
+ const utilsStore = require('@tetherto/hp-svc-facs-store/utils')
6
+
7
+ const createAlert = ({
8
+ name,
9
+ code,
10
+ description,
11
+ severity,
12
+ createdAt,
13
+ uuid = uuidv4(),
14
+ message = undefined
15
+ }) => {
16
+ return {
17
+ name,
18
+ code,
19
+ description,
20
+ severity,
21
+ createdAt,
22
+ uuid,
23
+ message
24
+ }
25
+ }
26
+
27
+ const updateExistingAlerts = (alerts, alertsPrev) => {
28
+ if (!Array.isArray(alertsPrev)) return
29
+ alerts.forEach(alert => {
30
+ const alertExists = alertsPrev.find(val => val.name === alert.name && val.description === alert.description && val.message === alert.message)
31
+ alert.createdAt = alertExists?.createdAt ?? alert.createdAt
32
+ alert.uuid = alertExists?.uuid ?? alert.uuid
33
+ })
34
+ }
35
+
36
+ const getErrorsFromSnap = (snap) => {
37
+ if (!snap) return null
38
+ if (!snap.raw_errors) return null
39
+ return snap.raw_errors
40
+ }
41
+
42
+ /**
43
+ * Alert evaluation + persistence. `lib` is the alerts spec module
44
+ * ({ specs: { <specTag>: { <alertKey>: { valid, probe } } } }) passed in
45
+ * directly — no loadLib indirection. `listDevices()` must return
46
+ * [{ id, last }] for saveAlerts; processThingAlerts takes one device record
47
+ * ({ id, type, info, last }).
48
+ */
49
+ class AlertsService {
50
+ constructor ({ logs, lib, conf, specTags, listDevices }) {
51
+ if (!logs) throw new Error('ERR_ALERTS_LOGS_REQUIRED')
52
+ this.logs = logs
53
+ this.lib = lib || null
54
+ this.conf = conf || {}
55
+ this.specTags = specTags || []
56
+ this.listDevices = listDevices || (() => [])
57
+ }
58
+
59
+ async saveAlerts () {
60
+ const log = await this.logs.getBeeTimeLog('thing-alerts', 0, true)
61
+ for (const dev of this.listDevices()) {
62
+ if (dev.last?.alerts) {
63
+ try {
64
+ await this._storeThgAlertsToDb(dev.id, log, dev.last.alerts)
65
+ } catch (error) {
66
+ debug('ERR_STORE_ALERTS_TO_DB thg:%s %s', dev.id, error.message)
67
+ }
68
+ }
69
+ }
70
+ await this.logs.releaseBeeTimeLog(log)
71
+ }
72
+
73
+ async _storeThgAlertsToDb (thingId, log, alerts = []) {
74
+ if (!Array.isArray(alerts)) {
75
+ return
76
+ }
77
+ const groupedAlerts = alerts.reduce((grouped, alert) => {
78
+ const { createdAt, uuid } = alert
79
+ grouped[createdAt] ??= {}
80
+ grouped[createdAt][uuid] = { ...alert, thingId }
81
+ return grouped
82
+ }, {})
83
+
84
+ const timestamps = Object.keys(groupedAlerts)
85
+
86
+ for (const timestamp of timestamps) {
87
+ const existingLogEntry = await log.get(utilsStore.convIntToBin(timestamp))
88
+ const existingAlerts = existingLogEntry?.value ? JSON.parse(existingLogEntry.value.toString()) : {}
89
+
90
+ const updatedAlerts = { ...existingAlerts, ...groupedAlerts[timestamp] }
91
+ await log.put(
92
+ utilsStore.convIntToBin(timestamp),
93
+ Buffer.from(JSON.stringify(updatedAlerts))
94
+ )
95
+ }
96
+ }
97
+
98
+ processThingAlerts (thg) {
99
+ if (!this.lib) {
100
+ return null
101
+ }
102
+
103
+ let alertsFromConfig = this.conf.thing?.alerts
104
+ const baseType = thg.type.split('-')[0]
105
+ const thingConf = this.conf.thing?.[baseType]
106
+
107
+ if (!alertsFromConfig) {
108
+ return null
109
+ }
110
+ alertsFromConfig = alertsFromConfig[thg.type]
111
+
112
+ if (!alertsFromConfig) {
113
+ return null
114
+ }
115
+
116
+ const specs = this.lib.specs
117
+ const snap = thg.last?.snap
118
+
119
+ if (!snap) {
120
+ const alert = createAlert({
121
+ name: 'error_snap',
122
+ code: 'error_snap',
123
+ description: thg.last?.err || 'No snap',
124
+ severity: 'medium',
125
+ createdAt: Date.now()
126
+ })
127
+ updateExistingAlerts([alert], thg.last?.alerts)
128
+ return [alert]
129
+ }
130
+
131
+ const acc = []
132
+
133
+ const alertsContext = {
134
+ conf: alertsFromConfig,
135
+ info: thg.info,
136
+ thingConf,
137
+ id: thg.id
138
+ }
139
+
140
+ const errorsFromSnap = getErrorsFromSnap(snap)
141
+
142
+ if (errorsFromSnap) {
143
+ acc.push(...errorsFromSnap.map(error => {
144
+ const alertConfig = alertsFromConfig[error.name]
145
+
146
+ return createAlert({
147
+ name: error.name,
148
+ code: error.code,
149
+ description: alertConfig?.description || 'unknown_error',
150
+ severity: alertConfig?.severity || 'high',
151
+ createdAt: error.timestamp || Date.now(),
152
+ uuid: uuidv4(),
153
+ message: error.message || undefined
154
+ })
155
+ }))
156
+ }
157
+
158
+ this.specTags.forEach(stype => {
159
+ if (!specs[stype]) {
160
+ return
161
+ }
162
+
163
+ const aks = Object.keys(specs[stype])
164
+
165
+ aks.forEach(ak => {
166
+ const check = specs[stype][ak]
167
+ let at = null
168
+
169
+ try {
170
+ if (!check.valid(alertsContext, snap)) {
171
+ return
172
+ }
173
+
174
+ at = check.probe(alertsContext, snap)
175
+ } catch (e) {
176
+ const alert = createAlert({
177
+ name: ak,
178
+ code: ak,
179
+ description: e.message,
180
+ severity: 'medium',
181
+ createdAt: Date.now()
182
+ })
183
+
184
+ acc.push(alert)
185
+ }
186
+
187
+ if (at) {
188
+ const alertConf = alertsFromConfig[ak]
189
+ const alert = createAlert({
190
+ name: alertConf?.name || ak,
191
+ code: alertConf?.code || ak,
192
+ description: alertConf?.description || ak,
193
+ severity: alertConf?.severity || 'medium',
194
+ createdAt: Date.now()
195
+ })
196
+
197
+ acc.push(alert)
198
+ }
199
+ })
200
+ })
201
+
202
+ updateExistingAlerts(acc, thg.last?.alerts)
203
+ return acc.length ? acc : null
204
+ }
205
+ }
206
+
207
+ module.exports = AlertsService