@tikoci/centrs 0.1.0

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 (182) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/GLOSSARY.txt +420 -0
  3. package/README.md +196 -0
  4. package/SECURITY.md +30 -0
  5. package/commands/AGENTS.md +50 -0
  6. package/commands/btest/README.md +257 -0
  7. package/commands/btest/examples.md +190 -0
  8. package/commands/check/README.md +59 -0
  9. package/commands/devices/README.md +405 -0
  10. package/commands/devices/examples.md +488 -0
  11. package/commands/discover/README.md +170 -0
  12. package/commands/discover/examples.md +98 -0
  13. package/commands/execute/README.md +208 -0
  14. package/commands/execute/examples.md +353 -0
  15. package/commands/mcp/README.md +260 -0
  16. package/commands/mcp/examples.md +158 -0
  17. package/commands/retrieve/README.md +209 -0
  18. package/commands/retrieve/examples.md +306 -0
  19. package/commands/settings/README.md +69 -0
  20. package/commands/stream/README.md +70 -0
  21. package/commands/terminal/README.md +177 -0
  22. package/commands/terminal/examples.md +144 -0
  23. package/commands/transfer/README.md +315 -0
  24. package/commands/transfer/examples.md +368 -0
  25. package/docs/CONSTITUTION.md +417 -0
  26. package/docs/MATRIX.md +224 -0
  27. package/docs/errors/README.md +74 -0
  28. package/docs/errors/auth/failed.md +9 -0
  29. package/docs/errors/cdb/already-exists.md +9 -0
  30. package/docs/errors/cdb/created.md +9 -0
  31. package/docs/errors/cdb/decrypt-failed.md +9 -0
  32. package/docs/errors/cdb/empty-group.md +9 -0
  33. package/docs/errors/cdb/invalid-option.md +9 -0
  34. package/docs/errors/cdb/invalid-record.md +9 -0
  35. package/docs/errors/cdb/not-found-target.md +9 -0
  36. package/docs/errors/cdb/not-found.md +9 -0
  37. package/docs/errors/cdb/override-applied.md +9 -0
  38. package/docs/errors/cdb/parse-failed.md +9 -0
  39. package/docs/errors/cdb/password-not-needed.md +9 -0
  40. package/docs/errors/cdb/password-required.md +9 -0
  41. package/docs/errors/cdb/reserved-key.md +9 -0
  42. package/docs/errors/cdb/reserved-option.md +9 -0
  43. package/docs/errors/cdb/target-not-registered.md +9 -0
  44. package/docs/errors/cdb/unknown-field.md +9 -0
  45. package/docs/errors/cdb/unknown-option.md +9 -0
  46. package/docs/errors/cdb/write-not-permitted.md +9 -0
  47. package/docs/errors/discover/broadcast-unavailable.md +9 -0
  48. package/docs/errors/discover/failed.md +9 -0
  49. package/docs/errors/identity/ambiguous.md +9 -0
  50. package/docs/errors/identity/no-match.md +9 -0
  51. package/docs/errors/input/invalid-command.md +9 -0
  52. package/docs/errors/input/invalid-match.md +9 -0
  53. package/docs/errors/input/invalid-path.md +9 -0
  54. package/docs/errors/input/invalid-routeros-path.md +9 -0
  55. package/docs/errors/input/invalid-target.md +9 -0
  56. package/docs/errors/input/local-file-not-found.md +9 -0
  57. package/docs/errors/input/mac-address.md +9 -0
  58. package/docs/errors/input/max-results-exceeded.md +9 -0
  59. package/docs/errors/internal/devices-failed.md +9 -0
  60. package/docs/errors/internal/mcp-start.md +9 -0
  61. package/docs/errors/internal/mcp-tool.md +9 -0
  62. package/docs/errors/internal/native-api-length.md +9 -0
  63. package/docs/errors/internal/unhandled.md +9 -0
  64. package/docs/errors/internal/unreachable.md +9 -0
  65. package/docs/errors/mndp/encode-failed.md +9 -0
  66. package/docs/errors/mndp/listen-failed.md +9 -0
  67. package/docs/errors/mndp/malformed.md +9 -0
  68. package/docs/errors/routeros/api-fatal.md +9 -0
  69. package/docs/errors/routeros/api-protocol.md +9 -0
  70. package/docs/errors/routeros/api-trap.md +9 -0
  71. package/docs/errors/routeros/btest-protocol.md +12 -0
  72. package/docs/errors/routeros/btest-too-many-sessions.md +9 -0
  73. package/docs/errors/routeros/command-failed.md +14 -0
  74. package/docs/errors/routeros/ec-srp5-protocol.md +11 -0
  75. package/docs/errors/routeros/error.md +9 -0
  76. package/docs/errors/routeros/invalid-value.md +14 -0
  77. package/docs/errors/routeros/mac-telnet-error.md +9 -0
  78. package/docs/errors/routeros/mac-telnet-not-ready.md +9 -0
  79. package/docs/errors/routeros/mac-telnet-protocol.md +9 -0
  80. package/docs/errors/routeros/mac-telnet-unsupported-auth.md +9 -0
  81. package/docs/errors/routeros/protocol-not-implemented.md +9 -0
  82. package/docs/errors/routeros/request-failed.md +9 -0
  83. package/docs/errors/routeros/session-closed.md +13 -0
  84. package/docs/errors/routeros/unknown-attribute.md +13 -0
  85. package/docs/errors/routeros/unknown-path.md +15 -0
  86. package/docs/errors/routeros/unsupported-capability.md +9 -0
  87. package/docs/errors/settings/invalid-boolean.md +9 -0
  88. package/docs/errors/settings/invalid-format.md +9 -0
  89. package/docs/errors/settings/invalid-integer.md +9 -0
  90. package/docs/errors/settings/invalid-timeout.md +9 -0
  91. package/docs/errors/settings/invalid-via.md +9 -0
  92. package/docs/errors/settings/unsafe-protocol-blocked.md +9 -0
  93. package/docs/errors/target/mac-not-in-arp.md +14 -0
  94. package/docs/errors/target/mac-required.md +23 -0
  95. package/docs/errors/target/mac-unresolved.md +17 -0
  96. package/docs/errors/target/unresolved.md +9 -0
  97. package/docs/errors/transport/auth-failed.md +9 -0
  98. package/docs/errors/transport/auto-method.md +9 -0
  99. package/docs/errors/transport/capability-unsupported.md +22 -0
  100. package/docs/errors/transport/checksum-unavailable.md +9 -0
  101. package/docs/errors/transport/connection-closed.md +9 -0
  102. package/docs/errors/transport/connection-refused.md +9 -0
  103. package/docs/errors/transport/dns.md +9 -0
  104. package/docs/errors/transport/host-key-mismatch.md +9 -0
  105. package/docs/errors/transport/incomplete-transfer.md +9 -0
  106. package/docs/errors/transport/insecure-trust.md +9 -0
  107. package/docs/errors/transport/local-tool-missing.md +11 -0
  108. package/docs/errors/transport/network.md +9 -0
  109. package/docs/errors/transport/timeout.md +9 -0
  110. package/docs/errors/transport/tls-certificate.md +9 -0
  111. package/docs/errors/transport/unreachable.md +9 -0
  112. package/docs/errors/transport/unsupported-operation.md +9 -0
  113. package/docs/errors/usage/confirmation-required.md +9 -0
  114. package/docs/errors/usage/conflicting-flags.md +9 -0
  115. package/docs/errors/usage/invalid-concurrency.md +9 -0
  116. package/docs/errors/usage/missing-group.md +9 -0
  117. package/docs/errors/usage/not-implemented.md +9 -0
  118. package/docs/errors/usage/target-exists.md +9 -0
  119. package/docs/errors/usage/timeout-out-of-range.md +9 -0
  120. package/docs/errors/validation/not-implemented.md +9 -0
  121. package/docs/errors/validation/option.md +9 -0
  122. package/docs/errors/validation/syntax.md +9 -0
  123. package/docs/errors/validation/unknown-attribute.md +9 -0
  124. package/docs/errors/validation/unknown-path.md +9 -0
  125. package/package.json +96 -0
  126. package/src/AGENTS.md +7 -0
  127. package/src/btest.ts +906 -0
  128. package/src/cli/btest.ts +455 -0
  129. package/src/cli/common.ts +53 -0
  130. package/src/cli/devices.ts +527 -0
  131. package/src/cli/discover.ts +186 -0
  132. package/src/cli/execute.ts +333 -0
  133. package/src/cli/mcp.ts +109 -0
  134. package/src/cli/retrieve.ts +383 -0
  135. package/src/cli/terminal.ts +303 -0
  136. package/src/cli/transfer.ts +471 -0
  137. package/src/cli.ts +115 -0
  138. package/src/core/envelope.ts +221 -0
  139. package/src/core/error-catalog.ts +469 -0
  140. package/src/core/routeros-errors.ts +268 -0
  141. package/src/data/AGENTS.md +11 -0
  142. package/src/data/dude-db.ts +0 -0
  143. package/src/data/mndp-cache.ts +132 -0
  144. package/src/data/mndp.ts +405 -0
  145. package/src/data/winbox-cdb-write.ts +200 -0
  146. package/src/data/winbox-cdb.ts +913 -0
  147. package/src/devices-schema.ts +107 -0
  148. package/src/devices.ts +1717 -0
  149. package/src/discover.ts +802 -0
  150. package/src/errors.ts +189 -0
  151. package/src/execute.ts +1350 -0
  152. package/src/index.ts +353 -0
  153. package/src/mcp/config.ts +66 -0
  154. package/src/mcp/resources.ts +98 -0
  155. package/src/mcp/safety.ts +119 -0
  156. package/src/mcp/server.ts +225 -0
  157. package/src/mcp/tools.ts +657 -0
  158. package/src/mcp.ts +33 -0
  159. package/src/protocols/AGENTS.md +14 -0
  160. package/src/protocols/adapter.ts +866 -0
  161. package/src/protocols/btest-session.ts +1493 -0
  162. package/src/protocols/btest.ts +447 -0
  163. package/src/protocols/ec-srp5.ts +466 -0
  164. package/src/protocols/index.ts +118 -0
  165. package/src/protocols/mac-telnet-console.ts +655 -0
  166. package/src/protocols/mac-telnet.ts +1299 -0
  167. package/src/protocols/mtwei.ts +47 -0
  168. package/src/protocols/native-api.ts +796 -0
  169. package/src/protocols/sftp.ts +427 -0
  170. package/src/protocols/ssh.ts +297 -0
  171. package/src/resolver/cdb.ts +580 -0
  172. package/src/resolver/comment-kv.ts +360 -0
  173. package/src/resolver/index.ts +74 -0
  174. package/src/resolver/mac.ts +227 -0
  175. package/src/resolver/settings.ts +228 -0
  176. package/src/resolver/target.ts +442 -0
  177. package/src/retrieve-fanout.ts +533 -0
  178. package/src/retrieve.ts +1215 -0
  179. package/src/terminal.ts +444 -0
  180. package/src/transfer.ts +1554 -0
  181. package/src/tui.ts +22 -0
  182. package/src/webproxy.ts +16 -0
@@ -0,0 +1,144 @@
1
+ # terminal — examples
2
+
3
+ Each numbered example is an executable spec. The integration test under
4
+ `test/integration/terminal-mac-telnet.test.ts` runs the CHR-testable examples
5
+ (T1–T3) against a CHR booted by `@tikoci/quickchr` over a real L2 path, driving
6
+ the **real `centrs terminal` binary** through the subprocess harness
7
+ (`test/integration/cli-process.ts`) pointed at the L2 bridge's UDP relay — the
8
+ same transport the `execute / mac-telnet` command path uses.
9
+
10
+ `terminal` is a **raw passthrough**, not an enveloped command: device terminal
11
+ bytes stream to **stdout**, and any error/summary goes to **stderr** (so the two
12
+ never interleave — the same split as `transfer download -`). On a clean session
13
+ close the exit code is `0`; a pre-stream failure (bad transport, auth, no route)
14
+ emits the standard error envelope on stderr and exits `1`.
15
+
16
+ Two input modes, chosen by whether stdin is a TTY:
17
+
18
+ - **Interactive** (stdin is a TTY): raw-mode stdin → the session, device output →
19
+ stdout, the real terminal size is reported to the device, and `SIGWINCH`
20
+ forwards resizes. This is the human path; it is described in T4 and verified by
21
+ hand (a piped test cannot be a TTY).
22
+ - **Batch** (stdin is a pipe/file): bytes are forwarded as typed and the session
23
+ closes when stdin reaches EOF (after a short output drain). This is what the
24
+ harness drives — pipe a command in, read the device's response off stdout.
25
+
26
+ Conventions (provided by the harness):
27
+
28
+ - `$MAC` is the CHR target interface MAC; `$U` / `$P` are MAC-Telnet credentials.
29
+ - `$HOST` / `$UDP` point the UDP transport at the loopback L2 bridge
30
+ (`--host 127.0.0.1 --port <bridge udpPort>`), exactly as the `execute /
31
+ mac-telnet` command-path test does. A real call omits these and lets the
32
+ adapter discover the L2 route.
33
+
34
+ ## CHR-tested (T1–T3)
35
+
36
+ ### T1. Run a command over a MAC-Telnet terminal (batch relay)
37
+
38
+ Pipe a CLI line into the terminal; the device's response appears on stdout.
39
+
40
+ ```bash
41
+ printf '/system/identity/print\n' | centrs terminal $MAC --via mac-telnet --host $HOST --port $UDP --username $U --password $P
42
+ ```
43
+
44
+ stdout contains the device identity (cross-checked against REST). Exit code `0`.
45
+
46
+ ### T2. `--via rest-api` has no terminal capability
47
+
48
+ ```bash
49
+ centrs terminal $MAC --via rest-api --username $U --password $P
50
+ ```
51
+
52
+ Envelope on stderr: `ok: false`, `error.code="transport/capability-unsupported"`.
53
+ Exit code `1`. No bytes on stdout.
54
+
55
+ ### T3. `--via native-api` has no terminal capability
56
+
57
+ ```bash
58
+ centrs terminal $MAC --via native-api --username $U --password $P
59
+ ```
60
+
61
+ Same contract as T2: `transport/capability-unsupported` on stderr, exit `1`.
62
+
63
+ ## Interactive (verified by hand, not in CI)
64
+
65
+ ### T4. Open an interactive console to a MAC
66
+
67
+ ```bash
68
+ centrs terminal $MAC --username $U --password $P
69
+ ```
70
+
71
+ A MAC target with no `--via` selects `mac-telnet` (the L2 default). stdin goes
72
+ into raw mode; keystrokes reach the device and its output paints stdout. The real
73
+ terminal rows/cols are reported to the console (so it neither wraps nor paginates)
74
+ and a window resize forwards via `SIGWINCH`. `Ctrl-D` / session end exits `0`.
75
+
76
+ ## ssh (`--via ssh`)
77
+
78
+ `terminal / ssh` execs the **host `ssh`** with inherited stdio: RouterOS grants no
79
+ pseudo-tty, but `ssh user@host` opens the interactive console and the OS relays it
80
+ (the no-PTY stream is already clean — no screen emulation). centrs's value is
81
+ resolving the target/key/trust and building the argv; the interactive TTY, raw
82
+ mode, and signals are the inherited terminal's. A **host target defaults to ssh**
83
+ (a MAC target defaults to mac-telnet). Because the session is interactive, the
84
+ terminal argv omits `BatchMode=yes` (the batch sftp/execute clients keep it), so
85
+ the host `ssh` may prompt on the TTY for an encrypted key's passphrase or a
86
+ password — centrs does not forward `--password`, since OpenSSH takes no password
87
+ on the argv.
88
+
89
+ Harness inputs (from `test/integration/terminal-ssh.test.ts`): `$SSH_PORT` is
90
+ `chr.sshPort` (passed via `--port`), and `$KEY` is the ephemeral private key the
91
+ test mints with `ssh-keygen -f <tmp>/id` and passes via `--ssh-key`. **TS1–TS2 are
92
+ the CHR-tested set** (green via `bun run test:integration`, CHR 7.23.1) — the
93
+ matrix's `CHR-passed` claim rests on them. **TS3 is the interactive hand-verified
94
+ analog of T4** (a piped subprocess stdin cannot be a TTY), not in CI.
95
+
96
+ ### TS1. Run a command over an SSH terminal (batch relay)
97
+
98
+ ```bash
99
+ printf '/system/identity/print\n/quit\n' | centrs terminal 127.0.0.1 --via ssh --port $SSH_PORT --username $U --ssh-key $KEY --insecure
100
+ ```
101
+
102
+ stdout contains the device identity (cross-checked against REST). centrs returns
103
+ ssh's exit code (a no-PTY console closed by EOF can exit non-zero — that is the
104
+ device/ssh's result, not a centrs failure; a clean `/quit` from a real TTY exits
105
+ `0`).
106
+
107
+ ### TS2. `--via rest-api` has no terminal capability
108
+
109
+ ```bash
110
+ centrs terminal 127.0.0.1 --via rest-api --username $U --password $P
111
+ ```
112
+
113
+ Envelope on stderr: `ok: false`, `error.code="transport/capability-unsupported"`,
114
+ exit `1` — same gate as mac-telnet's T2/T3, evaluated before any connection.
115
+
116
+ ### TS3. Open an interactive console over SSH (verified by hand, not in CI)
117
+
118
+ ```bash
119
+ centrs terminal 192.0.2.10 --username $U --ssh-key $KEY
120
+ ```
121
+
122
+ A host target with no `--via` selects `ssh`. The inherited terminal hands the
123
+ RouterOS console to the user; `/quit` (or `Ctrl-D`) ends the session. RouterOS's
124
+ no-PTY limitation applies (e.g. multi-line brace blocks are not supported over
125
+ SSH — see the SSH page); single-line commands work.
126
+
127
+ ## MAC target over `--via ssh` resolution (behavior note — not a numbered CHR example)
128
+
129
+ This path resolves a MAC to an IP and, on the unresolved / no-opt-in branch,
130
+ fails before reaching any router — so it is **not** a numbered CHR integration
131
+ example. It is covered by the network-free smoke tier
132
+ (`test/integration/cli-smoke.test.ts`, JG-01) and `test/unit/{terminal,mac}.test.ts`.
133
+
134
+ ```bash
135
+ centrs terminal aa:bb:cc:dd:ee:ff --via ssh --username $U --json
136
+ ```
137
+
138
+ A MAC pinned to the IP transport needs an IP. With no matching CDB record and the
139
+ default `--resolve none`, the envelope on stderr is `ok: false`,
140
+ `error.code="target/mac-unresolved"`, exit `1` — and the remediation leads with
141
+ `--via mac-telnet` (reach it over L2, no IP needed). `--resolve arp` opts into the
142
+ host ARP cache instead; a miss is `target/mac-not-in-arp`. centrs never resolves a
143
+ MAC over ARP, or swaps to another transport, on its own (see
144
+ `commands/terminal/README.md` → *MAC target over SSH*).
@@ -0,0 +1,315 @@
1
+ # transfer
2
+
3
+ Copy files to and from a RouterOS device, and manage device files.
4
+
5
+ Status (agrees with `docs/MATRIX.md`): `CHR-passed` for `rest-api` / `native-api`
6
+ and for `ssh`/**sftp** (`src/transfer.ts`, `src/cli/transfer.ts`,
7
+ `src/protocols/sftp.ts`). `test/integration/transfer.test.ts` is green against a
8
+ real CHR 7.23.1 (110 assertions): the rest + native round-trip, list + filters,
9
+ validate-before-write, device file management (mkdir/copy/remove), leading-slash
10
+ normalization, the >60 KB REST-write rejection, the error contract, the
11
+ stdin/stdout/default-local forms (examples 8–10, driven through the real CLI
12
+ binary via the subprocess harness `test/integration/cli-process.ts`), the native
13
+ `N1`–`N4` mirror, the sftp `S1`–`S5` round-trip (key-auth, the >60 KB upload REST
14
+ cannot do, list/mkdir/remove), and example 17 (chunked REST read of an
15
+ sftp-seeded >60 KB file) — confirming the `/file`
16
+ `get`/`set`/`add`/`copy`/`remove` wire shapes and the SFTP subsystem on real
17
+ RouterOS. Unit coverage is `test/unit/transfer.test.ts`. **sftp** is the first SSH
18
+ consumer — a self-contained SFTP transfer client over the host OpenSSH `sftp`
19
+ subsystem. `scp` and `fetch` are **designed but deferred** past this pass.
20
+
21
+ ## Intent
22
+
23
+ `transfer` is centrs's file verb. Direction is explicit in the sub-verb —
24
+ `upload` is host → device (the WinBox "upload files" direction), `download` is
25
+ device → host. It honors the same `<router>` resolution, credentials, envelope,
26
+ and settings as every other command (see [`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md)
27
+ for identity/CDB resolution and the result envelope). Large transfers are exempt
28
+ from the REST 60 s timeout cap because they do not ride a single REST request.
29
+
30
+ centrs does not pick one method for you and hope; it picks the **cheapest method
31
+ that can carry this file in this direction**, reports the choice in
32
+ `meta.via`, and never silently crosses an explicit `--via`. See *Method
33
+ selection*.
34
+
35
+ ## Synopsis
36
+
37
+ ```text
38
+ centrs transfer <router> upload <local> [<remote>] [flags]
39
+ centrs transfer <router> download <remote> [<local>] [flags]
40
+ centrs transfer <router> list [<path>] [flags]
41
+ centrs transfer <router> remove <remote> [flags]
42
+ centrs transfer <router> mkdir <remote> [flags]
43
+ centrs transfer <router> copy <remote-src> <remote-dst> [flags] # on-device
44
+ ```
45
+
46
+ - `<router>` — IP, DNS, MAC, or CDB-resolved name. See constitution: identity.
47
+ - `<local>` — a host path, or `-` for stdin (`upload`) / stdout (`download`), so
48
+ `transfer` composes in shell pipelines. If omitted, `download` writes the
49
+ remote basename into the current directory.
50
+ - `<remote>` — a RouterOS file path (e.g. `flash/fw.rsc`). If omitted on
51
+ `upload`, centrs uses the **local basename**. *(Planned: prefer the device's
52
+ `flash` disk — `flash/<basename>` — when one is present.)*
53
+ - Remote paths match RouterOS exactly — no path rewriting. RouterOS file names
54
+ carry **no leading slash** (`/file/print` shows `flash/fw.rsc`), but the
55
+ `/file/add name=/flash/…` form *accepts* one, so centrs accepts a leading `/`
56
+ for ergonomics and **normalizes it away** to the canonical no-slash form used
57
+ on the wire (REST `/file` keys and SFTP paths both want it stripped).
58
+ `flash/fw.rsc` and `/flash/fw.rsc` are the same target.
59
+ - Reboot-persistence: on devices that have a `flash` disk, anything stored
60
+ **outside** `flash/` lives on a RAM disk and is lost on reboot — so prefer an
61
+ explicit `flash/…` remote on such devices. CHR has no `flash` disk —
62
+ everything persists. *(Planned: detect a `type=disk name=flash` row to default
63
+ omitted uploads into `flash/` and warn on an explicit non-`flash/` path;
64
+ centrs never silently relocates an explicit path.)*
65
+
66
+ `list` is thin sugar over `retrieve <router> /file` (a `/file/print` menu read),
67
+ rendered for humans (human-readable sizes, `flash/` tree, `.npk` package
68
+ metadata). For RouterOS-side row queries beyond `--type` / `--name`, use
69
+ `retrieve /file --query` directly.
70
+
71
+ ### Top-level shortcuts
72
+
73
+ `upload` and `download` are also promoted to **top-level command aliases** for the
74
+ two highest-frequency operations, so the router stays the first positional:
75
+
76
+ ```text
77
+ centrs upload <router> <local> [<remote>] [flags] → centrs transfer <router> upload <local> [<remote>]
78
+ centrs download <router> <remote> [<local>] [flags] → centrs transfer <router> download <remote> [<local>]
79
+ ```
80
+
81
+ They are pure aliases (identical flags, envelope, and validation); help and docs
82
+ show the canonical `transfer …` form, and the shared "did you mean?" / level-aware
83
+ help applies. Only `upload`/`download` are promoted — `list`/`remove`/`mkdir`/
84
+ `copy` stay under `transfer` because those verbs would be ambiguous at the top
85
+ level (every command can list or remove something).
86
+
87
+ ## Method selection
88
+
89
+ The RouterOS file plumbing is asymmetric, so the default is direction- and
90
+ size-aware. Grounding: [Files](https://help.mikrotik.com/docs/spaces/ROS/pages/2555971/Files),
91
+ [Fetch](https://help.mikrotik.com/docs/spaces/ROS/pages/8978514/Fetch).
92
+
93
+ | Method (`--via`) | Grid transport | download (device → host) | upload (host → device) |
94
+ | ---------------- | -------------- | ------------------------ | ---------------------- |
95
+ | `rest` / `native` | rest-api / native-api | small via `/file/get … contents` (**≤ 60 KB**); **large via chunked `/file/read offset chunk-size≤32768`** | **`/file/set contents=` ≤ 60 KB only** — no chunked write |
96
+ | `sftp` (default secure) | ssh | any size | any size |
97
+ | `scp` | ssh | any size | any size |
98
+ | `fetch` *(deferred)* | rest-api/native-api + inbound HTTP | router PUTs to centrs (`upload=yes http-method=put src-path=…`) | router GETs from centrs (`url=http://centrs/… dst-path=…`) |
99
+ | `ftp` *(gated)* | ftp | any size | any size |
100
+
101
+ Auto-selection (no `--via`):
102
+
103
+ - **download** — `rest`/`native` for everything (chunked `/file/read` scales to
104
+ any size with no SSH service required); fall back to `sftp` only if the REST
105
+ family is unreachable.
106
+ - **upload ≤ 60 KB** — `rest`/`native` (`/file/set contents`); one round trip, no
107
+ SSH needed.
108
+ - **upload > 60 KB** — `sftp`. The REST family cannot write past 60 KB, and
109
+ `fetch` is the only REST-family way to push a large file without SSH — but it
110
+ needs inbound reachability we can't assume, so it is **explicit-only** (see
111
+ below), never auto-selected.
112
+
113
+ Every auto hop is reported in `meta.warnings` with its reason. An explicit
114
+ `--via` is never silently downgraded: if you pin `--via rest` and ask to upload a
115
+ 2 MB file, centrs fails with `transport/unsupported-operation` (REST cannot write
116
+ past 60 KB) rather than quietly switching to sftp. `scp`, `fetch`, and `ftp` are
117
+ explicit-only; `ftp` additionally requires `ALLOW_UNSAFE_PROTOCOLS=ftp` because
118
+ it is cleartext.
119
+
120
+ > Future methods documented so callers know the capability exists, behind an
121
+ > explicit `--via`: `/system/smb` (not enabled by default) and `rose-storage`
122
+ > (`rsync`/`nfs`/`nvme-over-tcp`/`iscsi`, needs `rose-storage.npk`).
123
+
124
+ ### SFTP vs SCP (why sftp is the SSH default)
125
+
126
+ RouterOS supports both over its SSH server, and MikroTik documents both — their
127
+ container guides use `sftp admin@<router>` to push/pull config files
128
+ ([example 1](https://help.mikrotik.com/docs/spaces/ROS/pages/172294200/Container%2B-%2Bfreeradius%2Bserver),
129
+ [example 2](https://help.mikrotik.com/docs/spaces/ROS/pages/169246787/Container%2B-%2Bmosquitto%2BMQTT%2Bserver)),
130
+ while the [CHR-ProxMox guide](https://help.mikrotik.com/docs/spaces/ROS/pages/48660553/CHR%2BProxMox%2Binstallation)
131
+ uses `scp`. centrs makes **sftp the default and the primary investment**; `scp`
132
+ is an explicit `--via scp` escape hatch. The reason is capability, not taste:
133
+
134
+ - **SFTP is a real protocol** — `stat`, `readdir`, partial reads/writes, `mkdir`,
135
+ `remove`, `rename`. Those are exactly what centrs's feature set needs: the
136
+ validate-before-write existence check (`stat` the target) and the
137
+ `list`/`remove`/`mkdir` verbs all fall out of SFTP. (`--verify size` is still
138
+ supported over sftp — it just trusts the SFTP transfer guarantee rather than
139
+ re-reading a settled size, since RouterOS's `ls -l` has no reliable size column;
140
+ see *Integrity*.)
141
+ - **SCP is a dumb byte-stream** — no stat, no listing, no existence check; it just
142
+ blasts bytes and truncates on collision. An `--via scp` upload therefore
143
+ **cannot do the existence check itself** and would need a side-channel REST/SFTP
144
+ `stat` anyway — so `--via scp` skips the refuse-overwrite guard unless REST is
145
+ also reachable, and `--verify` degrades to `off` (warned). scp buys only a
146
+ marginally lighter single-file stream, irrelevant under the CHR 1 Mb/s cap.
147
+ - **Direction of the ecosystem** — OpenSSH deprecated the legacy SCP wire protocol
148
+ (9.0, 2022); modern `scp` rides SFTP underneath, so the reliable SSH file channel
149
+ is the **SFTP subsystem**, not an exec-driven copy. (RouterOS's SSH server has no
150
+ pseudo-tty, but a single-line `ssh user@host "<command>"` *does* run and return
151
+ clean output — that path drives `execute / ssh`, see `commands/execute/README.md`;
152
+ it is not a file-transfer channel.)
153
+
154
+ ## Flags
155
+
156
+ | Flag | Behavior |
157
+ | ---- | -------- |
158
+ | `--via <method>` | Pin the method: `sftp` (built; secure default for large transfers), `rest`, `native`, or `scp` *(deferred)* / `fetch` *(deferred)* / `ftp` *(gated)*. No silent downgrade. See *Method selection* and constitution: protocol selection. |
159
+ | `--force` (alias `--overwrite`) | Replace an existing target. Default **refuses** an existing target with `usage/target-exists` (mirrors how `add` refuses an existing record). Applies to the destination side: the local file for `download`, the remote file for `upload`. |
160
+ | `--verify <size\|checksum\|off>` | Post-transfer integrity check. Default `size` (compare the byte count centrs sent/received against the settled `/file` size). `off` (alias `--no-verify`) skips it. See *Integrity*. |
161
+ | `--type <file\|directory\|disk\|package>` | `list` only: filter by `/file` row type. |
162
+ | `--name <glob>` | `list` only: filter by file name glob. |
163
+ | `--out-dir <dir>` | `download` fanout only *(deferred — fanout not built yet)*: write one file per target into `<dir>` (named by CDB target), since N devices cannot share one local path. |
164
+ | `--advertise-host <host>` / `--advertise-port <n>` / `--bind <addr>` | `fetch` only *(deferred)*: the host/IP/port centrs advertises in the fetch URL and the local bind address. Default: auto-detect the local IP on the route to the router; ephemeral port; single-use random URL token. |
165
+ | `--resolve <none\|arp>` | *(deferred — not parsed yet)* Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
166
+ | `--format text` (default) | Human summary: method, bytes, duration, verified. Errors print `[code] summary` + `Fix:` lines. |
167
+ | `--format json` (alias `--json`) / `--format yaml` (alias `--yaml`) | Structured envelope. `CENTRS_FORMAT=json` makes JSON the default. |
168
+ | `--validate=false` | Escape hatch; default `true`. See constitution: validation is the product. |
169
+ | `--timeout <ms>` | Request timeout. `rest`/`native` per-request ≤ 60000 (a chunked read is many short requests, each capped); `sftp`/`scp`/`fetch` accept longer for a single large transfer. |
170
+ | `--username` / `--password` | Override CDB-resolved or env credentials. |
171
+ | `--host <host\|url>` / `--port <n>` | Override the resolved host / transport port. |
172
+ | `--ssh-key <path>` | `sftp` (and future `scp`): explicit private-key path. Same `sshKey` setting as `terminal`/`execute` (`CENTRS_SSH_KEY`, CDB `ssh-key=`). When unset, the ssh-agent / `~/.ssh/config` is used. See `commands/terminal/README.md`. |
173
+ | `--insecure` | Accept a self-signed TLS cert (`https`/`api-ssl`) or a new SSH host key (TOFU). Default verifies; see constitution: transport trust. Adds a `transport/insecure-trust` warning. |
174
+ | `--cdb-file` / `--cdb-password` | Override CDB file location / decrypt password. |
175
+ | `--group <name>` / `--where <attr>=<value>` / `--all` / `--default` / `--concurrency <n>` | *(deferred — fanout not built for `transfer` yet)* Fan out across CDB targets (e.g. push one firmware file to a fleet). Same selector grammar and per-target envelope as `retrieve` (constitution: target selection). `download` fanout requires `--out-dir`; `remove`/`mkdir`/`copy`/`upload` fan out directly. |
176
+
177
+ ## Integrity
178
+
179
+ `--verify size` (default) over `rest`/`native` reads the settled `/file` size
180
+ after the transfer and compares it to the byte count centrs moved. Over **sftp**,
181
+ RouterOS's `ls -l` reports no reliable byte size (CHR 7.23.1), so `--verify size`
182
+ instead trusts the SFTP transfer guarantee — a partial `put`/`get` errors — rather
183
+ than re-reading a size. Two RouterOS facts shape the `/file`-size path:
184
+
185
+ - **NAND write-back** delays the flush up to ~40 s on some devices, so the size
186
+ may read short immediately after an upload. centrs does a single post-write
187
+ `/file/print` size probe today. *(Planned: a brief poll/retry to absorb the
188
+ write-back window before declaring a mismatch.)*
189
+ - **Transparent compression**: the `/file` menu reports the *uncompressed* size
190
+ (compression only shows up in `/system/resource` free space), so size-compare
191
+ against the uncompressed byte count we sent stays valid.
192
+
193
+ A confirmed shortfall (clear truncation after the poll window) is
194
+ `transport/incomplete-transfer`. `--verify checksum` is stronger but only where
195
+ RouterOS exposes a usable digest; otherwise it degrades to `size` with a warning.
196
+
197
+ ## Output shape
198
+
199
+ The downloaded/uploaded bytes go to disk (or stdout for `download -`); the
200
+ envelope carries transfer *metadata*, not file contents.
201
+
202
+ ```ts
203
+ {
204
+ ok: true,
205
+ data: {
206
+ op: "upload", // upload | download | list | remove | mkdir | copy
207
+ remote: "flash/fw.rsc",
208
+ local: "./fw.rsc", // null for list/remove/mkdir/copy
209
+ bytes: 1048576,
210
+ verified: "size", // size | checksum | off
211
+ durationMs: 1840
212
+ },
213
+ warnings: [], // e.g. auto-method hops, insecure-trust
214
+ meta: { target, via: "ssh", settings, validation, timing } // via is the grid transport: rest-api | native-api | ssh
215
+ }
216
+ ```
217
+
218
+ `list` returns `data` as the `/file` row array (same shape as
219
+ `retrieve /file`). Fanout returns the shared one-outer-envelope form documented
220
+ in `commands/retrieve/README.md` (Group fanout): `ok` reports orchestration
221
+ success, per-target results live in `data.targets`.
222
+
223
+ ## Validation
224
+
225
+ `transfer` validates what it can locally first — conflicting flags
226
+ (`usage/conflicting-flags`), a missing local source on `upload`, a `--via rest`
227
+ upload that exceeds 60 KB (`transport/unsupported-operation`). The
228
+ **refuse-overwrite** guard is a real precondition probe, not a local check:
229
+ unless `--force`, centrs `stat`s the destination (SFTP `stat`, or `/file/print`
230
+ for the REST family) and fails with `usage/target-exists` if it is already there.
231
+ This is the "validate before write" gate applied to files — see constitution:
232
+ validation is the product.
233
+
234
+ The `/file` menu path is fixed, so there is no `/console/inspect` path/attribute
235
+ gate as in `retrieve` / `execute`; RouterOS's own menu errors (missing remote
236
+ file, permission denied) map to `routeros/*`, and SFTP/SCP/fetch errors map to
237
+ `transport/*` and `auth/*`.
238
+
239
+ **Error codes reuse existing namespaces — `transfer` adds no namespace of its
240
+ own.** Everything specific to transfer fits `usage/*` (`usage/target-exists`),
241
+ `transport/*` (`transport/unsupported-operation` for the REST 60 KB write cap,
242
+ `transport/incomplete-transfer` for a verified shortfall, `transport/unreachable`
243
+ for fetch inbound-reachability), `routeros/*`, `auth/*`, and `validation/*`. A new
244
+ code is only justified where the *semantics* are genuinely file-transfer-specific
245
+ and not already expressible in those namespaces.
246
+
247
+ ## Decided (2026-06-10)
248
+
249
+ - **Verbs are `upload` / `download` / `list`** (+ `remove` / `mkdir` / `copy` for
250
+ device file management). Direction is explicit in the sub-verb; `upload` =
251
+ host → device per WinBox wording. **`copy` needs RouterOS ≥ 7.23beta2**:
252
+ server-side copy rides the REST `/file/copy` endpoint, first seen in 7.23beta2;
253
+ earlier RouterOS (e.g. 7.21.x long-term) returns `no such command` (the
254
+ integration test gates the `copy` example on the running version).
255
+ - **Method selection is size/direction-aware** (table above), superseding the
256
+ earlier "sftp is always the default" decision. REST/native carry small writes
257
+ and all reads (reads scale via chunked `/file/read`); sftp carries large
258
+ uploads. This updates the constitution's protocol-selection row.
259
+ - **sftp is the SSH default, scp is `--via scp` only** — sftp's `stat`/`readdir`/
260
+ partial ops are what enable the existence check, `--verify`, and
261
+ `list`/`remove`/`mkdir`; scp can't do those and degrades the overwrite guard and
262
+ `--verify`. See *SFTP vs SCP*.
263
+ - **Remote paths match RouterOS** (no rewriting); a leading `/` is accepted but
264
+ normalized away to the canonical no-slash wire form. The omitted-`<remote>`
265
+ default uses `flash/` on devices that have a flash disk.
266
+ - **`upload` / `download` are also top-level command aliases** (`centrs upload
267
+ <router> …`), the only sub-verbs promoted to the top level.
268
+ - **Error codes reuse existing namespaces** — no `transfer/*` namespace; new codes
269
+ only where the semantics are genuinely transfer-specific.
270
+ - **`fetch` (centrs-as-HTTP-server + `/tool/fetch`) is explicit-only and
271
+ deferred** — designed here, built after sftp + REST land. It is the only
272
+ REST-family way to push a > 60 KB file without SSH, but needs inbound
273
+ reachability (router → centrs), so it is never auto-selected.
274
+ - **v1 includes** device file management (`remove`/`mkdir`/`copy`), integrity
275
+ verification (`--verify`), and stdout/stdin piping (`-`). **Directory /
276
+ recursive transfer is a later phase.**
277
+ - **CI proof is a small-file round-trip**, not a throughput stress test — the
278
+ free CHR license caps throughput at 1 Mb/s.
279
+
280
+ ## Definition of done
281
+
282
+ `CHR-passed` only when every line in `examples.md` runs green against a real CHR
283
+ through `bun run test:integration` (one example ↔ one assertion). Disabling
284
+ validation to reach green is forbidden. See `docs/CONSTITUTION.md` for the full
285
+ done rule. The first coded pass targets `rest`/`native` (no SSH dependency);
286
+ `sftp`/`scp` advance with the SSH transport; `fetch` and `ftp` follow.
287
+
288
+ ## Notes for future cells
289
+
290
+ - **fetch design (deferred).** centrs starts an ephemeral single-use HTTP server
291
+ (random URL token, bound to the interface facing the router) and drives the
292
+ router with `/tool/fetch` over `rest`/`native`. **upload**: serve the local
293
+ file, `fetch url=http://<advertise>/<token> dst-path=<remote>` → router GETs.
294
+ **download**: `fetch upload=yes http-method=put url=http://<advertise>/<token>
295
+ src-path=<remote>` → router PUTs to centrs (`http-method` any since 7.21;
296
+ binary REST serialization fixed 7.17). Open questions: advertise-host
297
+ auto-detection behind NAT, HTTP-vs-HTTPS + `check-certificate`, and whether to
298
+ add fetch's `user`/`password` on top of the URL token.
299
+ - **ssh** — `sftp` is **built** (`src/protocols/sftp.ts`, host OpenSSH `sftp`
300
+ subsystem; `scp` is the deferred follow-on). The host SSH client owns
301
+ host-key/agent/algorithm negotiation; the centrs-side trust knob is the unified
302
+ `--insecure` (constitution: transport trust). Two RouterOS auth facts transfer
303
+ inherits, grounded on the [SSH](https://help.mikrotik.com/docs/spaces/ROS/pages/132350014/SSH)
304
+ / [User](https://help.mikrotik.com/docs/spaces/ROS/pages/8978504/User) pages:
305
+ **by default** a user cannot password-auth over SSH once an SSH key is set for it
306
+ (`/ip/ssh password-authentication=yes-if-no-key`, the default — settable to
307
+ `yes` to allow both), so key auth is the normal path; and `strong-crypto=yes`
308
+ disables ssh-rsa/SHA1, so the client must offer ed25519 / rsa-sha2-256 (the host
309
+ OpenSSH negotiates this natively). The SSH file path is the SFTP **subsystem**
310
+ (a real file protocol), not exec-driven scp; on-device `copy`
311
+ has no SFTP primitive and stays on rest/native (`--via sftp copy` →
312
+ `transport/unsupported-operation`). See `commands/terminal/README.md` (RouterOS
313
+ SSH surface) for the full device-side option alignment.
314
+ - **list ↔ retrieve** — `transfer list` is sugar over `retrieve /file`; keep the
315
+ read logic in one place.