@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/GLOSSARY.txt +23 -1
  3. package/README.md +30 -18
  4. package/commands/api/AGENTS.md +21 -3
  5. package/commands/api/README.md +18 -7
  6. package/commands/api/examples.md +1 -1
  7. package/commands/btest/README.md +9 -0
  8. package/commands/btest/examples.md +7 -0
  9. package/commands/devices/README.md +72 -3
  10. package/commands/devices/examples.md +56 -1
  11. package/commands/execute/README.md +37 -6
  12. package/commands/execute/examples.md +24 -5
  13. package/commands/explain/README.md +675 -64
  14. package/commands/explain/examples.md +199 -42
  15. package/commands/retrieve/README.md +35 -3
  16. package/commands/retrieve/examples.md +114 -1
  17. package/commands/transfer/README.md +33 -4
  18. package/commands/transfer/examples.md +46 -0
  19. package/docs/CLI.md +30 -8
  20. package/docs/CONSTITUTION.md +14 -9
  21. package/docs/MATRIX.md +37 -22
  22. package/docs/errors/cdb/encrypt-failed.md +10 -0
  23. package/docs/errors/cdb/password-required.md +5 -4
  24. package/docs/errors/cdb/plaintext-backups.md +11 -0
  25. package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
  26. package/docs/errors/input/invalid-routeros-path.md +25 -3
  27. package/docs/errors/routeros/permission-denied.md +24 -0
  28. package/docs/errors/transport/edge-auth-failed.md +22 -0
  29. package/docs/errors/transport/write-outcome-unknown.md +3 -0
  30. package/docs/errors/usage/invalid-cursor.md +20 -0
  31. package/docs/errors/validation/unknown-path.md +9 -0
  32. package/package.json +9 -5
  33. package/src/api-fanout.ts +2 -0
  34. package/src/api.ts +46 -39
  35. package/src/btest.ts +7 -0
  36. package/src/cli/api.ts +18 -1
  37. package/src/cli/btest.ts +9 -0
  38. package/src/cli/devices.ts +76 -8
  39. package/src/cli/execute.ts +9 -0
  40. package/src/cli/explain.ts +206 -47
  41. package/src/cli/retrieve.ts +9 -0
  42. package/src/cli/terminal.ts +9 -0
  43. package/src/cli/transfer.ts +10 -1
  44. package/src/core/envelope.ts +10 -3
  45. package/src/core/error-catalog.ts +28 -2
  46. package/src/core/execute-command.ts +53 -0
  47. package/src/core/fanout.ts +12 -2
  48. package/src/core/inspect.ts +4 -5
  49. package/src/core/menu-path.ts +89 -0
  50. package/src/core/routeros-errors.ts +25 -0
  51. package/src/devices.ts +154 -9
  52. package/src/execute-cmr.ts +2 -8
  53. package/src/execute-fanout.ts +2 -0
  54. package/src/execute.ts +55 -20
  55. package/src/explain/args.ts +115 -10
  56. package/src/explain/blocks.ts +2 -2
  57. package/src/explain/catalog.ts +22 -12
  58. package/src/explain/group-faults.ts +162 -29
  59. package/src/explain/operators.ts +2 -1
  60. package/src/explain/pasted-prompt.ts +198 -0
  61. package/src/explain/pathresolve.ts +2 -2
  62. package/src/explain/scope-brace.ts +226 -11
  63. package/src/explain/segment.ts +138 -3
  64. package/src/explain/symbols.ts +148 -8
  65. package/src/explain/values.ts +55 -4
  66. package/src/explain/verbsplit.ts +2 -2
  67. package/src/explain/write.ts +2 -2
  68. package/src/explain-live-facets.ts +739 -0
  69. package/src/explain-live-types.ts +490 -0
  70. package/src/explain-live.ts +593 -0
  71. package/src/explain.ts +397 -43
  72. package/src/index.ts +10 -0
  73. package/src/protocols/adapter.ts +90 -10
  74. package/src/protocols/btest-session.ts +10 -30
  75. package/src/protocols/ec-srp5.ts +19 -10
  76. package/src/protocols/mac-telnet-console.ts +2 -2
  77. package/src/protocols/mac-telnet.ts +38 -10
  78. package/src/protocols/native-api.ts +13 -12
  79. package/src/resolver/cdb.ts +58 -2
  80. package/src/resolver/index.ts +1 -0
  81. package/src/resolver/quickchr-provider.ts +5 -0
  82. package/src/resolver/selection.ts +11 -0
  83. package/src/retrieve-fanout.ts +2 -0
  84. package/src/retrieve-follow.ts +2 -1
  85. package/src/retrieve-sample.ts +2 -1
  86. package/src/retrieve-wait.ts +10 -6
  87. package/src/retrieve.ts +88 -27
  88. package/src/settings.ts +1 -0
  89. package/src/terminal.ts +7 -0
  90. package/src/transfer-fanout.ts +2 -0
  91. package/src/transfer.ts +117 -27
@@ -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`.
@@ -44,7 +44,14 @@ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
44
44
  `isWriteShaped` is deliberately independent: script fallback for `[find]`, a
45
45
  positional id, or other non-`key=value` input never bypasses confirmation.
46
46
  Known read verbs are allowlisted; an unknown menu verb fails closed as a
47
- write-shaped operation. In script mode every command head in a statement
47
+ write-shaped operation. A truly bare known-menu path (`/system/identity`,
48
+ example W6) is the one exception: structured mode proves the input is a
49
+ single path token, so it is console navigation — a no-op read — rather than
50
+ a command. The device still answers truthfully (`no such command` on the
51
+ APIs) and the envelope carries a steering tip naming the read that answers.
52
+ A bare no-argument *command* (`/system/reboot`) stays gated, as does
53
+ script-branch navigation (#387), where a later line may carry a relative
54
+ verb. In script mode every command head in a statement
48
55
  (the statement start and each `[…]` or `{…}` body) must be a read: its verb
49
56
  is the first word past the known menu prefix, so `/ip address print` is a
50
57
  read and `/user expire-password print` is not (examples W3). Script
@@ -283,7 +290,14 @@ A sent write is **never retried** (#448). Fan-out retries a target only for
283
290
  when the failure came before the request was sent (for example while it was
284
291
  being validated). A write that failed after it was sent may have run, so its
285
292
  error envelope, single-target or fan-out, carries the warning
286
- `transport/write-outcome-unknown`.
293
+ `transport/write-outcome-unknown`. A multi-statement write refused with
294
+ `routeros/permission-denied` carries the same warning: statements before the
295
+ refused one may already have run (#549). Statements inside executed control
296
+ scopes (`:if … do={…}`) and command substitutions (`[…]`) count; stored bodies
297
+ (`source={…}`, `:global f do={…}`) do not, and a `/menu { … }` prefix is
298
+ navigation, not a statement. A single refused statement did not
299
+ run and stays silent, including one fed by a lone substitution
300
+ (`remove [find …]`) or wrapped in a menu block.
287
301
 
288
302
  ## CMR clients through their controller (`--cmr`)
289
303
 
@@ -361,8 +375,9 @@ when the target is itself a CMR client.
361
375
  controller's own row through `run-script`, and rebooted a client through
362
376
  both. So a "reader" login on a 7.26beta1 controller is not read-only, and
363
377
  centrs' write gate (and `mcp=ro` on MCP) is the only gate. MikroTik
364
- [says](https://forum.mikrotik.com/t/7-26beta-development-is-released/273582/139) 7.26beta2 refuses this for a `read` group; that is not yet
365
- checked on a beta2 build. For a write,
378
+ [says](https://forum.mikrotik.com/t/7-26beta-development-is-released/273582/139) 7.26beta2 refuses this for a `read` group; #527 now carries
379
+ direct 7.26beta3 evidence that a restricted caller is refused, with the
380
+ first refusing release still unbracketed. For a write,
366
381
  centrs reads the login's own `/user` row and `/user/group` once (both are
367
382
  readable with just `read` + `api`). When the login lacks `write`, `test` or
368
383
  `reboot`, the result and the confirmation error carry
@@ -514,14 +529,30 @@ How it is wired:
514
529
  but rejects the MD5 proof, so centrs advertises MTWEI (`src/protocols/mtwei.ts`)
515
530
  by default. `END_AUTH` ≠ success (a failed login emits `END_AUTH` then "Login
516
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`.
517
543
  - **Console quirks to know:** a successful write **prints no output** (no `.id`,
518
544
  unlike REST/native — `data.ret` is empty on a successful add); and there is a
519
545
  ~10s terminal-negotiation stall on every login (a latency cost, not a
520
546
  correctness issue — see `commands/terminal/README.md` for the optional fix).
521
547
  - **Delivery:** the UDP transport sends to `--host`/`--port` (default L2 broadcast
522
548
  `255.255.255.255:20561`); the device replies to our source IP:port, and the
523
- in-packet MACs do the addressing. The CHR test points `--host`/`--port` at the
524
- loopback L2 bridge.
549
+ in-packet MACs do the addressing. The device answers only broadcast delivery
550
+ (unicast gets no reply), and only when the in-packet *source* MAC is the
551
+ egress interface's real MAC — a synthetic source gets no reply on hardware
552
+ (CHR answers it, so the suite cannot catch a wrong source). With the default
553
+ host centrs sprays per-interface discovery with real MACs; with an explicit
554
+ `--host <directed-broadcast>` it resolves the owning interface's MAC. The CHR
555
+ test points `--host`/`--port` at the loopback L2 bridge.
525
556
 
526
557
  Protocol selection: an unresolved MAC target auto-selects mac-telnet (covered by
527
558
  `test/unit/execute.test.ts`); callers wanting IP-level execution opt into ARP
@@ -67,9 +67,9 @@ centrs execute $R ':local a true; :if ($a = true) do={:put [/system/identity/get
67
67
 
68
68
  Envelope: `ok: true`, `data` is string-shaped (`data.output` or `data.ret`) and
69
69
  contains the CHR identity name, `meta.via=rest-api`, and
70
- `meta.validation.syntax=true`. `meta.validation.semantic` is absent or marked
71
- `not-applicable` because `/console/inspect` validates path-shaped commands, not
72
- arbitrary script expressions. The `$a` variable must survive the validation
70
+ `meta.validation.syntax=true`. `meta.validation.semantic` is absent because
71
+ `/console/inspect` validates path-shaped commands, not arbitrary script
72
+ expressions (#535). The `$a` variable must survive the validation
73
73
  wrapper: if that outer string expands it before `:parse`, this example fails even
74
74
  though RouterOS accepts the original command.
75
75
 
@@ -490,8 +490,8 @@ Envelope: `ok: false`, `error.code=validation/package-missing`,
490
490
  `error.context.packages` names the published package, the remediation points at
491
491
  `/system/package/print` and never at the syntax, and `error.position` and
492
492
  `error.context.detail` keep RouterOS's own column and words. V5–V7 all report
493
- `meta.validation.syntax=false`. `semantic` is absent, or `not-applicable` for
494
- the script-shaped `/container print` (GH#435).
493
+ `meta.validation.syntax=false`, and `semantic` is absent because the
494
+ name-level check was never asked (GH#435, #535).
495
495
 
496
496
  ### V6. A menu no RouterOS build has is an unknown path
497
497
 
@@ -633,6 +633,25 @@ This is a grounding version, not an introduction boundary. Unknown or
633
633
  ambiguous output-file prefixes stay write-shaped until the device resolves
634
634
  them.
635
635
 
636
+ ### W6. A bare menu path needs no confirmation and answers with a steering tip
637
+
638
+ ```bash
639
+ centrs execute $R '/system/identity' --via rest-api --username $U --password $P
640
+ ```
641
+
642
+ No `--yes` gate: a truly bare known-menu path is console navigation, a no-op
643
+ read, and structured mode proves the input is a single path token with no
644
+ follow-up. The device still answers truthfully — REST reports
645
+ `routeros/request-failed`, native API `routeros/api-trap`, both carrying
646
+ RouterOS's own `no such command` — and the envelope carries
647
+ `tip/execute-bare-menu-path`, naming `/system/identity/print` (or
648
+ `centrs retrieve $R /system/identity`) as the read that answers. A bare
649
+ no-argument *command* such as `/system/reboot` stays write-shaped and still
650
+ requires confirmation.
651
+
652
+ `test/integration/execute-write-gate.test.ts` proves the gate stays open for
653
+ navigation and shut for no-arg writes, on REST and native API.
654
+
636
655
  ### R1. A post-validation `/execute` rejection fails the overall envelope
637
656
 
638
657
  ```bash