edge-ai-client-ts 1.0.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/CHANGELOG.md +72 -0
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/bin/ec-ts.js +18 -0
- package/dist/buffer/disk-queue.d.ts +140 -0
- package/dist/buffer/disk-queue.js +370 -0
- package/dist/cli/devices.d.ts +1 -0
- package/dist/cli/devices.js +61 -0
- package/dist/cli/enroll.d.ts +2 -0
- package/dist/cli/enroll.js +89 -0
- package/dist/cli/index.d.ts +10 -0
- package/dist/cli/index.js +116 -0
- package/dist/cli/messages.d.ts +1 -0
- package/dist/cli/messages.js +59 -0
- package/dist/cli/run.d.ts +5 -0
- package/dist/cli/run.js +112 -0
- package/dist/cli/status.d.ts +1 -0
- package/dist/cli/status.js +56 -0
- package/dist/cli/whoami.d.ts +2 -0
- package/dist/cli/whoami.js +41 -0
- package/dist/config/cmd-gate.d.ts +65 -0
- package/dist/config/cmd-gate.js +128 -0
- package/dist/config/settings.d.ts +209 -0
- package/dist/config/settings.js +627 -0
- package/dist/crypto/aes-gcm.d.ts +38 -0
- package/dist/crypto/aes-gcm.js +90 -0
- package/dist/crypto/hmac.d.ts +31 -0
- package/dist/crypto/hmac.js +52 -0
- package/dist/crypto/tls-guard.d.ts +36 -0
- package/dist/crypto/tls-guard.js +54 -0
- package/dist/daemon/manager.d.ts +82 -0
- package/dist/daemon/manager.js +461 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +63 -0
- package/dist/logging/file-logger.d.ts +21 -0
- package/dist/logging/file-logger.js +71 -0
- package/dist/network/ws-client.d.ts +221 -0
- package/dist/network/ws-client.js +1134 -0
- package/dist/session/fail-fast.d.ts +70 -0
- package/dist/session/fail-fast.js +122 -0
- package/dist/session/manager.d.ts +136 -0
- package/dist/session/manager.js +291 -0
- package/dist/session/persistence.d.ts +103 -0
- package/dist/session/persistence.js +194 -0
- package/dist/session/pi-rpc.d.ts +164 -0
- package/dist/session/pi-rpc.js +412 -0
- package/dist/session/sftp.d.ts +64 -0
- package/dist/session/sftp.js +335 -0
- package/dist/session/shell-frame.d.ts +77 -0
- package/dist/session/shell-frame.js +199 -0
- package/dist/session/shell.d.ts +124 -0
- package/dist/session/shell.js +300 -0
- package/docs/CONFIGURATION.md +169 -0
- package/docs/INSTALLATION.md +164 -0
- package/docs/PROTOCOL.md +248 -0
- package/docs/TROUBLESHOOTING.md +177 -0
- package/package.json +79 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM encrypt/decrypt matching the Rust `DiskDumper` envelope format.
|
|
3
|
+
*
|
|
4
|
+
* On-disk format (mirrors `edge-client/src/buffer/disk_dump.rs`):
|
|
5
|
+
* ```
|
|
6
|
+
* [KEY_VERSION: 1 byte][nonce: 12 bytes][ciphertext + GCM auth tag: 16 bytes appended]
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* The Rust `aes-gcm` crate appends the 16-byte GCM tag to the ciphertext.
|
|
10
|
+
* Node's `crypto.createCipheriv('aes-256-gcm')` returns the tag separately via
|
|
11
|
+
* `getAuthTag()`, so we manually append it to match the Rust byte layout.
|
|
12
|
+
*/
|
|
13
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
14
|
+
/** On-disk encrypted-blob format version (first byte of every dump file). */
|
|
15
|
+
export const KEY_VERSION = 1;
|
|
16
|
+
/** AES-256-GCM nonce length (bytes). */
|
|
17
|
+
export const NONCE_LEN = 12;
|
|
18
|
+
/** AES-256-GCM authentication tag length (bytes, appended to ciphertext). */
|
|
19
|
+
export const GCM_TAG_LEN = 16;
|
|
20
|
+
/** Minimum blob size: version(1) + nonce(12) + tag(16). */
|
|
21
|
+
export const MIN_BLOB_LEN = 1 + NONCE_LEN + GCM_TAG_LEN;
|
|
22
|
+
/**
|
|
23
|
+
* Encrypt `plaintext` with a 32-byte `key` using AES-256-GCM.
|
|
24
|
+
*
|
|
25
|
+
* Returns a Buffer in the DiskDumper format:
|
|
26
|
+
* `[KEY_VERSION][nonce:12][ciphertext][tag:16]`.
|
|
27
|
+
*/
|
|
28
|
+
export function encrypt(key, plaintext) {
|
|
29
|
+
const nonce = randomBytes(NONCE_LEN);
|
|
30
|
+
const cipher = createCipheriv('aes-256-gcm', Buffer.from(key), nonce, {
|
|
31
|
+
authTagLength: GCM_TAG_LEN,
|
|
32
|
+
});
|
|
33
|
+
const ciphertext = Buffer.concat([
|
|
34
|
+
cipher.update(plaintext, 'utf8'),
|
|
35
|
+
cipher.final(),
|
|
36
|
+
]);
|
|
37
|
+
const tag = cipher.getAuthTag(); // 16 bytes
|
|
38
|
+
return Buffer.concat([Buffer.from([KEY_VERSION]), nonce, ciphertext, tag]);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Decrypt a DiskDumper-format blob. Tries `currentKey` first, then
|
|
42
|
+
* `previousKey` (rotation window). Throws on malformed data or auth failure.
|
|
43
|
+
*/
|
|
44
|
+
export function decrypt(data, currentKey, previousKey) {
|
|
45
|
+
if (data.length < MIN_BLOB_LEN) {
|
|
46
|
+
throw new Error('Invalid encrypted data: too short');
|
|
47
|
+
}
|
|
48
|
+
const version = data[0];
|
|
49
|
+
if (version !== KEY_VERSION) {
|
|
50
|
+
throw new Error(`Unsupported buffer-dump key version ${version} (expected ${KEY_VERSION})`);
|
|
51
|
+
}
|
|
52
|
+
const nonce = data.subarray(1, 1 + NONCE_LEN);
|
|
53
|
+
const ciphertextWithTag = data.subarray(1 + NONCE_LEN);
|
|
54
|
+
const ciphertextLen = ciphertextWithTag.length - GCM_TAG_LEN;
|
|
55
|
+
if (ciphertextLen < 0) {
|
|
56
|
+
throw new Error('Invalid encrypted data: ciphertext shorter than auth tag');
|
|
57
|
+
}
|
|
58
|
+
const ciphertext = ciphertextWithTag.subarray(0, ciphertextLen);
|
|
59
|
+
const tag = ciphertextWithTag.subarray(ciphertextLen);
|
|
60
|
+
// Trial decryption: current key first (common path), then previous.
|
|
61
|
+
const currentResult = tryDecrypt(currentKey, nonce, ciphertext, tag);
|
|
62
|
+
if (currentResult !== undefined) {
|
|
63
|
+
return { plaintext: currentResult, usedPrevious: false };
|
|
64
|
+
}
|
|
65
|
+
if (previousKey !== undefined) {
|
|
66
|
+
const prevResult = tryDecrypt(previousKey, nonce, ciphertext, tag);
|
|
67
|
+
if (prevResult !== undefined) {
|
|
68
|
+
return { plaintext: prevResult, usedPrevious: true };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw new Error('Decryption failed: no matching key in keyring');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Attempt AES-256-GCM decryption → UTF-8 string, or `undefined` on failure
|
|
75
|
+
* (auth-tag mismatch / malformed). Used for trial decryption across the
|
|
76
|
+
* keyring so a wrong key is a benign `undefined` rather than a thrown error.
|
|
77
|
+
*/
|
|
78
|
+
function tryDecrypt(key, nonce, ciphertext, tag) {
|
|
79
|
+
try {
|
|
80
|
+
const decipher = createDecipheriv('aes-256-gcm', Buffer.from(key), nonce, {
|
|
81
|
+
authTagLength: GCM_TAG_LEN,
|
|
82
|
+
});
|
|
83
|
+
decipher.setAuthTag(tag);
|
|
84
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
85
|
+
return plaintext.toString('utf8');
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMAC-SHA256 + SHA-256 hex digest primitives.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Rust `hmac` + `sha2` crate usage in the edge-client (used for
|
|
5
|
+
* TLS SPKI fingerprinting, message integrity, etc.).
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Compute HMAC-SHA256 of `message` using `key`.
|
|
9
|
+
* Returns the raw 32-byte digest as a `Buffer`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function hmacSha256(key: Uint8Array, message: Uint8Array | string): Buffer;
|
|
12
|
+
/**
|
|
13
|
+
* Compute HMAC-SHA256 and return the hex-encoded digest (64 hex chars).
|
|
14
|
+
*/
|
|
15
|
+
export declare function hmacSha256Hex(key: Uint8Array, message: Uint8Array | string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Compute SHA-256 of `data` and return the hex-encoded digest (64 hex chars).
|
|
18
|
+
*/
|
|
19
|
+
export declare function sha256Hex(data: Uint8Array | string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Compute SHA-256 of `data` and return the raw 32-byte digest as a `Buffer`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sha256(data: Uint8Array | string): Buffer;
|
|
24
|
+
/**
|
|
25
|
+
* Constant-time comparison of two hex strings.
|
|
26
|
+
* Returns `true` if they are equal (case-insensitive hex, same length).
|
|
27
|
+
*
|
|
28
|
+
* Mirrors the Rust `fingerprint_matches` using a timing-safe compare.
|
|
29
|
+
* If lengths differ, returns `false` immediately (no timing leak of content).
|
|
30
|
+
*/
|
|
31
|
+
export declare function constantTimeHexEquals(a: string, b: string): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMAC-SHA256 + SHA-256 hex digest primitives.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Rust `hmac` + `sha2` crate usage in the edge-client (used for
|
|
5
|
+
* TLS SPKI fingerprinting, message integrity, etc.).
|
|
6
|
+
*/
|
|
7
|
+
import { createHmac, createHash, timingSafeEqual } from 'node:crypto';
|
|
8
|
+
/**
|
|
9
|
+
* Compute HMAC-SHA256 of `message` using `key`.
|
|
10
|
+
* Returns the raw 32-byte digest as a `Buffer`.
|
|
11
|
+
*/
|
|
12
|
+
export function hmacSha256(key, message) {
|
|
13
|
+
const hmac = createHmac('sha256', Buffer.from(key));
|
|
14
|
+
hmac.update(message);
|
|
15
|
+
return hmac.digest();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compute HMAC-SHA256 and return the hex-encoded digest (64 hex chars).
|
|
19
|
+
*/
|
|
20
|
+
export function hmacSha256Hex(key, message) {
|
|
21
|
+
return hmacSha256(key, message).toString('hex');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Compute SHA-256 of `data` and return the hex-encoded digest (64 hex chars).
|
|
25
|
+
*/
|
|
26
|
+
export function sha256Hex(data) {
|
|
27
|
+
return createHash('sha256').update(data).digest('hex');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compute SHA-256 of `data` and return the raw 32-byte digest as a `Buffer`.
|
|
31
|
+
*/
|
|
32
|
+
export function sha256(data) {
|
|
33
|
+
return createHash('sha256').update(data).digest();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Constant-time comparison of two hex strings.
|
|
37
|
+
* Returns `true` if they are equal (case-insensitive hex, same length).
|
|
38
|
+
*
|
|
39
|
+
* Mirrors the Rust `fingerprint_matches` using a timing-safe compare.
|
|
40
|
+
* If lengths differ, returns `false` immediately (no timing leak of content).
|
|
41
|
+
*/
|
|
42
|
+
export function constantTimeHexEquals(a, b) {
|
|
43
|
+
if (a.length !== b.length) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const bufA = Buffer.from(a.toLowerCase(), 'hex');
|
|
47
|
+
const bufB = Buffer.from(b.toLowerCase(), 'hex');
|
|
48
|
+
if (bufA.length !== bufB.length || bufA.length === 0) {
|
|
49
|
+
return bufA.length === bufB.length;
|
|
50
|
+
}
|
|
51
|
+
return timingSafeEqual(bufA, bufB);
|
|
52
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAYER-1: TLS enforcement guard — pure decision logic.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `edge-client/src/network/tls_guard.rs`.
|
|
5
|
+
*
|
|
6
|
+
* Refuses to connect to a plaintext `ws://` URL when `NODE_ENV=production`
|
|
7
|
+
* (or `EDGEAI_REQUIRE_TLS=1`). Defense-in-depth: even if an operator
|
|
8
|
+
* misconfigures the deploy, a mis-pinned cert cannot silently downgrade
|
|
9
|
+
* to plaintext over the wire.
|
|
10
|
+
*
|
|
11
|
+
* Rules:
|
|
12
|
+
* - `wss://` → Allow (TLS path)
|
|
13
|
+
* - `ws://` + `NODE_ENV=production` → Refuse (hard fail)
|
|
14
|
+
* - `ws://` + `EDGEAI_REQUIRE_TLS=1` → Refuse (operator forced TLS)
|
|
15
|
+
* - `ws://` + dev/test → Allow (dev escape hatch)
|
|
16
|
+
*/
|
|
17
|
+
/** Decision for whether the configured URL is acceptable. */
|
|
18
|
+
export type TlsGuardDecision = {
|
|
19
|
+
readonly kind: 'Allow';
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'Refuse';
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Pure decision function. Takes explicit env values (not `process.env`)
|
|
26
|
+
* so it is race-free to unit-test.
|
|
27
|
+
*
|
|
28
|
+
* @param url The relay WebSocket URL.
|
|
29
|
+
* @param nodeEnv The `NODE_ENV` env value (undefined if unset).
|
|
30
|
+
* @param requireTls Whether `EDGEAI_REQUIRE_TLS=1` is set.
|
|
31
|
+
*/
|
|
32
|
+
export declare function decideTls(url: string, nodeEnv: string | undefined, requireTls: boolean): TlsGuardDecision;
|
|
33
|
+
/**
|
|
34
|
+
* Convenience wrapper that reads `process.env`. Mirrors Rust `decide_tls(url)`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function decideTlsFromEnv(url: string): TlsGuardDecision;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAYER-1: TLS enforcement guard — pure decision logic.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `edge-client/src/network/tls_guard.rs`.
|
|
5
|
+
*
|
|
6
|
+
* Refuses to connect to a plaintext `ws://` URL when `NODE_ENV=production`
|
|
7
|
+
* (or `EDGEAI_REQUIRE_TLS=1`). Defense-in-depth: even if an operator
|
|
8
|
+
* misconfigures the deploy, a mis-pinned cert cannot silently downgrade
|
|
9
|
+
* to plaintext over the wire.
|
|
10
|
+
*
|
|
11
|
+
* Rules:
|
|
12
|
+
* - `wss://` → Allow (TLS path)
|
|
13
|
+
* - `ws://` + `NODE_ENV=production` → Refuse (hard fail)
|
|
14
|
+
* - `ws://` + `EDGEAI_REQUIRE_TLS=1` → Refuse (operator forced TLS)
|
|
15
|
+
* - `ws://` + dev/test → Allow (dev escape hatch)
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Pure decision function. Takes explicit env values (not `process.env`)
|
|
19
|
+
* so it is race-free to unit-test.
|
|
20
|
+
*
|
|
21
|
+
* @param url The relay WebSocket URL.
|
|
22
|
+
* @param nodeEnv The `NODE_ENV` env value (undefined if unset).
|
|
23
|
+
* @param requireTls Whether `EDGEAI_REQUIRE_TLS=1` is set.
|
|
24
|
+
*/
|
|
25
|
+
export function decideTls(url, nodeEnv, requireTls) {
|
|
26
|
+
if (url.startsWith('wss://')) {
|
|
27
|
+
return { kind: 'Allow' };
|
|
28
|
+
}
|
|
29
|
+
// Below this point: URL is ws:// (plaintext).
|
|
30
|
+
const isProduction = nodeEnv === 'production';
|
|
31
|
+
if (isProduction) {
|
|
32
|
+
return {
|
|
33
|
+
kind: 'Refuse',
|
|
34
|
+
reason: `NODE_ENV=production but relay URL uses plaintext ws:// (${url}). ` +
|
|
35
|
+
'Production deployments MUST use wss://. Set EDGEAI_REQUIRE_TLS=0 ' +
|
|
36
|
+
'only if you have an alternative transport-encryption layer ' +
|
|
37
|
+
'(e.g. a WireGuard tunnel to the relay host).',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (requireTls) {
|
|
41
|
+
return {
|
|
42
|
+
kind: 'Refuse',
|
|
43
|
+
reason: `EDGEAI_REQUIRE_TLS=1 but relay URL uses plaintext ws:// (${url}). ` +
|
|
44
|
+
'Either change the URL to wss:// or unset EDGEAI_REQUIRE_TLS.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { kind: 'Allow' };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Convenience wrapper that reads `process.env`. Mirrors Rust `decide_tls(url)`.
|
|
51
|
+
*/
|
|
52
|
+
export function decideTlsFromEnv(url) {
|
|
53
|
+
return decideTls(url, process.env.NODE_ENV, process.env.EDGEAI_REQUIRE_TLS === '1');
|
|
54
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type Platform = 'linux' | 'darwin' | 'win32' | 'freebsd' | 'other';
|
|
2
|
+
export interface ServiceInstallOpts {
|
|
3
|
+
/** Directory where the binary lives (e.g. `$HOME/.local/bin`). */
|
|
4
|
+
installDir: string;
|
|
5
|
+
/** Directory where the config + secrets live. */
|
|
6
|
+
configDir: string;
|
|
7
|
+
/** Absolute path to the binary / shim to exec. */
|
|
8
|
+
binaryPath: string;
|
|
9
|
+
/** Optional env vars passed to the service (RELAY_URL, AGENT_ID, etc.). */
|
|
10
|
+
userEnv?: Record<string, string>;
|
|
11
|
+
/** Service name (default `edge-client-ts`). */
|
|
12
|
+
serviceName?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ServiceStatus {
|
|
15
|
+
installed: boolean;
|
|
16
|
+
running: boolean;
|
|
17
|
+
platform: Platform;
|
|
18
|
+
detail: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class ServiceManager {
|
|
21
|
+
static detectPlatform(): Platform;
|
|
22
|
+
/** Default service name. */
|
|
23
|
+
static defaultServiceName(): string;
|
|
24
|
+
/** Default install dir per platform (user-scoped, no root). */
|
|
25
|
+
static defaultInstallDir(platform?: Platform): string;
|
|
26
|
+
/** Resolve the service-unit file path for the current platform. */
|
|
27
|
+
getServiceFilePath(platform: Platform, serviceName: string): string;
|
|
28
|
+
/** Linux systemd --user unit file contents. */
|
|
29
|
+
generateSystemdUnit(opts: ServiceInstallOpts, serviceName: string): string;
|
|
30
|
+
/** macOS LaunchAgent plist contents. */
|
|
31
|
+
generateLaunchdPlist(opts: ServiceInstallOpts, serviceName: string): string;
|
|
32
|
+
/** Windows Task Scheduler XML (no admin needed when created per-user). */
|
|
33
|
+
generateSchtasksXml(opts: ServiceInstallOpts, serviceName: string): string;
|
|
34
|
+
install(opts: ServiceInstallOpts): Promise<void>;
|
|
35
|
+
uninstall(opts: ServiceInstallOpts): Promise<void>;
|
|
36
|
+
start(serviceName?: string): Promise<void>;
|
|
37
|
+
stop(serviceName?: string): Promise<void>;
|
|
38
|
+
status(serviceName?: string): Promise<ServiceStatus>;
|
|
39
|
+
isInstalled(serviceName?: string): Promise<boolean>;
|
|
40
|
+
}
|
|
41
|
+
/** Expand `~` / `$HOME` / `%USERPROFILE%` placeholders. */
|
|
42
|
+
export declare function expandUser(p: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Validate a string is safe to interpolate into a systemd unit file.
|
|
45
|
+
* systemd's INI-like parser treats `[section]` at the start of a line
|
|
46
|
+
* as a section boundary and `Key=` as a directive. A newline in any
|
|
47
|
+
* interpolated value therefore lets an attacker close the current
|
|
48
|
+
* directive, start a new `[Service]` section, and inject `ExecStart=`,
|
|
49
|
+
* `Environment=`, etc. We reject anything containing ASCII control
|
|
50
|
+
* characters; legitimate values never need them.
|
|
51
|
+
*/
|
|
52
|
+
export declare function escapeSystemdValue(value: string, fieldName: string): string;
|
|
53
|
+
/**
|
|
54
|
+
* XML-escape a string for use inside an attribute value or element
|
|
55
|
+
* text in plist/schtasks XML. The chars `<`, `>`, `&`, `"`, `'`
|
|
56
|
+
* MUST be escaped or an attacker can break out of the surrounding
|
|
57
|
+
* construct (`</string><key>ProgramArguments</key>...`) and inject
|
|
58
|
+
* arbitrary plist/schtasks directives. See C1 in security-review
|
|
59
|
+
* 2026-07-11.
|
|
60
|
+
*/
|
|
61
|
+
export declare function escapeXmlAttr(value: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Validate that a service name is a valid systemd unit / plist label /
|
|
64
|
+
* schtasks task name. The charset is intentionally narrow:
|
|
65
|
+
* - alphanumerics, dash, underscore, dot
|
|
66
|
+
* - no path separators, no newlines, no leading dot
|
|
67
|
+
* Mirrors the constraints checked by `systemd-escape`, launchd
|
|
68
|
+
* `Label=`, and `schtasks` at registration time — failing here gives a
|
|
69
|
+
* clear "invalid serviceName" error instead of a confusing OS-level
|
|
70
|
+
* "unit name not valid" rejection at install.
|
|
71
|
+
*/
|
|
72
|
+
export declare function validateServiceName(name: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Validate that an env var name is a portable POSIX identifier. systemd,
|
|
75
|
+
* launchd, and schtasks all follow the same rules:
|
|
76
|
+
* - [A-Za-z_][A-Za-z0-9_]*
|
|
77
|
+
* - no leading digit
|
|
78
|
+
* - empty refused
|
|
79
|
+
* A name like `RELAY;ExecStart=/bin/sh` would otherwise inject
|
|
80
|
+
* additional directives.
|
|
81
|
+
*/
|
|
82
|
+
export declare function validateEnvName(name: string): void;
|