@tikoci/centrs 0.1.9-next.24 → 0.1.9-next.27
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/CHANGELOG.md +57 -0
- package/GLOSSARY.txt +9 -1
- package/README.md +28 -16
- package/commands/api/AGENTS.md +3 -2
- package/commands/api/README.md +8 -1
- package/commands/btest/README.md +9 -0
- package/commands/btest/examples.md +7 -0
- package/commands/devices/README.md +72 -3
- package/commands/devices/examples.md +56 -1
- package/commands/execute/README.md +11 -0
- package/commands/explain/README.md +223 -3
- package/commands/explain/examples.md +8 -1
- package/commands/retrieve/README.md +22 -1
- package/commands/retrieve/examples.md +84 -0
- package/commands/transfer/README.md +25 -0
- package/commands/transfer/examples.md +46 -0
- package/docs/CLI.md +11 -3
- package/docs/MATRIX.md +5 -3
- package/docs/errors/cdb/encrypt-failed.md +10 -0
- package/docs/errors/cdb/password-required.md +5 -4
- package/docs/errors/cdb/plaintext-backups.md +11 -0
- package/docs/errors/input/invalid-routeros-path.md +25 -3
- package/package.json +8 -4
- package/src/api-fanout.ts +2 -0
- package/src/api.ts +12 -0
- package/src/btest.ts +7 -0
- package/src/cli/api.ts +18 -1
- package/src/cli/btest.ts +9 -0
- package/src/cli/devices.ts +76 -8
- package/src/cli/execute.ts +9 -0
- package/src/cli/retrieve.ts +9 -0
- package/src/cli/terminal.ts +9 -0
- package/src/cli/transfer.ts +10 -1
- package/src/core/error-catalog.ts +13 -2
- package/src/core/menu-path.ts +89 -0
- package/src/devices.ts +154 -9
- package/src/execute-fanout.ts +2 -0
- package/src/execute.ts +7 -0
- package/src/explain/blocks.ts +2 -2
- package/src/explain/catalog.ts +22 -12
- package/src/explain/pasted-prompt.ts +198 -0
- package/src/explain/pathresolve.ts +2 -2
- package/src/explain/segment.ts +2 -2
- package/src/explain/symbols.ts +2 -2
- package/src/explain/verbsplit.ts +2 -2
- package/src/explain/write.ts +2 -2
- package/src/explain.ts +2 -0
- package/src/index.ts +4 -0
- package/src/protocols/adapter.ts +2 -0
- package/src/protocols/btest-session.ts +10 -30
- package/src/protocols/ec-srp5.ts +19 -10
- package/src/protocols/mac-telnet-console.ts +2 -2
- package/src/resolver/cdb.ts +58 -2
- package/src/resolver/index.ts +1 -0
- package/src/resolver/quickchr-provider.ts +5 -0
- package/src/resolver/selection.ts +11 -0
- package/src/retrieve-fanout.ts +2 -0
- package/src/retrieve.ts +11 -0
- package/src/settings.ts +1 -0
- package/src/terminal.ts +7 -0
- package/src/transfer-fanout.ts +2 -0
- package/src/transfer.ts +79 -14
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,18 @@ part of this section.
|
|
|
17
17
|
|
|
18
18
|
### Added
|
|
19
19
|
|
|
20
|
+
- `explain` offers a non-applied repair tip for pasted console prompts
|
|
21
|
+
(#370, #567, #578), including `[user@host] /menu>` and HotLock `>>` forms.
|
|
22
|
+
The candidate preserves command separators and names the menu context;
|
|
23
|
+
multiline repairs render as fenced code. The original analysis stays intact.
|
|
24
|
+
- `--match user=<name>|target=<addr>|<record-type>` on `retrieve`, `execute`,
|
|
25
|
+
`api`, `transfer`, `terminal` and `btest client` picks one of several CDB
|
|
26
|
+
records saved for the same `<router>` (#583), as `devices show --match`
|
|
27
|
+
does. One address saved under two users was `identity/ambiguous` on every
|
|
28
|
+
command, and the error named a `--match` they rejected. The flag is
|
|
29
|
+
single-target: with a fan-out selection or `--quickchr` it is
|
|
30
|
+
`usage/conflicting-flags`, and with no CDB record in play it is
|
|
31
|
+
`identity/no-match` rather than ignored.
|
|
20
32
|
- **Live `explain` over rest-api + native-api is `CHR-passed`** (#236).
|
|
21
33
|
Examples 7–16, 19, 24, 48 and 52–53 pass on both transports on stable
|
|
22
34
|
7.24.5 and long-term 7.23.8. `runtimeAcceptance` stays `not-proven`.
|
|
@@ -92,6 +104,16 @@ part of this section.
|
|
|
92
104
|
client, its controller.
|
|
93
105
|
- `execute --cmr` accepts a command that opens with a relative menu change
|
|
94
106
|
(`/system; identity/print`); its `:parse` return was read as incomplete.
|
|
107
|
+
- CDB encryption lifecycle (#558). `devices init --encrypt` creates an
|
|
108
|
+
encrypted empty CDB; `devices encrypt` converts an existing open CDB in place
|
|
109
|
+
with a fresh salt, keeping its records, unknown fields, and file mode. Both
|
|
110
|
+
need a non-empty `--cdb-password` / `CENTRS_CDB_PASSWORD`; a password alone
|
|
111
|
+
still never converts. Conversion keeps every backup and writes a plaintext
|
|
112
|
+
snapshot unless `--no-backup` is passed. It warns with `cdb/plaintext-backups`.
|
|
113
|
+
An already-encrypted file is verified and left unchanged. Library:
|
|
114
|
+
`initCdb({ encrypt })`, `encryptCdb({ skipBackup })`. `init` results now
|
|
115
|
+
include `encrypted`. `init` and `encrypt` refuse flags they would otherwise
|
|
116
|
+
ignore (`--user`, `--group`, `--via`, …) with `input/invalid-command`.
|
|
95
117
|
|
|
96
118
|
### Changed
|
|
97
119
|
|
|
@@ -201,6 +223,35 @@ part of this section.
|
|
|
201
223
|
|
|
202
224
|
### Fixed
|
|
203
225
|
|
|
226
|
+
- **`retrieve` and `api` can no longer send a non-read** (#586). A `#` or
|
|
227
|
+
`?` in the path cut the appended `/print` off the REST URL, so
|
|
228
|
+
`retrieve '/system/reboot#' --no-validate --attribute uptime` posted
|
|
229
|
+
`/rest/system/reboot` and rebooted CHR 7.24.5 without `--yes`; `api` with a
|
|
230
|
+
projection did the same. Both now require every path segment to be a
|
|
231
|
+
RouterOS menu name (letters, digits, hyphens; case folds) on every
|
|
232
|
+
transport, even with `--validate=false`, failing
|
|
233
|
+
`input/invalid-routeros-path` before resolution. The REST adapter also
|
|
234
|
+
refuses any request path containing `?`, `#`, `\`, control characters or a
|
|
235
|
+
dot segment (including `%2e%2e`), for every command. Reported by
|
|
236
|
+
routeros-muse.
|
|
237
|
+
- `devices set <target> key=` removes the comment token instead of storing an
|
|
238
|
+
empty value, as documented (#590). Removing an absent token is a no-op;
|
|
239
|
+
`--password ""` still clears the password, and bare `password=`/`user=`
|
|
240
|
+
stay `cdb/reserved-key`.
|
|
241
|
+
- `transfer` honors a CDB record's `via=` preference (#591). A record with
|
|
242
|
+
`via=ssh` drove `execute` over SSH but transfer ignored it and needed
|
|
243
|
+
`--via sftp`. Transfer now maps the record's transport to a method
|
|
244
|
+
(`ssh`→`sftp`, `rest-api`→`rest`, `native-api`→`native`) after `--via` and
|
|
245
|
+
`CENTRS_TRANSFER_VIA`, and accepts `--via ssh` as an alias of `sftp`. A
|
|
246
|
+
record transport that cannot carry files fails `settings/invalid-via`
|
|
247
|
+
naming the record; a record-pinned REST upload over 60 KB names the record
|
|
248
|
+
in its `transport/unsupported-operation` remediation.
|
|
249
|
+
- The `explain` catalog generator skips the CLI Reference's new
|
|
250
|
+
`compare-versions` widget page (2026-10-30). The page carries no command
|
|
251
|
+
entry, and it aborted the catalog drift report. The refreshed catalog adds
|
|
252
|
+
10 published paths (`/interface/wifi/easymesh/*`,
|
|
253
|
+
`/app/settings/remove-certificate`, three `/system/resource/hardware`
|
|
254
|
+
commands), and the `/cmr/client` gates drop `!powerpc`.
|
|
204
255
|
- `retrieve --sample`, `--follow` and `--wait` no longer report a validation
|
|
205
256
|
verdict the gate never gave (#537). A cancellation before the gate ran now
|
|
206
257
|
reports the device stage `not reached` (`interrupted` for `--sample` and
|
|
@@ -277,6 +328,12 @@ part of this section.
|
|
|
277
328
|
`:for from=1 counter=i to=2` binds `i`, so later `$` references link;
|
|
278
329
|
previously the argument name was declared and a reordered `counter=` left
|
|
279
330
|
`$i` unbound.
|
|
331
|
+
- EC-SRP5 hashes shared `z` and the REDP1 loop seed minimal-length, as
|
|
332
|
+
RouterOS does (#565). centrs padded both to 32 bytes, following `mtwei.c`.
|
|
333
|
+
As a result, about 1 in 256 MAC-Telnet logins failed on a short `z`. On
|
|
334
|
+
both MAC-Telnet and btest, every login failed for an account whose salt hit
|
|
335
|
+
the REDP1 edge, about 1 in 317 password changes. btest's `z`-only workaround
|
|
336
|
+
(#516) is replaced by the shared fix.
|
|
280
337
|
- mac-telnet with an explicit directed-broadcast `--host` (`192.168.74.255`)
|
|
281
338
|
sends the owning interface's real MAC (#546, #547). On Linux, `connect()` to
|
|
282
339
|
a broadcast reports `0.0.0.0`, so the session fell back to a synthetic
|
package/GLOSSARY.txt
CHANGED
|
@@ -94,6 +94,7 @@ cwd
|
|
|
94
94
|
datagram
|
|
95
95
|
davidanson
|
|
96
96
|
DDNS
|
|
97
|
+
dsv # `:serialize to=dsv` delimiter-separated-values format (RouterOS 7.x); the #374 comma-value control pins offline acceptance of a `delimiter=,` value
|
|
97
98
|
dialable # an endpoint a client can actually connect to (advertised AND reachable)
|
|
98
99
|
defconf
|
|
99
100
|
demux
|
|
@@ -116,6 +117,7 @@ fat-chr
|
|
|
116
117
|
figcaption
|
|
117
118
|
FlaUI
|
|
118
119
|
frontends
|
|
120
|
+
frontmatter # YAML metadata block at the top of a Markdown file (e.g. instruction applyTo headers)
|
|
119
121
|
haakonnessjoen
|
|
120
122
|
Hammerspoon
|
|
121
123
|
HMAC
|
|
@@ -373,6 +375,8 @@ underflows
|
|
|
373
375
|
Werror
|
|
374
376
|
|
|
375
377
|
# MTWEI / EC-SRP vocabulary (src/protocols/mtwei.ts, mac-telnet auth)
|
|
378
|
+
# libucrypto: RouterOS's shared cryptography library; EC-SRP5 encoding source.
|
|
379
|
+
libucrypto
|
|
376
380
|
# docrypto: mtwei.c's client EC-SRP proof function (mtwei_docrypto), ported to TS.
|
|
377
381
|
docrypto
|
|
378
382
|
# redp: REDP1 (IEEE P1363.2) random-point derivation used in the MTWEI tangle.
|
|
@@ -721,8 +725,11 @@ remoteap # caps-man definition module for `/caps-man/remote-cap`
|
|
|
721
725
|
seccfg # caps-man definition module for `/caps-man/security`
|
|
722
726
|
ifaces # CLI-Reference definition module for `/tool/graphing/interface`
|
|
723
727
|
datapath # `/caps-man/datapath` — the real CLI menu behind `dpathcfg`
|
|
724
|
-
easymesh # `/interface/wifi/easymesh` —
|
|
728
|
+
easymesh # `/interface/wifi/easymesh` — once a doc spelling of `/interface/wifi/wps-push-button`; published as its own ungated menu since 2026-10-30, in no CHR tree through 7.26beta3
|
|
725
729
|
wps # Wi-Fi Protected Setup — `/interface/wifi/wps-push-button`
|
|
730
|
+
bss # Wi-Fi basic service set — `/interface/wifi/easymesh/bss`
|
|
731
|
+
dpp # Wi-Fi Device Provisioning Protocol — `/interface/wifi/easymesh/dpp-trigger`
|
|
732
|
+
overcurrent # `/system/resource/hardware/reset-overcurrent`, gated by syscap `usb-hub`
|
|
726
733
|
qos # switch-chip quality of service — `/interface/ethernet/switch/qos`, a real menu segment R1 must never drop
|
|
727
734
|
qca # Qualcomm Atheros — the `wireless-qca` package gate
|
|
728
735
|
multiswitch # syscap gate: more than one switch chip
|
|
@@ -807,6 +814,7 @@ zzznotamenu # a menu name no RouterOS build has — `/ip { zzznotamenu { print }
|
|
|
807
814
|
# #361 absent-menu controls. Deliberate NON-words: each names a path no RouterOS
|
|
808
815
|
# build has, or quotes a published gate verbatim, so the spelling is load-bearing.
|
|
809
816
|
nosuch # a top-level menu no RouterOS build has — `/nosuch` is `bad command name nosuch` on CHR 7.23.7, the `validation/unknown-path` control (#361)
|
|
817
|
+
nosuchcmd # a root command no RouterOS build has — `:local varID [nosuchcmd where name=x]` is `bad command name nosuchcmd (line 1 column 15)` on CHR 7.25 and 7.23.8, the #374 unknown-command control
|
|
810
818
|
nosuchmenu # a submenu no RouterOS build has — `/ip/nosuchmenu/print` is `syntax error (line 1 column 15)` on CHR 7.23.7, pointing at the `/` after it (#361)
|
|
811
819
|
nosuchverb # a command no RouterOS build has under a real menu — `/ip/address/nosuchverb` is `bad command name nosuchverb` on CHR 7.23.7 (#361)
|
|
812
820
|
qqq # the second segment of the parse-oracle row `/zzz qqq print`, captured on CHR 7.23.3 (GH#230)
|
package/README.md
CHANGED
|
@@ -40,7 +40,8 @@ product; without them this would just be a worse `curl`.
|
|
|
40
40
|
> Check current dist-tags with
|
|
41
41
|
> `npm view @tikoci/centrs dist-tags --json`. The library, device registry, and
|
|
42
42
|
> first MCP server phases are CHR-verified; the CLI is wired (`coded`);
|
|
43
|
-
> encrypted-CDB writes are
|
|
43
|
+
> encrypted-CDB reads and writes are supported, and the TUI/proxy frontends
|
|
44
|
+
> are planned.
|
|
44
45
|
> `docs/MATRIX.md` is the single source of truth for what works today — treat
|
|
45
46
|
> anything not green there as not-yet-shipped.
|
|
46
47
|
|
|
@@ -49,9 +50,10 @@ product; without them this would just be a worse `curl`.
|
|
|
49
50
|
- **Frontends:** TypeScript API, CLI, MCP server (Phase 1 + first Phase 2 CDB
|
|
50
51
|
mutation `CHR-passed` — see `commands/mcp/`), TUI and HTTP proxy/daemon
|
|
51
52
|
(future).
|
|
52
|
-
- **Protocols:** REST API
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
- **Protocols:** REST API and native API for reads and writes; SSH / MAC-Telnet
|
|
54
|
+
for execution and interactive terminals; SFTP for file transfer; MNDP for
|
|
55
|
+
discovery; bandwidth test for peer measurement. SNMP, RoMON and WinBox
|
|
56
|
+
Terminal are planned. centrs picks the protocol for you (and lets you pin one
|
|
55
57
|
with `via=` or `--via`).
|
|
56
58
|
- **Commands:** every router-facing command resolves a *target* — a router you
|
|
57
59
|
name on the CLI — to a real address, credentials, and a protocol, then runs.
|
|
@@ -60,12 +62,14 @@ product; without them this would just be a worse `curl`.
|
|
|
60
62
|
|
|
61
63
|
| Command | Purpose |
|
|
62
64
|
| ---------- | ------- |
|
|
63
|
-
| `retrieve` | Read RouterOS state over REST/native API
|
|
65
|
+
| `retrieve` | Read RouterOS state over REST/native API. `--follow` keeps a menu's rows current over native API (snapshot, then a line per change; NDJSON); `--sample <interval>` re-reads it on a timer for counters; `--query` filters on the router with the `print where` grammar; `--wait` retries until a read succeeds (or an optional `--until` condition holds), up to a deadline. SNMP OID/MIB reads are planned. |
|
|
64
66
|
| `execute` | Run RouterOS CLI-shaped read/write commands (add/set/remove) over native API/REST/L2 surfaces. |
|
|
65
67
|
| `api` | Structured RouterOS API passthrough (gh-api style): one command per operation, structured in/out, can write, over REST or native API. Follow changes with `api … <menu>/listen`; `--stream` streams any command's replies (native API; NDJSON). |
|
|
66
68
|
| `terminal` | Open an interactive console, primarily SSH or MAC-Telnet. |
|
|
69
|
+
| `transfer` | Upload, download and manage device files over REST/native API or SFTP; `upload` and `download` are shortcuts. |
|
|
70
|
+
| `btest` | Run a MikroTik bandwidth-test client or server over TCP/UDP. |
|
|
67
71
|
| `check` | Probe reachability and management protocol availability. **Designed, not implemented** — see `docs/MATRIX.md`. |
|
|
68
|
-
| `explain` | Analyze
|
|
72
|
+
| `explain` | Analyze RouterOS source offline, or add read-only device evidence over REST/native API, including path schemas, completion candidates and value types. Runtime acceptance remains unproven. |
|
|
69
73
|
| `devices` | View and maintain the CDB-backed device registry (the only writer). |
|
|
70
74
|
| `discover` | Discover MNDP neighbors and optionally save them into the CDB. |
|
|
71
75
|
| `settings` | Manage centrs's own settings (`centrs.env`, `__default__`) and run first-time setup. |
|
|
@@ -168,18 +172,24 @@ so you can always see *why* a given port or transport was chosen.
|
|
|
168
172
|
|
|
169
173
|
`devices` is the only command that writes the CDB, and it does so safely
|
|
170
174
|
(timestamped backup, atomic rename, unknown WinBox fields preserved verbatim).
|
|
171
|
-
Encrypted-CDB
|
|
172
|
-
|
|
175
|
+
Encrypted-CDB reads and writes work with `--cdb-password` /
|
|
176
|
+
`CENTRS_CDB_PASSWORD`; writes decrypt, mutate and re-encrypt with a fresh salt.
|
|
177
|
+
`devices init --encrypt` creates an encrypted registry, and `devices encrypt`
|
|
178
|
+
explicitly converts an existing open CDB. Conversion retains existing backups
|
|
179
|
+
and warns that plaintext copies remain. The manual WinBox round-trip is still
|
|
180
|
+
pending; see the per-command contract for the current evidence and limits.
|
|
173
181
|
The full contract — identity resolution, ambiguity handling, write strategy,
|
|
174
182
|
and the comment grammar — lives in `commands/devices/README.md` and
|
|
175
183
|
`docs/CONSTITUTION.md` (Identity and CDB).
|
|
176
184
|
|
|
177
185
|
## The result envelope, validation, and errors
|
|
178
186
|
|
|
179
|
-
- **Validation is on by default** (`validate=true`).
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
187
|
+
- **Validation is on by default** (`validate=true`). CLI-shaped `execute` and
|
|
188
|
+
`api /execute` scripts pass the offline analyzer before device validation.
|
|
189
|
+
Reads are checked through `/console/inspect`; CLI-shaped commands through
|
|
190
|
+
`[:parse]`. Set `validate=false` (per device or per call) only as a deliberate
|
|
191
|
+
escape hatch.
|
|
192
|
+
- **Every call returns one envelope:** `{ ok, data?, warnings, tips, error?, meta }`,
|
|
183
193
|
with provenance and source-of-truth reporting in `meta`.
|
|
184
194
|
- **Errors are typed values**, not thrown strings, with slash-namespaced codes
|
|
185
195
|
(`routeros/…`, `transport/…`, `validation/…`, `cdb/…`) and a `details_url`
|
|
@@ -250,7 +260,9 @@ bun run explain:value-census # value-surface census (#225)
|
|
|
250
260
|
|
|
251
261
|
## Done definition (short version)
|
|
252
262
|
|
|
253
|
-
|
|
254
|
-
real CHR via `bun run test:integration`.
|
|
255
|
-
|
|
256
|
-
|
|
263
|
+
Live capabilities require every applicable example in `commands/<name>/examples.md`
|
|
264
|
+
green on real CHR via `bun run test:integration`. Offline analysis requires its
|
|
265
|
+
command README's full baseline acceptance criteria, including committed
|
|
266
|
+
CHR-grounded semantic evidence. The fixture-backed `devices` and `settings`
|
|
267
|
+
integration suites retain their command-specific contracts. Full rule:
|
|
268
|
+
[`docs/CONSTITUTION.md` → Done definition](docs/CONSTITUTION.md#done-definition).
|
package/commands/api/AGENTS.md
CHANGED
|
@@ -87,8 +87,9 @@ stack-language access without centrs modelling the whole query grammar.
|
|
|
87
87
|
|
|
88
88
|
## CONFIRMED ON CHR (Phase 0 grounding spikes — CHR 7.23.1, 2026-06-29)
|
|
89
89
|
|
|
90
|
-
Probes:
|
|
91
|
-
`api-grounding-spike2.ts` (duration/monitor + query
|
|
90
|
+
Probes (phase-0 lab history, not runnable methods): `api-grounding-spike.ts`
|
|
91
|
+
(listen/CRUD/execute) and `api-grounding-spike2.ts` (duration/monitor + query
|
|
92
|
+
forms + inspect path). Raw
|
|
92
93
|
native codec for the listen probe; `fetch` for REST; `connectNativeApi().talk()`
|
|
93
94
|
for native.
|
|
94
95
|
|
package/commands/api/README.md
CHANGED
|
@@ -51,7 +51,14 @@ centrs api <router> <endpoint> [flags]
|
|
|
51
51
|
segment (`ip/address/*1`) addresses one row for GET/PATCH/DELETE; PUT and
|
|
52
52
|
POST reject it with `input/invalid-path`. A trailing `/listen` segment is
|
|
53
53
|
the menu's `listen` command, which implies `--stream` + `--via native-api`
|
|
54
|
-
and requires GET (`ip/address/*1/listen` follows one row).
|
|
54
|
+
and requires GET (`ip/address/*1/listen` follows one row). After that
|
|
55
|
+
normalization every menu segment must be a RouterOS menu name (letters,
|
|
56
|
+
digits, hyphens; case folds), or the request fails with
|
|
57
|
+
`input/invalid-routeros-path` before any transport is chosen, whatever
|
|
58
|
+
`--validate` says. A `#` or `?` once cut `/print` off a projected GET's
|
|
59
|
+
REST URL and posted the bare command (`/system/reboot#` rebooted CHR 7.24.5,
|
|
60
|
+
#586); the grammar and retrieve's REST guard are described in
|
|
61
|
+
[retrieve → Validation](../retrieve/README.md#validation).
|
|
55
62
|
|
|
56
63
|
The HTTP method (`-X`, default `GET`) is honored **literally** against RouterOS's
|
|
57
64
|
REST mapping — `GET`→print/get, **`PUT`→add**, `PATCH`→set, `DELETE`→remove,
|
package/commands/btest/README.md
CHANGED
|
@@ -139,6 +139,15 @@ is net-new — `mtwei.ts` only implements the client. A bad proof (or a client t
|
|
|
139
139
|
will not offer EC-SRP5 against an `authenticate=yes` server) maps to
|
|
140
140
|
`transport/auth-failed`. Legacy pre-6.43 double-MD5 is out of scope for v1.
|
|
141
141
|
|
|
142
|
+
The shared core hashes minimal-length big-endian integers in the REDP1 loop
|
|
143
|
+
and for shared-point `z`; public keys and the REDP1 input keep their fixed
|
|
144
|
+
32-byte X encoding (#565). The distinction is grounded in RouterOS
|
|
145
|
+
`libucrypto.so` 6.49.23/7.25 and `tikoci/m2ir`'s live WinBox discriminators.
|
|
146
|
+
`test/fixtures/ec-srp5.json` pins calculated regression transcripts;
|
|
147
|
+
`test/integration/ec-srp5.test.ts` forces the encoding edges against a real
|
|
148
|
+
CHR btest client, including a short `z` and a leading-zero REDP1 input
|
|
149
|
+
(confirmed on CHR 7.23.8 and 7.25).
|
|
150
|
+
|
|
142
151
|
## Output
|
|
143
152
|
|
|
144
153
|
btest runs over time, so it emits **live records** during the run plus a final
|
|
@@ -99,6 +99,13 @@ and non-zero throughput. This is the example that exercises the net-new
|
|
|
99
99
|
server-side EC-SRP5 verifier against real RouterOS (TCP keeps the gated path off
|
|
100
100
|
the SLIRP UDP-reverse-NAT question).
|
|
101
101
|
|
|
102
|
+
`test/integration/ec-srp5.test.ts` also exercises this exchange with chosen
|
|
103
|
+
test salts and ephemeral scalars, one edge per pass: a short REDP1 loop hash
|
|
104
|
+
input, a REDP1 input whose leading zero must stay, and that input again with a
|
|
105
|
+
forced short shared `z`. RouterOS's client proof must match the minimal-length
|
|
106
|
+
derivation. This discriminator stops at the
|
|
107
|
+
proof; the normal btest suites cover completed sessions and throughput (#565).
|
|
108
|
+
|
|
102
109
|
### 4. Wrong credentials are rejected
|
|
103
110
|
|
|
104
111
|
> *Designed, not yet gated* — the auth-reject path is asserted by the loopback
|
|
@@ -89,6 +89,17 @@ or a duplicated `identity=`):
|
|
|
89
89
|
lists all matching entries (by `cdbRecordIndex`, `target`, `user`, and
|
|
90
90
|
`recordType`). The caller pins the choice with `--match` by `user`
|
|
91
91
|
(`--match user=admin`), by record-type (`--match ipUser`), or by exact target.
|
|
92
|
+
`--match` takes the same selector on `devices show`/`set`/`remove` and on
|
|
93
|
+
every command that resolves a `<router>` from the CDB: `retrieve`,
|
|
94
|
+
`execute`, `api`, `transfer`, `terminal` and `btest client` (#583). It is a
|
|
95
|
+
single-target flag: with a fan-out selection or `--quickchr` it is
|
|
96
|
+
`usage/conflicting-flags`, and when no CDB record is in play (no CDB, an
|
|
97
|
+
unregistered literal host, a `__default__`-only fallback, or `--host`
|
|
98
|
+
without `<router>`) it fails `identity/no-match` rather than being ignored.
|
|
99
|
+
With no target at all (no `<router>`, no `--host`) the CLI still reports
|
|
100
|
+
the missing-target usage error first.
|
|
101
|
+
An ambiguous positional inside a fan-out names the single-target
|
|
102
|
+
`--match` re-run, since the fan-out itself refuses the flag.
|
|
92
103
|
|
|
93
104
|
`--prefer-family=ipv4|ipv6` is a non-interactive way to break v4/v6 ties
|
|
94
105
|
deterministically; with it set, no prompt is raised. It emits a warning
|
|
@@ -141,6 +152,15 @@ Rules:
|
|
|
141
152
|
- An **empty value removes** the token: `devices set <t> via=` deletes the `via`
|
|
142
153
|
override (matching the internal upsert/remove model in `comment-kv.ts`). There
|
|
143
154
|
is no current need to store a literally-empty value; revisit if one arises.
|
|
155
|
+
Removing a token that is already absent is a no-op success — the comment is
|
|
156
|
+
left byte-for-byte identical.
|
|
157
|
+
- First-class fields have their own empty-value rules, distinct from token
|
|
158
|
+
removal: `--password ""` clears the stored password (and its saved-password
|
|
159
|
+
flag); `--user ""` writes an empty user. Bare `user=` / `password=` /
|
|
160
|
+
`group=` / `profile=` / `session=` positionals never reach those fields —
|
|
161
|
+
they are refused with `cdb/reserved-key` — so comment-token removal (`key=`
|
|
162
|
+
deletes the token) and credential clearing (`--password ""` empties the
|
|
163
|
+
password) are deliberately different operations.
|
|
144
164
|
- `commentMirror` is kept in sync with `comment` on every write.
|
|
145
165
|
|
|
146
166
|
Precedence (lowest → highest): built-in defaults → user config file
|
|
@@ -340,7 +360,8 @@ runs produce stable diffs.
|
|
|
340
360
|
## Subcommands
|
|
341
361
|
|
|
342
362
|
```text
|
|
343
|
-
centrs devices init
|
|
363
|
+
centrs devices init [--encrypt] # create an empty CDB at the resolved path
|
|
364
|
+
centrs devices encrypt [--no-backup] # explicitly encrypt an existing open CDB
|
|
344
365
|
centrs devices list [--group G] [--where attr=value]
|
|
345
366
|
[--near lat,lon,radius] [--bbox south,west,north,east]
|
|
346
367
|
[--format json|yaml|text]
|
|
@@ -370,9 +391,11 @@ clack-style prompt used by `@tikoci/quickchr`); it is **not** a separate
|
|
|
370
391
|
field-editing verb, and invoking it today returns `usage/not-implemented`. There
|
|
371
392
|
is no `update`.
|
|
372
393
|
|
|
373
|
-
- `init` creates an empty open
|
|
394
|
+
- `init` creates an empty CDB (open by default, encrypted with `--encrypt`)
|
|
395
|
+
at the resolved path (`--cdb-file`,
|
|
374
396
|
`CENTRS_CDB_FILE`, or the default), with parent directories and mode `0600`.
|
|
375
|
-
It never overwrites: on an existing, readable CDB
|
|
397
|
+
It never overwrites: on an existing, readable CDB matching the encryption
|
|
398
|
+
requirement it succeeds with a
|
|
376
399
|
`cdb/file-exists` warning and writes nothing. An explicit path is created only
|
|
377
400
|
by `init`; every other subcommand reports `cdb/not-found` for it (typo
|
|
378
401
|
protection), and that error names `init`.
|
|
@@ -437,6 +460,52 @@ can edit it too. Precedence is per-field: args → env → device record →
|
|
|
437
460
|
**MCP** it never widens the allowlist — an unregistered target is still
|
|
438
461
|
rejected; `__default__` only fills missing creds for a registered device.
|
|
439
462
|
|
|
463
|
+
## CDB encryption lifecycle (#558)
|
|
464
|
+
|
|
465
|
+
Start encrypted with `devices init --encrypt`, or start open for a lab and run
|
|
466
|
+
`devices encrypt` when moving to production. Both require a non-empty
|
|
467
|
+
`--cdb-password` / `CENTRS_CDB_PASSWORD`. A password alone does not change the
|
|
468
|
+
mode of initialization or ordinary writes. The library exports
|
|
469
|
+
`initCdb({ encrypt: true, cdbPassword, cdbFile })` and
|
|
470
|
+
`encryptCdb({ cdbPassword, cdbFile })`; both use the shared settings precedence.
|
|
471
|
+
|
|
472
|
+
Encrypted initialization keeps atomic no-clobber creation and mode `0600`.
|
|
473
|
+
Initialization returns `data.encrypted` for both newly created and existing
|
|
474
|
+
files. Competing encrypted initializers with different passwords fail closed:
|
|
475
|
+
the loser gets `cdb/decrypt-failed` when it cannot unlock the winner's file.
|
|
476
|
+
An existing open CDB refuses `init --encrypt`; conversion must be explicit.
|
|
477
|
+
An existing encrypted CDB is verified with the supplied password and left alone.
|
|
478
|
+
Conversion requires an existing readable CDB, preserves its records and unknown
|
|
479
|
+
fields, uses a fresh salt and atomic replacement, and keeps its file mode.
|
|
480
|
+
`data` reports `cdbFile`, `encrypted: true`, `changed`, `recordCount`, and the
|
|
481
|
+
new `backupPath` when conversion occurred. Default text output reports
|
|
482
|
+
`encrypted` or `verified encrypted ... (unchanged)`, the record count, and
|
|
483
|
+
the backup path when created, followed by the plaintext-backup warning.
|
|
484
|
+
Already-encrypted conversion verifies
|
|
485
|
+
the password and returns `changed: false` without rewriting or rotating it.
|
|
486
|
+
|
|
487
|
+
**Backups remain sensitive.** Conversion retains every existing backup and
|
|
488
|
+
creates a plaintext snapshot of the original; it does not run backup retention.
|
|
489
|
+
The `cdb/plaintext-backups` warning accompanies conversion, including no-op runs:
|
|
490
|
+
only the active file is protected. Handle backups according to your own retention
|
|
491
|
+
policy; centrs neither encrypts nor deletes them during conversion. Subsequent
|
|
492
|
+
ordinary writes still use the normal five-backup retention below.
|
|
493
|
+
`--no-backup` (library: `skipBackup: true`) skips the new plaintext snapshot, so
|
|
494
|
+
no fresh plaintext copy is written; earlier backups are still retained and the
|
|
495
|
+
warning still applies to them. Without the snapshot, a forgotten password cannot
|
|
496
|
+
be recovered.
|
|
497
|
+
|
|
498
|
+
`init` and `encrypt` act on the CDB file, not on records: they accept only
|
|
499
|
+
`--cdb-file`, `--cdb-password`, `--format`/`--json`, plus `--encrypt` (`init`)
|
|
500
|
+
or `--no-backup` (`encrypt`). Any other flag is `input/invalid-command`
|
|
501
|
+
instead of being silently ignored.
|
|
502
|
+
|
|
503
|
+
Close WinBox and other CDB writers before conversion. If they share this path,
|
|
504
|
+
they must reopen it using the CDB password; a stale writer can overwrite the
|
|
505
|
+
conversion. Atomic replacement does not provide concurrent-writer transactions
|
|
506
|
+
(#581). Password rotation and decryption are outside this slice. WinBox manual
|
|
507
|
+
interoperability verification remains open (see Residual risks).
|
|
508
|
+
|
|
440
509
|
## CDB write strategy
|
|
441
510
|
|
|
442
511
|
All writes go through one helper (`writeWinBoxCdb`) that:
|
|
@@ -651,7 +651,7 @@ centrs devices add 192.0.2.1 --user robot-reader --cdb-file ./lab/winbox.cdb --j
|
|
|
651
651
|
```
|
|
652
652
|
|
|
653
653
|
Creates missing parent directories and an empty open CDB with mode `0600`.
|
|
654
|
-
`data` is `{ cdbFile, created: true, recordCount: 0 }`. The `add` then succeeds.
|
|
654
|
+
`data` is `{ cdbFile, created: true, encrypted: false, recordCount: 0 }`. The `add` then succeeds.
|
|
655
655
|
|
|
656
656
|
### 55. `init` never clobbers
|
|
657
657
|
|
|
@@ -660,8 +660,63 @@ centrs devices init --cdb-file ./lab/winbox.cdb --json
|
|
|
660
660
|
```
|
|
661
661
|
|
|
662
662
|
On an existing, readable CDB: `ok: true`, `data.created: false`,
|
|
663
|
+
`data.encrypted` reports the actual encryption mode,
|
|
663
664
|
`data.recordCount` is the current count, and a `cdb/file-exists` warning says
|
|
664
665
|
nothing was written. As with `git init`, re-running is safe. An existing file that
|
|
665
666
|
does not load fails with that load error (`cdb/parse-failed`,
|
|
666
667
|
`cdb/password-required`) and is left untouched. A location that cannot be
|
|
667
668
|
written fails with `cdb/create-failed`.
|
|
669
|
+
|
|
670
|
+
### 56. Initialize encrypted from the start
|
|
671
|
+
|
|
672
|
+
```bash
|
|
673
|
+
CENTRS_CDB_PASSWORD="$CDB_SECRET" centrs devices init --encrypt --cdb-file ./prod.cdb --json
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Creates a complete encrypted empty CDB with mode `0600`; subsequent loads require
|
|
677
|
+
that password. `data.encrypted` is `true`. Re-running verifies the password
|
|
678
|
+
and reports `created: false`, `encrypted: true`.
|
|
679
|
+
Test: `centrs devices encryption (#558)` in `test/integration/devices.test.ts`.
|
|
680
|
+
|
|
681
|
+
### 57. Convert a lab CDB to encrypted
|
|
682
|
+
|
|
683
|
+
```bash
|
|
684
|
+
centrs devices init --cdb-file ./lab.cdb
|
|
685
|
+
centrs devices add 192.0.2.1 --user reader --cdb-file ./lab.cdb
|
|
686
|
+
CENTRS_CDB_PASSWORD="$CDB_SECRET" centrs devices encrypt --cdb-file ./lab.cdb --json
|
|
687
|
+
CENTRS_CDB_PASSWORD="$CDB_SECRET" centrs devices list --cdb-file ./lab.cdb --json
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Conversion reports `encrypted: true`, `changed: true`, `recordCount: 1`, and
|
|
691
|
+
`backupPath`. Records survive a password-protected reload; loading without a
|
|
692
|
+
password fails. All backups remain, including the plaintext pre-conversion file;
|
|
693
|
+
`cdb/plaintext-backups` warns explicitly. Repeating with the same password returns
|
|
694
|
+
`changed: false`; a wrong password fails without writing. Without `--json`,
|
|
695
|
+
text reports conversion or verified unchanged, the record count, and the
|
|
696
|
+
plaintext snapshot path when created; the warning remains visible.
|
|
697
|
+
Test: `centrs devices encryption (#558)` in `test/integration/devices.test.ts`.
|
|
698
|
+
|
|
699
|
+
### 58. Encryption intent fails closed
|
|
700
|
+
|
|
701
|
+
`init --encrypt` and `encrypt` refuse a missing or empty CDB password before
|
|
702
|
+
creating anything. `init --encrypt` refuses an existing open CDB without changing
|
|
703
|
+
it. `--encrypt` on other subcommands and positional targets on `encrypt` are
|
|
704
|
+
invalid commands. Record, selection, and provenance flags (`--user`, `--group`,
|
|
705
|
+
`--password`, `--via`, …) on `init`/`encrypt`, and `--no-backup` on anything but
|
|
706
|
+
`encrypt`, are `input/invalid-command` rather than silently ignored. Test:
|
|
707
|
+
`centrs devices encryption (#558)` in `test/integration/devices.test.ts`.
|
|
708
|
+
|
|
709
|
+
### 59. Convert without a plaintext snapshot
|
|
710
|
+
|
|
711
|
+
```bash
|
|
712
|
+
centrs devices init --cdb-file ./lab-fresh.cdb
|
|
713
|
+
centrs devices add 192.0.2.1 --user reader --cdb-file ./lab-fresh.cdb
|
|
714
|
+
CENTRS_CDB_PASSWORD="$CDB_SECRET" centrs devices encrypt --no-backup --cdb-file ./lab-fresh.cdb --json
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
A fresh path, so it does not depend on example 57 having already converted
|
|
718
|
+
`./lab.cdb`. Conversion reports `changed: true`, `recordCount: 1`, and no
|
|
719
|
+
`backupPath`, and no new `.bak.*` file appears. The backup the `add` wrote
|
|
720
|
+
is retained, so
|
|
721
|
+
`cdb/plaintext-backups` still warns. Test: `centrs devices encryption (#558)`
|
|
722
|
+
in `test/integration/devices.test.ts`.
|
|
@@ -529,6 +529,17 @@ How it is wired:
|
|
|
529
529
|
but rejects the MD5 proof, so centrs advertises MTWEI (`src/protocols/mtwei.ts`)
|
|
530
530
|
by default. `END_AUTH` ≠ success (a failed login emits `END_AUTH` then "Login
|
|
531
531
|
failed" + `END`) → `transport/auth-failed`.
|
|
532
|
+
- **MTWEI hash encoding (grounded, #565):** RouterOS hashes shared `z` and the
|
|
533
|
+
REDP1 loop seed minimal-length; `haakonnessjoen/MAC-Telnet`'s `mtwei.c` pads
|
|
534
|
+
both to 32 bytes. When they differ, the padded proof fails. On CHR 7.25 over
|
|
535
|
+
the L2 harness (2026-10-31): short-`z` logins accepted the minimal proof 8 of
|
|
536
|
+
8 and rejected the padded proof 1 of 1 (about 700 logins). The `z` edge hits
|
|
537
|
+
about 1 in 256 logins. The REDP1 edge depends on the salt RouterOS generates
|
|
538
|
+
at each password change, about 1 in 317 changes; it fails every login until
|
|
539
|
+
the next change. A password-rotation scan over raw handshakes to PASSSALT
|
|
540
|
+
found divergent salts, and on one the minimal proof was accepted 3 of 3 while
|
|
541
|
+
the padded proof was rejected 3 of 3, all with a clean 32-byte `z` to isolate
|
|
542
|
+
the edge. The shared core is `src/protocols/ec-srp5.ts`.
|
|
532
543
|
- **Console quirks to know:** a successful write **prints no output** (no `.id`,
|
|
533
544
|
unlike REST/native — `data.ret` is empty on a successful add); and there is a
|
|
534
545
|
~10s terminal-negotiation stall on every login (a latency cost, not a
|