filemayor 3.6.0 → 4.0.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.
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const PING_URL = 'https://filemayor.com/api/ping';
8
+ const STATE_FILE = path.join(os.homedir(), '.filemayor-telemetry.json');
9
+
10
+ // Fire-and-forget anonymous ping.
11
+ // Sends: version, OS platform, CPU arch, and top-level command name.
12
+ // Never sends: file names, paths, sizes, contents, or any PII.
13
+ // Opt-out: set FILEMAYOR_NO_TELEMETRY=1 in environment.
14
+ // Fires at most once per version (tracked in ~/.filemayor-telemetry.json).
15
+ function ping(version, cmd) {
16
+ if (process.env.FILEMAYOR_NO_TELEMETRY === '1') return;
17
+ if (process.env.CI) return;
18
+
19
+ try {
20
+ const state = readState();
21
+ if (state.pinged && state.pinged.includes(version)) return;
22
+
23
+ const payload = JSON.stringify({
24
+ v: version,
25
+ os: os.platform(),
26
+ arch: os.arch(),
27
+ cmd: cmd || 'unknown',
28
+ });
29
+
30
+ // Fire over https using built-in https module — no extra deps
31
+ const https = require('https');
32
+ const url = new URL(PING_URL);
33
+ const req = https.request({
34
+ hostname: url.hostname,
35
+ path: url.pathname,
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'Content-Length': Buffer.byteLength(payload),
40
+ 'User-Agent': `filemayor-cli/${version}`,
41
+ },
42
+ timeout: 3000,
43
+ });
44
+
45
+ req.on('error', () => {}); // swallow all errors — telemetry must never crash the CLI
46
+ req.on('timeout', () => req.destroy());
47
+ req.end(payload);
48
+
49
+ markPinged(state, version);
50
+ } catch {
51
+ // intentionally silent
52
+ }
53
+ }
54
+
55
+ function readState() {
56
+ try {
57
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8'));
58
+ } catch {
59
+ return { pinged: [] };
60
+ }
61
+ }
62
+
63
+ function markPinged(state, version) {
64
+ try {
65
+ const pinged = Array.isArray(state.pinged) ? state.pinged : [];
66
+ pinged.push(version);
67
+ // Keep only last 20 versions to bound file size
68
+ fs.writeFileSync(STATE_FILE, JSON.stringify({ pinged: pinged.slice(-20) }), 'utf-8');
69
+ } catch {
70
+ // intentionally silent
71
+ }
72
+ }
73
+
74
+ module.exports = { ping };
package/core/vault.js CHANGED
@@ -3,21 +3,20 @@
3
3
  /**
4
4
  * ═══════════════════════════════════════════════════════════════════
5
5
  * FILEMAYOR CORE — VAULT (ZERO-DEPENDENCY SECRET MANAGER)
6
- *
6
+ *
7
7
  * Uses the OS-native credential store to keep API keys out of
8
- * the filesystem entirely. Falls back to .env / environment
9
- * variables if the system keychain is unavailable.
10
- *
8
+ * the filesystem entirely. Falls back to environment variables only.
9
+ *
11
10
  * Supported backends:
12
11
  * macOS → security (Keychain Services)
13
- * Windows → cmdkey / PowerShell CredentialManager
12
+ * Windows → PowerShell CredentialManager
14
13
  * Linux → secret-tool (freedesktop Secret Service)
15
14
  * ═══════════════════════════════════════════════════════════════════
16
15
  */
17
16
 
18
17
  'use strict';
19
18
 
20
- const { execSync } = require('child_process');
19
+ const { execFileSync } = require('child_process');
21
20
  const path = require('path');
22
21
  const fs = require('fs');
23
22
 
@@ -26,72 +25,96 @@ const SERVICE_NAME = 'FileMayor';
26
25
  // ─── Platform-Specific Implementations ───────────────────────────
27
26
 
28
27
  function _saveSecret_darwin(key, value) {
29
- // -U flag updates if already exists
30
- execSync(
31
- `security add-generic-password -a "${SERVICE_NAME}" -s "${key}" -w "${value}" -U`,
32
- { stdio: 'ignore' }
33
- );
28
+ execFileSync('security', [
29
+ 'add-generic-password',
30
+ '-a', SERVICE_NAME,
31
+ '-s', key,
32
+ '-w', value,
33
+ '-U',
34
+ ], { stdio: 'ignore' });
34
35
  }
35
36
 
36
37
  function _getSecret_darwin(key) {
37
- return execSync(
38
- `security find-generic-password -a "${SERVICE_NAME}" -s "${key}" -w`,
39
- { encoding: 'utf8' }
40
- ).trim();
38
+ return execFileSync('security', [
39
+ 'find-generic-password',
40
+ '-a', SERVICE_NAME,
41
+ '-s', key,
42
+ '-w',
43
+ ], { encoding: 'utf8' }).trim();
41
44
  }
42
45
 
43
46
  function _deleteSecret_darwin(key) {
44
- execSync(
45
- `security delete-generic-password -a "${SERVICE_NAME}" -s "${key}"`,
46
- { stdio: 'ignore' }
47
- );
47
+ execFileSync('security', [
48
+ 'delete-generic-password',
49
+ '-a', SERVICE_NAME,
50
+ '-s', key,
51
+ ], { stdio: 'ignore' });
48
52
  }
49
53
 
50
54
  function _saveSecret_win32(key, value) {
51
- // Use PowerShell to store in Windows Credential Manager
52
- const cmd = `powershell -Command "cmdkey /generic:${SERVICE_NAME}_${key} /user:${key} /pass:${value}"`;
53
- execSync(cmd, { stdio: 'ignore' });
55
+ // Pass the secret value via an environment variable to avoid it
56
+ // appearing in the command string or the process table.
57
+ const scriptBlock = `
58
+ $target = "${SERVICE_NAME}_${key.replace(/[^A-Za-z0-9_-]/g, '_')}"
59
+ $user = "${key.replace(/[^A-Za-z0-9_-]/g, '_')}"
60
+ $pass = $env:FM_VAULT_VALUE
61
+ cmdkey /generic:$target /user:$user /pass:$pass | Out-Null
62
+ `.trim();
63
+ execFileSync('powershell', [
64
+ '-NoProfile', '-NonInteractive', '-Command', scriptBlock,
65
+ ], {
66
+ stdio: 'ignore',
67
+ env: { ...process.env, FM_VAULT_VALUE: value },
68
+ });
54
69
  }
55
70
 
56
71
  function _getSecret_win32(key) {
57
- // Use PowerShell to retrieve from Windows Credential Manager
58
- const cmd = `powershell -Command "(cmdkey /list:${SERVICE_NAME}_${key}) -match 'User' | Out-Null; $cred = Get-StoredCredential -Target ${SERVICE_NAME}_${key}; if($cred){$cred.GetNetworkCredential().Password}else{throw 'Not found'}"`;
72
+ const safeKey = key.replace(/[^A-Za-z0-9_-]/g, '_');
73
+ const target = `${SERVICE_NAME}_${safeKey}`;
74
+ const scriptBlock =
75
+ `$c = Get-StoredCredential -Target '${target}'; ` +
76
+ `if ($c) { $c.GetNetworkCredential().Password } else { exit 1 }`;
59
77
  try {
60
- return execSync(cmd, { encoding: 'utf8' }).trim();
78
+ return execFileSync('powershell', [
79
+ '-NoProfile', '-NonInteractive', '-Command', scriptBlock,
80
+ ], { encoding: 'utf8' }).trim();
61
81
  } catch {
62
- // Fallback: try simpler cmdkey parsing
63
82
  return null;
64
83
  }
65
84
  }
66
85
 
67
86
  function _saveSecret_linux(key, value) {
68
- execSync(
69
- `echo "${value}" | secret-tool store --label="${SERVICE_NAME} ${key}" service "${SERVICE_NAME}" key "${key}"`,
70
- { stdio: 'ignore' }
71
- );
87
+ // Pass the secret via stdin so it never appears in the process table.
88
+ execFileSync('secret-tool', [
89
+ 'store',
90
+ '--label', `${SERVICE_NAME} ${key}`,
91
+ 'service', SERVICE_NAME,
92
+ 'key', key,
93
+ ], { input: value + '\n', stdio: ['pipe', 'ignore', 'ignore'] });
72
94
  }
73
95
 
74
96
  function _getSecret_linux(key) {
75
- return execSync(
76
- `secret-tool lookup service "${SERVICE_NAME}" key "${key}"`,
77
- { encoding: 'utf8' }
78
- ).trim();
97
+ return execFileSync('secret-tool', [
98
+ 'lookup',
99
+ 'service', SERVICE_NAME,
100
+ 'key', key,
101
+ ], { encoding: 'utf8' }).trim();
79
102
  }
80
103
 
81
104
  // ─── The Vault ────────────────────────────────────────────────────
82
105
 
83
106
  const Vault = {
84
107
  /**
85
- * Save a secret to the system keychain
86
- * @param {string} key - Secret name (e.g., 'GEMINI_API_KEY')
87
- * @param {string} value - Secret value
88
- * @returns {boolean} Success
108
+ * Save a secret to the system keychain.
109
+ * @param {string} key Secret name (e.g. 'GEMINI_API_KEY')
110
+ * @param {string} value Secret value
111
+ * @returns {boolean} true on success
89
112
  */
90
113
  saveSecret(key, value) {
91
114
  try {
92
- if (process.platform === 'darwin') _saveSecret_darwin(key, value);
93
- else if (process.platform === 'win32') _saveSecret_win32(key, value);
94
- else _saveSecret_linux(key, value);
115
+ if (process.platform === 'darwin') _saveSecret_darwin(key, value);
116
+ else if (process.platform === 'win32') _saveSecret_win32(key, value);
117
+ else _saveSecret_linux(key, value);
95
118
  return true;
96
119
  } catch (err) {
97
120
  console.warn(`[Vault] Could not save to system keychain: ${err.message}`);
@@ -100,14 +123,17 @@ const Vault = {
100
123
  },
101
124
 
102
125
  /**
103
- * Retrieve a secret — checks System Keychain → ENV.env file
104
- * @param {string} key - Secret name
105
- * @returns {string|null} The secret value, or null
126
+ * Retrieve a secret — checks System Keychain → ENV.
127
+ * The .env plaintext fallback has been removed. Set secrets via
128
+ * `filemayor config set KEY value` (stores in keychain) or
129
+ * export them as shell environment variables before starting.
130
+ * @param {string} key Secret name
131
+ * @returns {string|null}
106
132
  */
107
133
  getSecret(key) {
108
134
  // Priority 1: System Keychain (most secure)
109
135
  try {
110
- if (process.platform === 'darwin') return _getSecret_darwin(key);
136
+ if (process.platform === 'darwin') return _getSecret_darwin(key);
111
137
  else if (process.platform === 'win32') {
112
138
  const val = _getSecret_win32(key);
113
139
  if (val) return val;
@@ -118,34 +144,22 @@ const Vault = {
118
144
  // Priority 2: Environment Variable
119
145
  if (process.env[key]) return process.env[key];
120
146
 
121
- // Priority 3: .env file (least secure, but common)
122
- try {
123
- const envPath = path.join(process.cwd(), '.env');
124
- if (fs.existsSync(envPath)) {
125
- const envFile = fs.readFileSync(envPath, 'utf-8');
126
- for (const line of envFile.split('\n')) {
127
- const trimmed = line.trim();
128
- if (trimmed.startsWith('#') || !trimmed) continue;
129
- const [k, ...vals] = trimmed.split('=');
130
- if (k.trim() === key && vals.length) {
131
- return vals.join('=').trim();
132
- }
133
- }
134
- }
135
- } catch { /* .env not available */ }
136
-
137
147
  return null;
138
148
  },
139
149
 
140
150
  /**
141
- * Delete a secret from the system keychain
142
- * @param {string} key - Secret name
143
- * @returns {boolean} Success
151
+ * Delete a secret from the system keychain.
152
+ * @param {string} key Secret name
153
+ * @returns {boolean} true on success
144
154
  */
145
155
  deleteSecret(key) {
146
156
  try {
147
- if (process.platform === 'darwin') _deleteSecret_darwin(key);
148
- // Windows and Linux deletion handled differently
157
+ if (process.platform === 'darwin') _deleteSecret_darwin(key);
158
+ else if (process.platform === 'linux') {
159
+ execFileSync('secret-tool', [
160
+ 'clear', 'service', SERVICE_NAME, 'key', key,
161
+ ], { stdio: 'ignore' });
162
+ }
149
163
  return true;
150
164
  } catch {
151
165
  return false;
@@ -153,13 +167,13 @@ const Vault = {
153
167
  },
154
168
 
155
169
  /**
156
- * Check if a secret exists anywhere in the priority chain
157
- * @param {string} key
170
+ * Check if a secret exists anywhere in the priority chain.
171
+ * @param {string} key
158
172
  * @returns {boolean}
159
173
  */
160
174
  hasSecret(key) {
161
175
  return Vault.getSecret(key) !== null;
162
- }
176
+ },
163
177
  };
164
178
 
165
179
  module.exports = Vault;