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.
Files changed (57) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -0
  4. package/bin/ec-ts.js +18 -0
  5. package/dist/buffer/disk-queue.d.ts +140 -0
  6. package/dist/buffer/disk-queue.js +370 -0
  7. package/dist/cli/devices.d.ts +1 -0
  8. package/dist/cli/devices.js +61 -0
  9. package/dist/cli/enroll.d.ts +2 -0
  10. package/dist/cli/enroll.js +89 -0
  11. package/dist/cli/index.d.ts +10 -0
  12. package/dist/cli/index.js +116 -0
  13. package/dist/cli/messages.d.ts +1 -0
  14. package/dist/cli/messages.js +59 -0
  15. package/dist/cli/run.d.ts +5 -0
  16. package/dist/cli/run.js +112 -0
  17. package/dist/cli/status.d.ts +1 -0
  18. package/dist/cli/status.js +56 -0
  19. package/dist/cli/whoami.d.ts +2 -0
  20. package/dist/cli/whoami.js +41 -0
  21. package/dist/config/cmd-gate.d.ts +65 -0
  22. package/dist/config/cmd-gate.js +128 -0
  23. package/dist/config/settings.d.ts +209 -0
  24. package/dist/config/settings.js +627 -0
  25. package/dist/crypto/aes-gcm.d.ts +38 -0
  26. package/dist/crypto/aes-gcm.js +90 -0
  27. package/dist/crypto/hmac.d.ts +31 -0
  28. package/dist/crypto/hmac.js +52 -0
  29. package/dist/crypto/tls-guard.d.ts +36 -0
  30. package/dist/crypto/tls-guard.js +54 -0
  31. package/dist/daemon/manager.d.ts +82 -0
  32. package/dist/daemon/manager.js +461 -0
  33. package/dist/index.d.ts +39 -0
  34. package/dist/index.js +63 -0
  35. package/dist/logging/file-logger.d.ts +21 -0
  36. package/dist/logging/file-logger.js +71 -0
  37. package/dist/network/ws-client.d.ts +221 -0
  38. package/dist/network/ws-client.js +1134 -0
  39. package/dist/session/fail-fast.d.ts +70 -0
  40. package/dist/session/fail-fast.js +122 -0
  41. package/dist/session/manager.d.ts +136 -0
  42. package/dist/session/manager.js +291 -0
  43. package/dist/session/persistence.d.ts +103 -0
  44. package/dist/session/persistence.js +194 -0
  45. package/dist/session/pi-rpc.d.ts +164 -0
  46. package/dist/session/pi-rpc.js +412 -0
  47. package/dist/session/sftp.d.ts +64 -0
  48. package/dist/session/sftp.js +335 -0
  49. package/dist/session/shell-frame.d.ts +77 -0
  50. package/dist/session/shell-frame.js +199 -0
  51. package/dist/session/shell.d.ts +124 -0
  52. package/dist/session/shell.js +300 -0
  53. package/docs/CONFIGURATION.md +169 -0
  54. package/docs/INSTALLATION.md +164 -0
  55. package/docs/PROTOCOL.md +248 -0
  56. package/docs/TROUBLESHOOTING.md +177 -0
  57. package/package.json +79 -0
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Daemon/service manager — mirrors `edge-client/src/daemon/manager.rs`.
3
+ *
4
+ * Installs and manages the edge-client daemon as a user-scoped background
5
+ * service on Linux (systemd --user), macOS (launchd LaunchAgent), and Windows
6
+ * (Task Scheduler). NO root/admin required anywhere — Standard User only.
7
+ *
8
+ * Linux: writes ~/.config/systemd/user/${serviceName}.service + `systemctl --user enable --now`
9
+ * macOS: writes ~/Library/LaunchAgents/${serviceName}.plist + `launchctl bootstrap gui/$UID`
10
+ * Win: writes %LOCALAPPDATA%\\edge-client-ts\\tasks\\${serviceName}.xml + `schtasks /Create`
11
+ *
12
+ * The exec command is `node ${binaryPath}` so a `bin/ec-ts.js` shim works
13
+ * (or, on packaged installs, the resolved entry file).
14
+ */
15
+ import * as fs from 'node:fs/promises';
16
+ import * as fssync from 'node:fs';
17
+ import * as os from 'node:os';
18
+ import * as path from 'node:path';
19
+ import { execFile } from 'node:child_process';
20
+ import { promisify } from 'node:util';
21
+ const execFileP = promisify(execFile);
22
+ // ──────────────────────────── service manager ──────────────────────
23
+ export class ServiceManager {
24
+ static detectPlatform() {
25
+ switch (process.platform) {
26
+ case 'linux':
27
+ return 'linux';
28
+ case 'darwin':
29
+ return 'darwin';
30
+ case 'win32':
31
+ return 'win32';
32
+ case 'freebsd':
33
+ return 'freebsd';
34
+ default:
35
+ return 'other';
36
+ }
37
+ }
38
+ /** Default service name. */
39
+ static defaultServiceName() {
40
+ return 'edge-client-ts';
41
+ }
42
+ /** Default install dir per platform (user-scoped, no root). */
43
+ static defaultInstallDir(platform = ServiceManager.detectPlatform()) {
44
+ if (platform === 'win32') {
45
+ return path.join(process.env.LOCALAPPDATA ?? os.homedir(), 'edge-client-ts', 'bin');
46
+ }
47
+ return path.join(os.homedir(), '.local', 'bin');
48
+ }
49
+ // ────────────── paths ──────────────
50
+ /** Resolve the service-unit file path for the current platform. */
51
+ getServiceFilePath(platform, serviceName) {
52
+ switch (platform) {
53
+ case 'linux':
54
+ return path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config'), 'systemd', 'user', `${serviceName}.service`);
55
+ case 'darwin':
56
+ return path.join(os.homedir(), 'Library', 'LaunchAgents', `${serviceName}.plist`);
57
+ case 'win32': {
58
+ const dir = path.join(process.env.LOCALAPPDATA ?? os.homedir(), 'edge-client-ts', 'tasks');
59
+ return path.join(dir, `${serviceName}.xml`);
60
+ }
61
+ default:
62
+ throw new Error(`edge-client-ts does not support service install on platform '${platform}'`);
63
+ }
64
+ }
65
+ // ────────────── generators ──────────────
66
+ /** Linux systemd --user unit file contents. */
67
+ generateSystemdUnit(opts, serviceName) {
68
+ // SECURITY (C1, security-review 2026-07-11): validate every
69
+ // interpolated value. Newlines / control chars in `binaryPath`,
70
+ // `configDir`, or any `userEnv` value would let an attacker inject
71
+ // additional systemd directives (e.g. ExecStart=/bin/pwned).
72
+ // Throws on the first unsafe char.
73
+ validateServiceName(serviceName);
74
+ const binaryPath = escapeSystemdValue(opts.binaryPath, 'binaryPath');
75
+ const configDir = escapeSystemdValue(opts.configDir, 'configDir');
76
+ const envLines = Object.entries(opts.userEnv ?? {})
77
+ .map(([k, v]) => {
78
+ validateEnvName(k);
79
+ const safeV = escapeSystemdValue(v, `userEnv.${k}`);
80
+ return `Environment=${k}=${safeV}`;
81
+ })
82
+ .join('\n');
83
+ return `[Unit]
84
+ Description=Edge-client-ts daemon (Edge AI Agent fleet)
85
+ After=network-online.target
86
+ Wants=network-online.target
87
+
88
+ [Service]
89
+ Type=simple
90
+ ExecStart=${process.execPath} ${binaryPath} run
91
+ Restart=on-failure
92
+ RestartSec=5
93
+ WorkingDirectory=${configDir}
94
+ ${envLines}
95
+
96
+ [Install]
97
+ WantedBy=default.target
98
+ `;
99
+ }
100
+ /** macOS LaunchAgent plist contents. */
101
+ generateLaunchdPlist(opts, serviceName) {
102
+ validateServiceName(serviceName);
103
+ const envDict = Object.entries(opts.userEnv ?? {})
104
+ .map(([k, v]) => {
105
+ validateEnvName(k);
106
+ const safeK = escapeXmlAttr(k);
107
+ const safeV = escapeXmlAttr(v);
108
+ return ` <key>${safeK}</key>\n <string>${safeV}</string>`;
109
+ })
110
+ .join('\n');
111
+ return `<?xml version="1.0" encoding="UTF-8"?>
112
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
113
+ <plist version="1.0">
114
+ <dict>
115
+ <key>Label</key>
116
+ <string>${escapeXmlAttr(serviceName)}</string>
117
+ <key>ProgramArguments</key>
118
+ <array>
119
+ <string>${escapeXmlAttr(process.execPath)}</string>
120
+ <string>${escapeXmlAttr(opts.binaryPath)}</string>
121
+ <string>run</string>
122
+ </array>
123
+ <key>RunAtLoad</key>
124
+ <true/>
125
+ <key>KeepAlive</key>
126
+ <true/>
127
+ <key>WorkingDirectory</key>
128
+ <string>${escapeXmlAttr(opts.configDir)}</string>
129
+ <key>EnvironmentVariables</key>
130
+ <dict>
131
+ ${envDict}
132
+ </dict>
133
+ <key>StandardOutPath</key>
134
+ <string>${escapeXmlAttr(path.join(os.homedir(), "Library", "Logs", serviceName + ".log"))}</string>
135
+ <key>StandardErrorPath</key>
136
+ <string>${escapeXmlAttr(path.join(os.homedir(), "Library", "Logs", serviceName + ".err"))}</string>
137
+ </dict>
138
+ </plist>
139
+ `;
140
+ }
141
+ /** Windows Task Scheduler XML (no admin needed when created per-user). */
142
+ generateSchtasksXml(opts, serviceName) {
143
+ validateServiceName(serviceName);
144
+ const envLines = Object.entries(opts.userEnv ?? {})
145
+ .map(([k, v]) => {
146
+ validateEnvName(k);
147
+ return ` <EnvVar Name="${escapeXmlAttr(k)}" Value="${escapeXmlAttr(v)}"/>`;
148
+ })
149
+ .join('\n');
150
+ return `<?xml version="1.0" encoding="UTF-16"?>
151
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
152
+ <RegistrationInfo>
153
+ <Description>Edge-client-ts daemon (Edge AI Agent fleet)</Description>
154
+ </RegistrationInfo>
155
+ <Triggers>
156
+ <LogonTrigger>
157
+ <Enabled>true</Enabled>
158
+ </LogonTrigger>
159
+ </Triggers>
160
+ <Principals>
161
+ <Principal id="Author">
162
+ <GroupId>S-1-5-4</GroupId>
163
+ <RunLevel>LeastPrivilege</RunLevel>
164
+ </Principal>
165
+ </Principals>
166
+ <Settings>
167
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
168
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
169
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
170
+ <AllowHardTerminate>true</AllowHardTerminate>
171
+ <StartWhenAvailable>true</StartWhenAvailable>
172
+ <RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
173
+ <AllowStartOnDemand>true</AllowStartOnDemand>
174
+ <Enabled>true</Enabled>
175
+ <Hidden>false</Hidden>
176
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
177
+ <Priority>7</Priority>
178
+ </Settings>
179
+ <Actions Context="Author">
180
+ <Exec>
181
+ <Command>${escapeXmlAttr(process.execPath)}</Command>
182
+ <Arguments>${escapeXmlAttr(opts.binaryPath + ' run')}</Arguments>
183
+ <WorkingDirectory>${escapeXmlAttr(opts.configDir)}</WorkingDirectory>
184
+ ${envLines}
185
+ </Exec>
186
+ </Actions>
187
+ </Task>
188
+ `;
189
+ }
190
+ // ────────────── install / uninstall ──────────────
191
+ async install(opts) {
192
+ const platform = ServiceManager.detectPlatform();
193
+ const serviceName = opts.serviceName ?? ServiceManager.defaultServiceName();
194
+ const unitPath = this.getServiceFilePath(platform, serviceName);
195
+ await fs.mkdir(path.dirname(unitPath), { recursive: true });
196
+ let content;
197
+ switch (platform) {
198
+ case 'linux':
199
+ content = this.generateSystemdUnit(opts, serviceName);
200
+ await fs.writeFile(unitPath, content, { mode: 0o644 });
201
+ await execFileP('systemctl', ['--user', 'daemon-reload']);
202
+ await execFileP('systemctl', ['--user', 'enable', '--now', `${serviceName}.service`]);
203
+ return;
204
+ case 'darwin':
205
+ content = this.generateLaunchdPlist(opts, serviceName);
206
+ await fs.writeFile(unitPath, content, { mode: 0o644 });
207
+ await execFileP('launchctl', ['bootstrap', `gui/${process.getuid?.() ?? os.userInfo().uid}`, unitPath]);
208
+ return;
209
+ case 'win32': {
210
+ const schtasksDir = path.dirname(unitPath);
211
+ await fs.mkdir(schtasksDir, { recursive: true });
212
+ content = this.generateSchtasksXml(opts, serviceName);
213
+ await fs.writeFile(unitPath, content, 'utf16le');
214
+ // /RU $env:USERNAME so we don't need admin (per-user task).
215
+ await execFileP('schtasks', [
216
+ '/Create',
217
+ '/XML', unitPath,
218
+ '/TN', serviceName,
219
+ '/RU', process.env.USERNAME ?? os.userInfo().username,
220
+ '/F',
221
+ ]);
222
+ return;
223
+ }
224
+ default:
225
+ throw new Error(`edge-client-ts does not support service install on platform '${platform}'; run \`ec-ts run\` manually.`);
226
+ }
227
+ }
228
+ async uninstall(opts) {
229
+ const platform = ServiceManager.detectPlatform();
230
+ const serviceName = opts.serviceName ?? ServiceManager.defaultServiceName();
231
+ const unitPath = this.getServiceFilePath(platform, serviceName);
232
+ switch (platform) {
233
+ case 'linux':
234
+ try {
235
+ await execFileP('systemctl', ['--user', 'disable', '--now', `${serviceName}.service`]);
236
+ }
237
+ catch {
238
+ /* not installed */
239
+ }
240
+ break;
241
+ case 'darwin':
242
+ try {
243
+ await execFileP('launchctl', ['bootout', `gui/${process.getuid?.() ?? os.userInfo().uid}/${serviceName}`]);
244
+ }
245
+ catch {
246
+ /* not installed */
247
+ }
248
+ break;
249
+ case 'win32':
250
+ try {
251
+ await execFileP('schtasks', ['/Delete', '/TN', serviceName, '/F']);
252
+ }
253
+ catch {
254
+ /* not installed */
255
+ }
256
+ break;
257
+ default:
258
+ throw new Error(`edge-client-ts does not support service uninstall on platform '${platform}'`);
259
+ }
260
+ try {
261
+ await fs.unlink(unitPath);
262
+ }
263
+ catch {
264
+ /* file already gone */
265
+ }
266
+ }
267
+ async start(serviceName = ServiceManager.defaultServiceName()) {
268
+ switch (ServiceManager.detectPlatform()) {
269
+ case 'linux':
270
+ await execFileP('systemctl', ['--user', 'start', `${serviceName}.service`]);
271
+ return;
272
+ case 'darwin':
273
+ await execFileP('launchctl', ['kickstart', '-k', `gui/${process.getuid?.() ?? os.userInfo().uid}/${serviceName}`]);
274
+ return;
275
+ case 'win32':
276
+ await execFileP('schtasks', ['/Run', '/TN', serviceName]);
277
+ return;
278
+ default:
279
+ throw new Error('unsupported platform');
280
+ }
281
+ }
282
+ async stop(serviceName = ServiceManager.defaultServiceName()) {
283
+ switch (ServiceManager.detectPlatform()) {
284
+ case 'linux':
285
+ await execFileP('systemctl', ['--user', 'stop', `${serviceName}.service`]);
286
+ return;
287
+ case 'darwin':
288
+ await execFileP('launchctl', ['kill', `gui/${process.getuid?.() ?? os.userInfo().uid}/${serviceName}`]);
289
+ return;
290
+ case 'win32':
291
+ await execFileP('schtasks', ['/End', '/TN', serviceName]);
292
+ return;
293
+ default:
294
+ throw new Error('unsupported platform');
295
+ }
296
+ }
297
+ async status(serviceName = ServiceManager.defaultServiceName()) {
298
+ const platform = ServiceManager.detectPlatform();
299
+ const unitPath = this.getServiceFilePath(platform, serviceName);
300
+ const installed = (() => {
301
+ try {
302
+ fssync.accessSync(unitPath, fssync.constants.F_OK);
303
+ return true;
304
+ }
305
+ catch {
306
+ return false;
307
+ }
308
+ })();
309
+ let running = false;
310
+ let detail = '';
311
+ try {
312
+ switch (platform) {
313
+ case 'linux': {
314
+ const { stdout } = await execFileP('systemctl', ['--user', 'is-active', `${serviceName}.service`]);
315
+ running = stdout.trim() === 'active';
316
+ detail = `systemd ${running ? 'active' : 'inactive'}`;
317
+ break;
318
+ }
319
+ case 'darwin': {
320
+ const { stdout } = await execFileP('launchctl', ['list']);
321
+ running = stdout.includes(serviceName);
322
+ detail = running ? 'launchd loaded' : 'launchd unloaded';
323
+ break;
324
+ }
325
+ case 'win32': {
326
+ const { stdout } = await execFileP('schtasks', ['/Query', '/TN', serviceName, '/FO', 'LIST']);
327
+ running = stdout.includes('Running');
328
+ detail = running ? 'schtasks running' : 'schtasks idle';
329
+ break;
330
+ }
331
+ default:
332
+ detail = 'unsupported platform';
333
+ }
334
+ }
335
+ catch (e) {
336
+ detail = `query failed: ${String(e).slice(0, 100)}`;
337
+ running = false;
338
+ }
339
+ return { installed, running, platform, detail };
340
+ }
341
+ async isInstalled(serviceName = ServiceManager.defaultServiceName()) {
342
+ const platform = ServiceManager.detectPlatform();
343
+ const unitPath = this.getServiceFilePath(platform, serviceName);
344
+ try {
345
+ await fs.access(unitPath);
346
+ return true;
347
+ }
348
+ catch {
349
+ return false;
350
+ }
351
+ }
352
+ }
353
+ // ──────────────────────────── helpers ──────────────────────────────
354
+ /** Expand `~` / `$HOME` / `%USERPROFILE%` placeholders. */
355
+ export function expandUser(p) {
356
+ if (p.startsWith('~/') || p === '~') {
357
+ return path.join(os.homedir(), p.slice(p === '~' ? 1 : 2));
358
+ }
359
+ if (p.startsWith('$HOME')) {
360
+ return path.join(os.homedir(), p.slice(5));
361
+ }
362
+ if (p.startsWith('%USERPROFILE%')) {
363
+ return path.join(process.env.USERPROFILE ?? os.homedir(), p.slice(14));
364
+ }
365
+ if (p.startsWith('%LOCALAPPDATA%')) {
366
+ return path.join(process.env.LOCALAPPDATA ?? os.homedir(), p.slice(14));
367
+ }
368
+ return p;
369
+ }
370
+ // ──────────────────────────────────────────────────────────────────
371
+ // SECURITY: input validation + escaping for daemon template fields
372
+ // (security-review 2026-07-11 — C1: template injection).
373
+ //
374
+ // All values interpolated into systemd units, launchd plists, or
375
+ // schtasks XML MUST be validated by the helpers below. The historical
376
+ // raw-string interpolation allowed a malicious `binaryPath`,
377
+ // `configDir`, `serviceName`, or `userEnv` value containing a newline
378
+ // to inject additional directives such as:
379
+ //
380
+ // binaryPath: "/safe/path\n[Service]\nExecStart=/bin/pwned"
381
+ //
382
+ // which produces a systemd unit with TWO `ExecStart=` lines — the second
383
+ // one runs the attacker's command on `systemctl start`. Equivalent
384
+ // attacks work via plist XML injection (`</string><key>...
385
+ // <array><string>/bin/sh</string>...</array>`) and schtasks
386
+ // `</Command><Exec>...` manipulation.
387
+ //
388
+ // The helpers below throw on any un-safe input so `install()` fails
389
+ // fast instead of silently writing a malicious service definition.
390
+ // ──────────────────────────────────────────────────────────────────
391
+ /**
392
+ * Validate a string is safe to interpolate into a systemd unit file.
393
+ * systemd's INI-like parser treats `[section]` at the start of a line
394
+ * as a section boundary and `Key=` as a directive. A newline in any
395
+ * interpolated value therefore lets an attacker close the current
396
+ * directive, start a new `[Service]` section, and inject `ExecStart=`,
397
+ * `Environment=`, etc. We reject anything containing ASCII control
398
+ * characters; legitimate values never need them.
399
+ */
400
+ export function escapeSystemdValue(value, fieldName) {
401
+ // eslint-disable-next-line no-control-regex
402
+ if (/[\x00-\x1f]/.test(value)) {
403
+ throw new RangeError(`daemon: ${fieldName} contains control characters (refusing to emit injection):\n${value}`);
404
+ }
405
+ return value;
406
+ }
407
+ /**
408
+ * XML-escape a string for use inside an attribute value or element
409
+ * text in plist/schtasks XML. The chars `<`, `>`, `&`, `"`, `'`
410
+ * MUST be escaped or an attacker can break out of the surrounding
411
+ * construct (`</string><key>ProgramArguments</key>...`) and inject
412
+ * arbitrary plist/schtasks directives. See C1 in security-review
413
+ * 2026-07-11.
414
+ */
415
+ export function escapeXmlAttr(value) {
416
+ return value
417
+ .replace(/&/g, '&amp;')
418
+ .replace(/</g, '&lt;')
419
+ .replace(/>/g, '&gt;')
420
+ .replace(/"/g, '&quot;')
421
+ .replace(/'/g, '&apos;')
422
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, ''); // strip control chars (except \t, \n, \r)
423
+ }
424
+ /**
425
+ * Validate that a service name is a valid systemd unit / plist label /
426
+ * schtasks task name. The charset is intentionally narrow:
427
+ * - alphanumerics, dash, underscore, dot
428
+ * - no path separators, no newlines, no leading dot
429
+ * Mirrors the constraints checked by `systemd-escape`, launchd
430
+ * `Label=`, and `schtasks` at registration time — failing here gives a
431
+ * clear "invalid serviceName" error instead of a confusing OS-level
432
+ * "unit name not valid" rejection at install.
433
+ */
434
+ export function validateServiceName(name) {
435
+ if (typeof name !== 'string' || name.length === 0 || name.length > 64) {
436
+ throw new RangeError(`daemon: serviceName must be 1–64 chars (got ${name.length})`);
437
+ }
438
+ if (!/^[A-Za-z0-9_.-]+$/.test(name)) {
439
+ throw new RangeError(`daemon: serviceName contains invalid chars (only [A-Za-z0-9_.-] allowed): ${JSON.stringify(name)}`);
440
+ }
441
+ if (name === '.' || name === '..' || name.startsWith('.')) {
442
+ throw new RangeError(`daemon: serviceName cannot be "." or start with "." (got ${JSON.stringify(name)})`);
443
+ }
444
+ }
445
+ /**
446
+ * Validate that an env var name is a portable POSIX identifier. systemd,
447
+ * launchd, and schtasks all follow the same rules:
448
+ * - [A-Za-z_][A-Za-z0-9_]*
449
+ * - no leading digit
450
+ * - empty refused
451
+ * A name like `RELAY;ExecStart=/bin/sh` would otherwise inject
452
+ * additional directives.
453
+ */
454
+ export function validateEnvName(name) {
455
+ if (typeof name !== 'string' || name.length === 0) {
456
+ throw new RangeError('daemon: userEnv key must be a non-empty string');
457
+ }
458
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
459
+ throw new RangeError(`daemon: userEnv key '${name}' must match /^[A-Za-z_][A-Za-z0-9_]*$/`);
460
+ }
461
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @edgeai/edge-client-ts — TypeScript (Model 1) parallel edge-client.
3
+ *
4
+ * Public entry point. Re-exports every module the binary / library users need.
5
+ *
6
+ * Layout mirrors the Rust edge-client:
7
+ * config/ settings + cmd-gate schema
8
+ * crypto/ AES-256-GCM, HMAC-SHA256, TLS cert-pinning
9
+ * logging/ file logger
10
+ * network/ WS client (`/edge` JSON stream)
11
+ * buffer/ disk-backed offline queue
12
+ * session/ PTY shell (with 17-byte frame codec), SFTP, pi RPC, fail-fast,
13
+ * session manager, persistence
14
+ * cli/ dispatch + run/enroll/whoami/status/devices/messages subcommands
15
+ * daemon/ per-OS service install (systemd-user / launchd / schtasks)
16
+ */
17
+ export * from './config/settings.js';
18
+ export * from './config/cmd-gate.js';
19
+ export * from './crypto/aes-gcm.js';
20
+ export * from './crypto/hmac.js';
21
+ export * from './crypto/tls-guard.js';
22
+ export { FileLogger } from './logging/file-logger.js';
23
+ export { EdgeWsClient, type EdgeWsClientOptions, type EdgeWsClientEventMap, type RegisterMessage, type StreamMessage, type AckMessage, type HeartbeatMessage, type SystemMetrics, type JsonObject, type BufferDrainable, } from './network/ws-client.js';
24
+ export { DiskBufferQueue, BufferFullError, type QueueEntry } from './buffer/disk-queue.js';
25
+ export { SHELL_HEADER_LEN, SESSION_ID_LEN, RESIZE_PAYLOAD_LEN, FrameType, frameTypeFromByte, newSessionId, sessionIdFromString, sessionIdToString, dataFrame, resizeFrame, closeFrame, encodeFrame, decodeFrame, decodeResizePayload, type ShellFrame, } from './session/shell-frame.js';
26
+ export { FORBIDDEN_KEYWORDS, INSPECTABLE_EVENT_TYPES, FAIL_FAST_ERROR_CONTENT, ENV_FAIL_FAST, failFastEnabled, detectForbidden, buildSyntheticErrorEvent, } from './session/fail-fast.js';
27
+ export { ShellManager, type DataHandler, type ExitHandler } from './session/shell.js';
28
+ export { SftpForwarder } from './session/sftp.js';
29
+ export { PiProcess, hasLimits, systemdRunAvailable, resetSystemdRunCache, wrapWithCgroup, promptCommand, abortCommand, setModelCommand, setThinkingLevelCommand, compactCommand, getAvailableModelsCommand, newPiSessionId, extractSessionId, eventMsgId, type RpcCommand, type PiEvent, type ResourcesConfig, } from './session/pi-rpc.js';
30
+ export { SessionManager, type SessionManagerOptions, type HeartbeatMetadata } from './session/manager.js';
31
+ export { SessionIdStore, persistSessionState, loadSessionState, type SessionState } from './session/persistence.js';
32
+ export { dispatch } from './cli/index.js';
33
+ export { runDaemon } from './cli/run.js';
34
+ export { enroll } from './cli/enroll.js';
35
+ export { whoami } from './cli/whoami.js';
36
+ export { status } from './cli/status.js';
37
+ export { listDevices } from './cli/devices.js';
38
+ export { listMessages } from './cli/messages.js';
39
+ export { ServiceManager, expandUser, type Platform, type ServiceInstallOpts, type ServiceStatus, } from './daemon/manager.js';
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @edgeai/edge-client-ts — TypeScript (Model 1) parallel edge-client.
3
+ *
4
+ * Public entry point. Re-exports every module the binary / library users need.
5
+ *
6
+ * Layout mirrors the Rust edge-client:
7
+ * config/ settings + cmd-gate schema
8
+ * crypto/ AES-256-GCM, HMAC-SHA256, TLS cert-pinning
9
+ * logging/ file logger
10
+ * network/ WS client (`/edge` JSON stream)
11
+ * buffer/ disk-backed offline queue
12
+ * session/ PTY shell (with 17-byte frame codec), SFTP, pi RPC, fail-fast,
13
+ * session manager, persistence
14
+ * cli/ dispatch + run/enroll/whoami/status/devices/messages subcommands
15
+ * daemon/ per-OS service install (systemd-user / launchd / schtasks)
16
+ */
17
+ // ---------------------------------------------------------------------------
18
+ // Config
19
+ // ---------------------------------------------------------------------------
20
+ export * from './config/settings.js';
21
+ export * from './config/cmd-gate.js';
22
+ // ---------------------------------------------------------------------------
23
+ // Crypto
24
+ // ---------------------------------------------------------------------------
25
+ export * from './crypto/aes-gcm.js';
26
+ export * from './crypto/hmac.js';
27
+ export * from './crypto/tls-guard.js';
28
+ // ---------------------------------------------------------------------------
29
+ // Logging
30
+ // ---------------------------------------------------------------------------
31
+ export { FileLogger } from './logging/file-logger.js';
32
+ // ---------------------------------------------------------------------------
33
+ // Network
34
+ // ---------------------------------------------------------------------------
35
+ export { EdgeWsClient, } from './network/ws-client.js';
36
+ // ---------------------------------------------------------------------------
37
+ // Buffer (offline queue)
38
+ // ---------------------------------------------------------------------------
39
+ export { DiskBufferQueue, BufferFullError } from './buffer/disk-queue.js';
40
+ // ---------------------------------------------------------------------------
41
+ // Session
42
+ // ---------------------------------------------------------------------------
43
+ export { SHELL_HEADER_LEN, SESSION_ID_LEN, RESIZE_PAYLOAD_LEN, FrameType, frameTypeFromByte, newSessionId, sessionIdFromString, sessionIdToString, dataFrame, resizeFrame, closeFrame, encodeFrame, decodeFrame, decodeResizePayload, } from './session/shell-frame.js';
44
+ export { FORBIDDEN_KEYWORDS, INSPECTABLE_EVENT_TYPES, FAIL_FAST_ERROR_CONTENT, ENV_FAIL_FAST, failFastEnabled, detectForbidden, buildSyntheticErrorEvent, } from './session/fail-fast.js';
45
+ export { ShellManager } from './session/shell.js';
46
+ export { SftpForwarder } from './session/sftp.js';
47
+ export { PiProcess, hasLimits, systemdRunAvailable, resetSystemdRunCache, wrapWithCgroup, promptCommand, abortCommand, setModelCommand, setThinkingLevelCommand, compactCommand, getAvailableModelsCommand, newPiSessionId, extractSessionId, eventMsgId, } from './session/pi-rpc.js';
48
+ export { SessionManager } from './session/manager.js';
49
+ export { SessionIdStore, persistSessionState, loadSessionState } from './session/persistence.js';
50
+ // ---------------------------------------------------------------------------
51
+ // CLI dispatch (used by bin/ec-ts.js)
52
+ // ---------------------------------------------------------------------------
53
+ export { dispatch } from './cli/index.js';
54
+ export { runDaemon } from './cli/run.js';
55
+ export { enroll } from './cli/enroll.js';
56
+ export { whoami } from './cli/whoami.js';
57
+ export { status } from './cli/status.js';
58
+ export { listDevices } from './cli/devices.js';
59
+ export { listMessages } from './cli/messages.js';
60
+ // ---------------------------------------------------------------------------
61
+ // Daemon (service install)
62
+ // ---------------------------------------------------------------------------
63
+ export { ServiceManager, expandUser, } from './daemon/manager.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * File-based logger — mirrors `edge-client/src/logging/file_logger.rs`.
3
+ *
4
+ * Writes timestamped log lines to a daily log file under `logDir`
5
+ * (e.g. `edge-client-2026-07-11.log`). Thread-safe via a mutex
6
+ * (in Node, a simple serialization queue).
7
+ */
8
+ export declare class FileLogger {
9
+ private readonly logDir;
10
+ private stream;
11
+ private writeQueue;
12
+ constructor(logDir: string);
13
+ /** Write a formatted log line at the given level. */
14
+ log(level: string, message: string): Promise<void>;
15
+ info(message: string): Promise<void>;
16
+ warn(message: string): Promise<void>;
17
+ error(message: string): Promise<void>;
18
+ /** Flush + close the underlying stream. */
19
+ close(): Promise<void>;
20
+ private writeRaw;
21
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * File-based logger — mirrors `edge-client/src/logging/file_logger.rs`.
3
+ *
4
+ * Writes timestamped log lines to a daily log file under `logDir`
5
+ * (e.g. `edge-client-2026-07-11.log`). Thread-safe via a mutex
6
+ * (in Node, a simple serialization queue).
7
+ */
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ export class FileLogger {
11
+ logDir;
12
+ stream;
13
+ writeQueue = Promise.resolve();
14
+ constructor(logDir) {
15
+ this.logDir = logDir;
16
+ fs.mkdirSync(logDir, { recursive: true });
17
+ const today = formatDate(new Date());
18
+ const logFile = path.join(logDir, `edge-client-${today}.log`);
19
+ this.stream = fs.createWriteStream(logFile, { flags: 'a' });
20
+ }
21
+ /** Write a formatted log line at the given level. */
22
+ log(level, message) {
23
+ const timestamp = formatTimestamp(new Date());
24
+ const line = `[${timestamp}] [${level}] ${message}\n`;
25
+ return this.writeRaw(line);
26
+ }
27
+ info(message) {
28
+ return this.log('INFO', message);
29
+ }
30
+ warn(message) {
31
+ return this.log('WARN', message);
32
+ }
33
+ error(message) {
34
+ return this.log('ERROR', message);
35
+ }
36
+ /** Flush + close the underlying stream. */
37
+ close() {
38
+ return this.writeQueue.then(() => {
39
+ this.stream?.end();
40
+ this.stream = undefined;
41
+ });
42
+ }
43
+ writeRaw(line) {
44
+ this.writeQueue = this.writeQueue.then(() => {
45
+ return new Promise((resolve) => {
46
+ if (this.stream === undefined) {
47
+ resolve();
48
+ return;
49
+ }
50
+ this.stream.write(line, () => resolve());
51
+ });
52
+ });
53
+ return this.writeQueue;
54
+ }
55
+ }
56
+ /** Format a date as `YYYY-MM-DD` (for the log filename). */
57
+ function formatDate(d) {
58
+ const y = d.getFullYear();
59
+ const m = String(d.getMonth() + 1).padStart(2, '0');
60
+ const day = String(d.getDate()).padStart(2, '0');
61
+ return `${y}-${m}-${day}`;
62
+ }
63
+ /** Format a date as `YYYY-MM-DD HH:MM:SS.mmm` (for log line timestamps). */
64
+ function formatTimestamp(d) {
65
+ const date = formatDate(d);
66
+ const h = String(d.getHours()).padStart(2, '0');
67
+ const min = String(d.getMinutes()).padStart(2, '0');
68
+ const s = String(d.getSeconds()).padStart(2, '0');
69
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
70
+ return `${date} ${h}:${min}:${s}.${ms}`;
71
+ }