@tikoci/centrs 0.1.3 → 0.1.4
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 +39 -0
- package/GLOSSARY.txt +213 -0
- package/LICENSE +21 -0
- package/README.md +36 -8
- package/commands/AGENTS.md +7 -1
- package/commands/api/README.md +19 -18
- package/commands/api/examples.md +15 -0
- package/commands/btest/README.md +21 -32
- package/commands/check/README.md +6 -1
- package/commands/discover/README.md +5 -11
- package/commands/execute/README.md +41 -29
- package/commands/execute/examples.md +41 -3
- package/commands/explain/README.md +1716 -0
- package/commands/explain/examples.md +606 -0
- package/commands/retrieve/README.md +25 -27
- package/commands/retrieve/examples.md +16 -0
- package/commands/transfer/README.md +28 -20
- package/commands/transfer/examples.md +29 -0
- package/docs/CLI.md +353 -0
- package/docs/CONSTITUTION.md +66 -15
- package/docs/MATRIX.md +38 -1
- package/docs/errors/README.md +3 -0
- package/docs/errors/quickchr/machine-not-found.md +22 -0
- package/docs/errors/quickchr/machine-stopped.md +20 -0
- package/docs/errors/quickchr/package-unavailable.md +27 -0
- package/docs/errors/quickchr/unsupported-via.md +35 -0
- package/docs/errors/quickchr/unsupported.md +35 -0
- package/docs/errors/routeros/version-unsupported.md +26 -0
- package/docs/errors/transport/provider-trust.md +29 -0
- package/docs/errors/usage/stdin-ignored.md +28 -0
- package/docs/errors/validation/unknown-attribute.md +37 -4
- package/package.json +45 -10
- package/src/api-fanout.ts +29 -14
- package/src/api.ts +97 -54
- package/src/cli/api.ts +25 -6
- package/src/cli/btest.ts +18 -2
- package/src/cli/execute.ts +25 -6
- package/src/cli/explain.ts +503 -0
- package/src/cli/retrieve.ts +33 -1
- package/src/cli/selection.ts +71 -11
- package/src/cli/settings.ts +2 -2
- package/src/cli/transfer.ts +23 -6
- package/src/cli.ts +15 -2
- package/src/core/envelope.ts +5 -2
- package/src/core/error-catalog.ts +40 -0
- package/src/core/preflight.ts +306 -0
- package/src/core/routeros-string.ts +24 -0
- package/src/errors.ts +1 -0
- package/src/execute-fanout.ts +29 -14
- package/src/execute.ts +114 -63
- package/src/explain/arg-tokens.ts +114 -0
- package/src/explain/args.ts +1403 -0
- package/src/explain/blocks.ts +132 -0
- package/src/explain/brace-slots.ts +143 -0
- package/src/explain/brace-tokens.ts +38 -0
- package/src/explain/catalog.ts +1345 -0
- package/src/explain/coordinates.ts +329 -0
- package/src/explain/defects.ts +217 -0
- package/src/explain/is-known-menu.ts +17 -0
- package/src/explain/menus.ts +664 -0
- package/src/explain/operator-tokens.ts +310 -0
- package/src/explain/operators.ts +458 -0
- package/src/explain/path-tokens.ts +150 -0
- package/src/explain/pathresolve.ts +1324 -0
- package/src/explain/quoted-string.ts +241 -0
- package/src/explain/scope-brace.ts +308 -0
- package/src/explain/segment.ts +757 -0
- package/src/explain/string-tokens.ts +67 -0
- package/src/explain/symbol-values.ts +520 -0
- package/src/explain/symbols.ts +1597 -0
- package/src/explain/token-ranges.ts +72 -0
- package/src/explain/transport.ts +497 -0
- package/src/explain/value-tokens.ts +80 -0
- package/src/explain/values.ts +357 -0
- package/src/explain/verbs.ts +64 -0
- package/src/explain/verbsplit.ts +813 -0
- package/src/explain/write.ts +815 -0
- package/src/explain.ts +2152 -0
- package/src/index.ts +206 -4
- package/src/protocols/mac-telnet-console.ts +61 -17
- package/src/resolver/facts.ts +1 -1
- package/src/resolver/index.ts +23 -4
- package/src/resolver/quickchr-provider.ts +597 -0
- package/src/resolver/selection.ts +99 -12
- package/src/resolver/service-endpoint.ts +155 -0
- package/src/resolver/target.ts +1 -1
- package/src/retrieve-fanout.ts +26 -12
- package/src/retrieve.ts +81 -18
- package/src/transfer-fanout.ts +42 -25
- package/src/transfer.ts +87 -44
|
@@ -54,7 +54,7 @@ export interface SelectionWhereClause {
|
|
|
54
54
|
value: string;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/** The parsed target selection a command hands to {@link
|
|
57
|
+
/** The parsed target selection a command hands to {@link expandSelection}. */
|
|
58
58
|
export interface TargetSelection {
|
|
59
59
|
/** Literal/lookup positionals (NOT operation args — the caller splits those). */
|
|
60
60
|
positionals: readonly string[];
|
|
@@ -70,23 +70,41 @@ export interface TargetSelection {
|
|
|
70
70
|
near?: NearPredicate;
|
|
71
71
|
/** `--bbox <south>,<west>,<north>,<east>`: a geo union predicate (device GPS in box). */
|
|
72
72
|
bbox?: BboxPredicate;
|
|
73
|
+
/**
|
|
74
|
+
* Repeatable `--quickchr <name>`: quickchr machine names (the named-live-
|
|
75
|
+
* provider, #134). Exclusive in v1 — a selection targets quickchr machines OR
|
|
76
|
+
* CDB/literals, never both; the CLI enforces the usage error before expansion
|
|
77
|
+
* and {@link expandSelection} re-asserts it.
|
|
78
|
+
*/
|
|
79
|
+
quickchr?: readonly string[];
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
export interface
|
|
82
|
+
export interface SelectionResolveInput {
|
|
76
83
|
cdbFile?: string;
|
|
77
84
|
cdbPassword?: string;
|
|
78
85
|
/** CLI: true (literal positionals allowed). MCP: false (CDB is the allowlist). */
|
|
79
86
|
allowAdhoc: boolean;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
89
|
+
/**
|
|
90
|
+
* One resolved selection member: a CDB record or an ad-hoc literal target.
|
|
91
|
+
*
|
|
92
|
+
* This is the connection-fact origin ("Axis B") the fan-out callers consume. The
|
|
93
|
+
* selector-source (CDB) and literal arms carry a single endpoint materialized
|
|
94
|
+
* later by `./target.ts`. The `quickchr` arm is the named-live-provider (#134):
|
|
95
|
+
* it carries only the machine NAME here — the live descriptor is resolved in the
|
|
96
|
+
* per-member fan-out `resolve()` path (via `resolveQuickchrTarget`), so a
|
|
97
|
+
* stopped/unknown machine is an inner per-target failure, not an expansion
|
|
98
|
+
* error — see `docs/CONSTITUTION.md` → Resolution providers.
|
|
99
|
+
*/
|
|
100
|
+
export type SelectionMember =
|
|
84
101
|
| { kind: "cdb"; resolution: CdbResolution; recordIndex: number }
|
|
85
|
-
| { kind: "literal"; input: string }
|
|
102
|
+
| { kind: "literal"; input: string }
|
|
103
|
+
| { kind: "quickchr"; name: string };
|
|
86
104
|
|
|
87
|
-
export interface
|
|
105
|
+
export interface SelectionExpansion {
|
|
88
106
|
/** Members ordered by record index; literals appended in positional order. */
|
|
89
|
-
targets: readonly
|
|
107
|
+
targets: readonly SelectionMember[];
|
|
90
108
|
warnings: readonly ResolverWarning[];
|
|
91
109
|
/** True when nothing matched (an empty/unknown selection is not an error). */
|
|
92
110
|
empty: boolean;
|
|
@@ -113,12 +131,16 @@ const ENV_CDB_PASSWORD = "CENTRS_CDB_PASSWORD";
|
|
|
113
131
|
* literal host when no CDB is present). An explicit `--cdb-file` that is missing
|
|
114
132
|
* still throws.
|
|
115
133
|
*/
|
|
116
|
-
export async function
|
|
134
|
+
export async function expandSelection(
|
|
117
135
|
selection: TargetSelection,
|
|
118
|
-
input:
|
|
136
|
+
input: SelectionResolveInput,
|
|
119
137
|
env: Record<string, string | undefined>,
|
|
120
138
|
config: Record<string, string | undefined> = {},
|
|
121
|
-
): Promise<
|
|
139
|
+
): Promise<SelectionExpansion> {
|
|
140
|
+
const quickchrNames = selection.quickchr ?? [];
|
|
141
|
+
if (quickchrNames.length > 0) {
|
|
142
|
+
return expandQuickchrSelection(selection, input, quickchrNames);
|
|
143
|
+
}
|
|
122
144
|
const settings = resolveDevicesSettings({
|
|
123
145
|
cdbFile: input.cdbFile,
|
|
124
146
|
cdbPassword: input.cdbPassword,
|
|
@@ -224,7 +246,7 @@ export async function expandCdbSelection(
|
|
|
224
246
|
}
|
|
225
247
|
}
|
|
226
248
|
|
|
227
|
-
const literals:
|
|
249
|
+
const literals: SelectionMember[] = [];
|
|
228
250
|
const seenLiteral = new Set<string>();
|
|
229
251
|
for (const positional of selection.positionals) {
|
|
230
252
|
try {
|
|
@@ -255,7 +277,7 @@ export async function expandCdbSelection(
|
|
|
255
277
|
}
|
|
256
278
|
}
|
|
257
279
|
|
|
258
|
-
const cdbMembers:
|
|
280
|
+
const cdbMembers: SelectionMember[] = [...indices]
|
|
259
281
|
.sort((a, b) => a - b)
|
|
260
282
|
.map((index) => ({
|
|
261
283
|
kind: "cdb",
|
|
@@ -302,3 +324,68 @@ export async function expandCdbSelection(
|
|
|
302
324
|
|
|
303
325
|
return { targets, warnings, empty: false };
|
|
304
326
|
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Expand a quickchr-only selection. No CDB is loaded — a named-live-provider
|
|
330
|
+
* member never touches the registry, the `__default__` ladder, or the selector
|
|
331
|
+
* grammar. Members carry only the machine name (de-duped by exact name, flag
|
|
332
|
+
* order preserved); the live descriptor resolves per member inside the fan-out.
|
|
333
|
+
*
|
|
334
|
+
* Two guards are load-bearing:
|
|
335
|
+
* - Exclusivity (v1): mixing `--quickchr` with selectors/positionals is a
|
|
336
|
+
* usage error. The CLI throws first with flag-level wording; this re-assert
|
|
337
|
+
* protects library/MCP callers that build a {@link TargetSelection} directly.
|
|
338
|
+
* - `allowAdhoc: false` (the MCP surface) rejects quickchr members entirely:
|
|
339
|
+
* the CDB is the MCP allowlist, and a live provider must not widen it (#134
|
|
340
|
+
* non-goal).
|
|
341
|
+
*/
|
|
342
|
+
function expandQuickchrSelection(
|
|
343
|
+
selection: TargetSelection,
|
|
344
|
+
input: SelectionResolveInput,
|
|
345
|
+
names: readonly string[],
|
|
346
|
+
): SelectionExpansion {
|
|
347
|
+
if (!input.allowAdhoc) {
|
|
348
|
+
throw new CentrsError({
|
|
349
|
+
code: "cdb/target-not-registered",
|
|
350
|
+
summary:
|
|
351
|
+
"quickchr targets are not available on this surface; the CDB is the allowlist.",
|
|
352
|
+
remediation:
|
|
353
|
+
"Register the device with `centrs devices add` and select it by identity, or use the CLI for `--quickchr` targets.",
|
|
354
|
+
context: { quickchr: [...names] },
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (
|
|
358
|
+
selection.positionals.length > 0 ||
|
|
359
|
+
selection.groups.length > 0 ||
|
|
360
|
+
selection.all ||
|
|
361
|
+
selection.default ||
|
|
362
|
+
selection.where.length > 0 ||
|
|
363
|
+
selection.near !== undefined ||
|
|
364
|
+
selection.bbox !== undefined
|
|
365
|
+
) {
|
|
366
|
+
throw new CentrsError({
|
|
367
|
+
code: "usage/conflicting-flags",
|
|
368
|
+
summary:
|
|
369
|
+
"`--quickchr` is exclusive: a command targets quickchr machines or CDB/literal targets, not both.",
|
|
370
|
+
remediation:
|
|
371
|
+
"Drop the positional/selector (or the `--quickchr` flag) so the selection has one target source.",
|
|
372
|
+
context: {
|
|
373
|
+
quickchr: [...names],
|
|
374
|
+
positionals: selection.positionals.length,
|
|
375
|
+
groups: selection.groups.length,
|
|
376
|
+
all: selection.all,
|
|
377
|
+
default: selection.default,
|
|
378
|
+
where: selection.where.length,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
const seen = new Set<string>();
|
|
383
|
+
const targets: SelectionMember[] = [];
|
|
384
|
+
for (const name of names) {
|
|
385
|
+
if (!seen.has(name)) {
|
|
386
|
+
seen.add(name);
|
|
387
|
+
targets.push({ kind: "quickchr", name });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return { targets, warnings: [], empty: false };
|
|
391
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The neutral per-service endpoint shape — the connection-fact origin ("Axis B")
|
|
3
|
+
* for a resolved target, decoupled from *how* the member was selected ("Axis A").
|
|
4
|
+
*
|
|
5
|
+
* This is the load-bearing scaffold for the resolution-provider refactor
|
|
6
|
+
* (`docs/CONSTITUTION.md` → Resolution providers; centrs #174). Two provider
|
|
7
|
+
* kinds feed it:
|
|
8
|
+
*
|
|
9
|
+
* - **Selector-source** — participates in the `--group`/`--where`/`--all`/
|
|
10
|
+
* `--near`/`--bbox` selector grammar. **CDB** today; **TikTOML** next (#137).
|
|
11
|
+
* CDB is the *degenerate single-endpoint* case: one record yields one
|
|
12
|
+
* host/port/auth, materialized lazily by `./target.ts` once `--via` is known,
|
|
13
|
+
* so a CDB member never needs a populated {@link ServiceEndpointMap}.
|
|
14
|
+
* - **Named-live-provider** — addressed only by an explicit named flag, never
|
|
15
|
+
* matched by selectors, fanned out by repeating the flag; owns an ephemeral
|
|
16
|
+
* endpoint so it also bypasses the settings/`__default__` ladder and conflicts
|
|
17
|
+
* with `--host`/`--port`/`--username`/`--password`/`--ssh-key`. **quickchr**
|
|
18
|
+
* (`--quickchr <name>`, #134). A live provider resolves a member into a
|
|
19
|
+
* *populated* per-service map, because it genuinely exposes different
|
|
20
|
+
* host/port/auth per transport (e.g. a SLiRP-forwarded loopback port per
|
|
21
|
+
* service). Flattening that to a single host/port is the exact refactor #174
|
|
22
|
+
* exists to prevent — TikTOML's `services` block is per-service too.
|
|
23
|
+
*
|
|
24
|
+
* The shape here deliberately mirrors quickchr's published descriptor contract
|
|
25
|
+
* (`tikoci/quickchr` `docs/centrs-interface.md`, `ServiceEndpoint`/
|
|
26
|
+
* `SshServiceEndpoint`), which states that centrs reuses *this same internal
|
|
27
|
+
* type* for TikTOML resolution. It is provider-neutral: `source.provider` is a
|
|
28
|
+
* free string, not a quickchr literal. Mapping a resolved endpoint's auth onto
|
|
29
|
+
* centrs' {@link ../resolver/target.ts | ResolvedAuth} (notably
|
|
30
|
+
* `privateKeyPath` → `sshKey`) is the provider adapter's job, not this type's.
|
|
31
|
+
*
|
|
32
|
+
* Phase 1 (this file) defines the contract only; the quickchr provider adapter
|
|
33
|
+
* that *produces* a map, and the `--via` consumer that *reads* `map[via]`, land
|
|
34
|
+
* with the `--quickchr` plumbing (#134 Phase 2+).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { RouterOsProtocol } from "../protocols/index.ts";
|
|
38
|
+
|
|
39
|
+
/** Provenance for a resolved endpoint — which provider vouched for it. */
|
|
40
|
+
export interface ServiceEndpointSource {
|
|
41
|
+
/** Provider identity, e.g. `"quickchr"` or `"tiktoml"`. Neutral by design. */
|
|
42
|
+
provider: string;
|
|
43
|
+
/** Provider-internal forward name backing the service (e.g. `"api-ssl"`), for provenance. */
|
|
44
|
+
portMappingName?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** REST/native-api auth. RouterOS shares one credential across REST and native. */
|
|
48
|
+
export interface ServiceEndpointAuth {
|
|
49
|
+
username: string;
|
|
50
|
+
password?: string;
|
|
51
|
+
/** Pre-encoded HTTP Basic value, when the provider supplies one directly. */
|
|
52
|
+
basic?: string;
|
|
53
|
+
/** Pre-formed auth header value, when the provider supplies one directly. */
|
|
54
|
+
header?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** SSH auth. Same credential vocabulary `+ key path`, plus batch-mode capability. */
|
|
58
|
+
export interface SshEndpointAuth {
|
|
59
|
+
username: string;
|
|
60
|
+
/**
|
|
61
|
+
* Path to a usable private key. Populated only when the provider has actually
|
|
62
|
+
* verified a batch (non-interactive) login with it — maps to
|
|
63
|
+
* {@link ../resolver/target.ts | ResolvedAuth.sshKey} in the provider adapter.
|
|
64
|
+
*/
|
|
65
|
+
privateKeyPath?: string;
|
|
66
|
+
/** Every auth mode the endpoint accepts. */
|
|
67
|
+
modes: Array<"private-key" | "agent-or-config" | "password">;
|
|
68
|
+
/**
|
|
69
|
+
* The batch-capable subset. This is the gate centrs enforces for `--via ssh`
|
|
70
|
+
* and `transfer --via sftp`: an empty list means no non-interactive handoff is
|
|
71
|
+
* possible, so centrs must emit a typed unsupported-capability error rather
|
|
72
|
+
* than prompt for a password or silently fall back (#134 Phase 4).
|
|
73
|
+
*/
|
|
74
|
+
batchModes: Array<"private-key" | "agent-or-config">;
|
|
75
|
+
passwordAvailable?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Fields shared by every available endpoint regardless of auth flavor. */
|
|
79
|
+
interface AvailableEndpointBase {
|
|
80
|
+
available: true;
|
|
81
|
+
/** Hostname/IP to dial — NOT a port. `"127.0.0.1"` for a SLiRP-forwarded service. */
|
|
82
|
+
host: string;
|
|
83
|
+
port: number;
|
|
84
|
+
guestPort?: number;
|
|
85
|
+
transport: "tcp" | "udp";
|
|
86
|
+
/** True for TLS-backed endpoints (https / api-ssl); SSH is always `false` (own transport security). */
|
|
87
|
+
tls: boolean;
|
|
88
|
+
url?: string;
|
|
89
|
+
source?: ServiceEndpointSource;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** An endpoint the provider could not vouch for; a consumer MUST gate on `available` before dialing. */
|
|
93
|
+
export interface UnavailableEndpoint {
|
|
94
|
+
available: false;
|
|
95
|
+
unavailableReason: string;
|
|
96
|
+
/** Best-effort echoes only — never dial these without a re-resolve. */
|
|
97
|
+
host?: string;
|
|
98
|
+
port?: number;
|
|
99
|
+
guestPort?: number;
|
|
100
|
+
transport?: "tcp" | "udp";
|
|
101
|
+
tls?: boolean;
|
|
102
|
+
url?: string;
|
|
103
|
+
source?: ServiceEndpointSource;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** One per-service endpoint (rest-api / native-api). */
|
|
107
|
+
export type ServiceEndpoint =
|
|
108
|
+
| (AvailableEndpointBase & { auth?: ServiceEndpointAuth })
|
|
109
|
+
| UnavailableEndpoint;
|
|
110
|
+
|
|
111
|
+
/** The SSH endpoint — same union, with SSH-specific auth substituted. */
|
|
112
|
+
export type SshServiceEndpoint =
|
|
113
|
+
| (AvailableEndpointBase & { auth: SshEndpointAuth })
|
|
114
|
+
| UnavailableEndpoint;
|
|
115
|
+
|
|
116
|
+
/** Any resolved endpoint, whatever the service. */
|
|
117
|
+
export type AnyServiceEndpoint = ServiceEndpoint | SshServiceEndpoint;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The endpoint type for a given `--via` id. `ssh` carries the SSH-specific
|
|
121
|
+
* {@link SshServiceEndpoint} (its available form guarantees `auth: SshEndpointAuth`
|
|
122
|
+
* with `batchModes`, the gate Phase 4 enforces for `--via ssh`/`transfer --via
|
|
123
|
+
* sftp`); every other transport uses the shared credential {@link ServiceEndpoint}.
|
|
124
|
+
*/
|
|
125
|
+
export type ServiceEndpointFor<K extends RouterOsProtocol> = K extends "ssh"
|
|
126
|
+
? SshServiceEndpoint
|
|
127
|
+
: ServiceEndpoint;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A member's per-service connection facts, keyed by centrs `--via` id.
|
|
131
|
+
*
|
|
132
|
+
* Partial by design: a provider may cover only a subset of the eight protocols
|
|
133
|
+
* (quickchr v1 covers `rest-api`/`native-api`/`ssh`). A `--via` whose key is
|
|
134
|
+
* absent is a typed unsupported-provider error at consume time, not a silent
|
|
135
|
+
* fallback. CDB never populates this map (its single endpoint is materialized by
|
|
136
|
+
* `./target.ts`); only named-live-providers do.
|
|
137
|
+
*
|
|
138
|
+
* Keyed by service so per-`--via` precision survives: `map.ssh` is a
|
|
139
|
+
* {@link SshServiceEndpoint}, not a widened `AnyServiceEndpoint`, so a consumer
|
|
140
|
+
* reading `map.ssh` keeps the SSH `auth`/`batchModes` guarantee without a cast.
|
|
141
|
+
*/
|
|
142
|
+
export type ServiceEndpointMap = {
|
|
143
|
+
[K in RouterOsProtocol]?: ServiceEndpointFor<K>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Narrow an endpoint to its available form (the only form safe to dial). Generic
|
|
148
|
+
* so the caller's precise union is preserved: an {@link SshServiceEndpoint} narrows
|
|
149
|
+
* to its available arm (`auth: SshEndpointAuth` required), not the widened base.
|
|
150
|
+
*/
|
|
151
|
+
export function isEndpointAvailable<T extends AnyServiceEndpoint>(
|
|
152
|
+
endpoint: T,
|
|
153
|
+
): endpoint is Extract<T, { available: true }> {
|
|
154
|
+
return endpoint.available;
|
|
155
|
+
}
|
package/src/resolver/target.ts
CHANGED
|
@@ -455,6 +455,6 @@ export function defaultPortForScheme(scheme: "http" | "https"): number {
|
|
|
455
455
|
return scheme === "https" ? 443 : 80;
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
-
function formatHostForUrl(host: string): string {
|
|
458
|
+
export function formatHostForUrl(host: string): string {
|
|
459
459
|
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
460
460
|
}
|
package/src/retrieve-fanout.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Boundaries (see `commands/retrieve/README.md`, `docs/CONSTITUTION.md`), shared
|
|
14
14
|
* with `api-fanout.ts`:
|
|
15
|
-
* - The CDB is loaded + decrypted ONCE (`
|
|
15
|
+
* - The CDB is loaded + decrypted ONCE (`expandSelection`); a cdb member is
|
|
16
16
|
* resolved with its pre-resolved record, so `resolveCdb`'s `__default__`
|
|
17
17
|
* synthetic fallback never collides ad-hoc literals.
|
|
18
18
|
* - A `--default` / `__default__` member is guarded from being dialed as the
|
|
@@ -49,12 +49,12 @@ import {
|
|
|
49
49
|
import { CentrsError, serializeCentrsError } from "./errors.ts";
|
|
50
50
|
import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
|
|
51
51
|
import {
|
|
52
|
-
|
|
53
|
-
type CdbSelectionMember,
|
|
54
|
-
type CdbSelectionResolveInput,
|
|
55
|
-
expandCdbSelection,
|
|
52
|
+
expandSelection,
|
|
56
53
|
isDefaultRecordTarget,
|
|
57
54
|
loadEnvFileDefaults,
|
|
55
|
+
type SelectionExpansion,
|
|
56
|
+
type SelectionMember,
|
|
57
|
+
type SelectionResolveInput,
|
|
58
58
|
type TargetSelection,
|
|
59
59
|
} from "./resolver/index.ts";
|
|
60
60
|
import {
|
|
@@ -82,6 +82,8 @@ export interface RetrieveSelectionSummary {
|
|
|
82
82
|
all: boolean;
|
|
83
83
|
default: boolean;
|
|
84
84
|
positionals: readonly string[];
|
|
85
|
+
/** quickchr machine names (`--quickchr`, repeatable; exclusive of the rest). */
|
|
86
|
+
quickchr?: readonly string[];
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
export interface RetrieveFanoutOperationMeta {
|
|
@@ -109,10 +111,10 @@ export interface RetrieveFanoutInternals {
|
|
|
109
111
|
/** Selection expansion. Defaults to the real CDB-backed expansion. */
|
|
110
112
|
expand?: (
|
|
111
113
|
selection: TargetSelection,
|
|
112
|
-
input:
|
|
114
|
+
input: SelectionResolveInput,
|
|
113
115
|
env: Record<string, string | undefined>,
|
|
114
116
|
config?: Record<string, string | undefined>,
|
|
115
|
-
) => Promise<
|
|
117
|
+
) => Promise<SelectionExpansion>;
|
|
116
118
|
/** Backoff sleeper. Defaults to a real timer; tests pass a no-op. */
|
|
117
119
|
sleep?: (ms: number) => Promise<void>;
|
|
118
120
|
}
|
|
@@ -132,10 +134,11 @@ function summarizeSelection(
|
|
|
132
134
|
all: selection.all,
|
|
133
135
|
default: selection.default,
|
|
134
136
|
positionals: selection.positionals,
|
|
137
|
+
quickchr: selection.quickchr,
|
|
135
138
|
};
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
function memberTargetMeta(member:
|
|
141
|
+
function memberTargetMeta(member: SelectionMember): {
|
|
139
142
|
input?: string;
|
|
140
143
|
identity?: string;
|
|
141
144
|
recordIndex?: number;
|
|
@@ -147,6 +150,9 @@ function memberTargetMeta(member: CdbSelectionMember): {
|
|
|
147
150
|
recordIndex: member.recordIndex,
|
|
148
151
|
};
|
|
149
152
|
}
|
|
153
|
+
if (member.kind === "quickchr") {
|
|
154
|
+
return { input: member.name, identity: member.name };
|
|
155
|
+
}
|
|
150
156
|
return { input: member.input };
|
|
151
157
|
}
|
|
152
158
|
|
|
@@ -159,7 +165,7 @@ function memberTargetMeta(member: CdbSelectionMember): {
|
|
|
159
165
|
*/
|
|
160
166
|
function relabelAdhocMember(
|
|
161
167
|
envelope: RetrieveEnvelope,
|
|
162
|
-
member:
|
|
168
|
+
member: SelectionMember,
|
|
163
169
|
): RetrieveEnvelope {
|
|
164
170
|
if (member.kind !== "literal") {
|
|
165
171
|
return envelope;
|
|
@@ -194,7 +200,7 @@ export async function retrieveFanout(
|
|
|
194
200
|
);
|
|
195
201
|
const selectionSummary = summarizeSelection(selection);
|
|
196
202
|
|
|
197
|
-
const expand = internals.expand ??
|
|
203
|
+
const expand = internals.expand ?? expandSelection;
|
|
198
204
|
const expansion = await expand(
|
|
199
205
|
selection,
|
|
200
206
|
{
|
|
@@ -234,7 +240,7 @@ export async function retrieveFanout(
|
|
|
234
240
|
const sleep = internals.sleep ?? defaultFanoutSleep;
|
|
235
241
|
|
|
236
242
|
const targets = await runFanout<
|
|
237
|
-
|
|
243
|
+
SelectionMember,
|
|
238
244
|
ResolvedRetrieveRequest,
|
|
239
245
|
RetrieveEnvelope
|
|
240
246
|
>({
|
|
@@ -267,6 +273,14 @@ export async function retrieveFanout(
|
|
|
267
273
|
config,
|
|
268
274
|
);
|
|
269
275
|
}
|
|
276
|
+
if (member.kind === "quickchr") {
|
|
277
|
+
// The live descriptor resolves here, per member — a stopped/unknown
|
|
278
|
+
// machine is an inner per-target failure, not an expansion error.
|
|
279
|
+
return resolveRetrieveRequest(
|
|
280
|
+
{ ...request, targetInput: undefined, quickchr: member.name },
|
|
281
|
+
env,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
270
284
|
// Ad-hoc literal: full single-target resolution (CDB load → `__default__`
|
|
271
285
|
// fallback creds), mirroring single-target `retrieve`.
|
|
272
286
|
return resolveRetrieveRequest(
|
|
@@ -373,7 +387,7 @@ function resolveFanoutConcurrencyProtocol(
|
|
|
373
387
|
request: RetrieveRequest,
|
|
374
388
|
env: Record<string, string | undefined>,
|
|
375
389
|
globalVia: RouterOsProtocol,
|
|
376
|
-
expansion:
|
|
390
|
+
expansion: SelectionExpansion,
|
|
377
391
|
): RouterOsProtocol {
|
|
378
392
|
// Deliberately CLI/env only, not `config` — a `centrs.env` default is the
|
|
379
393
|
// weakest precedence tier and must not be treated as a hard pin the way an
|
package/src/retrieve.ts
CHANGED
|
@@ -26,11 +26,14 @@ import {
|
|
|
26
26
|
type RouterOsProtocol,
|
|
27
27
|
} from "./protocols/index.ts";
|
|
28
28
|
import {
|
|
29
|
+
assertNoQuickchrOverrideConflict,
|
|
29
30
|
type CdbResolution,
|
|
30
31
|
isIpTransport,
|
|
31
32
|
loadEnvFileDefaults,
|
|
32
33
|
parseDuration,
|
|
33
34
|
parseResolvePolicy,
|
|
35
|
+
type QuickchrResolution,
|
|
36
|
+
quickchrConnection,
|
|
34
37
|
type ResolvedAuth,
|
|
35
38
|
type ResolvedSetting,
|
|
36
39
|
type ResolvedTarget,
|
|
@@ -39,6 +42,7 @@ import {
|
|
|
39
42
|
resolveCdb,
|
|
40
43
|
resolveMacTarget,
|
|
41
44
|
resolveOptionalIntegerSetting,
|
|
45
|
+
resolveQuickchrTarget,
|
|
42
46
|
resolveStringSetting,
|
|
43
47
|
resolveTarget,
|
|
44
48
|
toCoreSource,
|
|
@@ -49,6 +53,12 @@ export type RetrieveOutputFormat = (typeof retrieveOutputFormats)[number];
|
|
|
49
53
|
|
|
50
54
|
export interface RetrieveRequest {
|
|
51
55
|
targetInput?: string;
|
|
56
|
+
/**
|
|
57
|
+
* quickchr machine name (`--quickchr <name>`): resolve host/port/auth from
|
|
58
|
+
* the live VM descriptor instead of CDB/env (`docs/CONSTITUTION.md` →
|
|
59
|
+
* Resolution providers). Conflicts with host/port/username/password.
|
|
60
|
+
*/
|
|
61
|
+
quickchr?: string;
|
|
52
62
|
path: string;
|
|
53
63
|
via?: string;
|
|
54
64
|
host?: string;
|
|
@@ -123,6 +133,14 @@ export interface ResolvedRetrieveRequest {
|
|
|
123
133
|
timeoutMs: ResolvedSetting<number>;
|
|
124
134
|
format: ResolvedSetting<RetrieveOutputFormat>;
|
|
125
135
|
validate: ResolvedSetting<boolean>;
|
|
136
|
+
/**
|
|
137
|
+
* Relax TLS peer verification. retrieve has no `--insecure` flag; this is set
|
|
138
|
+
* only by the quickchr provider (trust-by-provenance for the VM's self-signed
|
|
139
|
+
* TLS forward — see `src/resolver/quickchr-provider.ts`), so when present its
|
|
140
|
+
* source is always `provider` and it is surfaced as `meta.settings.insecure`
|
|
141
|
+
* for auditability.
|
|
142
|
+
*/
|
|
143
|
+
insecure?: ResolvedSetting<boolean>;
|
|
126
144
|
maxResultsBytes?: ResolvedSetting<number>;
|
|
127
145
|
attributes: readonly string[];
|
|
128
146
|
allAttributes: boolean;
|
|
@@ -161,6 +179,7 @@ export async function runResolvedRetrieve(
|
|
|
161
179
|
username: resolved.auth.username,
|
|
162
180
|
password: resolved.auth.password,
|
|
163
181
|
timeoutMs: resolved.timeoutMs.value,
|
|
182
|
+
insecure: resolved.insecure?.value,
|
|
164
183
|
});
|
|
165
184
|
|
|
166
185
|
try {
|
|
@@ -212,7 +231,8 @@ export async function runResolvedRetrieve(
|
|
|
212
231
|
"Check the attribute name, or use `--list-attributes` to inspect the available properties first.",
|
|
213
232
|
context: {
|
|
214
233
|
path: resolved.path,
|
|
215
|
-
|
|
234
|
+
parameter: missing[0],
|
|
235
|
+
requestedAttributes: resolved.attributes,
|
|
216
236
|
availableAttributes,
|
|
217
237
|
},
|
|
218
238
|
});
|
|
@@ -462,6 +482,9 @@ function metaFromResolved(
|
|
|
462
482
|
timeoutMs: toCoreSource(resolved.timeoutMs.source),
|
|
463
483
|
format: toCoreSource(resolved.format.source),
|
|
464
484
|
validate: toCoreSource(resolved.validate.source),
|
|
485
|
+
insecure: resolved.insecure
|
|
486
|
+
? toCoreSource(resolved.insecure.source)
|
|
487
|
+
: undefined,
|
|
465
488
|
maxResultsBytes: resolved.maxResultsBytes
|
|
466
489
|
? toCoreSource(resolved.maxResultsBytes.source)
|
|
467
490
|
: undefined,
|
|
@@ -620,6 +643,7 @@ export function buildResolvedRetrieve(
|
|
|
620
643
|
attributeSelections: readonly string[],
|
|
621
644
|
macResolution?: { mac: string; ip: string },
|
|
622
645
|
config: Record<string, string | undefined> = {},
|
|
646
|
+
quickchrResolution?: QuickchrResolution,
|
|
623
647
|
): ResolvedRetrieveRequest {
|
|
624
648
|
const via = resolveProtocol(request, env, cdbResolution, config);
|
|
625
649
|
const format = resolveFormat(request, env, config);
|
|
@@ -647,22 +671,33 @@ export function buildResolvedRetrieve(
|
|
|
647
671
|
undefined,
|
|
648
672
|
config,
|
|
649
673
|
);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
674
|
+
// A quickchr member substitutes the live descriptor's per-`--via` endpoint
|
|
675
|
+
// for the CDB/env target+auth path (`docs/CONSTITUTION.md` → Resolution
|
|
676
|
+
// providers); everything else (via/format/validate/timeout) resolved above
|
|
677
|
+
// stays on the normal ladder.
|
|
678
|
+
const connection = quickchrResolution
|
|
679
|
+
? quickchrConnection(quickchrResolution, via.value)
|
|
680
|
+
: null;
|
|
681
|
+
const target = connection
|
|
682
|
+
? connection.target
|
|
683
|
+
: resolveTarget(
|
|
684
|
+
{
|
|
685
|
+
targetInput: request.targetInput,
|
|
686
|
+
host: request.host,
|
|
687
|
+
port: request.port,
|
|
688
|
+
macResolution,
|
|
689
|
+
},
|
|
690
|
+
env,
|
|
691
|
+
via.value,
|
|
692
|
+
cdbResolution,
|
|
693
|
+
);
|
|
694
|
+
const auth = connection
|
|
695
|
+
? connection.auth
|
|
696
|
+
: resolveAuth(
|
|
697
|
+
{ username: request.username, password: request.password },
|
|
698
|
+
env,
|
|
699
|
+
cdbResolution,
|
|
700
|
+
);
|
|
666
701
|
|
|
667
702
|
return {
|
|
668
703
|
path: request.path,
|
|
@@ -672,12 +707,24 @@ export function buildResolvedRetrieve(
|
|
|
672
707
|
timeoutMs,
|
|
673
708
|
format,
|
|
674
709
|
validate,
|
|
710
|
+
insecure:
|
|
711
|
+
connection?.insecure === true && quickchrResolution
|
|
712
|
+
? {
|
|
713
|
+
value: true,
|
|
714
|
+
source: {
|
|
715
|
+
kind: "provider" as const,
|
|
716
|
+
key: `quickchr:${quickchrResolution.name}`,
|
|
717
|
+
},
|
|
718
|
+
}
|
|
719
|
+
: undefined,
|
|
675
720
|
maxResultsBytes,
|
|
676
721
|
attributes: attributeSelections,
|
|
677
722
|
allAttributes: request.allAttributes ?? false,
|
|
678
723
|
listAttributes: request.listAttributes ?? false,
|
|
679
724
|
verbose: request.verbose ?? false,
|
|
680
|
-
warnings:
|
|
725
|
+
warnings: connection
|
|
726
|
+
? [...connection.warnings]
|
|
727
|
+
: (cdbResolution?.warnings ?? []),
|
|
681
728
|
};
|
|
682
729
|
}
|
|
683
730
|
|
|
@@ -758,6 +805,22 @@ export async function resolveRetrieveRequest(
|
|
|
758
805
|
const attributeSelections = validateRetrieveRequestShape(request);
|
|
759
806
|
const config = await loadEnvFileDefaults(env);
|
|
760
807
|
|
|
808
|
+
// A quickchr target bypasses CDB and MAC resolution entirely — the live
|
|
809
|
+
// descriptor is the only connection-fact source for those fields.
|
|
810
|
+
if (request.quickchr !== undefined) {
|
|
811
|
+
assertNoQuickchrOverrideConflict(request, request.quickchr);
|
|
812
|
+
const quickchrResolution = await resolveQuickchrTarget(request.quickchr);
|
|
813
|
+
return buildResolvedRetrieve(
|
|
814
|
+
request,
|
|
815
|
+
env,
|
|
816
|
+
undefined,
|
|
817
|
+
attributeSelections,
|
|
818
|
+
undefined,
|
|
819
|
+
config,
|
|
820
|
+
quickchrResolution,
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
761
824
|
const cdbResolution = await resolveCdb(
|
|
762
825
|
{
|
|
763
826
|
targetInput: request.targetInput,
|