@tikoci/centrs 0.1.9-next.24 → 0.1.9
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 +121 -6
- package/GLOSSARY.txt +9 -1
- package/README.md +43 -29
- 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 +160 -30
- package/commands/devices/examples.md +168 -2
- package/commands/execute/README.md +13 -2
- package/commands/explain/README.md +223 -3
- package/commands/explain/examples.md +8 -1
- package/commands/retrieve/README.md +24 -3
- package/commands/retrieve/examples.md +84 -0
- package/commands/settings/README.md +6 -0
- package/commands/settings/examples.md +17 -0
- package/commands/transfer/README.md +25 -0
- package/commands/transfer/examples.md +46 -0
- package/docs/CLI.md +53 -11
- 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/empty-stdin.md +27 -0
- package/docs/errors/input/invalid-routeros-path.md +25 -3
- package/docs/errors/input/invalid-stdin.md +28 -0
- package/docs/errors/usage/stdin-is-tty.md +21 -0
- package/package.json +11 -7
- package/src/api-fanout.ts +2 -0
- package/src/api.ts +12 -0
- package/src/btest.ts +7 -0
- package/src/cli/api.ts +19 -2
- package/src/cli/btest.ts +9 -0
- package/src/cli/common.ts +1 -0
- package/src/cli/devices.ts +248 -15
- package/src/cli/execute.ts +12 -3
- package/src/cli/retrieve.ts +9 -0
- package/src/cli/settings.ts +28 -6
- package/src/cli/terminal.ts +9 -0
- package/src/cli/transfer.ts +10 -1
- package/src/core/error-catalog.ts +28 -2
- package/src/core/menu-path.ts +89 -0
- package/src/devices.ts +378 -33
- 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 +5 -0
- package/src/mcp/safety.ts +7 -6
- package/src/mcp/tools.ts +30 -68
- 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 +66 -9
- package/src/resolver/index.ts +1 -0
- package/src/resolver/quickchr-provider.ts +5 -0
- package/src/resolver/selection.ts +15 -4
- package/src/retrieve-fanout.ts +2 -0
- package/src/retrieve.ts +11 -0
- package/src/settings.ts +24 -8
- package/src/terminal.ts +7 -0
- package/src/transfer-fanout.ts +2 -0
- package/src/transfer.ts +79 -14
|
@@ -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
|
|
@@ -290,32 +310,27 @@ is no group-definition record. A group is the set of all entries whose
|
|
|
290
310
|
`group` value matches by exact, case-sensitive string compare. An entry with
|
|
291
311
|
`group=""` belongs to no group.
|
|
292
312
|
|
|
293
|
-
`--group <G>` on
|
|
313
|
+
`--group <G>` on a router-facing fan-out command resolves to the membership set.
|
|
294
314
|
`discover --save` writes MNDP records with `group=discovered`
|
|
295
315
|
unless the caller supplies another group.
|
|
296
316
|
|
|
297
317
|
### Promotion (discovered → managed)
|
|
298
318
|
|
|
299
319
|
There is no `promote` verb. Moving one device out of `group=discovered` and
|
|
300
|
-
giving it credentials
|
|
301
|
-
…`. A **bulk** promotion (a whole discovered class at once) is the same call with
|
|
302
|
-
a target selector picking the records:
|
|
320
|
+
giving it credentials uses `set` on that target:
|
|
303
321
|
|
|
304
322
|
```bash
|
|
305
|
-
devices set --group
|
|
306
|
-
--user admin --password … --force
|
|
323
|
+
centrs devices set <target> --group prod --user admin --password …
|
|
307
324
|
```
|
|
308
325
|
|
|
309
|
-
`--group`
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
setter today; `examples.md` reflects that current flag until the `--new-group`
|
|
314
|
-
split lands.)
|
|
326
|
+
On `devices add`/`set`, `--group` writes the first-class group field; on
|
|
327
|
+
`devices list` it filters. Bulk `set`/`remove` selection and a separate
|
|
328
|
+
`--new-group` flag remain designed, not implemented. Both mutations currently
|
|
329
|
+
require one target, with `--match` to disambiguate saved records.
|
|
315
330
|
|
|
316
331
|
## Fanout (multi-target invocations)
|
|
317
332
|
|
|
318
|
-
|
|
333
|
+
Router-facing fan-out commands may receive multiple positional targets, one or more `--group`
|
|
319
334
|
flags, `--all`, `--default`, `--where <attr>=<value>` (device-class), and/or the
|
|
320
335
|
geo selectors `--near <lat>,<lon>,<radius>` / `--bbox <south>,<west>,<north>,<east>`
|
|
321
336
|
(see "Geo query" above) — see `docs/CONSTITUTION.md` (Target selection grammar).
|
|
@@ -340,25 +355,29 @@ runs produce stable diffs.
|
|
|
340
355
|
## Subcommands
|
|
341
356
|
|
|
342
357
|
```text
|
|
343
|
-
centrs devices init
|
|
358
|
+
centrs devices init [--encrypt] # create an empty CDB at the resolved path
|
|
359
|
+
centrs devices encrypt [--no-backup] # explicitly encrypt an existing open CDB
|
|
344
360
|
centrs devices list [--group G] [--where attr=value]
|
|
345
361
|
[--near lat,lon,radius] [--bbox south,west,north,east]
|
|
346
362
|
[--format json|yaml|text]
|
|
347
|
-
centrs devices show <target> [--explain] [--via <protocol>] [--match <
|
|
363
|
+
centrs devices show <target> [--explain] [--via <protocol>] [--match <selector>]
|
|
364
|
+
[--show-password]
|
|
348
365
|
centrs devices groups [--members]
|
|
349
|
-
centrs devices discover [--timeout 15s] [--group G] #
|
|
350
|
-
centrs devices add <target> [--user U] [--password P] [--
|
|
366
|
+
centrs devices discover [--timeout 15s] [--group G] # designed, not implemented; use `centrs discover --save`
|
|
367
|
+
centrs devices add <target> [--user U] [--password P|--password-stdin] [--group G]
|
|
351
368
|
[--profile P|--profile-none|--profile-own]
|
|
352
369
|
[--session S] [--comment "text k=v"]
|
|
353
370
|
[--record-type ipAdmin|ipUser|macTarget|...]
|
|
354
371
|
[--gps lat,lon[,altitude[,altitude-type]]]
|
|
355
372
|
[--lat D] [--lon D] [--altitude M] [--altitude-type MSL|AGL]
|
|
356
|
-
|
|
373
|
+
[--show-password]
|
|
374
|
+
centrs devices set <target> [--match <selector>] [--user U] [--password P|--password-stdin] [--group G]
|
|
357
375
|
[--profile P|--profile-none|--profile-own]
|
|
358
376
|
[--session S] [k=v ...]
|
|
359
377
|
[--gps lat,lon[,altitude[,altitude-type]]]
|
|
360
378
|
[--lat D] [--lon D] [--altitude M] [--altitude-type MSL|AGL]
|
|
361
|
-
|
|
379
|
+
[--show-password]
|
|
380
|
+
centrs devices remove <target> [--match <selector>]
|
|
362
381
|
centrs devices edit <target> # future: clack/TUI wizard
|
|
363
382
|
```
|
|
364
383
|
|
|
@@ -370,9 +389,11 @@ clack-style prompt used by `@tikoci/quickchr`); it is **not** a separate
|
|
|
370
389
|
field-editing verb, and invoking it today returns `usage/not-implemented`. There
|
|
371
390
|
is no `update`.
|
|
372
391
|
|
|
373
|
-
- `init` creates an empty open
|
|
392
|
+
- `init` creates an empty CDB (open by default, encrypted with `--encrypt`)
|
|
393
|
+
at the resolved path (`--cdb-file`,
|
|
374
394
|
`CENTRS_CDB_FILE`, or the default), with parent directories and mode `0600`.
|
|
375
|
-
It never overwrites: on an existing, readable CDB
|
|
395
|
+
It never overwrites: on an existing, readable CDB matching the encryption
|
|
396
|
+
requirement it succeeds with a
|
|
376
397
|
`cdb/file-exists` warning and writes nothing. An explicit path is created only
|
|
377
398
|
by `init`; every other subcommand reports `cdb/not-found` for it (typo
|
|
378
399
|
protection), and that error names `init`.
|
|
@@ -400,18 +421,79 @@ is no `update`.
|
|
|
400
421
|
existing `(target, user)` it errors `cdb/already-exists` unless `--force`
|
|
401
422
|
overwrites; the same `target` under a *different* `--user` is a new record,
|
|
402
423
|
not a collision.
|
|
403
|
-
- `set` modifies
|
|
404
|
-
(`--user`, `--password`, `--
|
|
405
|
-
comment lookup/override keys change via `k=v` positionals.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
`--new-group` *writes* the group field (no overload). Against a missing target
|
|
409
|
-
it errors `cdb/not-found-target`; a multi-record `set` needs `--force`. Writing
|
|
424
|
+
- `set` modifies one matched record. First-class fields change via flags
|
|
425
|
+
(`--user`, `--password`, `--group`, `--profile[-none|-own]`, `--session`);
|
|
426
|
+
comment lookup/override keys change via `k=v` positionals. The positional
|
|
427
|
+
target and optional `--match` select the record. Against a missing target
|
|
428
|
+
it errors `cdb/not-found-target`. Writing
|
|
410
429
|
a first-class field name as a `k=v` positional (e.g. `user=x`) stays refused
|
|
411
430
|
with `cdb/reserved-key` — the flag is the only path. Unknown comment keys warn
|
|
412
431
|
(`cdb/unknown-option`), or error under `--strict`.
|
|
413
|
-
- `remove` (aliases `rm`, `delete`) removes
|
|
414
|
-
|
|
432
|
+
- `remove` (aliases `rm`, `delete`) removes one matched entry. The positional
|
|
433
|
+
target and optional `--match` select the record; group-wide deletion is not
|
|
434
|
+
implemented.
|
|
435
|
+
|
|
436
|
+
### Record password channels
|
|
437
|
+
|
|
438
|
+
`add` and `set` take the record password one of two ways:
|
|
439
|
+
|
|
440
|
+
- `--password <secret>` — argv. Simple, but `ps` can show the value while the
|
|
441
|
+
process runs, and shell history can keep the command after it exits.
|
|
442
|
+
- `--password-stdin` — read from stdin, so the value never appears in argv. For
|
|
443
|
+
automation and harnesses that spawn centrs (`printf '%s' "$PW" | centrs
|
|
444
|
+
devices set <t> --password-stdin`, or `centrs devices set <t>
|
|
445
|
+
--password-stdin < pw.txt`).
|
|
446
|
+
|
|
447
|
+
`--password-stdin` rules:
|
|
448
|
+
|
|
449
|
+
- Exactly one trailing newline (`\n` or `\r\n`) is stripped, so `echo pw |`
|
|
450
|
+
works. Any other whitespace is part of the password.
|
|
451
|
+
- The rest must be valid UTF-8 with no leading byte-order mark; otherwise it
|
|
452
|
+
is `input/invalid-stdin`. The CDB stores record strings as UTF-8 and drops a
|
|
453
|
+
leading BOM on read, so either would store a different credential than the
|
|
454
|
+
bytes piped in.
|
|
455
|
+
- Empty input (nothing, or only that one newline) is `input/empty-stdin`.
|
|
456
|
+
Clearing a stored password stays a deliberate `--password ""`, never an empty
|
|
457
|
+
pipe.
|
|
458
|
+
- A terminal on stdin is refused with `usage/stdin-is-tty`; centrs does not
|
|
459
|
+
prompt.
|
|
460
|
+
- `--password` together with `--password-stdin` is `usage/conflicting-flags`.
|
|
461
|
+
There is no precedence order between them.
|
|
462
|
+
- Only `add` and `set` accept it; every other subcommand refuses it with
|
|
463
|
+
`input/invalid-command`.
|
|
464
|
+
- The value is never echoed in errors. Success envelopes are the same as with
|
|
465
|
+
`--password`.
|
|
466
|
+
|
|
467
|
+
There is no environment-variable channel for the record password:
|
|
468
|
+
`CENTRS_PASSWORD` is the dial credential, not a CDB field.
|
|
469
|
+
|
|
470
|
+
### Record password in output
|
|
471
|
+
|
|
472
|
+
`show`, `add` and `set` withhold the stored record password by default, in text
|
|
473
|
+
and JSON (#551). Each entry always carries `passwordSet: boolean` (true when a
|
|
474
|
+
non-empty password is stored); text prints `password: (set, hidden)` or
|
|
475
|
+
`(not set)`. `--show-password` returns the cleartext `password` on those three
|
|
476
|
+
subcommands; every other subcommand refuses it with `input/invalid-command`.
|
|
477
|
+
The `--explain` record's password field reads `<redacted>` without the flag.
|
|
478
|
+
When an unknown-tcode `rawTail` comes before any decoded password field, the
|
|
479
|
+
tail may hold the password, so without the flag its `value` reads
|
|
480
|
+
`<redacted: N bytes, may contain password>` instead of the bytes. Such a
|
|
481
|
+
record decodes to an empty password, so `passwordSet` is `false` (it reports
|
|
482
|
+
the password centrs decoded and would dial with); the `cdb/unknown-field`
|
|
483
|
+
warning then carries `context.mayContainPassword: true`.
|
|
484
|
+
There is no encryption gate: the opt-in works the same on an open or encrypted
|
|
485
|
+
CDB. `passwordSet` is not the WinBox `savedPassword` ("keep password") flag.
|
|
486
|
+
|
|
487
|
+
The library matches the CLI: exported `showDevice` / `addDevice` / `setDevice`
|
|
488
|
+
redact by default and take `showPassword?: boolean` (default `false`); with
|
|
489
|
+
`showPassword: true` the entry type is `DevicesShowItemWithPassword`. MCP
|
|
490
|
+
(`centrs_devices`) never passes it, so MCP output never carries the password.
|
|
491
|
+
|
|
492
|
+
Dialing does not read this envelope. The resolver and the MCP allowlist gate
|
|
493
|
+
read the raw record through an internal lookup (`lookupDeviceRecord`, not
|
|
494
|
+
exported from the package root), so a CDB-credentialed dial still sends the
|
|
495
|
+
stored password. The public entry type has no `password` field, so a caller
|
|
496
|
+
that reached for the envelope instead fails to type-check.
|
|
415
497
|
|
|
416
498
|
`--profile <name>` sets a named WinBox profile; `--profile-none` /
|
|
417
499
|
`--profile-own` write the WinBox sentinels `<none>` / `<own>`. `profile` and
|
|
@@ -437,6 +519,54 @@ can edit it too. Precedence is per-field: args → env → device record →
|
|
|
437
519
|
**MCP** it never widens the allowlist — an unregistered target is still
|
|
438
520
|
rejected; `__default__` only fills missing creds for a registered device.
|
|
439
521
|
|
|
522
|
+
## CDB encryption lifecycle (#558)
|
|
523
|
+
|
|
524
|
+
Start encrypted with `devices init --encrypt`, or start open for a lab and run
|
|
525
|
+
`devices encrypt` when moving to production. Both require a non-empty
|
|
526
|
+
`--cdb-password` / `CENTRS_CDB_PASSWORD`. A password alone does not change the
|
|
527
|
+
mode of initialization or ordinary writes. The library exports
|
|
528
|
+
`initCdb({ encrypt: true, cdbPassword, cdbFile })` and
|
|
529
|
+
`encryptCdb({ cdbPassword, cdbFile })`; both use the shared settings precedence.
|
|
530
|
+
|
|
531
|
+
Encrypted initialization keeps atomic no-clobber creation and mode `0600`.
|
|
532
|
+
Initialization returns `data.encrypted` for both newly created and existing
|
|
533
|
+
files. Competing encrypted initializers with different passwords fail closed:
|
|
534
|
+
the loser gets `cdb/decrypt-failed` when it cannot unlock the winner's file.
|
|
535
|
+
An existing open CDB refuses `init --encrypt`; conversion must be explicit.
|
|
536
|
+
An existing encrypted CDB is verified with the supplied password and left alone.
|
|
537
|
+
Conversion requires an existing readable CDB, preserves its records and unknown
|
|
538
|
+
fields, uses a fresh salt and atomic replacement, and keeps its file mode.
|
|
539
|
+
`data` reports `cdbFile`, `encrypted: true`, `changed`, `recordCount`, and the
|
|
540
|
+
new `backupPath` when conversion occurred. Default text output reports
|
|
541
|
+
`encrypted` or `verified encrypted ... (unchanged)`, the record count, and
|
|
542
|
+
the backup path when created, followed by the plaintext-backup warning.
|
|
543
|
+
Already-encrypted conversion verifies
|
|
544
|
+
the password and returns `changed: false` without rewriting or rotating it.
|
|
545
|
+
|
|
546
|
+
**Backups remain sensitive.** Conversion retains every existing backup and
|
|
547
|
+
creates a plaintext snapshot of the original; it does not run backup retention.
|
|
548
|
+
The `cdb/plaintext-backups` warning accompanies conversion, including no-op runs:
|
|
549
|
+
only the active file is protected. Handle backups according to your own retention
|
|
550
|
+
policy; centrs neither encrypts nor deletes them during conversion. Subsequent
|
|
551
|
+
ordinary writes still use the normal five-backup retention below.
|
|
552
|
+
`--no-backup` (library: `skipBackup: true`) skips the new plaintext snapshot, so
|
|
553
|
+
no fresh plaintext copy is written; earlier backups are still retained and the
|
|
554
|
+
warning still applies to them. Without the snapshot, a forgotten password cannot
|
|
555
|
+
be recovered.
|
|
556
|
+
|
|
557
|
+
`init` and `encrypt` act on the CDB file, not on records: they accept only
|
|
558
|
+
`--cdb-file`, `--cdb-password`, `--format`/`--json`, plus `--encrypt` (`init`)
|
|
559
|
+
or `--no-backup` (`encrypt`). Any other flag is `input/invalid-command`
|
|
560
|
+
instead of being silently ignored. `devices init --help` and
|
|
561
|
+
`devices encrypt --help` show only those accepted flags; the same metadata
|
|
562
|
+
generates their sections in [`docs/CLI.md`](../../docs/CLI.md).
|
|
563
|
+
|
|
564
|
+
Close WinBox and other CDB writers before conversion. If they share this path,
|
|
565
|
+
they must reopen it using the CDB password; a stale writer can overwrite the
|
|
566
|
+
conversion. Atomic replacement does not provide concurrent-writer transactions
|
|
567
|
+
(#581). Password rotation and decryption are outside this slice. WinBox manual
|
|
568
|
+
interoperability verification remains open (see Residual risks).
|
|
569
|
+
|
|
440
570
|
## CDB write strategy
|
|
441
571
|
|
|
442
572
|
All writes go through one helper (`writeWinBoxCdb`) that:
|
|
@@ -94,7 +94,8 @@ centrs devices show 192.0.2.5 --cdb-file $CDB --explain
|
|
|
94
94
|
```
|
|
95
95
|
|
|
96
96
|
`data.record` is the raw `WinBoxCdbRecord` (tag/marker/tcode/value). Useful
|
|
97
|
-
for diagnosing unknown-field preservation.
|
|
97
|
+
for diagnosing unknown-field preservation. The password field value reads
|
|
98
|
+
`<redacted>` unless `--show-password` is passed (example 60).
|
|
98
99
|
|
|
99
100
|
## groups
|
|
100
101
|
|
|
@@ -651,7 +652,7 @@ centrs devices add 192.0.2.1 --user robot-reader --cdb-file ./lab/winbox.cdb --j
|
|
|
651
652
|
```
|
|
652
653
|
|
|
653
654
|
Creates missing parent directories and an empty open CDB with mode `0600`.
|
|
654
|
-
`data` is `{ cdbFile, created: true, recordCount: 0 }`. The `add` then succeeds.
|
|
655
|
+
`data` is `{ cdbFile, created: true, encrypted: false, recordCount: 0 }`. The `add` then succeeds.
|
|
655
656
|
|
|
656
657
|
### 55. `init` never clobbers
|
|
657
658
|
|
|
@@ -660,8 +661,173 @@ centrs devices init --cdb-file ./lab/winbox.cdb --json
|
|
|
660
661
|
```
|
|
661
662
|
|
|
662
663
|
On an existing, readable CDB: `ok: true`, `data.created: false`,
|
|
664
|
+
`data.encrypted` reports the actual encryption mode,
|
|
663
665
|
`data.recordCount` is the current count, and a `cdb/file-exists` warning says
|
|
664
666
|
nothing was written. As with `git init`, re-running is safe. An existing file that
|
|
665
667
|
does not load fails with that load error (`cdb/parse-failed`,
|
|
666
668
|
`cdb/password-required`) and is left untouched. A location that cannot be
|
|
667
669
|
written fails with `cdb/create-failed`.
|
|
670
|
+
|
|
671
|
+
### 56. Initialize encrypted from the start
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
CENTRS_CDB_PASSWORD="$CDB_SECRET" centrs devices init --encrypt --cdb-file ./prod.cdb --json
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Creates a complete encrypted empty CDB with mode `0600`; subsequent loads require
|
|
678
|
+
that password. `data.encrypted` is `true`. Re-running verifies the password
|
|
679
|
+
and reports `created: false`, `encrypted: true`.
|
|
680
|
+
Test: `centrs devices encryption (#558)` in `test/integration/devices.test.ts`.
|
|
681
|
+
|
|
682
|
+
### 57. Convert a lab CDB to encrypted
|
|
683
|
+
|
|
684
|
+
```bash
|
|
685
|
+
centrs devices init --cdb-file ./lab.cdb
|
|
686
|
+
centrs devices add 192.0.2.1 --user reader --cdb-file ./lab.cdb
|
|
687
|
+
CENTRS_CDB_PASSWORD="$CDB_SECRET" centrs devices encrypt --cdb-file ./lab.cdb --json
|
|
688
|
+
CENTRS_CDB_PASSWORD="$CDB_SECRET" centrs devices list --cdb-file ./lab.cdb --json
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
Conversion reports `encrypted: true`, `changed: true`, `recordCount: 1`, and
|
|
692
|
+
`backupPath`. Records survive a password-protected reload; loading without a
|
|
693
|
+
password fails. All backups remain, including the plaintext pre-conversion file;
|
|
694
|
+
`cdb/plaintext-backups` warns explicitly. Repeating with the same password returns
|
|
695
|
+
`changed: false`; a wrong password fails without writing. Without `--json`,
|
|
696
|
+
text reports conversion or verified unchanged, the record count, and the
|
|
697
|
+
plaintext snapshot path when created; the warning remains visible.
|
|
698
|
+
Test: `centrs devices encryption (#558)` in `test/integration/devices.test.ts`.
|
|
699
|
+
|
|
700
|
+
### 58. Encryption intent fails closed
|
|
701
|
+
|
|
702
|
+
`init --encrypt` and `encrypt` refuse a missing or empty CDB password before
|
|
703
|
+
creating anything. `init --encrypt` refuses an existing open CDB without changing
|
|
704
|
+
it. `--encrypt` on other subcommands and positional targets on `encrypt` are
|
|
705
|
+
invalid commands. Record, selection, and provenance flags (`--user`, `--group`,
|
|
706
|
+
`--password`, `--via`, …) on `init`/`encrypt`, and `--no-backup` on anything but
|
|
707
|
+
`encrypt`, are `input/invalid-command` rather than silently ignored. Test:
|
|
708
|
+
`centrs devices encryption (#558)` in `test/integration/devices.test.ts`.
|
|
709
|
+
|
|
710
|
+
### 59. Convert without a plaintext snapshot
|
|
711
|
+
|
|
712
|
+
```bash
|
|
713
|
+
centrs devices init --cdb-file ./lab-fresh.cdb
|
|
714
|
+
centrs devices add 192.0.2.1 --user reader --cdb-file ./lab-fresh.cdb
|
|
715
|
+
CENTRS_CDB_PASSWORD="$CDB_SECRET" centrs devices encrypt --no-backup --cdb-file ./lab-fresh.cdb --json
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
A fresh path, so it does not depend on example 57 having already converted
|
|
719
|
+
`./lab.cdb`. Conversion reports `changed: true`, `recordCount: 1`, and no
|
|
720
|
+
`backupPath`, and no new `.bak.*` file appears. The backup the `add` wrote
|
|
721
|
+
is retained, so
|
|
722
|
+
`cdb/plaintext-backups` still warns. Test: `centrs devices encryption (#558)`
|
|
723
|
+
in `test/integration/devices.test.ts`.
|
|
724
|
+
|
|
725
|
+
## Record password in output
|
|
726
|
+
|
|
727
|
+
`show`, `add` and `set` withhold the stored record password by default, in text
|
|
728
|
+
and JSON and in the library envelopes (#551). Every entry carries
|
|
729
|
+
`passwordSet: boolean`; `--show-password` (library: `showPassword: true`) opts in.
|
|
730
|
+
`passwordSet` is not the WinBox `savedPassword` flag. MCP never returns the
|
|
731
|
+
password and has no opt-in.
|
|
732
|
+
|
|
733
|
+
### 60. `show` withholds the password and reports `passwordSet`
|
|
734
|
+
|
|
735
|
+
```bash
|
|
736
|
+
centrs devices show 192.0.2.5 --cdb-file $CDB --explain --json
|
|
737
|
+
centrs devices show 192.0.2.5 --cdb-file $CDB
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
`data.entry` has `passwordSet: true` and no `password` key. The `--explain`
|
|
741
|
+
record's password field (tag 3) reads `<redacted>`. Text prints
|
|
742
|
+
`password: (set, hidden)`, or `(not set)` for an empty password.
|
|
743
|
+
|
|
744
|
+
### 61. `--show-password` returns the password on `show`, `add` and `set`
|
|
745
|
+
|
|
746
|
+
```bash
|
|
747
|
+
centrs devices show 192.0.2.5 --cdb-file $CDB --show-password --json
|
|
748
|
+
centrs devices add 198.51.100.57 --user writer --password pw57 --cdb-file $CDB --json
|
|
749
|
+
centrs devices set 198.51.100.57 --group lab --cdb-file $CDB --show-password --json
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
`show` returns `data.entry.password: "secret"`. The `add` without the flag
|
|
753
|
+
returns `passwordSet: true` and no `password`; the `set` with it returns
|
|
754
|
+
`data.entry.password: "pw57"`.
|
|
755
|
+
|
|
756
|
+
### 62. `--show-password` is refused outside `show` / `add` / `set`
|
|
757
|
+
|
|
758
|
+
```bash
|
|
759
|
+
centrs devices list --cdb-file $CDB --show-password --json
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
`input/invalid-command` with `error.context: { subcommand: "list", flag:
|
|
763
|
+
"--show-password" }`, the same refusal other subcommand-scoped flags use.
|
|
764
|
+
|
|
765
|
+
## Record password on stdin
|
|
766
|
+
|
|
767
|
+
`--password-stdin` (#582) reads the record password from fd 0 so it never rides
|
|
768
|
+
argv. These run as subprocesses (`test/integration/cli-process.ts`), because
|
|
769
|
+
only a child process can be handed real stdin bytes. The newline, empty and TTY
|
|
770
|
+
rules are also unit-tested in `test/unit/devices-password-stdin.test.ts`.
|
|
771
|
+
|
|
772
|
+
### 63. `add --password-stdin` stores the piped value
|
|
773
|
+
|
|
774
|
+
```bash
|
|
775
|
+
printf 'piped pw\n' | centrs devices add 192.0.2.77 --user robot --password-stdin --cdb-file $CDB --json
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
`ok: true`. The stored password is `piped pw`: one trailing newline is
|
|
779
|
+
stripped, the inner space is kept.
|
|
780
|
+
|
|
781
|
+
### 64. `set --password-stdin` strips a trailing CRLF
|
|
782
|
+
|
|
783
|
+
```bash
|
|
784
|
+
printf 'rotated\r\n' | centrs devices set 192.0.2.5 --password-stdin --cdb-file $CDB --json
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
`ok: true`. The stored password is `rotated`; group and comment are unchanged.
|
|
788
|
+
|
|
789
|
+
### 65. Empty stdin is refused
|
|
790
|
+
|
|
791
|
+
```bash
|
|
792
|
+
printf '' | centrs devices set 192.0.2.5 --password-stdin --cdb-file $CDB --json
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
Errors with `input/empty-stdin` (also for a lone `\n`). The CDB is not written
|
|
796
|
+
and no backup is made.
|
|
797
|
+
|
|
798
|
+
### 66. `--password` with `--password-stdin` conflicts
|
|
799
|
+
|
|
800
|
+
```bash
|
|
801
|
+
echo stdin-secret | centrs devices set 192.0.2.5 --password argv-secret --password-stdin --cdb-file $CDB --json
|
|
802
|
+
echo stdin-secret | centrs devices set 192.0.2.5 --password --password-stdin --cdb-file $CDB --json
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
Both error with `usage/conflicting-flags`. Neither value appears in the error;
|
|
806
|
+
`--password` cannot consume `--password-stdin` as a literal credential.
|
|
807
|
+
|
|
808
|
+
### 67. Non-UTF-8 or BOM-prefixed stdin is refused
|
|
809
|
+
|
|
810
|
+
```bash
|
|
811
|
+
printf 'pw\377\n' | centrs devices set 192.0.2.5 --password-stdin --cdb-file $CDB --json
|
|
812
|
+
printf '\357\273\277pw\n' | centrs devices set 192.0.2.5 --password-stdin --cdb-file $CDB --json
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
Both error with `input/invalid-stdin`: the first is not valid UTF-8, the second
|
|
816
|
+
starts with a byte-order mark. The bytes are not echoed, the CDB is not written
|
|
817
|
+
and no backup is made.
|
|
818
|
+
|
|
819
|
+
## File-command help
|
|
820
|
+
|
|
821
|
+
### 68. `init` and `encrypt` show only their accepted flags
|
|
822
|
+
|
|
823
|
+
```bash
|
|
824
|
+
centrs devices init --help
|
|
825
|
+
centrs devices encrypt --help
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
Both exit 0 without reading or writing a CDB. Each shows its own usage, the
|
|
829
|
+
CDB path/password and output flags, plus `--encrypt` for `init` or
|
|
830
|
+
`--no-backup` for `encrypt`. Record and selection flags are absent, including
|
|
831
|
+
`--user`, `--password-stdin`, `--show-password`, `--group` and `--match`.
|
|
832
|
+
Test: `devices file-command help is scoped without opening a CDB` in
|
|
833
|
+
`test/integration/cli-smoke.test.ts` (network-free).
|
|
@@ -146,8 +146,8 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
|
|
|
146
146
|
all-strings). Unlike `jo`-style KV builders, there is no per-value
|
|
147
147
|
type-forcing escape, because the device, not centrs, owns value typing.
|
|
148
148
|
- On an empty-CDB resolution failure, execute (like retrieve) emits the
|
|
149
|
-
`tip/no-devices` tip, steering toward `centrs
|
|
150
|
-
`centrs
|
|
149
|
+
`tip/no-devices` tip, steering toward `centrs discover --save` /
|
|
150
|
+
`centrs devices add`.
|
|
151
151
|
- Protocol surfaces are native API, REST, mac-telnet, and SSH. RoMON and
|
|
152
152
|
WinBox Terminal are later. SNMP is retrieve-only and must reject execute.
|
|
153
153
|
- For a MAC target not resolved from CDB, auto-selection defaults to
|
|
@@ -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
|