@knowttl/ssh-axi 0.0.0-stage → 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.
package/README.md CHANGED
@@ -1,3 +1,176 @@
1
- # Temporary Holding Version
1
+ # ssh-axi
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Discover approved SSH hosts, check encrypted-key authentication, and execute confirmed bounded commands.
4
+ Requires Node >=24.11.0 and pnpm 10.22.0.
5
+ The npm package is `@knowttl/ssh-axi`; its executable is `ssh-axi`.
6
+
7
+ ```sh
8
+ ssh-axi
9
+ ssh-axi host list
10
+ ssh-axi host show lab
11
+ ssh-axi doctor lab --json
12
+ ssh-axi doctor --profile lab
13
+ ssh-axi auth status --profile lab
14
+ ssh-axi profile list
15
+ ssh-axi config path
16
+ ssh-axi exec lab --command 'printf "%s\n" "quoted value"' --confirm --timeout 30s --json
17
+ ```
18
+
19
+ Output defaults to TOON; `--json` returns the same fields and empty-state meaning.
20
+ Each command accepts `--help`.
21
+ Unknown arguments and flags fail with exit code 2; operational failures use exit code 1.
22
+ Help, discovery, profile inspection and credential status open no SSH transports.
23
+ Native execution is one-shot.
24
+ Managed execution records, recovery and cancellation use the matching Artium host extension.
25
+ Reusable connections require the matching Artium connection-lifetime extension.
26
+ Managed sudo requires a distinct approved account and SSH credential with a current sudo grant.
27
+ Agents, forwarding and interactive shells remain unavailable.
28
+
29
+ Configuration lives at `~/.config/ssh-axi/config.json` on Linux, or beneath an absolute `XDG_CONFIG_HOME`.
30
+ Select a file with `--config` or `SSH_AXI_CONFIG`.
31
+ Profiles use the following template; replace the address, allowed IP and independently verified host key before use.
32
+
33
+ ```json
34
+ {
35
+ "version": 1,
36
+ "defaultProfile": "lab",
37
+ "profiles": {
38
+ "lab": {
39
+ "address": "127.0.0.1",
40
+ "port": 2222,
41
+ "username": "ordinary",
42
+ "allowedAddresses": ["127.0.0.1"],
43
+ "approvedHostKeys": ["ssh-ed25519 <independently-verified-base64-key>"],
44
+ "privateKey": {"file": "/absolute/private/key.pem"},
45
+ "passphrase": {"env": "LAB_KEY_PASSPHRASE"}
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ The strict profile contract is shipped as `axi-credential.schema.json`.
52
+ Keys and passphrases accept environment or protected-file SecretRefs only.
53
+ Secret values and inline private keys are refused in JSON and arguments.
54
+ An omitted private-key reference defaults to `SSH_AXI_PRIVATE_KEY`; the passphrase defaults to `SSH_AXI_PASSPHRASE`.
55
+ `auth status` reports credential availability and sources as `env:<name>` or `protected file`, without displaying secret values or file paths or checking network authentication.
56
+ Linux secret files must be owned by the caller, regular, protected from group/other access and reachable through protected directories without symlinks.
57
+ Configuration files must also be owned and protected against group/other writes.
58
+
59
+ Profile precedence is explicit host/`--profile`, `SSH_AXI_PROFILE`, a complete environment profile, the default profile, then the only profile.
60
+ An explicitly selected file profile ignores competing endpoint environment fields.
61
+ Environment profiles use `SSH_AXI_ADDRESS`, `SSH_AXI_PORT`, `SSH_AXI_USERNAME`, JSON arrays in `SSH_AXI_APPROVED_HOST_KEYS` and `SSH_AXI_ALLOWED_ADDRESSES`, and the secret environment fields above.
62
+ Partial environment profiles fail.
63
+ No SSH config, agent, known-hosts file or ambient key is consulted.
64
+
65
+ Every new transport, including doctor probes and reusable connections, validates every resolved IP against the configured address set and dials the exact validated IP.
66
+ An exact approved server public key is required before sending authentication credentials.
67
+ The authentication deadline is 15 seconds, including resolution and connection setup.
68
+ Doctor requests no SSH channel and closes its socket, probe worker and private directories before returning.
69
+ Private-key buffers are released where supported; JavaScript strings and process termination do not provide a secure-erasure guarantee.
70
+
71
+ Artium supplies authenticated host grants, separate ordinary and sudo account bindings, encrypted SSH credentials, private IPC and one isolated `artium` profile through its shared writer.
72
+ Hosted callers may list and show their granted hosts, run doctor and submit confirmed commands when the host write policy permits.
73
+ They cannot supply configuration, account, trust, secret, identity or worker overrides.
74
+ The matching Artium admission change must be installed alongside this artifact; stock Artium does not admit it.
75
+ The matching host extension provides `/admin/ssh` for confirmed, revision-checked host, trust, binding and grant changes.
76
+ Server keys require independent verification and at most two approved keys may overlap during rotation.
77
+ Host and credential administration have separate delegated permissions; only credential administrators can replace or delete encrypted SSH secrets, which are never displayed.
78
+ SSH credential identifiers cannot resolve credentials for other tools.
79
+ Changes fence affected connections before acknowledgement.
80
+ The corresponding authenticated APIs live under `/v1/admin/ssh/`.
81
+
82
+ Every arbitrary command requires `--confirm`, including inspection-looking commands.
83
+ `SSH_AXI_READ_ONLY=true` rejects arbitrary execution before opening a transport or writing a command journal.
84
+ Commands must contain 1-16384 UTF-8 bytes and no NUL.
85
+ Known managed secrets and encoded variants are rejected before command journaling.
86
+ Native command intents are written durably to `~/.local/state/ssh-axi/writes.log`, or the absolute `XDG_STATE_HOME` override.
87
+ `SSH_AXI_WRITE_LOG` selects an absolute protected journal path.
88
+ No local shell is invoked; the exact command text is sent to the remote SSH exec channel.
89
+ Remote stdin is closed without forwarding input; both output streams continue draining.
90
+ Native Ctrl+C requests cancellation through the private worker.
91
+
92
+ ```sh
93
+ artium-axi ssh-axi exec lab --command 'systemctl --failed' --confirm --detach
94
+ artium-axi ssh-axi execution list --json
95
+ artium-axi ssh-axi execution show <id> --full
96
+ artium-axi ssh-axi execution show --request-key <key>
97
+ artium-axi ssh-axi execution show
98
+ artium-axi ssh-axi execution cancel <id> --confirm
99
+ artium-axi ssh-axi connection open lab --idle-timeout 10m
100
+ artium-axi ssh-axi connection list
101
+ artium-axi ssh-axi connection show <connection-id>
102
+ artium-axi ssh-axi connection set <connection-id> --idle-timeout 5m
103
+ artium-axi ssh-axi exec lab --connection <connection-id> --command 'pwd' --confirm
104
+ artium-axi ssh-axi exec lab --sudo --command 'printf "%s\n" "value" | cat' --confirm
105
+ artium-axi ssh-axi connection close <connection-id>
106
+ ```
107
+
108
+ Hosted connections belong to one authenticated user and login session family.
109
+ Each command starts a fresh remote process on the same transport.
110
+ An explicit connection must match the selected host and ordinary or sudo binding, with no fallback reconnection.
111
+ Sudo dispatch sends `sudo -n -- /bin/sh -c <quoted-command>` through its own matching connection.
112
+ Pipelines and redirects remain inside that privileged shell.
113
+ Password-required or terminal-required sudo returns `SUDO_DENIED` with administrator guidance.
114
+ Controlled peers establish account, key and command selection only.
115
+ Actual ordinary-account non-elevation and successful non-root passwordless elevation remain commissioning checks.
116
+ There is one active command per connection; overlapping use returns `CONNECTION_BUSY`.
117
+ Closing an active connection requires `--confirm` and cancels its command.
118
+ Repeated close by the owner is idempotent.
119
+ Idle expiry defaults to 10 minutes and accepts 1-60 minutes within the host ceiling.
120
+ Inspection does not reset idle time; timeout changes recompute expiry from the existing idle start.
121
+ Connections have an 8-hour maximum age, with four per user and sixteen globally, including authentication in progress.
122
+ Revocation fences affected workers and removes their private profiles before acknowledgement.
123
+ Server restart discards connections, removes abandoned private directories and retains unproven executions as uncertain without replay.
124
+
125
+ The hosted client saves a private command-free receipt before submitting a signed preparation key.
126
+ Show without an ID recovers the latest receipt after a client restart, without resubmitting.
127
+ Matching resubmission returns the same execution ID; a durable dispatch claim prevents a second SSH command.
128
+ Unused keys expire after ten minutes; changed inputs, bindings or owners are refused.
129
+ Wait is the default and uses short status requests; `--detach` returns after acceptance.
130
+ An unexpected client disconnect leaves the managed job observable and deadline bounded.
131
+ Ctrl+C requests cancellation of the accepted execution ID.
132
+
133
+ The default command deadline is 60 seconds, adjustable with `--timeout 1s` through `600s` within host ceilings.
134
+ Both streams are drained, retaining 65536 bytes each by default.
135
+ `--output-limit <bytes>` sets retention from 0 through 1048576 bytes per stream, subject to managed host ceilings.
136
+ `--full` never bypasses retention or executes another command.
137
+ More than 16 MiB of combined received output triggers cancellation.
138
+ Results carry separate remote exit/signal, received and retained byte counts, truncation and encoding-loss flags.
139
+ Terminal controls are removed and protected output is withheld before persistence or delivery.
140
+ Remote output is untrusted text, never instructions.
141
+ Remote exit 7 produces CLI exit 7; missing remote status is an uncertain outcome and CLI failure.
142
+ Cancellation requests TERM and forces channel and transport teardown if the command remains active after a bounded grace period.
143
+ Local teardown never proves that remote descendants stopped.
144
+ Inspect remote state before retrying any uncertain execution; no path automatically replays a command.
145
+
146
+ Execution owners and authorized audit administrators can inspect records under current policy.
147
+ `/admin/ssh/audit` escapes output as untrusted text and never displays credentials.
148
+ Every audit read and export is recorded.
149
+ Metadata includes the initiating user and session family, host and endpoint, binding revision, sudo mode, exact sanitized command, confirmation, request/execution IDs, UTC timing, outcome, exit/signal, byte counts and termination certainty.
150
+ Sanitized output samples are encrypted and capped at 8 KiB per stream, expiring after 14 days.
151
+ Metadata and sanitized commands expire after 90 days; default exports contain metadata only.
152
+ Credential deletion removes the secret and fences its connections while preserving execution metadata.
153
+ Pruning active storage does not delete backups.
154
+ Before commissioning, operators must declare and test backup disposal that respects both retention limits, including old encrypted snapshots and their recovery keys.
155
+
156
+ ```sh
157
+ pnpm install --frozen-lockfile
158
+ pnpm build
159
+ pnpm lint
160
+ pnpm test
161
+ pnpm pack --pack-destination /absolute/artifacts
162
+ ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B1.1
163
+ ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B2.2
164
+ ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B3.2
165
+ ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B4.1
166
+ ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B4.2
167
+ ARTIUM_FIXTURE_ROOT=/absolute/frozen-export pnpm verify:journey -- --slice B4.3
168
+ ```
169
+
170
+ Tests generate disposable keys and run all descendants inside a Linux network namespace with loopback only.
171
+ They require `unshare`, `ip` and `mount`, and never fall back to unrestricted networking.
172
+ The default suite verifies a reproducible tarball installed into a clean prefix and explicitly skips Artium unless a fixture is provided.
173
+ The opt-in journey requires an exported, admitted Artium fixture with its dependencies installed.
174
+ The fixture harness seeds minimal pre-existing hosts directly in its disposable database and exercises B4 changes through public administration; no fixture-seeding route exists in the product.
175
+ On Windows, the default configuration path is `%APPDATA%/ssh-axi/config.json`, with an absolute `APPDATA` required.
176
+ DACL verification and native Windows evidence remain pending, so protected-file operations fail closed there.
@@ -0,0 +1,164 @@
1
+ {
2
+ "revision": 1,
3
+ "commands": [
4
+ {
5
+ "command": "home",
6
+ "arguments": 0,
7
+ "flags": {
8
+ "help": "boolean",
9
+ "json": "boolean"
10
+ },
11
+ "effect": "read"
12
+ },
13
+ {
14
+ "command": "host list",
15
+ "arguments": 0,
16
+ "flags": {
17
+ "help": "boolean",
18
+ "json": "boolean"
19
+ },
20
+ "effect": "read"
21
+ },
22
+ {
23
+ "command": "host show",
24
+ "arguments": 1,
25
+ "flags": {
26
+ "help": "boolean",
27
+ "json": "boolean"
28
+ },
29
+ "effect": "read"
30
+ },
31
+ {
32
+ "command": "doctor",
33
+ "arguments": 1,
34
+ "flags": {
35
+ "help": "boolean",
36
+ "json": "boolean"
37
+ },
38
+ "effect": "read"
39
+ },
40
+ {
41
+ "command": "auth status",
42
+ "arguments": 0,
43
+ "flags": {
44
+ "help": "boolean",
45
+ "json": "boolean"
46
+ },
47
+ "effect": "read"
48
+ },
49
+ {
50
+ "command": "profile list",
51
+ "arguments": 0,
52
+ "flags": {
53
+ "help": "boolean",
54
+ "json": "boolean"
55
+ },
56
+ "effect": "read"
57
+ },
58
+ {
59
+ "command": "config path",
60
+ "arguments": 0,
61
+ "flags": {
62
+ "help": "boolean",
63
+ "json": "boolean"
64
+ },
65
+ "effect": "read"
66
+ },
67
+ {
68
+ "command": "exec",
69
+ "arguments": 1,
70
+ "flags": {
71
+ "help": "boolean",
72
+ "json": "boolean",
73
+ "command": "string",
74
+ "confirm": "boolean",
75
+ "timeout": "string",
76
+ "output-limit": "string",
77
+ "detach": "boolean",
78
+ "request-key": "string",
79
+ "sudo": "boolean",
80
+ "connection": "string"
81
+ },
82
+ "effect": "write"
83
+ },
84
+ {
85
+ "command": "connection open",
86
+ "arguments": 1,
87
+ "flags": {
88
+ "help": "boolean",
89
+ "json": "boolean",
90
+ "idle-timeout": "string",
91
+ "sudo": "boolean"
92
+ },
93
+ "effect": "read"
94
+ },
95
+ {
96
+ "command": "connection list",
97
+ "arguments": 0,
98
+ "flags": {
99
+ "help": "boolean",
100
+ "json": "boolean"
101
+ },
102
+ "effect": "read"
103
+ },
104
+ {
105
+ "command": "connection show",
106
+ "arguments": 1,
107
+ "flags": {
108
+ "help": "boolean",
109
+ "json": "boolean"
110
+ },
111
+ "effect": "read"
112
+ },
113
+ {
114
+ "command": "connection set",
115
+ "arguments": 1,
116
+ "flags": {
117
+ "help": "boolean",
118
+ "json": "boolean",
119
+ "idle-timeout": "string"
120
+ },
121
+ "effect": "read"
122
+ },
123
+ {
124
+ "command": "connection close",
125
+ "arguments": 1,
126
+ "flags": {
127
+ "help": "boolean",
128
+ "json": "boolean",
129
+ "confirm": "boolean"
130
+ },
131
+ "effect": "write"
132
+ },
133
+ {
134
+ "command": "execution list",
135
+ "arguments": 0,
136
+ "flags": {
137
+ "help": "boolean",
138
+ "json": "boolean"
139
+ },
140
+ "effect": "read"
141
+ },
142
+ {
143
+ "command": "execution show",
144
+ "arguments": 1,
145
+ "flags": {
146
+ "help": "boolean",
147
+ "json": "boolean",
148
+ "request-key": "string",
149
+ "full": "boolean"
150
+ },
151
+ "effect": "read"
152
+ },
153
+ {
154
+ "command": "execution cancel",
155
+ "arguments": 1,
156
+ "flags": {
157
+ "help": "boolean",
158
+ "json": "boolean",
159
+ "confirm": "boolean"
160
+ },
161
+ "effect": "write"
162
+ }
163
+ ]
164
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "SSH profile",
4
+ "x-protocol-revision": 1,
5
+ "x-connection-protocol-revision": 2,
6
+ "x-commands": {
7
+ "": { "arguments": 0, "description": "Discover configured SSH hosts" },
8
+ "host list": { "arguments": 0, "description": "List approved hosts without opening a transport" },
9
+ "host show": { "arguments": 1, "description": "Show one approved host without opening a transport" },
10
+ "doctor": { "arguments": 1, "description": "Authenticate to one host without executing a remote command" },
11
+ "auth status": { "arguments": 0, "description": "Inspect selected credential sources offline" },
12
+ "profile list": { "arguments": 0, "description": "List configured profiles offline" },
13
+ "config path": { "arguments": 0, "description": "Show the configuration path offline" },
14
+ "exec": { "arguments": 1, "description": "Execute one confirmed arbitrary command", "flags": { "command": "string", "confirm": "boolean", "timeout": "string", "output-limit": "string", "detach": "boolean", "request-key": "string", "sudo": "boolean", "connection": "string" } },
15
+ "connection open": { "arguments": 1, "description": "Open a private hosted connection", "flags": { "idle-timeout": "string", "sudo": "boolean" } },
16
+ "connection list": { "arguments": 0, "description": "List caller-owned hosted connections" },
17
+ "connection show": { "arguments": 1, "description": "Show a caller-owned hosted connection" },
18
+ "connection set": { "arguments": 1, "description": "Change a hosted connection idle timeout", "flags": { "idle-timeout": "string" } },
19
+ "connection close": { "arguments": 1, "description": "Close a caller-owned hosted connection", "flags": { "confirm": "boolean" } },
20
+ "execution list": { "arguments": 0, "description": "List caller-owned executions" },
21
+ "execution show": { "arguments": 1, "description": "Recover an execution without replay", "flags": { "request-key": "string", "full": "boolean" } },
22
+ "execution cancel": { "arguments": 1, "description": "Cancel one caller-owned execution", "flags": { "confirm": "boolean" } }
23
+ },
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "required": ["address", "port", "username", "approvedHostKeys", "allowedAddresses"],
27
+ "properties": {
28
+ "address": { "type": "string", "minLength": 1, "maxLength": 253, "pattern": "^[A-Za-z0-9.:_-]+$", "description": "Approved SSH hostname or IP address; every resolved address must be allowed." },
29
+ "port": { "type": "integer", "minimum": 1, "maximum": 65535, "description": "Approved SSH TCP port." },
30
+ "username": { "type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[A-Za-z0-9_.-]+$", "description": "Ordinary SSH account selected by the host binding." },
31
+ "approvedHostKeys": { "type": "array", "minItems": 1, "maxItems": 2, "uniqueItems": true, "description": "Independently verified server public keys, each as algorithm plus base64 key; at most two overlap during rotation.", "items": { "type": "string", "pattern": "^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(256|384|521)) [A-Za-z0-9+/]+={0,2}$" } },
32
+ "allowedAddresses": { "type": "array", "minItems": 1, "uniqueItems": true, "description": "Approved literal IPv4 or IPv6 addresses; DNS cannot expand this set.", "items": { "type": "string", "minLength": 2 } },
33
+ "privateKey": { "$ref": "#/definitions/secretRef", "x-secret": true },
34
+ "passphrase": { "$ref": "#/definitions/secretRef", "x-secret": true }
35
+ },
36
+ "definitions": {
37
+ "secretRef": {
38
+ "oneOf": [
39
+ { "type": "object", "additionalProperties": false, "required": ["env"], "properties": { "env": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" } } },
40
+ { "type": "object", "additionalProperties": false, "required": ["file"], "properties": { "file": { "type": "string", "minLength": 1 } } }
41
+ ]
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,17 @@
1
+ export declare const commands: Record<string, {
2
+ arguments: number;
3
+ description: string;
4
+ flags?: Record<string, string>;
5
+ }>;
6
+ export declare const flagHelp = "--json, --help, --config <absolute-path>, --profile <name>";
7
+ export declare function parseArguments(args: readonly string[]): {
8
+ command: string;
9
+ json: boolean;
10
+ help: boolean;
11
+ config: string | undefined;
12
+ profile: string | undefined;
13
+ values: Record<string, string | boolean>;
14
+ timeout: number;
15
+ outputLimit: number;
16
+ id: string | undefined;
17
+ };
@@ -0,0 +1,71 @@
1
+ import schema from '../axi-credential.schema.json' with { type: 'json' };
2
+ import { SshError } from './errors.js';
3
+ export const commands = schema['x-commands'];
4
+ export const flagHelp = '--json, --help, --config <absolute-path>, --profile <name>';
5
+ export function parseArguments(args) {
6
+ let prefix = 0;
7
+ while (['--json', '--help', '--config', '--profile'].includes(args[prefix]))
8
+ prefix += ['--config', '--profile'].includes(args[prefix]) ? 2 : 1;
9
+ const commandName = args[prefix] === 'exec' || args[prefix] === 'doctor' ? args[prefix] : args.slice(prefix, prefix + 2).join(' ');
10
+ const extraFlags = commands[commandName]?.flags ?? {};
11
+ const values = {};
12
+ const positional = [];
13
+ let json = false;
14
+ let help = false;
15
+ let config;
16
+ let profile;
17
+ const seen = new Set();
18
+ for (let index = 0; index < args.length; index++) {
19
+ const arg = args[index];
20
+ if (!arg.startsWith('-')) {
21
+ positional.push(arg);
22
+ continue;
23
+ }
24
+ if (!['--json', '--help', '--config', '--profile', ...Object.keys(extraFlags).map(key => `--${key}`)].includes(arg)) {
25
+ throw new SshError('USAGE', `Unknown flag. Valid flags: ${flagHelp}${Object.keys(extraFlags).map(key => `, --${key}`).join('')}.`, 2);
26
+ }
27
+ if (seen.has(arg))
28
+ throw new SshError('USAGE', 'Flags may only appear once.', 2);
29
+ seen.add(arg);
30
+ if (arg === '--json')
31
+ json = true;
32
+ else if (arg === '--help')
33
+ help = true;
34
+ else if (extraFlags[arg.slice(2)] === 'boolean')
35
+ values[arg.slice(2)] = true;
36
+ else {
37
+ const value = args[++index];
38
+ if (value === undefined || (arg !== '--command' && (!value || value.startsWith('-'))))
39
+ throw new SshError('USAGE', `A flag value is required. Valid flags: ${flagHelp}.`, 2);
40
+ if (arg === '--config')
41
+ config = value;
42
+ else if (arg === '--profile')
43
+ profile = value;
44
+ else
45
+ values[arg.slice(2)] = value;
46
+ }
47
+ }
48
+ const command = ['doctor', 'exec'].includes(positional[0]) ? positional[0] : positional.slice(0, 2).join(' ');
49
+ const definition = commands[command];
50
+ const count = command === '' ? 0 : ['doctor', 'exec'].includes(command) ? 1 : 2;
51
+ const selectedDoctor = command === 'doctor' && positional.length === count;
52
+ const receiptLookup = command === 'execution show' && positional.length === count;
53
+ if (!definition || positional.length !== count + definition.arguments && !selectedDoctor && !receiptLookup && !(help && positional.length === count)) {
54
+ throw new SshError('USAGE', 'Use ssh-axi host list, host show <host>, doctor <host>, exec <host> --command <text> --confirm, or execution list/show/cancel.', 2);
55
+ }
56
+ if (['doctor', 'host show', 'exec'].includes(command) && positional[count] !== undefined) {
57
+ if (profile !== undefined)
58
+ throw new SshError('USAGE', 'Use the host argument to select the profile for this command.', 2);
59
+ profile = positional[count];
60
+ }
61
+ if (command === 'execution show' && positional[count] && values['request-key'])
62
+ throw new SshError('USAGE', 'Use an execution ID or --request-key, not both.', 2);
63
+ const timeoutValue = values.timeout ?? '60s';
64
+ const timeout = typeof timeoutValue === 'string' && /^\d+s?$/.test(timeoutValue) ? Number(timeoutValue.replace(/s$/, '')) : NaN;
65
+ const outputValue = values['output-limit'] ?? '65536';
66
+ const outputLimit = typeof outputValue === 'string' && /^\d+$/.test(outputValue) ? Number(outputValue) : NaN;
67
+ if (command === 'exec' && (!Number.isInteger(timeout) || timeout < 1 || timeout > 600 || !Number.isInteger(outputLimit) || outputLimit < 0 || outputLimit > 1024 * 1024)) {
68
+ throw new SshError('USAGE', 'Use --timeout 1s-600s and --output-limit 0-1048576 (bytes).', 2);
69
+ }
70
+ return { command, json, help, config, profile, values, timeout, outputLimit, id: positional[count] };
71
+ }
@@ -0,0 +1,30 @@
1
+ import { type Profile } from './profile.js';
2
+ import { Output } from './output.js';
3
+ export declare const AUTHENTICATION_DEADLINE_MS = 15000;
4
+ export interface ExecutionSettings {
5
+ command: string;
6
+ timeout: number;
7
+ outputLimit: number;
8
+ }
9
+ export interface ExecutionResult {
10
+ address: string;
11
+ outcome: 'completed' | 'failed' | 'cancelled' | 'timed_out' | 'unknown';
12
+ remoteExit: number | null;
13
+ remoteSignal: string | null;
14
+ termination: 'remote-exit' | 'unknown';
15
+ stdout: ReturnType<Output['finish']>;
16
+ stderr: ReturnType<Output['finish']>;
17
+ durationMs: number;
18
+ diagnostic?: string;
19
+ }
20
+ export declare function authenticate(profile: Profile): Promise<{
21
+ authenticated: true;
22
+ address: string;
23
+ }>;
24
+ export declare function execute(profile: Profile, settings: ExecutionSettings, signal?: AbortSignal): Promise<ExecutionResult>;
25
+ export declare function openTransport(profile: Profile, signal?: AbortSignal): Promise<{
26
+ address: string;
27
+ disconnected: Promise<void>;
28
+ close: () => Promise<void>;
29
+ execute(settings: ExecutionSettings, signal?: AbortSignal): Promise<ExecutionResult>;
30
+ }>;