@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,368 @@
1
+ # transfer — examples
2
+
3
+ Each numbered example is an executable spec. The integration test under
4
+ `test/integration/transfer.test.ts` runs every example against a CHR booted by
5
+ `@tikoci/quickchr`. Examples 8–10 (stdin/stdout/default-local) run through the
6
+ subprocess harness (`test/integration/cli-process.ts`), which drives the real CLI
7
+ binary so piped stdin, raw stdout bytes, and cwd are exercised — the in-process
8
+ `runCli` console capture cannot see those. If a line here is not exercised by a
9
+ test, the test file is wrong; if a line passes only with `validate=false`, the
10
+ **implementation** is wrong (see `docs/CONSTITUTION.md`).
11
+
12
+ Conventions (provided by the harness):
13
+
14
+ - `$R` is `<host>:<rest-port>`; `$U` / `$P` are CHR credentials.
15
+ - `$A` is `<host>` and `$API_PORT` is the native-API port (`chr.ports.api`) for
16
+ `--via native-api`.
17
+ - `$SRC` is a small local text file (~2 KB) the harness creates; `$TMP` is a
18
+ scratch directory for downloads. `$BIG` is a local file **larger than 60 KB**.
19
+ - Device-side paths are **bare** (a CHR has no `flash` disk, so everything
20
+ persists for the session); the harness removes `centrs-*` test files between
21
+ runs. The CHR free license caps throughput at 1 Mb/s, so fixtures stay small —
22
+ the proof is correctness (a byte-exact round-trip), not throughput.
23
+
24
+ The first coded pass is `rest`/`native` only (no SSH dependency). `sftp`/`scp`/
25
+ `fetch`/`ftp` examples assert their *current* contract (not-implemented / gated)
26
+ and flip to real round-trips when their transport lands — exactly as
27
+ `retrieve`'s `--query` examples assert `validation/not-implemented` today.
28
+
29
+ ## REST round-trip (the CI proof)
30
+
31
+ ### 1. Upload a small file (auto → rest)
32
+
33
+ A ≤60 KB upload needs no `--via`; auto-selection uses the REST family.
34
+
35
+ ```bash
36
+ centrs transfer $R upload $SRC centrs-up.txt --username $U --password $P --json
37
+ ```
38
+
39
+ Envelope: `ok: true`, `data.op="upload"`, `data.remote="centrs-up.txt"`,
40
+ `data.bytes` equals the size of `$SRC`, `data.verified="size"`,
41
+ `meta.via="rest-api"`.
42
+
43
+ ### 2. Download it back (round-trip)
44
+
45
+ ```bash
46
+ centrs transfer $R download centrs-up.txt $TMP/down.txt --username $U --password $P --json
47
+ ```
48
+
49
+ Envelope: `ok: true`, `data.op="download"`. The byte content of `$TMP/down.txt`
50
+ is **identical** to `$SRC` — this is the small-file round-trip that gates
51
+ `CHR-passed`.
52
+
53
+ ## List (sugar over `retrieve /file`)
54
+
55
+ ### 3. List files
56
+
57
+ ```bash
58
+ centrs transfer $R list --username $U --password $P --json
59
+ ```
60
+
61
+ Envelope: `data` is the `/file` row array (same shape as `retrieve /file`) and
62
+ includes a row with `name="centrs-up.txt"`.
63
+
64
+ ### 4. Filter by type
65
+
66
+ ```bash
67
+ centrs transfer $R list --type file --username $U --password $P --json
68
+ ```
69
+
70
+ Envelope: every returned row has a file `type` (no `directory`/`disk` rows).
71
+
72
+ ### 5. Filter by name glob
73
+
74
+ ```bash
75
+ centrs transfer $R list --name 'centrs-*' --username $U --password $P --json
76
+ ```
77
+
78
+ Envelope: every row's `name` matches the glob; `centrs-up.txt` is present.
79
+
80
+ ## Validate before write
81
+
82
+ ### 6. Refuse overwrite without `--force`
83
+
84
+ Re-uploading onto an existing remote path fails the precondition probe (centrs
85
+ `stat`s the target via `/file/print`) **before** writing.
86
+
87
+ ```bash
88
+ centrs transfer $R upload $SRC centrs-up.txt --username $U --password $P --json
89
+ ```
90
+
91
+ Envelope: `ok: false`, `error.code="usage/target-exists"`. The existing file is
92
+ unchanged.
93
+
94
+ ### 7. `--force` overwrites
95
+
96
+ ```bash
97
+ centrs transfer $R upload $SRC centrs-up.txt --force --username $U --password $P --json
98
+ ```
99
+
100
+ Envelope: `ok: true`, `data.op="upload"`.
101
+
102
+ ## Defaults and piping
103
+
104
+ ### 8. Omitted local on download → basename in the current directory
105
+
106
+ ```bash
107
+ centrs transfer $R download centrs-up.txt --username $U --password $P --json
108
+ ```
109
+
110
+ Writes `./centrs-up.txt`; envelope `data.local` ends in `centrs-up.txt`.
111
+
112
+ ### 9. Download to stdout (`-`)
113
+
114
+ Payload bytes go to **stdout**; the human/JSON summary goes to stderr, so the
115
+ two never interleave.
116
+
117
+ ```bash
118
+ centrs transfer $R download centrs-up.txt - --username $U --password $P
119
+ ```
120
+
121
+ stdout is byte-identical to `$SRC`.
122
+
123
+ ### 10. Upload from stdin (`-`)
124
+
125
+ ```bash
126
+ printf 'hello-centrs' | centrs transfer $R upload - centrs-stdin.txt --username $U --password $P --json
127
+ ```
128
+
129
+ Envelope: `ok: true`, `data.bytes=12`. A follow-up `download centrs-stdin.txt -`
130
+ emits `hello-centrs`.
131
+
132
+ ## Device file management
133
+
134
+ ### 11. mkdir
135
+
136
+ ```bash
137
+ centrs transfer $R mkdir centrs-dir --username $U --password $P --json
138
+ ```
139
+
140
+ Envelope: `ok: true`. A subsequent `list --type directory` includes
141
+ `name="centrs-dir"`.
142
+
143
+ ### 12. Upload into a directory
144
+
145
+ ```bash
146
+ centrs transfer $R upload $SRC centrs-dir/nested.txt --username $U --password $P --json
147
+ ```
148
+
149
+ Envelope: `ok: true`, `data.remote="centrs-dir/nested.txt"`.
150
+
151
+ ### 13. On-device copy
152
+
153
+ ```bash
154
+ centrs transfer $R copy centrs-dir/nested.txt centrs-dir/copy.txt --username $U --password $P --json
155
+ ```
156
+
157
+ Envelope: `ok: true`, `data.op="copy"`. `list --name 'centrs-dir/*'` shows both
158
+ `nested.txt` and `copy.txt`.
159
+
160
+ ### 14. Remove
161
+
162
+ ```bash
163
+ centrs transfer $R remove centrs-dir/copy.txt --username $U --password $P --json
164
+ ```
165
+
166
+ Envelope: `ok: true`, `data.op="remove"`. The file no longer appears in `list`.
167
+
168
+ ## Path handling
169
+
170
+ ### 15. Leading-slash normalization
171
+
172
+ A leading `/` is accepted and normalized away, so `/centrs-up.txt` and
173
+ `centrs-up.txt` are the same target.
174
+
175
+ ```bash
176
+ centrs transfer $R download /centrs-up.txt $TMP/slash.txt --username $U --password $P --json
177
+ ```
178
+
179
+ Envelope: `ok: true`; `$TMP/slash.txt` is identical to `$SRC`, and
180
+ `data.remote="centrs-up.txt"` (no leading slash on the wire).
181
+
182
+ ## Integrity
183
+
184
+ ### 16. `--no-verify` skips the size check
185
+
186
+ ```bash
187
+ centrs transfer $R upload $SRC centrs-no-verify.txt --no-verify --username $U --password $P --json
188
+ ```
189
+
190
+ Envelope: `ok: true`, `data.verified="off"`.
191
+
192
+ ## Large file over REST (chunked read scales)
193
+
194
+ The harness seeds the >60 KB device file `centrs-big.bin` with an **sftp upload**
195
+ (see S3 below) — REST cannot write past 60 KB, so sftp is the natural seeder and
196
+ no host-HTTP `fetch` hack is needed.
197
+
198
+ ### 17. Download >60 KB via chunked `/file/read` (auto → rest)
199
+
200
+ ```bash
201
+ centrs transfer $R download centrs-big.bin $TMP/big.bin --username $U --password $P --json
202
+ ```
203
+
204
+ Envelope: `ok: true`, `meta.via="rest-api"`, `data.bytes` equals the seeded size
205
+ (>60 KB). `$TMP/big.bin` is byte-identical to the seed — proving the chunked read
206
+ reassembles correctly across `/file/read` calls.
207
+
208
+ ## Error / transport contract
209
+
210
+ ### 18. `--via rest` upload over 60 KB is rejected up front
211
+
212
+ REST cannot write past 60 KB, and an explicit `--via` is never silently
213
+ downgraded.
214
+
215
+ ```bash
216
+ centrs transfer $R upload $BIG centrs-too-big.txt --via rest --username $U --password $P --json
217
+ ```
218
+
219
+ Envelope: `ok: false`, `error.code="transport/unsupported-operation"`. No partial
220
+ file is created on the device.
221
+
222
+ ### 19. Download a missing remote file
223
+
224
+ ```bash
225
+ centrs transfer $R download centrs-does-not-exist.txt $TMP/x --username $U --password $P --json
226
+ ```
227
+
228
+ Envelope: `ok: false`, `error.code` in the `routeros/*` family (RouterOS's own
229
+ "no such item" surfaced through the menu).
230
+
231
+ ### 20. Bad credentials
232
+
233
+ ```bash
234
+ centrs transfer $R list --username wrong --password wrong --json
235
+ ```
236
+
237
+ Envelope: `ok: false`, `error.code="transport/auth-failed"`, `details_url`
238
+ populated.
239
+
240
+ ### 21. Conflicting flags
241
+
242
+ ```bash
243
+ centrs transfer $R upload $SRC centrs-up.txt --verify size --no-verify --username $U --password $P --json
244
+ ```
245
+
246
+ Rejected before any network call with `usage/conflicting-flags`.
247
+
248
+ ## native-api (`--via native-api`)
249
+
250
+ The same contract over the RouterOS binary API. Exercised by
251
+ `test/integration/transfer.test.ts` against the native-API port.
252
+
253
+ ### N1. Upload small over native-api
254
+
255
+ ```bash
256
+ centrs transfer $A upload $SRC centrs-nv.txt --via native-api --port $API_PORT --username $U --password $P --json
257
+ ```
258
+
259
+ Envelope: `ok: true`, `meta.via="native-api"`.
260
+
261
+ ### N2. Download small over native-api (round-trip)
262
+
263
+ ```bash
264
+ centrs transfer $A download centrs-nv.txt $TMP/nv.txt --via native-api --port $API_PORT --username $U --password $P --json
265
+ ```
266
+
267
+ `$TMP/nv.txt` is identical to `$SRC`.
268
+
269
+ ### N3. List over native-api
270
+
271
+ ```bash
272
+ centrs transfer $A list --via native-api --port $API_PORT --username $U --password $P --json
273
+ ```
274
+
275
+ Envelope: `data` is the `/file` row array; includes `centrs-nv.txt`.
276
+
277
+ ### N4. native-api upload over 60 KB is rejected
278
+
279
+ Same 60 KB write cap as REST (it is a `/file` menu limit, not REST-specific).
280
+
281
+ ```bash
282
+ centrs transfer $A upload $BIG centrs-too-big.txt --via native-api --port $API_PORT --username $U --password $P --json
283
+ ```
284
+
285
+ Envelope: `ok: false`, `error.code="transport/unsupported-operation"`.
286
+
287
+ ## ssh (`--via sftp`)
288
+
289
+ The first SSH consumer: a key-authenticated SFTP round-trip over the host OpenSSH
290
+ `sftp` subsystem. The harness generates an ed25519 keypair, imports the public
291
+ half to the CHR user (`/user ssh-keys import`), and drives `--via sftp --ssh-key`
292
+ against the CHR's forwarded SSH port; `--insecure` accepts the ephemeral host key
293
+ (default is `accept-new` trust-on-first-use). RouterOS refuses password login once
294
+ a key is set, and centrs's sftp runs `BatchMode=yes`, so key auth is the path.
295
+
296
+ ### S1. sftp upload (host → device)
297
+
298
+ ```bash
299
+ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $KEY --insecure upload $SRC centrs-sftp.txt --json
300
+ ```
301
+
302
+ Envelope: `ok: true`, `meta.via="ssh"`, `data.op="upload"`.
303
+
304
+ ### S2. sftp download (device → host, round-trip)
305
+
306
+ ```bash
307
+ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $KEY --insecure download centrs-sftp.txt $TMP/sftp-down.txt --json
308
+ ```
309
+
310
+ `$TMP/sftp-down.txt` is byte-identical to `$SRC`.
311
+
312
+ ### S3. sftp carries a >60 KB file (the gap REST cannot write)
313
+
314
+ ```bash
315
+ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $KEY --insecure upload $BIG centrs-big.bin --json
316
+ ```
317
+
318
+ Envelope: `ok: true`. This is also the seed for example 17 (the same file then
319
+ reads back over REST via chunked `/file/read`).
320
+
321
+ ### S4. list over sftp
322
+
323
+ ```bash
324
+ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $KEY --insecure list --json
325
+ ```
326
+
327
+ Envelope: `data` is the file array; includes `centrs-sftp.txt`.
328
+
329
+ ### S5. mkdir + remove over sftp
330
+
331
+ ```bash
332
+ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $KEY --insecure mkdir centrs-sftp-dir --json
333
+ centrs transfer 127.0.0.1 --via sftp --port $SSH_PORT --username $U --ssh-key $KEY --insecure remove centrs-big.bin --json
334
+ ```
335
+
336
+ Both `ok: true`. On-device `copy` has no SFTP primitive — `--via sftp copy` returns
337
+ `transport/unsupported-operation` (it stays on rest/native).
338
+
339
+ ## Pending transports (contract defined; flips when the transport lands)
340
+
341
+ ### P2. `--via scp` (follow-on pass after sftp)
342
+
343
+ ```bash
344
+ centrs transfer $R upload $SRC centrs-up.txt --via scp --username $U --password $P --json
345
+ ```
346
+
347
+ Envelope: `ok: false`, `error.code="usage/not-implemented"`.
348
+
349
+ ### P3. `--via fetch` (deferred, explicit-only)
350
+
351
+ ```bash
352
+ centrs transfer $R upload $BIG centrs-up.bin --via fetch --username $U --password $P --json
353
+ ```
354
+
355
+ Envelope: `ok: false`, `error.code="usage/not-implemented"`.
356
+
357
+ ### P4. `--via ftp` without the unsafe-protocol opt-in
358
+
359
+ `ftp` is cleartext, so it is gated behind `ALLOW_UNSAFE_PROTOCOLS=ftp` and never
360
+ chosen silently.
361
+
362
+ ```bash
363
+ centrs transfer $R upload $SRC centrs-up.txt --via ftp --username $U --password $P --json
364
+ ```
365
+
366
+ Envelope: `ok: false`, the error reports the protocol is gated (the cleartext-FTP
367
+ opt-in is absent). When `ALLOW_UNSAFE_PROTOCOLS=ftp` is set but the method is not
368
+ yet built, it is `usage/not-implemented` instead.