@tikoci/centrs 0.1.9-next.23 → 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 +163 -0
- package/GLOSSARY.txt +23 -1
- package/README.md +30 -18
- package/commands/api/AGENTS.md +21 -3
- package/commands/api/README.md +18 -7
- package/commands/api/examples.md +1 -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 +37 -6
- package/commands/execute/examples.md +24 -5
- package/commands/explain/README.md +675 -64
- package/commands/explain/examples.md +199 -42
- package/commands/retrieve/README.md +35 -3
- package/commands/retrieve/examples.md +114 -1
- package/commands/transfer/README.md +33 -4
- package/commands/transfer/examples.md +46 -0
- package/docs/CLI.md +30 -8
- package/docs/CONSTITUTION.md +14 -9
- package/docs/MATRIX.md +37 -22
- 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/cmr/caller-policy-not-enforced.md +5 -3
- package/docs/errors/input/invalid-routeros-path.md +25 -3
- package/docs/errors/routeros/permission-denied.md +24 -0
- package/docs/errors/transport/edge-auth-failed.md +22 -0
- package/docs/errors/transport/write-outcome-unknown.md +3 -0
- package/docs/errors/usage/invalid-cursor.md +20 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +9 -5
- package/src/api-fanout.ts +2 -0
- package/src/api.ts +46 -39
- 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/explain.ts +206 -47
- package/src/cli/retrieve.ts +9 -0
- package/src/cli/terminal.ts +9 -0
- package/src/cli/transfer.ts +10 -1
- package/src/core/envelope.ts +10 -3
- package/src/core/error-catalog.ts +28 -2
- package/src/core/execute-command.ts +53 -0
- package/src/core/fanout.ts +12 -2
- package/src/core/inspect.ts +4 -5
- package/src/core/menu-path.ts +89 -0
- package/src/core/routeros-errors.ts +25 -0
- package/src/devices.ts +154 -9
- package/src/execute-cmr.ts +2 -8
- package/src/execute-fanout.ts +2 -0
- package/src/execute.ts +55 -20
- package/src/explain/args.ts +115 -10
- package/src/explain/blocks.ts +2 -2
- package/src/explain/catalog.ts +22 -12
- package/src/explain/group-faults.ts +162 -29
- package/src/explain/operators.ts +2 -1
- package/src/explain/pasted-prompt.ts +198 -0
- package/src/explain/pathresolve.ts +2 -2
- package/src/explain/scope-brace.ts +226 -11
- package/src/explain/segment.ts +138 -3
- package/src/explain/symbols.ts +148 -8
- package/src/explain/values.ts +55 -4
- package/src/explain/verbsplit.ts +2 -2
- package/src/explain/write.ts +2 -2
- package/src/explain-live-facets.ts +739 -0
- package/src/explain-live-types.ts +490 -0
- package/src/explain-live.ts +593 -0
- package/src/explain.ts +397 -43
- package/src/index.ts +10 -0
- package/src/protocols/adapter.ts +90 -10
- 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/protocols/mac-telnet.ts +38 -10
- package/src/protocols/native-api.ts +13 -12
- 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-follow.ts +2 -1
- package/src/retrieve-sample.ts +2 -1
- package/src/retrieve-wait.ts +10 -6
- package/src/retrieve.ts +88 -27
- package/src/settings.ts +1 -0
- package/src/terminal.ts +7 -0
- package/src/transfer-fanout.ts +2 -0
- package/src/transfer.ts +117 -27
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,41 @@ 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.
|
|
32
|
+
- **Live `explain` over rest-api + native-api is `CHR-passed`** (#236).
|
|
33
|
+
Examples 7–16, 19, 24, 48 and 52–53 pass on both transports on stable
|
|
34
|
+
7.24.5 and long-term 7.23.8. `runtimeAcceptance` stays `not-proven`.
|
|
35
|
+
- `explain <router> '<input>'` and `explainLiveEnvelope()` add read-only parse,
|
|
36
|
+
highlight and command-argument evidence over REST/native API. Probe outcomes,
|
|
37
|
+
byte coverage and provenance remain explicit; analyzed input never executes (#491).
|
|
38
|
+
- Live `explain` fills the two live value facts (#225). `observedType` names a
|
|
39
|
+
type only where the device visibly converts a bare scripting literal
|
|
40
|
+
(`2.2` → `2.0.0.2` is `ip`, `0x10` → `16` is `num`), read from an escaped
|
|
41
|
+
`:parse` of the literal alone; an unchanged rendering abstains, and no
|
|
42
|
+
`:typeof` is sent. `schemaType` normalizes the argument's live `syntax`
|
|
43
|
+
declaration into leaf types (`gateway` → `ip`, `ip6`, `enum`), with the
|
|
44
|
+
device's own term in `declared`. Menu arguments are never observed, and
|
|
45
|
+
`op` has no producer.
|
|
46
|
+
- Live `explain --schema` describes the path at the cursor: a menu's verbs and
|
|
47
|
+
submenus with counts, a command's arguments with their declared syntax, and
|
|
48
|
+
`print`'s proplist. `--complete [--cursor <byte>]` returns menu, verb,
|
|
49
|
+
argument-name, `where` variable or value candidates. They are the device's
|
|
50
|
+
own input completion at the cursor, cited by `ev`; declared names and
|
|
51
|
+
syntax stay in `--schema`. `--full` lifts the sizing limits up to a safety
|
|
52
|
+
ceiling.
|
|
53
|
+
Only probe shapes that answer on RouterOS 7.12.2 and 7.20.8 are sent (#492).
|
|
54
|
+
|
|
20
55
|
- `centrs --version` / `-V` prints the package version alone; `--json` (or
|
|
21
56
|
`--format json|yaml`) returns an envelope with `data.version` (#463).
|
|
22
57
|
- `execute --cmr` warns `cmr/caller-policy-not-enforced` when a write runs
|
|
@@ -69,9 +104,47 @@ part of this section.
|
|
|
69
104
|
client, its controller.
|
|
70
105
|
- `execute --cmr` accepts a command that opens with a relative menu change
|
|
71
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`.
|
|
72
117
|
|
|
73
118
|
### Changed
|
|
74
119
|
|
|
120
|
+
- **Breaking:** permission and edge-auth failures get their own error codes
|
|
121
|
+
(#541). Three codes change for `--json` and library readers:
|
|
122
|
+
- A REST policy refusal (HTTP 500 `not enough permissions (9)` or `std
|
|
123
|
+
failure: not allowed (9)`): `transport/connection-closed` →
|
|
124
|
+
`routeros/permission-denied`. It is a deterministic router answer, so it
|
|
125
|
+
is never retried and no longer carries
|
|
126
|
+
`transport/write-outcome-unknown`.
|
|
127
|
+
- A native-API `not enough permissions (9)` trap: `routeros/api-trap` →
|
|
128
|
+
`routeros/permission-denied`. Retrieve's path probe and live explain's
|
|
129
|
+
child probe surface it instead of reading "path doesn't exist".
|
|
130
|
+
- A 401/403 from a layer in front of RouterOS (auth proxy, tunnel edge,
|
|
131
|
+
WAF): `transport/auth-failed` → `transport/edge-auth-failed`, with the
|
|
132
|
+
challenge scheme, server, content type and `cf-ray` presence as context
|
|
133
|
+
(no secrets). RouterOS's own 401 — JSON `{"error":401,...}`, measured on
|
|
134
|
+
CHR 7.12.1, 7.16.2, 7.23.8, 7.25, 7.26beta1 and 7.26beta3 — stays
|
|
135
|
+
`transport/auth-failed`.
|
|
136
|
+
- **`meta.validation` success shapes (#535).** This is a breaking change for
|
|
137
|
+
`--json` readers of the flat fields. `syntax` and `semantic` are now present
|
|
138
|
+
only when a gate stage gave that verdict; absent means no stage was asked.
|
|
139
|
+
- `api` structured requests (no `:parse`) drop `syntax: false`.
|
|
140
|
+
- Script input drops `semantic: "not-applicable"`; the value is gone from
|
|
141
|
+
the type.
|
|
142
|
+
- `--validate=false` drops both fields on `execute` and `api`.
|
|
143
|
+
- `retrieve`, `transfer` and `api` structured successes now report
|
|
144
|
+
`stages[]`. `transfer list` runs no existence probe, so it reports
|
|
145
|
+
`{ enabled: false }` instead of claiming one.
|
|
146
|
+
- `explain` reports `meta.validation: { enabled: false }` with no `result`
|
|
147
|
+
in all three cases (offline, live, error); it is not a gate.
|
|
75
148
|
- Text stdout is data (#470). In text mode, single-target `execute`, `api`,
|
|
76
149
|
`retrieve` and `transfer` write only the result to stdout. Everything else
|
|
77
150
|
goes to **stderr** on success and on failure: the `--verbose` header, one
|
|
@@ -150,6 +223,42 @@ part of this section.
|
|
|
150
223
|
|
|
151
224
|
### Fixed
|
|
152
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`.
|
|
255
|
+
- `retrieve --sample`, `--follow` and `--wait` no longer report a validation
|
|
256
|
+
verdict the gate never gave (#537). A cancellation before the gate ran now
|
|
257
|
+
reports the device stage `not reached` (`interrupted` for `--sample` and
|
|
258
|
+
`--follow`, `wait/interrupted` for `--wait`) instead of
|
|
259
|
+
`{enabled: true, result: "skipped"}`. A `--wait` cancelled after retried,
|
|
260
|
+
unanswered attempts no longer reports `result: "failed"`. With validation
|
|
261
|
+
off, all three report `{enabled: false, source: "disabled"}`.
|
|
153
262
|
- Abbreviated output-file arguments (`fi=`, `file-n=`), and the spaced
|
|
154
263
|
`file-name value` form under print/export/serialize, require write
|
|
155
264
|
confirmation in `execute`, including inside scripts, and are refused for
|
|
@@ -181,6 +290,56 @@ part of this section.
|
|
|
181
290
|
a native API login. A write that failed after it was sent, in single-target
|
|
182
291
|
or fan-out mode, carries the new warning `transport/write-outcome-unknown`,
|
|
183
292
|
because it may have run. Reads keep the retry.
|
|
293
|
+
- A bare known-menu path (`/system/identity`) reads as a read in `execute`,
|
|
294
|
+
not `usage/confirmation-required` (#543). It still answers nothing, so the
|
|
295
|
+
envelope steers to `/print` / `retrieve` with `tip/execute-bare-menu-path`.
|
|
296
|
+
Unknown paths, known-mutator verbs, and any attribute or query stay gated.
|
|
297
|
+
- `retrieve /system/routerboard` on a router without the menu (CHR) names the
|
|
298
|
+
workaround: the `validation/unknown-path` remediation points at
|
|
299
|
+
`/system/license --attributes system-id` instead of the generic text (#545).
|
|
300
|
+
- `execute` warns when a multi-statement write is refused for permission after
|
|
301
|
+
earlier statements may have run (#549). Such a refusal carries
|
|
302
|
+
`transport/write-outcome-unknown` alongside `routeros/permission-denied`,
|
|
303
|
+
counting statements inside executed `:if`/`:for`/`:foreach`/`:while`/`:do`/`:retry`/`:onerror` scopes and
|
|
304
|
+
command substitutions (`[…]`) as well as top-level ones; a `/menu { … }`
|
|
305
|
+
prefix is navigation, not a statement. Single refused statements, including
|
|
306
|
+
`remove [find …]` and one wrapped in a menu block, stay silent. The refusal itself
|
|
307
|
+
now surfaces as the error instead of `ok:true`: `/execute` answers HTTP 200
|
|
308
|
+
with `not enough permissions (9) (…; line N)` as the last `ret` line after
|
|
309
|
+
earlier statements' output (CHR 7.25, both transports).
|
|
310
|
+
- The offline gate no longer hard-rejects first-segment script-body
|
|
311
|
+
abbreviations it cannot prove (#525). `down={ :beep }`, accepted on device
|
|
312
|
+
as `down-script=`, warns `explain/canonicalizer/possible-script-body-abbreviation` and
|
|
313
|
+
reaches device validation instead of failing with `invalid-command-brace`. Bodies
|
|
314
|
+
opening with one bare command word or after a comment line warn too, and
|
|
315
|
+
the brace reads as a body for every reader, and whitespace after the `=`
|
|
316
|
+
(`down= { :beep }`) warns like the glued spelling. A bare word followed by
|
|
317
|
+
`;`-separated members keeps its error, as do the other array-content
|
|
318
|
+
braces (`comment= {1;2}` included), the space-for-`=` spelling
|
|
319
|
+
(`comment { :beep }`), positional braces, root directive slots
|
|
320
|
+
(`:put x={…}`), and `where`; `comment={ :beep }` deliberately warns though
|
|
321
|
+
the device rejects it.
|
|
322
|
+
- Native-API traps classify authentication by sentence, never trap text
|
|
323
|
+
(#540). Only a trap on `/login` is `transport/auth-failed`; a later command
|
|
324
|
+
rejected with login-like text (`/user/add name=-2` answering `invalid user
|
|
325
|
+
name`) is `routeros/command-failed`, agreeing with REST.
|
|
326
|
+
- `explain` symbols declare the value of named `name=`/`counter=` spellings in
|
|
327
|
+
any argument order (#533). `:local name=no` declares `no`, and
|
|
328
|
+
`:for from=1 counter=i to=2` binds `i`, so later `$` references link;
|
|
329
|
+
previously the argument name was declared and a reordered `counter=` left
|
|
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.
|
|
337
|
+
- mac-telnet with an explicit directed-broadcast `--host` (`192.168.74.255`)
|
|
338
|
+
sends the owning interface's real MAC (#546, #547). On Linux, `connect()` to
|
|
339
|
+
a broadcast reports `0.0.0.0`, so the session fell back to a synthetic
|
|
340
|
+
source MAC, which hardware ignores: a 30 s timeout on an RB1100Dx4 running
|
|
341
|
+
7.26beta3. On that unit a real MAC gets a reply in ~0.35 s and a synthetic
|
|
342
|
+
one none. CHR answers both, so the CHR suite never saw it.
|
|
184
343
|
|
|
185
344
|
### Maintenance
|
|
186
345
|
|
|
@@ -200,6 +359,10 @@ part of this section.
|
|
|
200
359
|
(Security tab), and locally with `bun run lint:actions` (#266). Its first
|
|
201
360
|
run found two `${{ … }}` expansions inside shell scripts, in `lab.yaml` and
|
|
202
361
|
`qa.yaml`; both now go through `env:`.
|
|
362
|
+
- The unit tier no longer reads the developer's `~/.config/tikoci` state
|
|
363
|
+
(settings, CDB) (#550, #557). A `bunfig.toml` preload sandboxes `HOME` and
|
|
364
|
+
`XDG_CONFIG_HOME`, so a real and a fake home give the same result. Ambient
|
|
365
|
+
`CENTRS_*` variables are still read.
|
|
203
366
|
|
|
204
367
|
## 0.1.8 — 2026-10-12
|
|
205
368
|
|
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)
|
|
@@ -821,3 +829,17 @@ apptraffic # two paths new in 7.26beta1 with different published gates: the `/to
|
|
|
821
829
|
rle # the corpus's `highlight-rle-v1` response encoding: each highlight stream stored as run-length `[class, count]` pairs, decoded by `decodeHighlightRuns` in scripts/corpus-db.ts (#498)
|
|
822
830
|
removenumbers # the IL head `/ip/firewall/address-list/removenumbers=…` that `remove [find …]` renders as, quoted verbatim in the bracket-`=` corroboration test (#498)
|
|
823
831
|
vscod # the last segment of a corpus path the forum importer truncated at its slug limit (`…-editors-like-vscod/post-0014-snippet-02.rsc`); cited as the path, so the spelling must match the corpus row (#498)
|
|
832
|
+
|
|
833
|
+
# #358/#371 unquoted-byte controls. Deliberate NON-words: each puts a non-ASCII
|
|
834
|
+
# byte where RouterOS reads only ASCII, so the spelling is the probe.
|
|
835
|
+
systemé # `/systemé/identity print` — a non-ASCII byte in a path, `expected command name` at the `é` on CHR 7.24.5/7.23.8 (#371)
|
|
836
|
+
printé # `/system/identity printé` — a non-ASCII byte in a verb, rejected at the `é` (#371)
|
|
837
|
+
namé # `/system/identity/set namé=x` — a non-ASCII byte in a menu command's argument name, rejected at the name's first byte (#371)
|
|
838
|
+
addré # `/ip/address/add addré=1` — as `namé`: the device reports the argument name's first byte, not the `é` (#371)
|
|
839
|
+
caf # the ASCII prefix the device reads of `café` before the `é`: `:put "a$café"` is `$caf` then `é` (#371)
|
|
840
|
+
|
|
841
|
+
# #225 live value types: CLI Reference `Type` cell spellings the schema vocabulary maps.
|
|
842
|
+
iface # the CLI Reference type `iface_enum`: an argument that names an interface, which `schemaType` calls `name` (#225)
|
|
843
|
+
ubit # the CLI Reference type `ubit (…)`: a set of listed bits, e.g. `ubit (pap, chap, mschap1, mschap2)`, a list of `enum` (#225)
|
|
844
|
+
forcounter # the IL head `/forcounter=$i;…` that `:for i …` renders as; quoted verbatim in the #225 directive-argument probe test (#225)
|
|
845
|
+
foreachcounter # the IL head `/foreachcounter=$i;…` that `:foreach i …` renders as, in any `counter=` order; quoted verbatim in the #533 reorder test (#533)
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# centrs
|
|
2
2
|
|
|
3
3
|
`centrs` is the tikoci RouterOS interaction hub: a Bun/TypeScript library and
|
|
4
|
-
CLI (with MCP
|
|
5
|
-
RouterOS devices through a regularized, validated interface.
|
|
4
|
+
CLI (with an MCP server; TUI and HTTP proxy frontends planned) for talking to
|
|
5
|
+
MikroTik RouterOS devices through a regularized, validated interface.
|
|
6
6
|
|
|
7
7
|
Preview builds are published on npm. To try the current offline analyzer without
|
|
8
8
|
installing centrs or connecting to a router:
|
|
@@ -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
|
|
|
@@ -184,7 +185,7 @@ green on CHR 7.23.1.
|
|
|
184
185
|
validity (`request=child` ⇒ `validation/unknown-attribute`) fire
|
|
185
186
|
identically over rest-api and native-api, **before** any write — no `:put
|
|
186
187
|
[:parse]` involved (api input is a path, not a CLI string). A `/execute` script
|
|
187
|
-
is a CLI string ⇒ `meta.validation.semantic
|
|
188
|
+
is a CLI string ⇒ no `meta.validation.semantic` field (#535).
|
|
188
189
|
|
|
189
190
|
## CONFIRMED ON CHR (RouterOS 7.12.2 compatibility — GH#343, 2026-09-14)
|
|
190
191
|
|
|
@@ -197,6 +198,23 @@ green on CHR 7.23.1.
|
|
|
197
198
|
validation therefore uses command-level `request=child` only. This does not
|
|
198
199
|
prohibit argument-level completion probes such as `print,proplist`, which are
|
|
199
200
|
a separate shape and returned immediately on the same 7.12.2 controls.
|
|
201
|
+
- **Command-level `request=syntax` is unsafe too, on 7.12.2 and 7.20.8** (#492,
|
|
202
|
+
2026-10-19, reproduced on a fresh boot of each).
|
|
203
|
+
- REST: `syntax` with `path=` set to a command (`ip,address,set`,
|
|
204
|
+
`system,identity,set`, `ip,route,add`, `interface,print`) got no answer
|
|
205
|
+
within 12 s, 5 of 5 times. The `child` controls queued behind it timed out
|
|
206
|
+
until the handler recovered about 30 s later.
|
|
207
|
+
- Native API closed the connection.
|
|
208
|
+
- The same syntax requests at a menu (`ip,address`) or an argument
|
|
209
|
+
(`ip,address,set,address`, `put,message`, `do,command`) answered in about
|
|
210
|
+
25 ms. 7.23.8 and 7.24.5 answer every shape.
|
|
211
|
+
- restraml's crawlers send `syntax` to `arg` nodes only, so `inspect.json`
|
|
212
|
+
never exercised the hanging shape.
|
|
213
|
+
- Input-based `completion` (`input="/ip/address add comm"`) answered at every
|
|
214
|
+
position tried on both builds, including scripting keywords and `do={`
|
|
215
|
+
bodies on 7.20.8.
|
|
216
|
+
- Explain's `--schema`/`--complete` therefore send `syntax` for argument
|
|
217
|
+
paths and `completion` with `input=` only.
|
|
200
218
|
|
|
201
219
|
## CONFIRMED ON CHR (Phase 4 `--stream` follow — CHR 7.23.1, 2026-06-30)
|
|
202
220
|
|
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,
|
|
@@ -114,7 +121,9 @@ vs `--query` distinction.
|
|
|
114
121
|
before they were sent, keep the `transport/network` /
|
|
115
122
|
`transport/connection-closed` retry. A mutating request that failed after it
|
|
116
123
|
was sent carries the warning `transport/write-outcome-unknown`, in single-target
|
|
117
|
-
and fan-out mode alike.
|
|
124
|
+
and fan-out mode alike. A multi-statement `/execute` script refused with
|
|
125
|
+
`routeros/permission-denied` carries the same warning (#549); a single
|
|
126
|
+
refused statement, and a refused structured request, stay silent.
|
|
118
127
|
- **`--stream` (or a `/listen` endpoint) is single-session** → `usage/fanout-not-supported` in
|
|
119
128
|
fan-out mode. **`--raw` strips the envelope** → `usage/conflicting-flags` in
|
|
120
129
|
fan-out mode (per-target envelopes can't be bare).
|
|
@@ -134,7 +143,8 @@ gate is `/console/inspect` — **not** `:put [:parse]`:
|
|
|
134
143
|
does) answers normally on the same build. See the 7.12.2 finding in
|
|
135
144
|
`AGENTS.md` (GH#343).
|
|
136
145
|
- **Carve-out:** a script-shaped `POST /rest/execute` is a CLI string, not a path,
|
|
137
|
-
so structured inspect
|
|
146
|
+
so structured inspect does not apply and `meta.validation.semantic` is
|
|
147
|
+
absent. It
|
|
138
148
|
follows the CLI-string pipeline instead: offline analysis, then live
|
|
139
149
|
`:put [:parse ...]`, then run. An offline rejection is still a byte span with
|
|
140
150
|
no connection; an offline pass or abstention continues to the device stage.
|
|
@@ -148,10 +158,11 @@ gate is `/console/inspect` — **not** `:put [:parse]`:
|
|
|
148
158
|
unaffected — the result matchers are anchored, not substring.
|
|
149
159
|
- Structured path requests are **not** offline-gated. The input is a path plus a
|
|
150
160
|
body, not a CLI string, and an offline *path* gate collides with GH#211's
|
|
151
|
-
unlisted-path decision.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
unlisted-path decision. `meta.validation.stages[]` reports the offline stage
|
|
162
|
+
`skipped` with that reason, on success and on a `validation/*` rejection
|
|
163
|
+
alike, so the breakdown never implies an analysis that did not run. On
|
|
164
|
+
success the device stage is `passed`, `semantic` is `true`, and there is no
|
|
165
|
+
`syntax` field, because no `:parse` ran (#535).
|
|
155
166
|
|
|
156
167
|
`--validate=false` skips **both** stages of the preflight — one flag, one meaning
|
|
157
168
|
(constitution: validation); RouterOS still re-validates writes server-side.
|
package/commands/api/examples.md
CHANGED
|
@@ -156,7 +156,7 @@ centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --use
|
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
|
|
159
|
-
`meta.validation.
|
|
159
|
+
`meta.validation.syntax=true` with no `semantic` field (script, not a path) and
|
|
160
160
|
`meta.validation.stages` is `offline: passed` then live `device: passed` from
|
|
161
161
|
`:put [:parse ...]`.
|
|
162
162
|
|
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:
|