@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.
- package/LICENSE +202 -0
- package/README.md +572 -0
- package/index.js +519 -0
- package/lib/address-policy.js +124 -0
- package/lib/local-discovery.js +321 -0
- package/lib/services/actions.service.js +68 -0
- package/lib/services/alerts.service.js +207 -0
- package/lib/services/comments.service.js +118 -0
- package/lib/services/log-history.service.js +307 -0
- package/lib/services/logs.service.js +249 -0
- package/lib/services/pool-utils/constants.js +20 -0
- package/lib/services/pool-utils/time.js +49 -0
- package/lib/services/pool.service.js +397 -0
- package/lib/services/provisioning.service.js +420 -0
- package/lib/services/settings.service.js +26 -0
- package/lib/services/snaps.service.js +181 -0
- package/lib/services/stats.service.js +205 -0
- package/lib/stats/README.md +80 -0
- package/lib/stats/index.js +57 -0
- package/lib/stats/ops/alerts_aggr.js +40 -0
- package/lib/stats/ops/alerts_group_cnt.js +43 -0
- package/lib/stats/ops/arr_concat.js +27 -0
- package/lib/stats/ops/array_obj_calc.js +41 -0
- package/lib/stats/ops/avg.js +42 -0
- package/lib/stats/ops/cnt.js +19 -0
- package/lib/stats/ops/group.js +38 -0
- package/lib/stats/ops/group_avg.js +53 -0
- package/lib/stats/ops/group_cnt.js +38 -0
- package/lib/stats/ops/group_max.js +40 -0
- package/lib/stats/ops/group_multiple_stats.js +40 -0
- package/lib/stats/ops/group_sum.js +40 -0
- package/lib/stats/ops/index.js +35 -0
- package/lib/stats/ops/nested_obj_concat.js +54 -0
- package/lib/stats/ops/obj_concat.js +33 -0
- package/lib/stats/ops/sum.js +27 -0
- package/lib/stats/utils.js +99 -0
- package/lib/templates/alerts.js +128 -0
- package/lib/templates/stats.js +476 -0
- package/lib/things/constants.js +65 -0
- package/lib/things/container.js +84 -0
- package/lib/things/miner.js +190 -0
- package/lib/things/powermeter.js +30 -0
- package/lib/things/sensor.js +18 -0
- package/lib/things/thing.js +93 -0
- package/lib/utils.js +603 -0
- package/lib/worker-infra.js +221 -0
- package/package.json +64 -1
- package/services.js +246 -0
- package/utils/compose-yaml.js +56 -0
- package/utils/constants.js +30 -0
- package/utils/index.js +29 -0
- package/utils/initialize.js +148 -0
- package/utils/service-bootstrap.js +271 -0
- package/worker.js +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
# @tetherto/mdk-core
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Bootstrap utilities for MDK. This package is the primary entry point for application developers. It provides
|
|
6
|
+
high-level convenience functions that wire together the [Kernel](../kernel/README.md), [device Workers](../../workers/README.md),
|
|
7
|
+
and the [Gateway](../gateway/README.md) HTTP server without requiring direct knowledge of lower-level APIs.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
- Node.js >= 24
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
This package is part of the MDK monorepo and requires both core and Worker dependencies. After cloning, install from the repo root:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The [run a mining site tutorial](../../../docs/tutorials/run-a-site.md) is the full clone-and-install walkthrough.
|
|
22
|
+
|
|
23
|
+
> [!NOTE]
|
|
24
|
+
> Installed from npm, this package pulls in `@tetherto/mdk-gateway`, so `npm audit` reports an `@fastify/static` advisory until
|
|
25
|
+
> your app's root `package.json` overrides it. See the Gateway's
|
|
26
|
+
> [known dependency advisory](../gateway/README.md#known-dependency-advisory).
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const { getKernel, startGateway, waitForDiscovery, shutdown } = require('@tetherto/mdk-core')
|
|
32
|
+
const { trustKernel } = require('@tetherto/mdk-core/lib/local-discovery')
|
|
33
|
+
const { startAvalonWorker } = require('@tetherto/mdk-worker-avalon')
|
|
34
|
+
|
|
35
|
+
// 1. Start Kernel
|
|
36
|
+
const kernel = await getKernel()
|
|
37
|
+
|
|
38
|
+
// 2. Start a Worker: each Worker package ships its own boot function that
|
|
39
|
+
// constructs a WorkerRuntime internally (see @tetherto/mdk-worker).
|
|
40
|
+
// Worker RPC is fail-closed: allow the key the Kernel dials with, then keep
|
|
41
|
+
// following the callers the Kernel admits (the Gateway calls Workers too).
|
|
42
|
+
const { runtime } = await startAvalonWorker({
|
|
43
|
+
workerId: 'avalon-rack-1',
|
|
44
|
+
model: 'a1346',
|
|
45
|
+
storeDir: './data/avalon',
|
|
46
|
+
allowedKeys: [kernel.getClientPublicKey()],
|
|
47
|
+
seedDevices: [{ info: { serialNum: 'AV-001' }, opts: { address: '192.168.1.10', port: 4028, password: 'admin' } }]
|
|
48
|
+
})
|
|
49
|
+
trustKernel(runtime, kernel)
|
|
50
|
+
|
|
51
|
+
// 3. Register the Worker with Kernel (same-process mode, no DHT/local discovery needed)
|
|
52
|
+
await kernel.registerWorker(runtime.getPublicKey())
|
|
53
|
+
|
|
54
|
+
// 4. Wait for Kernel to discover and register the Worker
|
|
55
|
+
await waitForDiscovery(kernel)
|
|
56
|
+
|
|
57
|
+
// 5. Optionally start the HTTP API (admits the Gateway's key on the Kernel
|
|
58
|
+
// and, through trustKernel, on the Worker)
|
|
59
|
+
const server = await startGateway({ kernel, port: 3000 })
|
|
60
|
+
|
|
61
|
+
// The SDK installs no SIGINT/SIGTERM handlers unless asked
|
|
62
|
+
// (getKernel({ handleSignals: true })). Tear down explicitly:
|
|
63
|
+
// await shutdown(kernel)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> [!NOTE]
|
|
67
|
+
> - There is no single generic `startWorker(WorkerClass, opts)` entry point: every Worker package supplies its own
|
|
68
|
+
> boot function that [builds a `WorkerRuntime` for its plugin](#start-a-worker).
|
|
69
|
+
> - [Worker deployment options](../../../docs/concepts/deployment-topologies.md) include same-process, local, and DHT.
|
|
70
|
+
|
|
71
|
+
## API
|
|
72
|
+
|
|
73
|
+
### `getKernel(opts?)` → `Promise<KernelManager>`
|
|
74
|
+
|
|
75
|
+
Start the Kernel with defaults suited for single-process development. Automatically:
|
|
76
|
+
- In DHT mode, reads the topic from `DEFAULT_TOPIC_FILE` or generates one if absent
|
|
77
|
+
- In local mode, watches a shared Worker-key directory and does not create or join a DHT topic
|
|
78
|
+
- Publishes the HRPC public key as hex to `DEFAULT_KEY_FILE` after start, so out-of-process clients can connect without configuration
|
|
79
|
+
- Starts the HRPC listener **fail-closed**: the allowlist is empty unless you pass `opts.hrpc.whitelist`, and nobody is
|
|
80
|
+
admitted until a key is added (`kernel.allowKey()`, `startGateway({ kernel })`, or the trusted-keys dir below). See
|
|
81
|
+
[Trust and admission](#trust-and-admission)
|
|
82
|
+
- Publishes `kernel.pub` (its worker-dialing key, `kernel.getClientPublicKey()`) to the trusted-keys dir and admits the
|
|
83
|
+
other keys published there
|
|
84
|
+
- With `opts.handleSignals: true` only, registers `SIGINT`/`SIGTERM` handlers that drain `kernel._cleanup`, stop the
|
|
85
|
+
Kernel and call `process.exit(0)`. The default (library use) leaves the process's signals alone; call
|
|
86
|
+
[`shutdown(kernel)`](#shutdownhandle--promisevoid) yourself
|
|
87
|
+
|
|
88
|
+
The key file is not deleted on shutdown: the key is stable across restarts (HRPC seeds persist in the Kernel store), so a leftover
|
|
89
|
+
file stays correct for the same store directory.
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
const kernel = await getKernel()
|
|
93
|
+
// kernel.topic — hex DHT topic used, or undefined in local mode
|
|
94
|
+
// kernel.getPublicKey() — HRPC public key
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
| Option | Status | Type | Default | Description |
|
|
98
|
+
| --- | --- | --- | --- | --- |
|
|
99
|
+
| `opts.root` | Optional | `string` | `~/.mdk` | Data root directory |
|
|
100
|
+
| `opts.storeDir` | Optional | `string` | Under `opts.root` | Override Hyperbee store path |
|
|
101
|
+
| `opts.discovery` | Optional | `object` | DHT mode | Discovery config: `{ mode: 'dht' \| 'local', dir?, workerKeys?, allowAnyWorker? }`. In DHT mode a Worker announcing on the topic is admitted only if its RPC key is in `workerKeys` (or added later with `kernel.allowWorkerKey()`); an empty list admits nobody unless `allowAnyWorker: true` (development only, logs a warning). Local mode and `registerWorker()` do not use this list |
|
|
102
|
+
| `opts.topic` | Optional | `string` | Read from `opts.topicFile` if it exists, else a fresh random topic (not persisted) | 32-byte hex DHT topic; overrides the topic file |
|
|
103
|
+
| `opts.topicFile` | Optional | `string` | `DEFAULT_TOPIC_FILE` | Path to the topic file |
|
|
104
|
+
| `opts.keyFile` | Optional | `string\|false` | `DEFAULT_KEY_FILE` | Path for the HRPC key file; `false` to disable publishing |
|
|
105
|
+
| `opts.hrpc` | Optional | `object\|false` | Enabled, empty allowlist (admits nobody) | HRPC config; `false` disables the listener |
|
|
106
|
+
| `opts.hrpc.whitelist` | Optional | `Array<string\|{key, roles}>` | `[]` | Caller allowlist. A hex key gets full access; `{ key, roles }` limits it to `'read'`/`'write'`/`'admin'` ([roles](../kernel/README.md#caller-admission-and-roles)) |
|
|
107
|
+
| `opts.hrpc.allowAll` | Optional | `boolean` | `false` | Admit every DHT peer that knows the Kernel key. Development only; logs a warning |
|
|
108
|
+
| `opts.hrpc.bootstrap` | Optional | `array` | Public DHT | HyperDHT bootstrap nodes; pass the same list to every component on a private DHT network |
|
|
109
|
+
| `opts.trustedKeysDir` | Optional | `string\|false` | `<root>/.trusted-keys` | Same-machine key rendezvous (see [Trust and admission](#trust-and-admission)); `false` disables |
|
|
110
|
+
| `opts.telemetryPullMs` | Optional | `number` | The Kernel's own default | Telemetry poll interval in ms |
|
|
111
|
+
| `opts.healthPingMs` | Optional | `number` | The Kernel's own default | Health ping interval in ms |
|
|
112
|
+
| `opts.statePullMs` | Optional | `number` | The Kernel's own default | Worker identity and device-list refresh interval in ms |
|
|
113
|
+
| `opts.handleSignals` | Optional | `boolean` | `false` | Install `SIGINT`/`SIGTERM` handlers that stop the Kernel and exit the process. Set it in a standalone Kernel process |
|
|
114
|
+
|
|
115
|
+
Cadence options are flat on `getKernel()`. For nested `cadences` configuration, use the
|
|
116
|
+
[`createKernel()` API](../kernel/README.md#createkernelopts--kernelmanager).
|
|
117
|
+
|
|
118
|
+
### Start a Worker
|
|
119
|
+
|
|
120
|
+
There is no generic `startWorker(WorkerClass, opts)` export in this package. Each Worker package (e.g.
|
|
121
|
+
`@tetherto/mdk-worker-avalon`) supplies its own boot function that constructs a
|
|
122
|
+
[`WorkerRuntime`](../mdk-worker/lib/worker-runtime.js) internally and connects it to Kernel through [DHT, local, or
|
|
123
|
+
same-process discovery](../../../docs/concepts/deployment-topologies.md). Every boot function accepts (`kernelTopic`, `discovery`,
|
|
124
|
+
or direct `kernel.registerWorker(runtime.getPublicKey())`).
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const { startAvalonWorker } = require('@tetherto/mdk-worker-avalon')
|
|
128
|
+
|
|
129
|
+
const { runtime, stop } = await startAvalonWorker({
|
|
130
|
+
workerId: 'avalon-rack-1', // one runtime process = one workerId
|
|
131
|
+
model: 'a1346',
|
|
132
|
+
storeDir: './data/avalon',
|
|
133
|
+
kernelTopic: null, // omit/null to register by key instead of a DHT topic
|
|
134
|
+
seedDevices: [{ info: { serialNum: 'AV-001' }, opts: { address: '192.168.1.10', port: 4028, password: 'admin' } }]
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
await kernel.registerWorker(runtime.getPublicKey()) // same-process discovery
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
> [!CAUTION]
|
|
141
|
+
> This boot shape (`storeDir`, `seedDevices`) is the deprecated thing/adapter Worker surface. A new Worker runs on
|
|
142
|
+
> `WorkerRuntimeV2`; see [Build a third-party Worker](../../../docs/guides/workers/build-a-worker.md).
|
|
143
|
+
|
|
144
|
+
Returns vary by Worker package, but every boot function returns at least `{ runtime, stop }`: `runtime` is the
|
|
145
|
+
`WorkerRuntime` instance (`getPublicKey()`, `getDeviceContext(deviceId)`); `stop()` tears the Worker down.
|
|
146
|
+
|
|
147
|
+
On this surface `opts.conf.thing` sets the store's sampling and retention. The keys that decide disk growth:
|
|
148
|
+
|
|
149
|
+
| Key | Default | Effect |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| `storeSnapItvMs` | `300000` | How often one snapshot per device is appended to its `thing-5m-<deviceId>` log |
|
|
152
|
+
| `collectSnapsItvMs` | `60000` | How often the Worker polls its devices |
|
|
153
|
+
| `logRotateMaxLength` | Unset; rotation disabled | Rotate when the segment's Hypercore length reaches this threshold; this is not an exact count of telemetry samples |
|
|
154
|
+
| `logKeepCount` | `3` | Retention parameter used to select older segments for unlinking; it does not enable rotation |
|
|
155
|
+
| `rotateLogsItvMs` | `120000` | How often the rotation check runs |
|
|
156
|
+
|
|
157
|
+
To enable rotation, pass an explicit `opts.conf.thing.logRotateMaxLength`, for example `10000`. The bundled configuration examples
|
|
158
|
+
contain that value, but these boot functions do not load it automatically. Setting `logKeepCount` alone leaves rotation disabled.
|
|
159
|
+
Measure retained disk usage under the site's actual sampling rate and workload rather than treating these settings as a byte limit.
|
|
160
|
+
|
|
161
|
+
### `startGateway(opts?)` → `Promise<WrkServerHttp>`
|
|
162
|
+
|
|
163
|
+
Start the Fastify-based HTTP server. Writes config files under `opts.root`, deep-merging any override objects
|
|
164
|
+
with the example defaults.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
const server = await startGateway({
|
|
168
|
+
kernel,
|
|
169
|
+
port: 3000,
|
|
170
|
+
root: './data/gateway',
|
|
171
|
+
apps: [{ name: 'site', appDir: './apps/site', keyFile: './run/site.key' }],
|
|
172
|
+
gatewayKeyFile: './run/gateway.key'
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Option | Status | Type | Default | Description |
|
|
177
|
+
| --- | --- | --- | --- | --- |
|
|
178
|
+
| `opts.root` | Optional | `string` | `~/.mdk/gateway` | Config/data root |
|
|
179
|
+
| `opts.port` | Optional | `number` | `3000` | HTTP port |
|
|
180
|
+
| `opts.env` | Optional | `string` | `'development'` | Environment string |
|
|
181
|
+
| `opts.kernel` | Optional, one of `opts.kernel`/`opts.kernelKey`/`opts.keyFile` must resolve a key | `KernelManager` | None | Kernel instance; Gateway stop is registered on cleanup. Its `getPublicKey()` also resolves the Kernel key. |
|
|
182
|
+
| `opts.kernelKey` | Optional, see `opts.kernel` | `string\|Buffer\|false` | None | Kernel HRPC listener public key (hex or Buffer); `false` to run without one. The Gateway passes it to no app; each [app runtime](../app-runtime/README.md) takes its own `kernelKey` |
|
|
183
|
+
| `opts.keyFile` | Optional, see `opts.kernel` | `string` | `DEFAULT_KEY_FILE` | Key file to resolve the Kernel key from |
|
|
184
|
+
| `opts.bootstrap` | Optional | `array` | None | DHT bootstrap nodes for the Gateway's client to app runtimes (testnets) |
|
|
185
|
+
| `opts.common` | Optional | `object` | `{}` | Overrides for `common.json` |
|
|
186
|
+
| `opts.httpd` | Optional | `object` | `{}` | Overrides for `httpd.config.json` |
|
|
187
|
+
| `opts.store` | Optional | `object` | `{}` | Overrides for `store.config.json` |
|
|
188
|
+
| `opts.additionalRoutes` | Optional | `array` | None | Extra Fastify route definitions; a raw escape hatch, prefer `apps` |
|
|
189
|
+
| `opts.apps` | Optional | `array` | None | The complete set of apps this Gateway mounts — nothing else is registered. See [Apps](#apps) |
|
|
190
|
+
| `opts.gatewayKeyFile` | Optional | `string` | None | File the Gateway writes the public key it presents to app runtimes to, once it is serving |
|
|
191
|
+
| `opts.handleSignals` | Optional | `boolean` | `false` | Standalone Gateway (no `opts.kernel`) only: install `SIGINT`/`SIGTERM` handlers that stop it and exit the process |
|
|
192
|
+
| `opts.trustedKeysDir` | Optional | `string\|false` | `opts.kernel.trustedKeysDir`, else `DEFAULT_TRUSTED_KEYS_DIR` | Where the Gateway publishes its client key (`status.rpcClientKey`) as `gateway.pub`; `false` disables. With `opts.kernel` the key is also admitted in-process |
|
|
193
|
+
| `opts.trustedKeyName` | Optional | `string` | `'gateway'` | File name for that key (set it when several Gateways share a dir) |
|
|
194
|
+
|
|
195
|
+
### Apps
|
|
196
|
+
|
|
197
|
+
Each `opts.apps` entry is `{ name, appDir, keyFile, mount? }`. The Gateway reads only the app's route table (`mdk-plugin.json` in
|
|
198
|
+
`appDir` or `appDir/plugin`) and forwards every request over HRPC to the app's runtime
|
|
199
|
+
([`@tetherto/mdk-app-runtime`](../app-runtime/README.md)), whose public key it re-reads from `keyFile` on every request; until the
|
|
200
|
+
file holds a key the app's routes answer `503`. `name` matches `^[a-z][a-z0-9-]*$` and is unique; `mount` defaults to `/apps/<name>`,
|
|
201
|
+
and `/` mounts the app at the root. Entries are validated before any boot side effect, throwing `ERR_GATEWAY_APPS_INVALID`,
|
|
202
|
+
`ERR_GATEWAY_APP_NAME_INVALID`, `ERR_GATEWAY_APP_DUPLICATE`, `ERR_GATEWAY_APP_DIR_REQUIRED`, `ERR_GATEWAY_APP_KEY_REQUIRED` or
|
|
203
|
+
`ERR_GATEWAY_APP_MOUNT_INVALID`.
|
|
204
|
+
|
|
205
|
+
Every runtime sees the same Gateway key, stable across restarts (seeded from `<root>/identity/seedRuntimeClient`); point each
|
|
206
|
+
runtime's `allowedKeyFiles` at `opts.gatewayKeyFile`.
|
|
207
|
+
|
|
208
|
+
### Declaring apps
|
|
209
|
+
|
|
210
|
+
`opts.apps` is the whole list; MDK bundles no apps.
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
const { startGateway } = require('@tetherto/mdk-core')
|
|
214
|
+
|
|
215
|
+
await startGateway({
|
|
216
|
+
kernel,
|
|
217
|
+
port: 3847,
|
|
218
|
+
gatewayKeyFile: './run/gateway.key',
|
|
219
|
+
apps: [
|
|
220
|
+
{ name: 'demo', appDir: path.join(__dirname, 'apps', 'demo'), keyFile: './run/demo.key' },
|
|
221
|
+
{ name: 'site', appDir: path.join(__dirname, 'apps', 'site'), keyFile: './run/site.key', mount: '/' }
|
|
222
|
+
]
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
The Kernel HRPC key is resolved **before any boot side effects**, in this order:
|
|
228
|
+
|
|
229
|
+
1. `opts.kernelKey`: hex or Buffer; `false` means run without a Kernel connection.
|
|
230
|
+
2. `opts.kernel.getPublicKey()`: in-process Kernel handle.
|
|
231
|
+
3. Key file: `opts.keyFile` or `DEFAULT_KEY_FILE`.
|
|
232
|
+
4. Otherwise throws: `ERR_KERNEL_KEY_FILE_NOT_FOUND`.
|
|
233
|
+
|
|
234
|
+
The Gateway worker opens no Kernel connection: each app runtime builds its own [`@tetherto/mdk-client`](../client/README.md), and
|
|
235
|
+
a failed connect fails that app's calls with
|
|
236
|
+
[`ERR_MDK_CLIENT_UNAVAILABLE`](../client/README.md#createmdkclientconfig-opts--auto-connecting-client) so as not to crash it.
|
|
237
|
+
|
|
238
|
+
### `startKernel(opts?)` → `Promise<KernelManager>`
|
|
239
|
+
|
|
240
|
+
Lower-level Kernel start. Prefer `getKernel()` for new code. Does not register SIGINT or read the topic file, and writes the key file only when
|
|
241
|
+
`opts.keyFile` is explicitly passed.
|
|
242
|
+
For caller-managed construction and lifecycle, use [`createKernel()` from `@tetherto/mdk-kernel`](../kernel/README.md#createkernelopts--kernelmanager).
|
|
243
|
+
|
|
244
|
+
### `waitForDiscovery(kernel, timeout?)` → `Promise<WorkerEntry[]>`
|
|
245
|
+
|
|
246
|
+
Poll the registry until at least one Worker reaches `READY` state with devices populated, or `timeout` ms elapses
|
|
247
|
+
(default: 30 000 ms). Returns the full list of registered Workers.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
await waitForDiscovery(kernel, 15000)
|
|
251
|
+
const workers = kernel.registry.listWorkers()
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `onShutdown(cleanupFn, opts?)` → handler
|
|
255
|
+
|
|
256
|
+
Register a one-shot cleanup handler on `SIGINT` / `SIGTERM`. Returns the handler so tests can invoke it directly.
|
|
257
|
+
|
|
258
|
+
| Option | Status | Type | Default | Description |
|
|
259
|
+
| --- | --- | --- | --- | --- |
|
|
260
|
+
| `opts.signals` | Optional | `string[]` | `['SIGINT', 'SIGTERM']` | Signals to listen for |
|
|
261
|
+
| `opts.forceMs` | Optional | `number` | `3000` | Force-exit timeout in ms if cleanup hangs |
|
|
262
|
+
|
|
263
|
+
> [!NOTE]
|
|
264
|
+
> `getKernel()`, `startGateway()`, and every Worker boot function register their own `onShutdown`
|
|
265
|
+
> handlers internally. Call this only when you need to add teardown logic outside
|
|
266
|
+
> a boot handle — for example, closing a database or flushing a log buffer.
|
|
267
|
+
|
|
268
|
+
### `shutdown(handle)` → `Promise<void>`
|
|
269
|
+
|
|
270
|
+
Gracefully stop any MDK boot handle — Kernel, Gateway, or Worker. Drains the handle's `_cleanup` array in registration order,
|
|
271
|
+
then calls `.stop()` on the handle itself. Idempotent: calling `shutdown` twice on the same handle is safe.
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
await shutdown(kernel) // stops Gateway and Workers (chained), then stops Kernel
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Prefer `shutdown(kernel)` over calling `shutdown` on each handle separately: passing the Kernel handle tears everything down in the
|
|
278
|
+
order services were started.
|
|
279
|
+
|
|
280
|
+
### Constants
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
const { DEFAULT_TOPIC_FILE, DEFAULT_KEY_FILE, DEFAULT_TRUSTED_KEYS_DIR } = require('@tetherto/mdk-core')
|
|
284
|
+
// DEFAULT_TOPIC_FILE — ~/.mdk/.dht-topic
|
|
285
|
+
// DEFAULT_KEY_FILE — ~/.mdk/.kernel-key (Kernel HRPC public key, hex)
|
|
286
|
+
// DEFAULT_TRUSTED_KEYS_DIR — ~/.mdk/.trusted-keys (kernel.pub, gateway.pub, ...)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### `libStats`
|
|
290
|
+
|
|
291
|
+
Telemetry aggregation ops (`applyStats`, `tallyStats`, `groupBy`, `getVal`, `defaults.timeframes`) used by `StatsService` and
|
|
292
|
+
by each Worker plugin's stats template. See [`lib/stats/README.md`](./lib/stats/README.md).
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
const { libStats } = require('@tetherto/mdk-core')
|
|
296
|
+
const { groupBy } = libStats
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Device data and caller input
|
|
300
|
+
|
|
301
|
+
> [!CAUTION]
|
|
302
|
+
> **Deprecated.** [`createWorkerInfra`](./lib/worker-infra.js), the `*Service` classes and the `Thing` base classes
|
|
303
|
+
> (`Thing`, `Miner`, `Container`, `PowerMeter`, `Sensor`) are the legacy thing/adapter Worker surface. The in-repo
|
|
304
|
+
> Workers still run on them, so nothing is removed. A new Worker is a directory-loaded package on `WorkerRuntimeV2` and
|
|
305
|
+
> needs none of them; the [gaps that remain](../mdk-worker/README.md#store-services) are listed with the Worker Runtime
|
|
306
|
+
> store services.
|
|
307
|
+
|
|
308
|
+
The Worker-infra services treat everything arriving over RPC as untrusted:
|
|
309
|
+
|
|
310
|
+
- **Secrets are redacted.** `thing_config`, `list` and `getDeviceMeta` return `password`, `pass`, `passwd`, `apiKey`,
|
|
311
|
+
`token`, `secret`, `privateKey`, `seed`, `mnemonic` (and keys ending in `Password`, `Secret`, `Token`, ...) as
|
|
312
|
+
`'[REDACTED]'`, including `pools[].password`/`pass`. A `'[REDACTED]'` value sent back in an update is ignored, so an edit
|
|
313
|
+
form fed from `thing_config` never overwrites a stored credential.
|
|
314
|
+
- **Queries are allowlisted.** `list`, `forgetThings` and info-history filters accept `$eq $ne $in $nin $exists $gt $gte $lt
|
|
315
|
+
$lte $and $or $nor $not $elemMatch $all $size` and `$regex`. A `$regex` is a literal search, not a pattern: every
|
|
316
|
+
regex metacharacter is matched as itself, except an optional leading `^` (starts with) and trailing `$` (ends with); it
|
|
317
|
+
is capped at 256 characters and `$options` may only be `i`. `$where`, `$expr` and every other operator, paths under
|
|
318
|
+
`opts.*` and secret-named paths are refused with `ERR_QUERY_*`, and filters run over the redacted records.
|
|
319
|
+
- **Device labels are checked.** `registerThing`/`updateThing` accept `info.container` and `info.pos` only as strings of
|
|
320
|
+
up to 64 letters, digits, spaces and `_ . : # -`, starting with a letter or digit (`ERR_THING_INFO_CONTAINER_INVALID`,
|
|
321
|
+
`ERR_THING_INFO_POS_INVALID`). Stats ops build their group maps without a prototype and skip `__proto__`,
|
|
322
|
+
`constructor` and `prototype` keys, and op names are looked up in a fixed table (`ERR_STATS_OP_UNKNOWN`).
|
|
323
|
+
- **Device addresses are checked.** Remote `registerThing`/`updateThing` refuse loopback, unspecified, link-local (including
|
|
324
|
+
`169.254.169.254`), cloud-metadata and multicast addresses, hostnames resolving to them, and ports outside 1–65535. Set
|
|
325
|
+
`conf.thing.allowedAddressCidrs` (for example `['10.0.0.0/8']`) to replace that policy with an allowlist. Seed devices are
|
|
326
|
+
operator config and are not checked. Changing a device's address drops its stored credentials unless the update supplies new ones.
|
|
327
|
+
- **Log reads are bounded.** `offset` must be an integer in `0..logMaxHeight`, `limit` is capped at `conf.thing.logMaxLimit`
|
|
328
|
+
(default 10000), and `tailLog` opens only the `thing-5m` log of the addressed device and `stat-*` logs (plus keys in
|
|
329
|
+
`conf.thing.tailLogKeys`). A device-scoped stat read may only use that device's tags.
|
|
330
|
+
- **Comments need an author.** `saveThingComment`/`editThingComment`/`deleteThingComment(req, ctx)` take the user from the
|
|
331
|
+
authenticated `ctx.user` when the transport supplies one (a different `req.user` is refused), else from `req.user`. Only
|
|
332
|
+
the author may edit or delete. `conf.thing.requireAuthenticatedCommentUser: true` refuses calls without `ctx.user`.
|
|
333
|
+
|
|
334
|
+
`startServices(config)` writes `ecosystem.config.js`, `docker-compose.generated.yml` and `mdk/` into the current directory.
|
|
335
|
+
`config.repoRoot` (required, `ERR_MDK_REPO_ROOT_REQUIRED`) names the MDK source checkout the services run from; it is never
|
|
336
|
+
searched for, and it reaches each process as `MDK_REPO_ROOT` (`/app/repo` under Docker). The Workers it starts join the DHT topic,
|
|
337
|
+
so the Kernel you start with `getKernel()` must admit them: pass their RPC keys in `discovery.workerKeys` (or call
|
|
338
|
+
`kernel.allowWorkerKey()`), or set `discovery.allowAnyWorker: true` for local development. Each
|
|
339
|
+
file carries a `@generated by @tetherto/mdk-core` marker; a file without it is never overwritten
|
|
340
|
+
(`ERR_MDK_GENERATED_FILE_CONFLICT`) unless `config.overwrite: true`. Docker publishes Gateway ports on `127.0.0.1` unless
|
|
341
|
+
`config.host` or `services[].host` names an interface. `initialize({ sources, root })` seeds config from the MDK source
|
|
342
|
+
checkout named by `sources` (required, `ERR_MDK_INIT_SOURCES_REQUIRED`) and throws `ERR_MDK_INIT_SOURCES_NOT_FOUND` when it
|
|
343
|
+
is not one. MDK packages (the Gateway, Worker packages, this package) are always resolved by package name.
|
|
344
|
+
|
|
345
|
+
## Single-process full stack
|
|
346
|
+
|
|
347
|
+
The typical pattern for running everything in one process during development:
|
|
348
|
+
|
|
349
|
+
```js
|
|
350
|
+
const { getKernel, startGateway, waitForDiscovery } = require('@tetherto/mdk-core')
|
|
351
|
+
const { trustKernel } = require('@tetherto/mdk-core/lib/local-discovery')
|
|
352
|
+
const { startAvalonWorker } = require('@tetherto/mdk-worker-avalon')
|
|
353
|
+
const { startSatecWorker } = require('@tetherto/mdk-worker-satec')
|
|
354
|
+
|
|
355
|
+
async function main () {
|
|
356
|
+
const kernel = await getKernel()
|
|
357
|
+
const allowedKeys = [kernel.getClientPublicKey()]
|
|
358
|
+
|
|
359
|
+
const { runtime: av } = await startAvalonWorker({
|
|
360
|
+
workerId: 'avalon-rack-1',
|
|
361
|
+
model: 'a1346',
|
|
362
|
+
storeDir: './data/avalon',
|
|
363
|
+
allowedKeys,
|
|
364
|
+
seedDevices: [{ info: { serialNum: 'AV-001' }, opts: { address: '192.168.1.10', port: 4028, password: 'admin' } }]
|
|
365
|
+
})
|
|
366
|
+
await kernel.registerWorker(av.getPublicKey())
|
|
367
|
+
|
|
368
|
+
const { runtime: pm } = await startSatecWorker({
|
|
369
|
+
workerId: 'satec-site-1',
|
|
370
|
+
model: 'pm180',
|
|
371
|
+
storeDir: './data/satec',
|
|
372
|
+
allowedKeys,
|
|
373
|
+
seedDevices: [{ info: { serialNum: 'SATEC-001' }, opts: { address: '192.168.1.30', port: 502, unitId: 1 } }]
|
|
374
|
+
})
|
|
375
|
+
await kernel.registerWorker(pm.getPublicKey())
|
|
376
|
+
// Follow later admissions (the Gateway below also calls Workers directly)
|
|
377
|
+
trustKernel(av, kernel)
|
|
378
|
+
trustKernel(pm, kernel)
|
|
379
|
+
|
|
380
|
+
await waitForDiscovery(kernel)
|
|
381
|
+
|
|
382
|
+
await startGateway({ kernel, port: 3000 })
|
|
383
|
+
console.log('MDK running at http://localhost:3000')
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
main()
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Trust and admission
|
|
390
|
+
|
|
391
|
+
The Kernel HRPC listener and every Worker RPC server are **fail-closed**: they reject public keys they were not told
|
|
392
|
+
about, and an empty allowlist admits nobody. `allowAll: true` (Kernel `hrpc.allowAll`, Worker `allowAll`) is the only
|
|
393
|
+
way to admit everyone; it is meant for local experiments and logs a warning.
|
|
394
|
+
|
|
395
|
+
Who dials whom, and with which key:
|
|
396
|
+
|
|
397
|
+
| Caller | Dials | With key | Admitted by |
|
|
398
|
+
| --- | --- | --- | --- |
|
|
399
|
+
| Kernel | Workers | `kernel.getClientPublicKey()` (the Kernel's DHT keypair, stable across restarts) | Worker `allowedKeys` |
|
|
400
|
+
| Gateway plugins | Kernel, and Workers directly | `status.rpcClientKey` (Gateway DHT keypair, persisted) | Kernel allowlist and Worker `allowedKeys` |
|
|
401
|
+
| MCP / CLI clients | Kernel (and Workers) | a persistent `callerKeyPair(dir)` passed as `kernelKeyPair` | same |
|
|
402
|
+
|
|
403
|
+
The SDK wires the local cases:
|
|
404
|
+
|
|
405
|
+
- **Same process.** `startGateway({ kernel })` calls `kernel.allowKey(gatewayKey)`. Workers allow
|
|
406
|
+
`kernel.getClientPublicKey()` at boot and call `trustKernel(runtime, kernel)` to mirror every write-capable caller
|
|
407
|
+
the Kernel admits later.
|
|
408
|
+
- **Same machine, separate processes.** Every component meets in a trusted-keys dir (`getKernel` default
|
|
409
|
+
`<root>/.trusted-keys`, `DEFAULT_TRUSTED_KEYS_DIR` = `~/.mdk/.trusted-keys`). The Kernel publishes `kernel.pub` and
|
|
410
|
+
admits every other `<name>.pub`; a standalone `startGateway` publishes `gateway.pub`; a Worker process allows what it
|
|
411
|
+
finds there (`trustKeysFromDir(runtime, dir)`, after `waitForKernelKey(dir)`). The dir is created `0700`; key files
|
|
412
|
+
that are group/world-writable or owned by another user are ignored. Anyone who can write the dir is trusted, so keep it
|
|
413
|
+
private, or pass `trustedKeysDir: false` and configure keys explicitly.
|
|
414
|
+
- **Across machines.** Without a shared trust directory, list caller keys and roles explicitly in Kernel's `hrpc.whitelist`, and
|
|
415
|
+
Kernel's client key in each Worker's `allowedKeys`. Add other Worker callers only to grant direct access to all supported operations;
|
|
416
|
+
Kernel roles do not restrict those direct calls. A Worker that cannot learn a key fails to
|
|
417
|
+
start with `ERR_WORKER_ALLOWED_KEYS_REQUIRED` rather than admitting everyone.
|
|
418
|
+
|
|
419
|
+
Automatic admission is a grant of access, not just key discovery. The [site security blueprint](../../../docs/guides/security/index.md#automatic-admission-during-startup)
|
|
420
|
+
details the grants, explicit configuration, and revocation behavior. In particular, `trustedKeysDir: false` disables directory admission
|
|
421
|
+
on Kernel, but does not prevent `startGateway({ kernel })` from admitting its Gateway key with full access.
|
|
422
|
+
|
|
423
|
+
Helpers in `@tetherto/mdk-core/lib/local-discovery`: `trustedKeysDir(root)`, `publishTrustedKey(dir, name, key)`,
|
|
424
|
+
`readTrustedKeys(dir)`, `watchTrustedKeys(dir, onKey)`, `admitTrustedKeys(kernel, dir)`, `trustKeysFromDir(runtime, dir)`,
|
|
425
|
+
`waitForKernelKey(dir, { timeoutMs })`, `trustKernel(runtime, kernel)`, `callerKeyPair(dir)`.
|
|
426
|
+
|
|
427
|
+
`publishWorkerKey(dir, workerId, key)` / `discoverWorkerKeys(kernel, dir)` (local discovery) apply the same rules as the
|
|
428
|
+
trusted-keys dir: `workerId` must match `[A-Za-z0-9._-]+`, the dir is `0700` (an existing one that is a symlink, owned by
|
|
429
|
+
another user, or group/world-writable is refused with `ERR_KEYS_DIR_UNSAFE`), files are written `0600` atomically, and only
|
|
430
|
+
regular, owned, non-group/world-writable `<name>.key` files holding a 64-hex key are dialled.
|
|
431
|
+
|
|
432
|
+
`utils/service-bootstrap.js` (`SERVICE=worker`, which also needs `MDK_REPO_ROOT`) admits the keys in `DEFAULT_TRUSTED_KEYS_DIR` plus
|
|
433
|
+
`MDK_WORKER_ALLOWED_KEYS` (comma-separated hex); `MDK_WORKER_ALLOW_ALL=1` opts out for development.
|
|
434
|
+
|
|
435
|
+
## Config management
|
|
436
|
+
|
|
437
|
+
`startGateway()` and each Worker's boot function copy example config files into their `opts.root/config/` (or `storeDir`) on first run.
|
|
438
|
+
After that, the files are left untouched so your edits survive restarts. Pass override objects to `startGateway()` to programmatically
|
|
439
|
+
set specific values without editing files.
|
|
440
|
+
|
|
441
|
+
Config file precedence:
|
|
442
|
+
1. Existing file on disk (your edits are authoritative).
|
|
443
|
+
2. Deep-merged overrides from `opts.*`.
|
|
444
|
+
3. `.example` template from the package.
|
|
445
|
+
|
|
446
|
+
## Errors
|
|
447
|
+
|
|
448
|
+
| Code | Fires when | Fix |
|
|
449
|
+
| --- | --- | --- |
|
|
450
|
+
| `ERR_ALERTS_LOGS_REQUIRED` | AlertsService is constructed without a `logs` instance | Pass the logs service in the AlertsService constructor options |
|
|
451
|
+
| `ERR_BEE_LOG_META_NOTFOUND` | A log key is rotated but has no meta record to advance | Initialize the log meta before rotating, or rotate only known log keys |
|
|
452
|
+
| `ERR_BEE_ROTATED_LOG_META_NOTFOUND` | A corrupted time log is rotated but its new meta cannot be read back | Inspect the log store for corruption and rebuild the affected log key |
|
|
453
|
+
| `ERR_COMMENT_ACCESS_DENIED` | A comment edit or delete is requested by a user who is not the comment's author, or the comment has no author | Perform the edit or delete as the user who created the comment |
|
|
454
|
+
| `ERR_COMMENT_USER_MISMATCH` | `req.user` differs from the authenticated `ctx.user` | Drop `req.user`; the authenticated identity is used |
|
|
455
|
+
| `ERR_COMMENT_USER_REQUIRED` | A comment call carries no user | Pass `ctx.user` (or `req.user`) |
|
|
456
|
+
| `ERR_COMMENT_USER_UNAUTHENTICATED` | `requireAuthenticatedUser` is set and no `ctx.user` is passed | Forward the authenticated identity as `ctx.user` |
|
|
457
|
+
| `ERR_CRON_UNSUPPORTED` | A cron expression's minute and hour fields do not map to a supported interval | Use a `*/n` minute, `*/n` hour, or `0 0` daily form |
|
|
458
|
+
| `ERR_DEVICE_TYPE_REQUIRED` | `createWorkerInfra` is called without `opts.deviceType` | Pass a `deviceType` string in the Worker infra options |
|
|
459
|
+
| `ERR_DEVICE_UNAVAILABLE` | A device call targets a `deviceId` that has no live runtime context | Register and bind the device before calling it |
|
|
460
|
+
| `ERR_END_INVALID` | A DB range read is requested without an `end` bound | Supply an `end` timestamp in the query |
|
|
461
|
+
| `ERR_ENTRIES_INVALID` | Settings are saved with an entries argument that is not an object | Pass an object of key/value settings entries |
|
|
462
|
+
| `ERR_HIST_LOG_NOTFOUND` | A history log is read but does not parse to an array of entries | Check that the log key holds valid history entries |
|
|
463
|
+
| `ERR_INFO_HISTORY_LOG_NOTFOUND` | An `info` historical-logs request finds no `thing-history-log` time log | Confirm info-history logging is enabled and the log exists |
|
|
464
|
+
| `ERR_INFO_HISTORY_LOG_TYPE_INVALID` | A historical-logs request omits `logType` | Set `logType` to `alerts` or `info` |
|
|
465
|
+
| `ERR_INVALID_ARG_TYPE` | Miner pool pre-processing is given a `pools` value that is not an array | Pass pools as an array |
|
|
466
|
+
| `ERR_INVALID_GROUP_RANGE_FORMAT` | A group-range string does not match the `<number><H\|D\|W\|M>` pattern | Use a range like `1H`, `1D`, `1W` or `1M` |
|
|
467
|
+
| `ERR_KERNEL_CLIENT_KEY_NOT_FOUND` | `waitForKernelKey` finds no `kernel.pub` in the trusted-keys dir in time | Start the Kernel first, or pass the Worker's `allowedKeys` explicitly |
|
|
468
|
+
| `ERR_KERNEL_CLIENT_KEY_UNAVAILABLE` | `trustKernel` is called with a Kernel that has no HRPC client key | Start the Kernel (with HRPC enabled) before its Workers |
|
|
469
|
+
| `ERR_KERNEL_KEY_FILE_NOT_FOUND` | `startGateway` cannot resolve a Kernel key from options or the key file | Start the Kernel first, pass `opts.kernelKey` or `opts.kernel`, or set `kernelKey: false` to run without a Kernel |
|
|
470
|
+
| `ERR_MDK_WORKER_ALLOWED_KEYS_REQUIRED` | `service-bootstrap` boots a Worker with no trusted key | Start the Kernel first, set `MDK_WORKER_ALLOWED_KEYS`, or `MDK_WORKER_ALLOW_ALL=1` for development |
|
|
471
|
+
| `ERR_KEY_INVALID` | A Worker external-data request has a query with no `key` | Include a `key` in the query |
|
|
472
|
+
| `ERR_LOG_HISTORY_LOGS_REQUIRED` | LogHistoryService is constructed without a `logs` instance | Pass the logs service in the constructor options |
|
|
473
|
+
| `ERR_LOG_KEY_INVALID` | A `tailLog` key is malformed or not a `thing-5m` / `stat-*` / `conf.tailLogKeys` log | Use an allowed log key |
|
|
474
|
+
| `ERR_LOG_LIMIT_INVALID` | A log read's `limit` is not a non-negative integer | Pass an integer `limit` |
|
|
475
|
+
| `ERR_LOG_OFFSET_INVALID` | A log read's `offset` is not an integer in `0..logMaxHeight` | Pass an integer offset within the retained rotations |
|
|
476
|
+
| `ERR_LOG_START_INVALID` / `ERR_LOG_END_INVALID` | A log read's `start`/`end` is not a finite timestamp | Pass millisecond timestamps |
|
|
477
|
+
| `ERR_LOG_TAG_FORBIDDEN` | A `tailLog` tag is not a provisioned device (`thing-5m`) or not one of the addressed device's tags | Read only the addressed device's logs |
|
|
478
|
+
| `ERR_LOG_KEY_NOTFOUND` | `tailLog` is called without `req.key` | Provide the log `key` in the request |
|
|
479
|
+
| `ERR_LOG_NOTFOUND` | A requested time log is absent. This covers the `alerts` historical log and any `tailLog` `key`-`tag` log. | Confirm the log key exists and data has been written to it |
|
|
480
|
+
| `ERR_LOG_TAG_INVALID` | `tailLog` is called without `req.tag` | Provide the log `tag` in the request |
|
|
481
|
+
| `ERR_LOGS_META_LOGS_REQUIRED` | LogsService is constructed without a `metaLogs` store | Pass a `metaLogs` store in the constructor options |
|
|
482
|
+
| `ERR_LOGS_STORE_REQUIRED` | LogsService is constructed without a `store` | Pass a `store` in the constructor options |
|
|
483
|
+
| `ERR_MDK_GENERATED_FILE_CONFLICT` | `startServices` would overwrite a file in the site dir it did not generate | Move the file away or pass `overwrite: true` |
|
|
484
|
+
| `ERR_MDK_INIT_SOURCES_NOT_FOUND` | `initialize()` finds no MDK source checkout at `opts.sources` | Pass `opts.sources` pointing at an MDK monorepo checkout |
|
|
485
|
+
| `ERR_MDK_INIT_SOURCES_REQUIRED` | `initialize()` is called without `opts.sources` | Pass `opts.sources` pointing at an MDK monorepo checkout |
|
|
486
|
+
| `ERR_MDK_REPO_ROOT_REQUIRED` | `startServices` has no `config.repoRoot`, or the service bootstrap runs a Worker without `MDK_REPO_ROOT` | Pass `repoRoot` / set `MDK_REPO_ROOT` to the MDK source checkout |
|
|
487
|
+
| `ERR_KEYS_DIR_UNSAFE` | A Worker-keys dir is not a real directory, is owned by another user, or is group/world-writable | Use a private (`0700`) directory owned by the service user |
|
|
488
|
+
| `ERR_STATS_OP_UNKNOWN` | A stats spec names an op that is not one of the built-in ops | Use a built-in op name (`sum`, `avg`, `group_sum`, ...) |
|
|
489
|
+
| `ERR_THING_INFO_INVALID` | `registerThing`/`updateThing` `info` is not an object | Pass `info` as an object |
|
|
490
|
+
| `ERR_THING_INFO_CONTAINER_INVALID` / `ERR_THING_INFO_POS_INVALID` | `info.container` / `info.pos` is not a short label (64 chars of letters, digits, space, `_ . : # -`) or is a prototype key | Use a plain label |
|
|
491
|
+
| `ERR_MDK_SERVICE_ENV` | The `SERVICE` env var is unset | Set `SERVICE` to `gateway` or `worker` |
|
|
492
|
+
| `ERR_MDK_SERVICE_UNKNOWN` | `SERVICE` is set to something other than `gateway` or `worker` | Set `SERVICE` to `gateway` or `worker` |
|
|
493
|
+
| `ERR_MDK_WORKER_ENV` | A Worker is started without both `WORKER` and `RACK`, or a non-pool Worker is started without `TYPE` | Set `WORKER` and `RACK`, and set `TYPE` for non-pool Workers |
|
|
494
|
+
| `ERR_MDK_WORKER_EXPORT` | The resolved Worker package does not export the expected factory function | Check the Worker package exports the factory named in its boot spec |
|
|
495
|
+
| `ERR_MDK_WORKER_UNKNOWN` | The `WORKER` name has no runtime boot entry, or the `WORKER`+`TYPE` pair has no plugin model | Use a known Worker name and a `TYPE` its boot spec defines |
|
|
496
|
+
| `ERR_NO_IMPL` | An abstract Thing or Container method is called on a class that has not overridden it | Implement the method on the concrete device class |
|
|
497
|
+
| `ERR_PROC_RACK_UNDEFINED` | PoolService is constructed with a context that has no `rack` | Pass `ctx.rack` when constructing the pool service |
|
|
498
|
+
| `ERR_PROVISIONING_DB_REQUIRED` | DeviceProvisioningService is constructed without a `db` | Pass a `db` in the constructor options |
|
|
499
|
+
| `ERR_PROVISIONING_DEVICE_TYPE_REQUIRED` | DeviceProvisioningService is constructed without a `deviceType` string | Pass a non-empty `deviceType` string |
|
|
500
|
+
| `ERR_QUERY_INVALID` | A Worker external-data request has no `query`, or a caller filter is not an object | Include a `query` object in the request |
|
|
501
|
+
| `ERR_QUERY_OPERATOR_FORBIDDEN` | A caller filter uses an operator outside the allowlist (`$where`, `$expr`, ...) | Use the allowlisted operators |
|
|
502
|
+
| `ERR_QUERY_PATH_FORBIDDEN` | A caller filter, sort or projection names `opts.*`, a secret-named field or a prototype key | Filter on `info.*`, `tags`, `type`, `id`, `code` |
|
|
503
|
+
| `ERR_QUERY_REGEX_INVALID` | A `$regex` is not a string, is over 256 characters, or has `$options` other than `i` | Send the literal text to search for, with optional `^` / `$` |
|
|
504
|
+
| `ERR_QUERY_TOO_COMPLEX` | A caller filter is deeper than 8 levels or has more than 500 nodes | Simplify the filter |
|
|
505
|
+
| `ERR_QUERY_OFFSET_INVALID` / `ERR_QUERY_LIMIT_INVALID` | `offset`/`limit` is not a non-negative integer | Pass integers |
|
|
506
|
+
| `ERR_STATS_KEYPATH_FORBIDDEN` / `ERR_STATS_KEYPATH_INVALID` | A `libStats` keypath reaches `__proto__`/`constructor`/`prototype` or is not `a.b[0].c` syntax | Use a plain property path |
|
|
507
|
+
| `ERR_RUNTIME_NOT_BOUND` | A device call runs before the runtime device-context getter is bound | Bind the runtime (start the Worker infra) before making device calls |
|
|
508
|
+
| `ERR_SET_LED_ENABLED_INVALID` | A miner `setLED` action is given a non-boolean `enabled` | Pass `enabled` as a boolean |
|
|
509
|
+
| `ERR_SETTINGS_DB_REQUIRED` | SettingsService is constructed without a `settingsDb` | Pass a `settingsDb` in the constructor options |
|
|
510
|
+
| `ERR_SNAPS_COLLECT_SNAP_REQUIRED` | SnapsService is constructed without a `collectSnap` function | Pass a `collectSnap` function in the constructor options |
|
|
511
|
+
| `ERR_SNAPS_LOGS_REQUIRED` | SnapsService is constructed without a `logs` instance | Pass the logs service in the constructor options |
|
|
512
|
+
| `ERR_START_INVALID` | A DB range read is requested without a `start` bound | Supply a `start` timestamp in the query |
|
|
513
|
+
| `ERR_STATS_LOGS_REQUIRED` | StatsService is constructed without a `logs` instance | Pass the logs service in the constructor options |
|
|
514
|
+
| `ERR_STORE_DIR_REQUIRED` | `createWorkerInfra` is called without `opts.storeDir` | Pass a `storeDir` path in the Worker infra options |
|
|
515
|
+
| `ERR_SWITCH_CONTAINER_ENABLED_INVALID` | A container `switchContainer` action is given a non-boolean `enabled` | Pass `enabled` as a boolean |
|
|
516
|
+
| `ERR_SWITCH_COOLING_SYSTEM_ENABLED_INVALID` | A `switchCoolingSystem` action is given a non-boolean `enabled` | Pass `enabled` as a boolean |
|
|
517
|
+
| `ERR_SWITCH_SOCKET_ARGS_INVALID` | A `switchSocket` action's first argument is not an array of tuples | Pass socket instructions as an array |
|
|
518
|
+
| `ERR_SWITCH_SOCKET_ENABLED_INVALID` | A `switchSocket` tuple's `enabled` is not a boolean | Pass the tuple's `enabled` as a boolean |
|
|
519
|
+
| `ERR_SWITCH_SOCKET_PDU_INDEX_INVALID` | A `switchSocket` tuple's PDU index is not a string | Pass the PDU index as a string |
|
|
520
|
+
| `ERR_SWITCH_SOCKET_SOCKET_INDEX_INVALID` | A `switchSocket` tuple's socket index is not a string | Pass the socket index as a string |
|
|
521
|
+
| `ERR_THING_CODE_INVALID` | A device is registered with a `code` that does not end in `-<digits>` | Supply a code ending in a hyphen and numeric suffix, or omit it to auto-generate |
|
|
522
|
+
| `ERR_THING_COMMENT_NOTFOUND` | A comment edit or delete targets an `id` or `ts` that no comment on the thing matches | Reference an existing comment `id` or `ts` |
|
|
523
|
+
| `ERR_THING_COMMENTS_NOTFOUND` | A comment edit or delete runs on a thing that has no comments | Add a comment before editing or deleting one |
|
|
524
|
+
| `ERR_THING_ADDRESS_FORBIDDEN` | A remote register/update points a device at a loopback, link-local, metadata, unspecified or multicast address, or outside `allowedAddressCidrs` | Use the device's LAN address, or add its network to `conf.thing.allowedAddressCidrs` |
|
|
525
|
+
| `ERR_THING_ADDRESS_INVALID` / `ERR_THING_ADDRESS_UNRESOLVABLE` | `opts.address` is not an IP or hostname, or the hostname does not resolve | Pass a valid, resolvable address |
|
|
526
|
+
| `ERR_THING_PORT_INVALID` | `opts.port` is not an integer 1–65535 | Pass a valid port |
|
|
527
|
+
| `ERR_TIME_RANGE_INVALID` / `ERR_TIME_RANGE_TOO_LARGE` | A pool time range is non-numeric or spans more than 9000 buckets | Narrow the range |
|
|
528
|
+
| `ERR_THING_IP_ADDRESS_EXISTS` | A device is registered or updated with an address already held by another device while duplicate IPs are disallowed | Use a unique address or enable `allowDuplicateIPs` |
|
|
529
|
+
| `ERR_THING_MACADDRESS_EXISTS` | A device is registered or updated with a MAC address already held by another device | Use a unique MAC address |
|
|
530
|
+
| `ERR_THING_NOTFOUND` | An operation references a device `id` that is not in the store | Register the device first or correct the `id` |
|
|
531
|
+
| `ERR_THING_POS_EXISTS` | A device is registered or updated with a position and container pair already held by another device | Use a free position within that container |
|
|
532
|
+
| `ERR_THING_SERIALNUM_EXISTS` | A device is registered or updated with a serial number already held by another device | Use a unique serial number |
|
|
533
|
+
| `ERR_THING_TAGS_INVALID` | Auxiliary tags are not an array, number more than 32, or one is not a label of at most 64 characters (letters, digits, space, `_ . : # -`, starting with a letter or digit; `__proto__`, `constructor` and `prototype` refused) | Pass short label strings as an array |
|
|
534
|
+
| `ERR_THING_VALIDATE_OPTS_INVALID` | A device is registered without an `opts` object | Include the device's `opts` in the registration payload |
|
|
535
|
+
| `ERR_THING_WITH_CODE_ALREADY_EXISTS` | A device is registered with a `code` already assigned to another device | Use a unique code or omit it to auto-generate |
|
|
536
|
+
| `ERR_THING_WITH_ID_ALREADY_EXISTS` | A device is registered with an `id` that already exists | Use a new `id` or omit it to auto-generate |
|
|
537
|
+
| `ERR_UNKNOWN_OPERATION` | Numeric aggregation is asked for an operation other than `avg` or `sum` | Use `avg` or `sum` |
|
|
538
|
+
|
|
539
|
+
## Directory layout
|
|
540
|
+
|
|
541
|
+
```text
|
|
542
|
+
mdk/
|
|
543
|
+
├── index.js # `getKernel`, `startGateway`, `waitForDiscovery`
|
|
544
|
+
├── services.js # `startServices` — facility bootstrap helpers
|
|
545
|
+
├── worker.js # Worker-side entry point
|
|
546
|
+
├── lib/
|
|
547
|
+
│ ├── local-discovery.js # `discovery: { mode: 'local' }` helpers and the trusted-keys (admission) helpers
|
|
548
|
+
│ ├── utils.js # Shared helpers (isValidSnap, isOffline, etc.)
|
|
549
|
+
│ ├── worker-infra.js # `createWorkerInfra` — per-Worker infra bootstrap
|
|
550
|
+
│ ├── stats/ # `libStats` — telemetry aggregation ops (sum, avg, group_*, …)
|
|
551
|
+
│ ├── services/ # One class per domain service (Actions, Alerts, Comments, LogHistory, Logs, Pool, Provisioning, Settings, Snaps, Stats)
|
|
552
|
+
│ │ └── pool-utils/ # Shared pool-service constants and time helpers
|
|
553
|
+
│ ├── templates/ # Alert and stats computation templates
|
|
554
|
+
│ └── things/ # Thing subclasses — Container, Miner, PowerMeter, Sensor (base: Thing)
|
|
555
|
+
├── utils/
|
|
556
|
+
│ ├── constants.js # MDK_STORE and other well-known names
|
|
557
|
+
│ ├── index.js # Facility bootstrap helpers (Intervals, Store, ActionApprover)
|
|
558
|
+
│ ├── initialize.js # Service initialization helpers
|
|
559
|
+
│ ├── service-bootstrap.js # Spawns/manages service subprocesses
|
|
560
|
+
│ └── compose-yaml.js # `buildComposeYaml` — docker-compose file generation
|
|
561
|
+
└── tests/
|
|
562
|
+
├── unit/ # One suite per lib/service/thing/util module
|
|
563
|
+
└── integration/ # Kernel-key-file, local-discovery, actions-flow (per Worker family)
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## Next steps
|
|
567
|
+
|
|
568
|
+
- [Run the Gateway](../../../docs/guides/gateway/run.md): programmatic and standalone startup, auth configuration, and HRPC key setup
|
|
569
|
+
- [Add hardware devices](../../workers/README.md): understand how Workers register devices and expose them to Kernel
|
|
570
|
+
- [Add custom routes with plugins](../../../docs/guides/gateway/plugins.md): extend the Gateway with `apps`
|
|
571
|
+
- [See the MVP site example](../../../examples/mvp-site/README.md): multi-Worker, multi-device setup in a separate-process topology
|
|
572
|
+
- [Understand the the full MDK layer model](../../../docs/concepts/architecture.md)
|