@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,148 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const { MDK_ROOT, LIB_TYPES } = require('./constants')
6
+
7
+ // The gateway worker is package code: resolved by name, never by its
8
+ // position in a checkout.
9
+ const gatewayPackageDir = () => path.dirname(require.resolve('@tetherto/mdk-gateway/package.json'))
10
+
11
+ const thingConfigSources = (localPackages) => [
12
+ {
13
+ destDir: 'types/AvalonMinerManagerA1346/config',
14
+ srcDirs: [path.join(localPackages, LIB_TYPES.AVALON, 'config')]
15
+ },
16
+ {
17
+ destDir: 'types/BitdeerManagerD40M56/config',
18
+ srcDirs: [path.join(localPackages, LIB_TYPES.BITDEER, 'config')]
19
+ },
20
+ {
21
+ destDir: 'types/AnstspaceManagerHK3/config',
22
+ srcDirs: [path.join(localPackages, LIB_TYPES.ANTSPACE, 'config')]
23
+ },
24
+ {
25
+ destDir: 'types/B23PowerMeterManager/config',
26
+ srcDirs: [path.join(localPackages, LIB_TYPES.ABB, 'config')]
27
+ },
28
+ {
29
+ destDir: 'types/TempSenecaSensorManager/config',
30
+ srcDirs: [path.join(localPackages, LIB_TYPES.SENECA, 'config')]
31
+ }
32
+ ]
33
+
34
+ // Points the tmp tree's worker stub at the installed @tetherto/mdk-gateway
35
+ // worker, relative to <root>/<MDK_ROOT>/workers (in the monorepo the
36
+ // workspace link resolves to backend/core/gateway).
37
+ const httpNodeWrkStub = (rootStoreDir) => {
38
+ const target = path.join(gatewayPackageDir(), 'workers', 'http.node.wrk')
39
+ let rel = path.relative(path.join(rootStoreDir, 'workers'), target).split(path.sep).join('/')
40
+ if (!rel.startsWith('.')) rel = `./${rel}`
41
+ return `module.exports = require(${JSON.stringify(rel).replace(/"/g, "'")})\n`
42
+ }
43
+
44
+ const TEMPLATE = {
45
+ dirs: [
46
+ 'config/facs',
47
+ 'db',
48
+ 'types/AvalonMinerManagerA1346/config',
49
+ 'types/BitdeerManagerD40M56/config',
50
+ 'types/AnstspaceManagerHK3/config',
51
+ 'types/B23PowerMeterManager/config',
52
+ 'types/TempSenecaSensorManager/config',
53
+ 'workers'
54
+ ],
55
+ files: {
56
+ 'workers/http.node.wrk.js': httpNodeWrkStub
57
+ }
58
+ }
59
+
60
+ const resolveSource = (dir, filename) => {
61
+ const plain = path.join(dir, filename)
62
+ const example = path.join(dir, `${filename}.example`)
63
+ if (fs.existsSync(plain)) return plain
64
+ if (fs.existsSync(example)) return example
65
+ return null
66
+ }
67
+
68
+ const ensureThingConfigs = (rootStoreDir, localPackages) => {
69
+ for (const { destDir, srcDirs } of thingConfigSources(localPackages)) {
70
+ const configDir = path.join(rootStoreDir, destDir)
71
+ fs.mkdirSync(configDir, { recursive: true })
72
+ const dest = path.join(configDir, 'base.thing.json')
73
+ if (!fs.existsSync(dest)) {
74
+ const src = (srcDirs || []).reduce((found, dir) => {
75
+ if (found) return found
76
+ return resolveSource(dir, 'base.thing.json')
77
+ }, null)
78
+ if (src) fs.copyFileSync(src, dest)
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Seeds the `<root>/tmp` config tree from the MDK monorepo's example configs.
85
+ *
86
+ * opts.sources MDK monorepo checkout to read configs from (the dir holding
87
+ * `backend/core/kernel`, `backend/core/gateway`, ...). Required:
88
+ * it is never guessed from this package's location.
89
+ * opts.root where the `tmp` tree is created (default: opts.sources).
90
+ */
91
+ module.exports = (opts = {}) => {
92
+ if (typeof opts.sources !== 'string' || !opts.sources.trim()) {
93
+ throw new Error(
94
+ 'ERR_MDK_INIT_SOURCES_REQUIRED: initialize() needs opts.sources — the MDK monorepo checkout to seed config from'
95
+ )
96
+ }
97
+ const sources = path.resolve(opts.sources)
98
+ const localPackages = path.join(sources, 'backend')
99
+ const orkConfDir = path.join(localPackages, LIB_TYPES.Kernel, 'config')
100
+ const gatewayConfigsDir = path.join(localPackages, LIB_TYPES.GATEWAY, 'config', 'facs')
101
+ if (!fs.existsSync(orkConfDir) || !fs.existsSync(gatewayConfigsDir)) {
102
+ throw new Error(
103
+ `ERR_MDK_INIT_SOURCES_NOT_FOUND: no MDK monorepo configs under ${sources} — ` +
104
+ 'initialize() seeds from a source checkout; pass opts.sources pointing at one'
105
+ )
106
+ }
107
+ const rootStoreDir = path.join(path.resolve(opts.root || sources), MDK_ROOT)
108
+
109
+ for (const dir of TEMPLATE.dirs) {
110
+ fs.mkdirSync(path.join(rootStoreDir, dir), { recursive: true })
111
+ }
112
+
113
+ const commonDest = path.join(rootStoreDir, 'config', 'common.json')
114
+ if (!fs.existsSync(commonDest)) {
115
+ const src = resolveSource(path.join(localPackages, LIB_TYPES.GATEWAY, 'config'), 'common.json')
116
+ if (src) fs.copyFileSync(src, commonDest)
117
+ }
118
+
119
+ for (const entry of fs.readdirSync(orkConfDir)) {
120
+ if (!entry.endsWith('.example')) continue
121
+ const filename = entry.slice(0, -'.example'.length)
122
+ const dest = path.join(rootStoreDir, 'config', filename)
123
+ if (!fs.existsSync(dest)) {
124
+ fs.copyFileSync(path.join(orkConfDir, entry), dest)
125
+ }
126
+ }
127
+
128
+ for (const entry of fs.readdirSync(gatewayConfigsDir)) {
129
+ if (!entry.endsWith('.example')) continue
130
+ const filename = entry.slice(0, -'.example'.length)
131
+ const dest = path.join(rootStoreDir, 'config', 'facs', filename)
132
+ if (!fs.existsSync(dest)) {
133
+ fs.copyFileSync(path.join(gatewayConfigsDir, entry), dest)
134
+ }
135
+ }
136
+
137
+ for (const [relPath, content] of Object.entries(TEMPLATE.files)) {
138
+ const filePath = path.join(rootStoreDir, relPath)
139
+ if (!fs.existsSync(filePath)) {
140
+ fs.writeFileSync(filePath, typeof content === 'function' ? content(rootStoreDir) : content, 'utf8')
141
+ continue
142
+ }
143
+ }
144
+
145
+ if (fs.existsSync(rootStoreDir)) {
146
+ ensureThingConfigs(rootStoreDir, localPackages)
147
+ }
148
+ }
@@ -0,0 +1,271 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const crypto = require('crypto')
6
+ const { spawn } = require('child_process')
7
+
8
+ // Code is never located by walking up the directory tree: MDK packages are
9
+ // resolved by name (node_modules), and the one thing that still needs the
10
+ // source checkout — seeding config from its example files — takes it from
11
+ // MDK_REPO_ROOT, which startServices writes into the generated PM2/compose
12
+ // config.
13
+ function resolveRepoRoot (env = process.env) {
14
+ const root = env.MDK_REPO_ROOT
15
+ if (typeof root !== 'string' || !root.trim()) {
16
+ throw new Error('ERR_MDK_REPO_ROOT_REQUIRED: set MDK_REPO_ROOT to the MDK source checkout (startServices({ repoRoot }) writes it into the generated PM2/compose config)')
17
+ }
18
+ return path.resolve(root)
19
+ }
20
+
21
+ function gatewayPackageDir () {
22
+ return path.dirname(require.resolve('@tetherto/mdk-gateway/package.json'))
23
+ }
24
+
25
+ function ensureConfigFromExamples (dir) {
26
+ if (!fs.existsSync(dir)) return
27
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
28
+ const full = path.join(dir, entry.name)
29
+ if (entry.isDirectory()) {
30
+ ensureConfigFromExamples(full)
31
+ continue
32
+ }
33
+ if (!entry.name.endsWith('.example')) continue
34
+ const dest = path.join(dir, entry.name.slice(0, -'.example'.length))
35
+ if (!fs.existsSync(dest)) fs.copyFileSync(full, dest)
36
+ }
37
+ }
38
+
39
+ // Every worker family boots through its package's runtime plugin boot,
40
+ // resolved by package name.
41
+ // TYPE (env) maps onto the boot's model id; workerId is
42
+ // `<prefix>-<model>-<rack>` so persistent stores and RPC seeds are stable
43
+ // per rack. Pool workers take no model — one logical pool per rack.
44
+ const WORKER_BOOTS = {
45
+ 'miner-avalon': {
46
+ pkg: '@tetherto/mdk-worker-avalon',
47
+ factory: 'startAvalonWorker',
48
+ prefix: 'avalon',
49
+ models: { A1346: 'a1346' }
50
+ },
51
+ 'container-antspace': {
52
+ pkg: '@tetherto/mdk-worker-antspace',
53
+ factory: 'startAntspaceWorker',
54
+ prefix: 'antspace',
55
+ models: { HK3: 'hk3', IMMERSION: 'immersion' }
56
+ },
57
+ 'container-bitdeer': {
58
+ pkg: '@tetherto/mdk-worker-bitdeer',
59
+ factory: 'startBitdeerWorker',
60
+ prefix: 'bitdeer',
61
+ models: { D40_A1346: 'a1346', D40_M30: 'm30', D40_M56: 'm56', D40_S19XP: 's19xp' }
62
+ },
63
+ 'powermeter-abb': {
64
+ pkg: '@tetherto/mdk-worker-abb',
65
+ factory: 'startAbbWorker',
66
+ prefix: 'abb',
67
+ models: { B23: 'b23', B24: 'b24', M1M20: 'm1m20', M4M20: 'm4m20', REU615: 'reu615' }
68
+ },
69
+ 'powermeter-satec': {
70
+ pkg: '@tetherto/mdk-worker-satec',
71
+ factory: 'startSatecWorker',
72
+ prefix: 'satec',
73
+ models: { PM180: 'pm180' }
74
+ },
75
+ 'powermeter-schneider': {
76
+ pkg: '@tetherto/mdk-worker-schneider',
77
+ factory: 'startSchneiderWorker',
78
+ prefix: 'schneider',
79
+ models: { P3U30: 'p3u30', PM5340: 'pm5340' }
80
+ },
81
+ 'sensor-seneca': {
82
+ pkg: '@tetherto/mdk-worker-seneca',
83
+ factory: 'startSenecaWorker',
84
+ prefix: 'seneca',
85
+ models: { 'Z-4RTD-2': 'z-4rtd-2' },
86
+ noModelOpt: true
87
+ },
88
+ 'minerpool-ocean': {
89
+ pkg: '@tetherto/mdk-worker-ocean',
90
+ factory: 'startOceanPoolWorker',
91
+ prefix: 'ocean',
92
+ pool: true
93
+ },
94
+ 'minerpool-f2pool': {
95
+ pkg: '@tetherto/mdk-worker-f2pool',
96
+ factory: 'startF2poolWorker',
97
+ prefix: 'f2pool',
98
+ pool: true
99
+ }
100
+ }
101
+
102
+ // `load` is the module loader (require by default; tests inject a fake).
103
+ function resolveWorkerBoot (worker, load = require) {
104
+ const spec = Object.hasOwn(WORKER_BOOTS, worker) ? WORKER_BOOTS[worker] : null
105
+ if (!spec) {
106
+ throw new Error(`ERR_MDK_WORKER_UNKNOWN: no runtime boot for "${worker}"`)
107
+ }
108
+ const mod = load(spec.pkg)
109
+ const factory = mod[spec.factory]
110
+ if (!factory) {
111
+ throw new Error(`ERR_MDK_WORKER_EXPORT: ${spec.factory} not found in ${worker}`)
112
+ }
113
+ return { spec, factory }
114
+ }
115
+
116
+ function requireMdk () {
117
+ return require('@tetherto/mdk-core')
118
+ }
119
+
120
+ // Same topic-file rendezvous as getKernel's standalone DHT mode (a worker may
121
+ // boot before the kernel and writes the topic file a later getKernel() picks
122
+ // up).
123
+ function resolveKernelTopic () {
124
+ const { DEFAULT_TOPIC_FILE } = requireMdk()
125
+ if (fs.existsSync(DEFAULT_TOPIC_FILE)) {
126
+ return fs.readFileSync(DEFAULT_TOPIC_FILE, 'utf8').trim()
127
+ }
128
+ const kernelTopic = crypto.randomBytes(32).toString('hex')
129
+ fs.mkdirSync(path.dirname(DEFAULT_TOPIC_FILE), { recursive: true })
130
+ fs.writeFileSync(DEFAULT_TOPIC_FILE, kernelTopic, 'utf8')
131
+ return kernelTopic
132
+ }
133
+
134
+ // Worker RPC is fail-closed. A standalone worker admits the keys published to
135
+ // the default trusted-keys dir (the Kernel's client key, a Gateway's) plus any
136
+ // MDK_WORKER_ALLOWED_KEYS (comma-separated hex); MDK_WORKER_ALLOW_ALL=1 is the
137
+ // explicit development opt-out.
138
+ function resolveWorkerTrust (env = process.env) {
139
+ const { DEFAULT_TRUSTED_KEYS_DIR } = requireMdk()
140
+ const { readTrustedKeys } = require('@tetherto/mdk-core/lib/local-discovery')
141
+ const allowAll = env.MDK_WORKER_ALLOW_ALL === '1' || env.MDK_WORKER_ALLOW_ALL === 'true'
142
+ const allowedKeys = [
143
+ ...(env.MDK_WORKER_ALLOWED_KEYS || '').split(',').map((k) => k.trim()).filter(Boolean),
144
+ ...readTrustedKeys(DEFAULT_TRUSTED_KEYS_DIR).map((k) => k.key)
145
+ ]
146
+ if (!allowAll && allowedKeys.length === 0) {
147
+ throw new Error(`ERR_MDK_WORKER_ALLOWED_KEYS_REQUIRED: no trusted keys in ${DEFAULT_TRUSTED_KEYS_DIR} — start the Kernel first (it publishes kernel.pub), set MDK_WORKER_ALLOWED_KEYS, or MDK_WORKER_ALLOW_ALL=1 for local development`)
148
+ }
149
+ return { allowedKeys: [...new Set(allowedKeys)], allowAll, dir: DEFAULT_TRUSTED_KEYS_DIR }
150
+ }
151
+
152
+ // Keep following the trusted-keys dir after boot (a Gateway may publish later).
153
+ function _followTrust (handle, dir) {
154
+ const { trustKeysFromDir } = require('@tetherto/mdk-core/lib/local-discovery')
155
+ if (handle && handle.runtime) trustKeysFromDir(handle.runtime, dir)
156
+ return handle
157
+ }
158
+
159
+ async function runWorker (repoRoot) {
160
+ if (typeof repoRoot !== 'string' || !repoRoot) {
161
+ throw new Error('ERR_MDK_REPO_ROOT_REQUIRED: runWorker needs the MDK source checkout (MDK_REPO_ROOT)')
162
+ }
163
+ const { initialize } = requireMdk()
164
+ const worker = process.env.WORKER
165
+ const type = process.env.TYPE
166
+ const rack = process.env.RACK
167
+
168
+ if (!worker || !rack) {
169
+ throw new Error('ERR_MDK_WORKER_ENV: WORKER and RACK are required')
170
+ }
171
+
172
+ initialize({ sources: repoRoot })
173
+
174
+ const root = process.env.MDK_WORKER_ROOT ||
175
+ path.join(process.cwd(), 'data', rack)
176
+ const kernelTopic = resolveKernelTopic()
177
+ const { allowedKeys, allowAll, dir: trustDir } = resolveWorkerTrust()
178
+
179
+ const { spec, factory } = resolveWorkerBoot(worker)
180
+
181
+ if (spec.pool) {
182
+ const workerId = `${spec.prefix}-${rack}`
183
+ const storeDir = path.join(root, 'workers', workerId, 'store')
184
+ fs.mkdirSync(storeDir, { recursive: true })
185
+ return _followTrust(await factory({ workerId, rack, storeDir, root, kernelTopic, allowedKeys, allowAll }), trustDir)
186
+ }
187
+
188
+ if (!type) {
189
+ throw new Error('ERR_MDK_WORKER_ENV: TYPE is required')
190
+ }
191
+ const model = spec.models[type]
192
+ if (!model) {
193
+ throw new Error(`ERR_MDK_WORKER_UNKNOWN: no plugin model for ${worker}:${type}`)
194
+ }
195
+
196
+ const workerId = `${spec.prefix}-${model}-${rack}`
197
+ const storeDir = path.join(root, 'workers', workerId, 'store')
198
+ fs.mkdirSync(storeDir, { recursive: true })
199
+
200
+ const opts = { workerId, storeDir, kernelTopic, allowedKeys, allowAll }
201
+ if (!spec.noModelOpt) opts.model = model
202
+ return _followTrust(await factory(opts), trustDir)
203
+ }
204
+
205
+ function runGateway () {
206
+ const gatewayRoot = gatewayPackageDir()
207
+ ensureConfigFromExamples(path.join(gatewayRoot, 'config'))
208
+
209
+ const env = process.env.MDK_ENV || 'development'
210
+ const port = process.env.PORT || '3000'
211
+ const workerPath = path.join(gatewayRoot, 'worker.js')
212
+
213
+ // Spawn so bfx-svc-boot-js uses gateway as serviceRoot (require.main.filename).
214
+ const child = spawn(process.execPath, [
215
+ workerPath,
216
+ '--wtype', 'wrk-node-http',
217
+ '--env', env,
218
+ '--port', String(port)
219
+ ], {
220
+ cwd: gatewayRoot,
221
+ stdio: 'inherit',
222
+ env: process.env
223
+ })
224
+
225
+ return new Promise((resolve, reject) => {
226
+ child.on('error', reject)
227
+ child.on('exit', (code, signal) => {
228
+ if (code === 0) return resolve()
229
+ reject(new Error(
230
+ `gateway exited with code ${code}${signal ? ` (${signal})` : ''}`
231
+ ))
232
+ })
233
+ })
234
+ }
235
+
236
+ async function main () {
237
+ const service = process.env.SERVICE
238
+
239
+ if (!service) {
240
+ throw new Error('ERR_MDK_SERVICE_ENV: SERVICE env var is required (gateway | worker)')
241
+ }
242
+
243
+ if (service === 'gateway') {
244
+ return runGateway()
245
+ }
246
+
247
+ if (service === 'worker') {
248
+ return runWorker(resolveRepoRoot())
249
+ }
250
+
251
+ throw new Error(`ERR_MDK_SERVICE_UNKNOWN: "${service}"`)
252
+ }
253
+
254
+ module.exports = {
255
+ resolveRepoRoot,
256
+ gatewayPackageDir,
257
+ ensureConfigFromExamples,
258
+ WORKER_BOOTS,
259
+ resolveWorkerBoot,
260
+ runGateway,
261
+ runWorker,
262
+ resolveWorkerTrust,
263
+ main
264
+ }
265
+
266
+ if (require.main === module) {
267
+ main().catch((err) => {
268
+ console.error(err)
269
+ process.exit(1)
270
+ })
271
+ }
package/worker.js ADDED
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ // Shared process entry for PM2, Docker, and direct `node mdk/worker.js` runs.
4
+ const { main } = require('./utils/service-bootstrap')
5
+
6
+ main().catch((err) => {
7
+ console.error(err)
8
+ process.exit(1)
9
+ })