knoxis-helper 1.4.4 → 1.4.5

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.
@@ -28,6 +28,8 @@ const DEFAULT_PORT = parseInt(process.env.KNOXIS_AGENT_PORT || '3456', 10);
28
28
  const CERT_DIR = process.env.KNOXIS_CERT_DIR || path.join(os.homedir(), '.knoxis', 'certs');
29
29
  const CERT_FILE = process.env.KNOXIS_CERT_FILE || path.join(CERT_DIR, 'localhost.pem');
30
30
  const KEY_FILE = process.env.KNOXIS_CERT_KEY || path.join(CERT_DIR, 'localhost-key.pem');
31
+ const PFX_FILE = process.env.KNOXIS_CERT_PFX || path.join(CERT_DIR, 'localhost.pfx');
32
+ const PFX_PASS = 'knoxis-local'; // Not a real secret — self-signed cert for localhost only
31
33
 
32
34
  // Trusted origins for CORS (deployed frontends)
33
35
  const TRUSTED_ORIGINS = [
@@ -77,10 +79,116 @@ function generateSelfSignedCert() {
77
79
  return true;
78
80
  }
79
81
 
80
- console.warn('⚠️ OpenSSL failed - running in HTTP mode');
82
+ console.warn('⚠️ OpenSSL not available');
81
83
  return false;
82
84
  }
83
85
 
86
+ /**
87
+ * Windows fallback: generate a self-signed certificate via PowerShell.
88
+ * Uses New-SelfSignedCertificate (built into Windows 10+ / Server 2016+).
89
+ * Always produces a PFX file. Also attempts PEM export when .NET 5+ is present.
90
+ * Cert is created in the user's personal store (no admin required) and removed
91
+ * from the store after export.
92
+ */
93
+ function generateSelfSignedCertWindows() {
94
+ console.log('🔐 Generating self-signed certificate via PowerShell...');
95
+
96
+ if (!fs.existsSync(CERT_DIR)) {
97
+ fs.mkdirSync(CERT_DIR, { recursive: true });
98
+ }
99
+
100
+ // Write a PS1 script to a temp file to avoid all shell-escaping issues.
101
+ const psLines = [
102
+ '$ErrorActionPreference = "Stop"',
103
+ '$cert = New-SelfSignedCertificate -Subject "CN=localhost" -DnsName "localhost","127.0.0.1" `',
104
+ ' -CertStoreLocation "Cert:\\CurrentUser\\My" -NotAfter (Get-Date).AddDays(365) `',
105
+ ' -KeyExportPolicy Exportable -KeySpec KeyExchange',
106
+ '$thumb = $cert.Thumbprint',
107
+ '',
108
+ '# Export PFX (works on all Windows versions with this cmdlet)',
109
+ '$pwd = ConvertTo-SecureString -String "' + PFX_PASS + '" -Force -AsPlainText',
110
+ 'Export-PfxCertificate -Cert "Cert:\\CurrentUser\\My\\$thumb" -FilePath "' + PFX_FILE + '" -Password $pwd | Out-Null',
111
+ '',
112
+ '# Attempt PEM export — only works on .NET 5+ (PowerShell 7+)',
113
+ 'try {',
114
+ ' $certDer = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)',
115
+ ' $certB64 = [Convert]::ToBase64String($certDer, "InsertLineBreaks")',
116
+ ' $certPem = "-----BEGIN CERTIFICATE-----`r`n$certB64`r`n-----END CERTIFICATE-----"',
117
+ ' [System.IO.File]::WriteAllText("' + CERT_FILE + '", $certPem)',
118
+ '',
119
+ ' $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)',
120
+ ' $keyBytes = $rsa.ExportPkcs8PrivateKey()',
121
+ ' $keyB64 = [Convert]::ToBase64String($keyBytes, "InsertLineBreaks")',
122
+ ' $keyPem = "-----BEGIN PRIVATE KEY-----`r`n$keyB64`r`n-----END PRIVATE KEY-----"',
123
+ ' [System.IO.File]::WriteAllText("' + KEY_FILE + '", $keyPem)',
124
+ '} catch {',
125
+ ' # PEM export not available on this .NET version — PFX is sufficient',
126
+ '}',
127
+ '',
128
+ '# Remove cert from user store (the exported files are all we need)',
129
+ 'Remove-Item "Cert:\\CurrentUser\\My\\$thumb" -Force -ErrorAction SilentlyContinue',
130
+ ];
131
+
132
+ const tmpScript = path.join(os.tmpdir(), 'knoxis-cert-' + Date.now() + '.ps1');
133
+ fs.writeFileSync(tmpScript, psLines.join('\r\n'), 'utf8');
134
+
135
+ try {
136
+ const result = spawnSync('powershell', [
137
+ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass',
138
+ '-File', tmpScript
139
+ ], { stdio: 'pipe', timeout: 30000 });
140
+
141
+ if (result.status === 0 && fs.existsSync(PFX_FILE)) {
142
+ console.log('✅ Certificate generated via PowerShell');
143
+ if (fs.existsSync(CERT_FILE) && fs.existsSync(KEY_FILE)) {
144
+ console.log(' PEM files exported (PFX + PEM)');
145
+ } else {
146
+ console.log(' PFX exported (PEM not available on this .NET version)');
147
+ }
148
+ return true;
149
+ }
150
+
151
+ console.warn('⚠️ PowerShell certificate generation failed');
152
+ if (result.stderr) {
153
+ const firstLine = result.stderr.toString().trim().split('\n')[0];
154
+ if (firstLine) console.warn(' ' + firstLine);
155
+ }
156
+ return false;
157
+ } finally {
158
+ try { fs.unlinkSync(tmpScript); } catch (e) {}
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Try to create an HTTPS server from the available cert material.
164
+ * Returns { opts, source } on success, null on failure.
165
+ * Checks PEM first (cross-platform), then PFX (Windows PowerShell fallback).
166
+ */
167
+ function tryLoadCerts() {
168
+ // Prefer PEM files (generated by openssl or PS .NET 5+)
169
+ try {
170
+ if (fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {
171
+ const key = fs.readFileSync(KEY_FILE);
172
+ const cert = fs.readFileSync(CERT_FILE);
173
+ return { opts: { key, cert }, source: 'PEM' };
174
+ }
175
+ } catch (err) {
176
+ console.warn('⚠️ Failed to load PEM certificates:', err.message);
177
+ }
178
+
179
+ // Fallback: PFX file (Windows PS 5.1 where PEM export isn't available)
180
+ try {
181
+ if (fs.existsSync(PFX_FILE)) {
182
+ const pfx = fs.readFileSync(PFX_FILE);
183
+ return { opts: { pfx, passphrase: PFX_PASS }, source: 'PFX' };
184
+ }
185
+ } catch (err) {
186
+ console.warn('⚠️ Failed to load PFX certificate:', err.message);
187
+ }
188
+
189
+ return null;
190
+ }
191
+
84
192
  /**
85
193
  * Get CORS headers for the given request origin
86
194
  */
@@ -132,6 +240,38 @@ function buildShellCommand(command) {
132
240
  return { cmd: 'bash', args: ['-lc', command] };
133
241
  }
134
242
 
243
+ /**
244
+ * Build a cross-platform command to pipe a file's contents to stdout.
245
+ * Uses `type` on Windows (cmd.exe) and `cat` on Unix.
246
+ */
247
+ function buildCatCommand(filePath) {
248
+ if (os.platform() === 'win32') {
249
+ return `type "${filePath}"`;
250
+ }
251
+ return `cat "${filePath}"`;
252
+ }
253
+
254
+ /**
255
+ * Build a cross-platform command string that sets environment variables
256
+ * before running a child command.
257
+ * Unix: VAR="val" command
258
+ * Windows: set "VAR=val" && command
259
+ * @param {Record<string,string>} envVars
260
+ * @param {string} command
261
+ */
262
+ function buildEnvCommand(envVars, command) {
263
+ if (os.platform() === 'win32') {
264
+ const sets = Object.entries(envVars)
265
+ .map(([k, v]) => `set "${k}=${v}"`)
266
+ .join(' && ');
267
+ return `${sets} && ${command}`;
268
+ }
269
+ const prefix = Object.entries(envVars)
270
+ .map(([k, v]) => `${k}="${v}"`)
271
+ .join(' ');
272
+ return `${prefix} ${command}`;
273
+ }
274
+
135
275
  function commandExists(cmd) {
136
276
  const detector = os.platform() === 'win32' ? 'where' : 'which';
137
277
  const result = spawnSync(detector, [cmd], { stdio: 'ignore' });
@@ -509,7 +649,7 @@ async function handleRequest(req, res) {
509
649
  if (prompt && prompt.trim().length > 0) {
510
650
  promptFile = path.join(os.tmpdir(), 'knoxis-task-' + (sessionId || Date.now()) + '.txt');
511
651
  fs.writeFileSync(promptFile, prompt, 'utf8');
512
- finalCommand = 'cat "' + promptFile + '" | claude --dangerously-skip-permissions';
652
+ finalCommand = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
513
653
  console.log('📝 Task written to ' + promptFile + ' (' + prompt.length + ' chars)');
514
654
  }
515
655
 
@@ -768,17 +908,17 @@ async function handleRequest(req, res) {
768
908
  const scriptPath = resolveInteractiveScript();
769
909
  if (scriptPath) {
770
910
  // Interactive mode: multi-turn with Groq pair programmer
771
- command = `KNOXIS_TASK_FILE="${promptFile}" node "${scriptPath}"`;
911
+ command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
772
912
  mode = 'interactive';
773
913
  console.log(`🤝 Interactive mode: ${scriptPath}`);
774
914
  } else {
775
915
  // Interactive requested but script not found - fall back to single-shot
776
916
  console.warn('⚠️ Interactive script not found, falling back to single-shot');
777
- command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
917
+ command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
778
918
  }
779
919
  } else {
780
920
  // Standard single-shot mode: pipe task to Claude
781
- command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
921
+ command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
782
922
  }
783
923
 
784
924
  if (headless) {
@@ -979,34 +1119,38 @@ function openLinuxTerminal(workspaceDir, command) {
979
1119
  }
980
1120
 
981
1121
  function createServer() {
982
- // Try to load existing certs
983
- try {
984
- if (fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {
985
- const key = fs.readFileSync(KEY_FILE);
986
- const cert = fs.readFileSync(CERT_FILE);
1122
+ // 1. Try to load existing certs (PEM or PFX)
1123
+ const existing = tryLoadCerts();
1124
+ if (existing) {
1125
+ try {
987
1126
  serverMeta.secure = true;
988
- return https.createServer({ key, cert }, handleRequest);
1127
+ console.log('🔒 Loaded existing ' + existing.source + ' certificate');
1128
+ return https.createServer(existing.opts, handleRequest);
1129
+ } catch (err) {
1130
+ console.warn('⚠️ Failed to create HTTPS server from existing certs:', err.message);
989
1131
  }
990
- } catch (err) {
991
- console.warn('⚠️ Failed to load TLS certificates:', err.message);
992
1132
  }
993
1133
 
994
- // No certs exist - try to generate them
995
- if (!fs.existsSync(CERT_FILE) || !fs.existsSync(KEY_FILE)) {
996
- const generated = generateSelfSignedCert();
997
- if (generated && fs.existsSync(KEY_FILE) && fs.existsSync(CERT_FILE)) {
1134
+ // 2. Generate new certs try openssl first, then Windows PowerShell
1135
+ let generated = generateSelfSignedCert();
1136
+ if (!generated && os.platform() === 'win32') {
1137
+ generated = generateSelfSignedCertWindows();
1138
+ }
1139
+
1140
+ if (generated) {
1141
+ const fresh = tryLoadCerts();
1142
+ if (fresh) {
998
1143
  try {
999
- const key = fs.readFileSync(KEY_FILE);
1000
- const cert = fs.readFileSync(CERT_FILE);
1001
1144
  serverMeta.secure = true;
1002
- return https.createServer({ key, cert }, handleRequest);
1145
+ console.log('🔒 Using freshly generated ' + fresh.source + ' certificate');
1146
+ return https.createServer(fresh.opts, handleRequest);
1003
1147
  } catch (err) {
1004
- console.warn('⚠️ Failed to load generated certificates:', err.message);
1148
+ console.warn('⚠️ Failed to create HTTPS server from generated certs:', err.message);
1005
1149
  }
1006
1150
  }
1007
1151
  }
1008
1152
 
1009
- // Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
1153
+ // 3. Fallback to HTTP (will cause mixed content issues from HTTPS frontends)
1010
1154
  serverMeta.secure = false;
1011
1155
  console.warn('');
1012
1156
  console.warn('⚠️ RUNNING IN HTTP MODE - This will cause 405/CORS errors from HTTPS frontends!');
@@ -1203,14 +1347,14 @@ function connectRelayWebSocket() {
1203
1347
  // Interactive mode: use multi-turn pair programming script
1204
1348
  const scriptPath = resolveInteractiveScript();
1205
1349
  if (scriptPath) {
1206
- command = `KNOXIS_TASK_FILE="${promptFile}" node "${scriptPath}"`;
1350
+ command = buildEnvCommand({ KNOXIS_TASK_FILE: promptFile }, `node "${scriptPath}"`);
1207
1351
  console.log(` 🤝 Interactive mode: ${scriptPath}`);
1208
1352
  } else {
1209
- command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
1353
+ command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
1210
1354
  console.warn(` ⚠️ Interactive script not found, falling back to single-shot`);
1211
1355
  }
1212
1356
  } else {
1213
- command = `cat "${promptFile}" | claude --dangerously-skip-permissions`;
1357
+ command = buildCatCommand(promptFile) + ' | claude --dangerously-skip-permissions';
1214
1358
  }
1215
1359
  console.log(` 📝 Task written to ${promptFile} (${taskPrompt.length} chars)`);
1216
1360
  } else {
@@ -1429,9 +1573,17 @@ server.listen(serverMeta.port, () => {
1429
1573
  console.warn('║ Browsers will block requests from HTTPS sites (like qig.ai) ║');
1430
1574
  console.warn('╚══════════════════════════════════════════════════════════════╝');
1431
1575
  console.warn('');
1432
- console.warn('To fix this, either:');
1433
- console.warn(` 1. Install OpenSSL and restart (auto-generates certs)`);
1434
- console.warn(` 2. Manually place certs in: ${CERT_DIR}`);
1576
+ if (os.platform() === 'win32') {
1577
+ console.warn('To fix this on Windows, try one of:');
1578
+ console.warn(' 1. winget install ShiningLight.OpenSSL.Light (then restart)');
1579
+ console.warn(' 2. choco install openssl (then restart)');
1580
+ console.warn(' 3. Install Git for Windows (ships with OpenSSL)');
1581
+ console.warn(` 4. Manually place PEM or PFX certs in: ${CERT_DIR}`);
1582
+ } else {
1583
+ console.warn('To fix this, either:');
1584
+ console.warn(' 1. Install OpenSSL and restart (auto-generates certs)');
1585
+ console.warn(` 2. Manually place certs in: ${CERT_DIR}`);
1586
+ }
1435
1587
  console.warn('');
1436
1588
  } else {
1437
1589
  console.log('✅ HTTPS enabled - ready for secure connections from deployed frontends');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knoxis-helper",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "description": "Local helper for Knoxis pair programming - connects your machine to Knoxis on qig.ai",
5
5
  "bin": {
6
6
  "knoxis-helper": "./bin/knoxis-helper.js"