@tikoci/centrs 0.1.9-next.23 → 0.1.9-next.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +163 -0
- package/GLOSSARY.txt +23 -1
- package/README.md +30 -18
- package/commands/api/AGENTS.md +21 -3
- package/commands/api/README.md +18 -7
- package/commands/api/examples.md +1 -1
- package/commands/btest/README.md +9 -0
- package/commands/btest/examples.md +7 -0
- package/commands/devices/README.md +72 -3
- package/commands/devices/examples.md +56 -1
- package/commands/execute/README.md +37 -6
- package/commands/execute/examples.md +24 -5
- package/commands/explain/README.md +675 -64
- package/commands/explain/examples.md +199 -42
- package/commands/retrieve/README.md +35 -3
- package/commands/retrieve/examples.md +114 -1
- package/commands/transfer/README.md +33 -4
- package/commands/transfer/examples.md +46 -0
- package/docs/CLI.md +30 -8
- package/docs/CONSTITUTION.md +14 -9
- package/docs/MATRIX.md +37 -22
- package/docs/errors/cdb/encrypt-failed.md +10 -0
- package/docs/errors/cdb/password-required.md +5 -4
- package/docs/errors/cdb/plaintext-backups.md +11 -0
- package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
- package/docs/errors/input/invalid-routeros-path.md +25 -3
- package/docs/errors/routeros/permission-denied.md +24 -0
- package/docs/errors/transport/edge-auth-failed.md +22 -0
- package/docs/errors/transport/write-outcome-unknown.md +3 -0
- package/docs/errors/usage/invalid-cursor.md +20 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +9 -5
- package/src/api-fanout.ts +2 -0
- package/src/api.ts +46 -39
- package/src/btest.ts +7 -0
- package/src/cli/api.ts +18 -1
- package/src/cli/btest.ts +9 -0
- package/src/cli/devices.ts +76 -8
- package/src/cli/execute.ts +9 -0
- package/src/cli/explain.ts +206 -47
- package/src/cli/retrieve.ts +9 -0
- package/src/cli/terminal.ts +9 -0
- package/src/cli/transfer.ts +10 -1
- package/src/core/envelope.ts +10 -3
- package/src/core/error-catalog.ts +28 -2
- package/src/core/execute-command.ts +53 -0
- package/src/core/fanout.ts +12 -2
- package/src/core/inspect.ts +4 -5
- package/src/core/menu-path.ts +89 -0
- package/src/core/routeros-errors.ts +25 -0
- package/src/devices.ts +154 -9
- package/src/execute-cmr.ts +2 -8
- package/src/execute-fanout.ts +2 -0
- package/src/execute.ts +55 -20
- package/src/explain/args.ts +115 -10
- package/src/explain/blocks.ts +2 -2
- package/src/explain/catalog.ts +22 -12
- package/src/explain/group-faults.ts +162 -29
- package/src/explain/operators.ts +2 -1
- package/src/explain/pasted-prompt.ts +198 -0
- package/src/explain/pathresolve.ts +2 -2
- package/src/explain/scope-brace.ts +226 -11
- package/src/explain/segment.ts +138 -3
- package/src/explain/symbols.ts +148 -8
- package/src/explain/values.ts +55 -4
- package/src/explain/verbsplit.ts +2 -2
- package/src/explain/write.ts +2 -2
- package/src/explain-live-facets.ts +739 -0
- package/src/explain-live-types.ts +490 -0
- package/src/explain-live.ts +593 -0
- package/src/explain.ts +397 -43
- package/src/index.ts +10 -0
- package/src/protocols/adapter.ts +90 -10
- package/src/protocols/btest-session.ts +10 -30
- package/src/protocols/ec-srp5.ts +19 -10
- package/src/protocols/mac-telnet-console.ts +2 -2
- package/src/protocols/mac-telnet.ts +38 -10
- package/src/protocols/native-api.ts +13 -12
- package/src/resolver/cdb.ts +58 -2
- package/src/resolver/index.ts +1 -0
- package/src/resolver/quickchr-provider.ts +5 -0
- package/src/resolver/selection.ts +11 -0
- package/src/retrieve-fanout.ts +2 -0
- package/src/retrieve-follow.ts +2 -1
- package/src/retrieve-sample.ts +2 -1
- package/src/retrieve-wait.ts +10 -6
- package/src/retrieve.ts +88 -27
- package/src/settings.ts +1 -0
- package/src/terminal.ts +7 -0
- package/src/transfer-fanout.ts +2 -0
- package/src/transfer.ts +117 -27
package/src/retrieve.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
isCommandNode,
|
|
21
21
|
pathTokens,
|
|
22
22
|
} from "./core/inspect.ts";
|
|
23
|
+
import { assertRouterOsMenuPath } from "./core/menu-path.ts";
|
|
23
24
|
import {
|
|
24
25
|
compileQueryWords,
|
|
25
26
|
type ParsedQuery,
|
|
@@ -101,6 +102,12 @@ export interface RetrieveRequest {
|
|
|
101
102
|
maxResultsBytes?: number;
|
|
102
103
|
cdbFile?: string;
|
|
103
104
|
cdbPassword?: string;
|
|
105
|
+
/**
|
|
106
|
+
* `--match` selector for a `<router>` several CDB records share
|
|
107
|
+
* (`user=<name>`, `target=<addr>`, or a record type): the same selector
|
|
108
|
+
* `devices show --match` takes. Single-target only (#583).
|
|
109
|
+
*/
|
|
110
|
+
match?: string;
|
|
104
111
|
/** Opt-in host ARP resolution for a MAC target (`none` default, or `arp`). */
|
|
105
112
|
resolve?: string;
|
|
106
113
|
/** CDB group selector — expands to a fanout over all matching records. */
|
|
@@ -455,12 +462,10 @@ export async function runResolvedRetrieve(
|
|
|
455
462
|
await inspectRetrievePath(resolved, backend),
|
|
456
463
|
backend,
|
|
457
464
|
);
|
|
458
|
-
const validation
|
|
459
|
-
|
|
460
|
-
source: "live /console/inspect request=child+completion",
|
|
461
|
-
result: "passed",
|
|
465
|
+
const validation = retrieveValidationPassed(
|
|
466
|
+
"live /console/inspect request=child+completion",
|
|
462
467
|
availableAttributes,
|
|
463
|
-
|
|
468
|
+
);
|
|
464
469
|
trace.validation = validation;
|
|
465
470
|
const envelope = buildSuccessEnvelope(
|
|
466
471
|
resolved,
|
|
@@ -533,14 +538,26 @@ export async function validateRetrieveRead(
|
|
|
533
538
|
return {
|
|
534
539
|
inspection,
|
|
535
540
|
query,
|
|
536
|
-
validation:
|
|
537
|
-
|
|
538
|
-
source: availableAttributes
|
|
541
|
+
validation: retrieveValidationPassed(
|
|
542
|
+
availableAttributes
|
|
539
543
|
? "live /console/inspect request=child+completion"
|
|
540
544
|
: "live /console/inspect request=child",
|
|
541
|
-
result: "passed",
|
|
542
545
|
availableAttributes,
|
|
543
|
-
|
|
546
|
+
),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** retrieve's gate is the device stage alone; a path read has no CLI string to analyze. */
|
|
551
|
+
function retrieveValidationPassed(
|
|
552
|
+
source: string,
|
|
553
|
+
availableAttributes: readonly string[] | undefined,
|
|
554
|
+
): EnvelopeValidationMeta {
|
|
555
|
+
return {
|
|
556
|
+
enabled: true,
|
|
557
|
+
source,
|
|
558
|
+
result: "passed",
|
|
559
|
+
availableAttributes,
|
|
560
|
+
stages: [{ stage: "device", source, result: "passed" }],
|
|
544
561
|
};
|
|
545
562
|
}
|
|
546
563
|
|
|
@@ -625,29 +642,20 @@ const RETRIEVE_DEVICE_STAGE_SOURCE = "live /console/inspect";
|
|
|
625
642
|
* an inspect answer: `validation/*`, and the singleton `--query` refusal
|
|
626
643
|
* (`usage/conflicting-flags`). A `transport/*` or `auth/*` failure means the
|
|
627
644
|
* device never answered; a `routeros/*` error is a device answer that is not
|
|
628
|
-
* a verdict; `internal/*` is a centrs defect.
|
|
645
|
+
* a verdict; `internal/*` is a centrs defect. A `wait/*` error ends a `--wait`
|
|
646
|
+
* whose gate never settled; one that settled reports its own result instead
|
|
647
|
+
* (#537). Those five report no `result`.
|
|
629
648
|
*/
|
|
630
649
|
function unsettledValidationMeta(
|
|
631
650
|
resolved: ResolvedRetrieveRequest,
|
|
632
651
|
error: CentrsError,
|
|
633
652
|
): EnvelopeValidationMeta {
|
|
653
|
+
if (stoppedBeforeGateAnswer(error.code)) {
|
|
654
|
+
return notReachedValidationMeta(resolved, error.code);
|
|
655
|
+
}
|
|
634
656
|
if (!resolved.validate.value) {
|
|
635
657
|
return { enabled: false, source: "disabled" };
|
|
636
658
|
}
|
|
637
|
-
if (/^(transport|auth|routeros|internal)\//.test(error.code)) {
|
|
638
|
-
return {
|
|
639
|
-
enabled: true,
|
|
640
|
-
source: RETRIEVE_DEVICE_STAGE_SOURCE,
|
|
641
|
-
stages: [
|
|
642
|
-
{
|
|
643
|
-
stage: "device",
|
|
644
|
-
source: RETRIEVE_DEVICE_STAGE_SOURCE,
|
|
645
|
-
result: "skipped",
|
|
646
|
-
reason: notReachedReason(error.code),
|
|
647
|
-
},
|
|
648
|
-
],
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
659
|
return {
|
|
652
660
|
enabled: true,
|
|
653
661
|
source: RETRIEVE_DEVICE_STAGE_SOURCE,
|
|
@@ -662,6 +670,37 @@ function unsettledValidationMeta(
|
|
|
662
670
|
};
|
|
663
671
|
}
|
|
664
672
|
|
|
673
|
+
/** Whether an error code stops a run without being a gate answer (GH#435, #537). */
|
|
674
|
+
export function stoppedBeforeGateAnswer(code: string): boolean {
|
|
675
|
+
return /^(transport|auth|routeros|internal|wait)\//.test(code);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* `meta.validation` when the gate was never reached. `cause` is the error code
|
|
680
|
+
* that stopped it, or `interrupted` for a caller's cancellation, which has no
|
|
681
|
+
* code (#537).
|
|
682
|
+
*/
|
|
683
|
+
export function notReachedValidationMeta(
|
|
684
|
+
resolved: ResolvedRetrieveRequest,
|
|
685
|
+
cause: string,
|
|
686
|
+
): EnvelopeValidationMeta {
|
|
687
|
+
if (!resolved.validate.value) {
|
|
688
|
+
return { enabled: false, source: "disabled" };
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
enabled: true,
|
|
692
|
+
source: RETRIEVE_DEVICE_STAGE_SOURCE,
|
|
693
|
+
stages: [
|
|
694
|
+
{
|
|
695
|
+
stage: "device",
|
|
696
|
+
source: RETRIEVE_DEVICE_STAGE_SOURCE,
|
|
697
|
+
result: "skipped",
|
|
698
|
+
reason: notReachedReason(cause),
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
665
704
|
export function renderRetrieveEnvelope(
|
|
666
705
|
envelope: RetrieveEnvelope,
|
|
667
706
|
format: RetrieveOutputFormat,
|
|
@@ -888,6 +927,9 @@ export function validateRetrieveRequestShape(
|
|
|
888
927
|
context: { path: request.path },
|
|
889
928
|
});
|
|
890
929
|
}
|
|
930
|
+
// Every transport, even under --validate=false: a malformed segment can
|
|
931
|
+
// cut the appended `/print` off a REST URL and dispatch a write (#586).
|
|
932
|
+
assertRouterOsMenuPath(request.path);
|
|
891
933
|
|
|
892
934
|
const query = resolveRetrieveQuery(request);
|
|
893
935
|
if (query && request.listAttributes) {
|
|
@@ -1325,6 +1367,7 @@ export async function resolveRetrieveRequest(
|
|
|
1325
1367
|
targetInput: request.targetInput,
|
|
1326
1368
|
cdbFile: request.cdbFile,
|
|
1327
1369
|
cdbPassword: request.cdbPassword,
|
|
1370
|
+
match: request.match,
|
|
1328
1371
|
},
|
|
1329
1372
|
env,
|
|
1330
1373
|
config,
|
|
@@ -1499,8 +1542,7 @@ export async function inspectRetrievePath(
|
|
|
1499
1542
|
throw new CentrsError({
|
|
1500
1543
|
code: "validation/unknown-path",
|
|
1501
1544
|
summary: `RouterOS path ${resolved.path} does not expose a retrieve command.`,
|
|
1502
|
-
remediation:
|
|
1503
|
-
"Check the slash-prefixed RouterOS path, or use a known readable path such as `/system/resource`, `/system/identity`, `/ip/address`, or `/interface`.",
|
|
1545
|
+
remediation: retrieveUnknownPathRemediation(resolved.path),
|
|
1504
1546
|
context: {
|
|
1505
1547
|
path: resolved.path,
|
|
1506
1548
|
validationSource: "/console/inspect request=child",
|
|
@@ -1526,6 +1568,25 @@ export async function inspectRetrievePath(
|
|
|
1526
1568
|
};
|
|
1527
1569
|
}
|
|
1528
1570
|
|
|
1571
|
+
/**
|
|
1572
|
+
* The `validation/unknown-path` remediation for a retrieve read. One path
|
|
1573
|
+
* earns a targeted hint: `/system/routerboard` exists only on MikroTik
|
|
1574
|
+
* hardware, so its absence on CHR and x86 systems is expected rather than a
|
|
1575
|
+
* typo — and the caller's likely want, the device's license ID, lives at
|
|
1576
|
+
* `/system/license` instead (`system-id` on CHR, `software-id` on x86; manual
|
|
1577
|
+
* "RouterOS License Keys", CLI Reference `/system/license`). The example
|
|
1578
|
+
* reads the whole object because a projected attribute would fail on one
|
|
1579
|
+
* platform or the other. The hint is static on purpose: on hardware the menu
|
|
1580
|
+
* always validates, so this branch only fires where the menu is genuinely
|
|
1581
|
+
* absent, and no extra round trip is needed to say so.
|
|
1582
|
+
*/
|
|
1583
|
+
export function retrieveUnknownPathRemediation(path: string): string {
|
|
1584
|
+
if (path.replace(/\/$/, "").toLowerCase() === "/system/routerboard") {
|
|
1585
|
+
return "This device exposes no /system/routerboard menu — expected on CHR and x86 systems, which have no routerboard. For this device's license ID, read /system/license instead: `centrs retrieve <router> /system/license` (system-id on CHR, software-id on x86).";
|
|
1586
|
+
}
|
|
1587
|
+
return "Check the slash-prefixed RouterOS path, or use a known readable path such as `/system/resource`, `/system/identity`, `/ip/address`, or `/interface`.";
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1529
1590
|
/**
|
|
1530
1591
|
* Inspect the menu's attribute names and reject requested ones it lacks with
|
|
1531
1592
|
* `validation/unknown-attribute`. Shared by one-shot retrieve and `--follow`.
|
package/src/settings.ts
CHANGED
package/src/terminal.ts
CHANGED
|
@@ -81,6 +81,12 @@ export interface TerminalRequest {
|
|
|
81
81
|
insecure?: boolean;
|
|
82
82
|
cdbFile?: string;
|
|
83
83
|
cdbPassword?: string;
|
|
84
|
+
/**
|
|
85
|
+
* `--match` selector for a `<router>` several CDB records share
|
|
86
|
+
* (`user=<name>`, `target=<addr>`, or a record type): the same selector
|
|
87
|
+
* `devices show --match` takes. Single-target only (#583).
|
|
88
|
+
*/
|
|
89
|
+
match?: string;
|
|
84
90
|
format?: string;
|
|
85
91
|
verbose?: boolean;
|
|
86
92
|
}
|
|
@@ -154,6 +160,7 @@ export async function resolveTerminalRequest(
|
|
|
154
160
|
targetInput: request.targetInput,
|
|
155
161
|
cdbFile: request.cdbFile,
|
|
156
162
|
cdbPassword: request.cdbPassword,
|
|
163
|
+
match: request.match,
|
|
157
164
|
},
|
|
158
165
|
env,
|
|
159
166
|
config,
|
package/src/transfer-fanout.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
import { promptForWriteConfirmation } from "./execute.ts";
|
|
49
49
|
import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
|
|
50
50
|
import {
|
|
51
|
+
assertNoMatchInFanout,
|
|
51
52
|
emptyWriteSelectionError,
|
|
52
53
|
expandSelection,
|
|
53
54
|
isDefaultRecordTarget,
|
|
@@ -265,6 +266,7 @@ export async function transferFanout(
|
|
|
265
266
|
options: TransferFanoutOptions = {},
|
|
266
267
|
): Promise<TransferFanoutEnvelope> {
|
|
267
268
|
validateTransferRequestShape(request);
|
|
269
|
+
assertNoMatchInFanout(request.match);
|
|
268
270
|
const config = await loadEnvFileDefaults(env);
|
|
269
271
|
const selectionSummary = summarizeSelection(selection);
|
|
270
272
|
const requestSummary: TransferFanoutRequestSummary = {
|
package/src/transfer.ts
CHANGED
|
@@ -113,6 +113,20 @@ export const transferMethods = [
|
|
|
113
113
|
] as const;
|
|
114
114
|
export type TransferMethod = (typeof transferMethods)[number];
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The transfer method a CDB record's transport preference (`via=` in its
|
|
118
|
+
* comment, shared with execute) selects (#591). A transport missing here
|
|
119
|
+
* (mac-telnet, winbox, …) cannot carry files and fails `settings/invalid-via`
|
|
120
|
+
* when the record's preference is what decided the method.
|
|
121
|
+
*/
|
|
122
|
+
export const transferMethodForTransport: Partial<
|
|
123
|
+
Record<RouterOsProtocol, TransferMethod>
|
|
124
|
+
> = {
|
|
125
|
+
ssh: "sftp",
|
|
126
|
+
"rest-api": "rest",
|
|
127
|
+
"native-api": "native",
|
|
128
|
+
};
|
|
129
|
+
|
|
116
130
|
/** RouterOS `/file` contents write cap (`/file/set contents`). */
|
|
117
131
|
export const REST_FILE_WRITE_CAP_BYTES = 60_000;
|
|
118
132
|
/** Maximum `/file/read` chunk RouterOS accepts (`chunk-size` upper bound). */
|
|
@@ -158,6 +172,12 @@ export interface TransferRequest {
|
|
|
158
172
|
name?: string;
|
|
159
173
|
cdbFile?: string;
|
|
160
174
|
cdbPassword?: string;
|
|
175
|
+
/**
|
|
176
|
+
* `--match` selector for a `<router>` several CDB records share
|
|
177
|
+
* (`user=<name>`, `target=<addr>`, or a record type): the same selector
|
|
178
|
+
* `devices show --match` takes. Single-target only (#583).
|
|
179
|
+
*/
|
|
180
|
+
match?: string;
|
|
161
181
|
/** `download` fan-out: directory to write one file per target into. */
|
|
162
182
|
outDir?: string;
|
|
163
183
|
/** Fan-out write gate: confirm a mutating fan-out (upload/remove/mkdir/copy). */
|
|
@@ -410,7 +430,7 @@ async function runUpload(
|
|
|
410
430
|
isRestFamily(resolved.method) &&
|
|
411
431
|
bytes.byteLength > REST_FILE_WRITE_CAP_BYTES
|
|
412
432
|
) {
|
|
413
|
-
throw restWriteCapError(remote, bytes.byteLength);
|
|
433
|
+
throw restWriteCapError(remote, bytes.byteLength, resolved.via);
|
|
414
434
|
}
|
|
415
435
|
|
|
416
436
|
const existing = await backend.findFile(remote);
|
|
@@ -879,13 +899,6 @@ export async function resolveTransferRequest(
|
|
|
879
899
|
const direction: TransferDirection =
|
|
880
900
|
request.verb === "upload" ? "upload" : "download";
|
|
881
901
|
const sizeHint = uploadSizeHint(request);
|
|
882
|
-
const { method, protocol, via } = selectTransferMethod(
|
|
883
|
-
request,
|
|
884
|
-
direction,
|
|
885
|
-
sizeHint,
|
|
886
|
-
env,
|
|
887
|
-
config,
|
|
888
|
-
);
|
|
889
902
|
|
|
890
903
|
// A quickchr target is a named-live-provider: host/port/auth come from the
|
|
891
904
|
// live descriptor, bypassing CDB and the `__default__` ladder. For
|
|
@@ -908,10 +921,21 @@ export async function resolveTransferRequest(
|
|
|
908
921
|
targetInput: request.targetInput,
|
|
909
922
|
cdbFile: request.cdbFile,
|
|
910
923
|
cdbPassword: request.cdbPassword,
|
|
924
|
+
match: request.match,
|
|
911
925
|
},
|
|
912
926
|
env,
|
|
913
927
|
config,
|
|
914
928
|
)));
|
|
929
|
+
// After CDB resolution: a record's `via=` preference is a layer of the
|
|
930
|
+
// method choice, mapped from transport to transfer-method names (#591).
|
|
931
|
+
const { method, protocol, via } = selectTransferMethod(
|
|
932
|
+
request,
|
|
933
|
+
direction,
|
|
934
|
+
sizeHint,
|
|
935
|
+
env,
|
|
936
|
+
config,
|
|
937
|
+
cdbResolution?.overrides.via,
|
|
938
|
+
);
|
|
915
939
|
|
|
916
940
|
const format = resolveTransferFormat(request, env, config);
|
|
917
941
|
const validate = resolveBooleanSetting(
|
|
@@ -1041,6 +1065,7 @@ export function selectTransferMethod(
|
|
|
1041
1065
|
uploadBytes: number | undefined,
|
|
1042
1066
|
env: Record<string, string | undefined>,
|
|
1043
1067
|
config: Record<string, string | undefined> = {},
|
|
1068
|
+
cdbVia?: ResolvedSetting<RouterOsProtocol>,
|
|
1044
1069
|
): {
|
|
1045
1070
|
method: TransferMethod;
|
|
1046
1071
|
protocol: RouterOsProtocol;
|
|
@@ -1053,10 +1078,32 @@ export function selectTransferMethod(
|
|
|
1053
1078
|
"auto",
|
|
1054
1079
|
"via",
|
|
1055
1080
|
undefined,
|
|
1056
|
-
undefined
|
|
1081
|
+
cdbVia === undefined
|
|
1082
|
+
? undefined
|
|
1083
|
+
: {
|
|
1084
|
+
value: transferMethodForTransport[cdbVia.value] ?? cdbVia.value,
|
|
1085
|
+
source: cdbVia.source,
|
|
1086
|
+
},
|
|
1057
1087
|
config,
|
|
1058
1088
|
) as ResolvedSetting<string>;
|
|
1059
1089
|
const requested = via.value;
|
|
1090
|
+
if (
|
|
1091
|
+
via.source.kind === "comment-kv" &&
|
|
1092
|
+
cdbVia !== undefined &&
|
|
1093
|
+
transferMethodForTransport[cdbVia.value] === undefined
|
|
1094
|
+
) {
|
|
1095
|
+
throw new CentrsError({
|
|
1096
|
+
code: "settings/invalid-via",
|
|
1097
|
+
summary: `The CDB record prefers \`via=${cdbVia.value}\`, which cannot carry files.`,
|
|
1098
|
+
remediation:
|
|
1099
|
+
"Pass `--via sftp`, `--via rest` or `--via native` for this transfer (or set `CENTRS_TRANSFER_VIA`). The record's `via` stays in effect for execute.",
|
|
1100
|
+
context: {
|
|
1101
|
+
via: cdbVia.value,
|
|
1102
|
+
source: cdbVia.source.key,
|
|
1103
|
+
supported: Object.keys(transferMethodForTransport),
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1060
1107
|
|
|
1061
1108
|
if (requested === "auto") {
|
|
1062
1109
|
// Auto: REST family carries reads and small writes; a large upload exceeds
|
|
@@ -1078,7 +1125,8 @@ export function selectTransferMethod(
|
|
|
1078
1125
|
if (requested === "native" || requested === "native-api") {
|
|
1079
1126
|
return { method: "native", protocol: "native-api", via };
|
|
1080
1127
|
}
|
|
1081
|
-
|
|
1128
|
+
// `ssh` is the transport name `execute` and CDB `via=` use (#591).
|
|
1129
|
+
if (requested === "sftp" || requested === "ssh") {
|
|
1082
1130
|
return { method: "sftp", protocol: "ssh", via };
|
|
1083
1131
|
}
|
|
1084
1132
|
if (requested === "scp" || requested === "fetch") {
|
|
@@ -1401,13 +1449,26 @@ function buildSuccessEnvelope(
|
|
|
1401
1449
|
data,
|
|
1402
1450
|
warnings,
|
|
1403
1451
|
tips: [],
|
|
1404
|
-
meta: metaFromResolved(
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
:
|
|
1409
|
-
|
|
1410
|
-
|
|
1452
|
+
meta: metaFromResolved(
|
|
1453
|
+
resolved,
|
|
1454
|
+
operationData,
|
|
1455
|
+
!hasExistenceProbe(resolved)
|
|
1456
|
+
? { enabled: false }
|
|
1457
|
+
: resolved.validate.value
|
|
1458
|
+
? {
|
|
1459
|
+
enabled: true,
|
|
1460
|
+
source: TRANSFER_DEVICE_STAGE_SOURCE,
|
|
1461
|
+
result: "passed",
|
|
1462
|
+
stages: [
|
|
1463
|
+
{
|
|
1464
|
+
stage: "device",
|
|
1465
|
+
source: TRANSFER_DEVICE_STAGE_SOURCE,
|
|
1466
|
+
result: "passed",
|
|
1467
|
+
},
|
|
1468
|
+
],
|
|
1469
|
+
}
|
|
1470
|
+
: { enabled: false, source: "disabled", result: "skipped" },
|
|
1471
|
+
),
|
|
1411
1472
|
};
|
|
1412
1473
|
}
|
|
1413
1474
|
|
|
@@ -1491,6 +1552,16 @@ function asTransferError(error: unknown): CentrsError {
|
|
|
1491
1552
|
* see which host, user and credential sources were tried. Secrets never enter
|
|
1492
1553
|
* it — auth meta is `username` + `passwordProvided` only.
|
|
1493
1554
|
*/
|
|
1555
|
+
const TRANSFER_DEVICE_STAGE_SOURCE = "device /file existence probe";
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* `list` reads `/file` and has no precondition probe, so it is not a gate:
|
|
1559
|
+
* like `explain`, it reports `{ enabled: false }` and no verdict (#535).
|
|
1560
|
+
*/
|
|
1561
|
+
function hasExistenceProbe(resolved: ResolvedTransferRequest): boolean {
|
|
1562
|
+
return resolved.verb !== "list";
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1494
1565
|
export function buildTransferErrorEnvelopeFromResolved(
|
|
1495
1566
|
resolved: ResolvedTransferRequest,
|
|
1496
1567
|
error: unknown,
|
|
@@ -1509,12 +1580,14 @@ export function buildTransferErrorEnvelopeFromResolved(
|
|
|
1509
1580
|
remote: resolved.remote,
|
|
1510
1581
|
local: resolved.local,
|
|
1511
1582
|
}),
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1583
|
+
hasExistenceProbe(resolved)
|
|
1584
|
+
? {
|
|
1585
|
+
enabled: resolved.validate.value,
|
|
1586
|
+
source: resolved.validate.value
|
|
1587
|
+
? TRANSFER_DEVICE_STAGE_SOURCE
|
|
1588
|
+
: "disabled",
|
|
1589
|
+
}
|
|
1590
|
+
: { enabled: false },
|
|
1518
1591
|
),
|
|
1519
1592
|
};
|
|
1520
1593
|
}
|
|
@@ -1747,13 +1820,30 @@ function targetExistsError(remote: string): CentrsError {
|
|
|
1747
1820
|
});
|
|
1748
1821
|
}
|
|
1749
1822
|
|
|
1750
|
-
function restWriteCapError(
|
|
1823
|
+
function restWriteCapError(
|
|
1824
|
+
remote: string,
|
|
1825
|
+
bytes: number,
|
|
1826
|
+
via: ResolvedSetting<string>,
|
|
1827
|
+
): CentrsError {
|
|
1828
|
+
// A pinned REST method is never hopped to sftp; name the pin so a record
|
|
1829
|
+
// or env preference the caller did not type is visible (#591 review).
|
|
1830
|
+
const pinnedBy =
|
|
1831
|
+
via.source.kind === "comment-kv"
|
|
1832
|
+
? ` The method came from the CDB record's \`via=\` (\`${via.source.key}\`).`
|
|
1833
|
+
: via.source.kind === "env" || via.source.kind === "config"
|
|
1834
|
+
? ` The method came from \`${via.source.key}=${via.value}\` (${via.source.kind}).`
|
|
1835
|
+
: "";
|
|
1751
1836
|
return new CentrsError({
|
|
1752
1837
|
code: "transport/unsupported-operation",
|
|
1753
1838
|
summary: `Cannot write ${bytes} bytes to ${remote} over REST/native (60 KB cap).`,
|
|
1754
|
-
remediation:
|
|
1755
|
-
|
|
1756
|
-
|
|
1839
|
+
remediation: `Pass \`--via sftp\` for this upload; the REST \`/file\` write cap is 60 KB.${pinnedBy}`,
|
|
1840
|
+
context: {
|
|
1841
|
+
remote,
|
|
1842
|
+
bytes,
|
|
1843
|
+
capBytes: REST_FILE_WRITE_CAP_BYTES,
|
|
1844
|
+
via: via.value,
|
|
1845
|
+
viaSource: via.source.key ?? via.source.kind,
|
|
1846
|
+
},
|
|
1757
1847
|
});
|
|
1758
1848
|
}
|
|
1759
1849
|
|